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
def snapshot
mutate SavedGardens::Snapshot.run(device: current_device)
mutate SavedGardens::Snapshot.run(raw_json, device: current_device)
end
def apply

View File

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

View File

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

View File

@ -3,6 +3,8 @@ import { mount } from "enzyme";
import { RawAddGarden as AddGarden, mapStateToProps } from "../garden_add";
import { GardenSnapshotProps } from "../garden_snapshot";
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 />", () => {
const fakeProps = (): GardenSnapshotProps => ({
@ -22,4 +24,13 @@ describe("mapStateToProps()", () => {
const props = mapStateToProps(fakeState());
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} />);
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()", () => {
@ -100,4 +109,16 @@ describe("mapStateToProps()", () => {
expect(props.gardenIsOpen).toEqual(true);
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 name = newSGName || `${savedGarden.body.name} (${t("copy")})`;
dispatch(initSaveGetId(savedGarden.kind, { name }))
.then((newSGId: number) => plantTemplates
.filter(x => x.body.saved_garden_id === sourceSavedGardenId)
.map(x => dispatch(initSave(x.kind, newPTBody(x, newSGId)))));
.then((newSGId: number) => {
plantTemplates
.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 { Everything } from "../../interfaces";
import { GardenSnapshotProps, GardenSnapshot } from "./garden_snapshot";
import { findSavedGarden } from "./garden_edit";
import { selectAllPlantTemplates } from "../../resources/selectors";
import {
selectAllPlantTemplates, findSavedGarden
} from "../../resources/selectors";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "../plants/designer_panel";
@ -12,11 +13,15 @@ import { Row } from "../../ui";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
export const mapStateToProps = (props: Everything): GardenSnapshotProps => ({
currentSavedGarden: findSavedGarden(props.resources.index),
dispatch: props.dispatch,
plantTemplates: selectAllPlantTemplates(props.resources.index),
});
export const mapStateToProps = (props: Everything): GardenSnapshotProps => {
const { openedSavedGarden } = props.resources.consumers.farm_designer;
return {
currentSavedGarden: openedSavedGarden
? findSavedGarden(props.resources.index, openedSavedGarden) : undefined,
dispatch: props.dispatch,
plantTemplates: selectAllPlantTemplates(props.resources.index),
};
};
export class RawAddGarden extends React.Component<GardenSnapshotProps, {}> {
render() {

View File

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

View File

@ -9,6 +9,7 @@ import {
sanityCheck,
isTaggedPlantTemplate,
isTaggedGenericPointer,
isTaggedSavedGarden,
} from "./tagged_resources";
import {
ResourceName,
@ -113,6 +114,13 @@ export function maybeFindPointById(index: ResourceIndex, id: number) {
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) => {
const regimen = byId("Regimen")(ri, regimen_id);
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 findPoints = uuidFinder<TaggedPoint>("Point");
export let findPointGroup = uuidFinder<TaggedPoint>("Point");
export let findSavedGarden = uuidFinder<TaggedSavedGarden>("SavedGarden");
export const selectAllCrops =
(i: ResourceIndex) => findAll<TaggedCrop>(i, "Crop");

View File

@ -14,6 +14,7 @@ import {
PointerType,
SpecialStatus,
TaggedPlantTemplate,
TaggedSavedGarden,
} from "farmbot";
export interface TaggedResourceBase {
@ -98,5 +99,7 @@ export let isTaggedGenericPointer =
(x: object): x is TaggedGenericPointer => {
return isTaggedPoint(x) && (x.body.pointer_type === "GenericPointer");
};
export let isTaggedSavedGarden =
(x: object): x is TaggedSavedGarden => is("SavedGarden")(x);
export let isTaggedPlantTemplate =
(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
templates_b4 = user.device.plant_templates.count
plants = FactoryBot.create_list(:plant, 3, device: user.device)
post :snapshot
post :snapshot, body: {}.to_json
expect(response.status).to eq(200)
expect(user.device.plant_templates.count).to eq(plants.length)
expect(user.device.saved_gardens.count).to be > gardens_b4