fix group edit mode actions
parent
fd867fca70
commit
6305e41e8f
|
@ -1,16 +1,13 @@
|
|||
jest.mock("../history",
|
||||
() => ({ push: jest.fn() }));
|
||||
jest.mock("../history", () => ({ push: jest.fn() }));
|
||||
const mockSyncThunk = jest.fn();
|
||||
jest.mock("../devices/actions",
|
||||
() => ({ sync: () => mockSyncThunk }));
|
||||
jest.mock("../farm_designer/actions",
|
||||
() => ({ unselectPlant: jest.fn() }));
|
||||
jest.mock("../devices/actions", () => ({ sync: () => mockSyncThunk }));
|
||||
jest.mock("../farm_designer/map/actions", () => ({ unselectPlant: jest.fn() }));
|
||||
|
||||
import { HotKeys } from "../hotkeys";
|
||||
import { betterCompact } from "../util";
|
||||
import { push } from "../history";
|
||||
import { sync } from "../devices/actions";
|
||||
import { unselectPlant } from "../farm_designer/actions";
|
||||
import { unselectPlant } from "../farm_designer/map/actions";
|
||||
|
||||
describe("hotkeys", () => {
|
||||
it("has key bindings", () => {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
NameInputBox, PinDropdown, ModeDropdown, DeleteButton,
|
||||
} from "../pin_form_fields";
|
||||
import { NameInputBox, PinDropdown, ModeDropdown } from "../pin_form_fields";
|
||||
import { fakeSensor } from "../../__test_support__/fake_state/resources";
|
||||
import { Actions } from "../../constants";
|
||||
import { FBSelect } from "../../ui";
|
||||
|
@ -64,17 +62,3 @@ describe("<ModeDropdown />", () => {
|
|||
expectedPayload({ mode: 0 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DeleteButton />", () => {
|
||||
const fakeProps = () => ({
|
||||
dispatch: jest.fn(() => Promise.resolve()),
|
||||
uuid: "resource uuid",
|
||||
});
|
||||
|
||||
it("deletes resource", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DeleteButton {...p} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,8 @@ import * as React from "react";
|
|||
import { PeripheralFormProps } from "./interfaces";
|
||||
import { sortResourcesById } from "../../util";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { NameInputBox, PinDropdown, DeleteButton } from "../pin_form_fields";
|
||||
import { DeleteButton } from "../../ui/delete_button";
|
||||
import { NameInputBox, PinDropdown } from "../pin_form_fields";
|
||||
|
||||
export const PeripheralForm = (props: PeripheralFormProps) =>
|
||||
<div className="peripheral-form">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { destroy, edit } from "../api/crud";
|
||||
import { edit } from "../api/crud";
|
||||
import { FBSelect } from "../ui";
|
||||
import {
|
||||
pinDropdowns
|
||||
|
@ -7,9 +7,8 @@ import {
|
|||
import { PIN_MODES } from "../sequences/step_tiles/tile_pin_support";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { TaggedPeripheral, TaggedSensor } from "farmbot";
|
||||
import { UUID } from "../resources/interfaces";
|
||||
import { isNumber } from "lodash";
|
||||
import { omit } from "lodash";
|
||||
|
||||
const MODES = (): { [s: string]: string } => ({
|
||||
0: t("Digital"),
|
||||
1: t("Analog")
|
||||
|
@ -58,35 +57,3 @@ export const ModeDropdown = (props: ModeDropdownProps) =>
|
|||
}))}
|
||||
selectedItem={{ label: MODES()[props.value], value: props.value }}
|
||||
list={PIN_MODES()} />;
|
||||
|
||||
interface ButtonCustomProps {
|
||||
dispatch: Function;
|
||||
uuid: UUID;
|
||||
children?: React.ReactChild
|
||||
onDestroy?: Function;
|
||||
}
|
||||
|
||||
type ButtonHtmlProps =
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
type DeleteButtonProps =
|
||||
ButtonCustomProps & ButtonHtmlProps;
|
||||
|
||||
/** Unfortunately, React will trigger a runtime
|
||||
* warning if we pass extra props to HTML elements */
|
||||
const OMIT_THESE: Record<keyof ButtonCustomProps, true> = {
|
||||
"dispatch": true,
|
||||
"uuid": true,
|
||||
"children": true,
|
||||
"onDestroy": true,
|
||||
};
|
||||
export const DeleteButton = (props: DeleteButtonProps) =>
|
||||
<button
|
||||
{...omit(props, Object.keys(OMIT_THESE))}
|
||||
className="red fb-button del-button"
|
||||
title={t("Delete")}
|
||||
onClick={() =>
|
||||
props.dispatch(destroy(props.uuid))
|
||||
.then(props.onDestroy || (() => { }))}>
|
||||
{props.children || <i className="fa fa-times" />}
|
||||
</button>;
|
||||
|
|
|
@ -2,9 +2,8 @@ import * as React from "react";
|
|||
import { SensorFormProps } from "./interfaces";
|
||||
import { sortResourcesById } from "../../util";
|
||||
import { Row, Col } from "../../ui";
|
||||
import {
|
||||
NameInputBox, PinDropdown, ModeDropdown, DeleteButton
|
||||
} from "../pin_form_fields";
|
||||
import { DeleteButton } from "../../ui/delete_button";
|
||||
import { NameInputBox, PinDropdown, ModeDropdown } from "../pin_form_fields";
|
||||
|
||||
export const SensorForm = (props: SensorFormProps) =>
|
||||
<div className="sensor-form">
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../history", () => ({
|
||||
history: { push: jest.fn() },
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
jest.mock("../../api/crud", () => ({
|
||||
edit: jest.fn()
|
||||
}));
|
||||
|
||||
import { movePlant, closePlantInfo, setDragIcon } from "../actions";
|
||||
import { MovePlantProps } from "../interfaces";
|
||||
import { fakePlant } from "../../__test_support__/fake_state/resources";
|
||||
import { edit } from "../../api/crud";
|
||||
import { Actions } from "../../constants";
|
||||
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
|
||||
import { history } from "../../history";
|
||||
|
||||
describe("movePlant", () => {
|
||||
function movePlantTest(
|
||||
caseDescription: string,
|
||||
attempted: { x: number, y: number },
|
||||
expected: { x: number, y: number }) {
|
||||
it(`restricts plant to grid area: ${caseDescription}`, () => {
|
||||
const payload: MovePlantProps = {
|
||||
deltaX: attempted.x,
|
||||
deltaY: attempted.y,
|
||||
plant: fakePlant(),
|
||||
gridSize: { x: 3000, y: 1500 }
|
||||
};
|
||||
movePlant(payload);
|
||||
expect(edit).toHaveBeenCalledWith(
|
||||
// Old plant
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
x: 100, y: 200
|
||||
})
|
||||
}),
|
||||
// Update
|
||||
expect.objectContaining({
|
||||
x: expected.x, y: expected.y
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
movePlantTest("within bounds", { x: 1, y: 2 }, { x: 101, y: 202 });
|
||||
movePlantTest("too high", { x: 10000, y: 10000 }, { x: 3000, y: 1500 });
|
||||
movePlantTest("too low", { x: -10000, y: -10000 }, { x: 0, y: 0 });
|
||||
});
|
||||
|
||||
describe("closePlantInfo()", () => {
|
||||
it("no plant info open", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("plant edit open", () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: undefined, type: Actions.SELECT_PLANT
|
||||
});
|
||||
});
|
||||
|
||||
it("plant info open", () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: undefined, type: Actions.SELECT_PLANT
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDragIcon()", () => {
|
||||
it("sets the drag icon", () => {
|
||||
const setDragImage = jest.fn();
|
||||
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
|
||||
setDragIcon("icon")(e);
|
||||
const img = new Image();
|
||||
img.src = svgToUrl("icon");
|
||||
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
|
||||
});
|
||||
|
||||
it("sets a default drag icon", () => {
|
||||
const setDragImage = jest.fn();
|
||||
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
|
||||
setDragIcon(undefined)(e);
|
||||
const img = new Image();
|
||||
img.src = DEFAULT_ICON;
|
||||
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
|
||||
});
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
jest.mock("../point_groups/group_detail", () => ({
|
||||
fetchGroupFromUrl: jest.fn(() => mockGroup)
|
||||
}));
|
||||
|
||||
jest.mock("../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
edit: jest.fn(),
|
||||
}));
|
||||
|
||||
let mockMode = "none";
|
||||
jest.mock("../map/util", () => ({ getMode: jest.fn(() => mockMode) }));
|
||||
|
||||
import {
|
||||
fakePlant, fakePointGroup
|
||||
} from "../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { GetState } from "../../redux/interfaces";
|
||||
import { clickMapPlant, selectPlant, toggleHoveredPlant } from "../actions";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../__test_support__/resource_index_builder";
|
||||
import { overwrite } from "../../api/crud";
|
||||
|
||||
const mockGroup = fakePointGroup();
|
||||
|
||||
describe("clickMapPlant", () => {
|
||||
// Base case
|
||||
it("selects plants and toggles hovered plant", () => {
|
||||
const state = fakeState();
|
||||
const dispatch = jest.fn();
|
||||
const getState: GetState = jest.fn(() => state);
|
||||
clickMapPlant("foo", "bar")(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith(selectPlant(["foo"]));
|
||||
expect(dispatch).toHaveBeenCalledWith(toggleHoveredPlant("foo", "bar"));
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("adds a point to current group if group editor is active", () => {
|
||||
mockMode = "addPointToGroup";
|
||||
const state = fakeState();
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 23;
|
||||
state.resources = buildResourceIndex([plant]);
|
||||
const dispatch = jest.fn();
|
||||
const getState: GetState = jest.fn(() => state);
|
||||
clickMapPlant(plant.uuid, "bar")(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith(selectPlant([plant.uuid]));
|
||||
expect(dispatch).toHaveBeenCalledWith(toggleHoveredPlant(plant.uuid, "bar"));
|
||||
const xp =
|
||||
expect.objectContaining({ name: "Fake", point_ids: [23] });
|
||||
expect(overwrite).toHaveBeenCalledWith(mockGroup, xp);
|
||||
expect(dispatch).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,158 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../../history", () => ({
|
||||
history: { push: jest.fn() },
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
overwrite: jest.fn(),
|
||||
}));
|
||||
|
||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
const mockGroup = fakePointGroup();
|
||||
jest.mock("../../point_groups/group_detail", () => ({
|
||||
fetchGroupFromUrl: jest.fn(() => mockGroup)
|
||||
}));
|
||||
|
||||
import {
|
||||
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPlant,
|
||||
setHoveredPlant
|
||||
} from "../actions";
|
||||
import { MovePlantProps } from "../../interfaces";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { edit, overwrite } from "../../../api/crud";
|
||||
import { Actions } from "../../../constants";
|
||||
import { DEFAULT_ICON, svgToUrl } from "../../../open_farm/icons";
|
||||
import { history } from "../../../history";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { GetState } from "../../../redux/interfaces";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("movePlant", () => {
|
||||
function movePlantTest(
|
||||
caseDescription: string,
|
||||
attempted: { x: number, y: number },
|
||||
expected: { x: number, y: number }) {
|
||||
it(`restricts plant to grid area: ${caseDescription}`, () => {
|
||||
const payload: MovePlantProps = {
|
||||
deltaX: attempted.x,
|
||||
deltaY: attempted.y,
|
||||
plant: fakePlant(),
|
||||
gridSize: { x: 3000, y: 1500 }
|
||||
};
|
||||
movePlant(payload);
|
||||
expect(edit).toHaveBeenCalledWith(
|
||||
// Old plant
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
x: 100, y: 200
|
||||
})
|
||||
}),
|
||||
// Update
|
||||
expect.objectContaining({
|
||||
x: expected.x, y: expected.y
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
movePlantTest("within bounds", { x: 1, y: 2 }, { x: 101, y: 202 });
|
||||
movePlantTest("too high", { x: 10000, y: 10000 }, { x: 3000, y: 1500 });
|
||||
movePlantTest("too low", { x: -10000, y: -10000 }, { x: 0, y: 0 });
|
||||
});
|
||||
|
||||
describe("closePlantInfo()", () => {
|
||||
it("no plant info open", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("plant edit open", () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: undefined, type: Actions.SELECT_PLANT
|
||||
});
|
||||
});
|
||||
|
||||
it("plant info open", () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: undefined, type: Actions.SELECT_PLANT
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDragIcon()", () => {
|
||||
it("sets the drag icon", () => {
|
||||
const setDragImage = jest.fn();
|
||||
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
|
||||
setDragIcon("icon")(e);
|
||||
const img = new Image();
|
||||
img.src = svgToUrl("icon");
|
||||
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
|
||||
});
|
||||
|
||||
it("sets a default drag icon", () => {
|
||||
const setDragImage = jest.fn();
|
||||
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
|
||||
setDragIcon(undefined)(e);
|
||||
const img = new Image();
|
||||
img.src = DEFAULT_ICON;
|
||||
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clickMapPlant", () => {
|
||||
it("selects plants and toggles hovered plant", () => {
|
||||
const state = fakeState();
|
||||
const dispatch = jest.fn();
|
||||
const getState: GetState = jest.fn(() => state);
|
||||
clickMapPlant("foo", "bar")(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith(selectPlant(["foo"]));
|
||||
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("foo", "bar"));
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("adds a point to current group if group editor is active", () => {
|
||||
mockPath = "/app/designer/groups/1";
|
||||
mockGroup.body.point_ids = [1];
|
||||
const state = fakeState();
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 23;
|
||||
state.resources = buildResourceIndex([plant]);
|
||||
const dispatch = jest.fn();
|
||||
const getState: GetState = jest.fn(() => state);
|
||||
clickMapPlant(plant.uuid, "bar")(dispatch, getState);
|
||||
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
|
||||
name: "Fake", point_ids: [1, 23]
|
||||
}));
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("removes a point from the current group if group editor is active", () => {
|
||||
mockPath = "/app/designer/groups/1";
|
||||
mockGroup.body.point_ids = [1, 2];
|
||||
const state = fakeState();
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 2;
|
||||
state.resources = buildResourceIndex([plant]);
|
||||
const dispatch = jest.fn();
|
||||
const getState: GetState = jest.fn(() => state);
|
||||
clickMapPlant(plant.uuid, "bar")(dispatch, getState);
|
||||
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
|
||||
name: "Fake", point_ids: [1]
|
||||
}));
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
jest.mock("../../actions", () => ({
|
||||
jest.mock("../actions", () => ({
|
||||
unselectPlant: jest.fn(() => jest.fn()),
|
||||
closePlantInfo: jest.fn(() => jest.fn()),
|
||||
}));
|
||||
|
@ -41,7 +41,7 @@ import { GardenMap } from "../garden_map";
|
|||
import { shallow, mount } from "enzyme";
|
||||
import { GardenMapProps } from "../../interfaces";
|
||||
import { setEggStatus, EggKeys } from "../easter_eggs/status";
|
||||
import { unselectPlant, closePlantInfo } from "../../actions";
|
||||
import { unselectPlant, closePlantInfo } from "../actions";
|
||||
import {
|
||||
dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant
|
||||
} from "../layers/plants/plant_actions";
|
||||
|
|
|
@ -348,7 +348,7 @@ describe("getMode()", () => {
|
|||
mockGardenOpen = true;
|
||||
expect(getMode()).toEqual(Mode.templateView);
|
||||
mockPath = "/app/designer/groups/1";
|
||||
expect(getMode()).toEqual(Mode.addPointToGroup);
|
||||
expect(getMode()).toEqual(Mode.editGroup);
|
||||
mockPath = "";
|
||||
mockGardenOpen = false;
|
||||
expect(getMode()).toEqual(Mode.none);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { MovePlantProps, DraggableEvent } from "./interfaces";
|
||||
import { defensiveClone } from "../util";
|
||||
import { edit, overwrite } from "../api/crud";
|
||||
import { history } from "../history";
|
||||
import { Actions } from "../constants";
|
||||
import { svgToUrl, DEFAULT_ICON } from "../open_farm/icons";
|
||||
import { Mode } from "./map/interfaces";
|
||||
import { MovePlantProps, DraggableEvent } from "../interfaces";
|
||||
import { defensiveClone } from "../../util";
|
||||
import { edit, overwrite } from "../../api/crud";
|
||||
import { history } from "../../history";
|
||||
import { Actions } from "../../constants";
|
||||
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
|
||||
import { Mode } from "../map/interfaces";
|
||||
import { clamp, uniq } from "lodash";
|
||||
import { GetState } from "../redux/interfaces";
|
||||
import { fetchGroupFromUrl } from "./point_groups/group_detail";
|
||||
import { GetState } from "../../redux/interfaces";
|
||||
import { fetchGroupFromUrl } from "../point_groups/group_detail";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
import { getMode } from "./map/util";
|
||||
import { getMode } from "../map/util";
|
||||
|
||||
export function movePlant(payload: MovePlantProps) {
|
||||
const tr = payload.plant;
|
||||
|
@ -25,8 +25,8 @@ export const selectPlant = (payload: string[] | undefined) => {
|
|||
return { type: Actions.SELECT_PLANT, payload };
|
||||
};
|
||||
|
||||
export const toggleHoveredPlant =
|
||||
(plantUUID: string | undefined, icon: string) => {
|
||||
export const setHoveredPlant =
|
||||
(plantUUID: string | undefined, icon = "") => {
|
||||
return {
|
||||
type: Actions.TOGGLE_HOVERED_PLANT,
|
||||
payload: { plantUUID, icon }
|
||||
|
@ -35,29 +35,33 @@ export const toggleHoveredPlant =
|
|||
|
||||
export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
|
||||
return (dispatch: Function, getState: GetState) => {
|
||||
dispatch(selectPlant([clickedPlantUuid]));
|
||||
dispatch(toggleHoveredPlant(clickedPlantUuid, icon));
|
||||
const isEditingGroup = getMode() === Mode.addPointToGroup;
|
||||
if (isEditingGroup) {
|
||||
if (getMode() === Mode.editGroup) {
|
||||
const { resources } = getState();
|
||||
const group = fetchGroupFromUrl(resources.index);
|
||||
const point =
|
||||
resources.index.references[clickedPlantUuid] as TaggedPoint | undefined;
|
||||
if (group && point && point.body.id) {
|
||||
type Body = (typeof group)["body"];
|
||||
const nextGroup: Body =
|
||||
({ ...group.body, point_ids: [...group.body.point_ids] });
|
||||
nextGroup.point_ids.push(point.body.id);
|
||||
const nextGroup: Body = ({
|
||||
...group.body,
|
||||
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
|
||||
});
|
||||
if (!group.body.point_ids.includes(point.body.id)) {
|
||||
nextGroup.point_ids.push(point.body.id);
|
||||
}
|
||||
nextGroup.point_ids = uniq(nextGroup.point_ids);
|
||||
dispatch(overwrite(group, nextGroup));
|
||||
}
|
||||
} else {
|
||||
dispatch(selectPlant([clickedPlantUuid]));
|
||||
dispatch(setHoveredPlant(clickedPlantUuid, icon));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const unselectPlant = (dispatch: Function) => () => {
|
||||
dispatch(selectPlant(undefined));
|
||||
dispatch(toggleHoveredPlant(undefined, ""));
|
||||
dispatch(setHoveredPlant(undefined));
|
||||
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@ import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
|
|||
import { SelectionBoxData } from "./selection_box";
|
||||
import { GardenMapState } from "../../interfaces";
|
||||
import { history } from "../../../history";
|
||||
import { selectPlant } from "../../actions";
|
||||
import { selectPlant } from "../actions";
|
||||
import { getMode } from "../util";
|
||||
|
||||
/** Return all plants within the selection box. */
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { closePlantInfo, unselectPlant } from "../actions";
|
||||
import { closePlantInfo, unselectPlant } from "./actions";
|
||||
import {
|
||||
MapTransformProps, TaggedPlant, Mode, AxisNumberProperty
|
||||
} from "./interfaces";
|
||||
|
|
|
@ -146,5 +146,5 @@ export enum Mode {
|
|||
points = "points",
|
||||
createPoint = "createPoint",
|
||||
templateView = "templateView",
|
||||
addPointToGroup = "addPointToGroup",
|
||||
editGroup = "editGroup",
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ jest.mock("../../../../../open_farm/cached_crop", () => ({
|
|||
cachedCrop: jest.fn(p => Promise.resolve({ spread: mockSpreads[p] })),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../actions", () => ({
|
||||
jest.mock("../../../actions", () => ({
|
||||
movePlant: jest.fn(),
|
||||
}));
|
||||
|
||||
|
@ -28,7 +28,7 @@ import { cachedCrop } from "../../../../../open_farm/cached_crop";
|
|||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { movePlant } from "../../../../actions";
|
||||
import { movePlant } from "../../../actions";
|
||||
import {
|
||||
fakeCropLiveSearchResult
|
||||
} from "../../../../../__test_support__/fake_crop_search_result";
|
||||
|
|
|
@ -6,7 +6,7 @@ import { DragHelpers } from "../../active_plant/drag_helpers";
|
|||
import { Color } from "../../../../ui/index";
|
||||
import { Actions } from "../../../../constants";
|
||||
import { cachedCrop } from "../../../../open_farm/cached_crop";
|
||||
import { clickMapPlant } from "../../../actions";
|
||||
import { clickMapPlant } from "../../actions";
|
||||
import { Circle } from "./circle";
|
||||
|
||||
export class GardenPlant extends
|
||||
|
|
|
@ -11,7 +11,7 @@ import { CropLiveSearchResult, GardenMapState } from "../../../interfaces";
|
|||
import { getPathArray } from "../../../../history";
|
||||
import { findBySlug } from "../../../search_selectors";
|
||||
import { transformXY, round, getZoomLevelFromMap } from "../../util";
|
||||
import { movePlant } from "../../../actions";
|
||||
import { movePlant } from "../../actions";
|
||||
import { cachedCrop } from "../../../../open_farm/cached_crop";
|
||||
import { t } from "../../../../i18next_wrapper";
|
||||
import { error } from "../../../../toast/toast";
|
||||
|
|
|
@ -44,7 +44,7 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
|
||||
key: p.uuid,
|
||||
};
|
||||
return getMode() === Mode.addPointToGroup
|
||||
return getMode() === Mode.editGroup
|
||||
? <g {...wrapperProps}>{plant}</g>
|
||||
: <Link {...wrapperProps}
|
||||
to={`/app/designer/${plantCategory}/${"" + p.body.id}`}>
|
||||
|
|
|
@ -291,9 +291,7 @@ export const transformForQuadrant =
|
|||
export const getMode = (): Mode => {
|
||||
const pathArray = getPathArray();
|
||||
if (pathArray) {
|
||||
if ((pathArray[3] === "groups") && pathArray[4]) {
|
||||
return Mode.addPointToGroup;
|
||||
}
|
||||
if ((pathArray[3] === "groups") && pathArray[4]) { return Mode.editGroup; }
|
||||
if (pathArray[6] === "add") { return Mode.clickToAdd; }
|
||||
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
|
||||
if (pathArray[5] === "edit") { return Mode.editPlant; }
|
||||
|
|
|
@ -9,7 +9,7 @@ import { AxisInputBox } from "../controls/axis_input_box";
|
|||
import { isNumber } from "lodash";
|
||||
import { Actions, Content } from "../constants";
|
||||
import { validBotLocationData } from "../util/util";
|
||||
import { unselectPlant } from "./actions";
|
||||
import { unselectPlant } from "./map/actions";
|
||||
import { AxisNumberProperty } from "./map/interfaces";
|
||||
import {
|
||||
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
|
||||
|
|
|
@ -6,7 +6,7 @@ jest.mock("../../../history", () => ({
|
|||
|
||||
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
|
||||
|
||||
jest.mock("../../actions", () => ({
|
||||
jest.mock("../../map/actions", () => ({
|
||||
unselectPlant: jest.fn(() => jest.fn()),
|
||||
setDragIcon: jest.fn(),
|
||||
}));
|
||||
|
@ -20,7 +20,7 @@ import { history } from "../../../history";
|
|||
import {
|
||||
fakeCropLiveSearchResult
|
||||
} from "../../../__test_support__/fake_crop_search_result";
|
||||
import { unselectPlant } from "../../actions";
|
||||
import { unselectPlant } from "../../map/actions";
|
||||
import { svgToUrl } from "../../../open_farm/icons";
|
||||
|
||||
describe("<CropInfo />", () => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Everything } from "../../interfaces";
|
|||
import { connect } from "react-redux";
|
||||
import { svgToUrl } from "../../open_farm/icons";
|
||||
import { CropLiveSearchResult, OpenfarmSearch } from "../interfaces";
|
||||
import { setDragIcon } from "../actions";
|
||||
import { setDragIcon } from "../map/actions";
|
||||
import { getCropHeaderProps, searchForCurrentCrop } from "./crop_info";
|
||||
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
|
||||
import { OFSearch } from "../util";
|
||||
|
|
|
@ -9,7 +9,7 @@ import { findBySlug } from "../search_selectors";
|
|||
import { Everything } from "../../interfaces";
|
||||
import { OpenFarm } from "../openfarm";
|
||||
import { OFSearch } from "../util";
|
||||
import { unselectPlant, setDragIcon } from "../actions";
|
||||
import { unselectPlant, setDragIcon } from "../map/actions";
|
||||
import { validBotLocationData } from "../../util";
|
||||
import { createPlant } from "../map/layers/plants/plant_actions";
|
||||
import { round } from "../map/util";
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { mapStateToProps, formatPlantInfo } from "./map_state_to_props";
|
||||
import { PlantPanel } from "./plant_panel";
|
||||
import { unselectPlant } from "../actions";
|
||||
import { unselectPlant } from "../map/actions";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
|
|
@ -7,7 +7,7 @@ import { get } from "lodash";
|
|||
import { unpackUUID } from "../../util";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { cachedCrop } from "../../open_farm/cached_crop";
|
||||
import { selectPlant, toggleHoveredPlant } from "../actions";
|
||||
import { selectPlant, setHoveredPlant } from "../map/actions";
|
||||
|
||||
type IMGEvent = React.SyntheticEvent<HTMLImageElement>;
|
||||
|
||||
|
@ -36,7 +36,7 @@ export class PlantInventoryItem extends
|
|||
const isEnter = action === "enter";
|
||||
const plantUUID = isEnter ? tpp.uuid : undefined;
|
||||
const icon = isEnter ? this.state.icon : "";
|
||||
dispatch(toggleHoveredPlant(plantUUID, icon));
|
||||
dispatch(setHoveredPlant(plantUUID, icon));
|
||||
};
|
||||
|
||||
const click = () => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from "react-redux";
|
|||
import { Everything } from "../../interfaces";
|
||||
import { PlantInventoryItem } from "./plant_inventory_item";
|
||||
import { destroy } from "../../api/crud";
|
||||
import { unselectPlant, selectPlant, toggleHoveredPlant } from "../actions";
|
||||
import { unselectPlant, selectPlant, setHoveredPlant } from "../map/actions";
|
||||
import { Actions, Content } from "../../constants";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { getPlants } from "../state_to_props";
|
||||
|
@ -36,7 +36,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
|||
if (selected && selected.length == 1) {
|
||||
unselectPlant(dispatch)();
|
||||
} else {
|
||||
dispatch(toggleHoveredPlant(undefined, ""));
|
||||
dispatch(setHoveredPlant(undefined));
|
||||
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@ jest.mock("../../../api/crud", () => ({
|
|||
edit: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../actions", () => ({
|
||||
toggleHoveredPlant: jest.fn()
|
||||
}));
|
||||
jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../account/dev/dev_support", () => ({
|
||||
|
|
|
@ -71,7 +71,7 @@ describe("nearest neighbor algorithm", () => {
|
|||
const p4 = fakePlant();
|
||||
p4.body.x = 1000;
|
||||
p4.body.y = 150;
|
||||
const points = nn([p4, p2, p3, p1]);
|
||||
const points = nn([p4, p2, p3, p1, p1]);
|
||||
expect(points).toEqual([p1, p2, p3, p4]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
jest.mock("../../actions", () => ({ toggleHoveredPlant: jest.fn() }));
|
||||
jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
|
||||
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
|
||||
|
||||
import React from "react";
|
||||
import { PointGroupItem } from "../point_group_item";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant, fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakePlant, fakePointGroup
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { DeepPartial } from "redux";
|
||||
import { cachedCrop } from "../../../open_farm/cached_crop";
|
||||
import { toggleHoveredPlant } from "../../actions";
|
||||
import { setHoveredPlant } from "../../map/actions";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
|
||||
describe("<PointGroupItem/>", () => {
|
||||
|
@ -51,8 +53,7 @@ describe("<PointGroupItem/>", () => {
|
|||
i.state.icon = "X";
|
||||
i.enter();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(toggleHoveredPlant)
|
||||
.toHaveBeenCalledWith(i.props.plant.uuid, "X");
|
||||
expect(setHoveredPlant).toHaveBeenCalledWith(i.props.plant.uuid, "X");
|
||||
});
|
||||
|
||||
it("handles mouse exit", () => {
|
||||
|
@ -60,13 +61,13 @@ describe("<PointGroupItem/>", () => {
|
|||
i.state.icon = "X";
|
||||
i.leave();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(toggleHoveredPlant).toHaveBeenCalledWith(undefined, "");
|
||||
expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("handles clicks", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
i.click();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(overwrite).toHaveBeenCalledWith({
|
||||
body: { name: "Fake", point_ids: [], sort_type: "xy_ascending" },
|
||||
kind: "PointGroup",
|
||||
|
@ -77,5 +78,6 @@ describe("<PointGroupItem/>", () => {
|
|||
point_ids: [],
|
||||
sort_type: "xy_ascending",
|
||||
});
|
||||
expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
DesignerPanelHeader
|
||||
} from "../plants/designer_panel";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { DeleteButton } from "../../controls/pin_form_fields";
|
||||
import { DeleteButton } from "../../ui/delete_button";
|
||||
import { save, edit } from "../../api/crud";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
|
||||
|
|
|
@ -40,6 +40,7 @@ export const nn = (points: TaggedPlant[]) => {
|
|||
const ordered: TaggedPlant[] = [];
|
||||
let from = { x: 0, y: 0 };
|
||||
points.map(() => {
|
||||
if (available.length < 1) { return; }
|
||||
const nearest = findNearest(from, available);
|
||||
ordered.push(nearest);
|
||||
from = { x: nearest.body.x, y: nearest.body.y };
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { cachedCrop } from "../../open_farm/cached_crop";
|
||||
import { toggleHoveredPlant } from "../actions";
|
||||
import { setHoveredPlant } from "../map/actions";
|
||||
import { TaggedPointGroup, uuid } from "farmbot";
|
||||
import { overwrite } from "../../api/crud";
|
||||
|
||||
|
@ -25,23 +25,23 @@ const removePoint = (group: TaggedPointGroup, pointId: number) => {
|
|||
};
|
||||
|
||||
// The individual plants in the point group detail page.
|
||||
export class PointGroupItem extends React.Component<PointGroupItemProps, PointGroupItemState> {
|
||||
export class PointGroupItem
|
||||
extends React.Component<PointGroupItemProps, PointGroupItemState> {
|
||||
|
||||
state: PointGroupItemState = { icon: "" };
|
||||
|
||||
key = uuid();
|
||||
|
||||
enter = () => this
|
||||
.props
|
||||
.dispatch(toggleHoveredPlant(this.props.plant.uuid, this.state.icon));
|
||||
enter = () => this.props.dispatch(
|
||||
setHoveredPlant(this.props.plant.uuid, this.state.icon));
|
||||
|
||||
leave = () => this
|
||||
.props
|
||||
.dispatch(toggleHoveredPlant(undefined, ""));
|
||||
leave = () => this.props.dispatch(setHoveredPlant(undefined));
|
||||
|
||||
click = () => this
|
||||
.props
|
||||
.dispatch(removePoint(this.props.group, this.props.plant.body.id || 0));
|
||||
click = () => {
|
||||
this.props.dispatch(
|
||||
removePoint(this.props.group, this.props.plant.body.id || 0));
|
||||
this.leave();
|
||||
}
|
||||
|
||||
maybeGetCachedIcon = ({ currentTarget }: IMGEvent) => {
|
||||
return cachedCrop(this.props.plant.body.openfarm_slug).then((crop) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Everything } from "../../interfaces";
|
||||
import { connect } from "react-redux";
|
||||
import { history } from "../../history";
|
||||
import { unselectPlant } from "../actions";
|
||||
import { unselectPlant } from "../map/actions";
|
||||
import {
|
||||
selectAllSavedGardens, selectAllPlantTemplates, selectAllPlantPointers
|
||||
} from "../../resources/selectors";
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
jest.mock("../../i18n", () => {
|
||||
return { detectLanguage: jest.fn(() => Promise.resolve()) };
|
||||
});
|
||||
jest.mock("../../i18n", () => ({
|
||||
detectLanguage: jest.fn(() => Promise.resolve())
|
||||
}));
|
||||
|
||||
jest.mock("../../util/stop_ie", () => {
|
||||
return { stopIE: jest.fn() };
|
||||
});
|
||||
jest.mock("../../util/stop_ie", () => ({ stopIE: jest.fn() }));
|
||||
|
||||
jest.mock("../../util",
|
||||
() => ({ attachToRoot: jest.fn(), trim: (s: string) => s }));
|
||||
jest.mock("../../util", () => ({
|
||||
attachToRoot: jest.fn(),
|
||||
trim: (s: string) => s,
|
||||
}));
|
||||
|
||||
import { detectLanguage } from "../../i18n";
|
||||
import { stopIE } from "../../util/stop_ie";
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from "@blueprintjs/core";
|
||||
import { findIndex } from "lodash";
|
||||
import { t } from "./i18next_wrapper";
|
||||
import { unselectPlant } from "./farm_designer/actions";
|
||||
import { unselectPlant } from "./farm_designer/map/actions";
|
||||
|
||||
interface Props {
|
||||
dispatch: Function;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { DeleteButton } from "../delete_button";
|
||||
|
||||
describe("<DeleteButton />", () => {
|
||||
const fakeProps = () => ({
|
||||
dispatch: jest.fn(() => Promise.resolve()),
|
||||
uuid: "resource uuid",
|
||||
});
|
||||
|
||||
it("deletes resource", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DeleteButton {...p} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import * as React from "react";
|
||||
import { destroy } from "../api/crud";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { UUID } from "../resources/interfaces";
|
||||
import { omit } from "lodash";
|
||||
|
||||
interface ButtonCustomProps {
|
||||
dispatch: Function;
|
||||
uuid: UUID;
|
||||
children?: React.ReactChild
|
||||
onDestroy?: Function;
|
||||
}
|
||||
|
||||
type ButtonHtmlProps =
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
type DeleteButtonProps =
|
||||
ButtonCustomProps & ButtonHtmlProps;
|
||||
|
||||
/** Unfortunately, React will trigger a runtime
|
||||
* warning if we pass extra props to HTML elements */
|
||||
const OMIT_THESE: Record<keyof ButtonCustomProps, true> = {
|
||||
"dispatch": true,
|
||||
"uuid": true,
|
||||
"children": true,
|
||||
"onDestroy": true,
|
||||
};
|
||||
|
||||
export const DeleteButton = (props: DeleteButtonProps) =>
|
||||
<button
|
||||
{...omit(props, Object.keys(OMIT_THESE))}
|
||||
className="red fb-button del-button"
|
||||
title={t("Delete")}
|
||||
onClick={() =>
|
||||
props.dispatch(destroy(props.uuid))
|
||||
.then(props.onDestroy || (() => { }))}>
|
||||
{props.children || <i className="fa fa-times" />}
|
||||
</button>;
|
|
@ -1,11 +1,22 @@
|
|||
export * from "./back_arrow";
|
||||
export * from "./blurable_input";
|
||||
export * from "./colors";
|
||||
export * from "./center_panel";
|
||||
export * from "./color_picker";
|
||||
export * from "./colors";
|
||||
export * from "./column";
|
||||
// export * from "./delete_button";
|
||||
export * from "./doc_link";
|
||||
export * from "./empty_state_wrapper";
|
||||
export * from "./expandable_header";
|
||||
export * from "./fallback_img";
|
||||
export * from "./fb_select";
|
||||
export * from "./help";
|
||||
export * from "./page";
|
||||
export * from "./input_error";
|
||||
export * from "./left_panel";
|
||||
export * from "./markdown";
|
||||
export * from "./new_fb_select";
|
||||
export * from "./page";
|
||||
export * from "./right_panel";
|
||||
export * from "./row";
|
||||
export * from "./saucer";
|
||||
export * from "./save_button";
|
||||
|
@ -14,10 +25,3 @@ export * from "./widget";
|
|||
export * from "./widget_header";
|
||||
export * from "./widget_footer";
|
||||
export * from "./widget_body";
|
||||
export * from "./fb_select";
|
||||
export * from "./new_fb_select";
|
||||
export * from "./fallback_img";
|
||||
export * from "./doc_link";
|
||||
export * from "./left_panel";
|
||||
export * from "./center_panel";
|
||||
export * from "./right_panel";
|
||||
|
|
Loading…
Reference in New Issue