update map actions

pull/1622/head
gabrielburnworth 2019-12-10 12:08:54 -08:00
parent bb9566345a
commit 240f86c7c1
17 changed files with 203 additions and 55 deletions

View File

@ -118,9 +118,9 @@ describe("clickMapPlant", () => {
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"));
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith(selectPlant(["fakeUuid"]));
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
expect(dispatch).toHaveBeenCalledTimes(2);
});
@ -133,7 +133,7 @@ describe("clickMapPlant", () => {
state.resources = buildResourceIndex([plant]);
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "bar")(dispatch, getState);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
name: "Fake", point_ids: [1, 23]
}));
@ -149,10 +149,41 @@ describe("clickMapPlant", () => {
state.resources = buildResourceIndex([plant]);
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "bar")(dispatch, getState);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
name: "Fake", point_ids: [1]
}));
expect(dispatch).toHaveBeenCalledTimes(1);
});
it("adds a plant to the current selection if plant select is active", () => {
mockPath = "/app/designer/plants/select";
const state = fakeState();
const plant = fakePlant();
plant.uuid = "fakePlantUuid";
state.resources = buildResourceIndex([plant]);
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT, payload: [plant.uuid]
});
expect(dispatch).toHaveBeenCalledTimes(1);
});
it("removes a plant to the current selection if plant select is active", () => {
mockPath = "/app/designer/plants/select";
const state = fakeState();
const plant = fakePlant();
plant.uuid = "fakePlantUuid";
state.resources = buildResourceIndex([plant]);
state.resources.consumers.farm_designer.selectedPlants = [plant.uuid];
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT, payload: []
});
expect(dispatch).toHaveBeenCalledTimes(1);
});
});

View File

@ -36,6 +36,15 @@ jest.mock("../background/selection_box_actions", () => ({
jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() }));
jest.mock("../../../history", () => ({
history: {
push: jest.fn(),
getPathArray: () => [],
},
push: jest.fn(),
getPathArray: () => [],
}));
import * as React from "react";
import { GardenMap } from "../garden_map";
import { shallow, mount } from "enzyme";
@ -56,6 +65,7 @@ import {
} from "../../../__test_support__/fake_designer_state";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { history } from "../../../history";
const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
@ -144,12 +154,46 @@ describe("<GardenMap/>", () => {
expect(dragPlant).toHaveBeenCalled();
});
it("starts drag: selecting", () => {
const wrapper = shallow(<GardenMap {...fakeProps()} />);
it("starts drag on background: selecting", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />);
mockMode = Mode.addPlant;
const e = { pageX: 1000, pageY: 2000 };
wrapper.find(".drop-area-background").simulate("mouseDown", e);
expect(startNewSelectionBox).toHaveBeenCalled();
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
expect(getGardenCoordinates).toHaveBeenCalledWith(
expect.objectContaining(e));
});
it("starts drag on background: selecting again", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />);
mockMode = Mode.boxSelect;
const e = { pageX: 1000, pageY: 2000 };
wrapper.find(".drop-area-svg").simulate("mouseDown", e);
wrapper.find(".drop-area-background").simulate("mouseDown", e);
expect(startNewSelectionBox).toHaveBeenCalled();
expect(history.push).not.toHaveBeenCalled();
expect(getGardenCoordinates).toHaveBeenCalledWith(
expect.objectContaining(e));
});
it("starts drag on background: does nothing when adding plants", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />);
mockMode = Mode.clickToAdd;
const e = { pageX: 1000, pageY: 2000 };
wrapper.find(".drop-area-background").simulate("mouseDown", e);
expect(startNewSelectionBox).not.toHaveBeenCalled();
expect(history.push).not.toHaveBeenCalled();
expect(getGardenCoordinates).not.toHaveBeenCalled();
});
it("starts drag on background: creating points", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />);
mockMode = Mode.createPoint;
const e = { pageX: 1000, pageY: 2000 };
wrapper.find(".drop-area-background").simulate("mouseDown", e);
expect(startNewPoint).toHaveBeenCalled();
expect(startNewSelectionBox).not.toHaveBeenCalled();
expect(history.push).not.toHaveBeenCalled();
expect(getGardenCoordinates).toHaveBeenCalledWith(
expect.objectContaining(e));
});

View File

