From 6bd32c243c462b9122b68c3e5749941c29060162 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 7 Nov 2019 11:17:50 -0800 Subject: [PATCH] update points panels --- .../__test_support__/fake_designer_state.ts | 1 + frontend/constants.ts | 6 +- frontend/css/farm_designer/farm_designer.scss | 6 + .../farm_designer/farm_designer_panels.scss | 29 ++-- .../farm_designer/__tests__/reducer_test.ts | 9 ++ frontend/farm_designer/interfaces.ts | 1 + frontend/farm_designer/map/garden_map.tsx | 2 + frontend/farm_designer/map/interfaces.ts | 5 +- .../points/__tests__/garden_point_test.tsx | 56 ++++++- .../points/__tests__/point_layer_test.tsx | 13 +- .../map/layers/points/garden_point.tsx | 40 +++-- .../map/layers/points/point_layer.tsx | 13 +- frontend/farm_designer/map/util.ts | 3 +- .../plants/__tests__/create_points_test.tsx | 137 +++++++++++++----- .../__tests__/point_edit_actions_test.tsx | 75 ++++++++++ .../plants/__tests__/point_info_test.tsx | 56 +++++-- .../__tests__/point_inventory_item_test.tsx | 54 +++++++ .../plants/__tests__/point_inventory_test.tsx | 1 + .../plants/__tests__/select_plants_test.tsx | 12 ++ .../plants/__tests__/weeds_inventory_test.tsx | 1 + .../farm_designer/plants/create_points.tsx | 106 +++++++++----- .../plants/point_edit_actions.tsx | 118 +++++++++++++++ frontend/farm_designer/plants/point_info.tsx | 68 +++------ .../farm_designer/plants/point_inventory.tsx | 20 ++- .../plants/point_inventory_item.tsx | 24 ++- frontend/farm_designer/plants/weeds_edit.tsx | 26 +++- .../farm_designer/plants/weeds_inventory.tsx | 19 ++- frontend/farm_designer/reducer.ts | 10 +- .../weed_detector/__tests__/actions_tests.ts | 6 +- frontend/farmware/weed_detector/actions.tsx | 6 +- frontend/farmware/weed_detector/index.tsx | 3 +- frontend/route_config.tsx | 4 +- public/app-resources/languages/_helper.js | 3 + .../languages/translation_metrics.md | 25 ++-- 34 files changed, 734 insertions(+), 224 deletions(-) create mode 100644 frontend/farm_designer/plants/__tests__/point_edit_actions_test.tsx create mode 100644 frontend/farm_designer/plants/__tests__/point_inventory_item_test.tsx create mode 100644 frontend/farm_designer/plants/point_edit_actions.tsx diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index b0b33ad2f..807972509 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -6,6 +6,7 @@ export const fakeDesignerState = (): DesignerState => ({ plantUUID: undefined, icon: "" }, + hoveredPoint: undefined, hoveredPlantListItem: undefined, cropSearchQuery: "", cropSearchResults: [], diff --git a/frontend/constants.ts b/frontend/constants.ts index 21cbafe8c..0c1c3fc89 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -735,8 +735,10 @@ export namespace Content { Press the back arrow to exit.`); export const CREATE_POINTS_DESCRIPTION = - trim(`Click and drag to draw a point or use the inputs and press - update. Press CREATE POINT to save, or the back arrow to exit.`); + trim(`Click and drag or use the inputs to draw a point.`); + + export const CREATE_WEEDS_DESCRIPTION = + trim(`Click and drag or use the inputs to draw a weed.`); export const BOX_SELECT_DESCRIPTION = trim(`Drag a box around the plants you would like to select. diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index 6777dccc4..348d0da47 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -290,6 +290,12 @@ transform-box: fill-box; } +.map-point { + stroke-width: 2; + stroke-opacity: 0.3; + fill-opacity: 0.1; +} + .plant-image { transform-origin: bottom; transform-box: fill-box; diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index ecf0b5475..a96e0e253 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -275,18 +275,6 @@ } } -.points-panel-tabs { - i { - padding-left: 1rem; - padding-right: 1rem; - } - label { - padding-left: 10rem; - padding-right: 10rem; - margin-top: 0 !important; - } -} - .plant-selection-panel { .panel-action-buttons { position: absolute; @@ -320,6 +308,8 @@ } } +.weed-info-panel, +.point-info-panel, .plant-info-panel { .panel-content { max-height: calc(100vh - 14rem); @@ -354,6 +344,21 @@ } } +.weed-info-panel-content, +.point-info-panel-content { + .saucer { + margin: 1rem; + margin-left: 2rem; + } + .red { + display: block; + margin-top: 3rem; + } + p { + margin-top: 1rem; + } +} + .crop-catalog-panel { .panel-content { padding: 1rem 1rem 6rem; diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index 164f0f896..20767d5b8 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -54,6 +54,15 @@ describe("designer reducer", () => { expect(newState.hoveredPlantListItem).toEqual("plantUuid"); }); + it("sets hovered point", () => { + const action: ReduxAction = { + type: Actions.TOGGLE_HOVERED_POINT, + payload: "uuid" + }; + const newState = designer(oldState(), action); + expect(newState.hoveredPoint).toEqual("uuid"); + }); + it("sets chosen location", () => { const action: ReduxAction = { type: Actions.CHOOSE_LOCATION, diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index bdec17d3f..5e4ebb478 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -101,6 +101,7 @@ export interface Crop { export interface DesignerState { selectedPlants: string[] | undefined; hoveredPlant: HoveredPlantPayl; + hoveredPoint: string | undefined; hoveredPlantListItem: string | undefined; cropSearchQuery: string; cropSearchResults: CropLiveSearchResult[]; diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 6b7531265..3eb06d0e4 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -288,6 +288,8 @@ export class GardenMap extends animate={this.animate} /> PointLayer = () => PlantLayer = () => ({ + history: { push: jest.fn() }, +})); + import * as React from "react"; import { GardenPoint } from "../garden_point"; -import { shallow } from "enzyme"; +import { shallow, mount } from "enzyme"; import { GardenPointProps } from "../../../interfaces"; import { fakePoint } from "../../../../../__test_support__/fake_state/resources"; import { fakeMapTransformProps } from "../../../../../__test_support__/map_transform_props"; +import { Actions } from "../../../../../constants"; +import { history } from "../../../../../history"; describe("", () => { - function fakeProps(): GardenPointProps { - return { - mapTransformProps: fakeMapTransformProps(), - point: fakePoint() - }; - } + const fakeProps = (): GardenPointProps => ({ + mapTransformProps: fakeMapTransformProps(), + point: fakePoint(), + hovered: false, + dispatch: jest.fn(), + }); it("renders point", () => { const wrapper = shallow(); expect(wrapper.find("#point-radius").props().r).toEqual(100); expect(wrapper.find("#point-center").props().r).toEqual(2); + expect(wrapper.find("#point-radius").props().fill).toEqual("transparent"); }); + it("hovers point", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("g").simulate("mouseEnter"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: p.point.uuid + }); + }); + + it("is hovered", () => { + const p = fakeProps(); + p.hovered = true; + const wrapper = mount(); + expect(wrapper.find("#point-radius").props().fill).toEqual("green"); + }); + + it("un-hovers point", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("g").simulate("mouseLeave"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: undefined + }); + }); + + it("opens point info", () => { + const p = fakeProps(); + p.point.body.name = "weed"; + const wrapper = shallow(); + wrapper.find("g").simulate("click"); + expect(history.push).toHaveBeenCalledWith( + `/app/designer/weeds/${p.point.body.id}`); + }); }); diff --git a/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx b/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx index c4b9b36b7..3fcc4a9cd 100644 --- a/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx +++ b/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx @@ -1,10 +1,11 @@ import * as React from "react"; import { PointLayer, PointLayerProps } from "../point_layer"; -import { shallow } from "enzyme"; +import { mount } from "enzyme"; import { fakePoint } from "../../../../../__test_support__/fake_state/resources"; import { fakeMapTransformProps } from "../../../../../__test_support__/map_transform_props"; +import { GardenPoint } from "../garden_point"; describe("", () => { function fakeProps(): PointLayerProps { @@ -12,21 +13,23 @@ describe("", () => { visible: true, points: [fakePoint()], mapTransformProps: fakeMapTransformProps(), + hoveredPoint: undefined, + dispatch: jest.fn(), }; } it("shows points", () => { const p = fakeProps(); - const wrapper = shallow(); + const wrapper = mount(); const layer = wrapper.find("#point-layer"); - expect(layer.find("GardenPoint").html()).toContain("r=\"100\""); + expect(layer.find(GardenPoint).html()).toContain("r=\"100\""); }); it("toggles visibility off", () => { const p = fakeProps(); p.visible = false; - const wrapper = shallow(); + const wrapper = mount(); const layer = wrapper.find("#point-layer"); - expect(layer.find("GardenPoint").length).toEqual(0); + expect(layer.find(GardenPoint).length).toEqual(0); }); }); diff --git a/frontend/farm_designer/map/layers/points/garden_point.tsx b/frontend/farm_designer/map/layers/points/garden_point.tsx index 14f3db35b..490ef96ba 100644 --- a/frontend/farm_designer/map/layers/points/garden_point.tsx +++ b/frontend/farm_designer/map/layers/points/garden_point.tsx @@ -1,23 +1,31 @@ import * as React from "react"; import { GardenPointProps } from "../../interfaces"; -import { defensiveClone } from "../../../../util"; import { transformXY } from "../../util"; +import { Actions } from "../../../../constants"; +import { history } from "../../../../history"; +import { isAWeed } from "../../../plants/weeds_inventory"; -const POINT_STYLES = { - stroke: "green", - strokeOpacity: 0.3, - strokeWidth: "2", - fill: "none", -}; +export const GardenPoint = (props: GardenPointProps) => { -export function GardenPoint(props: GardenPointProps) { - const { point, mapTransformProps } = props; - const { id, x, y } = point.body; - const styles = defensiveClone(POINT_STYLES); - styles.stroke = point.body.meta.color || "green"; + const iconHover = (action: "start" | "end") => () => { + const hover = action === "start"; + props.dispatch({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: hover ? props.point.uuid : undefined + }); + }; + + const { point, mapTransformProps, hovered } = props; + const { id, x, y, meta } = point.body; const { qx, qy } = transformXY(x, y, mapTransformProps); - return - - + const color = meta.color || "green"; + const panel = isAWeed(point.body.name, meta.type) ? "weeds" : "points"; + return history.push(`/app/designer/${panel}/${id}`)}> + + ; -} +}; diff --git a/frontend/farm_designer/map/layers/points/point_layer.tsx b/frontend/farm_designer/map/layers/points/point_layer.tsx index e9dd6319f..7fa30b19f 100644 --- a/frontend/farm_designer/map/layers/points/point_layer.tsx +++ b/frontend/farm_designer/map/layers/points/point_layer.tsx @@ -1,22 +1,29 @@ import * as React from "react"; import { TaggedGenericPointer } from "farmbot"; import { GardenPoint } from "./garden_point"; -import { MapTransformProps } from "../../interfaces"; +import { MapTransformProps, Mode } from "../../interfaces"; +import { getMode } from "../../util"; export interface PointLayerProps { visible: boolean; points: TaggedGenericPointer[]; mapTransformProps: MapTransformProps; + hoveredPoint: string | undefined; + dispatch: Function; } export function PointLayer(props: PointLayerProps) { - const { visible, points, mapTransformProps } = props; - return + const { visible, points, mapTransformProps, hoveredPoint } = props; + const style: React.CSSProperties = + getMode() === Mode.points ? {} : { pointerEvents: "none" }; + return {visible && points.map(p => )} ; diff --git a/frontend/farm_designer/map/util.ts b/frontend/farm_designer/map/util.ts index 96016194d..c4eb2804d 100644 --- a/frontend/farm_designer/map/util.ts +++ b/frontend/farm_designer/map/util.ts @@ -286,6 +286,7 @@ export const transformForQuadrant = }; /** Determine the current map mode based on path. */ +// tslint:disable-next-line:cyclomatic-complexity export const getMode = (): Mode => { const pathArray = getPathArray(); if (pathArray) { @@ -299,7 +300,7 @@ export const getMode = (): Mode => { if (pathArray[4] === "select") { return Mode.boxSelect; } if (pathArray[4] === "crop_search") { return Mode.addPlant; } if (pathArray[3] === "move_to") { return Mode.moveTo; } - if (pathArray[3] === "points") { + if (pathArray[3] === "points" || pathArray[3] === "weeds") { if (pathArray[4] === "add") { return Mode.createPoint; } return Mode.points; } diff --git a/frontend/farm_designer/plants/__tests__/create_points_test.tsx b/frontend/farm_designer/plants/__tests__/create_points_test.tsx index 8817825cd..a9d9daa3a 100644 --- a/frontend/farm_designer/plants/__tests__/create_points_test.tsx +++ b/frontend/farm_designer/plants/__tests__/create_points_test.tsx @@ -4,6 +4,12 @@ jest.mock("../../../farmware/weed_detector/actions", () => ({ deletePoints: jest.fn() })); +let mockPath = "/app/designer/points/add"; +jest.mock("../../../history", () => ({ + push: jest.fn(), + getPathArray: () => mockPath.split("/"), +})); + import * as React from "react"; import { mount, shallow } from "enzyme"; import { @@ -16,8 +22,9 @@ import { deletePoints } from "../../../farmware/weed_detector/actions"; import { Actions } from "../../../constants"; import { clickButton } from "../../../__test_support__/helpers"; import { fakeState } from "../../../__test_support__/fake_state"; -import { DeepPartial } from "redux"; import { CurrentPointPayl } from "../../interfaces"; +import { inputEvent } from "../../../__test_support__/fake_input_event"; +import { cloneDeep } from "lodash"; const FAKE_POINT: CurrentPointPayl = ({ name: "My Point", cx: 13, cy: 22, r: 345, color: "red" }); @@ -43,6 +50,10 @@ describe("mapStateToProps", () => { }); describe("", () => { + beforeEach(() => { + mockPath = "/app/designer/points/add"; + }); + const fakeProps = (): CreatePointsProps => ({ dispatch: jest.fn(), currentPoint: undefined, @@ -56,54 +67,89 @@ describe("", () => { return new CreatePoints(props); }; - it("renders", () => { + it("renders for points", () => { + mockPath = "/app/designer"; const wrapper = mount(); - ["create point", "cancel", "delete", "x", "y", "radius", "color"] + ["create point", "delete", "x", "y", "radius", "color"] + .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); + }); + + it("renders for weeds", () => { + mockPath = "/app/designer/weeds/add"; + const wrapper = mount(); + ["create weed", "delete", "x", "y", "radius", "color"] .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); it("updates specific fields", () => { const i = fakeInstance(); - const updater = i.updateValue("color"); - type Event = React.SyntheticEvent; - const e: DeepPartial = { - currentTarget: { - value: "cheerful hue" - } - }; - updater(e as Event); + i.updateValue("color")(inputEvent("cheerful hue")); expect(i.props.currentPoint).toBeTruthy(); + const expected = cloneDeep(FAKE_POINT); + expected.color = "cheerful hue"; expect(i.props.dispatch).toHaveBeenCalledWith({ - payload: { - color: "cheerful hue", - cx: 13, - cy: 22, - name: "My Point", - r: 345, - }, type: "SET_CURRENT_POINT_DATA", + payload: expected, }); }); - it("updates current point", () => { - const p = fakeInstance(); - p.updateCurrentPoint(); - expect(p.props.dispatch).toHaveBeenCalledWith({ + it("doesn't update fields without current point", () => { + const p = fakeProps(); + const wrapper = mount(); + jest.clearAllMocks(); + wrapper.instance().updateValue("r")(inputEvent("1")); + expect(p.dispatch).not.toHaveBeenCalled(); + }); + + it("loads default point data", () => { + const i = fakeInstance(); + i.loadDefaultPoint(); + expect(i.props.dispatch).toHaveBeenCalledWith({ type: "SET_CURRENT_POINT_DATA", - payload: { cx: 13, cy: 22, name: "My Point", r: 345, color: "red" }, + payload: { name: "Created Point", color: "green", cx: 1, cy: 1, r: 15 }, }); }); + + it("updates point name", () => { + mockPath = "/app/designer/weeds/add"; + const p = fakeProps(); + p.currentPoint = { cx: 0, cy: 0, r: 100 }; + const panel = mount(); + const wrapper = shallow(panel.instance().PointName()); + wrapper.find("BlurableInput").simulate("commit", { + currentTarget: { value: "new name" } + }); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_CURRENT_POINT_DATA, payload: { + cx: 0, cy: 0, r: 100, name: "new name", color: "red", + } + }); + }); + it("creates point", () => { + mockPath = "/app/designer/points/add"; const wrapper = mount(); - wrapper.setState({ cx: 10, cy: 20, r: 30, color: "red" }); - clickButton(wrapper, 0, "create point"); - expect(initSave).toHaveBeenCalledWith("Point", - { - meta: { color: "red", created_by: "farm-designer" }, - name: "Created Point", - pointer_type: "GenericPointer", - radius: 30, x: 10, y: 20, z: 0 - }); + wrapper.setState({ cx: 10, cy: 20, r: 30 }); + clickButton(wrapper, 0, "save"); + expect(initSave).toHaveBeenCalledWith("Point", { + meta: { color: "green", created_by: "farm-designer", type: "point" }, + name: "Created Point", + pointer_type: "GenericPointer", + radius: 30, x: 10, y: 20, z: 0, + }); + }); + + it("creates weed", () => { + mockPath = "/app/designer/weeds/add"; + const wrapper = mount(); + wrapper.setState({ cx: 10, cy: 20, r: 30 }); + clickButton(wrapper, 0, "save"); + expect(initSave).toHaveBeenCalledWith("Point", { + meta: { color: "red", created_by: "farm-designer", type: "weed" }, + name: "Created Weed", + pointer_type: "GenericPointer", + radius: 30, x: 10, y: 20, z: 0, + }); }); it("deletes all points", () => { @@ -115,11 +161,32 @@ describe("", () => { button.simulate("click"); expect(window.confirm).toHaveBeenCalledWith( expect.stringContaining("all the points")); - expect(p.dispatch).not.toHaveBeenCalled(); + expect(deletePoints).not.toHaveBeenCalled(); window.confirm = () => true; p.dispatch = jest.fn(x => x()); button.simulate("click"); - expect(deletePoints).toHaveBeenCalledWith("points", "farm-designer"); + expect(deletePoints).toHaveBeenCalledWith("points", { + created_by: "farm-designer", type: "point" + }); + }); + + it("deletes all weeds", () => { + mockPath = "/app/designer/weeds/add"; + const p = fakeProps(); + const wrapper = mount(); + const button = wrapper.find("button").last(); + expect(button.text()).toEqual("Delete all created weeds"); + window.confirm = jest.fn(); + button.simulate("click"); + expect(window.confirm).toHaveBeenCalledWith( + expect.stringContaining("all the weeds")); + expect(deletePoints).not.toHaveBeenCalled(); + window.confirm = () => true; + p.dispatch = jest.fn(x => x()); + button.simulate("click"); + expect(deletePoints).toHaveBeenCalledWith("points", { + created_by: "farm-designer", type: "weed" + }); }); it("changes color", () => { @@ -145,7 +212,7 @@ describe("", () => { currentTarget: { value: "10" } }); expect(p.dispatch).toHaveBeenCalledWith({ - payload: { cx: 10, cy: 0, r: 0 }, + payload: { cx: 10, cy: 0, r: 0, color: "green" }, type: Actions.SET_CURRENT_POINT_DATA }); }); diff --git a/frontend/farm_designer/plants/__tests__/point_edit_actions_test.tsx b/frontend/farm_designer/plants/__tests__/point_edit_actions_test.tsx new file mode 100644 index 000000000..9f46d179a --- /dev/null +++ b/frontend/farm_designer/plants/__tests__/point_edit_actions_test.tsx @@ -0,0 +1,75 @@ +jest.mock("../../../api/crud", () => ({ + edit: jest.fn(), + save: jest.fn(), +})); + +import * as React from "react"; +import { shallow } from "enzyme"; +import { + EditPointLocation, EditPointLocationProps, + EditPointRadius, EditPointRadiusProps, + EditPointColor, EditPointColorProps, updatePoint, +} from "../point_edit_actions"; +import { fakePoint } from "../../../__test_support__/fake_state/resources"; +import { edit, save } from "../../../api/crud"; + +describe("updatePoint()", () => { + it("updates a point", () => { + const point = fakePoint(); + updatePoint(point, jest.fn())({ radius: 100 }); + expect(edit).toHaveBeenCalledWith(point, { radius: 100 }); + expect(save).toHaveBeenCalledWith(point.uuid); + }); + + it("doesn't update point", () => { + updatePoint(undefined, jest.fn())({ radius: 100 }); + expect(edit).not.toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); + }); +}); + +describe("", () => { + const fakeProps = (): EditPointLocationProps => ({ + updatePoint: jest.fn(), + location: { x: 1, y: 2 }, + }); + + it("edits location", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("BlurableInput").first().simulate("commit", { + currentTarget: { value: 3 } + }); + expect(p.updatePoint).toHaveBeenCalledWith({ x: 3 }); + }); +}); + +describe("", () => { + const fakeProps = (): EditPointRadiusProps => ({ + updatePoint: jest.fn(), + radius: 100, + }); + + it("edits radius", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("BlurableInput").first().simulate("commit", { + currentTarget: { value: 300 } + }); + expect(p.updatePoint).toHaveBeenCalledWith({ radius: 300 }); + }); +}); + +describe("", () => { + const fakeProps = (): EditPointColorProps => ({ + updatePoint: jest.fn(), + color: "red", + }); + + it("edits color", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("ColorPicker").first().simulate("change", "blue"); + expect(p.updatePoint).toHaveBeenCalledWith({ meta: { color: "blue" } }); + }); +}); diff --git a/frontend/farm_designer/plants/__tests__/point_info_test.tsx b/frontend/farm_designer/plants/__tests__/point_info_test.tsx index 6a1fd77a1..e3a799a56 100644 --- a/frontend/farm_designer/plants/__tests__/point_info_test.tsx +++ b/frontend/farm_designer/plants/__tests__/point_info_test.tsx @@ -9,10 +9,15 @@ jest.mock("../../../device", () => ({ getDevice: () => ({ moveAbsolute: mockMoveAbs }) })); +jest.mock("../../../api/crud", () => ({ + destroy: jest.fn(), +})); + import * as React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { - RawEditPoint as EditPoint, EditPointProps, mapStateToProps, moveToPoint + RawEditPoint as EditPoint, EditPointProps, + mapStateToProps, } from "../point_info"; import { fakePoint } from "../../../__test_support__/fake_state/resources"; import { fakeState } from "../../../__test_support__/fake_state"; @@ -20,6 +25,11 @@ import { buildResourceIndex } from "../../../__test_support__/resource_index_builder"; import { getDevice } from "../../../device"; +import { Xyz } from "farmbot"; +import { clickButton } from "../../../__test_support__/helpers"; +import { destroy } from "../../../api/crud"; +import { DesignerPanelHeader } from "../designer_panel"; +import { Actions } from "../../../constants"; describe("", () => { const fakeProps = (): EditPointProps => ({ @@ -38,6 +48,39 @@ describe("", () => { const wrapper = mount(); expect(wrapper.text()).toContain("Edit Point 1"); }); + + it("moves the device to a particular point", () => { + mockPath = "/app/designer/points/1"; + const p = fakeProps(); + const point = fakePoint(); + const coords = { x: 1, y: -2, z: 3 }; + Object.entries(coords).map(([axis, value]: [Xyz, number]) => + point.body[axis] = value); + p.findPoint = () => point; + const wrapper = mount(); + wrapper.find("button").first().simulate("click"); + expect(getDevice().moveAbsolute).toHaveBeenCalledWith(coords); + }); + + it("goes back", () => { + mockPath = "/app/designer/points/1"; + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(DesignerPanelHeader).simulate("back"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, payload: undefined + }); + }); + + it("deletes point", () => { + mockPath = "/app/designer/points/1"; + const p = fakeProps(); + const point = fakePoint(); + p.findPoint = () => point; + const wrapper = mount(); + clickButton(wrapper, 1, "delete"); + expect(destroy).toHaveBeenCalledWith(point.uuid); + }); }); describe("mapStateToProps()", () => { @@ -50,12 +93,3 @@ describe("mapStateToProps()", () => { expect(props.findPoint(1)).toEqual(point); }); }); - -describe("moveToPoint()", () => { - it("moves the device to a particular point", () => { - const coords = { x: 1, y: -2, z: 3 }; - const mover = moveToPoint(coords); - mover(); - expect(getDevice().moveAbsolute).toHaveBeenCalledWith(coords); - }); -}); diff --git a/frontend/farm_designer/plants/__tests__/point_inventory_item_test.tsx b/frontend/farm_designer/plants/__tests__/point_inventory_item_test.tsx new file mode 100644 index 000000000..30f3d72fd --- /dev/null +++ b/frontend/farm_designer/plants/__tests__/point_inventory_item_test.tsx @@ -0,0 +1,54 @@ +jest.mock("../../../history", () => ({ push: jest.fn() })); + +import * as React from "react"; +import { shallow } from "enzyme"; +import { + PointInventoryItem, PointInventoryItemProps +} from "../point_inventory_item"; +import { fakePoint } from "../../../__test_support__/fake_state/resources"; +import { push } from "../../../history"; +import { Actions } from "../../../constants"; + +describe(" />", () => { + const fakeProps = (): PointInventoryItemProps => ({ + tpp: fakePoint(), + dispatch: jest.fn(), + hovered: false, + navName: "points", + }); + + it("navigates to point", () => { + const p = fakeProps(); + p.tpp.body.id = 1; + const wrapper = shallow(); + wrapper.simulate("click"); + expect(push).toHaveBeenCalledWith("/app/designer/points/1"); + }); + + it("hovers point", () => { + const p = fakeProps(); + p.tpp.body.id = 1; + const wrapper = shallow(); + wrapper.simulate("mouseEnter"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, payload: p.tpp.uuid + }); + }); + + it("shows hovered", () => { + const p = fakeProps(); + p.hovered = true; + const wrapper = shallow(); + expect(wrapper.hasClass("hovered")).toBeTruthy(); + }); + + it("un-hovers point", () => { + const p = fakeProps(); + p.tpp.body.id = 1; + const wrapper = shallow(); + wrapper.simulate("mouseLeave"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, payload: undefined + }); + }); +}); diff --git a/frontend/farm_designer/plants/__tests__/point_inventory_test.tsx b/frontend/farm_designer/plants/__tests__/point_inventory_test.tsx index 6f0b0e8ee..260664987 100644 --- a/frontend/farm_designer/plants/__tests__/point_inventory_test.tsx +++ b/frontend/farm_designer/plants/__tests__/point_inventory_test.tsx @@ -18,6 +18,7 @@ describe(" />", () => { const fakeProps = (): PointsProps => ({ points: [], dispatch: jest.fn(), + hoveredPoint: undefined, }); it("renders no points", () => { diff --git a/frontend/farm_designer/plants/__tests__/select_plants_test.tsx b/frontend/farm_designer/plants/__tests__/select_plants_test.tsx index 301063c47..5ff5b7669 100644 --- a/frontend/farm_designer/plants/__tests__/select_plants_test.tsx +++ b/frontend/farm_designer/plants/__tests__/select_plants_test.tsx @@ -138,6 +138,18 @@ describe("", () => { expect(destroy).not.toHaveBeenCalled(); }); + it("errors when deleting selected plants", () => { + const p = fakeProps(); + p.dispatch = jest.fn(() => Promise.reject()); + p.selected = ["plant.1", "plant.2"]; + const wrapper = mount(); + expect(wrapper.text()).toContain("Delete"); + window.confirm = () => true; + wrapper.find("button").at(2).simulate("click"); + expect(destroy).toHaveBeenCalledWith("plant.1", true); + expect(destroy).toHaveBeenCalledWith("plant.2", true); + }); + it("shows other buttons", () => { mockDev = true; const wrapper = mount(); diff --git a/frontend/farm_designer/plants/__tests__/weeds_inventory_test.tsx b/frontend/farm_designer/plants/__tests__/weeds_inventory_test.tsx index 62dbad729..10aaf6698 100644 --- a/frontend/farm_designer/plants/__tests__/weeds_inventory_test.tsx +++ b/frontend/farm_designer/plants/__tests__/weeds_inventory_test.tsx @@ -10,6 +10,7 @@ describe(" />", () => { const fakeProps = (): WeedsProps => ({ points: [], dispatch: jest.fn(), + hoveredPoint: undefined, }); it("renders no points", () => { diff --git a/frontend/farm_designer/plants/create_points.tsx b/frontend/farm_designer/plants/create_points.tsx index ed7560c40..7f0a2706a 100644 --- a/frontend/farm_designer/plants/create_points.tsx +++ b/frontend/farm_designer/plants/create_points.tsx @@ -23,6 +23,7 @@ import { import { parseIntInput } from "../../util"; import { t } from "../../i18next_wrapper"; import { Panel } from "../panel_header"; +import { getPathArray } from "../../history"; export function mapStateToProps(props: Everything): CreatePointsProps { const { position } = props.bot.hardware.location_data; @@ -44,11 +45,11 @@ export interface CreatePointsProps { type CreatePointsState = Partial; const DEFAULTS: CurrentPointPayl = { - name: "Created Point", + name: undefined, cx: 1, cy: 1, r: 15, - color: "red" + color: undefined, }; export class RawCreatePoints @@ -70,13 +71,21 @@ export class RawCreatePoints } }; + get defaultName() { + return this.panel == "weeds" + ? t("Created Weed") + : t("Created Point"); + } + + get defaultColor() { return this.panel == "weeds" ? "red" : "green"; } + getPointData = (): CurrentPointPayl => { return { name: this.attr("name"), cx: this.attr("cx"), cy: this.attr("cy"), r: this.attr("r"), - color: this.attr("color"), + color: this.attr("color") || this.defaultColor, }; } @@ -93,31 +102,43 @@ export class RawCreatePoints }); } - componentWillUnmount() { - this.cancel(); - } - - updateCurrentPoint = () => { + loadDefaultPoint = () => { this.props.dispatch({ type: Actions.SET_CURRENT_POINT_DATA, - payload: this.getPointData() + payload: { + name: this.defaultName, + cx: DEFAULTS.cx, + cy: DEFAULTS.cy, + r: DEFAULTS.r, + color: this.defaultColor, + } as CurrentPointPayl }); } + UNSAFE_componentWillMount() { + this.loadDefaultPoint(); + } + + componentWillUnmount() { + this.cancel(); + } + /** Update fields. */ updateValue = (key: keyof CreatePointsState) => { return (e: React.SyntheticEvent) => { const { value } = e.currentTarget; - this.setState({ [key]: value }); if (this.props.currentPoint) { const point = this.getPointData(); switch (key) { case "name": case "color": + this.setState({ [key]: value }); point[key] = value; break; default: - point[key] = parseIntInput(value); + const intValue = parseIntInput(value); + this.setState({ [key]: intValue }); + point[key] = intValue; } this.props.dispatch({ type: Actions.SET_CURRENT_POINT_DATA, @@ -129,19 +150,24 @@ export class RawCreatePoints changeColor = (color: ResourceColor) => { this.setState({ color }); + const point = this.getPointData(); + point.color = color; this.props.dispatch({ type: Actions.SET_CURRENT_POINT_DATA, - payload: this.getPointData() + payload: point }); } + get panel() { return getPathArray()[3] || "points"; } + createPoint = () => { const body: GenericPointer = { pointer_type: "GenericPointer", - name: this.attr("name") || "Created Point", + name: this.attr("name") || this.defaultName, meta: { - color: this.attr("color"), - created_by: "farm-designer" + color: this.attr("color") || this.defaultColor, + created_by: "farm-designer", + type: this.panel == "weeds" ? "weed" : "point", }, x: this.attr("cx"), y: this.attr("cy"), @@ -150,6 +176,7 @@ export class RawCreatePoints }; this.props.dispatch(initSave("Point", body)); this.cancel(); + this.loadDefaultPoint(); } PointName = () => @@ -160,7 +187,7 @@ export class RawCreatePoints name="name" type="text" onCommit={this.updateValue("name")} - value={this.attr("name") || ""} /> + value={this.attr("name") || this.defaultName} /> ; @@ -188,13 +215,13 @@ export class RawCreatePoints name="r" type="number" onCommit={this.updateValue("r")} - value={this.attr("r", DEFAULTS.r)} + value={this.attr("r")} min={0} /> ; @@ -204,48 +231,51 @@ export class RawCreatePoints - - - DeleteAllPoints = () => + DeleteAllPoints = (type: "point" | "weed") =>
-

{t("Delete all of the points created through this panel.")}

+

{type === "weed" + ? t("Delete all of the weeds created through this panel.") + : t("Delete all of the points created through this panel.")}

render() { - return + const panelType = this.panel == "weeds" ? Panel.Weeds : Panel.Points; + const panelDescription = this.panel == "weeds" ? + Content.CREATE_WEEDS_DESCRIPTION : Content.CREATE_POINTS_DESCRIPTION; + return + panel={panelType} + title={this.panel == "weeds" ? t("Create weed") : t("Create point")} + backTo={`/app/designer/${this.panel}`} + description={panelDescription} /> - + {this.DeleteAllPoints(this.panel == "weeds" ? "weed" : "point")} ; } diff --git a/frontend/farm_designer/plants/point_edit_actions.tsx b/frontend/farm_designer/plants/point_edit_actions.tsx new file mode 100644 index 000000000..82f52c1ff --- /dev/null +++ b/frontend/farm_designer/plants/point_edit_actions.tsx @@ -0,0 +1,118 @@ +import * as React from "react"; +import { t } from "../../i18next_wrapper"; +import { getDevice } from "../../device"; +import { destroy, edit, save } from "../../api/crud"; +import { ResourceColor } from "../../interfaces"; +import { TaggedGenericPointer } from "farmbot"; +import { ListItem } from "./plant_panel"; +import { round } from "lodash"; +import { Row, Col, BlurableInput, ColorPicker } from "../../ui"; +import { parseIntInput } from "../../util"; +import { UUID } from "../../resources/interfaces"; + +export const updatePoint = + (point: TaggedGenericPointer | undefined, dispatch: Function) => + (update: Partial) => { + if (point) { + dispatch(edit(point, update)); + dispatch(save(point.uuid)); + } + }; + +export interface EditPointPropertiesProps { + point: TaggedGenericPointer; + updatePoint(update: Partial): void; +} + +export const EditPointProperties = (props: EditPointPropertiesProps) => +
    + + + + + + + + + +
; + +export interface PointActionsProps { + x: number; + y: number; + z: number; + uuid: UUID; + dispatch: Function; +} + +export const PointActions = ({ x, y, z, uuid, dispatch }: PointActionsProps) => +
+ + +
; + +export interface EditPointLocationProps { + updatePoint(update: Partial): void; + location: Record<"x" | "y", number>; +} + +export const EditPointLocation = (props: EditPointLocationProps) => + + {["x", "y"].map((axis: "x" | "y") => + + + props.updatePoint({ + [axis]: round(parseIntInput(e.currentTarget.value)) + })} /> + )} + ; + +export interface EditPointRadiusProps { + updatePoint(update: Partial): void; + radius: number; +} + +export const EditPointRadius = (props: EditPointRadiusProps) => + + + + props.updatePoint({ + radius: round(parseIntInput(e.currentTarget.value)) + })} /> + + ; + +export interface EditPointColorProps { + updatePoint(update: Partial): void; + color: string | undefined; +} + +export const EditPointColor = (props: EditPointColorProps) => + + props.updatePoint({ meta: { color } })} /> + ; diff --git a/frontend/farm_designer/plants/point_info.tsx b/frontend/farm_designer/plants/point_info.tsx index 6a56b675f..c39d6f137 100644 --- a/frontend/farm_designer/plants/point_info.tsx +++ b/frontend/farm_designer/plants/point_info.tsx @@ -5,19 +5,18 @@ import { } from "./designer_panel"; import { t } from "../../i18next_wrapper"; import { history, getPathArray } from "../../history"; -import { Everything } from "../../interfaces"; -import { TaggedPoint, Vector3 } from "farmbot"; -import { maybeFindPointById } from "../../resources/selectors"; -import { DeleteButton } from "../../controls/pin_form_fields"; -import { getDevice } from "../../device"; import { Panel } from "../panel_header"; - -export const moveToPoint = - (body: Vector3) => () => getDevice().moveAbsolute(body); +import { Everything } from "../../interfaces"; +import { TaggedGenericPointer } from "farmbot"; +import { maybeFindPointById } from "../../resources/selectors"; +import { Actions } from "../../constants"; +import { + EditPointProperties, updatePoint, PointActions +} from "./point_edit_actions"; export interface EditPointProps { dispatch: Function; - findPoint(id: number): TaggedPoint | undefined; + findPoint(id: number): TaggedGenericPointer | undefined; } export const mapStateToProps = (props: Everything): EditPointProps => ({ @@ -32,50 +31,31 @@ export class RawEditPoint extends React.Component { return this.props.findPoint(parseInt(this.stringyID)); } } + get panelName() { return "point-info"; } + get backTo() { return "/app/designer/points"; } fallback = () => { - history.push("/app/designer/points"); + history.push(this.backTo); return {t("Redirecting...")}; } - temporaryMenu = (p: TaggedPoint) => { - const { body } = p; - return
-

- Point {body.name || body.id || ""} @ ({body.x}, {body.y}, {body.z}) -

-
    - { - Object.entries(body.meta).map(([k, v]) => { - return
  • {k}: {v}
  • ; - }) - } -
- - - {t("Delete Point")} - -
; - }; - - default = (point: TaggedPoint) => { - return + default = (point: TaggedGenericPointer) => { + const { x, y, z } = point.body; + return + backTo={this.backTo} + onBack={() => this.props.dispatch({ + type: Actions.TOGGLE_HOVERED_POINT, payload: undefined + })}> - - {this.point && this.temporaryMenu(this.point)} + + + ; } diff --git a/frontend/farm_designer/plants/point_inventory.tsx b/frontend/farm_designer/plants/point_inventory.tsx index ba8b2f4d9..be8468481 100644 --- a/frontend/farm_designer/plants/point_inventory.tsx +++ b/frontend/farm_designer/plants/point_inventory.tsx @@ -13,10 +13,12 @@ import { import { selectAllGenericPointers } from "../../resources/selectors"; import { TaggedGenericPointer } from "farmbot"; import { t } from "../../i18next_wrapper"; +import { isAWeed } from "./weeds_inventory"; export interface PointsProps { points: TaggedGenericPointer[]; dispatch: Function; + hoveredPoint: string | undefined; } interface PointsState { @@ -24,11 +26,13 @@ interface PointsState { } export function mapStateToProps(props: Everything): PointsProps { + const { hoveredPoint } = props.resources.consumers.farm_designer; return { points: selectAllGenericPointers(props.resources.index) .filter(x => !x.body.discarded_at) - .filter(x => !x.body.name.toLowerCase().includes("weed")), + .filter(x => !isAWeed(x.body.name, x.body.meta.type)), dispatch: props.dispatch, + hoveredPoint, }; } @@ -49,7 +53,7 @@ export class RawPoints extends React.Component { - + 0} graphic={EmptyStateGraphic.points} @@ -59,12 +63,12 @@ export class RawPoints extends React.Component { {this.props.points .filter(p => p.body.name.toLowerCase() .includes(this.state.searchTerm.toLowerCase())) - .map(p => { - return ; - })} + .map(p => )} ; diff --git a/frontend/farm_designer/plants/point_inventory_item.tsx b/frontend/farm_designer/plants/point_inventory_item.tsx index ad5dd86b5..cae14e396 100644 --- a/frontend/farm_designer/plants/point_inventory_item.tsx +++ b/frontend/farm_designer/plants/point_inventory_item.tsx @@ -1,11 +1,14 @@ import * as React from "react"; import { TaggedGenericPointer } from "farmbot"; import { Saucer } from "../../ui"; +import { Actions } from "../../constants"; import { push } from "../../history"; export interface PointInventoryItemProps { tpp: TaggedGenericPointer; dispatch: Function; + hovered: boolean; + navName: "points" | "weeds"; } // The individual points that show up in the farm designer sub nav. @@ -14,17 +17,30 @@ export class PointInventoryItem extends render() { const point = this.props.tpp.body; + const { tpp, dispatch } = this.props; const pointId = (point.id || "ERR_NO_POINT_ID").toString(); - const click = () => { - push(`/app/designer/points/${pointId}`); + const toggle = (action: "enter" | "leave") => { + const isEnter = action === "enter"; + dispatch({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: isEnter ? tpp.uuid : undefined + }); }; - const label = point.name || "Unknown point"; + const click = () => { + push(`/app/designer/${this.props.navName}/${pointId}`); + dispatch({ type: Actions.TOGGLE_HOVERED_POINT, payload: [tpp.uuid] }); + }; + + // Name given from OpenFarm's API. + const label = point.name || "Unknown plant"; return
toggle("enter")} + onMouseLeave={() => toggle("leave")} onClick={click}> diff --git a/frontend/farm_designer/plants/weeds_edit.tsx b/frontend/farm_designer/plants/weeds_edit.tsx index 4cf79d16f..003efa795 100644 --- a/frontend/farm_designer/plants/weeds_edit.tsx +++ b/frontend/farm_designer/plants/weeds_edit.tsx @@ -6,13 +6,16 @@ import { import { t } from "../../i18next_wrapper"; import { history, getPathArray } from "../../history"; import { Everything } from "../../interfaces"; -import { TaggedPoint } from "farmbot"; +import { TaggedGenericPointer } from "farmbot"; import { maybeFindPointById } from "../../resources/selectors"; import { Panel } from "../panel_header"; +import { + EditPointProperties, PointActions, updatePoint +} from "./point_edit_actions"; export interface EditWeedProps { dispatch: Function; - findPoint(id: number): TaggedPoint | undefined; + findPoint(id: number): TaggedGenericPointer | undefined; } export const mapStateToProps = (props: Everything): EditWeedProps => ({ @@ -27,21 +30,28 @@ export class RawEditWeed extends React.Component { return this.props.findPoint(parseInt(this.stringyID)); } } + get panelName() { return "weed-info"; } + get backTo() { return "/app/designer/weeds"; } fallback = () => { - history.push("/app/designer/weeds"); + history.push(this.backTo); return {t("Redirecting...")}; } - default = (point: TaggedPoint) => { - return + default = (point: TaggedGenericPointer) => { + const { x, y, z } = point.body; + return + backTo={this.backTo}> - + + + ; } diff --git a/frontend/farm_designer/plants/weeds_inventory.tsx b/frontend/farm_designer/plants/weeds_inventory.tsx index 82de73377..27d893dac 100644 --- a/frontend/farm_designer/plants/weeds_inventory.tsx +++ b/frontend/farm_designer/plants/weeds_inventory.tsx @@ -17,17 +17,22 @@ import { PointInventoryItem } from "./point_inventory_item"; export interface WeedsProps { points: TaggedGenericPointer[]; dispatch: Function; + hoveredPoint: string | undefined; } interface WeedsState { searchTerm: string; } +export const isAWeed = (pointName: string, type?: string) => + type == "weed" || pointName.toLowerCase().includes("weed"); + export const mapStateToProps = (props: Everything): WeedsProps => ({ points: selectAllGenericPointers(props.resources.index) .filter(x => !x.body.discarded_at) - .filter(x => x.body.name.toLowerCase().includes("weed")), + .filter(x => isAWeed(x.body.name, x.body.meta.type)), dispatch: props.dispatch, + hoveredPoint: props.resources.consumers.farm_designer.hoveredPoint, }); export class RawWeeds extends React.Component { @@ -57,12 +62,12 @@ export class RawWeeds extends React.Component { {this.props.points .filter(p => p.body.name.toLowerCase() .includes(this.state.searchTerm.toLowerCase())) - .map(p => { - return ; - })} + .map(p => )} ; diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index 45a84179e..8387003c2 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -13,6 +13,7 @@ export let initialState: DesignerState = { plantUUID: undefined, icon: "" }, + hoveredPoint: undefined, hoveredPlantListItem: undefined, cropSearchQuery: "", cropSearchResults: [], @@ -50,8 +51,15 @@ export let designer = generateReducer(initialState) s.hoveredPlantListItem = payload; return s; }) - .add(Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => { + .add(Actions.TOGGLE_HOVERED_POINT, (s, { payload }) => { + s.hoveredPoint = payload; + return s; + }) + .add(Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => { + const { color } = + (!payload || !payload.color) ? (s.currentPoint || { color: "green" }) : payload; s.currentPoint = payload; + s.currentPoint && (s.currentPoint.color = color); return s; }) .add(Actions.OF_SEARCH_RESULTS_OK, (s, a) => { diff --git a/frontend/farmware/weed_detector/__tests__/actions_tests.ts b/frontend/farmware/weed_detector/__tests__/actions_tests.ts index 2b15829c4..ca9a94166 100644 --- a/frontend/farmware/weed_detector/__tests__/actions_tests.ts +++ b/frontend/farmware/weed_detector/__tests__/actions_tests.ts @@ -62,7 +62,7 @@ describe("deletePoints()", () => { mockDelete = Promise.resolve(); mockData = [{ id: 1 }, { id: 2 }, { id: 3 }]; const dispatch = jest.fn(); - await deletePoints("weeds", "plant-detection")(dispatch, jest.fn()); + await deletePoints("weeds", { created_by: "plant-detection" })(dispatch, jest.fn()); expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search", { meta: { created_by: "plant-detection" } }); await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3"); @@ -80,7 +80,7 @@ describe("deletePoints()", () => { mockDelete = Promise.reject("error"); mockData = [{ id: 1 }, { id: 2 }, { id: 3 }]; const dispatch = jest.fn(); - await deletePoints("weeds", "plant-detection")(dispatch, jest.fn()); + await deletePoints("weeds", { created_by: "plant-detection" })(dispatch, jest.fn()); expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search", { meta: { created_by: "plant-detection" } }); await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3"); @@ -98,7 +98,7 @@ describe("deletePoints()", () => { mockDelete = Promise.resolve(); mockData = times(200, () => ({ id: 1 })); const dispatch = jest.fn(); - await deletePoints("weeds", "plant-detection")(dispatch, jest.fn()); + await deletePoints("weeds", { created_by: "plant-detection" })(dispatch, jest.fn()); expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search", { meta: { created_by: "plant-detection" } }); await expect(axios.delete).toHaveBeenCalledWith( diff --git a/frontend/farmware/weed_detector/actions.tsx b/frontend/farmware/weed_detector/actions.tsx index 2f405d4d1..2d9717e35 100644 --- a/frontend/farmware/weed_detector/actions.tsx +++ b/frontend/farmware/weed_detector/actions.tsx @@ -10,11 +10,13 @@ import { Actions } from "../../constants"; import { t } from "../../i18next_wrapper"; export function deletePoints( - pointName: string, createdBy: string, cb?: ProgressCallback): Thunk { + pointName: string, + metaQuery: { [key: string]: string }, + cb?: ProgressCallback): Thunk { // TODO: Generalize and add to api/crud.ts return async function (dispatch) { const URL = API.current.pointSearchPath; - const QUERY = { meta: { created_by: createdBy } }; + const QUERY = { meta: metaQuery }; try { const resp = await axios.post(URL, QUERY); const ids = resp.data.map(x => x.id); diff --git a/frontend/farmware/weed_detector/index.tsx b/frontend/farmware/weed_detector/index.tsx index 542e37190..ba704875d 100644 --- a/frontend/farmware/weed_detector/index.tsx +++ b/frontend/farmware/weed_detector/index.tsx @@ -35,7 +35,8 @@ export class WeedDetector const percentage = `${Math.round((p.completed / p.total) * 100)} %`; this.setState({ deletionProgress: p.isDone ? "" : percentage }); }; - this.props.dispatch(deletePoints(t("weeds"), "plant-detection", progress)); + this.props.dispatch(deletePoints(t("weeds"), + { created_by: "plant-detection" }, progress)); this.setState({ deletionProgress: t("Deleting...") }); } diff --git a/frontend/route_config.tsx b/frontend/route_config.tsx index 51c69609b..d5a8c703f 100644 --- a/frontend/route_config.tsx +++ b/frontend/route_config.tsx @@ -374,8 +374,8 @@ export const UNBOUND_ROUTES = [ $: "/designer/weeds/add", getModule, key, - getChild: () => import("./farm_designer/plants/weeds_add"), - childKey: "AddWeed" + getChild: () => import("./farm_designer/plants/create_points"), + childKey: "CreatePoints" }), route({ children: true, diff --git a/public/app-resources/languages/_helper.js b/public/app-resources/languages/_helper.js index 459fa44a9..c01b7d3b9 100644 --- a/public/app-resources/languages/_helper.js +++ b/public/app-resources/languages/_helper.js @@ -143,6 +143,9 @@ var HelperNamespace = (function () { markdown += 'Where `en` is your language code.\n\n'; markdown += 'Translation file format can be checked using:\n\n'; markdown += '```bash\nnpm run translation-check\n```\n\n'; + markdown += '_Note: If using Docker, add `sudo docker-compose run web`'; + markdown += ' before the commands.\nFor example, `sudo docker-compose'; + markdown += ' run web npm run translation-check`._\n\n'; markdown += 'See the [README](https://github.com/FarmBot/Farmbot-Web-App'; markdown += '#translating-the-web-app-into-your-language) for contribution'; markdown += ' instructions.\n\n'; diff --git a/public/app-resources/languages/translation_metrics.md b/public/app-resources/languages/translation_metrics.md index f31f23062..bd924a6a8 100644 --- a/public/app-resources/languages/translation_metrics.md +++ b/public/app-resources/languages/translation_metrics.md @@ -16,22 +16,25 @@ Translation file format can be checked using: npm run translation-check ``` +_Note: If using Docker, add `sudo docker-compose run web` before the commands. +For example, `sudo docker-compose run web npm run translation-check`._ + See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app-into-your-language) for contribution instructions. -Total number of phrases identified by the language helper for translation: __1114__ +Total number of phrases identified by the language helper for translation: __1131__ |Language|Percent translated|Translated|Untranslated|Other Translations| |:---:|---:|---:|---:|---:| -|da|10%|110|1004|34| -|de|38%|418|696|131| -|es|91%|1012|102|163| -|fr|68%|759|355|188| -|it|8%|90|1024|180| -|nl|7%|79|1035|151| -|pt|6%|71|1043|170| -|ru|54%|601|513|211| -|th|0%|0|1114|0| -|zh|8%|86|1028|151| +|da|10%|108|1023|41| +|de|37%|416|715|138| +|es|89%|1005|126|170| +|fr|67%|753|378|194| +|it|8%|91|1040|186| +|nl|7%|79|1052|158| +|pt|6%|71|1060|177| +|ru|53%|598|533|218| +|th|0%|0|1131|0| +|zh|8%|86|1045|158| **Percent translated** refers to the percent of phrases identified by the language helper that have been translated. Additional phrases not identified