Merge pull request #1604 from gabrielburnworth/staging

Add saved gardens panel
pull/1609/head v8.2.3
Rick Carlino 2019-12-02 14:41:29 -06:00 committed by GitHub
commit e8c6257a7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 81 additions and 27 deletions

View File

@ -19,7 +19,7 @@ module Api
end end
def snapshot def snapshot
mutate SavedGardens::Snapshot.run(device: current_device) mutate SavedGardens::Snapshot.run(raw_json, device: current_device)
end end
def apply def apply

View File

@ -17,6 +17,7 @@ import { shallow } from "enzyme";
describe("<DesignerNavTabs />", () => { describe("<DesignerNavTabs />", () => {
it("renders for map", () => { it("renders for map", () => {
mockPath = "/app/designer"; mockPath = "/app/designer";
mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("gray-panel")).toBeTruthy(); expect(wrapper.hasClass("gray-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -24,6 +25,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for plants", () => { it("renders for plants", () => {
mockPath = "/app/designer/plants"; mockPath = "/app/designer/plants";
mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("green-panel")).toBeTruthy(); expect(wrapper.hasClass("green-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -31,6 +33,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for farm events", () => { it("renders for farm events", () => {
mockPath = "/app/designer/events"; mockPath = "/app/designer/events";
mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("yellow-panel")).toBeTruthy(); expect(wrapper.hasClass("yellow-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -38,7 +41,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for saved gardens", () => { it("renders for saved gardens", () => {
mockPath = "/app/designer/gardens"; mockPath = "/app/designer/gardens";
mockDev = true; mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("navy-panel")).toBeTruthy(); expect(wrapper.hasClass("navy-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -46,7 +49,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for points", () => { it("renders for points", () => {
mockPath = "/app/designer/points"; mockPath = "/app/designer/points";
mockDev = true; mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("teal-panel")).toBeTruthy(); expect(wrapper.hasClass("teal-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -54,7 +57,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for groups", () => { it("renders for groups", () => {
mockPath = "/app/designer/groups"; mockPath = "/app/designer/groups";
mockDev = true; mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("blue-panel")).toBeTruthy(); expect(wrapper.hasClass("blue-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -62,7 +65,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for weeds", () => { it("renders for weeds", () => {
mockPath = "/app/designer/weeds"; mockPath = "/app/designer/weeds";
mockDev = true; mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("red-panel")).toBeTruthy(); expect(wrapper.hasClass("red-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");
@ -78,7 +81,7 @@ describe("<DesignerNavTabs />", () => {
it("renders for settings", () => { it("renders for settings", () => {
mockPath = "/app/designer/settings"; mockPath = "/app/designer/settings";
mockDev = true; mockDev = false;
const wrapper = shallow(<DesignerNavTabs />); const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("gray-panel")).toBeTruthy(); expect(wrapper.hasClass("gray-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active"); expect(wrapper.html()).toContain("active");

View File

@ -118,11 +118,10 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
panel={Panel.Groups} panel={Panel.Groups}
linkTo={"/app/designer/groups"} linkTo={"/app/designer/groups"}
title={t("Groups")} /> title={t("Groups")} />
{DevSettings.futureFeaturesEnabled() && <NavTab
<NavTab panel={Panel.SavedGardens}
panel={Panel.SavedGardens} linkTo={"/app/designer/gardens"}
linkTo={"/app/designer/gardens"} title={t("Gardens")} />
title={t("Gardens")} />}
<NavTab <NavTab
panel={Panel.FarmEvents} panel={Panel.FarmEvents}
linkTo={"/app/designer/events"} linkTo={"/app/designer/events"}

View File

@ -3,6 +3,8 @@ import { mount } from "enzyme";
import { RawAddGarden as AddGarden, mapStateToProps } from "../garden_add"; import { RawAddGarden as AddGarden, mapStateToProps } from "../garden_add";
import { GardenSnapshotProps } from "../garden_snapshot"; import { GardenSnapshotProps } from "../garden_snapshot";
import { fakeState } from "../../../__test_support__/fake_state"; import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { fakeSavedGarden } from "../../../__test_support__/fake_state/resources";
describe("<AddGarden />", () => { describe("<AddGarden />", () => {
const fakeProps = (): GardenSnapshotProps => ({ const fakeProps = (): GardenSnapshotProps => ({
@ -22,4 +24,13 @@ describe("mapStateToProps()", () => {
const props = mapStateToProps(fakeState()); const props = mapStateToProps(fakeState());
expect(props.currentSavedGarden).toEqual(undefined); expect(props.currentSavedGarden).toEqual(undefined);
}); });
it("finds saved garden", () => {
const state = fakeState();
const savedGarden = fakeSavedGarden();
state.resources = buildResourceIndex([savedGarden]);
state.resources.consumers.farm_designer.openedSavedGarden = savedGarden.uuid;
const props = mapStateToProps(state);
expect(props.currentSavedGarden).toEqual(savedGarden);
});
}); });

View File

@ -86,6 +86,15 @@ describe("<EditGarden />", () => {
const wrapper = mount(<EditGarden {...p} />); const wrapper = mount(<EditGarden {...p} />);
expect(wrapper.text()).toContain("exit"); expect(wrapper.text()).toContain("exit");
}); });
it("renders with missing data", () => {
const p = fakeProps();
p.savedGarden = fakeSavedGarden();
p.savedGarden.body.id = undefined;
p.savedGarden.body.name = undefined;
const wrapper = mount(<EditGarden {...p} />);
expect(wrapper.text().toLowerCase()).toContain("edit garden");
});
}); });
describe("mapStateToProps()", () => { describe("mapStateToProps()", () => {
@ -100,4 +109,16 @@ describe("mapStateToProps()", () => {
expect(props.gardenIsOpen).toEqual(true); expect(props.gardenIsOpen).toEqual(true);
expect(props.savedGarden).toEqual(sg); expect(props.savedGarden).toEqual(sg);
}); });
it("doesn't find saved garden", () => {
const sg = fakeSavedGarden();
sg.body.id = 1;
mockPath = "/app/designer/gardens/";
const state = fakeState();
state.resources = buildResourceIndex([sg]);
state.resources.consumers.farm_designer.openedSavedGarden = sg.uuid;
const props = mapStateToProps(state);
expect(props.gardenIsOpen).toEqual(false);
expect(props.savedGarden).toEqual(undefined);
});
}); });

View File

@ -95,7 +95,11 @@ export const copySavedGarden = ({ newSGName, savedGarden, plantTemplates }: {
const sourceSavedGardenId = savedGarden.body.id; const sourceSavedGardenId = savedGarden.body.id;
const name = newSGName || `${savedGarden.body.name} (${t("copy")})`; const name = newSGName || `${savedGarden.body.name} (${t("copy")})`;
dispatch(initSaveGetId(savedGarden.kind, { name })) dispatch(initSaveGetId(savedGarden.kind, { name }))
.then((newSGId: number) => plantTemplates .then((newSGId: number) => {
.filter(x => x.body.saved_garden_id === sourceSavedGardenId) plantTemplates
.map(x => dispatch(initSave(x.kind, newPTBody(x, newSGId))))); .filter(x => x.body.saved_garden_id === sourceSavedGardenId)
.map(x => dispatch(initSave(x.kind, newPTBody(x, newSGId))));
success(t("Garden Saved."));
history.push("/app/designer/gardens");
});
}; };

View File

@ -2,8 +2,9 @@ import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { GardenSnapshotProps, GardenSnapshot } from "./garden_snapshot"; import { GardenSnapshotProps, GardenSnapshot } from "./garden_snapshot";
import { findSavedGarden } from "./garden_edit"; import {
import { selectAllPlantTemplates } from "../../resources/selectors"; selectAllPlantTemplates, findSavedGarden
} from "../../resources/selectors";
import { import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "../plants/designer_panel"; } from "../plants/designer_panel";
@ -12,11 +13,15 @@ import { Row } from "../../ui";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header"; import { Panel } from "../panel_header";
export const mapStateToProps = (props: Everything): GardenSnapshotProps => ({ export const mapStateToProps = (props: Everything): GardenSnapshotProps => {
currentSavedGarden: findSavedGarden(props.resources.index), const { openedSavedGarden } = props.resources.consumers.farm_designer;
dispatch: props.dispatch, return {
plantTemplates: selectAllPlantTemplates(props.resources.index), currentSavedGarden: openedSavedGarden
}); ? findSavedGarden(props.resources.index, openedSavedGarden) : undefined,
dispatch: props.dispatch,
plantTemplates: selectAllPlantTemplates(props.resources.index),
};
};
export class RawAddGarden extends React.Component<GardenSnapshotProps, {}> { export class RawAddGarden extends React.Component<GardenSnapshotProps, {}> {
render() { render() {

View File

@ -7,7 +7,7 @@ import { BlurableInput, Row } from "../../ui";
import { edit, save } from "../../api/crud"; import { edit, save } from "../../api/crud";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
selectAllSavedGardens, selectAllPlantPointers, findResourceById selectAllPlantPointers, maybeFindSavedGardenById
} from "../../resources/selectors"; } from "../../resources/selectors";
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { import {
@ -53,18 +53,17 @@ const DestroyGardenButton =
{t("delete")} {t("delete")}
</button>; </button>;
export const findSavedGarden = (ri: ResourceIndex) => { export const findSavedGardenByUrl = (ri: ResourceIndex) => {
const id = getPathArray()[4]; const id = getPathArray()[4];
const num = parseInt(id || "NOPE", 10); const num = parseInt(id || "NOPE", 10);
if (isNumber(num) && !isNaN(num)) { if (isNumber(num) && !isNaN(num)) {
const uuid = findResourceById(ri, "SavedGarden", num); return maybeFindSavedGardenById(ri, num);
return selectAllSavedGardens(ri).filter(x => x.uuid === uuid)[0];
} }
}; };
export const mapStateToProps = (props: Everything): EditGardenProps => { export const mapStateToProps = (props: Everything): EditGardenProps => {
const { openedSavedGarden } = props.resources.consumers.farm_designer; const { openedSavedGarden } = props.resources.consumers.farm_designer;
const savedGarden = findSavedGarden(props.resources.index); const savedGarden = findSavedGardenByUrl(props.resources.index);
return { return {
savedGarden, savedGarden,
gardenIsOpen: !!(savedGarden && savedGarden.uuid === openedSavedGarden), gardenIsOpen: !!(savedGarden && savedGarden.uuid === openedSavedGarden),

View File

@ -9,6 +9,7 @@ import {
sanityCheck, sanityCheck,
isTaggedPlantTemplate, isTaggedPlantTemplate,
isTaggedGenericPointer, isTaggedGenericPointer,
isTaggedSavedGarden,
} from "./tagged_resources"; } from "./tagged_resources";
import { import {
ResourceName, ResourceName,
@ -113,6 +114,13 @@ export function maybeFindPointById(index: ResourceIndex, id: number) {
if (resource && isTaggedGenericPointer(resource)) { return resource; } if (resource && isTaggedGenericPointer(resource)) { return resource; }
} }
/** Unlike other findById methods, this one allows undefined (missed) values */
export function maybeFindSavedGardenById(index: ResourceIndex, id: number) {
const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)];
const resource = index.references[uuid || "nope"];
if (resource && isTaggedSavedGarden(resource)) { return resource; }
}
export let findRegimenById = (ri: ResourceIndex, regimen_id: number) => { export let findRegimenById = (ri: ResourceIndex, regimen_id: number) => {
const regimen = byId("Regimen")(ri, regimen_id); const regimen = byId("Regimen")(ri, regimen_id);
if (regimen && isTaggedRegimen(regimen) && sanityCheck(regimen)) { if (regimen && isTaggedRegimen(regimen) && sanityCheck(regimen)) {

View File

@ -59,6 +59,7 @@ export let findRegimen = uuidFinder<TaggedRegimen>("Regimen");
export let findFarmEvent = uuidFinder<TaggedFarmEvent>("FarmEvent"); export let findFarmEvent = uuidFinder<TaggedFarmEvent>("FarmEvent");
export let findPoints = uuidFinder<TaggedPoint>("Point"); export let findPoints = uuidFinder<TaggedPoint>("Point");
export let findPointGroup = uuidFinder<TaggedPoint>("Point"); export let findPointGroup = uuidFinder<TaggedPoint>("Point");
export let findSavedGarden = uuidFinder<TaggedSavedGarden>("SavedGarden");
export const selectAllCrops = export const selectAllCrops =
(i: ResourceIndex) => findAll<TaggedCrop>(i, "Crop"); (i: ResourceIndex) => findAll<TaggedCrop>(i, "Crop");

View File

@ -14,6 +14,7 @@ import {
PointerType, PointerType,
SpecialStatus, SpecialStatus,
TaggedPlantTemplate, TaggedPlantTemplate,
TaggedSavedGarden,
} from "farmbot"; } from "farmbot";
export interface TaggedResourceBase { export interface TaggedResourceBase {
@ -98,5 +99,7 @@ export let isTaggedGenericPointer =
(x: object): x is TaggedGenericPointer => { (x: object): x is TaggedGenericPointer => {
return isTaggedPoint(x) && (x.body.pointer_type === "GenericPointer"); return isTaggedPoint(x) && (x.body.pointer_type === "GenericPointer");
}; };
export let isTaggedSavedGarden =
(x: object): x is TaggedSavedGarden => is("SavedGarden")(x);
export let isTaggedPlantTemplate = export let isTaggedPlantTemplate =
(x: object): x is TaggedPlantTemplate => is("PlantTemplate")(x); (x: object): x is TaggedPlantTemplate => is("PlantTemplate")(x);

View File

@ -62,7 +62,7 @@ describe Api::SavedGardensController do
gardens_b4 = user.device.saved_gardens.count gardens_b4 = user.device.saved_gardens.count
templates_b4 = user.device.plant_templates.count templates_b4 = user.device.plant_templates.count
plants = FactoryBot.create_list(:plant, 3, device: user.device) plants = FactoryBot.create_list(:plant, 3, device: user.device)
post :snapshot post :snapshot, body: {}.to_json
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(user.device.plant_templates.count).to eq(plants.length) expect(user.device.plant_templates.count).to eq(plants.length)
expect(user.device.saved_gardens.count).to be > gardens_b4 expect(user.device.saved_gardens.count).to be > gardens_b4