@ -10,6 +10,7 @@ import { GetState } from "../../redux/interfaces";
import { fetchGroupFromUrl } from "../point_groups/group_detail";
import { TaggedPoint } from "farmbot";
import { getMode } from "../map/util";
import { ResourceIndex, UUID } from "../../resources/interfaces";
export function movePlant(payload: MovePlantProps) {
const tr = payload.plant;
@ -25,36 +26,55 @@ export const selectPlant = (payload: string[] | undefined) => {
return { type: Actions.SELECT_PLANT, payload };
};
export const setHoveredPlant =
(plantUUID: string | undefined, icon = "") => {
return {
type: Actions.TOGGLE_HOVERED_PLANT,
payload: { plantUUID, icon }
};
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
type: Actions.TOGGLE_HOVERED_PLANT,
payload: { plantUUID, icon }
});
const addOrRemoveFromGroup =
(clickedPlantUuid: UUID, resources: ResourceIndex) => {
const group = fetchGroupFromUrl(resources);
const point =
resources.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.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);
return overwrite(group, nextGroup);
}
};
const addOrRemoveFromSelection =
(clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => {
const nextSelected =
(selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid);
if (!(selectedPlants && selectedPlants.includes(clickedPlantUuid))) {
nextSelected.push(clickedPlantUuid);
}
return selectPlant(nextSelected);
};
export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
return (dispatch: Function, getState: GetState) => {
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.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));
switch (getMode()) {
case Mode.editGroup:
const { resources } = getState();
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
break;
case Mode.boxSelect:
const { selectedPlants } = getState().resources.consumers.farm_designer;
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPlants));
break;
default:
dispatch(selectPlant([clickedPlantUuid]));
dispatch(setHoveredPlant(clickedPlantUuid, icon));
break;
}
};
};

View File

@ -11,6 +11,7 @@ describe("<Grid/>", () => {
return {
mapTransformProps: fakeMapTransformProps(),
onClick: jest.fn(),
onMouseDown: jest.fn(),
};
}

View File

@ -1,8 +1,15 @@
import { Mode } from "../../interfaces";
let mockMode = Mode.none;
jest.mock("../../util", () => ({ getMode: () => mockMode }));
jest.mock("../../../../history", () => ({ history: { push: jest.fn() } }));
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
import {
getSelected, resizeBox, startNewSelectionBox
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps
} from "../selection_box_actions";
import { Actions } from "../../../../constants";
import { history } from "../../../../history";
describe("getSelected", () => {
it("returns some", () => {
@ -24,7 +31,11 @@ describe("getSelected", () => {
});
describe("resizeBox", () => {
const fakeProps = () => ({
beforeEach(() => {
mockMode = Mode.boxSelect;
});
const fakeProps = (): ResizeSelectionBoxProps => ({
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
plants: [],
gardenCoords: { x: 100, y: 200 },
@ -61,6 +72,24 @@ describe("resizeBox", () => {
expect(p.setMapState).not.toHaveBeenCalled();
expect(p.dispatch).not.toHaveBeenCalled();
});
it("resizes selection box", () => {
mockMode = Mode.none;
const p = fakeProps();
const plant = fakePlant();
plant.body.x = 50;
plant.body.y = 50;
p.plants = [plant];
resizeBox(p);
expect(p.setMapState).toHaveBeenCalledWith({
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT,
payload: [plant.uuid]
});
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
});
});
describe("startNewSelectionBox", () => {

View File

@ -13,7 +13,8 @@ export function Grid(props: GridProps) {
const arrowEnd = transformXY(25, 25, mapTransformProps);
const xLabel = transformXY(15, -10, mapTransformProps);
const yLabel = transformXY(-11, 18, mapTransformProps);
return <g className="drop-area-background" onClick={props.onClick}>
return <g className="drop-area-background" onClick={props.onClick}
onMouseDown={props.onMouseDown}>
<defs>
<pattern id="minor_grid"
width={10} height={10} patternUnits="userSpaceOnUse">

View File

@ -26,14 +26,16 @@ export const getSelected = (
return arraySelected.length > 0 ? arraySelected : undefined;
};
export interface ResizeSelectionBoxProps {
selectionBox: SelectionBoxData | undefined;
plants: TaggedPlant[];
gardenCoords: AxisNumberProperty | undefined;
setMapState: (x: Partial<GardenMapState>) => void;
dispatch: Function;
}
/** Resize a selection box. */
export const resizeBox = (props: {
selectionBox: SelectionBoxData | undefined,
plants: TaggedPlant[],
gardenCoords: AxisNumberProperty | undefined,
setMapState: (x: Partial<GardenMapState>) => void,
dispatch: Function,
}) => {
export const resizeBox = (props: ResizeSelectionBoxProps) => {
if (props.selectionBox) {
const current = props.gardenCoords;
if (current) {

View File

@ -31,6 +31,7 @@ import {
import { chooseLocation } from "../move_to";
import { GroupOrder } from "../point_groups/group_order_visual";
import { NNPath } from "../point_groups/paths";
import { history } from "../../history";
export class GardenMap extends
React.Component<GardenMapProps, Partial<GardenMapState>> {
@ -95,7 +96,7 @@ export class GardenMap extends
setMapState = (x: Partial<GardenMapState>) => this.setState(x);
/** Map drag start actions. */
/** Map (anywhere) drag start actions. */
startDrag = (e: React.MouseEvent<SVGElement>): void => {
switch (getMode()) {
case Mode.editPlant:
@ -108,7 +109,7 @@ export class GardenMap extends
selectedPlant: this.props.selectedPlant,
});
} else { // Actions away from plant exit plant edit mode.
closePlantInfo(this.props.dispatch)();
this.closePanel()();
startNewSelectionBox({
gardenCoords,
setMapState: this.setMapState,
@ -125,8 +126,25 @@ export class GardenMap extends
break;
case Mode.clickToAdd:
break;
}
}
/** Map background drag start actions. */
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
switch (getMode()) {
case Mode.createPoint:
case Mode.clickToAdd:
case Mode.editPlant:
break;
case Mode.boxSelect:
startNewSelectionBox({
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
dispatch: this.props.dispatch,
});
break;
default:
history.push("/app/designer/plants");
startNewSelectionBox({
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
@ -281,6 +299,7 @@ export class GardenMap extends
(this.props.getConfigValue("photo_filter_end") || "").toString()} />
Grid = () => <Grid
onClick={this.closePanel()}
onMouseDown={this.startDragOnBackground}
mapTransformProps={this.mapTransformProps} />
SensorReadingsLayer = () => <SensorReadingsLayer
visible={!!this.props.showSensorReadings}

View File

@ -111,6 +111,7 @@ export interface MapBackgroundProps {
export interface GridProps {
mapTransformProps: MapTransformProps;
onClick(): void;
onMouseDown(e: React.MouseEvent<SVGElement>): void;
}
export interface VirtualFarmBotProps {

View File

@ -60,7 +60,7 @@ describe("<PlantLayer/>", () => {
});
it("is in non-clickable mode", () => {
mockPath = "/app/designer/plants/select";
mockPath = "/app/designer/plants/crop_search/mint/add";
const p = fakeProps();
const wrapper = svgMount(<PlantLayer {...p} />);

View File

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

View File

@ -68,7 +68,7 @@ describe("<ToolSlotLayer/>", () => {
});
it("is in non-clickable mode", () => {
mockPath = "/app/designer/plants/select";
mockPath = "/app/designer/plants/crop_search/mint/add";
const p = fakeProps();
const wrapper = shallow(<ToolSlotLayer {...p} />);
expect(wrapper.find("g").props().style)

View File

@ -340,12 +340,11 @@ export const getGardenCoordinates = (props: {
export const maybeNoPointer =
(defaultStyle: React.CSSProperties): React.SVGProps<SVGGElement>["style"] => {
switch (getMode()) {
case Mode.boxSelect:
case Mode.clickToAdd:
case Mode.moveTo:
case Mode.points:
case Mode.createPoint:
return { "pointerEvents": "none" };
return { pointerEvents: "none" };
default:
return defaultStyle;
}

View File

@ -97,6 +97,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
title={t("{{length}} plants selected",
{ length: this.selected.length })}
backTo={"/app/designer/plants"}
onBack={unselectPlant(dispatch)}
description={Content.BOX_SELECT_DESCRIPTION} />
<this.ActionButtons />

View File

@ -1,6 +1,6 @@
const mockId = 123;
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => [mockId])
getPathArray: jest.fn(() => ["groups", mockId])
}));
import { fetchGroupFromUrl } from "../group_detail";

View File

@ -14,7 +14,7 @@ mockGroup.body.point_ids = [23];
let mockId = GOOD_ID;
jest.mock("../../../history", () => {
return {
getPathArray: jest.fn(() => [mockId]),
getPathArray: jest.fn(() => ["groups", mockId]),
push: jest.fn()
};
});

View File

@ -16,12 +16,12 @@ interface GroupDetailProps {
}
export function fetchGroupFromUrl(index: ResourceIndex) {
if (!getPathArray().includes("groups")) { return; }
/** TODO: Write better selectors. */
const groupId = parseInt(getPathArray().pop() || "?", 10);
let group: TaggedPointGroup | undefined;
try {
group =
findByKindAndId<TaggedPointGroup>(index, "PointGroup", groupId);
group = findByKindAndId<TaggedPointGroup>(index, "PointGroup", groupId);
} catch (error) {
group = undefined;
}