update saved gardens panel

pull/1561/head
gabrielburnworth 2019-11-06 09:01:05 -08:00
parent 212fa65a4e
commit 8215abc978
26 changed files with 499 additions and 261 deletions

View File

@ -743,15 +743,18 @@ export namespace Content {
Press the back arrow to exit.`);
export const SAVED_GARDENS =
trim(`Create new gardens from scratch or by copying plants from the
current garden. View and edit saved gardens, and, when ready, apply them
to the main garden.`);
trim(`Create a new garden from scratch or by copying plants from the
current garden.`);
export const ERROR_PLANT_TEMPLATE_GROUP =
trim(`Cannot create a group with these plants.
Try leaving the saved garden first.`);
export const NO_PLANTS =
trim(`Press "+" to add a plant to your garden.`);
export const NO_GARDENS =
trim(`Press "CREATE NEW GARDEN" to add a garden.`);
trim(`Press "+" to add a garden.`);
export const NO_POINTS =
trim(`Press "+" to add a point to your garden.`);

View File

@ -406,6 +406,8 @@
}
.garden-snapshot {
margin-left: 2rem;
margin-right: 2rem;
button {
&.pseudo-disabled {
cursor: not-allowed;
@ -414,22 +416,26 @@
}
.saved-garden-list {
margin: -15px;
.saved-garden-row {
padding: 0.25rem;
button {
margin-bottom: 1rem;
}
.saved-garden-info div {
height: 3rem;
line-height: 3rem;
cursor: pointer;
padding-right: 0;
input {
background: none;
box-shadow: none;
padding: 0;
label {
margin: 0;
pointer-events: none;
margin-left: 1rem;
}
p {
margin-top: 0.25rem;
float: right;
line-height: 3rem;
text-align: center;
margin-right: 1rem;
}
}
&:hover {

View File

@ -472,9 +472,7 @@
}
.move-to-panel-content {
&.with-nav {
margin-top: 6rem;
}
margin-top: 6rem;
margin-left: 1rem;
margin-right: 1rem;
button {
@ -526,11 +524,7 @@
}
.saved-garden-panel-content {
&.with-nav {
margin-top: 6rem;
}
margin-left: 1rem;
margin-right: 1rem;
padding: 0;
.row {
margin-top: 1rem;
}
@ -571,3 +565,17 @@
padding: 0;
}
}
.saved-garden-edit-panel-content {
margin-left: 3rem;
margin-right: 3rem;
button {
margin-left: 0.5rem;
margin-top: 1rem;
}
p {
font-size: 1.2rem;
text-align: center;
padding: 3rem;
}
}

View File

@ -37,7 +37,7 @@ describe("<DesignerNavTabs />", () => {
});
it("renders for saved gardens", () => {
mockPath = "/app/designer/saved_gardens";
mockPath = "/app/designer/gardens";
mockDev = true;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("green-panel")).toBeTruthy();

View File

@ -326,11 +326,11 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.clickToAdd);
mockPath = "/app/designer/plants/1/edit";
expect(getMode()).toEqual(Mode.editPlant);
mockPath = "/app/designer/saved_gardens/templates/1/edit";
mockPath = "/app/designer/gardens/templates/1/edit";
expect(getMode()).toEqual(Mode.editPlant);
mockPath = "/app/designer/plants/1";
expect(getMode()).toEqual(Mode.editPlant);
mockPath = "/app/designer/saved_gardens/templates/1";
mockPath = "/app/designer/gardens/templates/1";
expect(getMode()).toEqual(Mode.editPlant);
mockPath = "/app/designer/plants/select";
expect(getMode()).toEqual(Mode.boxSelect);
@ -342,7 +342,7 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.points);
mockPath = "/app/designer/points/add";
expect(getMode()).toEqual(Mode.createPoint);
mockPath = "/app/designer/saved_gardens";
mockPath = "/app/designer/gardens";
mockGardenOpen = true;
expect(getMode()).toEqual(Mode.templateView);
mockPath = "/app/designer/groups/1";

View File

@ -84,7 +84,7 @@ describe("<PlantLayer/>", () => {
p.plants[0].body.id = 5;
const wrapper = svgMount(<PlantLayer {...p} />);
expect(wrapper.find("Link").props().to)
.toEqual("/app/designer/saved_gardens/templates/5");
.toEqual("/app/designer/gardens/templates/5");
});
it("has selected plant", () => {

View File

@ -25,7 +25,7 @@ export function PlantLayer(props: PlantLayerProps) {
const selected = !!(currentPlant && (p.uuid === currentPlant.uuid));
const multiselected = !!(selectedForDel && (selectedForDel.includes(p.uuid)));
const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate"
? "saved_gardens/templates"
? "gardens/templates"
: "plants";
const plant = <GardenPlant
uuid={p.uuid}

View File

@ -66,7 +66,7 @@ const getCurrentTab = (): Tabs => {
return Panel.Map;
} else if (pathArray.includes("events")) {
return Panel.FarmEvents;
} else if (pathArray.includes("saved_gardens")) {
} else if (pathArray.includes("gardens")) {
return Panel.SavedGardens;
} else if (pathArray.includes("tools")) {
return Panel.Tools;
@ -120,7 +120,7 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
{DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.SavedGardens}
linkTo={"/app/designer/saved_gardens"}
linkTo={"/app/designer/gardens"}
title={t("Gardens")} />}
<NavTab
panel={Panel.FarmEvents}

View File

@ -63,7 +63,7 @@ describe("<PlantInfo />", () => {
});
it("gets template id", () => {
mockPath = "/app/designer/saved_gardens/templates/2";
mockPath = "/app/designer/gardens/templates/2";
const p = fakeProps();
p.openedSavedGarden = "uuid";
const wrapper = mount<PlantInfo>(<PlantInfo {...p} />);

View File

@ -85,7 +85,7 @@ describe("<PlantInventoryItem />", () => {
type: Actions.SELECT_PLANT
});
expect(push).toHaveBeenCalledWith(
"/app/designer/saved_gardens/templates/" + p.tpp.body.id);
"/app/designer/gardens/templates/" + p.tpp.body.id);
});
it("gets cached icon", async () => {

View File

@ -19,11 +19,12 @@ import {
RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps
} from "../select_plants";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { Actions } from "../../../constants";
import { Actions, Content } from "../../../constants";
import { clickButton } from "../../../__test_support__/helpers";
import { destroy } from "../../../api/crud";
import { createGroup } from "../../point_groups/actions";
import { fakeState } from "../../../__test_support__/fake_state";
import { error } from "../../../toast/toast";
describe("<SelectPlants />", () => {
beforeEach(function () {
@ -41,6 +42,7 @@ describe("<SelectPlants />", () => {
selected: ["plant.1"],
plants: [plant1, plant2],
dispatch: jest.fn(),
gardenOpen: undefined,
};
}
@ -147,6 +149,15 @@ describe("<SelectPlants />", () => {
wrapper.find(".dark-blue").simulate("click");
expect(createGroup).toHaveBeenCalled();
});
it("doesn't create group", () => {
const p = fakeProps();
p.gardenOpen = "uuid";
const wrapper = mount(<SelectPlants {...p} />);
wrapper.find(".dark-blue").simulate("click");
expect(createGroup).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(Content.ERROR_PLANT_TEMPLATE_GROUP);
});
});
describe("mapStateToProps", () => {

View File

@ -42,7 +42,7 @@ export class PlantInventoryItem extends
const click = () => {
const plantCategory =
unpackUUID(this.props.tpp.uuid).kind === "PlantTemplate"
? "saved_gardens/templates"
? "gardens/templates"
: "plants";
push(`/app/designer/${plantCategory}/${plantId}`);
dispatch(selectPlant([tpp.uuid]));

View File

@ -14,19 +14,20 @@ import {
import { t } from "../../i18next_wrapper";
import { createGroup } from "../point_groups/actions";
import { PanelColor } from "../panel_header";
import { error } from "../../toast/toast";
export function mapStateToProps(props: Everything) {
return {
selected: props.resources.consumers.farm_designer.selectedPlants,
plants: getPlants(props.resources),
dispatch: props.dispatch,
};
}
export const mapStateToProps = (props: Everything): SelectPlantsProps => ({
selected: props.resources.consumers.farm_designer.selectedPlants,
plants: getPlants(props.resources),
dispatch: props.dispatch,
gardenOpen: props.resources.consumers.farm_designer.openedSavedGarden,
});
export interface SelectPlantsProps {
plants: TaggedPlant[];
dispatch: Function;
selected: string[] | undefined;
gardenOpen: string | undefined;
}
export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
@ -74,9 +75,9 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
{t("Delete")}
</button>
<button className="fb-button dark-blue"
onClick={() => this.props.dispatch(createGroup({
points: this.selected
}))}>
onClick={() => !this.props.gardenOpen
? this.props.dispatch(createGroup({ points: this.selected }))
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>
{t("Create group")}
</button>
</div>

View File

@ -56,8 +56,9 @@ describe("destroySavedGarden", () => {
it("deletes garden", () => {
const dispatch = jest.fn(() => Promise.resolve());
destroySavedGarden("SavedGardenUuid")(dispatch);
expect(dispatch).toHaveBeenCalledWith(unselectSavedGarden);
expect(history.push).toHaveBeenCalledWith("/app/designer/gardens");
expect(destroy).toHaveBeenCalledWith("SavedGardenUuid");
expect(dispatch).toHaveBeenLastCalledWith(unselectSavedGarden);
});
});
@ -65,7 +66,7 @@ describe("closeSavedGarden", () => {
it("closes garden", () => {
const dispatch = jest.fn();
closeSavedGarden()(dispatch);
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens");
expect(history.push).toHaveBeenCalledWith("/app/designer/gardens");
expect(dispatch).toHaveBeenCalledWith(unselectSavedGarden);
});
});
@ -75,7 +76,7 @@ describe("openSavedGarden", () => {
const dispatch = jest.fn();
const uuid = "SavedGardenUuid.1.0";
openSavedGarden(uuid)(dispatch);
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens/1");
expect(history.push).toHaveBeenCalledWith("/app/designer/gardens/1");
expect(dispatch).toHaveBeenCalledWith({
type: Actions.CHOOSE_SAVED_GARDEN,
payload: uuid
@ -91,7 +92,7 @@ describe("openOrCloseGarden", () => {
gardenIsOpen: false,
};
openOrCloseGarden(props)();
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens/1");
expect(history.push).toHaveBeenCalledWith("/app/designer/gardens/1");
});
it("closes garden", () => {
@ -101,19 +102,19 @@ describe("openOrCloseGarden", () => {
gardenIsOpen: true,
};
openOrCloseGarden(props)();
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens");
expect(history.push).toHaveBeenCalledWith("/app/designer/gardens");
});
});
describe("newSavedGarden", () => {
it("creates a new saved garden", () => {
newSavedGarden("my saved garden")(jest.fn());
newSavedGarden("my saved garden")(jest.fn(() => Promise.resolve()));
expect(initSave).toHaveBeenCalledWith(
"SavedGarden", { name: "my saved garden" });
});
it("creates a new saved garden with default name", () => {
newSavedGarden("")(jest.fn());
newSavedGarden("")(jest.fn(() => Promise.resolve()));
expect(initSave).toHaveBeenCalledWith(
"SavedGarden", { name: "Untitled Garden" });
});

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { mount } from "enzyme";
import { RawAddGarden as AddGarden, mapStateToProps } from "../garden_add";
import { GardenSnapshotProps } from "../garden_snapshot";
import { fakeState } from "../../../__test_support__/fake_state";
describe("<AddGarden />", () => {
const fakeProps = (): GardenSnapshotProps => ({
currentSavedGarden: undefined,
plantTemplates: [],
dispatch: jest.fn(),
});
it("renders add garden panel", () => {
const wrapper = mount(<AddGarden {...fakeProps()} />);
expect(wrapper.text()).toContain("create new garden");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const props = mapStateToProps(fakeState());
expect(props.currentSavedGarden).toEqual(undefined);
});
});

View File

@ -0,0 +1,103 @@
jest.mock("../actions", () => ({
applyGarden: jest.fn(),
destroySavedGarden: jest.fn(),
openOrCloseGarden: jest.fn(),
closeSavedGarden: jest.fn(),
}));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
let mockPath = "";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { RawEditGarden as EditGarden, mapStateToProps } from "../garden_edit";
import { EditGardenProps } from "../interfaces";
import { fakeSavedGarden } from "../../../__test_support__/fake_state/resources";
import { clickButton } from "../../../__test_support__/helpers";
import { applyGarden, destroySavedGarden } from "../actions";
import { error } from "../../../toast/toast";
import { edit } from "../../../api/crud";
import { fakeState } from "../../../__test_support__/fake_state";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
describe("<EditGarden />", () => {
const fakeProps = (): EditGardenProps => ({
savedGarden: undefined,
gardenIsOpen: false,
dispatch: jest.fn(),
plantPointerCount: 0,
});
it("edits garden name", () => {
const p = fakeProps();
p.savedGarden = fakeSavedGarden();
const wrapper = shallow(<EditGarden {...p} />);
wrapper.find("BlurableInput").simulate("commit",
{ currentTarget: { value: "new name" } });
expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: "new name" });
});
it("applies garden", () => {
const p = fakeProps();
p.savedGarden = fakeSavedGarden();
p.savedGarden.body.id = 1;
p.plantPointerCount = 0;
const wrapper = mount(<EditGarden {...p} />);
clickButton(wrapper, 0, "apply");
expect(applyGarden).toHaveBeenCalledWith(1);
});
it("plants still in garden", () => {
const p = fakeProps();
p.savedGarden = fakeSavedGarden();
p.plantPointerCount = 1;
const wrapper = mount(<EditGarden {...p} />);
clickButton(wrapper, 0, "apply");
expect(error).toHaveBeenCalledWith(expect.stringContaining(
"Please clear current garden first"));
});
it("destroys garden", () => {
const p = fakeProps();
p.savedGarden = fakeSavedGarden();
const wrapper = mount(<EditGarden {...p} />);
clickButton(wrapper, 1, "delete");
expect(destroySavedGarden).toHaveBeenCalledWith(p.savedGarden.uuid);
});
it("shows garden not found", () => {
const wrapper = mount(<EditGarden {...fakeProps()} />);
expect(wrapper.text()).toContain("not found");
});
it("show when garden is open", () => {
const p = fakeProps();
p.savedGarden = fakeSavedGarden();
p.gardenIsOpen = true;
const wrapper = mount(<EditGarden {...p} />);
expect(wrapper.text()).toContain("exit");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const sg = fakeSavedGarden();
sg.body.id = 1;
mockPath = "/app/designer/gardens/1";
const state = fakeState();
state.resources = buildResourceIndex([sg]);
state.resources.consumers.farm_designer.openedSavedGarden = sg.uuid;
const props = mapStateToProps(state);
expect(props.gardenIsOpen).toEqual(true);
expect(props.savedGarden).toEqual(sg);
});
});

View File

@ -1,58 +1,23 @@
jest.mock("../actions", () => ({
openOrCloseGarden: jest.fn(),
}));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
jest.mock("../actions", () => ({ openSavedGarden: jest.fn() }));
import * as React from "react";
import { shallow, mount } from "enzyme";
import {
fakeSavedGarden, fakePlantTemplate
} from "../../../__test_support__/fake_state/resources";
import { edit } from "../../../api/crud";
import { GardenInfo, SavedGardenList } from "../garden_list";
import { SavedGardenInfoProps, SavedGardensProps } from "../interfaces";
import { shallow } from "enzyme";
import { GardenInfo } from "../garden_list";
import { fakeSavedGarden } from "../../../__test_support__/fake_state/resources";
import { SavedGardenInfoProps } from "../interfaces";
import { openSavedGarden } from "../actions";
describe("<GardenInfo />", () => {
const fakeProps = (): SavedGardenInfoProps => ({
dispatch: jest.fn(),
savedGarden: fakeSavedGarden(),
gardenIsOpen: false,
plantTemplateCount: 0,
dispatch: jest.fn(),
});
it("edits garden name", () => {
const wrapper = shallow(<GardenInfo {...fakeProps()} />);
wrapper.find("BlurableInput").simulate("commit",
{ currentTarget: { value: "new name" } });
expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: "new name" });
});
});
describe("<SavedGardenList />", () => {
const fakeProps = (): SavedGardensProps => {
const fakeSG = fakeSavedGarden();
return {
dispatch: jest.fn(),
plantPointerCount: 1,
savedGardens: [fakeSG],
plantTemplates: [fakePlantTemplate(), fakePlantTemplate()],
openedSavedGarden: fakeSG.uuid,
};
};
it("renders open garden", () => {
const wrapper = mount(<SavedGardenList {...fakeProps()} />);
expect(wrapper.text()).toContain("exit");
});
it("renders gardens closed", () => {
it("opens garden", () => {
const p = fakeProps();
p.openedSavedGarden = undefined;
const wrapper = mount(<SavedGardenList {...p} />);
expect(wrapper.text()).not.toContain("exit");
const wrapper = shallow(<GardenInfo {...p} />);
wrapper.simulate("click");
expect(openSavedGarden).toHaveBeenCalledWith(p.savedGarden.uuid);
});
});

View File

@ -36,9 +36,8 @@ import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { SavedGardensProps } from "../interfaces";
import { applyGarden, destroySavedGarden, closeSavedGarden } from "../actions";
import { closeSavedGarden } from "../actions";
import { Actions } from "../../../constants";
import { error } from "../../../toast/toast";
describe("<SavedGardens />", () => {
const fakeProps = (): SavedGardensProps => ({
@ -50,36 +49,14 @@ describe("<SavedGardens />", () => {
});
it("renders saved gardens", () => {
const wrapper = mount(<SavedGardens {...fakeProps()} />);
["saved garden 1", "2", "apply"].map(string =>
const p = fakeProps();
p.plantTemplates[0].body.saved_garden_id = p.savedGardens[0].body.id || 0;
p.plantTemplates[1].body.saved_garden_id = p.savedGardens[0].body.id || 0;
const wrapper = mount(<SavedGardens {...p} />);
["saved garden 1", "2 plants"].map(string =>
expect(wrapper.html().toLowerCase()).toContain(string));
});
it("applies garden", () => {
const p = fakeProps();
p.savedGardens[0].uuid = "SavedGarden.1.0";
p.savedGardens[0].body.id = 1;
p.plantPointerCount = 0;
const wrapper = mount(<SavedGardens {...p} />);
clickButton(wrapper, 3, "apply");
expect(applyGarden).toHaveBeenCalledWith(1);
});
it("plants still in garden", () => {
const wrapper = mount(<SavedGardens {...fakeProps()} />);
wrapper.find("button").first().simulate("click");
clickButton(wrapper, 3, "apply");
expect(error).toHaveBeenCalledWith(expect.stringContaining(
"Please clear current garden first"));
});
it("destroys garden", () => {
const p = fakeProps();
const wrapper = mount(<SavedGardens {...p} />);
clickButton(wrapper, 2, "");
expect(destroySavedGarden).toHaveBeenCalledWith(p.savedGardens[0].uuid);
});
it("has no saved gardens yet", () => {
const p = fakeProps();
p.savedGardens = [];
@ -87,11 +64,31 @@ describe("<SavedGardens />", () => {
expect(wrapper.text().toLowerCase()).toContain("no saved gardens yet");
});
it("shows alt display", () => {
mockDev = true;
const wrapper = mount(<SavedGardens {...fakeProps()} />);
expect(wrapper.html()).toContain("-nav");
mockDev = false;
it("changes search term", () => {
const wrapper = shallow<SavedGardens>(<SavedGardens {...fakeProps()} />);
expect(wrapper.state().searchTerm).toEqual("");
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "spring" } });
expect(wrapper.state().searchTerm).toEqual("spring");
});
it("shows filtered gardens", () => {
const p = fakeProps();
p.savedGardens = [fakeSavedGarden(), fakeSavedGarden()];
p.savedGardens[0].body.name = "winter";
p.savedGardens[1].body.name = "spring";
const wrapper = mount(<SavedGardens {...p} />);
wrapper.setState({ searchTerm: "winter" });
expect(wrapper.text()).toContain("winter");
expect(wrapper.text()).not.toContain("spring");
});
it("shows when garden is open", () => {
const p = fakeProps();
p.savedGardens = [fakeSavedGarden(), fakeSavedGarden()];
p.openedSavedGarden = p.savedGardens[0].uuid;
const wrapper = mount(<SavedGardens {...p} />);
expect(wrapper.html()).toContain("selected");
});
});
@ -115,7 +112,7 @@ describe("<SavedGardensLink />", () => {
const wrapper = shallow(<SavedGardensLink />);
clickButton(wrapper, 0, "saved gardens");
expect(history.push).toHaveBeenCalledWith(
"/app/designer/saved_gardens");
"/app/designer/gardens");
mockDev = false;
});
@ -130,7 +127,7 @@ describe("<SavedGardensLink />", () => {
describe("savedGardenOpen", () => {
it("is open", () => {
const result = savedGardenOpen(["", "", "", "saved_gardens", "4", ""]);
const result = savedGardenOpen(["", "", "", "gardens", "4", ""]);
expect(result).toEqual(4);
});
});
@ -145,7 +142,7 @@ describe("<SavedGardenHUD />", () => {
it("opens menu", () => {
const wrapper = mount(<SavedGardenHUD dispatch={jest.fn()} />);
clickButton(wrapper, 0, "menu");
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens");
expect(history.push).toHaveBeenCalledWith("/app/designer/gardens");
});
it("navigates to plants", () => {

View File

@ -13,7 +13,10 @@ import { stopTracking } from "../../connectivity/data_consistency";
/** Save all Plant to PlantTemplates in a new SavedGarden. */
export const snapshotGarden = (name?: string | undefined) =>
axios.post<void>(API.current.snapshotPath, name ? { name } : {})
.then(() => success(t("Garden Saved.")));
.then(() => {
success(t("Garden Saved."));
history.push("/app/designer/gardens");
});
export const unselectSavedGarden = {
type: Actions.CHOOSE_SAVED_GARDEN,
@ -32,19 +35,19 @@ export const applyGarden = (gardenId: number) => (dispatch: Function) => axios
});
export const destroySavedGarden = (uuid: string) => (dispatch: Function) => {
dispatch(destroy(uuid))
.then(dispatch(unselectSavedGarden))
.catch(() => { });
dispatch(unselectSavedGarden);
history.push("/app/designer/gardens");
dispatch(destroy(uuid));
};
export const closeSavedGarden = () => {
history.push("/app/designer/saved_gardens");
history.push("/app/designer/gardens");
return (dispatch: Function) =>
dispatch(unselectSavedGarden);
};
export const openSavedGarden = (savedGarden: string) => {
history.push("/app/designer/saved_gardens/" + unpackUUID(savedGarden).remoteId);
history.push("/app/designer/gardens/" + unpackUUID(savedGarden).remoteId);
return (dispatch: Function) =>
dispatch({ type: Actions.CHOOSE_SAVED_GARDEN, payload: savedGarden });
};
@ -62,7 +65,11 @@ export const openOrCloseGarden = (props: {
/** Create a new SavedGarden with the chosen name. */
export const newSavedGarden = (name: string) =>
(dispatch: Function) => {
dispatch(initSave("SavedGarden", { name: name || "Untitled Garden" }));
dispatch(initSave("SavedGarden", { name: name || "Untitled Garden" }))
.then(() => {
success(t("Garden Saved."));
history.push("/app/designer/gardens");
});
};
/** Create a copy of a PlantTemplate body and assign it a new SavedGarden. */

View File

@ -0,0 +1,42 @@
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 {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "../plants/designer_panel";
import { Content } from "../../constants";
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 class RawAddGarden extends React.Component<GardenSnapshotProps, {}> {
render() {
return <DesignerPanel panelName={"saved-garden"} panel={Panel.SavedGardens}>
<DesignerPanelHeader
panelName={"saved-garden"}
panel={Panel.SavedGardens}
title={t("Add Garden")}
description={Content.SAVED_GARDENS}
backTo={"/app/designer/gardens"} />
<DesignerPanelContent panelName={"saved-garden"}>
<Row>
<GardenSnapshot
currentSavedGarden={this.props.currentSavedGarden}
plantTemplates={this.props.plantTemplates}
dispatch={this.props.dispatch} />
</Row>
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const AddGarden = connect(mapStateToProps)(RawAddGarden);

View File

@ -0,0 +1,120 @@
import * as React from "react";
import { GardenViewButtonProps, EditGardenProps } from "./interfaces";
import { openOrCloseGarden, applyGarden, destroySavedGarden } from "./actions";
import { error } from "../../toast/toast";
import { trim } from "../../util";
import { BlurableInput, Row } from "../../ui";
import { edit, save } from "../../api/crud";
import { connect } from "react-redux";
import {
selectAllSavedGardens, selectAllPlantPointers, findResourceById
} from "../../resources/selectors";
import { Everything } from "../../interfaces";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "../plants/designer_panel";
import { getPathArray } from "../../history";
import { isNumber } from "lodash";
import { ResourceIndex } from "../../resources/interfaces";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
/** Open or close a SavedGarden. */
const GardenViewButton = (props: GardenViewButtonProps) => {
const { dispatch, savedGarden, gardenIsOpen } = props;
const onClick = openOrCloseGarden({ savedGarden, gardenIsOpen, dispatch });
const btnText = gardenIsOpen
? t("exit")
: t("view");
return <button
className={`fb-button ${gardenIsOpen ? "gray" : "yellow"}`}
onClick={onClick}>
{btnText}
</button>;
};
/** Apply a SavedGarden after checking that the current garden is empty. */
const ApplyGardenButton =
(props: { plantPointerCount: number, gardenId: number, dispatch: Function }) =>
<button
className="fb-button green"
onClick={() => props.plantPointerCount > 0
? error(trim(`${t("Please clear current garden first.")}
(${props.plantPointerCount} ${t("plants")})`))
: props.dispatch(applyGarden(props.gardenId))}>
{t("apply")}
</button>;
const DestroyGardenButton =
(props: { dispatch: Function, gardenUuid: string }) =>
<button
className="fb-button red"
onClick={() => props.dispatch(destroySavedGarden(props.gardenUuid))}>
{t("delete")}
</button>;
export const findSavedGarden = (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];
}
};
export const mapStateToProps = (props: Everything): EditGardenProps => {
const { openedSavedGarden } = props.resources.consumers.farm_designer;
const savedGarden = findSavedGarden(props.resources.index);
return {
savedGarden,
gardenIsOpen: !!(savedGarden && savedGarden.uuid === openedSavedGarden),
dispatch: props.dispatch,
plantPointerCount: selectAllPlantPointers(props.resources.index).length,
};
};
export class RawEditGarden extends React.Component<EditGardenProps, {}> {
render() {
const { savedGarden } = this.props;
return <DesignerPanel panelName={"saved-garden-edit"}
panel={Panel.SavedGardens}>
<DesignerPanelHeader
panelName={"saved-garden"}
panel={Panel.SavedGardens}
title={t("Edit Garden")}
backTo={"/app/designer/gardens"} />
<DesignerPanelContent panelName={"saved-garden-edit"}>
{savedGarden
? <div>
<Row>
<label>{t("Garden name")}</label>
<BlurableInput
value={savedGarden.body.name || ""}
onCommit={e => {
this.props.dispatch(edit(savedGarden, {
name: e.currentTarget.value
}));
this.props.dispatch(save(savedGarden.uuid));
}} />
</Row>
<Row>
<ApplyGardenButton
dispatch={this.props.dispatch}
plantPointerCount={this.props.plantPointerCount}
gardenId={savedGarden.body.id || -1} />
<DestroyGardenButton
dispatch={this.props.dispatch}
gardenUuid={savedGarden.uuid} />
<GardenViewButton
dispatch={this.props.dispatch}
savedGarden={savedGarden.uuid}
gardenIsOpen={this.props.gardenIsOpen} />
</Row>
</div>
: <p>{t("Garden not found.")}</p>}
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const EditGarden = connect(mapStateToProps)(RawEditGarden);

View File

@ -1,115 +1,43 @@
import * as React from "react";
import { Row, Col, BlurableInput } from "../../ui";
import { error } from "../../toast/toast";
import { Col } from "../../ui";
import { isNumber, isString } from "lodash";
import { openOrCloseGarden, applyGarden, destroySavedGarden } from "./actions";
import { openSavedGarden } from "./actions";
import {
SavedGardensProps, GardenViewButtonProps, SavedGardenItemProps,
SavedGardenInfoProps
SavedGardenItemProps, SavedGardenInfoProps, SavedGardensListProps
} from "./interfaces";
import { edit, save } from "../../api/crud";
import { trim } from "../../util";
import { t } from "../../i18next_wrapper";
/** Name input and PlantTemplate count for a single SavedGarden. */
export const GardenInfo = (props: SavedGardenInfoProps) => {
const { savedGarden, gardenIsOpen, dispatch } = props;
const { savedGarden, dispatch } = props;
return <div className="saved-garden-info"
onClick={openOrCloseGarden({
savedGarden: savedGarden.uuid, gardenIsOpen, dispatch
})}>
<Col xs={4}>
<BlurableInput
value={savedGarden.body.name || ""}
onCommit={e => {
dispatch(edit(savedGarden, { name: e.currentTarget.value }));
dispatch(save(savedGarden.uuid));
}} />
</Col>
<Col xs={2}>
<p style={{ textAlign: "center" }}>{props.plantTemplateCount}</p>
onClick={() => dispatch(openSavedGarden(savedGarden.uuid))}>
<Col>
<label>{savedGarden.body.name}</label>
<p>{props.plantTemplateCount} {t("plants")}</p>
</Col>
</div>;
};
/** Open or close a SavedGarden. */
const GardenViewButton = (props: GardenViewButtonProps) => {
const { dispatch, savedGarden, gardenIsOpen } = props;
const onClick = openOrCloseGarden({ savedGarden, gardenIsOpen, dispatch });
const btnText = gardenIsOpen
? t("exit")
: t("view");
return <button
className={`fb-button ${gardenIsOpen ? "gray" : "yellow"}`}
onClick={onClick}>
{btnText}
</button>;
};
/** Apply a SavedGarden after checking that the current garden is empty. */
const ApplyGardenButton =
(props: { plantPointerCount: number, gardenId: number, dispatch: Function }) =>
<button
className="fb-button green"
onClick={() => props.plantPointerCount > 0
? error(trim(`${t("Please clear current garden first.")}
(${props.plantPointerCount} ${t("plants")})`))
: props.dispatch(applyGarden(props.gardenId))}>
{t("apply")}
</button>;
const DestroyGardenButton =
(props: { dispatch: Function, gardenUuid: string }) =>
<button
className="fb-button red del-button"
title={t("Delete")}
onClick={() => props.dispatch(destroySavedGarden(props.gardenUuid))}>
<i className="fa fa-times" />
</button>;
/** Info and actions for a single SavedGarden. */
const SavedGardenItem = (props: SavedGardenItemProps) => {
return <div
className={`saved-garden-row ${props.gardenIsOpen ? "selected" : ""}`}>
<Row>
<GardenInfo
savedGarden={props.savedGarden}
gardenIsOpen={props.gardenIsOpen}
plantTemplateCount={props.plantTemplateCount}
dispatch={props.dispatch} />
<Col xs={6}>
<DestroyGardenButton
dispatch={props.dispatch}
gardenUuid={props.savedGarden.uuid} />
<ApplyGardenButton
dispatch={props.dispatch}
plantPointerCount={props.plantPointerCount}
gardenId={props.savedGarden.body.id || -1} />
<GardenViewButton
dispatch={props.dispatch}
savedGarden={props.savedGarden.uuid}
gardenIsOpen={props.gardenIsOpen} />
</Col>
</Row>
<GardenInfo
savedGarden={props.savedGarden}
plantTemplateCount={props.plantTemplateCount}
dispatch={props.dispatch} />
</div>;
};
/** Info and action list for all SavedGardens. */
export const SavedGardenList = (props: SavedGardensProps) =>
export const SavedGardenList = (props: SavedGardensListProps) =>
<div className="saved-garden-list">
<Row>
<Col xs={4}>
<label>{t("name")}</label>
</Col>
<Col xs={2}>
<label>{t("plants")}</label>
</Col>
<Col xs={6}>
<label style={{ float: "right" }}>{t("actions")}</label>
</Col>
</Row>
{props.savedGardens.map(sg => {
if (isString(sg.uuid) && isNumber(sg.body.id) && isString(sg.body.name)) {
const validSavedGarden =
isString(sg.uuid) && isNumber(sg.body.id) && isString(sg.body.name);
if (validSavedGarden && (sg.body.name || "").toLowerCase()
.includes(props.searchTerm.toLowerCase())) {
return <SavedGardenItem
key={sg.uuid}
savedGarden={sg}

View File

@ -8,6 +8,14 @@ export interface SavedGardensProps {
openedSavedGarden: string | undefined;
}
export interface SavedGardensListProps extends SavedGardensProps {
searchTerm: string;
}
export interface SavedGardensState {
searchTerm: string;
}
export interface GardenViewButtonProps {
dispatch: Function;
savedGarden: string | undefined;
@ -24,7 +32,13 @@ export interface SavedGardenItemProps {
export interface SavedGardenInfoProps {
savedGarden: TaggedSavedGarden;
gardenIsOpen: boolean;
plantTemplateCount: number;
dispatch: Function;
}
export interface EditGardenProps {
savedGarden: TaggedSavedGarden | undefined;
gardenIsOpen: boolean;
dispatch: Function;
plantPointerCount: number;
}

View File

@ -6,19 +6,18 @@ import { unselectPlant } from "../actions";
import {
selectAllSavedGardens, selectAllPlantTemplates, selectAllPlantPointers
} from "../../resources/selectors";
import { GardenSnapshot } from "./garden_snapshot";
import { SavedGardenList } from "./garden_list";
import { SavedGardensProps } from "./interfaces";
import { SavedGardensProps, SavedGardensState } from "./interfaces";
import { closeSavedGarden } from "./actions";
import { TaggedSavedGarden } from "farmbot";
import { Content } from "../../constants";
import {
DesignerPanel,
DesignerPanelContent
DesignerPanel, DesignerPanelContent, DesignerPanelTop
} from "../plants/designer_panel";
import { DesignerNavTabs, Panel } from "../panel_header";
import { t } from "../../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
import {
EmptyStateWrapper, EmptyStateGraphic
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
export const mapStateToProps = (props: Everything): SavedGardensProps => ({
savedGardens: selectAllSavedGardens(props.resources.index),
@ -28,35 +27,35 @@ export const mapStateToProps = (props: Everything): SavedGardensProps => ({
openedSavedGarden: props.resources.consumers.farm_designer.openedSavedGarden,
});
export class RawSavedGardens extends React.Component<SavedGardensProps, {}> {
export class RawSavedGardens
extends React.Component<SavedGardensProps, SavedGardensState> {
state: SavedGardensState = { searchTerm: "" };
componentDidMount() {
unselectPlant(this.props.dispatch)();
}
get currentSavedGarden(): TaggedSavedGarden | undefined {
return this.props.savedGardens
.filter(x => x.uuid === this.props.openedSavedGarden)[0];
}
onChange = (e: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ searchTerm: e.currentTarget.value });
render() {
return <DesignerPanel panelName={"saved-garden"} panel={Panel.SavedGardens}>
<DesignerNavTabs />
<DesignerPanelContent panelName={"saved-garden"}
className={"with-nav"}>
<p>{t(Content.SAVED_GARDENS)}</p>
<GardenSnapshot
currentSavedGarden={this.currentSavedGarden}
plantTemplates={this.props.plantTemplates}
dispatch={this.props.dispatch} />
<hr />
<DesignerPanelContent panelName={"saved-garden"}>
<DesignerPanelTop
panel={Panel.SavedGardens}
linkTo={"/app/designer/gardens/add"}
title={t("Add garden")}>
<input type="text" onChange={this.onChange}
placeholder={t("Search your gardens...")} />
</DesignerPanelTop>
<EmptyStateWrapper
notEmpty={this.props.savedGardens.length > 0}
title={t("No saved gardens yet.")}
// text={t(Content.NO_GARDENS)}
text={t(Content.NO_GARDENS)}
colorScheme="gardens"
graphic={EmptyStateGraphic.plants}>
<SavedGardenList {...this.props} />
<SavedGardenList {...this.props} searchTerm={this.state.searchTerm} />
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
@ -67,13 +66,13 @@ export class RawSavedGardens extends React.Component<SavedGardensProps, {}> {
export const SavedGardensLink = () =>
<button className="fb-button green"
hidden={true}
onClick={() => history.push("/app/designer/saved_gardens")}>
onClick={() => history.push("/app/designer/gardens")}>
{t("Saved Gardens")}
</button>;
/** Check if a SavedGarden is currently open (URL approach). */
export const savedGardenOpen = (pathArray: string[]) =>
pathArray[3] === "saved_gardens" && parseInt(pathArray[4]) > 0
pathArray[3] === "gardens" && parseInt(pathArray[4]) > 0
? parseInt(pathArray[4]) : false;
/** Sticky an indicator and actions menu when a SavedGarden is open. */
@ -81,7 +80,7 @@ export const SavedGardenHUD = (props: { dispatch: Function }) =>
<div className="saved-garden-indicator">
<label>{t("Viewing saved garden")}</label>
<button className="fb-button gray"
onClick={() => history.push("/app/designer/saved_gardens")}>
onClick={() => history.push("/app/designer/gardens")}>
{t("Menu")}
</button>
<button className="fb-button green"

View File

@ -1,4 +1,4 @@
import { ResourceIndex } from "./interfaces";
import { ResourceIndex, UUID } from "./interfaces";
import {
ResourceName,
TaggedGenericPointer,
@ -50,7 +50,7 @@ export let maybeDetermineUuid =
}
};
export let findId = (index: ResourceIndex, kind: ResourceName, id: number) => {
export let findId = (index: ResourceIndex, kind: ResourceName, id: number): UUID => {
const uuid = maybeDetermineUuid(index, kind, id);
if (uuid) {
return uuid;

View File

@ -203,7 +203,7 @@ export const UNBOUND_ROUTES = [
}),
route({
children: true,
$: "/designer/plants/saved_gardens",
$: "/designer/plants/gardens",
getModule,
key,
getChild: () => import("./farm_designer/saved_gardens/saved_gardens"),
@ -275,7 +275,7 @@ export const UNBOUND_ROUTES = [
}),
route({
children: true,
$: "/designer/saved_gardens",
$: "/designer/gardens",
getModule,
key,
getChild: () => import("./farm_designer/saved_gardens/saved_gardens"),
@ -283,7 +283,7 @@ export const UNBOUND_ROUTES = [
}),
route({
children: true,
$: "/designer/saved_gardens/templates",
$: "/designer/gardens/templates",
getModule,
key,
getChild: () => import("./farm_designer/plants/plant_inventory"),
@ -291,7 +291,7 @@ export const UNBOUND_ROUTES = [
}),
route({
children: true,
$: "/designer/saved_gardens/templates/:plant_template_id",
$: "/designer/gardens/templates/:plant_template_id",
getModule,
key,
getChild: () => import("./farm_designer/plants/plant_info"),
@ -299,11 +299,19 @@ export const UNBOUND_ROUTES = [
}),
route({
children: true,
$: "/designer/saved_gardens/:saved_garden_id",
$: "/designer/gardens/add",
getModule,
key,
getChild: () => import("./farm_designer/saved_gardens/saved_gardens"),
childKey: "SavedGardens"
getChild: () => import("./farm_designer/saved_gardens/garden_add"),
childKey: "AddGarden"
}),
route({
children: true,
$: "/designer/gardens/:saved_garden_id",
getModule,
key,
getChild: () => import("./farm_designer/saved_gardens/garden_edit"),
childKey: "EditGarden"
}),
route({
children: true,