Merge pull request #1604 from gabrielburnworth/staging
Add saved gardens panelpull/1609/head v8.2.3
commit
e8c6257a7f
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue