fix group edit mode actions

pull/1581/head
gabrielburnworth 2019-11-22 11:57:22 -08:00
parent fd867fca70
commit 6305e41e8f
38 changed files with 317 additions and 302 deletions

View File

@ -1,16 +1,13 @@
jest.mock("../history", jest.mock("../history", () => ({ push: jest.fn() }));
() => ({ push: jest.fn() }));
const mockSyncThunk = jest.fn(); const mockSyncThunk = jest.fn();
jest.mock("../devices/actions", jest.mock("../devices/actions", () => ({ sync: () => mockSyncThunk }));
() => ({ sync: () => mockSyncThunk })); jest.mock("../farm_designer/map/actions", () => ({ unselectPlant: jest.fn() }));
jest.mock("../farm_designer/actions",
() => ({ unselectPlant: jest.fn() }));
import { HotKeys } from "../hotkeys"; import { HotKeys } from "../hotkeys";
import { betterCompact } from "../util"; import { betterCompact } from "../util";
import { push } from "../history"; import { push } from "../history";
import { sync } from "../devices/actions"; import { sync } from "../devices/actions";
import { unselectPlant } from "../farm_designer/actions"; import { unselectPlant } from "../farm_designer/map/actions";
describe("hotkeys", () => { describe("hotkeys", () => {
it("has key bindings", () => { it("has key bindings", () => {

View File

@ -1,8 +1,6 @@
import * as React from "react"; import * as React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { import { NameInputBox, PinDropdown, ModeDropdown } from "../pin_form_fields";
NameInputBox, PinDropdown, ModeDropdown, DeleteButton,
} from "../pin_form_fields";
import { fakeSensor } from "../../__test_support__/fake_state/resources"; import { fakeSensor } from "../../__test_support__/fake_state/resources";
import { Actions } from "../../constants"; import { Actions } from "../../constants";
import { FBSelect } from "../../ui"; import { FBSelect } from "../../ui";
@ -64,17 +62,3 @@ describe("<ModeDropdown />", () => {
expectedPayload({ mode: 0 })); 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));
});
});

View File

@ -2,7 +2,8 @@ import * as React from "react";
import { PeripheralFormProps } from "./interfaces"; import { PeripheralFormProps } from "./interfaces";
import { sortResourcesById } from "../../util"; import { sortResourcesById } from "../../util";
import { Row, Col } from "../../ui"; 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) => export const PeripheralForm = (props: PeripheralFormProps) =>
<div className="peripheral-form"> <div className="peripheral-form">

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { destroy, edit } from "../api/crud"; import { edit } from "../api/crud";
import { FBSelect } from "../ui"; import { FBSelect } from "../ui";
import { import {
pinDropdowns pinDropdowns
@ -7,9 +7,8 @@ import {
import { PIN_MODES } from "../sequences/step_tiles/tile_pin_support"; import { PIN_MODES } from "../sequences/step_tiles/tile_pin_support";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { TaggedPeripheral, TaggedSensor } from "farmbot"; import { TaggedPeripheral, TaggedSensor } from "farmbot";
import { UUID } from "../resources/interfaces";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
import { omit } from "lodash";
const MODES = (): { [s: string]: string } => ({ const MODES = (): { [s: string]: string } => ({
0: t("Digital"), 0: t("Digital"),
1: t("Analog") 1: t("Analog")
@ -58,35 +57,3 @@ export const ModeDropdown = (props: ModeDropdownProps) =>
}))} }))}
selectedItem={{ label: MODES()[props.value], value: props.value }} selectedItem={{ label: MODES()[props.value], value: props.value }}
list={PIN_MODES()} />; 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>;

View File

@ -2,9 +2,8 @@ import * as React from "react";
import { SensorFormProps } from "./interfaces"; import { SensorFormProps } from "./interfaces";
import { sortResourcesById } from "../../util"; import { sortResourcesById } from "../../util";
import { Row, Col } from "../../ui"; import { Row, Col } from "../../ui";
import { import { DeleteButton } from "../../ui/delete_button";
NameInputBox, PinDropdown, ModeDropdown, DeleteButton import { NameInputBox, PinDropdown, ModeDropdown } from "../pin_form_fields";
} from "../pin_form_fields";
export const SensorForm = (props: SensorFormProps) => export const SensorForm = (props: SensorFormProps) =>
<div className="sensor-form"> <div className="sensor-form">

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
jest.mock("../../actions", () => ({ jest.mock("../actions", () => ({
unselectPlant: jest.fn(() => jest.fn()), unselectPlant: jest.fn(() => jest.fn()),
closePlantInfo: jest.fn(() => jest.fn()), closePlantInfo: jest.fn(() => jest.fn()),
})); }));
@ -41,7 +41,7 @@ import { GardenMap } from "../garden_map";
import { shallow, mount } from "enzyme"; import { shallow, mount } from "enzyme";
import { GardenMapProps } from "../../interfaces"; import { GardenMapProps } from "../../interfaces";
import { setEggStatus, EggKeys } from "../easter_eggs/status"; import { setEggStatus, EggKeys } from "../easter_eggs/status";
import { unselectPlant, closePlantInfo } from "../../actions"; import { unselectPlant, closePlantInfo } from "../actions";
import { import {
dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant
} from "../layers/plants/plant_actions"; } from "../layers/plants/plant_actions";

View File

@ -348,7 +348,7 @@ describe("getMode()", () => {
mockGardenOpen = true; mockGardenOpen = true;
expect(getMode()).toEqual(Mode.templateView); expect(getMode()).toEqual(Mode.templateView);
mockPath = "/app/designer/groups/1"; mockPath = "/app/designer/groups/1";
expect(getMode()).toEqual(Mode.addPointToGroup); expect(getMode()).toEqual(Mode.editGroup);
mockPath = ""; mockPath = "";
mockGardenOpen = false; mockGardenOpen = false;
expect(getMode()).toEqual(Mode.none); expect(getMode()).toEqual(Mode.none);

View File

@ -1,15 +1,15 @@
import { MovePlantProps, DraggableEvent } from "./interfaces"; import { MovePlantProps, DraggableEvent } from "../interfaces";
import { defensiveClone } from "../util"; import { defensiveClone } from "../../util";
import { edit, overwrite } from "../api/crud"; import { edit, overwrite } from "../../api/crud";
import { history } from "../history"; import { history } from "../../history";
import { Actions } from "../constants"; import { Actions } from "../../constants";
import { svgToUrl, DEFAULT_ICON } from "../open_farm/icons"; import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
import { Mode } from "./map/interfaces"; import { Mode } from "../map/interfaces";
import { clamp, uniq } from "lodash"; import { clamp, uniq } from "lodash";
import { GetState } from "../redux/interfaces"; import { GetState } from "../../redux/interfaces";
import { fetchGroupFromUrl } from "./point_groups/group_detail"; import { fetchGroupFromUrl } from "../point_groups/group_detail";
import { TaggedPoint } from "farmbot"; import { TaggedPoint } from "farmbot";
import { getMode } from "./map/util"; import { getMode } from "../map/util";
export function movePlant(payload: MovePlantProps) { export function movePlant(payload: MovePlantProps) {
const tr = payload.plant; const tr = payload.plant;
@ -25,8 +25,8 @@ export const selectPlant = (payload: string[] | undefined) => {
return { type: Actions.SELECT_PLANT, payload }; return { type: Actions.SELECT_PLANT, payload };
}; };
export const toggleHoveredPlant = export const setHoveredPlant =
(plantUUID: string | undefined, icon: string) => { (plantUUID: string | undefined, icon = "") => {
return { return {
type: Actions.TOGGLE_HOVERED_PLANT, type: Actions.TOGGLE_HOVERED_PLANT,
payload: { plantUUID, icon } payload: { plantUUID, icon }
@ -35,29 +35,33 @@ export const toggleHoveredPlant =
export const clickMapPlant = (clickedPlantUuid: string, icon: string) => { export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
return (dispatch: Function, getState: GetState) => { return (dispatch: Function, getState: GetState) => {
dispatch(selectPlant([clickedPlantUuid])); if (getMode() === Mode.editGroup) {
dispatch(toggleHoveredPlant(clickedPlantUuid, icon));
const isEditingGroup = getMode() === Mode.addPointToGroup;
if (isEditingGroup) {
const { resources } = getState(); const { resources } = getState();
const group = fetchGroupFromUrl(resources.index); const group = fetchGroupFromUrl(resources.index);
const point = const point =
resources.index.references[clickedPlantUuid] as TaggedPoint | undefined; resources.index.references[clickedPlantUuid] as TaggedPoint | undefined;
if (group && point && point.body.id) { if (group && point && point.body.id) {
type Body = (typeof group)["body"]; type Body = (typeof group)["body"];
const nextGroup: Body = const nextGroup: Body = ({
({ ...group.body, point_ids: [...group.body.point_ids] }); ...group.body,
nextGroup.point_ids.push(point.body.id); 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); nextGroup.point_ids = uniq(nextGroup.point_ids);
dispatch(overwrite(group, nextGroup)); dispatch(overwrite(group, nextGroup));
} }
} else {
dispatch(selectPlant([clickedPlantUuid]));
dispatch(setHoveredPlant(clickedPlantUuid, icon));
} }
}; };
}; };
export const unselectPlant = (dispatch: Function) => () => { export const unselectPlant = (dispatch: Function) => () => {
dispatch(selectPlant(undefined)); dispatch(selectPlant(undefined));
dispatch(toggleHoveredPlant(undefined, "")); dispatch(setHoveredPlant(undefined));
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined }); dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
}; };

View File

@ -3,7 +3,7 @@ import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
import { SelectionBoxData } from "./selection_box"; import { SelectionBoxData } from "./selection_box";
import { GardenMapState } from "../../interfaces"; import { GardenMapState } from "../../interfaces";
import { history } from "../../../history"; import { history } from "../../../history";
import { selectPlant } from "../../actions"; import { selectPlant } from "../actions";
import { getMode } from "../util"; import { getMode } from "../util";
/** Return all plants within the selection box. */ /** Return all plants within the selection box. */

View File

@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { BooleanSetting } from "../../session_keys"; import { BooleanSetting } from "../../session_keys";
import { closePlantInfo, unselectPlant } from "../actions"; import { closePlantInfo, unselectPlant } from "./actions";
import { import {
MapTransformProps, TaggedPlant, Mode, AxisNumberProperty MapTransformProps, TaggedPlant, Mode, AxisNumberProperty
} from "./interfaces"; } from "./interfaces";

View File

@ -146,5 +146,5 @@ export enum Mode {
points = "points", points = "points",
createPoint = "createPoint", createPoint = "createPoint",
templateView = "templateView", templateView = "templateView",
addPointToGroup = "addPointToGroup", editGroup = "editGroup",
} }

View File

@ -9,7 +9,7 @@ jest.mock("../../../../../open_farm/cached_crop", () => ({
cachedCrop: jest.fn(p => Promise.resolve({ spread: mockSpreads[p] })), cachedCrop: jest.fn(p => Promise.resolve({ spread: mockSpreads[p] })),
})); }));
jest.mock("../../../../actions", () => ({ jest.mock("../../../actions", () => ({
movePlant: jest.fn(), movePlant: jest.fn(),
})); }));
@ -28,7 +28,7 @@ import { cachedCrop } from "../../../../../open_farm/cached_crop";
import { import {
fakeMapTransformProps fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props"; } from "../../../../../__test_support__/map_transform_props";
import { movePlant } from "../../../../actions"; import { movePlant } from "../../../actions";
import { import {
fakeCropLiveSearchResult fakeCropLiveSearchResult
} from "../../../../../__test_support__/fake_crop_search_result"; } from "../../../../../__test_support__/fake_crop_search_result";

View File

@ -6,7 +6,7 @@ import { DragHelpers } from "../../active_plant/drag_helpers";
import { Color } from "../../../../ui/index"; import { Color } from "../../../../ui/index";
import { Actions } from "../../../../constants"; import { Actions } from "../../../../constants";
import { cachedCrop } from "../../../../open_farm/cached_crop"; import { cachedCrop } from "../../../../open_farm/cached_crop";
import { clickMapPlant } from "../../../actions"; import { clickMapPlant } from "../../actions";
import { Circle } from "./circle"; import { Circle } from "./circle";
export class GardenPlant extends export class GardenPlant extends

View File

@ -11,7 +11,7 @@ import { CropLiveSearchResult, GardenMapState } from "../../../interfaces";
import { getPathArray } from "../../../../history"; import { getPathArray } from "../../../../history";
import { findBySlug } from "../../../search_selectors"; import { findBySlug } from "../../../search_selectors";
import { transformXY, round, getZoomLevelFromMap } from "../../util"; import { transformXY, round, getZoomLevelFromMap } from "../../util";
import { movePlant } from "../../../actions"; import { movePlant } from "../../actions";
import { cachedCrop } from "../../../../open_farm/cached_crop"; import { cachedCrop } from "../../../../open_farm/cached_crop";
import { t } from "../../../../i18next_wrapper"; import { t } from "../../../../i18next_wrapper";
import { error } from "../../../../toast/toast"; import { error } from "../../../../toast/toast";

View File

@ -44,7 +44,7 @@ export function PlantLayer(props: PlantLayerProps) {
style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }), style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
key: p.uuid, key: p.uuid,
}; };
return getMode() === Mode.addPointToGroup return getMode() === Mode.editGroup
? <g {...wrapperProps}>{plant}</g> ? <g {...wrapperProps}>{plant}</g>
: <Link {...wrapperProps} : <Link {...wrapperProps}
to={`/app/designer/${plantCategory}/${"" + p.body.id}`}> to={`/app/designer/${plantCategory}/${"" + p.body.id}`}>

View File

@ -291,9 +291,7 @@ export const transformForQuadrant =
export const getMode = (): Mode => { export const getMode = (): Mode => {
const pathArray = getPathArray(); const pathArray = getPathArray();
if (pathArray) { if (pathArray) {
if ((pathArray[3] === "groups") && pathArray[4]) { if ((pathArray[3] === "groups") && pathArray[4]) { return Mode.editGroup; }
return Mode.addPointToGroup;
}
if (pathArray[6] === "add") { return Mode.clickToAdd; } if (pathArray[6] === "add") { return Mode.clickToAdd; }
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; } if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
if (pathArray[5] === "edit") { return Mode.editPlant; } if (pathArray[5] === "edit") { return Mode.editPlant; }

View File

@ -9,7 +9,7 @@ import { AxisInputBox } from "../controls/axis_input_box";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
import { Actions, Content } from "../constants"; import { Actions, Content } from "../constants";
import { validBotLocationData } from "../util/util"; import { validBotLocationData } from "../util/util";
import { unselectPlant } from "./actions"; import { unselectPlant } from "./map/actions";
import { AxisNumberProperty } from "./map/interfaces"; import { AxisNumberProperty } from "./map/interfaces";
import { import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader DesignerPanel, DesignerPanelContent, DesignerPanelHeader

View File

@ -6,7 +6,7 @@ jest.mock("../../../history", () => ({
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() })); jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
jest.mock("../../actions", () => ({ jest.mock("../../map/actions", () => ({
unselectPlant: jest.fn(() => jest.fn()), unselectPlant: jest.fn(() => jest.fn()),
setDragIcon: jest.fn(), setDragIcon: jest.fn(),
})); }));
@ -20,7 +20,7 @@ import { history } from "../../../history";
import { import {
fakeCropLiveSearchResult fakeCropLiveSearchResult
} from "../../../__test_support__/fake_crop_search_result"; } from "../../../__test_support__/fake_crop_search_result";
import { unselectPlant } from "../../actions"; import { unselectPlant } from "../../map/actions";
import { svgToUrl } from "../../../open_farm/icons"; import { svgToUrl } from "../../../open_farm/icons";
describe("<CropInfo />", () => { describe("<CropInfo />", () => {

View File

@ -3,7 +3,7 @@ import { Everything } from "../../interfaces";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { svgToUrl } from "../../open_farm/icons"; import { svgToUrl } from "../../open_farm/icons";
import { CropLiveSearchResult, OpenfarmSearch } from "../interfaces"; import { CropLiveSearchResult, OpenfarmSearch } from "../interfaces";
import { setDragIcon } from "../actions"; import { setDragIcon } from "../map/actions";
import { getCropHeaderProps, searchForCurrentCrop } from "./crop_info"; import { getCropHeaderProps, searchForCurrentCrop } from "./crop_info";
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel"; import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
import { OFSearch } from "../util"; import { OFSearch } from "../util";

View File

@ -9,7 +9,7 @@ import { findBySlug } from "../search_selectors";
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { OpenFarm } from "../openfarm"; import { OpenFarm } from "../openfarm";
import { OFSearch } from "../util"; import { OFSearch } from "../util";
import { unselectPlant, setDragIcon } from "../actions"; import { unselectPlant, setDragIcon } from "../map/actions";
import { validBotLocationData } from "../../util"; import { validBotLocationData } from "../../util";
import { createPlant } from "../map/layers/plants/plant_actions"; import { createPlant } from "../map/layers/plants/plant_actions";
import { round } from "../map/util"; import { round } from "../map/util";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { mapStateToProps, formatPlantInfo } from "./map_state_to_props"; import { mapStateToProps, formatPlantInfo } from "./map_state_to_props";
import { PlantPanel } from "./plant_panel"; import { PlantPanel } from "./plant_panel";
import { unselectPlant } from "../actions"; import { unselectPlant } from "../map/actions";
import { TaggedPlant } from "../map/interfaces"; import { TaggedPlant } from "../map/interfaces";
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel"; import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";

View File

@ -7,7 +7,7 @@ import { get } from "lodash";
import { unpackUUID } from "../../util"; import { unpackUUID } from "../../util";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { cachedCrop } from "../../open_farm/cached_crop"; import { cachedCrop } from "../../open_farm/cached_crop";
import { selectPlant, toggleHoveredPlant } from "../actions"; import { selectPlant, setHoveredPlant } from "../map/actions";
type IMGEvent = React.SyntheticEvent<HTMLImageElement>; type IMGEvent = React.SyntheticEvent<HTMLImageElement>;
@ -36,7 +36,7 @@ export class PlantInventoryItem extends
const isEnter = action === "enter"; const isEnter = action === "enter";
const plantUUID = isEnter ? tpp.uuid : undefined; const plantUUID = isEnter ? tpp.uuid : undefined;
const icon = isEnter ? this.state.icon : ""; const icon = isEnter ? this.state.icon : "";
dispatch(toggleHoveredPlant(plantUUID, icon)); dispatch(setHoveredPlant(plantUUID, icon));
}; };
const click = () => { const click = () => {

View File

@ -4,7 +4,7 @@ import { connect } from "react-redux";
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { PlantInventoryItem } from "./plant_inventory_item"; import { PlantInventoryItem } from "./plant_inventory_item";
import { destroy } from "../../api/crud"; import { destroy } from "../../api/crud";
import { unselectPlant, selectPlant, toggleHoveredPlant } from "../actions"; import { unselectPlant, selectPlant, setHoveredPlant } from "../map/actions";
import { Actions, Content } from "../../constants"; import { Actions, Content } from "../../constants";
import { TaggedPlant } from "../map/interfaces"; import { TaggedPlant } from "../map/interfaces";
import { getPlants } from "../state_to_props"; import { getPlants } from "../state_to_props";
@ -36,7 +36,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
if (selected && selected.length == 1) { if (selected && selected.length == 1) {
unselectPlant(dispatch)(); unselectPlant(dispatch)();
} else { } else {
dispatch(toggleHoveredPlant(undefined, "")); dispatch(setHoveredPlant(undefined));
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined }); dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
} }
} }

View File

@ -4,9 +4,7 @@ jest.mock("../../../api/crud", () => ({
edit: jest.fn(), edit: jest.fn(),
})); }));
jest.mock("../../actions", () => ({ jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
toggleHoveredPlant: jest.fn()
}));
let mockDev = false; let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({ jest.mock("../../../account/dev/dev_support", () => ({

View File

@ -71,7 +71,7 @@ describe("nearest neighbor algorithm", () => {
const p4 = fakePlant(); const p4 = fakePlant();
p4.body.x = 1000; p4.body.x = 1000;
p4.body.y = 150; 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]); expect(points).toEqual([p1, p2, p3, p4]);
}); });
}); });

View File

@ -1,13 +1,15 @@
jest.mock("../../actions", () => ({ toggleHoveredPlant: jest.fn() })); jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() })); jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
import React from "react"; import React from "react";
import { PointGroupItem } from "../point_group_item"; import { PointGroupItem } from "../point_group_item";
import { shallow } from "enzyme"; 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 { DeepPartial } from "redux";
import { cachedCrop } from "../../../open_farm/cached_crop"; import { cachedCrop } from "../../../open_farm/cached_crop";
import { toggleHoveredPlant } from "../../actions"; import { setHoveredPlant } from "../../map/actions";
import { overwrite } from "../../../api/crud"; import { overwrite } from "../../../api/crud";
describe("<PointGroupItem/>", () => { describe("<PointGroupItem/>", () => {
@ -51,8 +53,7 @@ describe("<PointGroupItem/>", () => {
i.state.icon = "X"; i.state.icon = "X";
i.enter(); i.enter();
expect(i.props.dispatch).toHaveBeenCalledTimes(1); expect(i.props.dispatch).toHaveBeenCalledTimes(1);
expect(toggleHoveredPlant) expect(setHoveredPlant).toHaveBeenCalledWith(i.props.plant.uuid, "X");
.toHaveBeenCalledWith(i.props.plant.uuid, "X");
}); });
it("handles mouse exit", () => { it("handles mouse exit", () => {
@ -60,13 +61,13 @@ describe("<PointGroupItem/>", () => {
i.state.icon = "X"; i.state.icon = "X";
i.leave(); i.leave();
expect(i.props.dispatch).toHaveBeenCalledTimes(1); expect(i.props.dispatch).toHaveBeenCalledTimes(1);
expect(toggleHoveredPlant).toHaveBeenCalledWith(undefined, ""); expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
}); });
it("handles clicks", () => { it("handles clicks", () => {
const i = new PointGroupItem(newProps()); const i = new PointGroupItem(newProps());
i.click(); i.click();
expect(i.props.dispatch).toHaveBeenCalledTimes(1); expect(i.props.dispatch).toHaveBeenCalledTimes(2);
expect(overwrite).toHaveBeenCalledWith({ expect(overwrite).toHaveBeenCalledWith({
body: { name: "Fake", point_ids: [], sort_type: "xy_ascending" }, body: { name: "Fake", point_ids: [], sort_type: "xy_ascending" },
kind: "PointGroup", kind: "PointGroup",
@ -77,5 +78,6 @@ describe("<PointGroupItem/>", () => {
point_ids: [], point_ids: [],
sort_type: "xy_ascending", sort_type: "xy_ascending",
}); });
expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
}); });
}); });

View File

@ -7,7 +7,7 @@ import {
DesignerPanelHeader DesignerPanelHeader
} from "../plants/designer_panel"; } from "../plants/designer_panel";
import { TaggedPointGroup } from "farmbot"; import { TaggedPointGroup } from "farmbot";
import { DeleteButton } from "../../controls/pin_form_fields"; import { DeleteButton } from "../../ui/delete_button";
import { save, edit } from "../../api/crud"; import { save, edit } from "../../api/crud";
import { TaggedPlant } from "../map/interfaces"; import { TaggedPlant } from "../map/interfaces";
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector"; import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";

View File

@ -40,6 +40,7 @@ export const nn = (points: TaggedPlant[]) => {
const ordered: TaggedPlant[] = []; const ordered: TaggedPlant[] = [];
let from = { x: 0, y: 0 }; let from = { x: 0, y: 0 };
points.map(() => { points.map(() => {
if (available.length < 1) { return; }
const nearest = findNearest(from, available); const nearest = findNearest(from, available);
ordered.push(nearest); ordered.push(nearest);
from = { x: nearest.body.x, y: nearest.body.y }; from = { x: nearest.body.x, y: nearest.body.y };

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons"; import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { TaggedPlant } from "../map/interfaces"; import { TaggedPlant } from "../map/interfaces";
import { cachedCrop } from "../../open_farm/cached_crop"; import { cachedCrop } from "../../open_farm/cached_crop";
import { toggleHoveredPlant } from "../actions"; import { setHoveredPlant } from "../map/actions";
import { TaggedPointGroup, uuid } from "farmbot"; import { TaggedPointGroup, uuid } from "farmbot";
import { overwrite } from "../../api/crud"; import { overwrite } from "../../api/crud";
@ -25,23 +25,23 @@ const removePoint = (group: TaggedPointGroup, pointId: number) => {
}; };
// The individual plants in the point group detail page. // 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: "" }; state: PointGroupItemState = { icon: "" };
key = uuid(); key = uuid();
enter = () => this enter = () => this.props.dispatch(
.props setHoveredPlant(this.props.plant.uuid, this.state.icon));
.dispatch(toggleHoveredPlant(this.props.plant.uuid, this.state.icon));
leave = () => this leave = () => this.props.dispatch(setHoveredPlant(undefined));
.props
.dispatch(toggleHoveredPlant(undefined, ""));
click = () => this click = () => {
.props this.props.dispatch(
.dispatch(removePoint(this.props.group, this.props.plant.body.id || 0)); removePoint(this.props.group, this.props.plant.body.id || 0));
this.leave();
}
maybeGetCachedIcon = ({ currentTarget }: IMGEvent) => { maybeGetCachedIcon = ({ currentTarget }: IMGEvent) => {
return cachedCrop(this.props.plant.body.openfarm_slug).then((crop) => { return cachedCrop(this.props.plant.body.openfarm_slug).then((crop) => {

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { history } from "../../history"; import { history } from "../../history";
import { unselectPlant } from "../actions"; import { unselectPlant } from "../map/actions";
import { import {
selectAllSavedGardens, selectAllPlantTemplates, selectAllPlantPointers selectAllSavedGardens, selectAllPlantTemplates, selectAllPlantPointers
} from "../../resources/selectors"; } from "../../resources/selectors";

View File

@ -1,13 +1,13 @@
jest.mock("../../i18n", () => { jest.mock("../../i18n", () => ({
return { detectLanguage: jest.fn(() => Promise.resolve()) }; detectLanguage: jest.fn(() => Promise.resolve())
}); }));
jest.mock("../../util/stop_ie", () => { jest.mock("../../util/stop_ie", () => ({ stopIE: jest.fn() }));
return { stopIE: jest.fn() };
});
jest.mock("../../util", jest.mock("../../util", () => ({
() => ({ attachToRoot: jest.fn(), trim: (s: string) => s })); attachToRoot: jest.fn(),
trim: (s: string) => s,
}));
import { detectLanguage } from "../../i18n"; import { detectLanguage } from "../../i18n";
import { stopIE } from "../../util/stop_ie"; import { stopIE } from "../../util/stop_ie";

View File

@ -13,7 +13,7 @@ import {
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { findIndex } from "lodash"; import { findIndex } from "lodash";
import { t } from "./i18next_wrapper"; import { t } from "./i18next_wrapper";
import { unselectPlant } from "./farm_designer/actions"; import { unselectPlant } from "./farm_designer/map/actions";
interface Props { interface Props {
dispatch: Function; dispatch: Function;

View File

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

View File

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

View File

@ -1,11 +1,22 @@
export * from "./back_arrow"; export * from "./back_arrow";
export * from "./blurable_input"; export * from "./blurable_input";
export * from "./colors"; export * from "./center_panel";
export * from "./color_picker"; export * from "./color_picker";
export * from "./colors";
export * from "./column"; 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 "./help";
export * from "./page"; export * from "./input_error";
export * from "./left_panel";
export * from "./markdown"; export * from "./markdown";
export * from "./new_fb_select";
export * from "./page";
export * from "./right_panel";
export * from "./row"; export * from "./row";
export * from "./saucer"; export * from "./saucer";
export * from "./save_button"; export * from "./save_button";
@ -14,10 +25,3 @@ export * from "./widget";
export * from "./widget_header"; export * from "./widget_header";
export * from "./widget_footer"; export * from "./widget_footer";
export * from "./widget_body"; 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";