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",
() => ({ 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", () => {

View File

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

View File

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

View File

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

View File

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

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()),
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";

View File

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

View File

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

View File

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

View File

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

View File

@ -146,5 +146,5 @@ export enum Mode {
points = "points",
createPoint = "createPoint",
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] })),
}));
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";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 />", () => {

View File

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

View File

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

View File

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

View File

@ -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 = () => {

View File

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

View File

@ -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", () => ({

View File

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

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

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 "./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";