update points panels

pull/1563/head
gabrielburnworth 2019-11-07 11:17:50 -08:00
parent dedb98c15f
commit 6bd32c243c
34 changed files with 734 additions and 224 deletions

View File

@ -6,6 +6,7 @@ export const fakeDesignerState = (): DesignerState => ({
plantUUID: undefined,
icon: ""
},
hoveredPoint: undefined,
hoveredPlantListItem: undefined,
cropSearchQuery: "",
cropSearchResults: [],

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -54,6 +54,15 @@ describe("designer reducer", () => {
expect(newState.hoveredPlantListItem).toEqual("plantUuid");
});
it("sets hovered point", () => {
const action: ReduxAction<string> = {
type: Actions.TOGGLE_HOVERED_POINT,
payload: "uuid"
};
const newState = designer(oldState(), action);
expect(newState.hoveredPoint).toEqual("uuid");
});
it("sets chosen location", () => {
const action: ReduxAction<BotPosition> = {
type: Actions.CHOOSE_LOCATION,

View File

@ -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[];

View File

@ -288,6 +288,8 @@ export class GardenMap extends
animate={this.animate} />
PointLayer = () => <PointLayer
mapTransformProps={this.mapTransformProps}
dispatch={this.props.dispatch}
hoveredPoint={this.props.designer.hoveredPoint}
visible={!!this.props.showPoints}
points={this.props.points} />
PlantLayer = () => <PlantLayer

View File

@ -71,9 +71,8 @@ export interface GardenPlantState {
export interface GardenPointProps {
mapTransformProps: MapTransformProps;
point: TaggedGenericPointer;
}
export interface GardenPointState {
hovered: boolean;
dispatch: Function;
}
interface DragHelpersBaseProps {

View File

@ -1,24 +1,66 @@
jest.mock("../../../../../history", () => ({
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("<GardenPoint/>", () => {
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(<GardenPoint {...fakeProps()} />);
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(<GardenPoint {...p} />);
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(<GardenPoint {...p} />);
expect(wrapper.find("#point-radius").props().fill).toEqual("green");
});
it("un-hovers point", () => {
const p = fakeProps();
const wrapper = shallow(<GardenPoint {...p} />);
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(<GardenPoint {...p} />);
wrapper.find("g").simulate("click");
expect(history.push).toHaveBeenCalledWith(
`/app/designer/weeds/${p.point.body.id}`);
});
});

View File

@ -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("<PointLayer/>", () => {
function fakeProps(): PointLayerProps {
@ -12,21 +13,23 @@ describe("<PointLayer/>", () => {
visible: true,
points: [fakePoint()],
mapTransformProps: fakeMapTransformProps(),
hoveredPoint: undefined,
dispatch: jest.fn(),
};
}
it("shows points", () => {
const p = fakeProps();
const wrapper = shallow(<PointLayer {...p} />);
const wrapper = mount(<PointLayer {...p} />);
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(<PointLayer {...p} />);
const wrapper = mount(<PointLayer {...p} />);
const layer = wrapper.find("#point-layer");
expect(layer.find("GardenPoint").length).toEqual(0);
expect(layer.find(GardenPoint).length).toEqual(0);
});
});

View File

@ -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 <g id={"point-" + id}>
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius} {...styles} />
<circle id="point-center" cx={qx} cy={qy} r={2} {...styles} />
const color = meta.color || "green";
const panel = isAWeed(point.body.name, meta.type) ? "weeds" : "points";
return <g id={"point-" + id} className={"map-point"} stroke={color}
onMouseEnter={iconHover("start")}
onMouseLeave={iconHover("end")}
onClick={() => history.push(`/app/designer/${panel}/${id}`)}>
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius}
fill={hovered ? color : "transparent"} />
<circle id="point-center" cx={qx} cy={qy} r={2} />
</g>;
}
};

View File

@ -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 <g id="point-layer">
const { visible, points, mapTransformProps, hoveredPoint } = props;
const style: React.CSSProperties =
getMode() === Mode.points ? {} : { pointerEvents: "none" };
return <g id="point-layer" style={style}>
{visible &&
points.map(p =>
<GardenPoint
point={p}
key={p.uuid}
hovered={hoveredPoint == p.uuid}
dispatch={props.dispatch}
mapTransformProps={mapTransformProps} />
)}
</g>;

View File

@ -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;
}

View File

@ -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("<CreatePoints />", () => {
beforeEach(() => {
mockPath = "/app/designer/points/add";
});
const fakeProps = (): CreatePointsProps => ({
dispatch: jest.fn(),
currentPoint: undefined,
@ -56,54 +67,89 @@ describe("<CreatePoints />", () => {
return new CreatePoints(props);
};
it("renders", () => {
it("renders for points", () => {
mockPath = "/app/designer";
const wrapper = mount(<CreatePoints {...fakeProps()} />);
["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(<CreatePoints {...fakeProps()} />);
["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<HTMLInputElement>;
const e: DeepPartial<Event> = {
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<CreatePoints>(<CreatePoints {...p} />);
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<CreatePoints>(<CreatePoints {...p} />);
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(<CreatePoints {...fakeProps()} />);
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(<CreatePoints {...fakeProps()} />);
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("<CreatePoints />", () => {
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(<CreatePoints {...p} />);
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("<CreatePoints />", () => {
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
});
});

View File

@ -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("<EditPointLocation />", () => {
const fakeProps = (): EditPointLocationProps => ({
updatePoint: jest.fn(),
location: { x: 1, y: 2 },
});
it("edits location", () => {
const p = fakeProps();
const wrapper = shallow(<EditPointLocation {...p} />);
wrapper.find("BlurableInput").first().simulate("commit", {
currentTarget: { value: 3 }
});
expect(p.updatePoint).toHaveBeenCalledWith({ x: 3 });
});
});
describe("<EditPointRadius />", () => {
const fakeProps = (): EditPointRadiusProps => ({
updatePoint: jest.fn(),
radius: 100,
});
it("edits radius", () => {
const p = fakeProps();
const wrapper = shallow(<EditPointRadius {...p} />);
wrapper.find("BlurableInput").first().simulate("commit", {
currentTarget: { value: 300 }
});
expect(p.updatePoint).toHaveBeenCalledWith({ radius: 300 });
});
});
describe("<EditPointColor />", () => {
const fakeProps = (): EditPointColorProps => ({
updatePoint: jest.fn(),
color: "red",
});
it("edits color", () => {
const p = fakeProps();
const wrapper = shallow(<EditPointColor {...p} />);
wrapper.find("ColorPicker").first().simulate("change", "blue");
expect(p.updatePoint).toHaveBeenCalledWith({ meta: { color: "blue" } });
});
});

View File

@ -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("<EditPoint />", () => {
const fakeProps = (): EditPointProps => ({
@ -38,6 +48,39 @@ describe("<EditPoint />", () => {
const wrapper = mount(<EditPoint {...fakeProps()} />);
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(<EditPoint {...p} />);
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(<EditPoint {...p} />);
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(<EditPoint {...p} />);
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);
});
});

View File

@ -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("<PointInventoryItem> />", () => {
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(<PointInventoryItem {...p} />);
wrapper.simulate("click");
expect(push).toHaveBeenCalledWith("/app/designer/points/1");
});
it("hovers point", () => {
const p = fakeProps();
p.tpp.body.id = 1;
const wrapper = shallow(<PointInventoryItem {...p} />);
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(<PointInventoryItem {...p} />);
expect(wrapper.hasClass("hovered")).toBeTruthy();
});
it("un-hovers point", () => {
const p = fakeProps();
p.tpp.body.id = 1;
const wrapper = shallow(<PointInventoryItem {...p} />);
wrapper.simulate("mouseLeave");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.TOGGLE_HOVERED_POINT, payload: undefined
});
});
});

View File

@ -18,6 +18,7 @@ describe("<Points> />", () => {
const fakeProps = (): PointsProps => ({
points: [],
dispatch: jest.fn(),
hoveredPoint: undefined,
});
it("renders no points", () => {

View File

@ -138,6 +138,18 @@ describe("<SelectPlants />", () => {
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(<SelectPlants {...p} />);
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(<SelectPlants {...fakeProps()} />);

View File

@ -10,6 +10,7 @@ describe("<Weeds> />", () => {
const fakeProps = (): WeedsProps => ({
points: [],
dispatch: jest.fn(),
hoveredPoint: undefined,
});
it("renders no points", () => {

View File

@ -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<CurrentPointPayl>;
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<HTMLInputElement>) => {
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} />
</Col>
</Row>;
@ -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} />
</Col>
<Col xs={3}>
<label>{t("color")}</label>
<ColorPicker
current={this.attr("color") as ResourceColor}
current={(this.attr("color") || this.defaultColor) as ResourceColor}
onChange={this.changeColor} />
</Col>
</Row>;
@ -204,48 +231,51 @@ export class RawCreatePoints
<Row>
<button className="fb-button green"
onClick={this.createPoint}>
{t("Create point")}
</button>
<button className="fb-button yellow"
onClick={this.updateCurrentPoint}>
{t("Update")}
</button>
<button className="fb-button gray"
onClick={this.cancel}>
{t("Cancel")}
{t("Save")}
</button>
</Row>
DeleteAllPoints = () =>
DeleteAllPoints = (type: "point" | "weed") =>
<Row>
<div className="delete-row">
<label>{t("delete")}</label>
<p>{t("Delete all of the points created through this panel.")}</p>
<p>{type === "weed"
? t("Delete all of the weeds created through this panel.")
: t("Delete all of the points created through this panel.")}</p>
<button className="fb-button red delete"
onClick={() => {
if (confirm(t("Delete all the points you have created?"))) {
this.props.dispatch(deletePoints("points", "farm-designer"));
if (confirm(type === "weed"
? t("Delete all the weeds you have created?")
: t("Delete all the points you have created?"))) {
this.props.dispatch(deletePoints("points", {
created_by: "farm-designer", type,
}));
this.cancel();
}
}}>
{t("Delete all created points")}
{type === "weed"
? t("Delete all created weeds")
: t("Delete all created points")}
</button>
</div>
</Row>
render() {
return <DesignerPanel panelName={"point-creation"} panel={Panel.Points}>
const panelType = this.panel == "weeds" ? Panel.Weeds : Panel.Points;
const panelDescription = this.panel == "weeds" ?
Content.CREATE_WEEDS_DESCRIPTION : Content.CREATE_POINTS_DESCRIPTION;
return <DesignerPanel panelName={"point-creation"} panel={panelType}>
<DesignerPanelHeader
panelName={"point-creation"}
panel={Panel.Points}
title={t("Create point")}
backTo={"/app/designer/points"}
description={Content.CREATE_POINTS_DESCRIPTION} />
panel={panelType}
title={this.panel == "weeds" ? t("Create weed") : t("Create point")}
backTo={`/app/designer/${this.panel}`}
description={panelDescription} />
<DesignerPanelContent panelName={"point-creation"}>
<this.PointName />
<this.PointProperties />
<this.PointActions />
<this.DeleteAllPoints />
{this.DeleteAllPoints(this.panel == "weeds" ? "weed" : "point")}
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -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<TaggedGenericPointer["body"]>) => {
if (point) {
dispatch(edit(point, update));
dispatch(save(point.uuid));
}
};
export interface EditPointPropertiesProps {
point: TaggedGenericPointer;
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
}
export const EditPointProperties = (props: EditPointPropertiesProps) =>
<ul>
<ListItem name={t("Location")}>
<EditPointLocation
location={{ x: props.point.body.x, y: props.point.body.y }}
updatePoint={props.updatePoint} />
</ListItem>
<ListItem name={t("Size")}>
<EditPointRadius
radius={props.point.body.radius}
updatePoint={props.updatePoint} />
</ListItem>
<ListItem name={t("Color")}>
<EditPointColor
color={props.point.body.meta.color}
updatePoint={props.updatePoint} />
</ListItem>
</ul>;
export interface PointActionsProps {
x: number;
y: number;
z: number;
uuid: UUID;
dispatch: Function;
}
export const PointActions = ({ x, y, z, uuid, dispatch }: PointActionsProps) =>
<div>
<button
className="fb-button gray no-float"
type="button"
onClick={() => getDevice().moveAbsolute({ x, y, z })}>
{t("Move Device to location")}
</button>
<button
className="fb-button red no-float"
onClick={() => dispatch(destroy(uuid))}>
{t("Delete")}
</button>
</div>;
export interface EditPointLocationProps {
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
location: Record<"x" | "y", number>;
}
export const EditPointLocation = (props: EditPointLocationProps) =>
<Row>
{["x", "y"].map((axis: "x" | "y") =>
<Col xs={6} key={axis}>
<label style={{ marginTop: 0 }}>{t("{{axis}} (mm)", { axis })}</label>
<BlurableInput
type="number"
value={props.location[axis]}
min={0}
onCommit={e => props.updatePoint({
[axis]: round(parseIntInput(e.currentTarget.value))
})} />
</Col>)}
</Row>;
export interface EditPointRadiusProps {
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
radius: number;
}
export const EditPointRadius = (props: EditPointRadiusProps) =>
<Row>
<Col xs={6}>
<label style={{ marginTop: 0 }}>{t("radius (mm)")}</label>
<BlurableInput
type="number"
value={props.radius}
min={0}
onCommit={e => props.updatePoint({
radius: round(parseIntInput(e.currentTarget.value))
})} />
</Col>
</Row>;
export interface EditPointColorProps {
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
color: string | undefined;
}
export const EditPointColor = (props: EditPointColorProps) =>
<Row>
<ColorPicker
current={(props.color || "green") as ResourceColor}
onChange={color => props.updatePoint({ meta: { color } })} />
</Row>;

View File

@ -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<EditPointProps, {}> {
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 <span>{t("Redirecting...")}</span>;
}
temporaryMenu = (p: TaggedPoint) => {
const { body } = p;
return <div>
<h3>
Point {body.name || body.id || ""} @ ({body.x}, {body.y}, {body.z})
</h3>
<ul>
{
Object.entries(body.meta).map(([k, v]) => {
return <li key={k}>{k}: {v}</li>;
})
}
</ul>
<button
className="green fb-button"
type="button"
onClick={moveToPoint(body)}>
{t("Move Device to Point")}
</button>
<DeleteButton
dispatch={this.props.dispatch}
uuid={p.uuid}
onDestroy={this.fallback}>
{t("Delete Point")}
</DeleteButton>
</div>;
};
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"plant-info"} panel={Panel.Points}>
default = (point: TaggedGenericPointer) => {
const { x, y, z } = point.body;
return <DesignerPanel panelName={this.panelName} panel={Panel.Points}>
<DesignerPanelHeader
panelName={"plant-info"}
panelName={this.panelName}
panel={Panel.Points}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
backTo={this.backTo}
onBack={() => this.props.dispatch({
type: Actions.TOGGLE_HOVERED_POINT, payload: undefined
})}>
</DesignerPanelHeader>
<DesignerPanelContent panelName={"plants"}>
{this.point && this.temporaryMenu(this.point)}
<DesignerPanelContent panelName={this.panelName}>
<EditPointProperties point={point}
updatePoint={updatePoint(point, this.props.dispatch)} />
<PointActions x={x} y={y} z={z} uuid={point.uuid}
dispatch={this.props.dispatch} />
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -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<PointsProps, PointsState> {
<input type="text" onChange={this.update}
placeholder={t("Search your points...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"point"}>
<DesignerPanelContent panelName={"points"}>
<EmptyStateWrapper
notEmpty={this.props.points.length > 0}
graphic={EmptyStateGraphic.points}
@ -59,12 +63,12 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
{this.props.points
.filter(p => p.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(p => {
return <PointInventoryItem
key={p.uuid}
tpp={p}
dispatch={this.props.dispatch} />;
})}
.map(p => <PointInventoryItem
key={p.uuid}
navName={"points"}
tpp={p}
hovered={this.props.hoveredPoint === p.uuid}
dispatch={this.props.dispatch} />)}
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;

View File

@ -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 <div
className={`point-search-item`}
className={`point-search-item ${this.props.hovered ? "hovered" : ""}`}
key={pointId}
onMouseEnter={() => toggle("enter")}
onMouseLeave={() => toggle("leave")}
onClick={click}>
<Saucer color={point.meta.color || "green"} />
<span className="point-search-item-name">

View File

@ -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<EditWeedProps, {}> {
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 <span>{t("Redirecting...")}</span>;
}
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"weed-info"} panel={Panel.Weeds}>
default = (point: TaggedGenericPointer) => {
const { x, y, z } = point.body;
return <DesignerPanel panelName={this.panelName} panel={Panel.Weeds}>
<DesignerPanelHeader
panelName={"weed-info"}
panelName={this.panelName}
panel={Panel.Weeds}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
backTo={this.backTo}>
</DesignerPanelHeader>
<DesignerPanelContent panelName={"weed-info"}>
<DesignerPanelContent panelName={this.panelName}>
<EditPointProperties point={point}
updatePoint={updatePoint(point, this.props.dispatch)} />
<PointActions x={x} y={y} z={z} uuid={point.uuid}
dispatch={this.props.dispatch} />
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -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<WeedsProps, WeedsState> {
@ -57,12 +62,12 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
{this.props.points
.filter(p => p.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(p => {
return <PointInventoryItem
key={p.uuid}
tpp={p}
dispatch={this.props.dispatch} />;
})}
.map(p => <PointInventoryItem
key={p.uuid}
navName={"weeds"}
tpp={p}
hovered={this.props.hoveredPoint === p.uuid}
dispatch={this.props.dispatch} />)}
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;

View File

@ -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<DesignerState>(initialState)
s.hoveredPlantListItem = payload;
return s;
})
.add<CurrentPointPayl>(Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => {
.add<string | undefined>(Actions.TOGGLE_HOVERED_POINT, (s, { payload }) => {
s.hoveredPoint = payload;
return s;
})
.add<CurrentPointPayl | undefined>(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<CropLiveSearchResult[]>(Actions.OF_SEARCH_RESULTS_OK, (s, a) => {

View File

@ -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(

View File

@ -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<GenericPointer[]>(URL, QUERY);
const ids = resp.data.map(x => x.id);

View File

@ -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...") });
}

View File

@ -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,

View File

@ -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';

View File

@ -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