update points panels
parent
dedb98c15f
commit
6bd32c243c
|
@ -6,6 +6,7 @@ export const fakeDesignerState = (): DesignerState => ({
|
|||
plantUUID: undefined,
|
||||
icon: ""
|
||||
},
|
||||
hoveredPoint: undefined,
|
||||
hoveredPlantListItem: undefined,
|
||||
cropSearchQuery: "",
|
||||
cropSearchResults: [],
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -71,9 +71,8 @@ export interface GardenPlantState {
|
|||
export interface GardenPointProps {
|
||||
mapTransformProps: MapTransformProps;
|
||||
point: TaggedGenericPointer;
|
||||
}
|
||||
|
||||
export interface GardenPointState {
|
||||
hovered: boolean;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
interface DragHelpersBaseProps {
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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" } });
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,6 +18,7 @@ describe("<Points> />", () => {
|
|||
const fakeProps = (): PointsProps => ({
|
||||
points: [],
|
||||
dispatch: jest.fn(),
|
||||
hoveredPoint: undefined,
|
||||
});
|
||||
|
||||
it("renders no points", () => {
|
||||
|
|
|
@ -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()} />);
|
||||
|
|
|
@ -10,6 +10,7 @@ describe("<Weeds> />", () => {
|
|||
const fakeProps = (): WeedsProps => ({
|
||||
points: [],
|
||||
dispatch: jest.fn(),
|
||||
hoveredPoint: undefined,
|
||||
});
|
||||
|
||||
it("renders no points", () => {
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>;
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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...") });
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue