Skip to content

Commit

Permalink
saved garden updates
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielburnworth committed Nov 1, 2018
1 parent 6e8f8f0 commit e63c41d
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 36 deletions.
2 changes: 1 addition & 1 deletion app/models/saved_garden.rb
@@ -1,4 +1,4 @@
class SavedGarden < ApplicationRecord
belongs_to :device
has_many :plant_templates
has_many :plant_templates, dependent: :destroy
end
19 changes: 18 additions & 1 deletion webpack/css/farm_designer/farm_designer.scss
Expand Up @@ -325,6 +325,14 @@
}
}

.garden-snapshot {
button {
&.pseudo-disabled {
cursor: not-allowed;
}
}
}

.saved-garden-list {
margin: -15px;
.saved-garden-row {
Expand All @@ -335,13 +343,22 @@
.saved-garden-info div {
cursor: pointer;
padding-right: 0;
input {
background: none;
box-shadow: none;
padding: 0;
}
p {
margin-top: 0.25rem;
}
}
&:hover {
background: $light_gray;
}
&.selected {
background: $gray;
p {
p,
input {
font-weight: bold;
}
}
Expand Down
17 changes: 14 additions & 3 deletions webpack/farm_designer/saved_gardens/__tests__/actions_test.ts
Expand Up @@ -7,17 +7,20 @@ jest.mock("axios", () => ({

jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));

jest.mock("../../../api/crud", () => ({ destroy: jest.fn() }));
jest.mock("../../../api/crud", () => ({
destroy: jest.fn(),
initSave: jest.fn(),
}));

import { API } from "../../../api";
import axios from "axios";
import {
snapshotGarden, applyGarden, destroySavedGarden, closeSavedGarden,
openSavedGarden, openOrCloseGarden
openSavedGarden, openOrCloseGarden, newSavedGarden
} from "../actions";
import { history } from "../../../history";
import { Actions } from "../../../constants";
import { destroy } from "../../../api/crud";
import { destroy, initSave } from "../../../api/crud";

describe("snapshotGarden", () => {
it("calls the API and lets auto-sync do the rest", () => {
Expand Down Expand Up @@ -101,3 +104,11 @@ describe("openOrCloseGarden", () => {
expect(history.push).toHaveBeenCalledWith("/app/designer/saved_gardens");
});
});

describe("newSavedGarden", () => {
it("creates a new saved garden", () => {
newSavedGarden("my saved garden")(jest.fn());
expect(initSave).toHaveBeenCalledWith(
"SavedGarden", { name: "my saved garden" });
});
});
32 changes: 32 additions & 0 deletions webpack/farm_designer/saved_gardens/__tests__/garden_list_test.tsx
@@ -0,0 +1,32 @@
jest.mock("../actions", () => ({
openOrCloseGarden: jest.fn(),
}));

jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));

import * as React from "react";
import { shallow } from "enzyme";
import {
fakeSavedGarden
} from "../../../__test_support__/fake_state/resources";
import { edit } from "../../../api/crud";
import { GardenInfo } from "../garden_list";

describe("<GardenInfo />", () => {
const fakeProps = () => ({
dispatch: jest.fn(),
savedGarden: fakeSavedGarden(),
gardenIsOpen: false,
plantCount: 1,
});

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" });
});
});
Expand Up @@ -4,32 +4,46 @@ jest.mock("axios", () => {

jest.mock("../actions", () => ({
snapshotGarden: jest.fn(),
newSavedGarden: jest.fn(),
}));

import * as React from "react";
import { mount, shallow } from "enzyme";
import { GardenSnapshotProps, GardenSnapshot } from "../garden_snapshot";
import { clickButton } from "../../../__test_support__/helpers";
import { error } from "farmbot-toastr";
import { snapshotGarden } from "../actions";
import { snapshotGarden, newSavedGarden } from "../actions";
import { fakeSavedGarden } from "../../../__test_support__/fake_state/resources";

describe("<GardenSnapshot />", () => {
const fakeProps = (): GardenSnapshotProps => ({
plantsInGarden: true,
disabled: false,
currentSavedGarden: undefined,
plantTemplates: [],
dispatch: jest.fn(),
});

it("saves garden", () => {
const wrapper = mount(<GardenSnapshot {...fakeProps()} />);
clickButton(wrapper, 0, "snapshot");
expect(snapshotGarden).toHaveBeenCalledWith(undefined);
clickButton(wrapper, 0, "snapshot current garden");
expect(snapshotGarden).toHaveBeenCalledWith("");
});

it("doesn't snapshot saved garden", () => {
const p = fakeProps();
p.currentSavedGarden = fakeSavedGarden();
const wrapper = mount(<GardenSnapshot {...p} />);
clickButton(wrapper, 0, "snapshot current garden");
expect(snapshotGarden).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
expect.stringContaining("while saved garden is open"));
});

it("no garden to save", () => {
const p = fakeProps();
p.plantsInGarden = false;
const wrapper = mount(<GardenSnapshot {...p} />);
clickButton(wrapper, 0, "snapshot");
clickButton(wrapper, 0, "snapshot current garden");
expect(snapshotGarden).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(expect.stringContaining(
"No plants in garden"));
Expand All @@ -42,4 +56,12 @@ describe("<GardenSnapshot />", () => {
});
expect(wrapper.instance().state.name).toEqual("new name");
});

it("creates new garden", () => {
const wrapper = shallow<GardenSnapshot>(<GardenSnapshot {...fakeProps()} />);
wrapper.setState({ name: "new saved garden" });
wrapper.find("button").last().simulate("click");
expect(newSavedGarden).toHaveBeenCalledWith("new saved garden");
expect(wrapper.instance().state.name).toEqual("");
});
});
Expand Up @@ -12,6 +12,8 @@ jest.mock("../actions", () => ({

jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));

jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));

import * as React from "react";
import { mount, shallow } from "enzyme";
import {
Expand Down Expand Up @@ -43,30 +45,30 @@ describe("<SavedGardens />", () => {
it("renders saved gardens", () => {
const wrapper = mount(<SavedGardens {...fakeProps()} />);
["saved garden 1", "2", "apply"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
expect(wrapper.html().toLowerCase()).toContain(string));
});

it("applies garden", () => {
const p = fakeProps();
p.savedGardens[0].uuid = "SavedGarden.1.0";
p.plantsInGarden = false;
const wrapper = mount(<SavedGardens {...p} />);
clickButton(wrapper, 2, "apply");
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, 2, "apply");
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, 1, "");
clickButton(wrapper, 2, "");
expect(destroySavedGarden).toHaveBeenCalledWith(p.savedGardens[0].uuid);
});

Expand Down
14 changes: 11 additions & 3 deletions webpack/farm_designer/saved_gardens/actions.ts
Expand Up @@ -4,7 +4,7 @@ import { t } from "i18next";
import { success } from "farmbot-toastr";
import { history } from "../../history";
import { Actions } from "../../constants";
import { destroy } from "../../api/crud";
import { destroy, initSave } from "../../api/crud";
import { unpackUUID } from "../../util";
import { isString } from "lodash";

Expand All @@ -24,8 +24,11 @@ export const applyGarden = (gardenId: number) => (dispatch: Function) => axios
dispatch(unselectSavedGarden);
});

export const destroySavedGarden = (uuid: string) => (dispatch: Function) =>
dispatch(destroy(uuid)).catch(() => { });
export const destroySavedGarden = (uuid: string) => (dispatch: Function) => {
dispatch(destroy(uuid))
.then(dispatch(unselectSavedGarden))
.catch(() => { });
};

export const closeSavedGarden = () => {
history.push("/app/designer/saved_gardens");
Expand All @@ -47,3 +50,8 @@ export const openOrCloseGarden = (props: {
!props.gardenIsOpen && isString(props.savedGarden)
? props.dispatch(openSavedGarden(props.savedGarden))
: props.dispatch(closeSavedGarden());

export const newSavedGarden = (name: string) =>
(dispatch: Function) => {
dispatch(initSave("SavedGarden", { name: name || "Untitled Garden" }));
};
12 changes: 9 additions & 3 deletions webpack/farm_designer/saved_gardens/garden_list.tsx
@@ -1,15 +1,16 @@
import * as React from "react";
import { t } from "i18next";
import { Row, Col } from "../../ui";
import { Row, Col, BlurableInput } from "../../ui";
import { error } from "farmbot-toastr";
import { isNumber, isString } from "lodash";
import { openOrCloseGarden, applyGarden, destroySavedGarden } from "./actions";
import {
SavedGardensProps, GardenViewButtonProps, SavedGardenItemProps
} from "./interfaces";
import { TaggedSavedGarden } from "farmbot";
import { edit, save } from "../../api/crud";

const GardenInfo = (props: {
export const GardenInfo = (props: {
savedGarden: TaggedSavedGarden,
gardenIsOpen: boolean,
plantCount: number,
Expand All @@ -21,7 +22,12 @@ const GardenInfo = (props: {
savedGarden: savedGarden.uuid, gardenIsOpen, dispatch
})}>
<Col xs={4}>
<p>{savedGarden.body.name}</p>
<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.plantCount}</p>
Expand Down
56 changes: 41 additions & 15 deletions webpack/farm_designer/saved_gardens/garden_snapshot.tsx
@@ -1,38 +1,64 @@
import * as React from "react";
import { t } from "i18next";
import { error } from "farmbot-toastr";
import { snapshotGarden } from "./actions";
import { snapshotGarden, newSavedGarden } from "./actions";
import { TaggedPlantTemplate, TaggedSavedGarden } from "farmbot";

export interface GardenSnapshotProps {
plantsInGarden: boolean;
disabled: boolean;
currentSavedGarden: TaggedSavedGarden | undefined;
plantTemplates: TaggedPlantTemplate[];
dispatch: Function;
}

interface GardenSnapshotState {
name: string | undefined;
name: string;
}

const GARDEN_OPEN_ERROR = t("Can't snapshot while saved garden is open.");
const NO_PLANTS_ERROR = t("No plants in garden. Create some plants first.");

export class GardenSnapshot
extends React.Component<GardenSnapshotProps, GardenSnapshotState> {
state = { name: undefined };
state = { name: "" };

snapshot = () => {
const { currentSavedGarden } = this.props;
if (!currentSavedGarden) {
this.props.plantsInGarden
? snapshotGarden(this.state.name)
: error(NO_PLANTS_ERROR);
this.setState({ name: "" });
} else {
error(GARDEN_OPEN_ERROR);
}
}

new = () => {
this.props.dispatch(newSavedGarden(this.state.name));
this.setState({ name: "" });
};

render() {
return <div className="garden-snapshot"
title={this.props.disabled
? t("Can't snapshot while saved garden is open.")
: ""}>
const disabledClassName =
this.props.currentSavedGarden ? "pseudo-disabled" : "";
return <div className="garden-snapshot">
<label>{t("garden name")}</label>
<input
disabled={this.props.disabled}
onChange={e => this.setState({ name: e.currentTarget.value })}
value={this.state.name} />
<button
className="fb-button gray wide"
disabled={this.props.disabled}
onClick={() => this.props.plantsInGarden
? snapshotGarden(this.state.name)
: error(t("No plants in garden. Create some plants first."))}>
{t("Snapshot")}
title={this.props.currentSavedGarden
? GARDEN_OPEN_ERROR
: ""}
className={`fb-button gray wide ${disabledClassName}`}
onClick={this.snapshot}>
{t("Snapshot current garden")}
</button>
<button
className="fb-button green wide"
onClick={this.new}>
{t("create new garden")}
</button>
</div>;
}
Expand Down
10 changes: 9 additions & 1 deletion webpack/farm_designer/saved_gardens/saved_gardens.tsx
Expand Up @@ -11,6 +11,7 @@ import { GardenSnapshot } from "./garden_snapshot";
import { SavedGardenList } from "./garden_list";
import { SavedGardensProps } from "./interfaces";
import { closeSavedGarden } from "./actions";
import { TaggedSavedGarden } from "farmbot";

export function mapStateToProps(props: Everything): SavedGardensProps {
return {
Expand All @@ -29,6 +30,11 @@ export class SavedGardens extends React.Component<SavedGardensProps, {}> {
unselectPlant(this.props.dispatch)();
}

get currentSavedGarden(): TaggedSavedGarden | undefined {
return this.props.savedGardens
.filter(x => x.uuid === this.props.openedSavedGarden)[0];
}

render() {
return <div
className="panel-container green-panel saved-garden-panel">
Expand All @@ -47,7 +53,9 @@ export class SavedGardens extends React.Component<SavedGardensProps, {}> {
<div className="panel-content saved-garden-panel-content">
<GardenSnapshot
plantsInGarden={this.props.plantsInGarden}
disabled={!!this.props.openedSavedGarden} />
currentSavedGarden={this.currentSavedGarden}
plantTemplates={this.props.plantTemplates}
dispatch={this.props.dispatch} />
<hr />
{this.props.savedGardens.length > 0
? <SavedGardenList {...this.props} />
Expand Down

0 comments on commit e63c41d

Please sign in to comment.