groups updates
parent
837cbe8a85
commit
464b730cd8
|
@ -0,0 +1,8 @@
|
|||
class AddShowZonesToWebAppConfig < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :web_app_configs,
|
||||
:show_zones,
|
||||
:boolean,
|
||||
default: false
|
||||
end
|
||||
end
|
|
@ -1,3 +1,5 @@
|
|||
import * as React from "react";
|
||||
|
||||
jest.mock("browser-speech", () => ({
|
||||
talk: jest.fn(),
|
||||
}));
|
||||
|
@ -16,3 +18,8 @@ window.location = {
|
|||
pathname: "", href: "", hash: "", search: "",
|
||||
hostname: "", origin: "", port: "", protocol: "", host: "",
|
||||
};
|
||||
|
||||
jest.mock("../error_boundary", () => ({
|
||||
// tslint:disable-next-line:no-any
|
||||
ErrorBoundary: (p: any) => <div>{p.children}</div>,
|
||||
}));
|
|
@ -0,0 +1,36 @@
|
|||
import { DeepPartial } from "redux";
|
||||
|
||||
type DomEvent = React.SyntheticEvent<HTMLInputElement>;
|
||||
export const inputEvent = (value: string, name?: string): DomEvent => {
|
||||
const event: DeepPartial<DomEvent> = { currentTarget: { value, name } };
|
||||
return event as DomEvent;
|
||||
};
|
||||
|
||||
type ChangeEvent = React.ChangeEvent<HTMLInputElement>;
|
||||
export const changeEvent = (value: string): ChangeEvent => {
|
||||
const event: DeepPartial<ChangeEvent> = { currentTarget: { value } };
|
||||
return event as ChangeEvent;
|
||||
};
|
||||
|
||||
type IMGEvent = React.SyntheticEvent<HTMLImageElement, Event>;
|
||||
export const imgEvent = (): IMGEvent => {
|
||||
const event: DeepPartial<IMGEvent> = {
|
||||
currentTarget: {
|
||||
getAttribute: jest.fn(),
|
||||
setAttribute: jest.fn(),
|
||||
}
|
||||
};
|
||||
return event as IMGEvent;
|
||||
};
|
||||
|
||||
type FormEvent = React.FormEvent<HTMLFormElement>;
|
||||
export const formEvent = (): FormEvent => {
|
||||
const event: Partial<FormEvent> = { preventDefault: jest.fn() };
|
||||
return event as FormEvent;
|
||||
};
|
||||
|
||||
type DragEvent = React.DragEvent<HTMLElement>;
|
||||
export const dragEvent = (key: string): DragEvent => {
|
||||
const event: DeepPartial<DragEvent> = { dataTransfer: { getData: () => key } };
|
||||
return event as DragEvent;
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
import { DeepPartial } from "redux";
|
||||
|
||||
type DomEvent = React.SyntheticEvent<HTMLInputElement>;
|
||||
export const inputEvent = (value: string): DomEvent => {
|
||||
const event: DeepPartial<DomEvent> = { currentTarget: { value } };
|
||||
return event as DomEvent;
|
||||
};
|
|
@ -316,6 +316,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
|||
show_historic_points: false,
|
||||
time_format_24_hour: false,
|
||||
show_pins: false,
|
||||
show_zones: false,
|
||||
disable_emergency_unlock_confirmation: false,
|
||||
map_size_x: 2900,
|
||||
map_size_y: 1400,
|
||||
|
@ -459,7 +460,7 @@ export function fakePointGroup(): TaggedPointGroup {
|
|||
sort_type: "xy_ascending",
|
||||
point_ids: [],
|
||||
criteria: {
|
||||
day: { op: ">", days: 0 },
|
||||
day: { op: "<", days: 0 },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
jest.unmock("../error_boundary");
|
||||
|
||||
jest.mock("../util/errors.ts", () => ({ catchErrors: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
|
|
|
@ -4,8 +4,6 @@ jest.mock("../index", () => ({
|
|||
dispatchQosStart: jest.fn(),
|
||||
pingOK: jest.fn()
|
||||
}));
|
||||
const mockTimestamp = 0;
|
||||
jest.mock("../../util", () => ({ timestamp: () => mockTimestamp }));
|
||||
|
||||
import {
|
||||
readPing,
|
||||
|
|
|
@ -572,12 +572,6 @@ export namespace Content {
|
|||
export const CONFIRM_PLANT_DELETION =
|
||||
trim(`Show a confirmation dialog when deleting a plant.`);
|
||||
|
||||
export const SORT_DESCRIPTION =
|
||||
trim(`When executing a sequence over a Group of locations, FarmBot will
|
||||
travel to each group member in the order of the chosen sort method.
|
||||
If the random option is chosen, FarmBot will travel in a random order
|
||||
every time, so the ordering shown below will only be representative.`);
|
||||
|
||||
// Device
|
||||
export const NOT_HTTPS =
|
||||
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
|
||||
|
@ -824,6 +818,20 @@ export namespace Content {
|
|||
trim(`You haven't made any sequences or regimens yet. To add an event,
|
||||
first create a sequence or regimen.`);
|
||||
|
||||
// Groups
|
||||
export const SORT_DESCRIPTION =
|
||||
trim(`When executing a sequence over a Group of locations, FarmBot will
|
||||
travel to each group member in the order of the chosen sort method.
|
||||
If the random option is chosen, FarmBot will travel in a random order
|
||||
every time, so the ordering shown below will only be representative.`);
|
||||
|
||||
export const CRITERIA_SELECTION_COUNT =
|
||||
trim(`Criteria additions can only be removed by changing criteria.
|
||||
Click and drag in the map to modify zone selection criteria.
|
||||
Criteria will be applied at the time of sequence execution. The final
|
||||
selection at that time may differ from the selection currently
|
||||
displayed.`);
|
||||
|
||||
// Farmware
|
||||
export const NO_IMAGES_YET =
|
||||
trim(`You haven't yet taken any photos with your FarmBot.
|
||||
|
@ -912,12 +920,12 @@ export namespace DiagnosticMessages {
|
|||
network, a firewall may be blocking port 5672. Ensure that the blue LED
|
||||
communications light on the FarmBot electronics box is illuminated.`);
|
||||
|
||||
export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly, but
|
||||
we have no recent record of FarmBot connecting to the internet. This usually
|
||||
happens because of poor WiFi connectivity in the garden, a bad password during
|
||||
configuration, a very long power outage, or blocked ports on FarmBot's local
|
||||
network. Please refer IT staff to
|
||||
https://software.farm.bot/docs/for-it-security-professionals`);
|
||||
export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly,
|
||||
but we have no recent record of FarmBot connecting to the internet.
|
||||
This usually happens because of poor WiFi connectivity in the garden,
|
||||
a bad password during configuration, a very long power outage, or
|
||||
blocked ports on FarmBot's local network. Please refer IT staff to
|
||||
https://software.farm.bot/docs/for-it-security-professionals`);
|
||||
|
||||
export const NO_WS_AVAILABLE = trim(`You are either offline, using a web
|
||||
browser that does not support WebSockets, or are behind a firewall that
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
.groups-list-wrapper {
|
||||
padding: 0.5em 0em;
|
||||
}
|
||||
.groups-delete-btn {
|
||||
.group-delete-btn {
|
||||
float: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -332,6 +332,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.zones-layer {
|
||||
[id*="zones-1D-"] {
|
||||
stroke: $black;
|
||||
stroke-width: 5;
|
||||
}
|
||||
[id*="zones-"] {
|
||||
opacity: 0.1;
|
||||
&.current {
|
||||
opacity: 0.25;
|
||||
fill: $white;
|
||||
stroke: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-bot-trail,
|
||||
.virtual-peripherals {
|
||||
pointer-events: none;
|
||||
|
|
|
@ -738,6 +738,114 @@
|
|||
}
|
||||
}
|
||||
|
||||
.group-detail-panel {
|
||||
.panel-content {
|
||||
.group-criteria {
|
||||
margin-top: 1rem;
|
||||
.criteria-heading {
|
||||
margin-top: 0;
|
||||
}
|
||||
.fb-button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.group-criteria-presets {
|
||||
input[type="radio"] {
|
||||
width: auto;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.criteria-string,
|
||||
.criteria-pointer-type,
|
||||
.criteria-plant-status,
|
||||
.criteria-slug {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.location-criteria {
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.day-criteria {
|
||||
p {
|
||||
display: inline;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
.string-eq-criteria {
|
||||
margin-top: 1rem;
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
.number-eq-criteria,
|
||||
.number-gt-lt-criteria {
|
||||
margin-top: 1rem;
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
.expandable-header {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
}
|
||||
.criteria-point-count-breakdown {
|
||||
margin-bottom: 1rem;
|
||||
.manual-group-member-count,
|
||||
.criteria-group-member-count {
|
||||
margin-left: 2rem;
|
||||
div {
|
||||
display: inline;
|
||||
padding: 0.25rem;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid $panel_light_blue;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.criteria-group-member-count {
|
||||
div {
|
||||
border: 1px solid gray;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zone-info-panel {
|
||||
.panel-content {
|
||||
.location-criteria {
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weeds-inventory-panel,
|
||||
.zones-inventory-panel,
|
||||
.groups-panel {
|
||||
|
|
|
@ -95,9 +95,9 @@ select {
|
|||
}
|
||||
}
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
button {
|
||||
background: darken($white, 10%) !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { sequence2ddi, mapStateToProps, RawBootSequenceSelector } from "../boot_sequence_selector";
|
||||
import { fakeSequence, fakeFbosConfig } from "../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
sequence2ddi, mapStateToProps, RawBootSequenceSelector
|
||||
} from "../boot_sequence_selector";
|
||||
import {
|
||||
fakeSequence, fakeFbosConfig
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../../../__test_support__/fake_state";
|
||||
import { buildResourceIndex } from "../../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../../__test_support__/resource_index_builder";
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { FBSelect } from "../../../../ui";
|
||||
|
|
|
@ -24,7 +24,8 @@ export function PinGuard(props: PinGuardProps) {
|
|||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true} position={Position.RIGHT}/>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true}
|
||||
position={Position.RIGHT} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<label>
|
||||
|
|
|
@ -28,7 +28,8 @@ export function ZeroRow({ botDisconnected }: ZeroRowProps) {
|
|||
<label>
|
||||
{t("SET ZERO POSITION")}
|
||||
</label>
|
||||
<Help text={ToolTips.SET_ZERO_POSITION} requireClick={true} position={Position.RIGHT} />
|
||||
<Help text={ToolTips.SET_ZERO_POSITION} requireClick={true}
|
||||
position={Position.RIGHT} />
|
||||
</Col>
|
||||
{AXES.map((axis) => {
|
||||
return <Col xs={2} key={axis} className={"centered-button-div"}>
|
||||
|
|
|
@ -73,6 +73,7 @@ export enum Feature {
|
|||
backscheduled_regimens = "backscheduled_regimens",
|
||||
boot_sequence = "boot_sequence",
|
||||
change_ownership = "change_ownership",
|
||||
criteria_groups = "criteria_groups",
|
||||
endstop_invert = "endstop_invert",
|
||||
express_k10 = "express_k10",
|
||||
farmduino_k14 = "farmduino_k14",
|
||||
|
@ -89,7 +90,7 @@ export enum Feature {
|
|||
rpi_led_control = "rpi_led_control",
|
||||
sensors = "sensors",
|
||||
use_update_channel = "use_update_channel",
|
||||
variables = "variables"
|
||||
variables = "variables",
|
||||
}
|
||||
|
||||
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
|
||||
|
|
|
@ -14,4 +14,11 @@ describe("<DesignerPanelHeader />", () => {
|
|||
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
|
||||
expect(wrapper.find("div").first().hasClass("gray-panel")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("goes back", () => {
|
||||
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
|
||||
history.back = jest.fn();
|
||||
wrapper.find("i").first().simulate("click");
|
||||
expect(history.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,7 +35,8 @@ describe("<FarmDesigner/>", () => {
|
|||
selectedPlant: undefined,
|
||||
designer: fakeDesignerState(),
|
||||
hoveredPlant: undefined,
|
||||
points: [],
|
||||
genericPoints: [],
|
||||
allPoints: [],
|
||||
plants: [],
|
||||
toolSlots: [],
|
||||
crops: [],
|
||||
|
@ -59,6 +60,8 @@ describe("<FarmDesigner/>", () => {
|
|||
getConfigValue: jest.fn(),
|
||||
sensorReadings: [],
|
||||
sensors: [],
|
||||
groups: [],
|
||||
shouldDisplay: () => false,
|
||||
});
|
||||
|
||||
it("loads default map settings", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); }),
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
}));
|
||||
|
||||
let mockDev = false;
|
||||
|
|
|
@ -5,7 +5,9 @@ import {
|
|||
HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult
|
||||
} from "../interfaces";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { fakeCropLiveSearchResult } from "../../__test_support__/fake_crop_search_result";
|
||||
import {
|
||||
fakeCropLiveSearchResult
|
||||
} from "../../__test_support__/fake_crop_search_result";
|
||||
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ describe("mapStateToProps()", () => {
|
|||
expect.objectContaining({ uuid: plantUuid }));
|
||||
});
|
||||
|
||||
it("returns all points", () => {
|
||||
it("returns all genericPoints", () => {
|
||||
const state = fakeState();
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
(webAppConfig.body as WebAppConfig).show_historic_points = true;
|
||||
|
@ -67,10 +67,10 @@ describe("mapStateToProps()", () => {
|
|||
const point3 = fakePoint();
|
||||
point3.body.discarded_at = DISCARDED_AT;
|
||||
state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]);
|
||||
expect(mapStateToProps(state).points.length).toEqual(3);
|
||||
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
|
||||
});
|
||||
|
||||
it("returns active points", () => {
|
||||
it("returns active genericPoints", () => {
|
||||
const state = fakeState();
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
(webAppConfig.body as WebAppConfig).show_historic_points = false;
|
||||
|
@ -81,7 +81,7 @@ describe("mapStateToProps()", () => {
|
|||
const point3 = fakePoint();
|
||||
point3.body.discarded_at = DISCARDED_AT;
|
||||
state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]);
|
||||
expect(mapStateToProps(state).points.length).toEqual(1);
|
||||
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("returns sensor readings", () => {
|
||||
|
|
|
@ -78,13 +78,15 @@ export const DesignerPanelHeader = (props: DesignerPanelHeaderProps) => {
|
|||
interface DesignerPanelTopProps {
|
||||
panel: Panel;
|
||||
linkTo?: string;
|
||||
onClick?(): void;
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
noIcon?: boolean;
|
||||
}
|
||||
|
||||
export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
||||
return <div className={`panel-top ${props.linkTo ? "with-button" : ""}`}>
|
||||
const withBtn = !!props.linkTo || !!props.onClick;
|
||||
return <div className={`panel-top ${withBtn ? "with-button" : ""}`}>
|
||||
<div className="thin-search-wrapper">
|
||||
<div className="text-input-wrapper">
|
||||
{!props.noIcon &&
|
||||
|
@ -94,6 +96,13 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
|||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
{props.onClick &&
|
||||
<a>
|
||||
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}
|
||||
onClick={props.onClick}>
|
||||
<i className="fa fa-plus" title={props.title} />
|
||||
</div>
|
||||
</a>}
|
||||
{props.linkTo &&
|
||||
<Link to={props.linkTo}>
|
||||
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); }),
|
||||
history: {
|
||||
push: jest.fn()
|
||||
}
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
history: { push: jest.fn() }
|
||||
}));
|
||||
|
||||
import { mapStateToPropsAddEdit } from "../map_state_to_props_add_edit";
|
||||
|
@ -15,6 +13,7 @@ import {
|
|||
fakeSequence, fakeRegimen, fakeFarmEvent
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { history } from "../../../history";
|
||||
import { inputEvent } from "../../../__test_support__/fake_html_events";
|
||||
|
||||
describe("mapStateToPropsAddEdit()", () => {
|
||||
|
||||
|
@ -22,25 +21,19 @@ describe("mapStateToPropsAddEdit()", () => {
|
|||
const { handleTime } = mapStateToPropsAddEdit(fakeState());
|
||||
|
||||
it("handles an element with name `start_time`", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:54", name: "start_time" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent("10:54", "start_time");
|
||||
const result = handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(result).toContain("54");
|
||||
});
|
||||
|
||||
it("handles an element with name `end_time`", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:53", name: "end_time" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent("10:53", "end_time");
|
||||
const result = handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(result).toContain("53");
|
||||
});
|
||||
|
||||
it("crashes on other names", () => {
|
||||
const e = {
|
||||
currentTarget: { value: "10:52", name: "other" }
|
||||
} as React.SyntheticEvent<HTMLInputElement>;
|
||||
const e = inputEvent("10:52", "other");
|
||||
const boom = () => handleTime(e, "2017-05-21T22:00:00.000");
|
||||
expect(boom).toThrowError("Expected a name attribute from time field.");
|
||||
});
|
||||
|
|
|
@ -73,6 +73,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
show_spread: init(BooleanSetting.show_spread, false),
|
||||
show_farmbot: init(BooleanSetting.show_farmbot, true),
|
||||
show_images: init(BooleanSetting.show_images, false),
|
||||
show_zones: init(BooleanSetting.show_zones, false),
|
||||
show_sensor_readings: init(BooleanSetting.show_sensor_readings, false),
|
||||
bot_origin_quadrant: this.getBotOriginQuadrant(),
|
||||
zoom_level: calcZoomLevel(getZoomLevelIndex(this.props.getConfigValue)),
|
||||
|
@ -118,6 +119,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
show_spread,
|
||||
show_farmbot,
|
||||
show_images,
|
||||
show_zones,
|
||||
show_sensor_readings,
|
||||
zoom_level
|
||||
} = this.state;
|
||||
|
@ -156,6 +158,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
showSpread={show_spread}
|
||||
showFarmbot={show_farmbot}
|
||||
showImages={show_images}
|
||||
showZones={show_zones}
|
||||
showSensorReadings={show_sensor_readings}
|
||||
hasSensorReadings={this.props.sensorReadings.length > 0}
|
||||
dispatch={this.props.dispatch}
|
||||
|
@ -181,13 +184,15 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
showSpread={show_spread}
|
||||
showFarmbot={show_farmbot}
|
||||
showImages={show_images}
|
||||
showZones={show_zones}
|
||||
showSensorReadings={show_sensor_readings}
|
||||
selectedPlant={this.props.selectedPlant}
|
||||
crops={this.props.crops}
|
||||
dispatch={this.props.dispatch}
|
||||
designer={this.props.designer}
|
||||
plants={this.props.plants}
|
||||
points={this.props.points}
|
||||
genericPoints={this.props.genericPoints}
|
||||
allPoints={this.props.allPoints}
|
||||
toolSlots={this.props.toolSlots}
|
||||
botLocationData={this.props.botLocationData}
|
||||
botSize={botSize}
|
||||
|
@ -204,7 +209,9 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
getConfigValue={this.props.getConfigValue}
|
||||
sensorReadings={this.props.sensorReadings}
|
||||
timeSettings={this.props.timeSettings}
|
||||
sensors={this.props.sensors} />
|
||||
sensors={this.props.sensors}
|
||||
groups={this.props.groups}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
</div>
|
||||
|
||||
{this.props.designer.openedSavedGarden &&
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
TaggedImage,
|
||||
TaggedSensorReading,
|
||||
TaggedSensor,
|
||||
TaggedPoint,
|
||||
TaggedPointGroup,
|
||||
} from "farmbot";
|
||||
import { SlotWithTool, ResourceIndex } from "../resources/interfaces";
|
||||
import {
|
||||
|
@ -49,6 +51,7 @@ export interface State extends TypeCheckerHint {
|
|||
show_spread: boolean;
|
||||
show_farmbot: boolean;
|
||||
show_images: boolean;
|
||||
show_zones: boolean;
|
||||
show_sensor_readings: boolean;
|
||||
bot_origin_quadrant: BotOriginQuadrant;
|
||||
zoom_level: number;
|
||||
|
@ -59,7 +62,8 @@ export interface Props {
|
|||
selectedPlant: TaggedPlant | undefined;
|
||||
designer: DesignerState;
|
||||
hoveredPlant: TaggedPlant | undefined;
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
allPoints: TaggedPoint[];
|
||||
plants: TaggedPlant[];
|
||||
toolSlots: SlotWithTool[];
|
||||
crops: TaggedCrop[];
|
||||
|
@ -74,6 +78,8 @@ export interface Props {
|
|||
getConfigValue: GetWebAppConfigValue;
|
||||
sensorReadings: TaggedSensorReading[];
|
||||
sensors: TaggedSensor[];
|
||||
groups: TaggedPointGroup[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface MovePlantProps {
|
||||
|
@ -176,10 +182,12 @@ export interface GardenMapProps {
|
|||
showSpread: boolean | undefined;
|
||||
showFarmbot: boolean | undefined;
|
||||
showImages: boolean | undefined;
|
||||
showZones: boolean | undefined;
|
||||
showSensorReadings: boolean | undefined;
|
||||
dispatch: Function;
|
||||
designer: DesignerState;
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
allPoints: TaggedPoint[];
|
||||
plants: TaggedPlant[];
|
||||
toolSlots: SlotWithTool[];
|
||||
selectedPlant: TaggedPlant | undefined;
|
||||
|
@ -200,6 +208,8 @@ export interface GardenMapProps {
|
|||
sensorReadings: TaggedSensorReading[];
|
||||
sensors: TaggedSensor[];
|
||||
timeSettings: TimeSettings;
|
||||
groups: TaggedPointGroup[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface GardenMapState {
|
||||
|
|
|
@ -12,7 +12,7 @@ jest.mock("../../../api/crud", () => ({
|
|||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
const mockGroup = fakePointGroup();
|
||||
jest.mock("../../point_groups/group_detail", () => ({
|
||||
fetchGroupFromUrl: jest.fn(() => mockGroup)
|
||||
findGroupFromUrl: jest.fn(() => mockGroup)
|
||||
}));
|
||||
|
||||
import {
|
||||
|
|
|
@ -32,6 +32,7 @@ jest.mock("../drawn_point/drawn_point_actions", () => ({
|
|||
jest.mock("../background/selection_box_actions", () => ({
|
||||
startNewSelectionBox: jest.fn(),
|
||||
resizeBox: jest.fn(),
|
||||
maybeUpdateGroupCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() }));
|
||||
|
@ -45,6 +46,11 @@ jest.mock("../../../history", () => ({
|
|||
getPathArray: () => [],
|
||||
}));
|
||||
|
||||
let mockGroup: TaggedPointGroup | undefined = undefined;
|
||||
jest.mock("../../point_groups/group_detail", () => ({
|
||||
findGroupFromUrl: () => mockGroup,
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { GardenMap } from "../garden_map";
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
@ -55,7 +61,7 @@ import {
|
|||
dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant
|
||||
} from "../layers/plants/plant_actions";
|
||||
import {
|
||||
startNewSelectionBox, resizeBox
|
||||
startNewSelectionBox, resizeBox, maybeUpdateGroupCriteria
|
||||
} from "../background/selection_box_actions";
|
||||
import { getGardenCoordinates } from "../util";
|
||||
import { chooseLocation } from "../../move_to";
|
||||
|
@ -63,9 +69,12 @@ import { startNewPoint, resizePoint } from "../drawn_point/drawn_point_actions";
|
|||
import {
|
||||
fakeDesignerState
|
||||
} from "../../../__test_support__/fake_designer_state";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakePlant, fakePointGroup, fakePoint
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { history } from "../../../history";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
|
||||
const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
|
||||
|
||||
|
@ -75,13 +84,15 @@ const fakeProps = (): GardenMapProps => ({
|
|||
showSpread: false,
|
||||
showFarmbot: false,
|
||||
showImages: false,
|
||||
showZones: false,
|
||||
showSensorReadings: false,
|
||||
selectedPlant: undefined,
|
||||
crops: [],
|
||||
dispatch: jest.fn(),
|
||||
designer: fakeDesignerState(),
|
||||
plants: [],
|
||||
points: [],
|
||||
genericPoints: [],
|
||||
allPoints: [],
|
||||
toolSlots: [],
|
||||
botLocationData: {
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
|
@ -111,6 +122,8 @@ const fakeProps = (): GardenMapProps => ({
|
|||
sensorReadings: [],
|
||||
sensors: [],
|
||||
timeSettings: fakeTimeSettings(),
|
||||
groups: [],
|
||||
shouldDisplay: () => false,
|
||||
});
|
||||
|
||||
describe("<GardenMap/>", () => {
|
||||
|
@ -144,6 +157,7 @@ describe("<GardenMap/>", () => {
|
|||
wrapper.setState({ isDragging: true });
|
||||
wrapper.find(".drop-area-svg").simulate("mouseUp", DEFAULT_EVENT);
|
||||
expect(maybeSavePlantLocation).toHaveBeenCalled();
|
||||
expect(maybeUpdateGroupCriteria).toHaveBeenCalled();
|
||||
expect(wrapper.instance().state.isDragging).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -198,6 +212,18 @@ describe("<GardenMap/>", () => {
|
|||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("starts drag on background: selecting zone", () => {
|
||||
const wrapper = mount(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.editGroup;
|
||||
const e = { pageX: 1000, pageY: 2000 };
|
||||
wrapper.find(".drop-area-background").simulate("mouseDown", e);
|
||||
expect(startNewSelectionBox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ plantActions: false }));
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
||||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("starts drag: click-to-add mode", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.clickToAdd;
|
||||
|
@ -217,6 +243,17 @@ describe("<GardenMap/>", () => {
|
|||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("drags: selecting zone", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.editGroup;
|
||||
const e = { pageX: 2000, pageY: 2000 };
|
||||
wrapper.find(".drop-area-svg").simulate("mouseMove", e);
|
||||
expect(resizeBox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ plantActions: false }));
|
||||
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
||||
expect.objectContaining(e));
|
||||
});
|
||||
|
||||
it("selects location", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
mockMode = Mode.moveTo;
|
||||
|
@ -361,4 +398,14 @@ describe("<GardenMap/>", () => {
|
|||
expect(svg.props().height).toEqual(1000);
|
||||
});
|
||||
|
||||
it("gets group points", () => {
|
||||
mockGroup = fakePointGroup();
|
||||
mockGroup.body.point_ids = [1];
|
||||
const p = fakeProps();
|
||||
const point = fakePoint();
|
||||
point.body.id = 1;
|
||||
p.allPoints = [point];
|
||||
const wrapper = shallow<GardenMap>(<GardenMap {...p} />);
|
||||
expect(wrapper.instance().pointsSelectedByGroup).toEqual([point]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,10 +7,11 @@ 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 { findGroupFromUrl } from "../point_groups/group_detail";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
import { getMode } from "../map/util";
|
||||
import { ResourceIndex, UUID } from "../../resources/interfaces";
|
||||
import { selectAllPointGroups } from "../../resources/selectors";
|
||||
|
||||
export function movePlant(payload: MovePlantProps) {
|
||||
const tr = payload.plant;
|
||||
|
@ -33,7 +34,7 @@ export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
|||
|
||||
const addOrRemoveFromGroup =
|
||||
(clickedPlantUuid: UUID, resources: ResourceIndex) => {
|
||||
const group = fetchGroupFromUrl(resources);
|
||||
const group = findGroupFromUrl(selectAllPointGroups(resources));
|
||||
const point =
|
||||
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
|
||||
if (group && point?.body.id) {
|
||||
|
|
|
@ -4,12 +4,22 @@ jest.mock("../../util", () => ({ getMode: () => mockMode }));
|
|||
|
||||
jest.mock("../../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
|
||||
jest.mock("../../../point_groups/criteria", () => ({
|
||||
editGtLtCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps
|
||||
fakePlant, fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps,
|
||||
StartNewSelectionBoxProps,
|
||||
maybeUpdateGroupCriteria,
|
||||
MaybeUpdateGroupCriteriaProps,
|
||||
} from "../selection_box_actions";
|
||||
import { Actions } from "../../../../constants";
|
||||
import { history } from "../../../../history";
|
||||
import { editGtLtCriteria } from "../../../point_groups/criteria";
|
||||
|
||||
describe("getSelected", () => {
|
||||
it("returns some", () => {
|
||||
|
@ -41,6 +51,7 @@ describe("resizeBox", () => {
|
|||
gardenCoords: { x: 100, y: 200 },
|
||||
setMapState: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
plantActions: true,
|
||||
});
|
||||
|
||||
it("resizes selection box", () => {
|
||||
|
@ -55,6 +66,16 @@ describe("resizeBox", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("resizes selection box without plant actions", () => {
|
||||
const p = fakeProps();
|
||||
p.plantActions = false;
|
||||
resizeBox(p);
|
||||
expect(p.setMapState).toHaveBeenCalledWith({
|
||||
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
||||
});
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't resize box: no location", () => {
|
||||
const p = fakeProps();
|
||||
// tslint:disable-next-line:no-any
|
||||
|
@ -93,10 +114,11 @@ describe("resizeBox", () => {
|
|||
});
|
||||
|
||||
describe("startNewSelectionBox", () => {
|
||||
const fakeProps = () => ({
|
||||
const fakeProps = (): StartNewSelectionBoxProps => ({
|
||||
gardenCoords: { x: 100, y: 200 },
|
||||
setMapState: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
plantActions: true,
|
||||
});
|
||||
|
||||
it("starts selection box", () => {
|
||||
|
@ -111,6 +133,16 @@ describe("startNewSelectionBox", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("starts selection box without plant actions", () => {
|
||||
const p = fakeProps();
|
||||
p.plantActions = false;
|
||||
startNewSelectionBox(p);
|
||||
expect(p.setMapState).toHaveBeenCalledWith({
|
||||
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
|
||||
});
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't start box", () => {
|
||||
const p = fakeProps();
|
||||
// tslint:disable-next-line:no-any
|
||||
|
@ -123,3 +155,25 @@ describe("startNewSelectionBox", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeUpdateGroupCriteria()", () => {
|
||||
const fakeProps = (): MaybeUpdateGroupCriteriaProps => ({
|
||||
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
shouldDisplay: () => true,
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
maybeUpdateGroupCriteria(p);
|
||||
expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox);
|
||||
});
|
||||
|
||||
it("doesn't update criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => false;
|
||||
maybeUpdateGroupCriteria(p);
|
||||
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,9 @@ import { GardenMapState } from "../../interfaces";
|
|||
import { history } from "../../../history";
|
||||
import { selectPlant } from "../actions";
|
||||
import { getMode } from "../util";
|
||||
import { editGtLtCriteria } from "../../point_groups/criteria";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
||||
|
||||
/** Return all plants within the selection box. */
|
||||
export const getSelected = (
|
||||
|
@ -32,6 +35,7 @@ export interface ResizeSelectionBoxProps {
|
|||
gardenCoords: AxisNumberProperty | undefined;
|
||||
setMapState: (x: Partial<GardenMapState>) => void;
|
||||
dispatch: Function;
|
||||
plantActions: boolean;
|
||||
}
|
||||
|
||||
/** Resize a selection box. */
|
||||
|
@ -45,24 +49,29 @@ export const resizeBox = (props: ResizeSelectionBoxProps) => {
|
|||
x1: current.x, y1: current.y // Update box active corner
|
||||
};
|
||||
props.setMapState({ selectionBox: newSelectionBox });
|
||||
// Select all plants within the updated selection box
|
||||
const payload = getSelected(props.plants, newSelectionBox);
|
||||
if (payload && getMode() === Mode.none) {
|
||||
history.push("/app/designer/plants/select");
|
||||
if (props.plantActions) {
|
||||
// Select all plants within the updated selection box
|
||||
const payload = getSelected(props.plants, newSelectionBox);
|
||||
if (payload && getMode() === Mode.none) {
|
||||
history.push("/app/designer/plants/select");
|
||||
}
|
||||
props.dispatch(selectPlant(payload));
|
||||
}
|
||||
props.dispatch(selectPlant(payload));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export interface StartNewSelectionBoxProps {
|
||||
gardenCoords: AxisNumberProperty | undefined;
|
||||
setMapState: (x: Partial<GardenMapState>) => void;
|
||||
dispatch: Function;
|
||||
plantActions: boolean;
|
||||
}
|
||||
|
||||
/** Create a new selection box. */
|
||||
export const startNewSelectionBox = (props: {
|
||||
gardenCoords: AxisNumberProperty | undefined,
|
||||
setMapState: (x: Partial<GardenMapState>) => void,
|
||||
dispatch: Function,
|
||||
}) => {
|
||||
export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
|
||||
if (props.gardenCoords) {
|
||||
// Set the starting point (initial corner) of a selection box
|
||||
// Set the starting point (initial corner) of a selection box
|
||||
props.setMapState({
|
||||
selectionBox: {
|
||||
x0: props.gardenCoords.x, y0: props.gardenCoords.y,
|
||||
|
@ -70,6 +79,23 @@ export const startNewSelectionBox = (props: {
|
|||
}
|
||||
});
|
||||
}
|
||||
// Clear the previous plant selection when starting a new selection box
|
||||
props.dispatch(selectPlant(undefined));
|
||||
if (props.plantActions) {
|
||||
// Clear the previous plant selection when starting a new selection box
|
||||
props.dispatch(selectPlant(undefined));
|
||||
}
|
||||
};
|
||||
|
||||
export interface MaybeUpdateGroupCriteriaProps {
|
||||
selectionBox: SelectionBoxData | undefined;
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export const maybeUpdateGroupCriteria =
|
||||
(props: MaybeUpdateGroupCriteriaProps) => {
|
||||
if (props.selectionBox && props.group &&
|
||||
props.shouldDisplay(Feature.criteria_groups)) {
|
||||
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import {
|
||||
Grid, MapBackground,
|
||||
TargetCoordinate,
|
||||
SelectionBox, resizeBox, startNewSelectionBox
|
||||
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroupCriteria,
|
||||
} from "./background";
|
||||
import {
|
||||
PlantLayer,
|
||||
|
@ -32,6 +32,11 @@ import { chooseLocation } from "../move_to";
|
|||
import { GroupOrder } from "../point_groups/group_order_visual";
|
||||
import { NNPath } from "../point_groups/paths";
|
||||
import { history } from "../../history";
|
||||
import { ZonesLayer } from "./layers/zones/zones_layer";
|
||||
import { ErrorBoundary } from "../../error_boundary";
|
||||
import { TaggedPoint, TaggedPointGroup } from "farmbot";
|
||||
import { findGroupFromUrl } from "../point_groups/group_detail";
|
||||
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
||||
|
||||
export class GardenMap extends
|
||||
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
||||
|
@ -67,6 +72,14 @@ export class GardenMap extends
|
|||
get animate(): boolean {
|
||||
return !this.props.getConfigValue(BooleanSetting.disable_animations);
|
||||
}
|
||||
get group(): TaggedPointGroup | undefined {
|
||||
return findGroupFromUrl(this.props.groups);
|
||||
}
|
||||
|
||||
get pointsSelectedByGroup(): TaggedPoint[] {
|
||||
return this.group ?
|
||||
pointsSelectedByGroup(this.group, this.props.allPoints) : [];
|
||||
}
|
||||
|
||||
/** Save the current plant (if needed) and reset drag state. */
|
||||
endDrag = () => {
|
||||
|
@ -75,6 +88,12 @@ export class GardenMap extends
|
|||
isDragging: this.state.isDragging,
|
||||
dispatch: this.props.dispatch,
|
||||
});
|
||||
maybeUpdateGroupCriteria({
|
||||
selectionBox: this.state.selectionBox,
|
||||
group: this.group,
|
||||
dispatch: this.props.dispatch,
|
||||
shouldDisplay: this.props.shouldDisplay,
|
||||
});
|
||||
this.setState({
|
||||
isDragging: false, qPageX: 0, qPageY: 0,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -114,9 +133,18 @@ export class GardenMap extends
|
|||
gardenCoords,
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case Mode.editGroup:
|
||||
startNewSelectionBox({
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
});
|
||||
break;
|
||||
case Mode.createPoint:
|
||||
startNewPoint({
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
|
@ -141,6 +169,15 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
break;
|
||||
case Mode.editGroup:
|
||||
startNewSelectionBox({
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -149,6 +186,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -236,6 +274,16 @@ export class GardenMap extends
|
|||
isDragging: this.state.isDragging,
|
||||
});
|
||||
break;
|
||||
case Mode.editGroup:
|
||||
resizeBox({
|
||||
selectionBox: this.state.selectionBox,
|
||||
plants: this.props.plants,
|
||||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
});
|
||||
break;
|
||||
case Mode.boxSelect:
|
||||
default:
|
||||
resizeBox({
|
||||
|
@ -244,6 +292,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -302,6 +351,12 @@ export class GardenMap extends
|
|||
onMouseDown={this.startDragOnBackground}
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
zoomLvl={this.props.zoomLvl} />
|
||||
ZonesLayer = () => <ZonesLayer
|
||||
visible={!!this.props.showZones}
|
||||
botSize={this.props.botSize}
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
groups={this.props.groups}
|
||||
currentGroup={this.group?.uuid} />
|
||||
SensorReadingsLayer = () => <SensorReadingsLayer
|
||||
visible={!!this.props.showSensorReadings}
|
||||
sensorReadings={this.props.sensorReadings}
|
||||
|
@ -324,7 +379,7 @@ export class GardenMap extends
|
|||
dispatch={this.props.dispatch}
|
||||
hoveredPoint={this.props.designer.hoveredPoint}
|
||||
visible={!!this.props.showPoints}
|
||||
points={this.props.points} />
|
||||
genericPoints={this.props.genericPoints} />
|
||||
PlantLayer = () => <PlantLayer
|
||||
mapTransformProps={this.mapTransformProps}
|
||||
dispatch={this.props.dispatch}
|
||||
|
@ -333,7 +388,8 @@ export class GardenMap extends
|
|||
currentPlant={this.getPlant()}
|
||||
dragging={!!this.state.isDragging}
|
||||
editing={this.isEditing}
|
||||
selectedForDel={this.props.designer.selectedPlants}
|
||||
boxSelected={this.props.designer.selectedPlants}
|
||||
groupSelected={this.pointsSelectedByGroup.map(point => point.uuid)}
|
||||
zoomLvl={this.props.zoomLvl}
|
||||
activeDragXY={this.state.activeDragXY}
|
||||
animate={this.animate} />
|
||||
|
@ -383,9 +439,10 @@ export class GardenMap extends
|
|||
key={"currentPoint"}
|
||||
mapTransformProps={this.mapTransformProps} />
|
||||
GroupOrder = () => <GroupOrder
|
||||
plants={this.props.plants}
|
||||
group={this.group}
|
||||
groupPoints={this.pointsSelectedByGroup}
|
||||
mapTransformProps={this.mapTransformProps} />
|
||||
NNPath = () => <NNPath plants={this.props.plants}
|
||||
NNPath = () => <NNPath pathPoints={this.props.allPoints}
|
||||
mapTransformProps={this.mapTransformProps} />
|
||||
Bugs = () => showBugs() ? <Bugs mapTransformProps={this.mapTransformProps}
|
||||
botSize={this.props.botSize} /> : <g />
|
||||
|
@ -393,27 +450,30 @@ export class GardenMap extends
|
|||
/** Render layers in order from back to front. */
|
||||
render() {
|
||||
return <div className={"drop-area"} {...this.mapDropAreaProps()}>
|
||||
<svg id={"map-background-svg"}>
|
||||
<this.MapBackground />
|
||||
<svg className={"drop-area-svg"} {...this.svgDropAreaProps()}>
|
||||
<this.ImageLayer />
|
||||
<this.Grid />
|
||||
<this.SensorReadingsLayer />
|
||||
<this.SpreadLayer />
|
||||
<this.PointLayer />
|
||||
<this.PlantLayer />
|
||||
<this.ToolSlotLayer />
|
||||
<this.FarmBotLayer />
|
||||
<this.HoveredPlant />
|
||||
<this.DragHelper />
|
||||
<this.SelectionBox />
|
||||
<this.TargetCoordinate />
|
||||
<this.DrawnPoint />
|
||||
<this.GroupOrder />
|
||||
<this.NNPath />
|
||||
<this.Bugs />
|
||||
<ErrorBoundary>
|
||||
<svg id={"map-background-svg"}>
|
||||
<this.MapBackground />
|
||||
<svg className={"drop-area-svg"} {...this.svgDropAreaProps()}>
|
||||
<this.ImageLayer />
|
||||
<this.Grid />
|
||||
<this.ZonesLayer />
|
||||
<this.SensorReadingsLayer />
|
||||
<this.SpreadLayer />
|
||||
<this.PointLayer />
|
||||
<this.PlantLayer />
|
||||
<this.ToolSlotLayer />
|
||||
<this.FarmBotLayer />
|
||||
<this.HoveredPlant />
|
||||
<this.DragHelper />
|
||||
<this.SelectionBox />
|
||||
<this.TargetCoordinate />
|
||||
<this.DrawnPoint />
|
||||
<this.GroupOrder />
|
||||
<this.NNPath />
|
||||
<this.Bugs />
|
||||
</svg>
|
||||
</svg>
|
||||
</svg>
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
TaggedPlantPointer,
|
||||
TaggedGenericPointer,
|
||||
TaggedPlantTemplate
|
||||
TaggedPlantTemplate,
|
||||
} from "farmbot";
|
||||
import { State, BotOriginQuadrant } from "../interfaces";
|
||||
import { BotPosition, BotLocationData } from "../../devices/interfaces";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
|
||||
export type TaggedPlant = TaggedPlantPointer | TaggedPlantTemplate;
|
||||
|
||||
|
@ -20,7 +21,8 @@ export interface PlantLayerProps {
|
|||
mapTransformProps: MapTransformProps;
|
||||
zoomLvl: number;
|
||||
activeDragXY: BotPosition | undefined;
|
||||
selectedForDel: string[] | undefined;
|
||||
boxSelected: string[] | undefined;
|
||||
groupSelected: UUID[];
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
|
@ -33,6 +35,7 @@ export interface GardenMapLegendProps {
|
|||
showSpread: boolean;
|
||||
showFarmbot: boolean;
|
||||
showImages: boolean;
|
||||
showZones: boolean;
|
||||
showSensorReadings: boolean;
|
||||
hasSensorReadings: boolean;
|
||||
dispatch: Function;
|
||||
|
@ -59,7 +62,6 @@ export interface GardenPlantProps {
|
|||
zoomLvl: number;
|
||||
activeDragXY: BotPosition | undefined;
|
||||
uuid: string;
|
||||
multiselected: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ describe("<GardenPlant/>", () => {
|
|||
plant: fakePlant(),
|
||||
selected: false,
|
||||
editing: false,
|
||||
multiselected: false,
|
||||
dragging: false,
|
||||
dispatch: jest.fn(),
|
||||
zoomLvl: 1.8,
|
||||
|
@ -27,7 +26,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("renders plant", () => {
|
||||
const p = fakeProps();
|
||||
p.multiselected = true;
|
||||
p.selected = true;
|
||||
p.animate = false;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find("image").length).toEqual(1);
|
||||
|
@ -42,7 +41,7 @@ describe("<GardenPlant/>", () => {
|
|||
it("renders plant animations", () => {
|
||||
const p = fakeProps();
|
||||
p.animate = true;
|
||||
p.multiselected = true;
|
||||
p.selected = true;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find(".soil-cloud").length).toEqual(1);
|
||||
expect(wrapper.find(".animate").length).toEqual(2);
|
||||
|
@ -82,9 +81,9 @@ describe("<GardenPlant/>", () => {
|
|||
expect(wrapper.find(".plant-indicator").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("indicator cirlce is there", () => {
|
||||
it("indicator circle is rendered", () => {
|
||||
const p = fakeProps();
|
||||
p.multiselected = true;
|
||||
p.selected = true;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find(".plant-indicator").length).toEqual(1);
|
||||
expect(wrapper.find("Circle").length).toEqual(1);
|
||||
|
|
|
@ -22,7 +22,8 @@ describe("<PlantLayer/>", () => {
|
|||
currentPlant: undefined,
|
||||
dragging: false,
|
||||
editing: false,
|
||||
selectedForDel: undefined,
|
||||
boxSelected: undefined,
|
||||
groupSelected: [],
|
||||
dispatch: jest.fn(),
|
||||
zoomLvl: 1,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -97,14 +98,14 @@ describe("<PlantLayer/>", () => {
|
|||
expect(wrapper.find("GardenPlant").props().selected).toEqual(true);
|
||||
});
|
||||
|
||||
it("has plant selected for deletion", () => {
|
||||
it("has plant selected by selection box", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const plant = fakePlant();
|
||||
p.plants = [plant];
|
||||
p.selectedForDel = [plant.uuid];
|
||||
p.boxSelected = [plant.uuid];
|
||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||
expect((wrapper.find("GardenPlant").props() as GardenPlantProps).multiselected)
|
||||
expect((wrapper.find("GardenPlant").props() as GardenPlantProps).selected)
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export class GardenPlant extends
|
|||
}
|
||||
|
||||
render() {
|
||||
const { selected, dragging, plant, multiselected, mapTransformProps,
|
||||
const { selected, dragging, plant, mapTransformProps,
|
||||
activeDragXY, zoomLvl, animate, editing } = this.props;
|
||||
const { id, radius, x, y } = plant.body;
|
||||
const { icon } = this.state;
|
||||
|
@ -65,7 +65,7 @@ export class GardenPlant extends
|
|||
fill={Color.soilCloud}
|
||||
fillOpacity={0} />}
|
||||
|
||||
{multiselected && !editing &&
|
||||
{selected && !editing &&
|
||||
<g id="selected-plant-indicator">
|
||||
<Circle
|
||||
className={`plant-indicator ${animate ? "animate" : ""}`}
|
||||
|
@ -73,8 +73,7 @@ export class GardenPlant extends
|
|||
y={qy}
|
||||
r={radius}
|
||||
selected={true} />
|
||||
</g>
|
||||
}
|
||||
</g>}
|
||||
|
||||
<g id="plant-icon">
|
||||
<image
|
||||
|
|
|
@ -16,14 +16,16 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
mapTransformProps,
|
||||
zoomLvl,
|
||||
activeDragXY,
|
||||
selectedForDel,
|
||||
boxSelected,
|
||||
groupSelected,
|
||||
animate,
|
||||
} = props;
|
||||
|
||||
return <g id="plant-layer">
|
||||
{visible && plants.map(p => {
|
||||
const selected = !!(currentPlant && (p.uuid === currentPlant.uuid));
|
||||
const multiselected = !!(selectedForDel && (selectedForDel.includes(p.uuid)));
|
||||
const selected = !!(p.uuid === currentPlant?.uuid);
|
||||
const selectedByBox = !!boxSelected?.includes(p.uuid);
|
||||
const selectedByGroup = groupSelected.includes(p.uuid);
|
||||
const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate"
|
||||
? "gardens/templates"
|
||||
: "plants";
|
||||
|
@ -31,9 +33,8 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
uuid={p.uuid}
|
||||
mapTransformProps={mapTransformProps}
|
||||
plant={p}
|
||||
selected={selected}
|
||||
selected={selected || selectedByBox || selectedByGroup}
|
||||
editing={editing}
|
||||
multiselected={multiselected}
|
||||
dragging={selected && dragging && editing}
|
||||
dispatch={dispatch}
|
||||
zoomLvl={zoomLvl}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../../../../history", () => ({
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { PointLayer, PointLayerProps } from "../point_layer";
|
||||
import { fakePoint } from "../../../../../__test_support__/fake_state/resources";
|
||||
|
@ -8,21 +13,20 @@ import { GardenPoint } from "../garden_point";
|
|||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
|
||||
describe("<PointLayer/>", () => {
|
||||
function fakeProps(): PointLayerProps {
|
||||
return {
|
||||
visible: true,
|
||||
points: [fakePoint()],
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
hoveredPoint: undefined,
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
}
|
||||
const fakeProps = (): PointLayerProps => ({
|
||||
visible: true,
|
||||
genericPoints: [fakePoint()],
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
hoveredPoint: undefined,
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("shows points", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = svgMount(<PointLayer {...p} />);
|
||||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
|
||||
expect(layer.props().style).toEqual({ pointerEvents: "none" });
|
||||
});
|
||||
|
||||
it("toggles visibility off", () => {
|
||||
|
@ -32,4 +36,12 @@ describe("<PointLayer/>", () => {
|
|||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.find(GardenPoint).length).toEqual(0);
|
||||
});
|
||||
|
||||
it("allows point mode interaction", () => {
|
||||
mockPath = "/app/designer/points";
|
||||
const p = fakeProps();
|
||||
const wrapper = svgMount(<PointLayer {...p} />);
|
||||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.props().style).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,19 +6,19 @@ import { getMode } from "../../util";
|
|||
|
||||
export interface PointLayerProps {
|
||||
visible: boolean;
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
mapTransformProps: MapTransformProps;
|
||||
hoveredPoint: string | undefined;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export function PointLayer(props: PointLayerProps) {
|
||||
const { visible, points, mapTransformProps, hoveredPoint } = props;
|
||||
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
|
||||
const style: React.CSSProperties =
|
||||
getMode() === Mode.points ? {} : { pointerEvents: "none" };
|
||||
return <g id="point-layer" style={style}>
|
||||
{visible &&
|
||||
points.map(p =>
|
||||
genericPoints.map(p =>
|
||||
<GardenPoint
|
||||
point={p}
|
||||
key={p.uuid}
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import * as React from "react";
|
||||
import { SpreadLayer, SpreadLayerProps } from "../spread_layer";
|
||||
import {
|
||||
SpreadLayer, SpreadLayerProps, SpreadCircle, SpreadCircleProps
|
||||
} from "../spread_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { SpreadOverlapHelper } from "../spread_overlap_helper";
|
||||
|
||||
describe("<SpreadLayer/>", () => {
|
||||
function fakeProps(): SpreadLayerProps {
|
||||
return {
|
||||
visible: true,
|
||||
plants: [fakePlant()],
|
||||
currentPlant: undefined,
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dragging: false,
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
activeDragSpread: undefined,
|
||||
editing: false,
|
||||
animate: false,
|
||||
};
|
||||
}
|
||||
const fakeProps = (): SpreadLayerProps => ({
|
||||
visible: true,
|
||||
plants: [fakePlant()],
|
||||
currentPlant: undefined,
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dragging: false,
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
activeDragSpread: undefined,
|
||||
editing: false,
|
||||
animate: false,
|
||||
});
|
||||
|
||||
it("shows spread", () => {
|
||||
const p = fakeProps();
|
||||
|
@ -36,4 +37,30 @@ describe("<SpreadLayer/>", () => {
|
|||
const layer = wrapper.find("#spread-layer");
|
||||
expect(layer.find("SpreadCircle").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("is dragging", () => {
|
||||
const p = fakeProps();
|
||||
p.dragging = true;
|
||||
p.editing = true;
|
||||
p.currentPlant = p.plants[0];
|
||||
const wrapper = shallow(<SpreadLayer {...p} />);
|
||||
expect(wrapper.find(SpreadOverlapHelper).props().dragging).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SpreadCircle />", () => {
|
||||
const fakeProps = (): SpreadCircleProps => ({
|
||||
plant: fakePlant(),
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
visible: true,
|
||||
animate: true,
|
||||
});
|
||||
|
||||
it("uses spread value", () => {
|
||||
const wrapper = shallow(<SpreadCircle {...fakeProps()} />);
|
||||
wrapper.setState({ spread: 20 });
|
||||
expect(wrapper.find("circle").props().r).toEqual(100);
|
||||
expect(wrapper.find("circle").hasClass("animate")).toBeTruthy();
|
||||
expect(wrapper.find("circle").props().fill).toEqual("url(#SpreadGradient)");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ export function SpreadLayer(props: SpreadLayerProps) {
|
|||
</defs>
|
||||
|
||||
{plants.map(p => {
|
||||
const selected = !!(currentPlant && (p.uuid === currentPlant.uuid));
|
||||
const selected = p.uuid === currentPlant?.uuid;
|
||||
return <g id={"spread-components-" + p.body.id} key={p.uuid}>
|
||||
{visible &&
|
||||
<SpreadCircle
|
||||
|
@ -58,7 +58,7 @@ export function SpreadLayer(props: SpreadLayerProps) {
|
|||
</g>;
|
||||
}
|
||||
|
||||
interface SpreadCircleProps {
|
||||
export interface SpreadCircleProps {
|
||||
plant: TaggedPlant;
|
||||
mapTransformProps: MapTransformProps;
|
||||
visible: boolean;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import * as React from "react";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
import { ZonesLayer, ZonesLayerProps } from "../zones_layer";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<ZonesLayer />", () => {
|
||||
const fakeProps = (): ZonesLayerProps => ({
|
||||
visible: true,
|
||||
groups: [fakePointGroup(), fakePointGroup()],
|
||||
currentGroup: undefined,
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = svgMount(<ZonesLayer {...fakeProps()} />);
|
||||
expect(wrapper.find(".zones-layer").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders current group's zones: 2D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria.number_gt = { x: 100 };
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
p.groups[1].body.id = 2;
|
||||
p.groups[1].body.criteria.number_gt = { x: 200 };
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("#zones-2D-2").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders current group's zones: 1D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria.number_eq = { x: [100] };
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders current group's zones: 0D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria.number_eq = { x: [100], y: [100] };
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders current group's zones: none", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
p.groups[0].body.id = 1;
|
||||
p.groups[0].body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.html())
|
||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
||||
});
|
||||
|
||||
it("doesn't render current group's zones", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.html())
|
||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
import * as React from "react";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
import {
|
||||
Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType
|
||||
} from "../zones";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
const fakeProps = (): ZonesProps => ({
|
||||
group: fakePointGroup(),
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
currentGroup: undefined,
|
||||
});
|
||||
|
||||
describe("<Zones0D />", () => {
|
||||
it("renders none: no data", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders none: some data", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100] };
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders one", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100], y: [200] };
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders some", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100], y: [200, 300] };
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Zones1D />", () => {
|
||||
it("renders none: no data", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders none: too constrained", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100], y: [100] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders one: x", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [100] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders one: y", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { y: [100] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders some", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_eq = { x: [], y: [200, 300] };
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Zones2D />", () => {
|
||||
it("renders none", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders one", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_gt = { x: 100, y: 200 };
|
||||
p.group.body.criteria.number_lt = { x: 300, y: 400 };
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders one: rotated", () => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.quadrant = 4;
|
||||
p.mapTransformProps.xySwap = true;
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria.number_gt = { x: 100, y: 200 };
|
||||
p.group.body.criteria.number_lt = { x: 300, y: 400 };
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getZoneType()", () => {
|
||||
it("returns none", () => {
|
||||
const group = fakePointGroup();
|
||||
expect(getZoneType(group)).toEqual(ZoneType.none);
|
||||
});
|
||||
|
||||
it("returns area", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.number_gt = { x: 100 };
|
||||
expect(getZoneType(group)).toEqual(ZoneType.area);
|
||||
});
|
||||
|
||||
it("returns lines", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.number_eq = { x: [100] };
|
||||
expect(getZoneType(group)).toEqual(ZoneType.lines);
|
||||
});
|
||||
|
||||
it("returns points", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.number_eq = { x: [100], y: [100] };
|
||||
expect(getZoneType(group)).toEqual(ZoneType.points);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
import * as React from "react";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { MapTransformProps, BotSize } from "../../interfaces";
|
||||
import { transformXY } from "../../util";
|
||||
import { isUndefined } from "lodash";
|
||||
import { UUID } from "../../../../resources/interfaces";
|
||||
|
||||
export interface ZonesProps {
|
||||
currentGroup: UUID | undefined;
|
||||
group: TaggedPointGroup;
|
||||
mapTransformProps: MapTransformProps;
|
||||
botSize: BotSize;
|
||||
}
|
||||
|
||||
interface GetBoundaryProps {
|
||||
group: TaggedPointGroup;
|
||||
botSize: BotSize;
|
||||
}
|
||||
|
||||
type Boundary = {
|
||||
x1: number, y1: number, x2: number, y2: number,
|
||||
selectsAll: boolean
|
||||
};
|
||||
type Line = { x?: number, y?: number };
|
||||
type Point = { x: number, y: number };
|
||||
export enum ZoneType { points, lines, area, none }
|
||||
|
||||
export const getZoneType = (group: TaggedPointGroup): ZoneType => {
|
||||
const numEq = group.body.criteria?.number_eq || {};
|
||||
const numGt = group.body.criteria?.number_gt || {};
|
||||
const numLt = group.body.criteria?.number_lt || {};
|
||||
const hasXEq = !!numEq.x?.length;
|
||||
const hasYEq = !!numEq.y?.length;
|
||||
if (hasXEq && hasYEq) {
|
||||
return ZoneType.points;
|
||||
}
|
||||
if ((hasXEq && !hasYEq) || (!hasXEq && hasYEq)) {
|
||||
return ZoneType.lines;
|
||||
}
|
||||
if (numGt.x || numGt.y || numLt.x || numLt.y) {
|
||||
return ZoneType.area;
|
||||
}
|
||||
return ZoneType.none;
|
||||
};
|
||||
|
||||
/** Bounds for area selected by criteria or bot extents. */
|
||||
const getBoundary = (props: GetBoundaryProps): Boundary => {
|
||||
const { criteria } = props.group.body;
|
||||
const gt = criteria?.number_gt || {};
|
||||
const lt = criteria?.number_lt || {};
|
||||
const x1 = gt.x || 0;
|
||||
const x2 = lt.x || props.botSize.x.value;
|
||||
const y1 = gt.y || 0;
|
||||
const y2 = lt.y || props.botSize.y.value;
|
||||
const selectsAll = !(gt.x || lt.x || gt.y || lt.y);
|
||||
return { x1, x2, y1, y2, selectsAll };
|
||||
};
|
||||
|
||||
/** Apply bounds to zone data. */
|
||||
const filter: <T extends Point | Line>(
|
||||
boundary: Boundary, data: T[] | undefined) => T[] =
|
||||
(boundary, data) =>
|
||||
data?.filter(({ x, y }) =>
|
||||
(isUndefined(x) || (x > boundary.x1 && x < boundary.x2)) &&
|
||||
(isUndefined(y) || (y > boundary.y1 && y < boundary.y2))) || [];
|
||||
|
||||
/** Coordinates selected by both x and y number equal values. */
|
||||
const getPoints =
|
||||
(boundary: Boundary, group: TaggedPointGroup): Point[] => {
|
||||
const xs = group.body.criteria?.number_eq.x;
|
||||
const ys = group.body.criteria?.number_eq.y;
|
||||
const points: Point[] = [];
|
||||
xs?.map(x => ys?.map(y => points.push({ x, y })));
|
||||
return filter<Point>(boundary, points);
|
||||
};
|
||||
|
||||
/** Coordinates selected by both x and y number equal values. */
|
||||
const zone0D = (props: ZonesProps) =>
|
||||
getPoints(getBoundary(props), props.group)
|
||||
.map(point => {
|
||||
const { qx, qy } = transformXY(point.x, point.y, props.mapTransformProps);
|
||||
return { x: qx, y: qy };
|
||||
});
|
||||
|
||||
/** Coordinates selected by both x and y number equal values. */
|
||||
export const Zones0D = (props: ZonesProps) => {
|
||||
const current = props.group.uuid == props.currentGroup;
|
||||
return <g id={`zones-0D-${props.group.body.id}`}
|
||||
className={current ? "current" : ""}>
|
||||
{zone0D(props).map((point, i) =>
|
||||
<circle key={i} cx={point.x} cy={point.y} r={5} />)}
|
||||
</g>;
|
||||
};
|
||||
|
||||
/** Lines selected by an x or y number equal value. */
|
||||
const getLines =
|
||||
(boundary: Boundary, group: TaggedPointGroup): Line[] => {
|
||||
const xs = group.body.criteria?.number_eq.x;
|
||||
const ys = group.body.criteria?.number_eq.y;
|
||||
const onlyXs = !!xs?.length && !ys?.length;
|
||||
const onlyYs = !!ys?.length && !xs?.length;
|
||||
const xLineData = onlyXs ? xs?.map(x => ({ x })) : undefined;
|
||||
const yLineData = onlyYs ? ys?.map(y => ({ y })) : undefined;
|
||||
return filter<Line>(boundary, xLineData || yLineData);
|
||||
};
|
||||
|
||||
/** Lines selected by an x or y number equal value. */
|
||||
const zone1D = (props: ZonesProps) => {
|
||||
const boundary = getBoundary(props);
|
||||
return getLines(boundary, props.group).map(line => {
|
||||
const min = transformXY(
|
||||
line.x || boundary.x1,
|
||||
line.y || boundary.y1, props.mapTransformProps);
|
||||
const max = transformXY(
|
||||
line.x || boundary.x2,
|
||||
line.y || boundary.y2, props.mapTransformProps);
|
||||
return {
|
||||
x1: min.qx,
|
||||
y1: min.qy,
|
||||
x2: max.qx,
|
||||
y2: max.qy,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** Lines selected by an x or y number equal value. */
|
||||
export const Zones1D = (props: ZonesProps) => {
|
||||
const current = props.group.uuid == props.currentGroup;
|
||||
return <g id={`zones-1D-${props.group.body.id}`}
|
||||
className={current ? "current" : ""}>
|
||||
{zone1D(props).map((line, i) =>
|
||||
<line key={i} x1={line.x1} y1={line.y1}
|
||||
x2={line.x2} y2={line.y2} />)}
|
||||
</g>;
|
||||
};
|
||||
|
||||
/** Area selected by x and y number gt/lt values. */
|
||||
const zone2D = (boundary: Boundary, mapTransformProps: MapTransformProps) => {
|
||||
const position = transformXY(boundary.x1, boundary.y1, mapTransformProps);
|
||||
const { xySwap, quadrant } = mapTransformProps;
|
||||
const xLength = boundary.x2 - boundary.x1;
|
||||
const yLength = boundary.y2 - boundary.y1;
|
||||
return {
|
||||
x: [1, 4].includes(quadrant) ? position.qx - xLength : position.qx,
|
||||
y: [3, 4].includes(quadrant) ? position.qy - yLength : position.qy,
|
||||
width: xySwap ? yLength : xLength,
|
||||
height: xySwap ? xLength : yLength,
|
||||
selectsAll: boundary.selectsAll,
|
||||
};
|
||||
};
|
||||
|
||||
/** Area selected by x and y number gt/lt values. */
|
||||
export const Zones2D = (props: ZonesProps) => {
|
||||
const zone = zone2D(getBoundary(props), props.mapTransformProps);
|
||||
const current = props.group.uuid == props.currentGroup;
|
||||
return <g id={`zones-2D-${props.group.body.id}`}
|
||||
className={current ? "current" : ""}>
|
||||
{!zone.selectsAll &&
|
||||
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height} />}
|
||||
</g>;
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import * as React from "react";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { MapTransformProps, BotSize } from "../../interfaces";
|
||||
import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones";
|
||||
import { UUID } from "../../../../resources/interfaces";
|
||||
|
||||
export interface ZonesLayerProps {
|
||||
visible: boolean;
|
||||
currentGroup: UUID | undefined;
|
||||
groups: TaggedPointGroup[];
|
||||
botSize: BotSize;
|
||||
mapTransformProps: MapTransformProps;
|
||||
}
|
||||
|
||||
export function ZonesLayer(props: ZonesLayerProps) {
|
||||
const { groups, botSize, mapTransformProps, currentGroup } = props;
|
||||
const commonProps = { botSize, mapTransformProps, currentGroup };
|
||||
const visible = (group: TaggedPointGroup) =>
|
||||
props.visible || (group.uuid == currentGroup);
|
||||
return <g className="zones-layer">
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.area &&
|
||||
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.lines &&
|
||||
<Zones1D {...commonProps} key={group.uuid} group={group} />)}
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.points &&
|
||||
<Zones0D {...commonProps} key={group.uuid} group={group} />)}
|
||||
</g>;
|
||||
}
|
|
@ -40,6 +40,7 @@ describe("<GardenMapLegend />", () => {
|
|||
showSpread: false,
|
||||
showFarmbot: false,
|
||||
showImages: false,
|
||||
showZones: false,
|
||||
showSensorReadings: false,
|
||||
hasSensorReadings: false,
|
||||
dispatch: jest.fn(),
|
||||
|
|
|
@ -84,6 +84,11 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
dispatch={props.dispatch}
|
||||
getConfigValue={getConfigValue}
|
||||
imageAgeInfo={props.imageAgeInfo} />} />
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<LayerToggle
|
||||
value={props.showZones}
|
||||
label={t("Zones?")}
|
||||
onClick={toggle(BooleanSetting.show_zones)} />}
|
||||
{DevSettings.futureFeaturesEnabled() && props.hasSensorReadings &&
|
||||
<LayerToggle
|
||||
value={props.showSensorReadings}
|
||||
|
|
|
@ -291,7 +291,8 @@ export const transformForQuadrant =
|
|||
export const getMode = (): Mode => {
|
||||
const pathArray = getPathArray();
|
||||
if (pathArray) {
|
||||
if ((pathArray[3] === "groups") && pathArray[4]) { return Mode.editGroup; }
|
||||
if ((pathArray[3] === "groups" || pathArray[3] === "zones")
|
||||
&& 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; }
|
||||
|
|
|
@ -5,7 +5,7 @@ jest.mock("../../../history", () => ({
|
|||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { render } from "enzyme";
|
||||
import { mount } from "enzyme";
|
||||
import {
|
||||
RawAddPlant as AddPlant, AddPlantProps, mapStateToProps
|
||||
} from "../add_plant";
|
||||
|
@ -14,7 +14,8 @@ import {
|
|||
} from "../../../__test_support__/fake_crop_search_result";
|
||||
import { svgToUrl } from "../../../open_farm/icons";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { CropLiveSearchResult } from "../../interfaces";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<AddPlant />", () => {
|
||||
const fakeProps = (): AddPlantProps => {
|
||||
|
@ -24,40 +25,40 @@ describe("<AddPlant />", () => {
|
|||
cropSearchResults: [cropSearchResult],
|
||||
dispatch: jest.fn(),
|
||||
xy_swap: false,
|
||||
openfarmSearch: jest.fn(),
|
||||
openfarmSearch: jest.fn(() => jest.fn()),
|
||||
};
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||
const wrapper = render(<AddPlant {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => x(jest.fn()));
|
||||
const wrapper = mount(<AddPlant {...p} />);
|
||||
expect(wrapper.text()).toContain("Mint");
|
||||
expect(wrapper.text()).toContain("Preview");
|
||||
const img = wrapper.find("img");
|
||||
expect(img).toBeDefined();
|
||||
expect(img.attr("src")).toEqual(svgToUrl("fake_mint_svg"));
|
||||
expect(img.props().src).toEqual(svgToUrl("fake_mint_svg"));
|
||||
expect(p.openfarmSearch).toHaveBeenCalledWith("mint");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps", () => {
|
||||
it("maps state to props", () => {
|
||||
const state = fakeState();
|
||||
const crop: CropLiveSearchResult = {
|
||||
crop: {
|
||||
name: "fake",
|
||||
slug: "fake",
|
||||
binomial_name: "fake",
|
||||
common_names: ["fake"],
|
||||
description: "",
|
||||
sun_requirements: "",
|
||||
sowing_method: "",
|
||||
processing_pictures: 0
|
||||
},
|
||||
image: "X"
|
||||
};
|
||||
const crop = fakeCropLiveSearchResult();
|
||||
state.resources.consumers.farm_designer.cropSearchResults = [crop];
|
||||
const results = mapStateToProps(state);
|
||||
expect(results.cropSearchResults).toEqual([crop]);
|
||||
expect(results.xy_swap).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns xy_swap equals true", () => {
|
||||
const state = fakeState();
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
webAppConfig.body.xy_swap = true;
|
||||
state.resources = buildResourceIndex([webAppConfig]);
|
||||
const results = mapStateToProps(state);
|
||||
expect(results.xy_swap).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { mapStateToProps } from "../map_state_to_props";
|
||||
import { mapStateToProps, plantAge } from "../map_state_to_props";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
buildResourceIndex
|
||||
|
@ -32,3 +32,19 @@ describe("mapStateToProps()", () => {
|
|||
expect.objectContaining({ uuid }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("plantAge()", () => {
|
||||
it("returns planted at date", () => {
|
||||
const plant = fakePlant();
|
||||
plant.body.planted_at = "2018-01-11T20:20:38.362Z";
|
||||
plant.body.created_at = undefined;
|
||||
expect(plantAge(plant)).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it("returns created at date", () => {
|
||||
const plant = fakePlant();
|
||||
plant.body.planted_at = undefined;
|
||||
plant.body.created_at = "2018-01-11T20:20:38.362Z";
|
||||
expect(plantAge(plant)).toBeGreaterThan(100);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
jest.mock("../../../open_farm/cached_crop", () => ({
|
||||
cachedCrop: jest.fn(() => Promise.resolve({ svg_icon: "icon" })),
|
||||
maybeGetCachedPlantIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../history", () => ({ push: jest.fn() }));
|
||||
|
@ -8,22 +8,20 @@ import * as React from "react";
|
|||
import {
|
||||
PlantInventoryItem, PlantInventoryItemProps
|
||||
} from "../plant_inventory_item";
|
||||
import { shallow } from "enzyme";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import {
|
||||
fakePlant, fakePlantTemplate
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { Actions } from "../../../constants";
|
||||
import { push } from "../../../history";
|
||||
import { svgToUrl } from "../../../open_farm/icons";
|
||||
import { maybeGetCachedPlantIcon } from "../../../open_farm/cached_crop";
|
||||
|
||||
describe("<PlantInventoryItem />", () => {
|
||||
const fakeProps = (): PlantInventoryItemProps => {
|
||||
return {
|
||||
tpp: fakePlant(),
|
||||
dispatch: jest.fn(),
|
||||
hovered: false,
|
||||
};
|
||||
};
|
||||
const fakeProps = (): PlantInventoryItemProps => ({
|
||||
plant: fakePlant(),
|
||||
dispatch: jest.fn(),
|
||||
hovered: false,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<PlantInventoryItem {...fakeProps()} />);
|
||||
|
@ -45,7 +43,7 @@ describe("<PlantInventoryItem />", () => {
|
|||
expect(p.dispatch).toBeCalledWith({
|
||||
payload: {
|
||||
icon: "",
|
||||
plantUUID: p.tpp.uuid
|
||||
plantUUID: p.plant.uuid
|
||||
},
|
||||
type: Actions.TOGGLE_HOVERED_PLANT
|
||||
});
|
||||
|
@ -69,30 +67,31 @@ describe("<PlantInventoryItem />", () => {
|
|||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||
wrapper.simulate("click");
|
||||
expect(p.dispatch).toBeCalledWith({
|
||||
payload: [p.tpp.uuid],
|
||||
payload: [p.plant.uuid],
|
||||
type: Actions.SELECT_PLANT
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith("/app/designer/plants/" + p.tpp.body.id);
|
||||
expect(push).toHaveBeenCalledWith("/app/designer/plants/" + p.plant.body.id);
|
||||
});
|
||||
|
||||
it("selects plant template", () => {
|
||||
const p = fakeProps();
|
||||
p.tpp = fakePlantTemplate();
|
||||
p.plant = fakePlantTemplate();
|
||||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||
wrapper.simulate("click");
|
||||
expect(p.dispatch).toBeCalledWith({
|
||||
payload: [p.tpp.uuid],
|
||||
payload: [p.plant.uuid],
|
||||
type: Actions.SELECT_PLANT
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith(
|
||||
"/app/designer/gardens/templates/" + p.tpp.body.id);
|
||||
"/app/designer/gardens/templates/" + p.plant.body.id);
|
||||
});
|
||||
|
||||
it("gets cached icon", async () => {
|
||||
it("gets cached icon", () => {
|
||||
const wrapper =
|
||||
shallow<PlantInventoryItem>(<PlantInventoryItem {...fakeProps()} />);
|
||||
const img = new Image;
|
||||
await wrapper.find("img").simulate("load", { currentTarget: img });
|
||||
expect(wrapper.state().icon).toEqual(svgToUrl("icon"));
|
||||
mount<PlantInventoryItem>(<PlantInventoryItem {...fakeProps()} />);
|
||||
const img = wrapper.find("img");
|
||||
img.simulate("load");
|
||||
expect(maybeGetCachedPlantIcon).toHaveBeenCalledWith("strawberry",
|
||||
img.instance(), expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
jest.mock("../../../open_farm/cached_crop", () => ({
|
||||
maybeGetCachedPlantIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { RawPlants as Plants, PlantInventoryProps } from "../plant_inventory";
|
||||
import {
|
||||
RawPlants as Plants, PlantInventoryProps, mapStateToProps
|
||||
} from "../plant_inventory";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
|
||||
describe("<PlantInventory />", () => {
|
||||
const fakeProps = (): PlantInventoryProps => ({
|
||||
|
@ -32,3 +39,12 @@ describe("<PlantInventory />", () => {
|
|||
expect(wrapper.state().searchTerm).toEqual("mint");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props", () => {
|
||||
const state = fakeState();
|
||||
state.resources.consumers.farm_designer.hoveredPlantListItem = "uuid";
|
||||
const result = mapStateToProps(state);
|
||||
expect(result.hoveredPlantListItem).toEqual("uuid");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { testGrid } from "./generate_grid_test";
|
|||
import { GridInput, InputCell, InputCellProps, createCB } from "../grid_input";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { BlurableInput } from "../../../../ui/blurable_input";
|
||||
import { DeepPartial } from "redux";
|
||||
import { changeEvent } from "../../../../__test_support__/fake_html_events";
|
||||
|
||||
describe("<GridInput/>", () => {
|
||||
it("renders", () => {
|
||||
|
@ -37,15 +37,8 @@ describe("<InputCell/>", () => {
|
|||
|
||||
describe("createCB", () => {
|
||||
it("creates a callback", () => {
|
||||
type E = React.ChangeEvent<HTMLInputElement>;
|
||||
const e: DeepPartial<E> = {
|
||||
currentTarget: {
|
||||
value: "7"
|
||||
}
|
||||
};
|
||||
const dispatch = jest.fn();
|
||||
const cb = createCB("numPlantsH", dispatch);
|
||||
cb(e as E);
|
||||
createCB("numPlantsH", dispatch)(changeEvent("7"));
|
||||
expect(dispatch).toHaveBeenCalledWith("numPlantsH", 7);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,22 +49,30 @@ export interface FormattedPlantInfo {
|
|||
plantStatus: PlantStage;
|
||||
}
|
||||
|
||||
export function formatPlantInfo(rsrc: TaggedPlant): FormattedPlantInfo {
|
||||
const p = rsrc.body;
|
||||
const plantedAt = get(p, "planted_at", moment())
|
||||
? moment(get(p, "planted_at", moment()))
|
||||
: moment(get(p, "created_at", moment()));
|
||||
const currentDay = moment();
|
||||
const daysOld = currentDay.diff(plantedAt, "days") + 1;
|
||||
/** Get date planted or fallback to creation date. */
|
||||
const plantDate = (plant: TaggedPlant): moment.Moment => {
|
||||
const plantedAt = get(plant, "body.planted_at");
|
||||
const createdAt = get(plant, "body.created_at", moment());
|
||||
return plantedAt ? moment(plantedAt) : moment(createdAt);
|
||||
};
|
||||
|
||||
/** Compare planted or created date vs time now to determine age. */
|
||||
export const plantAge = (plant: TaggedPlant): number => {
|
||||
const currentDate = moment();
|
||||
const daysOld = currentDate.diff(plantDate(plant), "days") + 1;
|
||||
return daysOld;
|
||||
};
|
||||
|
||||
export function formatPlantInfo(plant: TaggedPlant): FormattedPlantInfo {
|
||||
return {
|
||||
slug: p.openfarm_slug,
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
daysOld,
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
uuid: rsrc.uuid,
|
||||
plantedAt,
|
||||
plantStatus: get(p, "plant_stage", "planned"),
|
||||
slug: plant.body.openfarm_slug,
|
||||
id: plant.body.id,
|
||||
name: plant.body.name,
|
||||
daysOld: plantAge(plant),
|
||||
x: plant.body.x,
|
||||
y: plant.body.y,
|
||||
uuid: plant.uuid,
|
||||
plantedAt: plantDate(plant),
|
||||
plantStatus: get(plant, "plant_stage", "planned"),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ interface State {
|
|||
searchTerm: string;
|
||||
}
|
||||
|
||||
function mapStateToProps(props: Everything): PlantInventoryProps {
|
||||
export function mapStateToProps(props: Everything): PlantInventoryProps {
|
||||
const { hoveredPlantListItem } = props.resources.consumers.farm_designer;
|
||||
return {
|
||||
plants: getPlants(props.resources),
|
||||
|
@ -62,7 +62,7 @@ export class RawPlants extends React.Component<PlantInventoryProps, State> {
|
|||
.includes(this.state.searchTerm.toLowerCase()))
|
||||
.map(p => <PlantInventoryItem
|
||||
key={p.uuid}
|
||||
tpp={p}
|
||||
plant={p}
|
||||
hovered={this.props.hoveredPlantListItem === p.uuid}
|
||||
dispatch={this.props.dispatch} />)}
|
||||
</EmptyStateWrapper>
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import * as React from "react";
|
||||
import moment from "moment";
|
||||
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
|
||||
import { DEFAULT_ICON } from "../../open_farm/icons";
|
||||
import { push } from "../../history";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { get } from "lodash";
|
||||
import { unpackUUID } from "../../util";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { cachedCrop } from "../../open_farm/cached_crop";
|
||||
import { maybeGetCachedPlantIcon } from "../../open_farm/cached_crop";
|
||||
import { selectPlant, setHoveredPlant } from "../map/actions";
|
||||
|
||||
type IMGEvent = React.SyntheticEvent<HTMLImageElement>;
|
||||
import { plantAge } from "./map_state_to_props";
|
||||
|
||||
export interface PlantInventoryItemProps {
|
||||
tpp: TaggedPlant;
|
||||
plant: TaggedPlant;
|
||||
dispatch: Function;
|
||||
hovered: boolean;
|
||||
}
|
||||
|
@ -28,49 +25,32 @@ export class PlantInventoryItem extends
|
|||
state: PlantInventoryItemState = { icon: "" };
|
||||
|
||||
render() {
|
||||
const plant = this.props.tpp.body;
|
||||
const { tpp, dispatch } = this.props;
|
||||
const plantId = (plant.id || "ERR_NO_PLANT_ID").toString();
|
||||
const { plant, dispatch } = this.props;
|
||||
const plantId = (plant.body.id || "ERR_NO_PLANT_ID").toString();
|
||||
|
||||
const toggle = (action: "enter" | "leave") => {
|
||||
const isEnter = action === "enter";
|
||||
const plantUUID = isEnter ? tpp.uuid : undefined;
|
||||
const plantUUID = isEnter ? plant.uuid : undefined;
|
||||
const icon = isEnter ? this.state.icon : "";
|
||||
dispatch(setHoveredPlant(plantUUID, icon));
|
||||
};
|
||||
|
||||
const click = () => {
|
||||
const plantCategory =
|
||||
unpackUUID(this.props.tpp.uuid).kind === "PlantTemplate"
|
||||
unpackUUID(plant.uuid).kind === "PlantTemplate"
|
||||
? "gardens/templates"
|
||||
: "plants";
|
||||
push(`/app/designer/${plantCategory}/${plantId}`);
|
||||
dispatch(selectPlant([tpp.uuid]));
|
||||
dispatch(selectPlant([plant.uuid]));
|
||||
};
|
||||
|
||||
// See `cachedIcon` for more details on this.
|
||||
const maybeGetCachedIcon = (e: IMGEvent) => {
|
||||
const OFS = tpp.body.openfarm_slug;
|
||||
const img = e.currentTarget;
|
||||
OFS && cachedCrop(OFS)
|
||||
.then((crop) => {
|
||||
const i = svgToUrl(crop.svg_icon);
|
||||
i !== img.getAttribute("src") && img.setAttribute("src", i);
|
||||
this.setState({ icon: i });
|
||||
});
|
||||
};
|
||||
const updateStateIcon = (i: string) => this.setState({ icon: i });
|
||||
const onLoad = (e: React.SyntheticEvent<HTMLImageElement>) =>
|
||||
maybeGetCachedPlantIcon(slug, e.currentTarget, updateStateIcon);
|
||||
|
||||
// Name given from OpenFarm's API.
|
||||
const label = plant.name || "Unknown plant";
|
||||
|
||||
// Original planted date vs time now to determine age.
|
||||
const getPlantedAt = get(plant, "planted_at", moment());
|
||||
const createdAt = get(plant, "created_at", moment());
|
||||
const plantedAt = getPlantedAt
|
||||
? moment(getPlantedAt)
|
||||
: moment(createdAt);
|
||||
const currentDay = moment();
|
||||
const daysOld = currentDay.diff(plantedAt, "days") + 1;
|
||||
const label = plant.body.name || "Unknown plant";
|
||||
const slug = plant.body.openfarm_slug;
|
||||
|
||||
return <div
|
||||
className={`plant-search-item ${this.props.hovered ? "hovered" : ""}`}
|
||||
|
@ -81,12 +61,12 @@ export class PlantInventoryItem extends
|
|||
<img
|
||||
className="plant-search-item-image"
|
||||
src={DEFAULT_ICON}
|
||||
onLoad={maybeGetCachedIcon} />
|
||||
onLoad={onLoad} />
|
||||
<span className="plant-search-item-name">
|
||||
{label}
|
||||
</span>
|
||||
<i className="plant-search-item-age">
|
||||
{daysOld} {t("days old")}
|
||||
{plantAge(plant)} {t("days old")}
|
||||
</i>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
|||
</button>
|
||||
<button className="fb-button dark-blue"
|
||||
onClick={() => !this.props.gardenOpen
|
||||
? this.props.dispatch(createGroup({ points: this.selected }))
|
||||
? this.props.dispatch(createGroup({ pointUuids: this.selected }))
|
||||
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>
|
||||
{t("Create group")}
|
||||
</button>
|
||||
|
@ -111,7 +111,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
|||
selectedPlantData.map(p =>
|
||||
<PlantInventoryItem
|
||||
key={p.uuid}
|
||||
tpp={p}
|
||||
plant={p}
|
||||
hovered={false}
|
||||
dispatch={dispatch} />)}
|
||||
</DesignerPanelContent>
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
jest.mock("../../../api/crud", () => {
|
||||
return {
|
||||
init: jest.fn(() => ({ payload: { uuid: "???" } })),
|
||||
save: jest.fn()
|
||||
};
|
||||
});
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
init: jest.fn(() => ({ payload: { uuid: "???" } })),
|
||||
save: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../../history", () => {
|
||||
return {
|
||||
history: { push: jest.fn() }
|
||||
};
|
||||
});
|
||||
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
jest.mock("../../../resources/selectors", () => ({
|
||||
findPointGroup: jest.fn(() => ({ body: { id: 323232332 } })),
|
||||
|
@ -19,32 +13,31 @@ jest.mock("../../../resources/selectors", () => ({
|
|||
import { createGroup } from "../actions";
|
||||
import { init, save } from "../../../api/crud";
|
||||
import { history } from "../../../history";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { fakePoint, fakePlant, fakeToolSlot } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
fakePoint, fakePlant, fakeToolSlot
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { DeepPartial } from "redux";
|
||||
import { Everything } from "../../../interfaces";
|
||||
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
|
||||
|
||||
describe("group action creators and thunks", () => {
|
||||
it("creates groups", async () => {
|
||||
const fakePoints = [fakePoint(), fakePlant(), fakeToolSlot()];
|
||||
const resources = buildResourceIndex(fakePoints);
|
||||
const points = fakePoints.map(x => x.uuid);
|
||||
const pointUuids = fakePoints.map(x => x.uuid);
|
||||
const fakeS: DeepPartial<Everything> = { resources };
|
||||
const dispatch = jest.fn(() => Promise.resolve());
|
||||
|
||||
const thunk = createGroup({ points, name: "Name123" });
|
||||
const thunk = createGroup({ pointUuids, groupName: "Name123" });
|
||||
await thunk(dispatch, () => fakeS as Everything);
|
||||
expect(init).toHaveBeenCalledWith("PointGroup", expect.objectContaining({
|
||||
name: "Name123",
|
||||
point_ids: [1, 2],
|
||||
sort_type: "xy_ascending",
|
||||
criteria: {
|
||||
day: { days: 0, op: ">" },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {},
|
||||
},
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
}));
|
||||
expect(save).toHaveBeenCalledWith("???");
|
||||
expect(history.push)
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
const mockId = 123;
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => ["groups", mockId])
|
||||
}));
|
||||
|
||||
import { fetchGroupFromUrl } from "../group_detail";
|
||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("fetchGroupFromUrl", () => {
|
||||
it("fetches a group from URL", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.id = mockId;
|
||||
const result = fetchGroupFromUrl(buildResourceIndex([group]).index);
|
||||
expect(result).toEqual(group);
|
||||
});
|
||||
|
||||
it("fetches a group from URL", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.id = 0; // intentionally wrong.
|
||||
const result = fetchGroupFromUrl(buildResourceIndex([group]).index);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
});
|
|
@ -14,39 +14,55 @@ jest.mock("../../../account/dev/dev_support", () => ({
|
|||
}));
|
||||
|
||||
import React from "react";
|
||||
import { GroupDetailActive } from "../group_detail_active";
|
||||
import {
|
||||
GroupDetailActive, GroupDetailActiveProps
|
||||
} from "../group_detail_active";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
fakePointGroup, fakePlant
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { save, edit } from "../../../api/crud";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
|
||||
|
||||
describe("<GroupDetailActive/>", () => {
|
||||
function fakeProps() {
|
||||
const fakeProps = (): GroupDetailActiveProps => {
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 1;
|
||||
const plants = [plant];
|
||||
const group = fakePointGroup();
|
||||
group.specialStatus = SpecialStatus.DIRTY;
|
||||
group.body.name = "XYZ";
|
||||
group.body.point_ids = [plant.body.id];
|
||||
return { dispatch: jest.fn(), group, plants };
|
||||
}
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
group,
|
||||
allPoints: [],
|
||||
shouldDisplay: () => true,
|
||||
slugs: [],
|
||||
};
|
||||
};
|
||||
|
||||
it("saves", () => {
|
||||
const p = fakeProps();
|
||||
const { dispatch } = p;
|
||||
const el = new GroupDetailActive(p);
|
||||
el.saveGroup();
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
expect(p.dispatch).toHaveBeenCalled();
|
||||
expect(save).toHaveBeenCalledWith(p.group.uuid);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const props = fakeProps();
|
||||
const el = mount(<GroupDetailActive {...props} />);
|
||||
expect(el.find("input").prop("defaultValue")).toContain("XYZ");
|
||||
const p = fakeProps();
|
||||
p.group.specialStatus = SpecialStatus.SAVED;
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.find("input").first().prop("defaultValue")).toContain("XYZ");
|
||||
expect(wrapper.text()).not.toContain("saving");
|
||||
});
|
||||
|
||||
it("shows saving indicator", () => {
|
||||
const p = fakeProps();
|
||||
p.group.specialStatus = SpecialStatus.DIRTY;
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text()).toContain("saving");
|
||||
});
|
||||
|
||||
it("changes group name", () => {
|
||||
|
@ -69,13 +85,7 @@ describe("<GroupDetailActive/>", () => {
|
|||
name: "XYZ",
|
||||
point_ids: [1],
|
||||
sort_type: "xy_ascending",
|
||||
criteria: {
|
||||
day: { days: 0, op: ">" },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {},
|
||||
}
|
||||
criteria: DEFAULT_CRITERIA
|
||||
},
|
||||
kind: "PointGroup",
|
||||
specialStatus: "DIRTY",
|
||||
|
@ -97,7 +107,6 @@ describe("<GroupDetailActive/>", () => {
|
|||
it("shows paths", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
p.plants = [fakePlant(), fakePlant()];
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("optimized");
|
||||
});
|
||||
|
|
|
@ -11,19 +11,18 @@ mockGroup.body.name = "one";
|
|||
mockGroup.body.id = GOOD_ID;
|
||||
mockGroup.body.point_ids = [23];
|
||||
|
||||
let mockId = GOOD_ID;
|
||||
jest.mock("../../../history", () => {
|
||||
return {
|
||||
getPathArray: jest.fn(() => ["groups", mockId]),
|
||||
push: jest.fn()
|
||||
};
|
||||
});
|
||||
const mockId = GOOD_ID;
|
||||
let mockPath = `/app/designer/groups/${mockId}`;
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
push: jest.fn()
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { mount } from "enzyme";
|
||||
import { GroupDetailActive } from "../group_detail_active";
|
||||
import { GroupDetail } from "../group_detail";
|
||||
import { GroupDetail, findGroupFromUrl } from "../group_detail";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { createStore } from "redux";
|
||||
import {
|
||||
|
@ -42,7 +41,7 @@ describe("<GroupDetail />", () => {
|
|||
};
|
||||
|
||||
it("redirects when group is not found", () => {
|
||||
mockId = -23;
|
||||
mockPath = "/app/designer/groups/-23";
|
||||
const store = fakeStore();
|
||||
const el = mount(<Provider store={store}>
|
||||
<GroupDetail />
|
||||
|
@ -53,7 +52,7 @@ describe("<GroupDetail />", () => {
|
|||
});
|
||||
|
||||
it("loads <GroupDetailActive/>", () => {
|
||||
mockId = GOOD_ID;
|
||||
mockPath = `/app/designer/groups/${mockId}`;
|
||||
const store = fakeStore();
|
||||
const el = mount(<Provider store={store}>
|
||||
<GroupDetail />
|
||||
|
@ -62,3 +61,35 @@ describe("<GroupDetail />", () => {
|
|||
expect(result.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findGroupFromUrl()", () => {
|
||||
it("finds group from URL", () => {
|
||||
mockPath = `/app/designer/groups/${mockId}`;
|
||||
const group = fakePointGroup();
|
||||
group.body.id = mockId;
|
||||
const otherGroup = fakePointGroup();
|
||||
otherGroup.body.id = mockId + 1;
|
||||
const result = findGroupFromUrl([group]);
|
||||
expect(result).toEqual(group);
|
||||
});
|
||||
|
||||
it("fails to find group from URL", () => {
|
||||
mockPath = `/app/designer/groups/${mockId}`;
|
||||
const result = findGroupFromUrl([]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("fails to find group from URL: undefined array item", () => {
|
||||
mockPath = "/app/designer/groups/";
|
||||
const result = findGroupFromUrl([]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("doesn't try to find a group when at a different URL", () => {
|
||||
mockPath = `/app/designer/plants/${mockId}`;
|
||||
const group = fakePointGroup();
|
||||
group.body.id = mockId;
|
||||
const result = findGroupFromUrl([group]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,33 @@
|
|||
import React from "react";
|
||||
import { GroupInventoryItem } from "../group_inventory_item";
|
||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
GroupInventoryItem, GroupInventoryItemProps
|
||||
} from "../group_inventory_item";
|
||||
import {
|
||||
fakePointGroup, fakePlant
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { mount } from "enzyme";
|
||||
|
||||
describe("<GroupInventoryItem/>", () => {
|
||||
it("renders information about the current group", () => {
|
||||
const dispatch = jest.fn();
|
||||
const group = fakePointGroup();
|
||||
const onClick = jest.fn();
|
||||
group.body.point_ids = [1, 2, 3];
|
||||
group.body.name = "woosh";
|
||||
describe("<GroupInventoryItem />", () => {
|
||||
const fakeProps = (): GroupInventoryItemProps => ({
|
||||
group: fakePointGroup(),
|
||||
allPoints: [],
|
||||
dispatch: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
hovered: true,
|
||||
});
|
||||
|
||||
const x = mount(<GroupInventoryItem
|
||||
group={group}
|
||||
hovered={true}
|
||||
dispatch={dispatch}
|
||||
onClick={onClick} />);
|
||||
it("renders information about the current group", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.point_ids = [1, 2, 3];
|
||||
p.group.body.name = "woosh";
|
||||
const point1 = fakePlant();
|
||||
point1.body.id = 1;
|
||||
const point2 = fakePlant();
|
||||
point2.body.id = 2;
|
||||
const point3 = fakePlant();
|
||||
point3.body.id = 3;
|
||||
p.allPoints = [point1, point2, point3];
|
||||
const x = mount(<GroupInventoryItem {...p} />);
|
||||
expect(x.text()).toContain("3 items");
|
||||
expect(x.text()).toContain("woosh");
|
||||
expect(x.find(".hovered").length).toBe(1);
|
||||
|
|
|
@ -8,7 +8,9 @@ import { mount, shallow } from "enzyme";
|
|||
import {
|
||||
RawGroupListPanel as GroupListPanel, GroupListPanelProps, mapStateToProps
|
||||
} from "../group_list_panel";
|
||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakePointGroup, fakePlant
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { history } from "../../../history";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
|
@ -17,15 +19,23 @@ import {
|
|||
|
||||
describe("<GroupListPanel />", () => {
|
||||
const fakeProps = (): GroupListPanelProps => {
|
||||
const fake1 = fakePointGroup();
|
||||
fake1.body.name = "one";
|
||||
fake1.body.id = 9;
|
||||
fake1.body.point_ids = [1, 2, 3];
|
||||
|
||||
const fake2 = fakePointGroup();
|
||||
fake2.body.name = "two";
|
||||
|
||||
return { dispatch: jest.fn(), groups: [fake1, fake2] };
|
||||
const group1 = fakePointGroup();
|
||||
group1.body.name = "one";
|
||||
group1.body.id = 9;
|
||||
group1.body.point_ids = [1, 2, 3];
|
||||
const group2 = fakePointGroup();
|
||||
group2.body.name = "two";
|
||||
const point1 = fakePlant();
|
||||
point1.body.id = 1;
|
||||
const point2 = fakePlant();
|
||||
point2.body.id = 2;
|
||||
const point3 = fakePlant();
|
||||
point3.body.id = 3;
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
groups: [group1, group2],
|
||||
allPoints: [point1, point2, point3],
|
||||
};
|
||||
};
|
||||
|
||||
it("changes search term", () => {
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
const mockGroup = fakePointGroup();
|
||||
mockGroup.body.point_ids = [1, 2, 3];
|
||||
jest.mock("../group_detail", () => ({ fetchGroupFromUrl: () => mockGroup }));
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
const mockState = fakeState();
|
||||
jest.mock("../../../redux/store", () => ({
|
||||
store: { getState: () => mockState },
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { GroupOrder, GroupOrderProps } from "../group_order_visual";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../__test_support__/map_transform_props";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakePlant, fakePointGroup
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { svgMount } from "../../../__test_support__/svg_mount";
|
||||
|
||||
describe("<GroupOrder />", () => {
|
||||
|
@ -23,9 +26,12 @@ describe("<GroupOrder />", () => {
|
|||
plant4.body.id = undefined;
|
||||
const plant5 = fakePlant();
|
||||
plant5.body.id = 5;
|
||||
const group = fakePointGroup();
|
||||
group.body.point_ids = [1, 2, 3];
|
||||
return {
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plants: [plant1, plant2, plant3],
|
||||
groupPoints: [plant1, plant2, plant3],
|
||||
group,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -33,4 +39,11 @@ describe("<GroupOrder />", () => {
|
|||
const wrapper = svgMount(<GroupOrder {...fakeProps()} />);
|
||||
expect(wrapper.find("line").length).toEqual(3);
|
||||
});
|
||||
|
||||
it("renders optimized group order", () => {
|
||||
const p = fakeProps();
|
||||
mockState.resources.consumers.farm_designer.tryGroupSortType = "nn";
|
||||
const wrapper = svgMount(<GroupOrder {...p} />);
|
||||
expect(wrapper.find("line").length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { PathInfoBar, nn, NNPath, PathInfoBarProps } from "../paths";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import {
|
||||
fakePlant, fakePointGroup
|
||||
PathInfoBar, nn, NNPath, PathInfoBarProps, Paths, PathsProps
|
||||
} from "../paths";
|
||||
import {
|
||||
fakePointGroup, fakePoint
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
|
@ -13,6 +15,48 @@ import { Actions } from "../../../constants";
|
|||
import { edit } from "../../../api/crud";
|
||||
import { error } from "../../../toast/toast";
|
||||
import { svgMount } from "../../../__test_support__/svg_mount";
|
||||
import { SORT_OPTIONS } from "../point_group_sort_selector";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
/**
|
||||
* p1 -- p2 --
|
||||
* -- -- -- --
|
||||
* -- -- -- --
|
||||
* p3 -- -- p4
|
||||
*/
|
||||
const pathTestCases = () => {
|
||||
const p1 = fakePoint();
|
||||
p1.body.x = 0;
|
||||
p1.body.y = 0;
|
||||
const p2 = fakePoint();
|
||||
p2.body.x = 2;
|
||||
p2.body.y = 0;
|
||||
const p3 = fakePoint();
|
||||
p3.body.x = 0;
|
||||
p3.body.y = 3;
|
||||
const p4 = fakePoint();
|
||||
p4.body.x = 3;
|
||||
p4.body.y = 3;
|
||||
return {
|
||||
points: { p1, p2, p3, p4 },
|
||||
order: {
|
||||
xy_ascending: [p1, p3, p2, p4],
|
||||
xy_descending: [p4, p2, p3, p1],
|
||||
yx_ascending: [p1, p2, p3, p4],
|
||||
yx_descending: [p4, p3, p2, p1],
|
||||
random: expect.arrayContaining([p1, p2, p3, p4]),
|
||||
nn: [p1, p2, p4, p3],
|
||||
},
|
||||
distance: {
|
||||
xy_ascending: 10,
|
||||
xy_descending: 10,
|
||||
yx_ascending: 9,
|
||||
yx_descending: 9,
|
||||
random: expect.any(Number),
|
||||
nn: 8,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe("<PathInfoBar />", () => {
|
||||
const fakeProps = (): PathInfoBarProps => ({
|
||||
|
@ -59,26 +103,16 @@ describe("<PathInfoBar />", () => {
|
|||
|
||||
describe("nearest neighbor algorithm", () => {
|
||||
it("returns optimized array", () => {
|
||||
const p1 = fakePlant();
|
||||
p1.body.x = 100;
|
||||
p1.body.y = 100;
|
||||
const p2 = fakePlant();
|
||||
p2.body.x = 200;
|
||||
p2.body.y = 200;
|
||||
const p3 = fakePlant();
|
||||
p3.body.x = 175;
|
||||
p3.body.y = 1000;
|
||||
const p4 = fakePlant();
|
||||
p4.body.x = 1000;
|
||||
p4.body.y = 150;
|
||||
const points = nn([p4, p2, p3, p1, p1]);
|
||||
expect(points).toEqual([p1, p2, p3, p4]);
|
||||
const cases = pathTestCases();
|
||||
const { p1, p2, p3, p4 } = cases.points;
|
||||
const pathPoints = nn([p4, p2, p3, p1, p1]);
|
||||
expect(pathPoints).toEqual(cases.order.nn);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<NNPath />", () => {
|
||||
const fakeProps = () => ({
|
||||
plants: [],
|
||||
pathPoints: [],
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
});
|
||||
|
||||
|
@ -93,3 +127,31 @@ describe("<NNPath />", () => {
|
|||
expect(wrapper.html()).not.toEqual("<svg><g></g></svg>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Paths />", () => {
|
||||
const fakeProps = (): PathsProps => ({
|
||||
pathPoints: [],
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
});
|
||||
|
||||
it("generates path data", () => {
|
||||
const p = fakeProps();
|
||||
const cases = pathTestCases();
|
||||
p.pathPoints = cases.order.xy_ascending;
|
||||
const wrapper = mount<Paths>(<Paths {...p} />);
|
||||
expect(wrapper.state().pathData).toEqual(cases.distance);
|
||||
});
|
||||
|
||||
it.each<[PointGroupSortType]>([
|
||||
["xy_ascending"],
|
||||
["xy_descending"],
|
||||
["yx_ascending"],
|
||||
["yx_descending"],
|
||||
["random"],
|
||||
])("checks sort order: %s", (sortType) => {
|
||||
const cases = pathTestCases();
|
||||
expect(SORT_OPTIONS[sortType](cases.order.xy_ascending))
|
||||
.toEqual(cases.order[sortType]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,63 +1,94 @@
|
|||
jest.mock("../../../open_farm/cached_crop", () => ({
|
||||
maybeGetCachedPlantIcon: jest.fn(),
|
||||
setImgSrc: 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 {
|
||||
PointGroupItem, PointGroupItemProps, genericPointIcon, OTHER_POINT_ICON
|
||||
} from "../point_group_item";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
fakePlant, fakePointGroup
|
||||
fakePlant, fakePointGroup, fakePoint, fakeToolSlot
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { DeepPartial } from "redux";
|
||||
import { cachedCrop } from "../../../open_farm/cached_crop";
|
||||
import {
|
||||
maybeGetCachedPlantIcon, setImgSrc
|
||||
} from "../../../open_farm/cached_crop";
|
||||
import { setHoveredPlant } from "../../map/actions";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { imgEvent } from "../../../__test_support__/fake_html_events";
|
||||
import { error } from "../../../toast/toast";
|
||||
import { svgToUrl } from "../../../open_farm/icons";
|
||||
|
||||
describe("<PointGroupItem/>", () => {
|
||||
const newProps = (): PointGroupItem["props"] => ({
|
||||
const fakeProps = (): PointGroupItemProps => ({
|
||||
dispatch: jest.fn(),
|
||||
plant: fakePlant(),
|
||||
point: fakePlant(),
|
||||
group: fakePointGroup(),
|
||||
hovered: true
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const props = newProps();
|
||||
const el = shallow<HTMLSpanElement>(<PointGroupItem {...props} />);
|
||||
const p = fakeProps();
|
||||
const el = shallow<PointGroupItem>(<PointGroupItem {...p} />);
|
||||
const i = el.instance() as PointGroupItem;
|
||||
expect(el.first().prop("onMouseEnter")).toEqual(i.enter);
|
||||
expect(el.first().prop("onMouseLeave")).toEqual(i.leave);
|
||||
expect(el.first().prop("onClick")).toEqual(i.click);
|
||||
});
|
||||
|
||||
it("handles hovering", async () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
it("fetches plant icon", async () => {
|
||||
const p = fakeProps();
|
||||
p.point = fakePlant();
|
||||
const i = new PointGroupItem(p);
|
||||
i.setState = jest.fn();
|
||||
type E = React.SyntheticEvent<HTMLImageElement, Event>;
|
||||
const partialE: DeepPartial<E> = {
|
||||
currentTarget: {
|
||||
getAttribute: jest.fn(),
|
||||
setAttribute: jest.fn(),
|
||||
}
|
||||
};
|
||||
const e = partialE as E;
|
||||
await i.maybeGetCachedIcon(e as E);
|
||||
const slug = i.props.plant.body.openfarm_slug;
|
||||
expect(cachedCrop).toHaveBeenCalledWith(slug);
|
||||
const icon = "data:image/svg+xml;utf8,icon";
|
||||
expect(i.setState).toHaveBeenCalledWith({ icon });
|
||||
expect(e.currentTarget.setAttribute).toHaveBeenCalledWith("src", icon);
|
||||
const fakeImgEvent = imgEvent();
|
||||
await i.maybeGetCachedIcon(fakeImgEvent);
|
||||
const slug = i.props.point.body.pointer_type === "Plant" ?
|
||||
i.props.point.body.openfarm_slug : "slug";
|
||||
expect(maybeGetCachedPlantIcon)
|
||||
.toHaveBeenCalledWith(slug, expect.any(Object), expect.any(Function));
|
||||
expect(setImgSrc).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fetches point icon", () => {
|
||||
const p = fakeProps();
|
||||
p.point = fakePoint();
|
||||
const i = new PointGroupItem(p);
|
||||
i.setState = jest.fn();
|
||||
const fakeImgEvent = imgEvent();
|
||||
i.maybeGetCachedIcon(fakeImgEvent);
|
||||
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
|
||||
expect(setImgSrc).toHaveBeenCalledWith(expect.any(Object),
|
||||
svgToUrl(genericPointIcon(undefined)));
|
||||
});
|
||||
|
||||
it("fetches other icon", () => {
|
||||
const p = fakeProps();
|
||||
p.point = fakeToolSlot();
|
||||
const i = new PointGroupItem(p);
|
||||
i.setState = jest.fn();
|
||||
const fakeImgEvent = imgEvent();
|
||||
i.maybeGetCachedIcon(fakeImgEvent);
|
||||
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
|
||||
expect(setImgSrc).toHaveBeenCalledWith(expect.any(Object),
|
||||
svgToUrl(OTHER_POINT_ICON));
|
||||
});
|
||||
|
||||
it("handles mouse enter", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
const i = new PointGroupItem(fakeProps());
|
||||
i.state.icon = "X";
|
||||
i.enter();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(setHoveredPlant).toHaveBeenCalledWith(i.props.plant.uuid, "X");
|
||||
expect(setHoveredPlant).toHaveBeenCalledWith(i.props.point.uuid, "X");
|
||||
});
|
||||
|
||||
it("handles mouse exit", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
const i = new PointGroupItem(fakeProps());
|
||||
i.state.icon = "X";
|
||||
i.leave();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
|
||||
|
@ -65,37 +96,28 @@ describe("<PointGroupItem/>", () => {
|
|||
});
|
||||
|
||||
it("handles clicks", () => {
|
||||
const i = new PointGroupItem(newProps());
|
||||
const p = fakeProps();
|
||||
p.point.body.id = 1;
|
||||
p.group.body.point_ids = [1];
|
||||
const i = new PointGroupItem(p);
|
||||
i.click();
|
||||
expect(i.props.dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(overwrite).toHaveBeenCalledWith({
|
||||
body: {
|
||||
name: "Fake",
|
||||
point_ids: [],
|
||||
sort_type: "xy_ascending",
|
||||
criteria: {
|
||||
day: { days: 0, op: ">" },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {},
|
||||
}
|
||||
},
|
||||
kind: "PointGroup",
|
||||
specialStatus: "",
|
||||
uuid: expect.any(String),
|
||||
}, {
|
||||
name: "Fake",
|
||||
point_ids: [],
|
||||
sort_type: "xy_ascending",
|
||||
criteria: {
|
||||
day: { days: 0, op: ">" },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {},
|
||||
}
|
||||
});
|
||||
const expectedGroupBody = cloneDeep(p.group.body);
|
||||
expectedGroupBody.point_ids = [];
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedGroupBody);
|
||||
expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("errors on click", () => {
|
||||
const p = fakeProps();
|
||||
p.point.body.id = 1;
|
||||
p.group.body.point_ids = [];
|
||||
const i = new PointGroupItem(p);
|
||||
i.click();
|
||||
expect(i.props.dispatch).not.toHaveBeenCalled();
|
||||
expect(overwrite).not.toHaveBeenCalled();
|
||||
expect(setHoveredPlant).not.toHaveBeenCalled();
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
"Cannot remove points selected by criteria.");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { isSortType, sortTypeChange, SORT_OPTIONS } from "../point_group_sort_selector";
|
||||
import * as React from "react";
|
||||
import {
|
||||
isSortType, sortTypeChange, SORT_OPTIONS, PointGroupSortSelector,
|
||||
PointGroupSortSelectorProps
|
||||
} from "../point_group_sort_selector";
|
||||
import { DropDownItem } from "../../../ui";
|
||||
import { DeepPartial } from "redux";
|
||||
import { TaggedPlant } from "../../map/interfaces";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { mount } from "enzyme";
|
||||
import { Content } from "../../../constants";
|
||||
|
||||
const tests: [string, boolean][] = [
|
||||
["", false],
|
||||
|
@ -38,11 +44,15 @@ describe("sortTypeChange", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("", () => {
|
||||
const phony = (name: string, x: number, y: number): DeepPartial<TaggedPlant> => {
|
||||
return { body: { name, x, y } };
|
||||
describe("sort()", () => {
|
||||
const phony = (name: string, x: number, y: number): TaggedPoint => {
|
||||
const plant = fakePlant();
|
||||
plant.body.name = name;
|
||||
plant.body.x = x;
|
||||
plant.body.y = y;
|
||||
return plant;
|
||||
};
|
||||
const plants = [
|
||||
const fakePoints = [
|
||||
phony("A", 0, 0),
|
||||
phony("B", 1, 0),
|
||||
phony("C", 1, 1),
|
||||
|
@ -50,13 +60,13 @@ describe("", () => {
|
|||
];
|
||||
|
||||
const sort = (sortType: PointGroupSortType): string[] => {
|
||||
const array = SORT_OPTIONS[sortType](plants as TaggedPlant[]);
|
||||
const array = SORT_OPTIONS[sortType](fakePoints);
|
||||
return array.map(x => x?.body?.name || "NA");
|
||||
};
|
||||
|
||||
it("sorts randomly", () => {
|
||||
const results = sort("random");
|
||||
expect(results.length).toEqual(plants.length);
|
||||
expect(results.length).toEqual(fakePoints.length);
|
||||
});
|
||||
|
||||
it("sorts by xy_ascending", () => {
|
||||
|
@ -79,3 +89,15 @@ describe("", () => {
|
|||
expect(results).toEqual(["C", "D", "B", "A"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<PointGroupSortSelector />", () => {
|
||||
const fakeProps = (): PointGroupSortSelectorProps => ({
|
||||
onChange: jest.fn(),
|
||||
value: "random",
|
||||
});
|
||||
|
||||
it("shows random warning text", () => {
|
||||
const wrapper = mount(<PointGroupSortSelector {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,42 +5,33 @@ import { history } from "../../history";
|
|||
import { GetState } from "../../redux/interfaces";
|
||||
import { findPointGroup } from "../../resources/selectors";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
const UNTITLED = () => t("Untitled Group");
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import { DEFAULT_CRITERIA } from "./criteria/interfaces";
|
||||
|
||||
interface CreateGroupProps {
|
||||
/** TaggedPoint UUIDs */
|
||||
points: string[];
|
||||
name?: string;
|
||||
pointUuids: UUID[];
|
||||
groupName?: string;
|
||||
}
|
||||
|
||||
export const createGroup = ({ points, name }: CreateGroupProps) => {
|
||||
return function (dispatch: Function, getState: GetState) {
|
||||
if (points.length > 0) {
|
||||
const { references } = getState().resources.index;
|
||||
const possiblyNil = points
|
||||
.map(x => references[x])
|
||||
.map(x => x ? x.body.id : undefined);
|
||||
const point_ids = betterCompact(possiblyNil);
|
||||
const group: PointGroup = {
|
||||
name: name || UNTITLED(),
|
||||
point_ids,
|
||||
sort_type: "xy_ascending",
|
||||
criteria: {
|
||||
day: { op: ">", days: 0 },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {}
|
||||
}
|
||||
};
|
||||
const action = init("PointGroup", group);
|
||||
dispatch(action);
|
||||
return dispatch(save(action.payload.uuid)).then(() => {
|
||||
export const createGroup = ({ pointUuids, groupName }: CreateGroupProps) =>
|
||||
(dispatch: Function, getState: GetState) => {
|
||||
const { references } = getState().resources.index;
|
||||
const possiblyNil = pointUuids
|
||||
.map(x => references[x])
|
||||
.map(x => x ? x.body.id : undefined);
|
||||
const point_ids = betterCompact(possiblyNil);
|
||||
const group: PointGroup = {
|
||||
name: groupName || t("Untitled Group"),
|
||||
point_ids,
|
||||
sort_type: "xy_ascending",
|
||||
criteria: DEFAULT_CRITERIA
|
||||
};
|
||||
const action = init("PointGroup", group);
|
||||
dispatch(action);
|
||||
dispatch(save(action.payload.uuid))
|
||||
.then(() => {
|
||||
const pg = findPointGroup(getState().resources.index, action.payload.uuid);
|
||||
const { id } = pg.body;
|
||||
history.push("/app/designer/groups/" + (id ? id : ""));
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
jest.mock("../edit", () => ({
|
||||
editCriteria: jest.fn(),
|
||||
toggleStringCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
AddEqCriteria, AddNumberCriteria, editCriteria, AddStringCriteria,
|
||||
toggleStringCriteria,
|
||||
POINTER_TYPE_LIST,
|
||||
PLANT_STAGE_LIST
|
||||
} from "..";
|
||||
import {
|
||||
AddEqCriteriaProps, NumberCriteriaProps, DEFAULT_CRITERIA,
|
||||
AddStringCriteriaProps,
|
||||
} from "../interfaces";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<AddEqCriteria<string> />", () => {
|
||||
const fakeProps = (): AddEqCriteriaProps<string> => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
type: "string",
|
||||
criteriaField: undefined,
|
||||
criteriaKey: "string_eq",
|
||||
});
|
||||
|
||||
it("renders string_eq add", () => {
|
||||
const wrapper = mount(<AddEqCriteria<string> {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("=");
|
||||
});
|
||||
|
||||
it("changes field", () => {
|
||||
const wrapper = shallow<AddEqCriteria<string>>(
|
||||
<AddEqCriteria<string> {...fakeProps()} />);
|
||||
wrapper.find("input").first().simulate("change", {
|
||||
currentTarget: { value: "openfarm_slug" }
|
||||
});
|
||||
expect(wrapper.state().key).toEqual("openfarm_slug");
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const wrapper = shallow<AddEqCriteria<string>>(
|
||||
<AddEqCriteria<string> {...fakeProps()} />);
|
||||
wrapper.find("input").last().simulate("change", {
|
||||
currentTarget: { value: "slug" }
|
||||
});
|
||||
expect(wrapper.state().value).toEqual("slug");
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<AddEqCriteria<string> {...p} />);
|
||||
wrapper.setState({ key: "openfarm_slug", value: "slug" });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(editCriteria).toHaveBeenCalledWith(p.group, {
|
||||
string_eq: { openfarm_slug: ["slug"] }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AddEqCriteria<number> />", () => {
|
||||
const fakeProps = (): AddEqCriteriaProps<number> => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
type: "number",
|
||||
criteriaField: {},
|
||||
criteriaKey: "number_eq",
|
||||
});
|
||||
|
||||
it("renders number_eq add", () => {
|
||||
const wrapper = mount(<AddEqCriteria<number> {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("=");
|
||||
});
|
||||
|
||||
it("changes field", () => {
|
||||
const wrapper = shallow<AddEqCriteria<number>>(
|
||||
<AddEqCriteria<number> {...fakeProps()} />);
|
||||
wrapper.find("input").first().simulate("change", {
|
||||
currentTarget: { value: "x" }
|
||||
});
|
||||
expect(wrapper.state().key).toEqual("x");
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const wrapper = shallow<AddEqCriteria<number>>(
|
||||
<AddEqCriteria<number> {...fakeProps()} />);
|
||||
wrapper.find("input").last().simulate("change", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
expect(wrapper.state().value).toEqual("1");
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<AddEqCriteria<number> {...p} />);
|
||||
wrapper.setState({ key: "x", value: 1 });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(editCriteria).toHaveBeenCalledWith(p.group, {
|
||||
number_eq: { x: [1] }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AddStringCriteria />", () => {
|
||||
const fakeProps = (): AddStringCriteriaProps => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
slugs: ["apple", "orange"],
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<AddStringCriteria {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("None");
|
||||
});
|
||||
|
||||
it("changes key", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.find("FBSelect").first().simulate("change", {
|
||||
value: "openfarm_slug", label: ""
|
||||
});
|
||||
expect(wrapper.state().key).toEqual("openfarm_slug");
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ key: "openfarm_slug" });
|
||||
wrapper.find("FBSelect").last().simulate("change", {
|
||||
label: "", value: "slug"
|
||||
});
|
||||
expect(wrapper.state().value).toEqual("slug");
|
||||
});
|
||||
|
||||
it("renders slug list", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ key: "openfarm_slug", value: "pear" });
|
||||
expect(wrapper.find("FBSelect").last().props().list).toEqual([
|
||||
{ label: "Apple", value: "apple" },
|
||||
{ label: "Orange", value: "orange" },
|
||||
]);
|
||||
expect(wrapper.instance().selected).toEqual({
|
||||
label: "Pear", value: "pear"
|
||||
});
|
||||
});
|
||||
|
||||
it("returns selected point type", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ key: "pointer_type", value: "" });
|
||||
expect(wrapper.instance().selected).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("renders point type list", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ key: "pointer_type", value: "Plant" });
|
||||
expect(wrapper.find("FBSelect").last().props().list)
|
||||
.toEqual(POINTER_TYPE_LIST());
|
||||
expect(wrapper.instance().selected).toEqual({
|
||||
label: "Plants", value: "Plant"
|
||||
});
|
||||
});
|
||||
|
||||
it("returns selected plant stage", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ key: "plant_stage", value: "" });
|
||||
expect(wrapper.instance().selected).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("renders plant stage list", () => {
|
||||
const wrapper = shallow<AddStringCriteria>(
|
||||
<AddStringCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ key: "plant_stage", value: "planted" });
|
||||
expect(wrapper.find("FBSelect").last().props().list)
|
||||
.toEqual(PLANT_STAGE_LIST());
|
||||
expect(wrapper.instance().selected).toEqual({
|
||||
label: "Planted", value: "planted"
|
||||
});
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<AddStringCriteria {...p} />);
|
||||
wrapper.setState({ key: "openfarm_slug", value: "slug" });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(toggleStringCriteria).toHaveBeenCalledWith(
|
||||
p.group, "openfarm_slug", "slug");
|
||||
});
|
||||
|
||||
it("doesn't update criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<AddStringCriteria {...p} />);
|
||||
wrapper.setState({ key: "openfarm_slug", value: "" });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(toggleStringCriteria).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AddNumberCriteria />", () => {
|
||||
const fakeProps = (): NumberCriteriaProps => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
criteriaKey: "number_lt",
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<AddNumberCriteria {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("<");
|
||||
});
|
||||
|
||||
it("changes field", () => {
|
||||
const wrapper = shallow<AddNumberCriteria>(
|
||||
<AddNumberCriteria {...fakeProps()} />);
|
||||
wrapper.find("input").first().simulate("change", {
|
||||
currentTarget: { value: "x" }
|
||||
});
|
||||
expect(wrapper.state().key).toEqual("x");
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const wrapper = shallow<AddNumberCriteria>(
|
||||
<AddNumberCriteria {...fakeProps()} />);
|
||||
wrapper.find("input").last().simulate("change", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
expect(wrapper.state().value).toEqual(1);
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<AddNumberCriteria {...p} />);
|
||||
wrapper.setState({ key: "x", value: 1 });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(editCriteria).toHaveBeenCalledWith(p.group, { number_lt: { x: 1 } });
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = mount(<AddNumberCriteria {...p} />);
|
||||
expect(wrapper.text()).toContain("<");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
import { selectPointsByCriteria, pointsSelectedByGroup } from "..";
|
||||
import {
|
||||
fakePoint, fakePlant, fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import moment from "moment";
|
||||
import { DEFAULT_CRITERIA } from "../interfaces";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
describe("selectPointsByCriteria()", () => {
|
||||
const fakeCriteria = (): PointGroup["criteria"] =>
|
||||
cloneDeep(DEFAULT_CRITERIA);
|
||||
|
||||
it("matches color", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.number_eq = { x: [], y: undefined, z: [] };
|
||||
criteria.string_eq = { "meta.color": ["red", "blue"] };
|
||||
const matchingPoint = fakePoint();
|
||||
matchingPoint.body.meta.color = "red";
|
||||
const otherPoint = fakePoint();
|
||||
otherPoint.body.meta = {};
|
||||
const allPoints = [matchingPoint, otherPoint];
|
||||
const result = selectPointsByCriteria(criteria, allPoints);
|
||||
expect(result).toEqual([matchingPoint]);
|
||||
});
|
||||
|
||||
it("matches positions: equal", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { "meta.color": [] };
|
||||
criteria.number_eq = { x: [0, 1], y: [100] };
|
||||
const matchingPoint1 = fakePoint();
|
||||
matchingPoint1.body.x = 0;
|
||||
matchingPoint1.body.y = 100;
|
||||
const matchingPoint2 = fakePoint();
|
||||
matchingPoint2.body.x = 1;
|
||||
matchingPoint2.body.y = 100;
|
||||
const otherPoint = fakePoint();
|
||||
otherPoint.body.x = 2;
|
||||
otherPoint.body.y = 100;
|
||||
const allPoints = [matchingPoint1, matchingPoint2, otherPoint];
|
||||
const result = selectPointsByCriteria(criteria, allPoints);
|
||||
expect(result).toEqual([matchingPoint1, matchingPoint2]);
|
||||
});
|
||||
|
||||
it("matches positions: gt/lt", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.number_gt = { x: 100 };
|
||||
criteria.number_lt = { x: 500 };
|
||||
const matchingPoint = fakePoint();
|
||||
matchingPoint.body.x = 200;
|
||||
const otherPoint = fakePoint();
|
||||
otherPoint.body.x = 0;
|
||||
const allPoints = [matchingPoint, otherPoint];
|
||||
const result = selectPointsByCriteria(criteria, allPoints);
|
||||
expect(result).toEqual([matchingPoint]);
|
||||
});
|
||||
|
||||
it("matches age greater than 1 day old", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.day = { days: 1, op: ">" };
|
||||
const matchingPoint = fakePoint();
|
||||
matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z";
|
||||
const otherPoint = fakePoint();
|
||||
otherPoint.body.created_at = "2020-02-20T20:00:00.000Z";
|
||||
const allPoints = [matchingPoint, otherPoint];
|
||||
const now = moment("2020-02-20T20:00:00.000Z");
|
||||
const result = selectPointsByCriteria(criteria, allPoints, now);
|
||||
expect(result).toEqual([matchingPoint]);
|
||||
});
|
||||
|
||||
it("matches age less than 1 day old", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.day = { days: 1, op: "<" };
|
||||
const matchingPoint = fakePoint();
|
||||
matchingPoint.body.created_at = "2020-02-20T20:00:00.000Z";
|
||||
const otherPoint = fakePoint();
|
||||
otherPoint.body.created_at = "2020-01-20T20:00:00.000Z";
|
||||
const allPoints = [matchingPoint, otherPoint];
|
||||
const now = moment("2020-02-20T20:00:00.000Z");
|
||||
const result = selectPointsByCriteria(criteria, allPoints, now);
|
||||
expect(result).toEqual([matchingPoint]);
|
||||
});
|
||||
|
||||
it("matches planted date less than 1 day old", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.day = { days: 1, op: "<" };
|
||||
const matchingPoint = fakePlant();
|
||||
matchingPoint.body.planted_at = "2020-02-20T20:00:00.000Z";
|
||||
matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z";
|
||||
const otherPoint = fakePlant();
|
||||
otherPoint.body.planted_at = "2020-01-20T20:00:00.000Z";
|
||||
otherPoint.body.created_at = "2020-01-20T20:00:00.000Z";
|
||||
const allPoints = [matchingPoint, otherPoint];
|
||||
const now = moment("2020-02-20T20:00:00.000Z");
|
||||
const result = selectPointsByCriteria(criteria, allPoints, now);
|
||||
expect(result).toEqual([matchingPoint]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pointsSelectedByGroup()", () => {
|
||||
it("returns group points", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.point_ids = [1];
|
||||
group.body.criteria.number_eq = { x: [123] };
|
||||
const selectedPoint1 = fakePoint();
|
||||
selectedPoint1.body.id = 1;
|
||||
const selectedPoint2 = fakePoint();
|
||||
selectedPoint2.body.id = 2;
|
||||
selectedPoint2.body.x = 123;
|
||||
const otherPoint = fakePoint();
|
||||
otherPoint.body.id = 0;
|
||||
const allPoints = [selectedPoint1, selectedPoint2, otherPoint];
|
||||
const result = pointsSelectedByGroup(group, allPoints);
|
||||
expect(result).toEqual([selectedPoint1, selectedPoint2]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
jest.mock("../../../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { GroupCriteria, GroupPointCountBreakdown } from "..";
|
||||
import {
|
||||
GroupCriteriaProps, GroupPointCountBreakdownProps, DEFAULT_CRITERIA
|
||||
} from "../interfaces";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { overwrite, save } from "../../../../api/crud";
|
||||
import { ExpandableHeader } from "../../../../ui";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<GroupCriteria />", () => {
|
||||
const fakeProps = (): GroupCriteriaProps => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
slugs: [],
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = mount(<GroupCriteria {...p} />);
|
||||
["criteria", "age selection"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
|
||||
it("clears criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupCriteria {...p} />);
|
||||
wrapper.find("button").first().simulate("click");
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria = DEFAULT_CRITERIA;
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(p.group.uuid);
|
||||
});
|
||||
|
||||
it("expands section", () => {
|
||||
const wrapper = mount(<GroupCriteria {...fakeProps()} />);
|
||||
expect(wrapper.text()).not.toContain("number criteria");
|
||||
wrapper.find(ExpandableHeader).simulate("click");
|
||||
expect(wrapper.text()).toContain("number criteria");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<GroupPointCountBreakdown />", () => {
|
||||
const fakeProps = (): GroupPointCountBreakdownProps => ({
|
||||
manualCount: 1,
|
||||
totalCount: 3,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<GroupPointCountBreakdown {...fakeProps()} />);
|
||||
["1manually selected", "2selected by criteria"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
jest.mock("../../../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
editCriteria, toggleEqCriteria,
|
||||
togglePointSelection, toggleStringCriteria, editGtLtCriteria
|
||||
} from "..";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { overwrite, save } from "../../../../api/crud";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import { DEFAULT_CRITERIA } from "../interfaces";
|
||||
|
||||
describe("editCriteria()", () => {
|
||||
it("edits criteria: all empty", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
editCriteria(group, {})(jest.fn());
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria = DEFAULT_CRITERIA;
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("edits criteria: empty update", () => {
|
||||
const group = fakePointGroup();
|
||||
editCriteria(group, {})(jest.fn());
|
||||
expect(overwrite).toHaveBeenCalledWith(group, group.body);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("edits criteria: full update", () => {
|
||||
const group = fakePointGroup();
|
||||
const criteria: PointGroup["criteria"] = {
|
||||
day: { days: 1, op: "<" },
|
||||
string_eq: { openfarm_slug: ["slug"] },
|
||||
number_eq: { x: [0] },
|
||||
number_gt: { x: 0 },
|
||||
number_lt: { x: 10 },
|
||||
};
|
||||
editCriteria(group, criteria)(jest.fn());
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria = criteria;
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleEqCriteria()", () => {
|
||||
it("adds criteria", () => {
|
||||
const result = toggleEqCriteria({})("openfarm_slug", "slug");
|
||||
expect(result).toEqual({ openfarm_slug: ["slug"] });
|
||||
});
|
||||
|
||||
it("removes criteria", () => {
|
||||
const result = toggleEqCriteria({ openfarm_slug: ["slug"] })(
|
||||
"openfarm_slug", "slug");
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
const dispatch = jest.fn(x => x(jest.fn()));
|
||||
|
||||
describe("togglePointSelection()", () => {
|
||||
it("adds criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
togglePointSelection(group)({ openfarm_slug: "slug" })(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleStringCriteria()", () => {
|
||||
it("adds criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
toggleStringCriteria(group, "openfarm_slug", "slug")(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
toggleStringCriteria(group, "openfarm_slug", "slug")(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria = cloneDeep(DEFAULT_CRITERIA);
|
||||
expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editGtLtCriteria()", () => {
|
||||
it("adds criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
const box = { x0: 0, y0: 2, x1: 3, y1: 4 };
|
||||
editGtLtCriteria(group, box)(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.number_gt = { x: 0, y: 2 };
|
||||
expectedBody.criteria.number_lt = { x: 3, y: 4 };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("doesn't edit criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
const box = { x0: undefined, y0: 2, x1: 3, y1: 4 };
|
||||
editGtLtCriteria(group, box)(dispatch);
|
||||
expect(overwrite).not.toHaveBeenCalled();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const box = { x0: 1, y0: 2, x1: 3, y1: 4 };
|
||||
editGtLtCriteria(group, box)(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria = cloneDeep(DEFAULT_CRITERIA);
|
||||
expectedBody.criteria.number_gt = { x: 1, y: 2 };
|
||||
expectedBody.criteria.number_lt = { x: 3, y: 4 };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
const mockToggle = jest.fn();
|
||||
jest.mock("../edit", () => ({
|
||||
togglePointSelection: jest.fn(() => mockToggle),
|
||||
toggleStringCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import {
|
||||
CheckboxSelections, togglePointSelection, criteriaSelected,
|
||||
} from "..";
|
||||
import { CheckboxSelectionsProps } from "../interfaces";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<CheckboxSelections />", () => {
|
||||
const fakeProps = (): CheckboxSelectionsProps => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = mount(<CheckboxSelections {...p} />);
|
||||
["planted plants", "detected weeds", "created points", "created weeds"
|
||||
].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("changes criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<CheckboxSelections {...p} />);
|
||||
wrapper.find("input").first().simulate("change");
|
||||
expect(togglePointSelection).toHaveBeenCalledWith(p.group);
|
||||
expect(mockToggle).toHaveBeenCalledWith({
|
||||
plant_stage: "planted", pointer_type: "Plant"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("criteriaSelected()", () => {
|
||||
it("returns selection state: false", () => {
|
||||
const result = criteriaSelected(undefined)({ pointer_type: "Plant" });
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns selection state: true", () => {
|
||||
const result = criteriaSelected({
|
||||
pointer_type: ["Plant"]
|
||||
})({ pointer_type: "Plant" });
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
jest.mock("../../../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
EqCriteriaSelection,
|
||||
NumberCriteriaSelection, DaySelection, LocationSelection, AddCriteria,
|
||||
} from "..";
|
||||
import {
|
||||
EqCriteriaSelectionProps,
|
||||
NumberCriteriaProps,
|
||||
CriteriaSelectionProps,
|
||||
DEFAULT_CRITERIA,
|
||||
LocationSelectionProps,
|
||||
GroupCriteriaProps
|
||||
} from "../interfaces";
|
||||
import {
|
||||
fakePointGroup
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { overwrite } from "../../../../api/crud";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { FBSelect } from "../../../../ui";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<EqCriteriaSelection<string> />", () => {
|
||||
const fakeProps = (): EqCriteriaSelectionProps<string> => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
type: "string",
|
||||
criteriaField: {},
|
||||
criteriaKey: "string_eq",
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<EqCriteriaSelection<string> {...p} />);
|
||||
expect(wrapper.text()).toContain("=");
|
||||
});
|
||||
|
||||
it("removes criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.criteriaField = { openfarm_slug: ["slug"] };
|
||||
const wrapper = mount(<EqCriteriaSelection<string> {...p} />);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.string_eq = {};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<NumberCriteriaSelection />", () => {
|
||||
const fakeProps = (): NumberCriteriaProps => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
criteriaKey: "number_lt",
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<NumberCriteriaSelection {...p} />);
|
||||
expect(wrapper.text()).toContain("<");
|
||||
});
|
||||
|
||||
it("removes criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.criteria.number_lt = { x: 1 };
|
||||
const wrapper = mount(<NumberCriteriaSelection {...p} />);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.number_lt = {};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DaySelection />", () => {
|
||||
const fakeProps = (): CriteriaSelectionProps => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
});
|
||||
|
||||
it("changes operator", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DaySelection {...p} />);
|
||||
wrapper.find(FBSelect).simulate("change", { label: "", value: "<" });
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.day.op = "<";
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
|
||||
it("changes day value", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DaySelection {...p} />);
|
||||
wrapper.find("input").last().simulate("change", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.day.days = 1;
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.criteria = {} as PointGroup["criteria"];
|
||||
const wrapper = shallow(<DaySelection {...p} />);
|
||||
expect(wrapper.find("input").last().props().value).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<LocationSelection />", () => {
|
||||
const fakeProps = (): LocationSelectionProps => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
});
|
||||
|
||||
it("changes number_gt", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<LocationSelection {...p} />);
|
||||
wrapper.find("input").first().simulate("blur", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.number_gt = { x: 1 };
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
|
||||
it("changes number_lt", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<LocationSelection {...p} />);
|
||||
wrapper.find("input").last().simulate("blur", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.number_lt = { x: 1, y: 1 };
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.criteria = {} as PointGroup["criteria"];
|
||||
const wrapper = shallow(<LocationSelection {...p} />);
|
||||
expect(wrapper.find("input").first().props().defaultValue).toEqual(undefined);
|
||||
expect(wrapper.find("input").last().props().defaultValue).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AddCriteria />", () => {
|
||||
const fakeProps = (): GroupCriteriaProps => ({
|
||||
slugs: [],
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn(y => y(jest.fn())))),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria.string_eq = {
|
||||
openfarm_slug: ["slug"],
|
||||
pointer_type: ["Plant"],
|
||||
plant_stage: ["planted"],
|
||||
};
|
||||
const wrapper = mount(<AddCriteria {...p} />);
|
||||
expect(wrapper.find("input").at(0).props().value).toEqual("Plant Type");
|
||||
expect(wrapper.find("input").at(1).props().value).toEqual("Slug");
|
||||
expect(wrapper.find("input").at(2).props().value).toEqual("Point Type");
|
||||
expect(wrapper.find("input").at(3).props().value).toEqual("Plants");
|
||||
expect(wrapper.find("input").at(4).props().value).toEqual("Plant Status");
|
||||
expect(wrapper.find("input").at(5).props().value).toEqual("Planted");
|
||||
});
|
||||
|
||||
it("removes criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria.string_eq = {
|
||||
openfarm_slug: ["slug"],
|
||||
pointer_type: ["Plant"],
|
||||
plant_stage: ["planted"],
|
||||
};
|
||||
const wrapper = mount(<AddCriteria {...p} />);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.string_eq = {
|
||||
openfarm_slug: ["slug"],
|
||||
pointer_type: ["Plant"],
|
||||
};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = mount(<AddCriteria {...p} />);
|
||||
expect(wrapper.text()).toEqual("SelectNone");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,215 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { cloneDeep, capitalize } from "lodash";
|
||||
import { Row, Col, FBSelect, DropDownItem } from "../../../ui";
|
||||
import { editCriteria, toggleStringCriteria } from ".";
|
||||
import {
|
||||
AddEqCriteriaProps,
|
||||
AddEqCriteriaState,
|
||||
NumberCriteriaProps,
|
||||
AddNumberCriteriaState,
|
||||
AddStringCriteriaProps,
|
||||
} from "./interfaces";
|
||||
|
||||
export class AddEqCriteria<T extends string | number>
|
||||
extends React.Component<AddEqCriteriaProps<T>, AddEqCriteriaState> {
|
||||
state: AddEqCriteriaState = { key: "", value: "" };
|
||||
|
||||
commit = () => {
|
||||
const { dispatch, group, criteriaKey, criteriaField } = this.props;
|
||||
const tempEqCriteria = cloneDeep(criteriaField || {});
|
||||
const tempValues = tempEqCriteria[this.state.key] || [];
|
||||
const value = this.props.type == "number"
|
||||
? parseInt(this.state.value)
|
||||
: this.state.value;
|
||||
this.state.value && tempValues.push(value as T);
|
||||
tempEqCriteria[this.state.key] = tempValues;
|
||||
dispatch(editCriteria(group, { [criteriaKey]: tempEqCriteria }));
|
||||
this.setState({ key: "", value: "" });
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className={`add-${this.props.type}-eq-criteria`}>
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<input type="string"
|
||||
placeholder={t("field")}
|
||||
value={this.state.key}
|
||||
onChange={e => this.setState({ key: e.currentTarget.value })} />
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
{"="}
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<input type={this.props.type}
|
||||
placeholder={t("value")}
|
||||
value={this.state.value}
|
||||
onChange={e => this.setState({ value: e.currentTarget.value })} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button green" onClick={this.commit}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const CRITERIA_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
||||
pointer_type: { label: t("Point Type"), value: "pointer_type" },
|
||||
plant_stage: { label: t("Plant Status"), value: "plant_stage" },
|
||||
openfarm_slug: { label: t("Plant Type"), value: "openfarm_slug" },
|
||||
});
|
||||
export const CRITERIA_TYPE_LIST = () => [
|
||||
CRITERIA_TYPE_DDI_LOOKUP().pointer_type,
|
||||
CRITERIA_TYPE_DDI_LOOKUP().plant_stage,
|
||||
CRITERIA_TYPE_DDI_LOOKUP().openfarm_slug,
|
||||
];
|
||||
|
||||
export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
||||
Plant: { label: t("Plants"), value: "Plant" },
|
||||
GenericPointer: { label: t("Points"), value: "GenericPointer" },
|
||||
ToolSlot: { label: t("Tool Slots"), value: "ToolSlot" },
|
||||
});
|
||||
export const POINTER_TYPE_LIST = () => [
|
||||
POINTER_TYPE_DDI_LOOKUP().Plant,
|
||||
POINTER_TYPE_DDI_LOOKUP().GenericPointer,
|
||||
POINTER_TYPE_DDI_LOOKUP().ToolSlot,
|
||||
];
|
||||
|
||||
export const PLANT_STAGE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
||||
planned: { label: t("Planned"), value: "planned" },
|
||||
planted: { label: t("Planted"), value: "planted" },
|
||||
sprouted: { label: t("Sprouted"), value: "sprouted" },
|
||||
harvested: { label: t("Harvested"), value: "harvested" },
|
||||
});
|
||||
export const PLANT_STAGE_LIST = () => [
|
||||
PLANT_STAGE_DDI_LOOKUP().planned,
|
||||
PLANT_STAGE_DDI_LOOKUP().planted,
|
||||
PLANT_STAGE_DDI_LOOKUP().sprouted,
|
||||
PLANT_STAGE_DDI_LOOKUP().harvested,
|
||||
];
|
||||
|
||||
export class AddStringCriteria
|
||||
extends React.Component<AddStringCriteriaProps, AddEqCriteriaState> {
|
||||
state: AddEqCriteriaState = { key: "", value: "" };
|
||||
|
||||
commit = () => {
|
||||
if (this.state.key && this.state.value) {
|
||||
this.props.dispatch(toggleStringCriteria(this.props.group,
|
||||
this.state.key, this.state.value));
|
||||
this.setState({ key: "", value: "" });
|
||||
}
|
||||
}
|
||||
|
||||
get key() { return JSON.stringify(this.props.group.body.criteria || {}); }
|
||||
|
||||
change = (ddi: DropDownItem) => this.setState({ value: "" + ddi.value });
|
||||
|
||||
get selected() {
|
||||
switch (this.state.key) {
|
||||
case "openfarm_slug":
|
||||
return this.state.value
|
||||
? { label: t(capitalize(this.state.value)), value: this.state.value }
|
||||
: undefined;
|
||||
case "pointer_type":
|
||||
return this.state.value
|
||||
? POINTER_TYPE_DDI_LOOKUP()[this.state.value]
|
||||
: undefined;
|
||||
case "plant_stage":
|
||||
return this.state.value
|
||||
? PLANT_STAGE_DDI_LOOKUP()[this.state.value]
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
get options() {
|
||||
switch (this.state.key) {
|
||||
case "openfarm_slug":
|
||||
return this.props.slugs.map(slug =>
|
||||
({ label: t(capitalize(slug)), value: slug }));
|
||||
case "pointer_type":
|
||||
return POINTER_TYPE_LIST();
|
||||
case "plant_stage":
|
||||
return PLANT_STAGE_LIST();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const noKey = this.options.length < 1;
|
||||
return <div className={"add-string-criteria"}>
|
||||
<Row>
|
||||
<Col xs={5}>
|
||||
<FBSelect key={this.key}
|
||||
customNullLabel={t("Select")}
|
||||
list={CRITERIA_TYPE_LIST()}
|
||||
selectedItem={CRITERIA_TYPE_DDI_LOOKUP()[this.state.key]}
|
||||
onChange={ddi => this.setState({ key: "" + ddi.value })} />
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
<FBSelect key={this.key}
|
||||
extraClass={noKey ? "disabled" : ""}
|
||||
list={this.options}
|
||||
selectedItem={this.selected}
|
||||
onChange={this.change} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button green" onClick={this.commit}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddNumberCriteria
|
||||
extends React.Component<NumberCriteriaProps, AddNumberCriteriaState> {
|
||||
state: AddNumberCriteriaState = { key: "", value: 0 };
|
||||
|
||||
commit = () => {
|
||||
const { dispatch, group, criteriaKey } = this.props;
|
||||
const tempNumberCriteria =
|
||||
cloneDeep(group.body.criteria?.[criteriaKey] || {});
|
||||
tempNumberCriteria[this.state.key] = this.state.value;
|
||||
dispatch(editCriteria(group, { [criteriaKey]: tempNumberCriteria }));
|
||||
this.setState({ key: "", value: 0 });
|
||||
}
|
||||
|
||||
changeKey = (e: React.FormEvent<HTMLInputElement>) =>
|
||||
this.setState({ key: e.currentTarget.value })
|
||||
|
||||
changeValue = (e: React.FormEvent<HTMLInputElement>) =>
|
||||
this.setState({ value: parseInt(e.currentTarget.value) })
|
||||
|
||||
render() {
|
||||
return <div className="add-number-criteria">
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<input type="string"
|
||||
placeholder={t("field")}
|
||||
value={this.state.key}
|
||||
onChange={this.changeKey} />
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
{this.props.criteriaKey == "number_gt" ? ">" : "<"}
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<input type="number"
|
||||
value={this.state.value}
|
||||
onChange={this.changeValue} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button green" onClick={this.commit}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { every, get, isEqual, uniq, gt, lt, isNumber } from "lodash";
|
||||
import { TaggedPoint, TaggedPointGroup } from "farmbot";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import moment from "moment";
|
||||
import { DEFAULT_CRITERIA } from "./interfaces";
|
||||
|
||||
const eqCriteriaEmpty =
|
||||
(eqCriteria: Record<string, (string | number)[] | undefined>) =>
|
||||
every(Object.values(eqCriteria).map(values => !values?.length));
|
||||
|
||||
const checkCriteria =
|
||||
(criteria: PointGroup["criteria"], now: moment.Moment) =>
|
||||
(point: TaggedPoint, criteriaKey: keyof PointGroup["criteria"]) => {
|
||||
switch (criteriaKey) {
|
||||
case "string_eq":
|
||||
case "number_eq":
|
||||
return every(Object.entries(criteria[criteriaKey])
|
||||
.map(([k, values]: [string, (string | number)[]]) =>
|
||||
values?.includes(get(point.body, k))))
|
||||
|| eqCriteriaEmpty(criteria[criteriaKey]);
|
||||
case "number_gt":
|
||||
case "number_lt":
|
||||
const compare = { number_gt: gt, number_lt: lt };
|
||||
return every(Object.entries(criteria[criteriaKey])
|
||||
.map(([key, value]) => isNumber(value) &&
|
||||
compare[criteriaKey](get(point.body, key), value)));
|
||||
case "day":
|
||||
const pointDate = moment(point.body.pointer_type == "Plant"
|
||||
&& point.body.planted_at
|
||||
? point.body.planted_at
|
||||
: point.body.created_at);
|
||||
const compareDate = moment(now)
|
||||
.subtract(criteria[criteriaKey].days, "days");
|
||||
const matchesDays = criteria[criteriaKey].op == "<"
|
||||
? pointDate.isAfter(compareDate)
|
||||
: pointDate.isBefore(compareDate);
|
||||
return matchesDays || !criteria[criteriaKey].days;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectPointsByCriteria = (
|
||||
criteria: PointGroup["criteria"] | undefined,
|
||||
allPoints: TaggedPoint[],
|
||||
now = moment(),
|
||||
): TaggedPoint[] => {
|
||||
if (!criteria || isEqual(criteria, DEFAULT_CRITERIA)) { return []; }
|
||||
const check = checkCriteria(criteria, now);
|
||||
return allPoints.filter(point =>
|
||||
every(Object.keys(criteria).map((key: keyof PointGroup["criteria"]) =>
|
||||
check(point, key))));
|
||||
};
|
||||
|
||||
export const pointsSelectedByGroup =
|
||||
(group: TaggedPointGroup, allPoints: TaggedPoint[]) =>
|
||||
uniq(allPoints
|
||||
.filter(p => group.body.point_ids.includes(p.body.id || 0))
|
||||
.concat(selectPointsByCriteria(group.body.criteria, allPoints)));
|
|
@ -0,0 +1,74 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { overwrite, save } from "../../../api/crud";
|
||||
import {
|
||||
CheckboxSelections, DaySelection, EqCriteriaSelection,
|
||||
NumberCriteriaSelection, LocationSelection, AddCriteria,
|
||||
} from ".";
|
||||
import {
|
||||
GroupCriteriaProps, GroupPointCountBreakdownProps, GroupCriteriaState,
|
||||
DEFAULT_CRITERIA,
|
||||
} from "./interfaces";
|
||||
import { ExpandableHeader } from "../../../ui";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
|
||||
export class GroupCriteria extends
|
||||
React.Component<GroupCriteriaProps, GroupCriteriaState> {
|
||||
state: GroupCriteriaState = { advanced: false, clearCount: 0 };
|
||||
render() {
|
||||
const { group, dispatch, slugs } = this.props;
|
||||
const criteria = group.body.criteria || {};
|
||||
const commonProps = { group, criteria, dispatch };
|
||||
return <div className="group-criteria">
|
||||
<label className="criteria-heading">{t("criteria")}</label>
|
||||
<button className="fb-button red" onClick={() => {
|
||||
dispatch(overwrite(group, {
|
||||
...group.body, criteria: DEFAULT_CRITERIA
|
||||
}));
|
||||
dispatch(save(group.uuid));
|
||||
}}>
|
||||
{t("clear all criteria")}
|
||||
</button>
|
||||
<div className="group-criteria-presets">
|
||||
<label>{t("presets")}</label>
|
||||
<CheckboxSelections group={group} dispatch={dispatch} />
|
||||
</div>
|
||||
<DaySelection {...commonProps} />
|
||||
<LocationSelection {...commonProps} />
|
||||
<label>{t("additional criteria")}</label>
|
||||
<AddCriteria group={group} dispatch={dispatch} slugs={slugs} />
|
||||
<ExpandableHeader
|
||||
expanded={this.state.advanced}
|
||||
title={t("Advanced")}
|
||||
onClick={() => this.setState({ advanced: !this.state.advanced })} />
|
||||
<Collapse isOpen={this.state.advanced}>
|
||||
<label>{t("string criteria")}</label>
|
||||
<EqCriteriaSelection<string> {...commonProps}
|
||||
type={"string"} criteriaField={criteria.string_eq}
|
||||
criteriaKey={"string_eq"} />
|
||||
<label>{t("number criteria")}</label>
|
||||
<EqCriteriaSelection<number> {...commonProps}
|
||||
type={"number"} criteriaField={criteria.number_eq}
|
||||
criteriaKey={"number_eq"} />
|
||||
<NumberCriteriaSelection {...commonProps} criteriaKey={"number_lt"} />
|
||||
<NumberCriteriaSelection {...commonProps} criteriaKey={"number_gt"} />
|
||||
</Collapse>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupPointCountBreakdown = (props: GroupPointCountBreakdownProps) =>
|
||||
<div className={"criteria-point-count-breakdown"}>
|
||||
<div className={"manual-group-member-count"}>
|
||||
<div>
|
||||
{props.manualCount}
|
||||
</div>
|
||||
<p>{t("manually selected")}</p>
|
||||
</div>
|
||||
<div className={"criteria-group-member-count"}>
|
||||
<div>
|
||||
{props.totalCount - props.manualCount}
|
||||
</div>
|
||||
<p>{t("selected by criteria")}</p>
|
||||
</div>
|
||||
</div>;
|
|
@ -0,0 +1,70 @@
|
|||
import { overwrite, save } from "../../../api/crud";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import { cloneDeep, isNumber } from "lodash";
|
||||
import { SelectionBoxData } from "../../map/background";
|
||||
import { DEFAULT_CRITERIA } from "./interfaces";
|
||||
|
||||
export const editCriteria =
|
||||
(group: TaggedPointGroup, update: Partial<PointGroup["criteria"]>) =>
|
||||
(dispatch: Function) => {
|
||||
const criteria = {
|
||||
string_eq: update.string_eq || group.body.criteria?.string_eq || {},
|
||||
day: update.day || group.body.criteria?.day || DEFAULT_CRITERIA.day,
|
||||
number_eq: update.number_eq || group.body.criteria?.number_eq || {},
|
||||
number_gt: update.number_gt || group.body.criteria?.number_gt || {},
|
||||
number_lt: update.number_lt || group.body.criteria?.number_lt || {},
|
||||
};
|
||||
dispatch(overwrite(group, { ...group.body, criteria }));
|
||||
dispatch(save(group.uuid));
|
||||
};
|
||||
|
||||
export const toggleEqCriteria = <T extends string | number>(
|
||||
eqCriteria: Record<string, T[] | undefined>
|
||||
) =>
|
||||
(key: string, value: T): Record<string, T[] | undefined> => {
|
||||
const values: T[] = eqCriteria[key] || [];
|
||||
if (values.includes(value)) {
|
||||
const newValues = values.filter(s => s != value);
|
||||
eqCriteria[key] = newValues;
|
||||
!newValues.length && delete eqCriteria[key];
|
||||
} else {
|
||||
values.push(value);
|
||||
eqCriteria[key] = values;
|
||||
}
|
||||
return eqCriteria;
|
||||
};
|
||||
|
||||
export const togglePointSelection =
|
||||
(group: TaggedPointGroup) => (toggleCriteria: Record<string, string>) =>
|
||||
(dispatch: Function) => {
|
||||
const stringCriteria = {};
|
||||
const toggle = toggleEqCriteria<string>(stringCriteria);
|
||||
Object.entries(toggleCriteria).map(([key, value]) => toggle(key, value));
|
||||
dispatch(editCriteria(group, { string_eq: stringCriteria }));
|
||||
};
|
||||
|
||||
export const toggleStringCriteria =
|
||||
(group: TaggedPointGroup, key: string, value: string) =>
|
||||
(dispatch: Function) => {
|
||||
const tempStringCriteria = cloneDeep(group.body.criteria?.string_eq || {});
|
||||
toggleEqCriteria<string>(tempStringCriteria)(key, value);
|
||||
dispatch(editCriteria(group, { string_eq: tempStringCriteria }));
|
||||
};
|
||||
|
||||
export const editGtLtCriteria =
|
||||
(group: TaggedPointGroup, box: SelectionBoxData) =>
|
||||
(dispatch: Function) => {
|
||||
if (!(isNumber(box.x0) && isNumber(box.y0)
|
||||
&& isNumber(box.x1) && isNumber(box.y1))) { return; }
|
||||
const tempGtCriteria = cloneDeep(group.body.criteria?.number_gt || {});
|
||||
const tempLtCriteria = cloneDeep(group.body.criteria?.number_lt || {});
|
||||
tempGtCriteria.x = Math.min(box.x0, box.x1);
|
||||
tempGtCriteria.y = Math.min(box.y0, box.y1);
|
||||
tempLtCriteria.x = Math.max(box.x0, box.x1);
|
||||
tempLtCriteria.y = Math.max(box.y0, box.y1);
|
||||
dispatch(editCriteria(group, {
|
||||
number_gt: tempGtCriteria,
|
||||
number_lt: tempLtCriteria,
|
||||
}));
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export * from "./add";
|
||||
export * from "./apply";
|
||||
export * from "./component";
|
||||
export * from "./edit";
|
||||
export * from "./presets";
|
||||
export * from "./show";
|
|
@ -0,0 +1,81 @@
|
|||
import { TaggedPointGroup } from "farmbot";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
export const DEFAULT_CRITERIA: Readonly<PointGroup["criteria"]> = {
|
||||
day: { op: "<", days: 0 },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {},
|
||||
};
|
||||
|
||||
export type EqCriteria = Record<string, (string | number)[] | undefined> | undefined;
|
||||
export type StringEqCriteria = PointGroup["criteria"]["string_eq"] | undefined;
|
||||
|
||||
export interface GroupCriteriaProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export interface GroupCriteriaState {
|
||||
advanced: boolean;
|
||||
clearCount: number;
|
||||
}
|
||||
|
||||
export interface GroupPointCountBreakdownProps {
|
||||
manualCount: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface CriteriaSelectionProps {
|
||||
criteria: PointGroup["criteria"];
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export interface LocationSelectionProps extends CriteriaSelectionProps {
|
||||
}
|
||||
|
||||
export interface EqCriteriaSelectionProps<T> extends CriteriaSelectionProps {
|
||||
type: "string" | "number";
|
||||
criteriaField: Record<string, T[] | undefined> | undefined;
|
||||
criteriaKey: keyof PointGroup["criteria"];
|
||||
}
|
||||
|
||||
export interface NumberCriteriaProps extends CriteriaSelectionProps {
|
||||
criteriaKey: "number_lt" | "number_gt";
|
||||
}
|
||||
|
||||
export interface AddEqCriteriaProps<T> {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
type: "string" | "number";
|
||||
criteriaField: Record<string, T[] | undefined> | undefined;
|
||||
criteriaKey: keyof PointGroup["criteria"];
|
||||
}
|
||||
|
||||
export interface AddEqCriteriaState {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
export interface AddCriteriaState {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface AddStringCriteriaProps {
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export interface AddNumberCriteriaState {
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface CheckboxSelectionsProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { every, } from "lodash";
|
||||
import { togglePointSelection } from ".";
|
||||
import { CheckboxSelectionsProps, StringEqCriteria } from "./interfaces";
|
||||
|
||||
const CRITERIA_PRESETS = (): {
|
||||
description: string, criteria: Record<string, string>
|
||||
}[] => [
|
||||
{
|
||||
description: t("planted plants"),
|
||||
criteria: {
|
||||
"pointer_type": "Plant",
|
||||
"plant_stage": "planted",
|
||||
}
|
||||
},
|
||||
{
|
||||
description: t("detected weeds"),
|
||||
criteria: {
|
||||
"meta.created_by": "plant-detection",
|
||||
"meta.color": "red",
|
||||
}
|
||||
},
|
||||
{
|
||||
description: t("created points"),
|
||||
criteria: {
|
||||
"meta.created_by": "farm-designer",
|
||||
"meta.type": "point",
|
||||
}
|
||||
},
|
||||
{
|
||||
description: t("created weeds"),
|
||||
criteria: {
|
||||
"meta.created_by": "farm-designer",
|
||||
"meta.type": "weed",
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const CheckboxSelections = (props: CheckboxSelectionsProps) => {
|
||||
const toggle = togglePointSelection(props.group);
|
||||
const stringCriteria = props.group.body.criteria?.string_eq;
|
||||
const selected = criteriaSelected(stringCriteria);
|
||||
return <div className={"criteria-checkbox-presets"}>
|
||||
{CRITERIA_PRESETS().map((selector, index) =>
|
||||
<div className="criteria-preset-checkbox" key={index}>
|
||||
<input type="radio"
|
||||
onChange={() => props.dispatch(toggle(selector.criteria))}
|
||||
checked={selected(selector.criteria)} />
|
||||
<p>{selector.description}</p>
|
||||
</div>)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const criteriaSelected = (stringCriteria: StringEqCriteria) =>
|
||||
(selectionCriteria: Record<string, string>) =>
|
||||
every(Object.entries(selectionCriteria).map(([key, value]) =>
|
||||
stringCriteria?.[key]?.includes(value)));
|
|
@ -0,0 +1,212 @@
|
|||
import * as React from "react";
|
||||
import { cloneDeep, capitalize } from "lodash";
|
||||
import { Row, Col, FBSelect, DropDownItem } from "../../../ui";
|
||||
import {
|
||||
AddEqCriteria, toggleEqCriteria, editCriteria, AddNumberCriteria,
|
||||
POINTER_TYPE_DDI_LOOKUP, PLANT_STAGE_DDI_LOOKUP, AddStringCriteria,
|
||||
CRITERIA_TYPE_DDI_LOOKUP, toggleStringCriteria
|
||||
} from ".";
|
||||
import {
|
||||
EqCriteriaSelectionProps, NumberCriteriaProps,
|
||||
CriteriaSelectionProps, LocationSelectionProps, GroupCriteriaProps,
|
||||
AddCriteriaState,
|
||||
DEFAULT_CRITERIA
|
||||
} from "./interfaces";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
export class EqCriteriaSelection<T extends string | number>
|
||||
extends React.Component<EqCriteriaSelectionProps<T>> {
|
||||
render() {
|
||||
const { criteriaField, criteriaKey, group, dispatch } = this.props;
|
||||
return <div className={`${this.props.type}-eq-criteria`}>
|
||||
<AddEqCriteria<T> group={group} dispatch={dispatch}
|
||||
type={this.props.type} criteriaField={criteriaField}
|
||||
criteriaKey={criteriaKey} />
|
||||
{criteriaField && Object.entries(criteriaField)
|
||||
.map(([key, values]: [string, T[]], keyIndex) =>
|
||||
values && values.length > 0 &&
|
||||
<div key={keyIndex}>
|
||||
<label>{key}</label>
|
||||
{values.map((value, valueIndex) =>
|
||||
<Row key={"" + keyIndex + valueIndex}>
|
||||
<Col xs={9}>
|
||||
<input
|
||||
disabled={true}
|
||||
value={value} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button red" onClick={() => {
|
||||
const tempCriteriaField = cloneDeep(criteriaField);
|
||||
toggleEqCriteria<T>(tempCriteriaField)(key, value);
|
||||
dispatch(editCriteria(group, {
|
||||
[criteriaKey]: tempCriteriaField
|
||||
}));
|
||||
}}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>)}
|
||||
</div>)}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const NumberCriteriaSelection = (props: NumberCriteriaProps) => {
|
||||
const criteriaField = props.criteria[props.criteriaKey];
|
||||
return <div className={"number-gt-lt-criteria"}>
|
||||
<AddNumberCriteria {...props} />
|
||||
{criteriaField && Object.entries(criteriaField)
|
||||
.map(([key, value], keyIndex) =>
|
||||
<div key={keyIndex}>
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<p>{key}</p>
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
{props.criteriaKey == "number_gt" ? ">" : "<"}
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<input key={"" + keyIndex}
|
||||
disabled={true}
|
||||
value={value} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button red" onClick={() => {
|
||||
const tempNumberCriteria = cloneDeep(criteriaField);
|
||||
delete tempNumberCriteria[key];
|
||||
props.dispatch(editCriteria(props.group, {
|
||||
[props.criteriaKey]: tempNumberCriteria
|
||||
}));
|
||||
}}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const DAY_OPERATOR_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
||||
["<"]: { label: t("less than"), value: "<" },
|
||||
[">"]: { label: t("greater than"), value: ">" },
|
||||
});
|
||||
|
||||
export const DaySelection = (props: CriteriaSelectionProps) => {
|
||||
const { group, criteria, dispatch } = props;
|
||||
const dayCriteria = criteria.day || cloneDeep(DEFAULT_CRITERIA.day);
|
||||
return <div className="day-criteria">
|
||||
<label>{t("Age selection")}</label>
|
||||
<Row>
|
||||
<Col xs={5}>
|
||||
<FBSelect key={JSON.stringify(criteria)}
|
||||
list={[DAY_OPERATOR_DDI_LOOKUP()["<"],
|
||||
DAY_OPERATOR_DDI_LOOKUP()[">"]]}
|
||||
selectedItem={DAY_OPERATOR_DDI_LOOKUP()[dayCriteria.op]}
|
||||
onChange={ddi => dispatch(editCriteria(group, {
|
||||
day: {
|
||||
days: dayCriteria.days,
|
||||
op: ddi.value as PointGroup["criteria"]["day"]["op"]
|
||||
}
|
||||
}))} />
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<input type="number" value={dayCriteria.days} onChange={e => {
|
||||
const { op } = dayCriteria;
|
||||
const days = parseInt(e.currentTarget.value);
|
||||
dispatch(editCriteria(group, { day: { days, op } }));
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<p>{t("days old")}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const LocationSelection = (props: LocationSelectionProps) => {
|
||||
const { group, criteria, dispatch } = props;
|
||||
const gtCriteria = criteria.number_gt || {};
|
||||
const ltCriteria = criteria.number_lt || {};
|
||||
return <div className="location-criteria">
|
||||
<label>{t("Location selection")}</label>
|
||||
{["x", "y"].map(axis =>
|
||||
<Row key={axis}>
|
||||
<Col xs={4}>
|
||||
<input key={JSON.stringify(gtCriteria)}
|
||||
type="number"
|
||||
defaultValue={gtCriteria[axis]}
|
||||
onBlur={e => {
|
||||
const tempGtCriteria = cloneDeep(gtCriteria);
|
||||
tempGtCriteria[axis] = parseInt(e.currentTarget.value);
|
||||
dispatch(editCriteria(group, { number_gt: tempGtCriteria }));
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<p>{"<"}</p>
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<label>{axis}</label>
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<p>{"<"}</p>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<input key={JSON.stringify(ltCriteria)}
|
||||
type="number"
|
||||
defaultValue={ltCriteria[axis]}
|
||||
onBlur={e => {
|
||||
const tempLtCriteria = cloneDeep(ltCriteria);
|
||||
tempLtCriteria[axis] = parseInt(e.currentTarget.value);
|
||||
dispatch(editCriteria(group, { number_lt: tempLtCriteria }));
|
||||
}} />
|
||||
</Col>
|
||||
</Row>)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export class AddCriteria
|
||||
extends React.Component<GroupCriteriaProps, AddCriteriaState> {
|
||||
|
||||
labelLookup = (key: string, value: string) => {
|
||||
switch (key) {
|
||||
case "openfarm_slug":
|
||||
return capitalize(value);
|
||||
case "pointer_type":
|
||||
return POINTER_TYPE_DDI_LOOKUP()[value].label;
|
||||
case "plant_stage":
|
||||
return PLANT_STAGE_DDI_LOOKUP()[value].label;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const stringCriteria = this.props.group.body.criteria?.string_eq || {};
|
||||
const displayedCriteria = Object.entries(stringCriteria)
|
||||
.filter(([key, _values]) =>
|
||||
["openfarm_slug", "pointer_type", "plant_stage"].includes(key));
|
||||
return <div className={"add-criteria"}>
|
||||
<AddStringCriteria
|
||||
group={props.group} dispatch={props.dispatch} slugs={props.slugs} />
|
||||
{displayedCriteria.map(([key, values]) =>
|
||||
values && values.map((value, index) =>
|
||||
<div key={key + index} className={"criteria-string"}>
|
||||
<Row>
|
||||
<Col xs={5}>
|
||||
<input value={CRITERIA_TYPE_DDI_LOOKUP()[key].label}
|
||||
disabled={true} />
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
<input value={this.labelLookup(key, value)} disabled={true} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button red" onClick={() => props.dispatch(
|
||||
toggleStringCriteria(props.group, key, value))}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>))}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -2,55 +2,40 @@ import * as React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
import { findByKindAndId } from "../../resources/selectors";
|
||||
import { betterCompact } from "../../util/util";
|
||||
import {
|
||||
selectAllActivePoints, selectAllPlantPointers, selectAllPointGroups
|
||||
} from "../../resources/selectors";
|
||||
import { push, getPathArray } from "../../history";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
import { GroupDetailActive } from "./group_detail_active";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { ShouldDisplay } from "../../devices/interfaces";
|
||||
import { getShouldDisplayFn } from "../../farmware/state_to_props";
|
||||
import { uniq } from "lodash";
|
||||
|
||||
interface GroupDetailProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup | undefined;
|
||||
plants: TaggedPlant[];
|
||||
allPoints: TaggedPoint[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export function fetchGroupFromUrl(index: ResourceIndex) {
|
||||
if (!getPathArray().includes("groups")) { return; }
|
||||
/** TODO: Write better selectors. */
|
||||
/** Find a group from a URL-provided ID. */
|
||||
export const findGroupFromUrl = (groups: TaggedPointGroup[]) => {
|
||||
const urlIncludes = (string: string) => getPathArray().includes(string);
|
||||
if (!urlIncludes("groups") && !urlIncludes("zones")) { return; }
|
||||
const groupId = parseInt(getPathArray().pop() || "?", 10);
|
||||
let group: TaggedPointGroup | undefined;
|
||||
try {
|
||||
group = findByKindAndId<TaggedPointGroup>(index, "PointGroup", groupId);
|
||||
} catch (error) {
|
||||
group = undefined;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
return groups.filter(group => group.body.id === groupId)[0];
|
||||
};
|
||||
|
||||
function mapStateToProps(props: Everything): GroupDetailProps {
|
||||
const plants: TaggedPlant[] = [];
|
||||
const group = fetchGroupFromUrl(props.resources.index);
|
||||
if (group) {
|
||||
betterCompact(group
|
||||
.body
|
||||
.point_ids
|
||||
.map((id) => {
|
||||
return props.resources.index.byKindAndId[`Point.${id}`];
|
||||
})).map(uuid => {
|
||||
const p =
|
||||
props.resources.index.references[uuid] as TaggedPoint | undefined;
|
||||
if (p) {
|
||||
if (p.kind === "Point") {
|
||||
if (p.body.pointer_type == "Plant") {
|
||||
plants.push(p as TaggedPlant); // Sorry.
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { plants, group, dispatch: props.dispatch };
|
||||
return {
|
||||
allPoints: selectAllActivePoints(props.resources.index),
|
||||
group: findGroupFromUrl(selectAllPointGroups(props.resources.index)),
|
||||
dispatch: props.dispatch,
|
||||
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
|
||||
slugs: uniq(selectAllPlantPointers(props.resources.index)
|
||||
.map(p => p.body.openfarm_slug)),
|
||||
};
|
||||
}
|
||||
|
||||
export class RawGroupDetail extends React.Component<GroupDetailProps, {}> {
|
||||
|
|
|
@ -4,20 +4,27 @@ import { t } from "../../i18next_wrapper";
|
|||
import {
|
||||
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
|
||||
} from "../designer_panel";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
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";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
import { PointGroupItem } from "./point_group_item";
|
||||
import { Paths } from "./paths";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { Feature, ShouldDisplay } from "../../devices/interfaces";
|
||||
import { ErrorBoundary } from "../../error_boundary";
|
||||
import {
|
||||
GroupCriteria, GroupPointCountBreakdown, pointsSelectedByGroup
|
||||
} from "./criteria";
|
||||
import { Content } from "../../constants";
|
||||
|
||||
interface GroupDetailActiveProps {
|
||||
export interface GroupDetailActiveProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
plants: TaggedPlant[];
|
||||
allPoints: TaggedPoint[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
type State = { timerId?: ReturnType<typeof setInterval> };
|
||||
|
@ -30,16 +37,19 @@ export class GroupDetailActive
|
|||
this.props.dispatch(edit(this.props.group, { name: currentTarget.value }));
|
||||
};
|
||||
|
||||
get icons() {
|
||||
const plants = sortGroupBy(this.props.group.body.sort_type,
|
||||
this.props.plants);
|
||||
get pointsSelectedByGroup() {
|
||||
return pointsSelectedByGroup(this.props.group, this.props.allPoints);
|
||||
}
|
||||
|
||||
return plants.map(point => {
|
||||
get icons() {
|
||||
const sortedPoints =
|
||||
sortGroupBy(this.props.group.body.sort_type, this.pointsSelectedByGroup);
|
||||
return sortedPoints.map(point => {
|
||||
return <PointGroupItem
|
||||
key={point.uuid}
|
||||
hovered={false}
|
||||
group={this.props.group}
|
||||
plant={point}
|
||||
point={point}
|
||||
dispatch={this.props.dispatch} />;
|
||||
});
|
||||
}
|
||||
|
@ -70,6 +80,7 @@ export class GroupDetailActive
|
|||
}
|
||||
|
||||
render() {
|
||||
const { group, dispatch } = this.props;
|
||||
return <DesignerPanel panelName={"group-detail"} panel={Panel.Groups}>
|
||||
<DesignerPanelHeader
|
||||
onBack={this.saveGroup}
|
||||
|
@ -79,35 +90,46 @@ export class GroupDetailActive
|
|||
backTo={"/app/designer/groups"} />
|
||||
<DesignerPanelContent
|
||||
panelName={"groups"}>
|
||||
<label>{t("GROUP NAME")}{this.saved ? "" : "*"}</label>
|
||||
<input
|
||||
defaultValue={this.props.group.body.name}
|
||||
onChange={this.update}
|
||||
onBlur={this.saveGroup} />
|
||||
<PointGroupSortSelector
|
||||
value={this.props.group.body.sort_type}
|
||||
onChange={this.changeSortType} />
|
||||
<label>
|
||||
{t("GROUP MEMBERS ({{count}})", { count: this.icons.length })}
|
||||
</label>
|
||||
<p>
|
||||
{t("Click plants in map to add or remove.")}
|
||||
</p>
|
||||
<div className="groups-list-wrapper">
|
||||
{this.icons}
|
||||
</div>
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<Paths
|
||||
points={this.props.plants}
|
||||
dispatch={this.props.dispatch}
|
||||
group={this.props.group} />}
|
||||
<DeleteButton
|
||||
className="groups-delete-btn"
|
||||
dispatch={this.props.dispatch}
|
||||
uuid={this.props.group.uuid}
|
||||
onDestroy={history.back}>
|
||||
{t("DELETE GROUP")}
|
||||
</DeleteButton>
|
||||
<ErrorBoundary>
|
||||
<label>{t("GROUP NAME")}</label>
|
||||
<i style={{ float: "right" }}>{this.saved ? "" : " saving..."}</i>
|
||||
<input
|
||||
defaultValue={group.body.name}
|
||||
onChange={this.update}
|
||||
onBlur={this.saveGroup} />
|
||||
<PointGroupSortSelector
|
||||
value={group.body.sort_type}
|
||||
onChange={this.changeSortType} />
|
||||
<label>
|
||||
{t("GROUP MEMBERS ({{count}})", { count: this.icons.length })}
|
||||
</label>
|
||||
{this.props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupPointCountBreakdown
|
||||
manualCount={group.body.point_ids.length}
|
||||
totalCount={this.pointsSelectedByGroup.length} />}
|
||||
<p>{t("Click plants in map to add or remove.")}</p>
|
||||
{this.props.shouldDisplay(Feature.criteria_groups) &&
|
||||
this.pointsSelectedByGroup.length != group.body.point_ids.length &&
|
||||
<p>{t(Content.CRITERIA_SELECTION_COUNT)}</p>}
|
||||
<div className="groups-list-wrapper">
|
||||
{this.icons}
|
||||
</div>
|
||||
{this.props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupCriteria dispatch={dispatch}
|
||||
group={group} slugs={this.props.slugs} />}
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<Paths
|
||||
pathPoints={this.pointsSelectedByGroup}
|
||||
dispatch={dispatch}
|
||||
group={group} />}
|
||||
<DeleteButton
|
||||
className="group-delete-btn"
|
||||
dispatch={dispatch}
|
||||
uuid={group.uuid}
|
||||
onDestroy={history.back}>
|
||||
{t("DELETE GROUP")}
|
||||
</DeleteButton>
|
||||
</ErrorBoundary>
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import React from "react";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { pointsSelectedByGroup } from "./criteria";
|
||||
|
||||
interface GroupInventoryItemProps {
|
||||
export interface GroupInventoryItemProps {
|
||||
group: TaggedPointGroup;
|
||||
allPoints: TaggedPoint[];
|
||||
hovered: boolean;
|
||||
dispatch: Function;
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
export function GroupInventoryItem(props: GroupInventoryItemProps) {
|
||||
const count = pointsSelectedByGroup(props.group, props.allPoints).length;
|
||||
return <div
|
||||
onClick={props.onClick}
|
||||
className={`group-search-item ${props.hovered ? "hovered" : ""}`}>
|
||||
|
@ -17,7 +20,7 @@ export function GroupInventoryItem(props: GroupInventoryItemProps) {
|
|||
{props.group.body.name}
|
||||
</span>
|
||||
<i className="group-item-count">
|
||||
{t("{{count}} items", { count: props.group.body.point_ids.length })}
|
||||
{t("{{count}} items", { count })}
|
||||
</i>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -7,15 +7,17 @@ import {
|
|||
DesignerPanel, DesignerPanelTop, DesignerPanelContent
|
||||
} from "../designer_panel";
|
||||
import { findAll } from "../../resources/find_all";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
import { history } from "../../history";
|
||||
import { GroupInventoryItem } from "./group_inventory_item";
|
||||
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
|
||||
import { Content } from "../../constants";
|
||||
import { selectAllActivePoints } from "../../resources/selectors";
|
||||
|
||||
export interface GroupListPanelProps {
|
||||
dispatch: Function;
|
||||
groups: TaggedPointGroup[];
|
||||
allPoints: TaggedPoint[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -23,9 +25,11 @@ interface State {
|
|||
}
|
||||
|
||||
export function mapStateToProps(props: Everything): GroupListPanelProps {
|
||||
const groups =
|
||||
findAll<TaggedPointGroup>(props.resources.index, "PointGroup");
|
||||
return { groups, dispatch: props.dispatch };
|
||||
return {
|
||||
groups: findAll<TaggedPointGroup>(props.resources.index, "PointGroup"),
|
||||
dispatch: props.dispatch,
|
||||
allPoints: selectAllActivePoints(props.resources.index)
|
||||
};
|
||||
}
|
||||
|
||||
export class RawGroupListPanel extends React.Component<GroupListPanelProps, State> {
|
||||
|
@ -62,6 +66,7 @@ export class RawGroupListPanel extends React.Component<GroupListPanelProps, Stat
|
|||
.map(group => <GroupInventoryItem
|
||||
key={group.uuid}
|
||||
group={group}
|
||||
allPoints={this.props.allPoints}
|
||||
hovered={false}
|
||||
dispatch={this.props.dispatch}
|
||||
onClick={() => this.navigate(group.body.id || 0)}
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
import * as React from "react";
|
||||
import { store } from "../../redux/store";
|
||||
import { MapTransformProps, TaggedPlant } from "../map/interfaces";
|
||||
import { fetchGroupFromUrl } from "./group_detail";
|
||||
import { MapTransformProps } from "../map/interfaces";
|
||||
import { isUndefined } from "lodash";
|
||||
import { sortGroupBy } from "./point_group_sort_selector";
|
||||
import { Color } from "../../ui";
|
||||
import { transformXY } from "../map/util";
|
||||
import { nn } from "./paths";
|
||||
import { TaggedPoint, TaggedPointGroup } from "farmbot";
|
||||
|
||||
export interface GroupOrderProps {
|
||||
plants: TaggedPlant[];
|
||||
group: TaggedPointGroup | undefined;
|
||||
groupPoints: TaggedPoint[];
|
||||
mapTransformProps: MapTransformProps;
|
||||
}
|
||||
|
||||
const sortedPointCoordinates =
|
||||
(plants: TaggedPlant[]): { x: number, y: number }[] => {
|
||||
const { resources } = store.getState();
|
||||
const group = fetchGroupFromUrl(resources.index);
|
||||
if (isUndefined(group)) { return []; }
|
||||
const groupPlants = plants
|
||||
.filter(p => group.body.point_ids.includes(p.body.id || 0));
|
||||
const groupSortType = resources.consumers.farm_designer.tryGroupSortType
|
||||
|| group.body.sort_type;
|
||||
const sorted = groupSortType == "nn"
|
||||
? nn(groupPlants)
|
||||
: sortGroupBy(groupSortType, groupPlants);
|
||||
return sorted.map(p => ({ x: p.body.x, y: p.body.y }));
|
||||
};
|
||||
const sortedPointCoordinates = (
|
||||
group: TaggedPointGroup | undefined, groupPoints: TaggedPoint[]
|
||||
): { x: number, y: number }[] => {
|
||||
if (isUndefined(group)) { return []; }
|
||||
const { resources } = store.getState();
|
||||
const groupSortType = resources.consumers.farm_designer.tryGroupSortType
|
||||
|| group.body.sort_type;
|
||||
const sorted = groupSortType == "nn"
|
||||
? nn(groupPoints)
|
||||
: sortGroupBy(groupSortType, groupPoints);
|
||||
return sorted.map(p => ({ x: p.body.x, y: p.body.y }));
|
||||
};
|
||||
|
||||
export interface PointsPathLineProps {
|
||||
orderedPoints: { x: number, y: number }[];
|
||||
|
@ -51,5 +50,5 @@ export const PointsPathLine = (props: PointsPathLineProps) =>
|
|||
|
||||
export const GroupOrder = (props: GroupOrderProps) =>
|
||||
<PointsPathLine
|
||||
orderedPoints={sortedPointCoordinates(props.plants)}
|
||||
orderedPoints={sortedPointCoordinates(props.group, props.groupPoints)}
|
||||
mapTransformProps={props.mapTransformProps} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { TaggedPlant, MapTransformProps } from "../map/interfaces";
|
||||
import { MapTransformProps } from "../map/interfaces";
|
||||
import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector";
|
||||
import { sortBy } from "lodash";
|
||||
import { PointsPathLine } from "./group_order_visual";
|
||||
|
@ -8,18 +8,18 @@ import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
|||
import { t } from "../../i18next_wrapper";
|
||||
import { Actions } from "../../constants";
|
||||
import { edit } from "../../api/crud";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
import { error } from "../../toast/toast";
|
||||
|
||||
const xy = (point: TaggedPlant) => ({ x: point.body.x, y: point.body.y });
|
||||
const xy = (point: TaggedPoint) => ({ x: point.body.x, y: point.body.y });
|
||||
|
||||
const distance = (p1: { x: number, y: number }, p2: { x: number, y: number }) =>
|
||||
Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5);
|
||||
|
||||
const pathDistance = (points: TaggedPlant[]) => {
|
||||
const pathDistance = (pathPoints: TaggedPoint[]) => {
|
||||
let total = 0;
|
||||
let prev: { x: number, y: number } | undefined = undefined;
|
||||
points.map(xy)
|
||||
pathPoints.map(xy)
|
||||
.map(p => {
|
||||
prev ? total += distance(p, prev) : 0;
|
||||
prev = p;
|
||||
|
@ -28,18 +28,18 @@ const pathDistance = (points: TaggedPlant[]) => {
|
|||
};
|
||||
|
||||
const findNearest =
|
||||
(from: { x: number, y: number }, available: TaggedPlant[]) => {
|
||||
(from: { x: number, y: number }, available: TaggedPoint[]) => {
|
||||
const distances = available.map(p => ({
|
||||
point: p, distance: distance(xy(p), from)
|
||||
}));
|
||||
return sortBy(distances, "distance")[0].point;
|
||||
};
|
||||
|
||||
export const nn = (points: TaggedPlant[]) => {
|
||||
let available = points.slice(0);
|
||||
const ordered: TaggedPlant[] = [];
|
||||
export const nn = (pathPoints: TaggedPoint[]) => {
|
||||
let available = pathPoints.slice(0);
|
||||
const ordered: TaggedPoint[] = [];
|
||||
let from = { x: 0, y: 0 };
|
||||
points.map(() => {
|
||||
pathPoints.map(() => {
|
||||
if (available.length < 1) { return; }
|
||||
const nearest = findNearest(from, available);
|
||||
ordered.push(nearest);
|
||||
|
@ -80,8 +80,8 @@ export const PathInfoBar = (props: PathInfoBarProps) => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
interface PathsProps {
|
||||
points: TaggedPlant[];
|
||||
export interface PathsProps {
|
||||
pathPoints: TaggedPoint[];
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
}
|
||||
|
@ -93,15 +93,15 @@ interface PathsState {
|
|||
export class Paths extends React.Component<PathsProps, PathsState> {
|
||||
state: PathsState = { pathData: {} };
|
||||
|
||||
generatePathData = (points: TaggedPlant[]) => {
|
||||
generatePathData = (pathPoints: TaggedPoint[]) => {
|
||||
SORT_TYPES.map((sortType: PointGroupSortType) =>
|
||||
this.state.pathData[sortType] =
|
||||
pathDistance(sortGroupBy(sortType, points)));
|
||||
this.state.pathData.nn = pathDistance(nn(points));
|
||||
pathDistance(sortGroupBy(sortType, pathPoints)));
|
||||
this.state.pathData.nn = pathDistance(nn(pathPoints));
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.pathData.nn) { this.generatePathData(this.props.points); }
|
||||
if (!this.state.pathData.nn) { this.generatePathData(this.props.pathPoints); }
|
||||
return <div>
|
||||
<label>{t("Path lengths by sort type")}</label>
|
||||
{SORT_TYPES.concat("nn").map(st =>
|
||||
|
@ -115,7 +115,7 @@ export class Paths extends React.Component<PathsProps, PathsState> {
|
|||
}
|
||||
|
||||
interface NNPathProps {
|
||||
plants: TaggedPlant[];
|
||||
pathPoints: TaggedPoint[];
|
||||
mapTransformProps: MapTransformProps;
|
||||
}
|
||||
|
||||
|
@ -125,6 +125,6 @@ export const NNPath = (props: NNPathProps) =>
|
|||
color={Color.blue}
|
||||
strokeWidth={2}
|
||||
dash={1}
|
||||
orderedPoints={nn(props.plants).map(xy)}
|
||||
orderedPoints={nn(props.pathPoints).map(xy)}
|
||||
mapTransformProps={props.mapTransformProps} />
|
||||
: <g />;
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
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 { setImgSrc, maybeGetCachedPlantIcon } from "../../open_farm/cached_crop";
|
||||
import { setHoveredPlant } from "../map/actions";
|
||||
import { TaggedPointGroup, uuid } from "farmbot";
|
||||
import { TaggedPointGroup, uuid, TaggedPoint } from "farmbot";
|
||||
import { overwrite } from "../../api/crud";
|
||||
|
||||
type IMGEvent = React.SyntheticEvent<HTMLImageElement>;
|
||||
import { error } from "../../toast/toast";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
export interface PointGroupItemProps {
|
||||
plant: TaggedPlant;
|
||||
point: TaggedPoint;
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
hovered: boolean;
|
||||
|
@ -24,6 +23,18 @@ const removePoint = (group: TaggedPointGroup, pointId: number) => {
|
|||
return overwrite(group, nextGroup);
|
||||
};
|
||||
|
||||
export const genericPointIcon = (color: string | undefined) =>
|
||||
`<svg xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none' stroke-width='1.5' stroke='${color || "gray"}'>
|
||||
<circle cx='15' cy='15' r='12' />
|
||||
<circle cx='15' cy='15' r='2' />
|
||||
</svg>`;
|
||||
|
||||
export const OTHER_POINT_ICON =
|
||||
`<svg xmlns='http://www.w3.org/2000/svg' fill='gray'>
|
||||
<circle cx='15' cy='15' r='12' />
|
||||
</svg>`;
|
||||
|
||||
// The individual plants in the point group detail page.
|
||||
export class PointGroupItem
|
||||
extends React.Component<PointGroupItemProps, PointGroupItemState> {
|
||||
|
@ -33,24 +44,41 @@ export class PointGroupItem
|
|||
key = uuid();
|
||||
|
||||
enter = () => this.props.dispatch(
|
||||
setHoveredPlant(this.props.plant.uuid, this.state.icon));
|
||||
setHoveredPlant(this.props.point.uuid, this.state.icon));
|
||||
|
||||
leave = () => this.props.dispatch(setHoveredPlant(undefined));
|
||||
|
||||
click = () => {
|
||||
if (this.criteriaIcon) {
|
||||
return error(t("Cannot remove points selected by criteria."));
|
||||
}
|
||||
this.props.dispatch(
|
||||
removePoint(this.props.group, this.props.plant.body.id || 0));
|
||||
removePoint(this.props.group, this.props.point.body.id || 0));
|
||||
this.leave();
|
||||
}
|
||||
|
||||
maybeGetCachedIcon = ({ currentTarget }: IMGEvent) => {
|
||||
return cachedCrop(this.props.plant.body.openfarm_slug).then((crop) => {
|
||||
const i = svgToUrl(crop.svg_icon);
|
||||
if (i !== currentTarget.getAttribute("src")) {
|
||||
currentTarget.setAttribute("src", i);
|
||||
}
|
||||
this.setState({ icon: i });
|
||||
});
|
||||
get criteriaIcon() {
|
||||
return !this.props.group.body.point_ids
|
||||
.includes(this.props.point.body.id || 0);
|
||||
}
|
||||
|
||||
maybeGetCachedIcon = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const img = e.currentTarget;
|
||||
switch (this.props.point.body.pointer_type) {
|
||||
case "Plant":
|
||||
const slug = this.props.point.body.openfarm_slug;
|
||||
maybeGetCachedPlantIcon(slug, img, icon => this.setState({ icon }));
|
||||
break;
|
||||
case "GenericPointer":
|
||||
const { color } = this.props.point.body.meta;
|
||||
const pointIcon = svgToUrl(genericPointIcon(color));
|
||||
setImgSrc(img, pointIcon);
|
||||
break;
|
||||
default:
|
||||
const otherIcon = svgToUrl(OTHER_POINT_ICON);
|
||||
setImgSrc(img, otherIcon);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -60,6 +88,10 @@ export class PointGroupItem
|
|||
onMouseLeave={this.leave}
|
||||
onClick={this.click}>
|
||||
<img
|
||||
style={{
|
||||
border: this.criteriaIcon ? "1px solid gray" : "none",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
src={DEFAULT_ICON}
|
||||
onLoad={this.maybeGetCachedIcon}
|
||||
width={32}
|
||||
|
|
|
@ -2,11 +2,11 @@ import * as React from "react";
|
|||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
import { FBSelect, DropDownItem } from "../../ui";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { shuffle, sortBy } from "lodash";
|
||||
import { Content } from "../../constants";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
|
||||
interface Props {
|
||||
export interface PointGroupSortSelectorProps {
|
||||
onChange(value: PointGroupSortType): void;
|
||||
value: PointGroupSortType;
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => {
|
|||
isSortType(value) && cb(value);
|
||||
};
|
||||
|
||||
export function PointGroupSortSelector(p: Props) {
|
||||
export function PointGroupSortSelector(p: PointGroupSortSelectorProps) {
|
||||
|
||||
return <div>
|
||||
<div className="default-value-tooltip">
|
||||
|
@ -60,25 +60,25 @@ export function PointGroupSortSelector(p: Props) {
|
|||
</div>;
|
||||
}
|
||||
|
||||
type Sorter = (p: TaggedPlant[]) => TaggedPlant[];
|
||||
type Sorter = (p: TaggedPoint[]) => TaggedPoint[];
|
||||
type SortDictionary = Record<PointGroupSortType, Sorter>;
|
||||
|
||||
export const SORT_OPTIONS: SortDictionary = {
|
||||
random(plants) {
|
||||
return shuffle(plants);
|
||||
random(points) {
|
||||
return shuffle(points);
|
||||
},
|
||||
xy_ascending(plants) {
|
||||
return sortBy(plants, ["body.x", "body.y"]);
|
||||
xy_ascending(points) {
|
||||
return sortBy(points, ["body.x", "body.y"]);
|
||||
},
|
||||
xy_descending(plants) {
|
||||
return sortBy(plants, ["body.x", "body.y"]).reverse();
|
||||
xy_descending(points) {
|
||||
return sortBy(points, ["body.x", "body.y"]).reverse();
|
||||
},
|
||||
yx_ascending(plants) {
|
||||
return sortBy(plants, ["body.y", "body.x"]);
|
||||
yx_ascending(points) {
|
||||
return sortBy(points, ["body.y", "body.x"]);
|
||||
},
|
||||
yx_descending(plants) {
|
||||
return sortBy(plants, ["body.y", "body.x"]).reverse();
|
||||
yx_descending(points) {
|
||||
return sortBy(points, ["body.y", "body.x"]).reverse();
|
||||
}
|
||||
};
|
||||
export const sortGroupBy =
|
||||
(st: PointGroupSortType, p: TaggedPlant[]) => SORT_OPTIONS[st](p);
|
||||
(st: PointGroupSortType, p: TaggedPoint[]) => SORT_OPTIONS[st](p);
|
||||
|
|
|
@ -23,7 +23,7 @@ import { Actions } from "../../../constants";
|
|||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { CurrentPointPayl } from "../../interfaces";
|
||||
import { inputEvent } from "../../../__test_support__/fake_input_event";
|
||||
import { inputEvent } from "../../../__test_support__/fake_html_events";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const FAKE_POINT: CurrentPointPayl =
|
||||
|
|
|
@ -16,7 +16,7 @@ import { mapStateToProps } from "../point_inventory";
|
|||
|
||||
describe("<Points> />", () => {
|
||||
const fakeProps = (): PointsProps => ({
|
||||
points: [],
|
||||
genericPoints: [],
|
||||
dispatch: jest.fn(),
|
||||
hoveredPoint: undefined,
|
||||
});
|
||||
|
@ -28,15 +28,15 @@ describe("<Points> />", () => {
|
|||
|
||||
it("renders points", () => {
|
||||
const p = fakeProps();
|
||||
p.points = [fakePoint()];
|
||||
p.genericPoints = [fakePoint()];
|
||||
const wrapper = mount(<Points {...p} />);
|
||||
expect(wrapper.text()).toContain("Point 1");
|
||||
});
|
||||
|
||||
it("navigates to point info", () => {
|
||||
const p = fakeProps();
|
||||
p.points = [fakePoint()];
|
||||
p.points[0].body.id = 1;
|
||||
p.genericPoints = [fakePoint()];
|
||||
p.genericPoints[0].body.id = 1;
|
||||
const wrapper = mount(<Points {...p} />);
|
||||
wrapper.find(".point-search-item").first().simulate("click");
|
||||
expect(push).toHaveBeenCalledWith("/app/designer/points/1");
|
||||
|
@ -44,9 +44,9 @@ describe("<Points> />", () => {
|
|||
|
||||
it("changes search term", () => {
|
||||
const p = fakeProps();
|
||||
p.points = [fakePoint(), fakePoint()];
|
||||
p.points[0].body.name = "point 0";
|
||||
p.points[1].body.name = "point 1";
|
||||
p.genericPoints = [fakePoint(), fakePoint()];
|
||||
p.genericPoints[0].body.name = "point 0";
|
||||
p.genericPoints[1].body.name = "point 1";
|
||||
const wrapper = shallow<Points>(<Points {...p} />);
|
||||
wrapper.find("input").first().simulate("change",
|
||||
{ currentTarget: { value: "0" } });
|
||||
|
@ -55,9 +55,9 @@ describe("<Points> />", () => {
|
|||
|
||||
it("filters points", () => {
|
||||
const p = fakeProps();
|
||||
p.points = [fakePoint(), fakePoint()];
|
||||
p.points[0].body.name = "point 0";
|
||||
p.points[1].body.name = "point 1";
|
||||
p.genericPoints = [fakePoint(), fakePoint()];
|
||||
p.genericPoints[0].body.name = "point 0";
|
||||
p.genericPoints[1].body.name = "point 1";
|
||||
const wrapper = mount(<Points {...p} />);
|
||||
wrapper.setState({ searchTerm: "0" });
|
||||
expect(wrapper.text()).not.toContain("point 1");
|
||||
|
@ -72,6 +72,6 @@ describe("mapStateToProps()", () => {
|
|||
discarded.body.discarded_at = "2016-05-22T05:00:00.000Z";
|
||||
state.resources = buildResourceIndex([point, discarded]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.points).toEqual([point]);
|
||||
expect(props.genericPoints).toEqual([point]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import { fakePoint } from "../../../__test_support__/fake_state/resources";
|
|||
|
||||
describe("<Weeds> />", () => {
|
||||
const fakeProps = (): WeedsProps => ({
|
||||
points: [],
|
||||
genericPoints: [],
|
||||
dispatch: jest.fn(),
|
||||
hoveredPoint: undefined,
|
||||
});
|
||||
|
@ -27,9 +27,9 @@ describe("<Weeds> />", () => {
|
|||
|
||||
it("filters points", () => {
|
||||
const p = fakeProps();
|
||||
p.points = [fakePoint(), fakePoint()];
|
||||
p.points[0].body.name = "weed 0";
|
||||
p.points[1].body.name = "weed 1";
|
||||
p.genericPoints = [fakePoint(), fakePoint()];
|
||||
p.genericPoints[0].body.name = "weed 0";
|
||||
p.genericPoints[1].body.name = "weed 1";
|
||||
const wrapper = mount(<Weeds {...p} />);
|
||||
wrapper.setState({ searchTerm: "0" });
|
||||
expect(wrapper.text()).toContain("weed 0");
|
||||
|
|
|
@ -8,7 +8,7 @@ import { history, getPathArray } from "../../history";
|
|||
import { Panel } from "../panel_header";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { TaggedGenericPointer } from "farmbot";
|
||||
import { maybeFindPointById } from "../../resources/selectors";
|
||||
import { maybeFindGenericPointerById } from "../../resources/selectors";
|
||||
import { Actions } from "../../constants";
|
||||
import {
|
||||
EditPointProperties, updatePoint, PointActions
|
||||
|
@ -21,7 +21,7 @@ export interface EditPointProps {
|
|||
|
||||
export const mapStateToProps = (props: Everything): EditPointProps => ({
|
||||
dispatch: props.dispatch,
|
||||
findPoint: id => maybeFindPointById(props.resources.index, id),
|
||||
findPoint: id => maybeFindGenericPointerById(props.resources.index, id),
|
||||
});
|
||||
|
||||
export class RawEditPoint extends React.Component<EditPointProps, {}> {
|
||||
|
|
|
@ -16,7 +16,7 @@ import { t } from "../../i18next_wrapper";
|
|||
import { isAWeed } from "./weeds_inventory";
|
||||
|
||||
export interface PointsProps {
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
dispatch: Function;
|
||||
hoveredPoint: string | undefined;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ interface PointsState {
|
|||
export function mapStateToProps(props: Everything): PointsProps {
|
||||
const { hoveredPoint } = props.resources.consumers.farm_designer;
|
||||
return {
|
||||
points: selectAllGenericPointers(props.resources.index)
|
||||
genericPoints: selectAllGenericPointers(props.resources.index)
|
||||
.filter(x => !x.body.discarded_at)
|
||||
.filter(x => !isAWeed(x.body.name, x.body.meta.type)),
|
||||
dispatch: props.dispatch,
|
||||
|
@ -55,12 +55,12 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
|
|||
</DesignerPanelTop>
|
||||
<DesignerPanelContent panelName={"points"}>
|
||||
<EmptyStateWrapper
|
||||
notEmpty={this.props.points.length > 0}
|
||||
notEmpty={this.props.genericPoints.length > 0}
|
||||
graphic={EmptyStateGraphic.points}
|
||||
title={t("No points yet.")}
|
||||
text={Content.NO_POINTS}
|
||||
colorScheme={"points"}>
|
||||
{this.props.points
|
||||
{this.props.genericPoints
|
||||
.filter(p => p.body.name.toLowerCase()
|
||||
.includes(this.state.searchTerm.toLowerCase()))
|
||||
.map(p => <PointInventoryItem
|
||||
|
|
|
@ -7,7 +7,7 @@ import { t } from "../../i18next_wrapper";
|
|||
import { history, getPathArray } from "../../history";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { TaggedGenericPointer } from "farmbot";
|
||||
import { maybeFindPointById } from "../../resources/selectors";
|
||||
import { maybeFindGenericPointerById } from "../../resources/selectors";
|
||||
import { Panel } from "../panel_header";
|
||||
import {
|
||||
EditPointProperties, PointActions, updatePoint
|
||||
|
@ -21,7 +21,7 @@ export interface EditWeedProps {
|
|||
|
||||
export const mapStateToProps = (props: Everything): EditWeedProps => ({
|
||||
dispatch: props.dispatch,
|
||||
findPoint: id => maybeFindPointById(props.resources.index, id),
|
||||
findPoint: id => maybeFindGenericPointerById(props.resources.index, id),
|
||||
});
|
||||
|
||||
export class RawEditWeed extends React.Component<EditWeedProps, {}> {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { selectAllGenericPointers } from "../../resources/selectors";
|
|||
import { PointInventoryItem } from "./point_inventory_item";
|
||||
|
||||
export interface WeedsProps {
|
||||
points: TaggedGenericPointer[];
|
||||
genericPoints: TaggedGenericPointer[];
|
||||
dispatch: Function;
|
||||
hoveredPoint: string | undefined;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export const isAWeed = (pointName: string, type?: string) =>
|
|||
type == "weed" || pointName.toLowerCase().includes("weed");
|
||||
|
||||
export const mapStateToProps = (props: Everything): WeedsProps => ({
|
||||
points: selectAllGenericPointers(props.resources.index)
|
||||
genericPoints: selectAllGenericPointers(props.resources.index)
|
||||
.filter(x => !x.body.discarded_at)
|
||||
.filter(x => isAWeed(x.body.name, x.body.meta.type)),
|
||||
dispatch: props.dispatch,
|
||||
|
@ -54,12 +54,12 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
|
|||
</DesignerPanelTop>
|
||||
<DesignerPanelContent panelName={"weeds-inventory"}>
|
||||
<EmptyStateWrapper
|
||||
notEmpty={this.props.points.length > 0}
|
||||
notEmpty={this.props.genericPoints.length > 0}
|
||||
graphic={EmptyStateGraphic.weeds}
|
||||
title={t("No weeds yet.")}
|
||||
text={Content.NO_WEEDS}
|
||||
colorScheme={"weeds"}>
|
||||
{this.props.points
|
||||
{this.props.genericPoints
|
||||
.filter(p => p.body.name.toLowerCase()
|
||||
.includes(this.state.searchTerm.toLowerCase()))
|
||||
.map(p => <PointInventoryItem
|
||||
|
|
|
@ -60,13 +60,14 @@ export const designer = generateReducer<DesignerState>(initialState)
|
|||
s.hoveredToolSlot = payload;
|
||||
return s;
|
||||
})
|
||||
.add<CurrentPointPayl | undefined>(Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => {
|
||||
const { color } =
|
||||
(!payload || !payload.color) ? (s.currentPoint || { color: "green" }) : payload;
|
||||
s.currentPoint = payload;
|
||||
s.currentPoint && (s.currentPoint.color = color);
|
||||
return s;
|
||||
})
|
||||
.add<CurrentPointPayl | undefined>(
|
||||
Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => {
|
||||
const { color } = (!payload || !payload.color) ?
|
||||
(s.currentPoint || { color: "green" }) : payload;
|
||||
s.currentPoint = payload;
|
||||
s.currentPoint && (s.currentPoint.color = color);
|
||||
return s;
|
||||
})
|
||||
.add<CropLiveSearchResult[]>(Actions.OF_SEARCH_RESULTS_OK, (s, a) => {
|
||||
s.cropSearchResults = a.payload;
|
||||
s.cropSearchInProgress = false;
|
||||
|
|
|
@ -9,7 +9,9 @@ import {
|
|||
selectAllPlantTemplates,
|
||||
selectAllSensorReadings,
|
||||
selectAllSensors,
|
||||
maybeGetTimeSettings
|
||||
maybeGetTimeSettings,
|
||||
selectAllPoints,
|
||||
selectAllPointGroups
|
||||
} from "../resources/selectors";
|
||||
import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
|
@ -47,10 +49,10 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const hoveredPlant = findPlant(plantUUID);
|
||||
|
||||
const getConfigValue = getWebAppConfigValue(() => props);
|
||||
const allPoints = selectAllGenericPointers(props.resources.index);
|
||||
const points = getConfigValue(BooleanSetting.show_historic_points)
|
||||
? allPoints
|
||||
: allPoints.filter(x => !x.body.discarded_at);
|
||||
const allGenericPoints = selectAllGenericPointers(props.resources.index);
|
||||
const genericPoints = getConfigValue(BooleanSetting.show_historic_points)
|
||||
? allGenericPoints
|
||||
: allGenericPoints.filter(x => !x.body.discarded_at);
|
||||
|
||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
const { mcu_params } = props.bot.hardware;
|
||||
|
@ -103,7 +105,8 @@ export function mapStateToProps(props: Everything): Props {
|
|||
dispatch: props.dispatch,
|
||||
selectedPlant,
|
||||
designer: props.resources.consumers.farm_designer,
|
||||
points,
|
||||
genericPoints,
|
||||
allPoints: selectAllPoints(props.resources.index),
|
||||
toolSlots: joinToolsAndSlot(props.resources.index),
|
||||
hoveredPlant,
|
||||
plants,
|
||||
|
@ -118,5 +121,7 @@ export function mapStateToProps(props: Everything): Props {
|
|||
getConfigValue,
|
||||
sensorReadings,
|
||||
sensors: selectAllSensors(props.resources.index),
|
||||
groups: selectAllPointGroups(props.resources.index),
|
||||
shouldDisplay,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,12 +4,22 @@ jest.mock("../../../history", () => ({
|
|||
history: { push: jest.fn() }
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
RawEditZone as EditZone, EditZoneProps, mapStateToProps
|
||||
} from "../edit_zone";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { save, edit } from "../../../api/crud";
|
||||
|
||||
describe("<EditZone />", () => {
|
||||
const fakeProps = (): EditZoneProps => ({
|
||||
|
@ -26,15 +36,29 @@ describe("<EditZone />", () => {
|
|||
it("renders", () => {
|
||||
mockPath = "/app/designer/zones/1";
|
||||
const p = fakeProps();
|
||||
p.findZone = () => "stub zone";
|
||||
p.findZone = () => fakePointGroup();
|
||||
const wrapper = mount(<EditZone {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("edit");
|
||||
});
|
||||
|
||||
it("changes name", () => {
|
||||
mockPath = "/app/designer/zones/1";
|
||||
const p = fakeProps();
|
||||
const group = fakePointGroup();
|
||||
p.findZone = () => group;
|
||||
const wrapper = shallow(<EditZone {...p} />);
|
||||
wrapper.find("input").first().simulate("blur", {
|
||||
currentTarget: { value: "new name" }
|
||||
});
|
||||
expect(edit).toHaveBeenCalledWith(group, { name: "new name" });
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props", () => {
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([fakePointGroup()]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.findZone(1)).toEqual(undefined);
|
||||
});
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
jest.mock("../../../history", () => ({
|
||||
history: { push: jest.fn() },
|
||||
getPathArray: () => [],
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({ initSaveGetId: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
RawZones as Zones, ZonesProps, mapStateToProps
|
||||
} from "../zones_inventory";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||
import { history } from "../../../history";
|
||||
import { initSaveGetId } from "../../../api/crud";
|
||||
import { DesignerPanelTop } from "../../designer_panel";
|
||||
|
||||
describe("<Zones> />", () => {
|
||||
const fakeProps = (): ZonesProps => ({
|
||||
dispatch: jest.fn(),
|
||||
zones: [],
|
||||
allPoints: [],
|
||||
});
|
||||
|
||||
it("renders no zones", () => {
|
||||
|
@ -21,6 +34,56 @@ describe("<Zones> />", () => {
|
|||
{ currentTarget: { value: "0" } });
|
||||
expect(wrapper.state().searchTerm).toEqual("0");
|
||||
});
|
||||
|
||||
it("navigates to zone info", () => {
|
||||
const p = fakeProps();
|
||||
p.zones = [fakePointGroup()];
|
||||
p.zones[0].body.id = 1;
|
||||
const wrapper = mount(<Zones {...p} />);
|
||||
wrapper.find(".group-search-item").first().simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/zones/1");
|
||||
});
|
||||
|
||||
it("navigates to unsaved zone", () => {
|
||||
const p = fakeProps();
|
||||
p.zones = [fakePointGroup()];
|
||||
p.zones[0].body.id = 0;
|
||||
const wrapper = mount(<Zones {...p} />);
|
||||
wrapper.find(".group-search-item").first().simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/zones/0");
|
||||
});
|
||||
|
||||
it("filters points", () => {
|
||||
const p = fakeProps();
|
||||
p.zones = [fakePointGroup(), fakePointGroup()];
|
||||
p.zones[0].body.name = "zone 0";
|
||||
p.zones[1].body.name = "zone 1";
|
||||
const wrapper = mount(<Zones {...p} />);
|
||||
wrapper.setState({ searchTerm: "0" });
|
||||
expect(wrapper.text()).not.toContain("zone 1");
|
||||
});
|
||||
|
||||
it("creates new zone", async () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.resolve(1));
|
||||
const wrapper = shallow(<Zones {...p} />);
|
||||
await wrapper.find(DesignerPanelTop).simulate("click");
|
||||
expect(initSaveGetId).toHaveBeenCalledWith("PointGroup", {
|
||||
name: "Untitled Zone", point_ids: []
|
||||
});
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/zones/1");
|
||||
});
|
||||
|
||||
it("handles zone creation error", async () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.reject());
|
||||
const wrapper = shallow(<Zones {...p} />);
|
||||
await wrapper.find(DesignerPanelTop).simulate("click");
|
||||
expect(initSaveGetId).toHaveBeenCalledWith("PointGroup", {
|
||||
name: "Untitled Zone", point_ids: []
|
||||
});
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue