group panel updates
parent
ec878e0dae
commit
fe9ff346a8
|
@ -16,4 +16,5 @@ export const fakeDesignerState = (): DesignerState => ({
|
|||
currentPoint: undefined,
|
||||
openedSavedGarden: undefined,
|
||||
tryGroupSortType: undefined,
|
||||
editGroupAreaInMap: false,
|
||||
});
|
||||
|
|
|
@ -858,7 +858,7 @@ export namespace Content {
|
|||
|
||||
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.
|
||||
Click and drag in the map to modify 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.`);
|
||||
|
@ -1151,6 +1151,7 @@ export enum Actions {
|
|||
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
|
||||
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
||||
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
||||
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
|
||||
|
||||
// Regimens
|
||||
PUSH_WEEK = "PUSH_WEEK",
|
||||
|
|
|
@ -210,16 +210,6 @@
|
|||
@extend %panel-item-base;
|
||||
padding-top: 0.6rem;
|
||||
}
|
||||
.groups-panel-content {
|
||||
padding: 0px;
|
||||
}
|
||||
.groups-list-wrapper {
|
||||
padding: 0.5em 0em;
|
||||
}
|
||||
.group-delete-btn {
|
||||
float: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.plant-search-item-name {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -809,7 +809,6 @@
|
|||
|
||||
.weeds-inventory-panel,
|
||||
.zones-inventory-panel,
|
||||
.group-detail-panel,
|
||||
.groups-panel {
|
||||
.panel-content {
|
||||
max-height: calc(100vh - 19rem);
|
||||
|
@ -821,6 +820,31 @@
|
|||
|
||||
.group-detail-panel {
|
||||
.panel-content {
|
||||
max-height: calc(100vh - 14rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 5rem;
|
||||
.group-member-display {
|
||||
i[class*=fa-caret-] {
|
||||
float: right;
|
||||
font-size: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.groups-list-wrapper {
|
||||
padding: 0.5em 0em;
|
||||
}
|
||||
}
|
||||
.group-member-display,
|
||||
.group-sort-section {
|
||||
.bp3-popover-wrapper {
|
||||
display: inline;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.group-delete-btn {
|
||||
float: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.group-criteria {
|
||||
margin-top: 1rem;
|
||||
.criteria-heading {
|
||||
|
@ -829,7 +853,61 @@
|
|||
.fb-button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.group-criteria-presets {
|
||||
.point-type-checkboxes {
|
||||
.point-type-section {
|
||||
.fb-checkbox {
|
||||
display: inline;
|
||||
margin-right: 1rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.point-type-checkbox {
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
margin-top: 0.75rem;
|
||||
cursor: pointer;
|
||||
.fb-checkbox {
|
||||
display: inline-block;
|
||||
height: 2rem;
|
||||
}
|
||||
i[class*=fa-caret-] {
|
||||
position: absolute;
|
||||
right: -0.5rem;
|
||||
width: 3rem;
|
||||
font-size: 2rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
.plant-criteria-options,
|
||||
.point-criteria-options,
|
||||
.tool-criteria-options {
|
||||
margin-left: 3rem;
|
||||
p {
|
||||
&.category {
|
||||
display: block;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
text-transform: none;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.lt-gt-criteria {
|
||||
margin-bottom: 1rem;
|
||||
.row {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.criteria-radio-presets {
|
||||
input[type="radio"] {
|
||||
width: auto;
|
||||
margin-right: 1rem;
|
||||
|
@ -845,29 +923,28 @@
|
|||
.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;
|
||||
}
|
||||
input {
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
}
|
||||
.string-eq-criteria {
|
||||
margin-top: 1rem;
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
code {
|
||||
display: inline-block;
|
||||
margin-top: 2rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: $black;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.number-eq-criteria,
|
||||
.number-gt-lt-criteria {
|
||||
|
@ -877,11 +954,49 @@
|
|||
}
|
||||
p {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
line-height: 2.75rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
.expandable-header {
|
||||
margin-top: 3rem;
|
||||
.fb-toggle-button {
|
||||
width: 85px;
|
||||
margin-top: 0;
|
||||
&.red {
|
||||
background: $dark_gray !important;
|
||||
}
|
||||
}
|
||||
.clear-criteria {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.basic,
|
||||
.advanced {
|
||||
margin-left: 1rem;
|
||||
.day-criteria {
|
||||
.row {
|
||||
margin-left: 0;
|
||||
}
|
||||
div[class*=col-] {
|
||||
padding: 0;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.advanced {
|
||||
.row {
|
||||
margin-left: 0;
|
||||
}
|
||||
div[class*=col-] {
|
||||
padding: 0;
|
||||
}
|
||||
.col-xs-9 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.col-xs-1 {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
margin-top: 0.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.criteria-point-count-breakdown {
|
||||
|
@ -910,19 +1025,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
.zone-info-panel {
|
||||
.panel-content {
|
||||
.location-criteria {
|
||||
.row {
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.lt-gt-criteria,
|
||||
.location-criteria {
|
||||
display: inline-block;
|
||||
.row {
|
||||
margin-left: 0;
|
||||
div[class*=col-] {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
display: block !important;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
label {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
.edit-in-map {
|
||||
float: right;
|
||||
button {
|
||||
margin: 1rem !important;
|
||||
width: 5rem !important;
|
||||
}
|
||||
label {
|
||||
margin-top: 1.1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1309,15 +1309,6 @@ ul {
|
|||
display: inline;
|
||||
position: relative;
|
||||
margin-right: 1rem;
|
||||
&.partial:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
bottom: 1.2rem;
|
||||
border: solid $dark_gray;
|
||||
border-width: 0 0 3px 0;
|
||||
padding: 0.6rem 0.3rem;
|
||||
}
|
||||
}
|
||||
.bp3-popover-wrapper,
|
||||
.bp3-popover-target {
|
||||
|
|
|
@ -138,6 +138,15 @@ select {
|
|||
padding: 0.6rem 0.3rem;
|
||||
}
|
||||
}
|
||||
&.partial:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
bottom: 1.2rem;
|
||||
border: solid $dark_gray;
|
||||
border-width: 0 0 3px 0;
|
||||
padding: 0.6rem 0.3rem;
|
||||
}
|
||||
&.large {
|
||||
input[type="checkbox"] {
|
||||
width: 3rem;
|
||||
|
@ -155,8 +164,10 @@ select {
|
|||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
input[type="checkbox"] {
|
||||
cursor: not-allowed;
|
||||
background: $light_gray;
|
||||
pointer-events: none;
|
||||
&:checked:after {
|
||||
border-color: $gray;
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ export class BooleanMCUInputGroup
|
|||
{caution &&
|
||||
<i className="fa fa-exclamation-triangle caution-icon" />}
|
||||
</label>
|
||||
<Help text={tooltip} requireClick={true} position={Position.TOP_RIGHT} />
|
||||
<Help text={tooltip} position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
{!this.newFormat && <this.Toggles />}
|
||||
</Row>
|
||||
|
|
|
@ -39,8 +39,7 @@ export class CalibrationRow extends React.Component<CalibrationRowProps> {
|
|||
<label>
|
||||
{t(this.props.title)}
|
||||
</label>
|
||||
<Help text={t(this.props.toolTip)}
|
||||
requireClick={true} position={Position.TOP_RIGHT} />
|
||||
<Help text={t(this.props.toolTip)} position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
{!this.newFormat && <this.Axes />}
|
||||
</Row>
|
||||
|
|
|
@ -29,7 +29,7 @@ export function PinGuard(props: PinGuardProps) {
|
|||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true}
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
|
||||
position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
|
|
|
@ -20,7 +20,7 @@ export const SingleSettingRow =
|
|||
<Row>
|
||||
<Col xs={newFormat ? 12 : 6} className={"widget-body-tooltips"}>
|
||||
<label>{t(label)}</label>
|
||||
<Help text={tooltip} requireClick={true} position={Position.TOP_RIGHT} />
|
||||
<Help text={tooltip} position={Position.RIGHT} />
|
||||
</Col>
|
||||
{settingType === "button"
|
||||
? <Col xs={newFormat ? 5 : 2} className={"centered-button-div"}>
|
||||
|
|
|
@ -59,7 +59,7 @@ export class NumericMCUInputGroup
|
|||
<label>
|
||||
{t(label)}
|
||||
</label>
|
||||
<Help text={tooltip} requireClick={true} position={Position.TOP_RIGHT} />
|
||||
<Help text={tooltip} position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
{!this.newFormat && <this.Inputs />}
|
||||
</Row>
|
||||
|
|
|
@ -76,7 +76,7 @@ export class PinGuardMCUInputGroup
|
|||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true}
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
|
||||
position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
|
|
|
@ -75,7 +75,7 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
|
|||
return <div className="pin-bindings">
|
||||
<Row>
|
||||
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
|
||||
position={Position.TOP_RIGHT} requireClick={true} />}
|
||||
position={Position.TOP_RIGHT} />}
|
||||
<StockPinBindingsButton
|
||||
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
||||
<Popover
|
||||
|
|
|
@ -118,6 +118,7 @@ export interface DesignerState {
|
|||
currentPoint: CurrentPointPayl | undefined;
|
||||
openedSavedGarden: string | undefined;
|
||||
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
||||
editGroupAreaInMap: boolean;
|
||||
}
|
||||
|
||||
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
|
||||
|
|
|
@ -32,7 +32,7 @@ jest.mock("../drawn_point/drawn_point_actions", () => ({
|
|||
jest.mock("../background/selection_box_actions", () => ({
|
||||
startNewSelectionBox: jest.fn(),
|
||||
resizeBox: jest.fn(),
|
||||
maybeUpdateGroupCriteria: jest.fn(),
|
||||
maybeUpdateGroup: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() }));
|
||||
|
@ -61,7 +61,7 @@ import {
|
|||
dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant,
|
||||
} from "../layers/plants/plant_actions";
|
||||
import {
|
||||
startNewSelectionBox, resizeBox, maybeUpdateGroupCriteria,
|
||||
startNewSelectionBox, resizeBox, maybeUpdateGroup,
|
||||
} from "../background/selection_box_actions";
|
||||
import { getGardenCoordinates } from "../util";
|
||||
import { chooseLocation } from "../../move_to";
|
||||
|
@ -158,7 +158,7 @@ describe("<GardenMap/>", () => {
|
|||
wrapper.setState({ isDragging: true });
|
||||
wrapper.find(".drop-area-svg").simulate("mouseUp", DEFAULT_EVENT);
|
||||
expect(maybeSavePlantLocation).toHaveBeenCalled();
|
||||
expect(maybeUpdateGroupCriteria).toHaveBeenCalled();
|
||||
expect(maybeUpdateGroup).toHaveBeenCalled();
|
||||
expect(wrapper.instance().state.isDragging).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -224,7 +224,9 @@ describe("<GardenMap/>", () => {
|
|||
});
|
||||
|
||||
it("starts drag on background: selecting zone", () => {
|
||||
const wrapper = mount(<GardenMap {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.designer.editGroupAreaInMap = true;
|
||||
const wrapper = mount(<GardenMap {...p} />);
|
||||
mockMode = Mode.editGroup;
|
||||
const e = { pageX: 1000, pageY: 2000 };
|
||||
wrapper.find(".drop-area-background").simulate("mouseDown", e);
|
||||
|
@ -255,7 +257,9 @@ describe("<GardenMap/>", () => {
|
|||
});
|
||||
|
||||
it("drags: selecting zone", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.designer.editGroupAreaInMap = true;
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
mockMode = Mode.editGroup;
|
||||
const e = { pageX: 2000, pageY: 2000 };
|
||||
wrapper.find(".drop-area-svg").simulate("mouseMove", e);
|
||||
|
|
|
@ -8,18 +8,25 @@ jest.mock("../../../point_groups/criteria", () => ({
|
|||
editGtLtCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
fakePlant, fakePointGroup,
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps,
|
||||
StartNewSelectionBoxProps,
|
||||
maybeUpdateGroupCriteria,
|
||||
MaybeUpdateGroupCriteriaProps,
|
||||
maybeUpdateGroup,
|
||||
MaybeUpdateGroupProps,
|
||||
} from "../selection_box_actions";
|
||||
import { Actions } from "../../../../constants";
|
||||
import { history } from "../../../../history";
|
||||
import { editGtLtCriteria } from "../../../point_groups/criteria";
|
||||
import { overwrite, save } from "../../../../api/crud";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
describe("getSelected", () => {
|
||||
it("returns some", () => {
|
||||
|
@ -156,24 +163,55 @@ describe("startNewSelectionBox", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("maybeUpdateGroupCriteria()", () => {
|
||||
const fakeProps = (): MaybeUpdateGroupCriteriaProps => ({
|
||||
describe("maybeUpdateGroup()", () => {
|
||||
const fakeProps = (): MaybeUpdateGroupProps => ({
|
||||
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
shouldDisplay: () => true,
|
||||
editGroupAreaInMap: false,
|
||||
boxSelected: undefined,
|
||||
});
|
||||
|
||||
it("updates group", () => {
|
||||
const p = fakeProps();
|
||||
p.editGroupAreaInMap = false;
|
||||
const plant1 = fakePlant();
|
||||
const plant2 = fakePlant();
|
||||
p.boxSelected = [plant1.uuid, plant2.uuid];
|
||||
p.group && (p.group.body.point_ids = [plant1.body.id || 0]);
|
||||
maybeUpdateGroup(p);
|
||||
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
||||
const expectedBody = cloneDeep(p.group?.body);
|
||||
expectedBody && (expectedBody.point_ids = [
|
||||
plant1.body.id || 0, plant2.body.id || 0,
|
||||
]);
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates criteria", () => {
|
||||
const p = fakeProps();
|
||||
maybeUpdateGroupCriteria(p);
|
||||
p.editGroupAreaInMap = true;
|
||||
maybeUpdateGroup(p);
|
||||
expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox);
|
||||
});
|
||||
|
||||
it("doesn't update criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => false;
|
||||
maybeUpdateGroupCriteria(p);
|
||||
maybeUpdateGroup(p);
|
||||
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles missing group or box", () => {
|
||||
const p = fakeProps();
|
||||
p.group = undefined;
|
||||
p.selectionBox = undefined;
|
||||
maybeUpdateGroup(p);
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
||||
expect(overwrite).not.toHaveBeenCalled();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isNumber } from "lodash";
|
||||
import { isNumber, uniq, cloneDeep, isEqual } from "lodash";
|
||||
import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
|
||||
import { SelectionBoxData } from "./selection_box";
|
||||
import { GardenMapState } from "../../interfaces";
|
||||
|
@ -8,6 +8,9 @@ import { getMode } from "../util";
|
|||
import { editGtLtCriteria } from "../../point_groups/criteria";
|
||||
import { TaggedPointGroup } from "farmbot";
|
||||
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
import { unpackUUID } from "../../../util";
|
||||
import { UUID } from "../../../resources/interfaces";
|
||||
|
||||
/** Return all plants within the selection box. */
|
||||
export const getSelected = (
|
||||
|
@ -85,17 +88,32 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
export interface MaybeUpdateGroupCriteriaProps {
|
||||
export interface MaybeUpdateGroupProps {
|
||||
selectionBox: SelectionBoxData | undefined;
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
editGroupAreaInMap: boolean;
|
||||
boxSelected: UUID[] | undefined;
|
||||
}
|
||||
|
||||
export const maybeUpdateGroupCriteria =
|
||||
(props: MaybeUpdateGroupCriteriaProps) => {
|
||||
if (props.selectionBox && props.group &&
|
||||
props.shouldDisplay(Feature.criteria_groups)) {
|
||||
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
|
||||
export const maybeUpdateGroup =
|
||||
(props: MaybeUpdateGroupProps) => {
|
||||
if (props.selectionBox && props.group) {
|
||||
if (props.editGroupAreaInMap
|
||||
&& props.shouldDisplay(Feature.criteria_groups)) {
|
||||
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
|
||||
} else {
|
||||
const nextGroupBody = cloneDeep(props.group.body);
|
||||
props.boxSelected?.map(uuid => {
|
||||
const { kind, remoteId } = unpackUUID(uuid);
|
||||
remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId);
|
||||
});
|
||||
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
|
||||
if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
|
||||
props.dispatch(overwrite(props.group, nextGroupBody));
|
||||
props.dispatch(selectPlant(undefined));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import {
|
||||
Grid, MapBackground,
|
||||
TargetCoordinate,
|
||||
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroupCriteria,
|
||||
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroup,
|
||||
} from "./background";
|
||||
import {
|
||||
PlantLayer,
|
||||
|
@ -88,11 +88,13 @@ export class GardenMap extends
|
|||
isDragging: this.state.isDragging,
|
||||
dispatch: this.props.dispatch,
|
||||
});
|
||||
maybeUpdateGroupCriteria({
|
||||
maybeUpdateGroup({
|
||||
selectionBox: this.state.selectionBox,
|
||||
group: this.group,
|
||||
dispatch: this.props.dispatch,
|
||||
shouldDisplay: this.props.shouldDisplay,
|
||||
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
|
||||
boxSelected: this.props.designer.selectedPlants,
|
||||
});
|
||||
this.setState({
|
||||
isDragging: false, qPageX: 0, qPageY: 0,
|
||||
|
@ -142,7 +144,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
plantActions: !this.props.designer.editGroupAreaInMap,
|
||||
});
|
||||
break;
|
||||
case Mode.createPoint:
|
||||
|
@ -179,7 +181,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
plantActions: !this.props.designer.editGroupAreaInMap,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -283,7 +285,7 @@ export class GardenMap extends
|
|||
gardenCoords: this.getGardenCoordinates(e),
|
||||
setMapState: this.setMapState,
|
||||
dispatch: this.props.dispatch,
|
||||
plantActions: false,
|
||||
plantActions: !this.props.designer.editGroupAreaInMap,
|
||||
});
|
||||
break;
|
||||
case Mode.boxSelect:
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
import {
|
||||
fakeMapTransformProps,
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<ZonesLayer />", () => {
|
||||
const fakeProps = (): ZonesLayerProps => ({
|
||||
|
@ -69,7 +68,6 @@ describe("<ZonesLayer />", () => {
|
|||
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())
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
import {
|
||||
fakeMapTransformProps,
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces";
|
||||
|
||||
const fakeProps = (): ZonesProps => ({
|
||||
group: fakePointGroup(),
|
||||
|
@ -25,7 +25,7 @@ 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"];
|
||||
p.group.body.criteria = DEFAULT_CRITERIA;
|
||||
const wrapper = svgMount(<Zones0D {...p} />);
|
||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||
expect(wrapper.find("circle").length).toEqual(0);
|
||||
|
@ -63,7 +63,7 @@ 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"];
|
||||
p.group.body.criteria = DEFAULT_CRITERIA;
|
||||
const wrapper = svgMount(<Zones1D {...p} />);
|
||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||
expect(wrapper.find("line").length).toEqual(0);
|
||||
|
@ -110,7 +110,7 @@ describe("<Zones2D />", () => {
|
|||
it("renders none", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.id = 1;
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
p.group.body.criteria = DEFAULT_CRITERIA;
|
||||
const wrapper = svgMount(<Zones2D {...p} />);
|
||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||
expect(wrapper.find("rect").length).toEqual(0);
|
||||
|
|
|
@ -26,9 +26,9 @@ 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 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) {
|
||||
|
@ -46,8 +46,8 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => {
|
|||
/** 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 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;
|
||||
|
@ -67,8 +67,8 @@ const filter: <T extends Point | Line>(
|
|||
/** 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 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);
|
||||
|
@ -95,12 +95,12 @@ export const Zones0D = (props: ZonesProps) => {
|
|||
/** 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 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;
|
||||
const xLineData = (onlyXs && xs) ? xs.map(x => ({ x })) : undefined;
|
||||
const yLineData = (onlyYs && ys) ? ys.map(y => ({ y })) : undefined;
|
||||
return filter<Line>(boundary, xLineData || yLineData);
|
||||
};
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<LayerToggle
|
||||
value={props.showZones}
|
||||
label={t("Zones?")}
|
||||
label={t("areas?")}
|
||||
onClick={toggle(BooleanSetting.show_zones)} />}
|
||||
{DevSettings.futureFeaturesEnabled() && props.hasSensorReadings &&
|
||||
<LayerToggle
|
||||
|
|
|
@ -17,13 +17,13 @@ import {
|
|||
import { save, edit } from "../../../api/crud";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
|
||||
import { Content } from "../../../constants";
|
||||
|
||||
describe("<GroupDetailActive/>", () => {
|
||||
const fakeProps = (): GroupDetailActiveProps => {
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 1;
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria = DEFAULT_CRITERIA;
|
||||
group.specialStatus = SpecialStatus.DIRTY;
|
||||
group.body.name = "XYZ";
|
||||
group.body.point_ids = [plant.body.id];
|
||||
|
@ -34,6 +34,7 @@ describe("<GroupDetailActive/>", () => {
|
|||
shouldDisplay: () => true,
|
||||
slugs: [],
|
||||
hovered: undefined,
|
||||
editGroupAreaInMap: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -45,11 +46,29 @@ describe("<GroupDetailActive/>", () => {
|
|||
expect(save).toHaveBeenCalledWith(p.group.uuid);
|
||||
});
|
||||
|
||||
it("is already saved", () => {
|
||||
const p = fakeProps();
|
||||
p.group.specialStatus = SpecialStatus.SAVED;
|
||||
const el = new GroupDetailActive(p);
|
||||
el.saveGroup();
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("toggles icon view", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount<GroupDetailActive>(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.state().iconDisplay).toBeTruthy();
|
||||
wrapper.instance().toggleIconShow();
|
||||
expect(wrapper.state().iconDisplay).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.group.specialStatus = SpecialStatus.SAVED;
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.find("input").first().prop("defaultValue")).toContain("XYZ");
|
||||
expect(wrapper.find(".groups-list-wrapper").length).toEqual(1);
|
||||
expect(wrapper.text()).not.toContain("saving");
|
||||
});
|
||||
|
||||
|
@ -109,6 +128,12 @@ describe("<GroupDetailActive/>", () => {
|
|||
const p = fakeProps();
|
||||
p.group.body.sort_type = "random";
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION);
|
||||
expect(wrapper.html()).toContain("exclamation-triangle");
|
||||
});
|
||||
|
||||
it("doesn't show icons", () => {
|
||||
const wrapper = mount(<GroupDetailActive {...fakeProps()} />);
|
||||
wrapper.setState({ iconDisplay: false });
|
||||
expect(wrapper.find(".groups-list-wrapper").length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ describe("<GroupListPanel />", () => {
|
|||
group1.body.point_ids = [1, 2, 3];
|
||||
const group2 = fakePointGroup();
|
||||
group2.body.name = "two";
|
||||
group2.body.criteria.day.days_ago = -1;
|
||||
const point1 = fakePlant();
|
||||
point1.body.id = 1;
|
||||
const point2 = fakePlant();
|
||||
|
|
|
@ -5,27 +5,20 @@ jest.mock("../edit", () => ({
|
|||
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
AddEqCriteria, AddNumberCriteria, editCriteria, AddStringCriteria,
|
||||
toggleStringCriteria,
|
||||
POINTER_TYPE_LIST,
|
||||
} from "..";
|
||||
import { AddEqCriteria, AddNumberCriteria, editCriteria } 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";
|
||||
import { PLANT_STAGE_LIST } from "../../../plants/edit_plant_status";
|
||||
|
||||
describe("<AddEqCriteria<string> />", () => {
|
||||
const fakeProps = (): AddEqCriteriaProps<string> => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
type: "string",
|
||||
criteriaField: undefined,
|
||||
eqCriteria: {},
|
||||
criteriaKey: "string_eq",
|
||||
});
|
||||
|
||||
|
@ -68,7 +61,7 @@ describe("<AddEqCriteria<number> />", () => {
|
|||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
type: "number",
|
||||
criteriaField: {},
|
||||
eqCriteria: {},
|
||||
criteriaKey: "number_eq",
|
||||
});
|
||||
|
||||
|
@ -106,104 +99,6 @@ describe("<AddEqCriteria<number> />", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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(),
|
||||
|
@ -242,11 +137,4 @@ describe("<AddNumberCriteria />", () => {
|
|||
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("<");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,13 +2,12 @@ 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 { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
describe("selectPointsByCriteria()", () => {
|
||||
const fakeCriteria = (): PointGroup["criteria"] =>
|
||||
const fakeCriteria = (): PointGroupCriteria =>
|
||||
cloneDeep(DEFAULT_CRITERIA);
|
||||
|
||||
it("matches color", () => {
|
||||
|
@ -44,6 +43,7 @@ describe("selectPointsByCriteria()", () => {
|
|||
|
||||
it("matches positions: gt/lt", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = {};
|
||||
criteria.number_gt = { x: 100 };
|
||||
criteria.number_lt = { x: 500 };
|
||||
const matchingPoint = fakePoint();
|
||||
|
@ -57,6 +57,7 @@ describe("selectPointsByCriteria()", () => {
|
|||
|
||||
it("matches age greater than 1 day old", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = {};
|
||||
criteria.day = { days_ago: 1, op: ">" };
|
||||
const matchingPoint = fakePoint();
|
||||
matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z";
|
||||
|
@ -70,6 +71,7 @@ describe("selectPointsByCriteria()", () => {
|
|||
|
||||
it("matches age less than 1 day old", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = {};
|
||||
criteria.day = { days_ago: 1, op: "<" };
|
||||
const matchingPoint = fakePoint();
|
||||
matchingPoint.body.created_at = "2020-02-20T20:00:00.000Z";
|
||||
|
|
|
@ -14,20 +14,17 @@ import {
|
|||
} 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: [],
|
||||
editGroupAreaInMap: false,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
const wrapper = mount(<GroupCriteria {...p} />);
|
||||
const wrapper = mount(<GroupCriteria {...fakeProps()} />);
|
||||
["criteria", "age selection"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
|
@ -35,17 +32,17 @@ describe("<GroupCriteria />", () => {
|
|||
it("clears criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupCriteria {...p} />);
|
||||
wrapper.find("button").first().simulate("click");
|
||||
wrapper.find("button").last().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", () => {
|
||||
it("toggles advanced view", () => {
|
||||
const wrapper = mount(<GroupCriteria {...fakeProps()} />);
|
||||
expect(wrapper.text()).not.toContain("number criteria");
|
||||
wrapper.find(ExpandableHeader).simulate("click");
|
||||
wrapper.find("ToggleButton").first().simulate("click");
|
||||
expect(wrapper.text()).toContain("number criteria");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,20 +5,25 @@ jest.mock("../../../../api/crud", () => ({
|
|||
|
||||
import {
|
||||
editCriteria, toggleEqCriteria,
|
||||
togglePointSelection, toggleStringCriteria, editGtLtCriteria,
|
||||
editGtLtCriteria,
|
||||
togglePointTypeCriteria,
|
||||
toggleAndEditEqCriteria,
|
||||
clearCriteriaField,
|
||||
removeEqCriteriaValue,
|
||||
editGtLtCriteriaField,
|
||||
} 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";
|
||||
import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces";
|
||||
import { inputEvent } from "../../../../__test_support__/fake_html_events";
|
||||
|
||||
describe("editCriteria()", () => {
|
||||
it("edits criteria: all empty", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
group.body.criteria = DEFAULT_CRITERIA;
|
||||
editCriteria(group, {})(jest.fn());
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria = DEFAULT_CRITERIA;
|
||||
|
@ -35,7 +40,7 @@ describe("editCriteria()", () => {
|
|||
|
||||
it("edits criteria: full update", () => {
|
||||
const group = fakePointGroup();
|
||||
const criteria: PointGroup["criteria"] = {
|
||||
const criteria: PointGroupCriteria = {
|
||||
day: { days_ago: 1, op: "<" },
|
||||
string_eq: { openfarm_slug: ["slug"] },
|
||||
number_eq: { x: [0] },
|
||||
|
@ -52,47 +57,162 @@ describe("editCriteria()", () => {
|
|||
|
||||
describe("toggleEqCriteria()", () => {
|
||||
it("adds criteria", () => {
|
||||
const result = toggleEqCriteria({})("openfarm_slug", "slug");
|
||||
expect(result).toEqual({ openfarm_slug: ["slug"] });
|
||||
const eqCriteria = {};
|
||||
toggleEqCriteria(eqCriteria)("openfarm_slug", "slug");
|
||||
expect(eqCriteria).toEqual({ openfarm_slug: ["slug"] });
|
||||
});
|
||||
|
||||
it("removes criteria", () => {
|
||||
const result = toggleEqCriteria({ openfarm_slug: ["slug"] })(
|
||||
const eqCriteria = { openfarm_slug: ["slug"] };
|
||||
toggleEqCriteria(eqCriteria)(
|
||||
"openfarm_slug", "slug");
|
||||
expect(result).toEqual({});
|
||||
expect(eqCriteria).toEqual({});
|
||||
});
|
||||
|
||||
it("toggles on", () => {
|
||||
const eqCriteria = { openfarm_slug: ["slug"] };
|
||||
toggleEqCriteria(eqCriteria, "on")(
|
||||
"openfarm_slug", "slug");
|
||||
expect(eqCriteria).toEqual({ openfarm_slug: ["slug"] });
|
||||
});
|
||||
|
||||
it("toggles off", () => {
|
||||
const eqCriteria = {};
|
||||
toggleEqCriteria(eqCriteria, "off")("openfarm_slug", "slug");
|
||||
expect(eqCriteria).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
const dispatch = jest.fn(x => x(jest.fn()));
|
||||
|
||||
describe("togglePointSelection()", () => {
|
||||
it("adds criteria", () => {
|
||||
describe("toggleAndEditEqCriteria()", () => {
|
||||
it("toggles criteria on", () => {
|
||||
const group = fakePointGroup();
|
||||
togglePointSelection(group)({ openfarm_slug: "slug" })(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] };
|
||||
expectedBody.criteria.string_eq = { openfarm_slug: ["mint"] };
|
||||
toggleAndEditEqCriteria(group, "openfarm_slug", "mint")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("toggles criteria on for point type", () => {
|
||||
const group = fakePointGroup();
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
group.body.criteria.string_eq = {
|
||||
pointer_type: ["GenericPointer", "Plant", "ToolSlot"],
|
||||
openfarm_slug: ["apple"],
|
||||
"meta.color": ["red"],
|
||||
};
|
||||
group.body.criteria.number_eq = {
|
||||
pullout_direction: [0]
|
||||
};
|
||||
expectedBody.criteria.string_eq = {
|
||||
pointer_type: ["Plant"],
|
||||
openfarm_slug: ["apple", "mint"],
|
||||
};
|
||||
expectedBody.criteria.number_eq = {};
|
||||
toggleAndEditEqCriteria(group, "openfarm_slug", "mint", "Plant")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("toggles off", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.string_eq = {
|
||||
pointer_type: ["GenericPointer", "Plant", "ToolSlot"],
|
||||
openfarm_slug: ["mint"],
|
||||
"meta.color": ["red"],
|
||||
};
|
||||
group.body.criteria.number_eq = {
|
||||
pullout_direction: [0],
|
||||
};
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
delete expectedBody.criteria.string_eq.openfarm_slug;
|
||||
toggleAndEditEqCriteria(group, "openfarm_slug", "mint", "Plant")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("toggles on: empty criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.string_eq = {
|
||||
openfarm_slug: undefined,
|
||||
"meta.color": undefined,
|
||||
};
|
||||
group.body.criteria.number_eq = {
|
||||
pullout_direction: undefined,
|
||||
};
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.number_eq = { pullout_direction: [0] };
|
||||
toggleAndEditEqCriteria(group, "pullout_direction", 0, "ToolSlot")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleStringCriteria()", () => {
|
||||
it("adds criteria", () => {
|
||||
describe("togglePointTypeCriteria()", () => {
|
||||
it("toggles on", () => {
|
||||
const group = fakePointGroup();
|
||||
toggleStringCriteria(group, "openfarm_slug", "slug")(dispatch);
|
||||
group.body.criteria.string_eq = {
|
||||
pointer_type: ["GenericPointer"],
|
||||
openfarm_slug: ["mint"],
|
||||
"meta.color": ["red"],
|
||||
};
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] };
|
||||
expectedBody.criteria.string_eq.pointer_type?.push("Plant");
|
||||
togglePointTypeCriteria(group, "Plant")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
it("toggles off", () => {
|
||||
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"] };
|
||||
group.body.criteria.string_eq = {
|
||||
pointer_type: ["GenericPointer", "Plant"],
|
||||
openfarm_slug: ["mint"],
|
||||
"meta.color": ["red"],
|
||||
};
|
||||
expectedBody.criteria.string_eq = {
|
||||
pointer_type: ["GenericPointer"],
|
||||
"meta.color": ["red"],
|
||||
};
|
||||
togglePointTypeCriteria(group, "Plant")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("toggles on: empty criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.string_eq = {};
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = { pointer_type: ["Plant"] };
|
||||
togglePointTypeCriteria(group, "Plant")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("toggles off: empty criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
group.body.criteria.string_eq = { pointer_type: ["ToolSlot"] };
|
||||
group.body.criteria.number_eq = {
|
||||
pullout_direction: undefined,
|
||||
};
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = {};
|
||||
togglePointTypeCriteria(group, "ToolSlot")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearCriteriaField()", () => {
|
||||
it("clears field", () => {
|
||||
const group = fakePointGroup();
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
group.body.criteria.string_eq = { plant_stage: ["planted"] };
|
||||
expectedBody.criteria.string_eq = {};
|
||||
clearCriteriaField(group, ["string_eq"], "plant_stage")(dispatch);
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
@ -117,16 +237,41 @@ describe("editGtLtCriteria()", () => {
|
|||
expect(overwrite).not.toHaveBeenCalled();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
describe("removeEqCriteriaValue()", () => {
|
||||
it("removes value", () => {
|
||||
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);
|
||||
group.body.criteria.string_eq = { plant_stage: ["planted", "planned"] };
|
||||
removeEqCriteriaValue(group, group.body.criteria.string_eq,
|
||||
"string_eq", "plant_stage", "planned")(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 };
|
||||
expectedBody.criteria.string_eq = { plant_stage: ["planted"] };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editGtLtCriteriaField()", () => {
|
||||
it("changes value", () => {
|
||||
const group = fakePointGroup();
|
||||
const e = inputEvent("1");
|
||||
editGtLtCriteriaField(group, "number_lt", "radius")(e)(dispatch);
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.number_lt = { radius: 1 };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
||||
it("clears incompatible criteria", () => {
|
||||
const group = fakePointGroup();
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
group.body.criteria.string_eq = { plant_stage: ["planted"] };
|
||||
const e = inputEvent("1");
|
||||
editGtLtCriteriaField(
|
||||
group, "number_lt", "radius", "GenericPointer",
|
||||
)(e)(dispatch);
|
||||
expectedBody.criteria.number_lt = { radius: 1 };
|
||||
expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
|
||||
expect(save).toHaveBeenCalledWith(group.uuid);
|
||||
});
|
||||
|
|
|
@ -1,56 +1,93 @@
|
|||
const mockToggle = jest.fn();
|
||||
jest.mock("../edit", () => ({
|
||||
togglePointSelection: jest.fn(() => mockToggle),
|
||||
toggleStringCriteria: jest.fn(),
|
||||
togglePointTypeCriteria: jest.fn(),
|
||||
toggleAndEditEqCriteria: jest.fn(),
|
||||
clearCriteriaField: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
CheckboxSelections, togglePointSelection, criteriaSelected,
|
||||
CheckboxSelections, togglePointTypeCriteria, clearCriteriaField,
|
||||
} from "..";
|
||||
import { CheckboxSelectionsProps } from "../interfaces";
|
||||
import {
|
||||
fakePointGroup,
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import { Checkbox } from "../../../../ui";
|
||||
|
||||
describe("<CheckboxSelections />", () => {
|
||||
const fakeProps = (): CheckboxSelectionsProps => ({
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
slugs: ["mint"],
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
it("renders all criteria", () => {
|
||||
const STRINGS = [
|
||||
"planted", "mint",
|
||||
"farm designer", "radius", "green",
|
||||
"positive x",
|
||||
];
|
||||
const wrapper = mount(<CheckboxSelections {...fakeProps()} />);
|
||||
STRINGS.map(string =>
|
||||
expect(wrapper.text().toLowerCase()).not.toContain(string.toLowerCase()));
|
||||
wrapper.setState({ Plant: true, GenericPointer: true, ToolSlot: true });
|
||||
STRINGS.map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
|
||||
});
|
||||
|
||||
it("clears sub criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria = undefined as unknown as PointGroup["criteria"];
|
||||
p.group.body.criteria.string_eq = { plant_stage: ["planned"] };
|
||||
const wrapper = mount(<CheckboxSelections {...p} />);
|
||||
["planted plants", "detected weeds", "created points", "created weeds",
|
||||
].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
wrapper.setState({ Plant: true, GenericPointer: false, ToolSlot: false });
|
||||
wrapper.find(".plant-criteria-options")
|
||||
.find("input").first().simulate("change");
|
||||
expect(clearCriteriaField).toHaveBeenCalledWith(
|
||||
p.group, ["string_eq"], "plant_stage",
|
||||
);
|
||||
});
|
||||
|
||||
it("changes criteria", () => {
|
||||
it("toggles section", () => {
|
||||
const wrapper =
|
||||
shallow<CheckboxSelections>(<CheckboxSelections {...fakeProps()} />);
|
||||
expect(wrapper.state().Plant).toBeFalsy();
|
||||
wrapper.instance().toggleMore("Plant")();
|
||||
expect(wrapper.state().Plant).toBeTruthy();
|
||||
});
|
||||
|
||||
it("toggles point type", () => {
|
||||
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);
|
||||
expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant");
|
||||
});
|
||||
|
||||
it("returns selection state: true", () => {
|
||||
const result = criteriaSelected({
|
||||
pointer_type: ["Plant"]
|
||||
})({ pointer_type: "Plant" });
|
||||
expect(result).toEqual(true);
|
||||
it("stops propagation", () => {
|
||||
const wrapper = mount(<CheckboxSelections {...fakeProps()} />);
|
||||
const e = { stopPropagation: jest.fn() };
|
||||
wrapper.find(".fb-checkbox").first().simulate("click", e);
|
||||
expect(e.stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is not disabled", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<CheckboxSelections {...p} />);
|
||||
const pointTypeBoxes = wrapper.find(".point-type-checkbox").find("input");
|
||||
expect(pointTypeBoxes.first().props().disabled).toBeFalsy();
|
||||
expect(pointTypeBoxes.at(1).props().disabled).toBeFalsy();
|
||||
expect(pointTypeBoxes.last().props().disabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it("is disabled", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.criteria.string_eq = { plant_stage: ["planted"] };
|
||||
p.group.body.criteria.number_eq = { pullout_direction: [0] };
|
||||
p.group.body.criteria.number_gt = { radius: 0 };
|
||||
const wrapper = mount(<CheckboxSelections {...p} />);
|
||||
const pointTypeBoxes = wrapper.find(".point-type-checkbox").find(Checkbox);
|
||||
expect(pointTypeBoxes.first().props().disabled).toBeTruthy();
|
||||
expect(pointTypeBoxes.at(1).props().disabled).toBeTruthy();
|
||||
expect(pointTypeBoxes.last().props().disabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { eqCriteriaSelected, criteriaHasKey, hasSubCriteria, typeDisabled } from "..";
|
||||
import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const fakeCriteria = (): PointGroupCriteria =>
|
||||
cloneDeep(DEFAULT_CRITERIA);
|
||||
|
||||
describe("eqCriteriaSelected()", () => {
|
||||
it("returns selected", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.number_eq = { pullout_direction: [0] };
|
||||
const result = eqCriteriaSelected(criteria)("pullout_direction", 0);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns not selected", () => {
|
||||
const criteria = fakeCriteria();
|
||||
const result = eqCriteriaSelected(criteria)(
|
||||
"pullout_direction", false as unknown as string);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("criteriaHasKey()", () => {
|
||||
it("has key", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { plant_stage: ["planted"] };
|
||||
const result = criteriaHasKey(criteria, ["string_eq"], "plant_stage");
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("doesn't have key", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = {};
|
||||
const result = criteriaHasKey(criteria, ["string_eq"], "plant_stage");
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasSubCriteria()", () => {
|
||||
it("has criteria", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { plant_stage: ["planted"] };
|
||||
const result = hasSubCriteria(criteria)("Plant");
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("doesn't have criteria", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { "meta.color": ["red"] };
|
||||
const result = hasSubCriteria(criteria)("Plant");
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("typeDisabled()", () => {
|
||||
it("is disabled", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { "meta.color": ["red"] };
|
||||
const result = typeDisabled(criteria, "Plant");
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("isn't disabled", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { plant_stage: ["planted"] };
|
||||
const result = typeDisabled(criteria, "Plant");
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -1,13 +1,22 @@
|
|||
jest.mock("../../../../api/crud", () => ({
|
||||
overwrite: jest.fn(),
|
||||
save: jest.fn(),
|
||||
jest.mock("../edit", () => ({
|
||||
editCriteria: jest.fn(),
|
||||
editGtLtCriteriaField: jest.fn(() => jest.fn()),
|
||||
removeEqCriteriaValue: jest.fn(),
|
||||
clearCriteriaField: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
EqCriteriaSelection,
|
||||
NumberCriteriaSelection, DaySelection, LocationSelection, AddCriteria,
|
||||
NumberCriteriaSelection,
|
||||
DaySelection,
|
||||
LocationSelection,
|
||||
NumberLtGtInput,
|
||||
removeEqCriteriaValue,
|
||||
clearCriteriaField,
|
||||
editCriteria,
|
||||
editGtLtCriteriaField,
|
||||
} from "..";
|
||||
import {
|
||||
EqCriteriaSelectionProps,
|
||||
|
@ -15,23 +24,21 @@ import {
|
|||
CriteriaSelectionProps,
|
||||
DEFAULT_CRITERIA,
|
||||
LocationSelectionProps,
|
||||
GroupCriteriaProps,
|
||||
NumberLtGtInputProps,
|
||||
} 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";
|
||||
import { Actions } from "../../../../constants";
|
||||
|
||||
describe("<EqCriteriaSelection<string> />", () => {
|
||||
const fakeProps = (): EqCriteriaSelectionProps<string> => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
dispatch: jest.fn(),
|
||||
type: "string",
|
||||
criteriaField: {},
|
||||
eqCriteria: {},
|
||||
criteriaKey: "string_eq",
|
||||
});
|
||||
|
||||
|
@ -43,12 +50,16 @@ describe("<EqCriteriaSelection<string> />", () => {
|
|||
|
||||
it("removes criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.criteriaField = { openfarm_slug: ["slug"] };
|
||||
p.eqCriteria = { 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);
|
||||
expect(removeEqCriteriaValue).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
{ openfarm_slug: ["slug"] },
|
||||
"string_eq",
|
||||
"openfarm_slug",
|
||||
"slug",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -56,24 +67,29 @@ describe("<NumberCriteriaSelection />", () => {
|
|||
const fakeProps = (): NumberCriteriaProps => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
dispatch: jest.fn(),
|
||||
criteriaKey: "number_lt",
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.criteria.number_lt = { x: 1 };
|
||||
const wrapper = mount(<NumberCriteriaSelection {...p} />);
|
||||
expect(wrapper.text()).toContain("<");
|
||||
});
|
||||
|
||||
it("removes criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.criteria.number_lt = { x: 1 };
|
||||
p.criteriaKey = "number_gt";
|
||||
p.criteria.number_gt = { x: 1 };
|
||||
const wrapper = mount(<NumberCriteriaSelection {...p} />);
|
||||
expect(wrapper.text()).toContain(">");
|
||||
wrapper.find("button").last().simulate("click");
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.number_lt = {};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
expect(clearCriteriaField).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
["number_gt"],
|
||||
"x",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -81,16 +97,17 @@ describe("<DaySelection />", () => {
|
|||
const fakeProps = (): CriteriaSelectionProps => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
dispatch: 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);
|
||||
expect(editCriteria).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
{ day: { days_ago: 0, op: "<" } },
|
||||
);
|
||||
});
|
||||
|
||||
it("changes day value", () => {
|
||||
|
@ -99,16 +116,46 @@ describe("<DaySelection />", () => {
|
|||
wrapper.find("input").last().simulate("change", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
const expectedBody = cloneDeep(p.group.body);
|
||||
expectedBody.criteria.day.days_ago = 1;
|
||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
||||
expect(editCriteria).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
{ day: { days_ago: 1, op: "<" } },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<NumberLtGtInput />", () => {
|
||||
const fakeProps = (): NumberLtGtInputProps => ({
|
||||
criteriaKey: "x",
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("handles missing criteria", () => {
|
||||
it("changes number_gt", () => {
|
||||
const p = fakeProps();
|
||||
p.criteria = {} as PointGroup["criteria"];
|
||||
const wrapper = shallow(<DaySelection {...p} />);
|
||||
expect(wrapper.find("input").last().props().value).toEqual(0);
|
||||
const wrapper = shallow(<NumberLtGtInput {...p} />);
|
||||
wrapper.find("input").first().simulate("blur", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
expect(editGtLtCriteriaField).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
"number_gt",
|
||||
"x",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("changes number_lt", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<NumberLtGtInput {...p} />);
|
||||
wrapper.find("input").last().simulate("blur", {
|
||||
currentTarget: { value: "1" }
|
||||
});
|
||||
expect(editGtLtCriteriaField).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
"number_lt",
|
||||
"x",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -116,84 +163,17 @@ describe("<LocationSelection />", () => {
|
|||
const fakeProps = (): LocationSelectionProps => ({
|
||||
criteria: DEFAULT_CRITERIA,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(x => x(jest.fn())),
|
||||
dispatch: jest.fn(),
|
||||
editGroupAreaInMap: false,
|
||||
});
|
||||
|
||||
it("changes number_gt", () => {
|
||||
it("toggles selection box behavior", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<LocationSelection {...p} />);
|
||||
wrapper.find("input").first().simulate("blur", {
|
||||
currentTarget: { value: "1" }
|
||||
const wrapper = mount(<LocationSelection {...p} />);
|
||||
wrapper.find("button").first().simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.EDIT_GROUP_AREA_IN_MAP,
|
||||
payload: true
|
||||
});
|
||||
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,32 @@
|
|||
jest.mock("../edit", () => ({
|
||||
toggleAndEditEqCriteria: jest.fn(),
|
||||
}));
|
||||
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { toggleAndEditEqCriteria } from "..";
|
||||
import { CheckboxListProps } from "../interfaces";
|
||||
import {
|
||||
fakePointGroup,
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import { CheckboxList } from "../subcriteria";
|
||||
|
||||
describe("<CheckboxList />", () => {
|
||||
const fakeProps = (): CheckboxListProps<string> => ({
|
||||
criteriaKey: "openfarm_slug",
|
||||
list: [{ label: "label", value: "value" }],
|
||||
dispatch: jest.fn(),
|
||||
group: fakePointGroup(),
|
||||
pointerType: "Plant",
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
it("toggles criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<CheckboxList {...p} />);
|
||||
expect(wrapper.text()).toContain("label");
|
||||
wrapper.find("input").first().simulate("change");
|
||||
expect(toggleAndEditEqCriteria).toHaveBeenCalledWith(
|
||||
p.group, "openfarm_slug", "value", "Plant");
|
||||
});
|
||||
});
|
|
@ -1,32 +1,28 @@
|
|||
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 { cloneDeep, uniq } from "lodash";
|
||||
import { Row, Col } from "../../../ui";
|
||||
import { editCriteria } from ".";
|
||||
import {
|
||||
AddEqCriteriaProps,
|
||||
AddEqCriteriaState,
|
||||
NumberCriteriaProps,
|
||||
AddNumberCriteriaState,
|
||||
AddStringCriteriaProps,
|
||||
} from "./interfaces";
|
||||
import {
|
||||
PLANT_STAGE_DDI_LOOKUP, PLANT_STAGE_LIST,
|
||||
} from "../../plants/edit_plant_status";
|
||||
|
||||
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 { dispatch, group, criteriaKey, eqCriteria } = this.props;
|
||||
const tempEqCriteria = cloneDeep(eqCriteria);
|
||||
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;
|
||||
tempEqCriteria[this.state.key] = uniq(tempValues);
|
||||
dispatch(editCriteria(group, { [criteriaKey]: tempEqCriteria }));
|
||||
this.setState({ key: "", value: "" });
|
||||
}
|
||||
|
@ -62,115 +58,13 @@ export class AddEqCriteria<T extends string | number>
|
|||
}
|
||||
}
|
||||
|
||||
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("Slots"), value: "ToolSlot" },
|
||||
});
|
||||
export const POINTER_TYPE_LIST = () => [
|
||||
POINTER_TYPE_DDI_LOOKUP().Plant,
|
||||
POINTER_TYPE_DDI_LOOKUP().GenericPointer,
|
||||
POINTER_TYPE_DDI_LOOKUP().ToolSlot,
|
||||
];
|
||||
|
||||
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"
|
||||
title={t("add string criteria")}
|
||||
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] || {});
|
||||
const tempNumberCriteria = cloneDeep(group.body.criteria[criteriaKey]);
|
||||
tempNumberCriteria[this.state.key] = this.state.value;
|
||||
dispatch(editCriteria(group, { [criteriaKey]: tempNumberCriteria }));
|
||||
this.setState({ key: "", value: 0 });
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { every, get, isEqual, uniq, gt, lt, isNumber } from "lodash";
|
||||
import { every, get, 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";
|
||||
import { PointGroupCriteria } from "./interfaces";
|
||||
|
||||
/** Check if a string or number criteria field is empty. */
|
||||
const eqCriteriaEmpty =
|
||||
(eqCriteria: Record<string, (string | number)[] | undefined>) =>
|
||||
every(Object.values(eqCriteria).map(values => !values?.length));
|
||||
|
||||
/** Check if a point matches the criteria in the provided category. */
|
||||
const checkCriteria =
|
||||
(criteria: PointGroup["criteria"], now: moment.Moment) =>
|
||||
(point: TaggedPoint, criteriaKey: keyof PointGroup["criteria"]) => {
|
||||
(criteria: PointGroupCriteria, now: moment.Moment) =>
|
||||
(point: TaggedPoint, criteriaKey: keyof PointGroupCriteria) => {
|
||||
switch (criteriaKey) {
|
||||
case "string_eq":
|
||||
case "number_eq":
|
||||
|
@ -38,18 +39,19 @@ const checkCriteria =
|
|||
}
|
||||
};
|
||||
|
||||
/** Check if a point matches all criteria provided. */
|
||||
export const selectPointsByCriteria = (
|
||||
criteria: PointGroup["criteria"] | undefined,
|
||||
criteria: PointGroupCriteria,
|
||||
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"]) =>
|
||||
every(Object.keys(criteria).map((key: keyof PointGroupCriteria) =>
|
||||
check(point, key))));
|
||||
};
|
||||
|
||||
/** Return all points selected by group manual additions and criteria. */
|
||||
export const pointsSelectedByGroup =
|
||||
(group: TaggedPointGroup, allPoints: TaggedPoint[]) =>
|
||||
uniq(allPoints
|
||||
|
|
|
@ -2,63 +2,68 @@ import * as React from "react";
|
|||
import { t } from "../../../i18next_wrapper";
|
||||
import { overwrite, save } from "../../../api/crud";
|
||||
import {
|
||||
CheckboxSelections, DaySelection, EqCriteriaSelection,
|
||||
NumberCriteriaSelection, LocationSelection, AddCriteria,
|
||||
DaySelection, EqCriteriaSelection,
|
||||
NumberCriteriaSelection, LocationSelection, CheckboxSelections,
|
||||
} from ".";
|
||||
import {
|
||||
GroupCriteriaProps, GroupPointCountBreakdownProps, GroupCriteriaState,
|
||||
DEFAULT_CRITERIA,
|
||||
DEFAULT_CRITERIA, ClearCriteriaProps,
|
||||
} from "./interfaces";
|
||||
import { ExpandableHeader } from "../../../ui";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import { ToggleButton } from "../../../controls/toggle_button";
|
||||
|
||||
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 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"
|
||||
title={t("clear all criteria")}
|
||||
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>
|
||||
<ToggleButton
|
||||
title={t("toggle advanced view")}
|
||||
toggleValue={!this.state.advanced}
|
||||
customText={{ textTrue: t("basic"), textFalse: t("advanced") }}
|
||||
toggleAction={() => this.setState({ advanced: !this.state.advanced })} />
|
||||
{!this.state.advanced
|
||||
? <div className={"basic"}>
|
||||
<CheckboxSelections group={group} dispatch={dispatch} slugs={slugs} />
|
||||
<DaySelection {...commonProps} />
|
||||
<LocationSelection {...commonProps}
|
||||
editGroupAreaInMap={this.props.editGroupAreaInMap} />
|
||||
</div>
|
||||
: <div className={"advanced"}>
|
||||
<DaySelection {...commonProps} />
|
||||
<label>{t("string criteria")}</label>
|
||||
<EqCriteriaSelection<string> {...commonProps}
|
||||
type={"string"} eqCriteria={criteria.string_eq}
|
||||
criteriaKey={"string_eq"} />
|
||||
<label>{t("number criteria")}</label>
|
||||
<EqCriteriaSelection<number> {...commonProps}
|
||||
type={"number"} eqCriteria={criteria.number_eq}
|
||||
criteriaKey={"number_eq"} />
|
||||
<NumberCriteriaSelection {...commonProps} criteriaKey={"number_lt"} />
|
||||
<NumberCriteriaSelection {...commonProps} criteriaKey={"number_gt"} />
|
||||
</div>}
|
||||
<ClearCriteria dispatch={dispatch} group={group} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset all group criteria to defaults. */
|
||||
const ClearCriteria = (props: ClearCriteriaProps) =>
|
||||
<button className="clear-criteria fb-button red no-float"
|
||||
title={t("clear all criteria")}
|
||||
onClick={() => {
|
||||
props.dispatch(overwrite(props.group, {
|
||||
...props.group.body, criteria: DEFAULT_CRITERIA
|
||||
}));
|
||||
props.dispatch(save(props.group.uuid));
|
||||
}}>
|
||||
{t("clear all criteria")}
|
||||
</button>;
|
||||
|
||||
/** Show counts of manual and criteria selections. */
|
||||
export const GroupPointCountBreakdown = (props: GroupPointCountBreakdownProps) =>
|
||||
<div className={"criteria-point-count-breakdown"}>
|
||||
<div className={"manual-group-member-count"}>
|
||||
|
|
|
@ -1,64 +1,130 @@
|
|||
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";
|
||||
import {
|
||||
PointGroupCriteria, POINTER_TYPES, EqCriteria, PointerType,
|
||||
StrAndNumCriteriaKeys,
|
||||
} from "./interfaces";
|
||||
|
||||
/** Update and save group criteria. */
|
||||
export const editCriteria =
|
||||
(group: TaggedPointGroup, update: Partial<PointGroup["criteria"]>) =>
|
||||
(group: TaggedPointGroup, update: Partial<PointGroupCriteria>) =>
|
||||
(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 || {},
|
||||
string_eq: update.string_eq || group.body.criteria.string_eq,
|
||||
day: update.day || group.body.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));
|
||||
};
|
||||
|
||||
/** Toggle string or number equal criteria. */
|
||||
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)) {
|
||||
eqCriteria: EqCriteria<T>,
|
||||
direction?: "on" | "off",
|
||||
) => (key: string, value: T) => {
|
||||
const values: T[] = eqCriteria[key] || [];
|
||||
if (values.includes(value)) {
|
||||
if (direction != "on") {
|
||||
const newValues = values.filter(s => s != value);
|
||||
eqCriteria[key] = newValues;
|
||||
!newValues.length && delete eqCriteria[key];
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
if (direction != "off") {
|
||||
values.push(value);
|
||||
eqCriteria[key] = values;
|
||||
}
|
||||
return eqCriteria;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle and save string or number equal criteria.
|
||||
* When adding criteria with a pointerType provided, clear incompatible criteria.
|
||||
*/
|
||||
export const toggleAndEditEqCriteria = <T extends string | number>(
|
||||
group: TaggedPointGroup,
|
||||
key: string,
|
||||
value: T,
|
||||
pointerType?: PointerType,
|
||||
) =>
|
||||
(dispatch: Function) => {
|
||||
const tempCriteria = cloneDeep(group.body.criteria);
|
||||
const criteriaField = typeof value == "string" ? "string_eq" : "number_eq";
|
||||
const tempEqCriteria = tempCriteria[criteriaField] as EqCriteria<T>;
|
||||
const wasOff = !tempEqCriteria[key]?.includes(value);
|
||||
toggleEqCriteria<T>(tempEqCriteria)(key, value);
|
||||
pointerType && wasOff && clearSubCriteria(
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
|
||||
dispatch(editCriteria(group, tempCriteria));
|
||||
};
|
||||
|
||||
export const togglePointSelection =
|
||||
(group: TaggedPointGroup) => (toggleCriteria: Record<string, string>) =>
|
||||
/** Clear incompatible criteria. */
|
||||
const clearSubCriteria = (
|
||||
pointerTypes: PointerType[],
|
||||
tempCriteria: PointGroupCriteria,
|
||||
) => {
|
||||
const toggleStrEq = toggleEqCriteria<string>(tempCriteria.string_eq, "off");
|
||||
const toggleNumEq = toggleEqCriteria<number>(tempCriteria.number_eq, "off");
|
||||
if (pointerTypes.includes("Plant")) {
|
||||
Object.entries(tempCriteria.string_eq).map(([key, values]) =>
|
||||
["openfarm_slug", "plant_stage"].includes(key)
|
||||
&& values?.map(v => toggleStrEq(key, v)));
|
||||
toggleStrEq("pointer_type", "Plant");
|
||||
}
|
||||
if (pointerTypes.includes("GenericPointer")) {
|
||||
Object.entries(tempCriteria.string_eq).map(([key, values]) =>
|
||||
key.includes("meta") && values?.map(v => toggleStrEq(key, v)));
|
||||
delete tempCriteria.number_lt.radius;
|
||||
delete tempCriteria.number_gt.radius;
|
||||
toggleStrEq("pointer_type", "GenericPointer");
|
||||
}
|
||||
if (pointerTypes.includes("ToolSlot")) {
|
||||
tempCriteria.number_eq.pullout_direction?.map(value =>
|
||||
toggleNumEq("pullout_direction", value));
|
||||
toggleStrEq("pointer_type", "ToolSlot");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle and save pointer_type string equal criteria.
|
||||
* When removing pointer_type criteria, clear pointer_type-specific criteria.
|
||||
*/
|
||||
export const togglePointTypeCriteria =
|
||||
(group: TaggedPointGroup, pointerType: PointerType) =>
|
||||
(dispatch: Function) => {
|
||||
const stringCriteria = {};
|
||||
const toggle = toggleEqCriteria<string>(stringCriteria);
|
||||
Object.entries(toggleCriteria).map(([key, value]) => toggle(key, value));
|
||||
dispatch(editCriteria(group, { string_eq: stringCriteria }));
|
||||
const tempCriteria = cloneDeep(group.body.criteria);
|
||||
const wasOn = tempCriteria.string_eq.pointer_type?.includes(pointerType);
|
||||
const toggle = toggleEqCriteria<string>(tempCriteria.string_eq);
|
||||
toggle("pointer_type", pointerType);
|
||||
wasOn && clearSubCriteria([pointerType], tempCriteria);
|
||||
dispatch(editCriteria(group, tempCriteria));
|
||||
};
|
||||
|
||||
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 }));
|
||||
};
|
||||
/** Clear and save all fields in the provided criteria categories. */
|
||||
export const clearCriteriaField = (
|
||||
group: TaggedPointGroup,
|
||||
categories: StrAndNumCriteriaKeys,
|
||||
field: string,
|
||||
) =>
|
||||
(dispatch: Function) => {
|
||||
const tempCriteria = cloneDeep(group.body.criteria);
|
||||
categories.map(category => delete tempCriteria[category][field]);
|
||||
dispatch(editCriteria(group, tempCriteria));
|
||||
};
|
||||
|
||||
/** For map selection box actions maybeUpdateGroup. */
|
||||
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 || {});
|
||||
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);
|
||||
|
@ -68,3 +134,36 @@ export const editGtLtCriteria =
|
|||
number_lt: tempLtCriteria,
|
||||
}));
|
||||
};
|
||||
|
||||
/** For EqCriteriaSelection form. */
|
||||
export const removeEqCriteriaValue = <T extends string | number>(
|
||||
group: TaggedPointGroup,
|
||||
eqCriteria: EqCriteria<T>,
|
||||
eqCriteriaName: string,
|
||||
key: string,
|
||||
value: T,
|
||||
) => (dispatch: Function) => {
|
||||
const tempCriteriaField = cloneDeep(eqCriteria);
|
||||
toggleEqCriteria<T>(tempCriteriaField, "off")(key, value);
|
||||
dispatch(editCriteria(group, { [eqCriteriaName]: tempCriteriaField }));
|
||||
};
|
||||
|
||||
/**
|
||||
* For criteria form NumberLtGtInput.
|
||||
* Clear incompatible criteria if pointer_type is provided.
|
||||
*/
|
||||
export const editGtLtCriteriaField = (
|
||||
group: TaggedPointGroup,
|
||||
criteriaField: "number_gt" | "number_lt",
|
||||
criteriaKey: string,
|
||||
pointerType?: PointerType,
|
||||
) =>
|
||||
(e: React.FormEvent<HTMLInputElement>) =>
|
||||
(dispatch: Function) => {
|
||||
const tempCriteria = cloneDeep(group.body.criteria);
|
||||
pointerType && clearSubCriteria(
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
|
||||
tempCriteria[criteriaField][criteriaKey] =
|
||||
parseInt(e.currentTarget.value);
|
||||
dispatch(editCriteria(group, tempCriteria));
|
||||
};
|
||||
|
|
|
@ -3,4 +3,6 @@ export * from "./apply";
|
|||
export * from "./component";
|
||||
export * from "./edit";
|
||||
export * from "./presets";
|
||||
export * from "./selected";
|
||||
export * from "./show";
|
||||
export * from "./subcriteria";
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
import { TaggedPointGroup } from "farmbot";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import { PointGroup, Point } from "farmbot/dist/resources/api_resources";
|
||||
|
||||
export const DEFAULT_CRITERIA: Readonly<PointGroup["criteria"]> = {
|
||||
export type PointGroupCriteria = PointGroup["criteria"];
|
||||
export type StringEqCriteria = PointGroupCriteria["string_eq"];
|
||||
export type PointerType = Point["pointer_type"];
|
||||
export type StrAndNumCriteriaKeys = (keyof Omit<PointGroupCriteria, "day">)[];
|
||||
export type EqCriteria<T> = Record<string, T[] | undefined>;
|
||||
|
||||
export const POINTER_TYPES: PointerType[] =
|
||||
["Plant", "GenericPointer", "ToolSlot"];
|
||||
|
||||
export const DEFAULT_CRITERIA: Readonly<PointGroupCriteria> = {
|
||||
day: { op: "<", days_ago: 0 },
|
||||
number_eq: {},
|
||||
number_gt: {},
|
||||
number_lt: {},
|
||||
string_eq: {},
|
||||
string_eq: { pointer_type: ["Plant"] },
|
||||
};
|
||||
|
||||
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[];
|
||||
editGroupAreaInMap: boolean;
|
||||
}
|
||||
|
||||
export interface GroupCriteriaState {
|
||||
|
@ -23,24 +30,30 @@ export interface GroupCriteriaState {
|
|||
clearCount: number;
|
||||
}
|
||||
|
||||
export interface ClearCriteriaProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
}
|
||||
|
||||
export interface GroupPointCountBreakdownProps {
|
||||
manualCount: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface CriteriaSelectionProps {
|
||||
criteria: PointGroup["criteria"];
|
||||
criteria: PointGroupCriteria;
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export interface LocationSelectionProps extends CriteriaSelectionProps {
|
||||
editGroupAreaInMap: boolean;
|
||||
}
|
||||
|
||||
export interface EqCriteriaSelectionProps<T> extends CriteriaSelectionProps {
|
||||
type: "string" | "number";
|
||||
criteriaField: Record<string, T[] | undefined> | undefined;
|
||||
criteriaKey: keyof PointGroup["criteria"];
|
||||
eqCriteria: EqCriteria<T>;
|
||||
criteriaKey: keyof PointGroupCriteria;
|
||||
}
|
||||
|
||||
export interface NumberCriteriaProps extends CriteriaSelectionProps {
|
||||
|
@ -51,31 +64,64 @@ export interface AddEqCriteriaProps<T> {
|
|||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
type: "string" | "number";
|
||||
criteriaField: Record<string, T[] | undefined> | undefined;
|
||||
criteriaKey: keyof PointGroup["criteria"];
|
||||
eqCriteria: EqCriteria<T>;
|
||||
criteriaKey: keyof PointGroupCriteria;
|
||||
}
|
||||
|
||||
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 SubCriteriaProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface PlantSubCriteriaProps extends SubCriteriaProps {
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export interface CheckboxSelectionsProps {
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export interface CheckboxSelectionsState {
|
||||
Plant: boolean;
|
||||
GenericPointer: boolean;
|
||||
ToolSlot: boolean;
|
||||
}
|
||||
|
||||
export interface NumberLtGtInputProps {
|
||||
criteriaKey: "x" | "y" | "radius";
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
inputWidth?: number;
|
||||
labelWidth?: number;
|
||||
disabled?: boolean;
|
||||
pointerType?: PointerType;
|
||||
}
|
||||
|
||||
export interface ClearCategoryProps {
|
||||
group: TaggedPointGroup;
|
||||
criteriaCategories: StrAndNumCriteriaKeys;
|
||||
criteriaKey: string;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export interface CheckboxListProps<T> {
|
||||
criteriaKey: string;
|
||||
list: { label: string, value: T }[];
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
pointerType: PointerType;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
|
@ -1,58 +1,77 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { every } from "lodash";
|
||||
import { togglePointSelection } from ".";
|
||||
import { CheckboxSelectionsProps, StringEqCriteria } from "./interfaces";
|
||||
import {
|
||||
togglePointTypeCriteria,
|
||||
eqCriteriaSelected,
|
||||
hasSubCriteria,
|
||||
typeDisabled,
|
||||
PlantCriteria,
|
||||
PointCriteria,
|
||||
ToolCriteria,
|
||||
} from ".";
|
||||
import {
|
||||
CheckboxSelectionsProps,
|
||||
CheckboxSelectionsState,
|
||||
PointerType,
|
||||
} from "./interfaces";
|
||||
import { Checkbox } from "../../../ui";
|
||||
|
||||
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",
|
||||
}
|
||||
},
|
||||
const CRITERIA_POINT_TYPES =
|
||||
(): { label: string, pointerType: PointerType }[] => [
|
||||
{ label: t("Plants"), pointerType: "Plant" },
|
||||
{ label: t("Points and Weeds"), pointerType: "GenericPointer" },
|
||||
{ label: t("Slots"), pointerType: "ToolSlot" },
|
||||
];
|
||||
|
||||
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 class CheckboxSelections extends React.Component
|
||||
<CheckboxSelectionsProps, Partial<CheckboxSelectionsState>> {
|
||||
state: CheckboxSelectionsState = {
|
||||
Plant: false, GenericPointer: false, ToolSlot: false
|
||||
};
|
||||
|
||||
export const criteriaSelected = (stringCriteria: StringEqCriteria) =>
|
||||
(selectionCriteria: Record<string, string>) =>
|
||||
every(Object.entries(selectionCriteria).map(([key, value]) =>
|
||||
stringCriteria?.[key]?.includes(value)));
|
||||
toggleMore = (section: keyof CheckboxSelectionsState) => () =>
|
||||
this.setState({ [section]: !this.state[section] });
|
||||
|
||||
render() {
|
||||
const { group, dispatch, slugs } = this.props;
|
||||
const { criteria } = group.body;
|
||||
const selected = eqCriteriaSelected<string>(criteria);
|
||||
return <div className={"point-type-checkboxes"}>
|
||||
{CRITERIA_POINT_TYPES().map(({ label, pointerType }, index) => {
|
||||
const typeSelected = selected("pointer_type", pointerType);
|
||||
const partial = hasSubCriteria(criteria)(pointerType) && !typeSelected;
|
||||
return <div className="point-type-section" key={index}>
|
||||
<div className="point-type-checkbox"
|
||||
onClick={this.toggleMore(pointerType)}>
|
||||
<Checkbox
|
||||
onChange={() =>
|
||||
dispatch(togglePointTypeCriteria(group, pointerType))}
|
||||
checked={typeSelected}
|
||||
partial={partial}
|
||||
title={t(label)}
|
||||
disabled={typeDisabled(criteria, pointerType)}
|
||||
onClick={e => e.stopPropagation()} />
|
||||
<p>{label}</p>
|
||||
<i className={
|
||||
`fa fa-caret-${this.state[pointerType] ? "up" : "down"}`}
|
||||
title={this.state[pointerType]
|
||||
? t("hide additional criteria")
|
||||
: t("show additional criteria")} />
|
||||
</div>
|
||||
{this.state.Plant && pointerType == "Plant" &&
|
||||
<PlantCriteria
|
||||
disabled={!typeSelected && !partial}
|
||||
group={group} dispatch={dispatch} slugs={slugs} />}
|
||||
{this.state.GenericPointer && pointerType == "GenericPointer" &&
|
||||
<PointCriteria
|
||||
disabled={!typeSelected && !partial}
|
||||
group={group} dispatch={dispatch} />}
|
||||
{this.state.ToolSlot && pointerType == "ToolSlot" &&
|
||||
<ToolCriteria
|
||||
disabled={!typeSelected && !partial}
|
||||
group={group} dispatch={dispatch} />}
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import { isNumber, some } from "lodash";
|
||||
import {
|
||||
StringEqCriteria,
|
||||
PointerType,
|
||||
PointGroupCriteria,
|
||||
StrAndNumCriteriaKeys,
|
||||
POINTER_TYPES,
|
||||
} from "./interfaces";
|
||||
|
||||
/** Check if equal criteria values exist. */
|
||||
export const eqCriteriaSelected =
|
||||
<T extends string | number>(criteria: PointGroupCriteria) =>
|
||||
(key: string, value: T): boolean => {
|
||||
if (typeof value == "string") {
|
||||
return !!criteria.string_eq[key]?.includes(value);
|
||||
}
|
||||
if (typeof value == "number") {
|
||||
return !!criteria.number_eq[key]?.includes(value);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Check if string equal criteria fields exist. */
|
||||
const strCriteriaHasKey = (stringCriteria: StringEqCriteria) =>
|
||||
(key: string) => (stringCriteria[key]?.length || 0) > 0;
|
||||
|
||||
/** Check if number criteria fields exist. */
|
||||
const numCriteriaHasKey = (criteria: PointGroupCriteria) =>
|
||||
(key: string) => (criteria.number_eq[key]?.length || 0) > 0
|
||||
|| isNumber(criteria.number_lt[key])
|
||||
|| isNumber(criteria.number_gt[key]);
|
||||
|
||||
/** Check if a string or number criteria field exists. */
|
||||
export const criteriaHasKey = (
|
||||
criteria: PointGroupCriteria,
|
||||
categories: StrAndNumCriteriaKeys,
|
||||
key: string,
|
||||
) =>
|
||||
some(categories.map(category => {
|
||||
if (category == "string_eq") {
|
||||
return strCriteriaHasKey(criteria.string_eq)(key);
|
||||
} else {
|
||||
return numCriteriaHasKey(criteria)(key);
|
||||
}
|
||||
}));
|
||||
|
||||
/** Check for point type specific sub criteria. */
|
||||
export const hasSubCriteria = (criteria: PointGroupCriteria) =>
|
||||
(pointerType: PointerType) => {
|
||||
const selected = strCriteriaHasKey(criteria.string_eq);
|
||||
const numSelected = numCriteriaHasKey(criteria);
|
||||
switch (pointerType) {
|
||||
case "GenericPointer":
|
||||
return !!(
|
||||
selected("meta.type")
|
||||
|| selected("meta.color")
|
||||
|| selected("meta.created_by")
|
||||
|| numSelected("radius"));
|
||||
case "Plant":
|
||||
return !!(
|
||||
selected("openfarm_slug")
|
||||
|| selected("plant_stage"));
|
||||
case "ToolSlot":
|
||||
return !!(
|
||||
numSelected("tool_id")
|
||||
|| numSelected("pullout_direction")
|
||||
|| numSelected("gantry_mounted"));
|
||||
}
|
||||
};
|
||||
|
||||
/** Check for criteria specific to other point types. */
|
||||
export const typeDisabled =
|
||||
(criteria: PointGroupCriteria, pointerType: PointerType): boolean =>
|
||||
some(POINTER_TYPES
|
||||
.filter(x => x != pointerType)
|
||||
.map(hasSubCriteria(criteria)));
|
|
@ -1,34 +1,35 @@
|
|||
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, AddStringCriteria,
|
||||
CRITERIA_TYPE_DDI_LOOKUP, toggleStringCriteria,
|
||||
AddEqCriteria, editCriteria, AddNumberCriteria,
|
||||
editGtLtCriteriaField,
|
||||
removeEqCriteriaValue,
|
||||
clearCriteriaField,
|
||||
} from ".";
|
||||
import {
|
||||
EqCriteriaSelectionProps, NumberCriteriaProps,
|
||||
CriteriaSelectionProps, LocationSelectionProps, GroupCriteriaProps,
|
||||
AddCriteriaState,
|
||||
DEFAULT_CRITERIA,
|
||||
CriteriaSelectionProps, LocationSelectionProps,
|
||||
NumberLtGtInputProps,
|
||||
PointGroupCriteria,
|
||||
} from "./interfaces";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||
import { PLANT_STAGE_DDI_LOOKUP } from "../../plants/edit_plant_status";
|
||||
import { ToggleButton } from "../../../controls/toggle_button";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
/** Add and view string or number equal criteria. */
|
||||
export class EqCriteriaSelection<T extends string | number>
|
||||
extends React.Component<EqCriteriaSelectionProps<T>> {
|
||||
render() {
|
||||
const { criteriaField, criteriaKey, group, dispatch } = this.props;
|
||||
const { eqCriteria, 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}
|
||||
type={this.props.type} eqCriteria={eqCriteria}
|
||||
criteriaKey={criteriaKey} />
|
||||
{criteriaField && Object.entries(criteriaField)
|
||||
{eqCriteria && Object.entries(eqCriteria)
|
||||
.map(([key, values]: [string, T[]], keyIndex) =>
|
||||
values && values.length > 0 &&
|
||||
<div key={keyIndex}>
|
||||
<label>{key}</label>
|
||||
<code>{key}</code>
|
||||
{values.map((value, valueIndex) =>
|
||||
<Row key={"" + keyIndex + valueIndex}>
|
||||
<Col xs={9}>
|
||||
|
@ -39,13 +40,8 @@ export class EqCriteriaSelection<T extends string | number>
|
|||
<Col xs={2}>
|
||||
<button className="fb-button red"
|
||||
title={t("remove criteria")}
|
||||
onClick={() => {
|
||||
const tempCriteriaField = cloneDeep(criteriaField);
|
||||
toggleEqCriteria<T>(tempCriteriaField)(key, value);
|
||||
dispatch(editCriteria(group, {
|
||||
[criteriaKey]: tempCriteriaField
|
||||
}));
|
||||
}}>
|
||||
onClick={() => dispatch(removeEqCriteriaValue(
|
||||
group, eqCriteria, criteriaKey, key, value))}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
|
@ -55,6 +51,7 @@ export class EqCriteriaSelection<T extends string | number>
|
|||
}
|
||||
}
|
||||
|
||||
/** Add and view > or < number criteria. */
|
||||
export const NumberCriteriaSelection = (props: NumberCriteriaProps) => {
|
||||
const criteriaField = props.criteria[props.criteriaKey];
|
||||
return <div className={"number-gt-lt-criteria"}>
|
||||
|
@ -70,21 +67,13 @@ export const NumberCriteriaSelection = (props: NumberCriteriaProps) => {
|
|||
{props.criteriaKey == "number_gt" ? ">" : "<"}
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<input key={"" + keyIndex}
|
||||
name="value"
|
||||
disabled={true}
|
||||
value={value} />
|
||||
<p>{value}</p>
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button red"
|
||||
title={t("remove number criteria")}
|
||||
onClick={() => {
|
||||
const tempNumberCriteria = cloneDeep(criteriaField);
|
||||
delete tempNumberCriteria[key];
|
||||
props.dispatch(editCriteria(props.group, {
|
||||
[props.criteriaKey]: tempNumberCriteria
|
||||
}));
|
||||
}}>
|
||||
onClick={() => props.dispatch(clearCriteriaField(
|
||||
props.group, [props.criteriaKey], key))}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
|
@ -98,9 +87,10 @@ const DAY_OPERATOR_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
|||
[">"]: { label: t("greater than"), value: ">" },
|
||||
});
|
||||
|
||||
/** Edit and view day criteria. */
|
||||
export const DaySelection = (props: CriteriaSelectionProps) => {
|
||||
const { group, criteria, dispatch } = props;
|
||||
const dayCriteria = criteria.day || cloneDeep(DEFAULT_CRITERIA.day);
|
||||
const dayCriteria = criteria.day;
|
||||
return <div className="day-criteria">
|
||||
<label>{t("Age selection")}</label>
|
||||
<Row>
|
||||
|
@ -112,7 +102,7 @@ export const DaySelection = (props: CriteriaSelectionProps) => {
|
|||
onChange={ddi => dispatch(editCriteria(group, {
|
||||
day: {
|
||||
days_ago: dayCriteria.days_ago,
|
||||
op: ddi.value as PointGroup["criteria"]["day"]["op"]
|
||||
op: ddi.value as PointGroupCriteria["day"]["op"]
|
||||
}
|
||||
}))} />
|
||||
</Col>
|
||||
|
@ -131,96 +121,64 @@ export const DaySelection = (props: CriteriaSelectionProps) => {
|
|||
</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"
|
||||
name={`${axis}-number-gt`}
|
||||
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"
|
||||
name={`${axis}-number-lt`}
|
||||
defaultValue={ltCriteria[axis]}
|
||||
onBlur={e => {
|
||||
const tempLtCriteria = cloneDeep(ltCriteria);
|
||||
tempLtCriteria[axis] = parseInt(e.currentTarget.value);
|
||||
dispatch(editCriteria(group, { number_lt: tempLtCriteria }));
|
||||
}} />
|
||||
</Col>
|
||||
</Row>)}
|
||||
</div>;
|
||||
/** Edit number < and > criteria. */
|
||||
export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
|
||||
const { group, dispatch, criteriaKey, pointerType } = props;
|
||||
const gtCriteria = props.group.body.criteria.number_gt;
|
||||
const ltCriteria = props.group.body.criteria.number_lt;
|
||||
return <Row>
|
||||
<Col xs={props.inputWidth || 4}>
|
||||
<input key={JSON.stringify(gtCriteria)}
|
||||
type="number"
|
||||
name={`${criteriaKey}-number-gt`}
|
||||
defaultValue={gtCriteria[criteriaKey]}
|
||||
disabled={props.disabled}
|
||||
onBlur={e => dispatch(editGtLtCriteriaField(
|
||||
group, "number_gt", criteriaKey, pointerType)(e))} />
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<p>{"<"}</p>
|
||||
</Col>
|
||||
<Col xs={props.labelWidth || 1}>
|
||||
<p>{criteriaKey}</p>
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<p>{"<"}</p>
|
||||
</Col>
|
||||
<Col xs={props.inputWidth || 4}>
|
||||
<input key={JSON.stringify(ltCriteria)}
|
||||
type="number"
|
||||
name={`${criteriaKey}-number-lt`}
|
||||
defaultValue={ltCriteria[criteriaKey]}
|
||||
disabled={props.disabled}
|
||||
onBlur={e => dispatch(editGtLtCriteriaField(
|
||||
group, "number_lt", criteriaKey, pointerType)(e))} />
|
||||
</Col>
|
||||
</Row>;
|
||||
};
|
||||
|
||||
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}
|
||||
name="key"
|
||||
disabled={true} />
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
<input value={this.labelLookup(key, value)}
|
||||
name="value"
|
||||
disabled={true} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button red"
|
||||
title={t("remove criteria")}
|
||||
onClick={() => props.dispatch(
|
||||
toggleStringCriteria(props.group, key, value))}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>))}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
/** Form inputs to define a 2D group criteria area. */
|
||||
export const LocationSelection = (props: LocationSelectionProps) =>
|
||||
<div className="location-criteria">
|
||||
<label>{t("Location selection")}</label>
|
||||
{["x", "y"].map((axis: "x" | "y") =>
|
||||
<NumberLtGtInput
|
||||
key={axis}
|
||||
criteriaKey={axis}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch} />)}
|
||||
<div className={"edit-in-map"}>
|
||||
<ToggleButton
|
||||
title={props.editGroupAreaInMap
|
||||
? t("map boxes will change location criteria")
|
||||
: t("map boxes will manually add plants")}
|
||||
customText={{ textFalse: t("off"), textTrue: t("on") }}
|
||||
toggleValue={props.editGroupAreaInMap}
|
||||
toggleAction={() =>
|
||||
props.dispatch({
|
||||
type: Actions.EDIT_GROUP_AREA_IN_MAP,
|
||||
payload: !props.editGroupAreaInMap
|
||||
})} />
|
||||
<label>{t("edit in map")}</label>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { capitalize, uniq } from "lodash";
|
||||
import {
|
||||
NumberLtGtInput,
|
||||
toggleAndEditEqCriteria,
|
||||
clearCriteriaField,
|
||||
eqCriteriaSelected,
|
||||
criteriaHasKey,
|
||||
} from ".";
|
||||
import {
|
||||
CheckboxListProps,
|
||||
SubCriteriaProps,
|
||||
PlantSubCriteriaProps,
|
||||
ClearCategoryProps,
|
||||
} from "./interfaces";
|
||||
import { PLANT_STAGE_LIST } from "../../plants/edit_plant_status";
|
||||
import { DIRECTION_CHOICES } from "../../tools/tool_slot_edit_components";
|
||||
import { Checkbox } from "../../../ui";
|
||||
|
||||
/** "All" (any) checkbox to show or choose state of criteria subcategory. */
|
||||
const ClearCategory = (props: ClearCategoryProps) => {
|
||||
const { group, criteriaCategories, criteriaKey, dispatch } = props;
|
||||
const all =
|
||||
!criteriaHasKey(group.body.criteria, criteriaCategories, criteriaKey);
|
||||
return <div className="criteria-checkbox-list-item">
|
||||
<Checkbox
|
||||
onChange={() =>
|
||||
dispatch(clearCriteriaField(group, criteriaCategories, criteriaKey))}
|
||||
checked={all}
|
||||
disabled={all}
|
||||
title={t("clear selections")}
|
||||
customDisabledText={t("selections empty")} />
|
||||
<p>{t("all")}</p>
|
||||
</div>;
|
||||
};
|
||||
|
||||
/** List of criteria toggle checkboxes. */
|
||||
export const CheckboxList =
|
||||
<T extends string | number>(props: CheckboxListProps<T>) => {
|
||||
const { criteria } = props.group.body;
|
||||
const selected = eqCriteriaSelected<T>(criteria);
|
||||
const toggle = toggleAndEditEqCriteria;
|
||||
return <div className={"criteria-checkbox-list"}>
|
||||
{props.list.map(({ label, value }: { label: string, value: T }, index) =>
|
||||
<div className="criteria-checkbox-list-item" key={index}>
|
||||
<Checkbox
|
||||
onChange={() => props.dispatch(toggle<T>(
|
||||
props.group, props.criteriaKey, value, props.pointerType))}
|
||||
checked={selected(props.criteriaKey, value)}
|
||||
title={t(label)}
|
||||
disabled={props.disabled} />
|
||||
<p>{label}</p>
|
||||
</div>)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
/** Criteria specific to plants. */
|
||||
export const PlantCriteria = (props: PlantSubCriteriaProps) => {
|
||||
const { group, dispatch, disabled } = props;
|
||||
const commonProps = { group, dispatch, disabled };
|
||||
return <div className={"plant-criteria-options"}>
|
||||
<PlantStage {...commonProps} />
|
||||
<PlantType {...commonProps} slugs={props.slugs} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PlantStage = (props: SubCriteriaProps) =>
|
||||
<div className={"plant-stage-criteria"}>
|
||||
<p className={"category"}>{t("Stage")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"plant_stage"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList<string>
|
||||
disabled={props.disabled}
|
||||
pointerType={"Plant"}
|
||||
criteriaKey={"plant_stage"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={PLANT_STAGE_LIST().map(ddi =>
|
||||
({ label: ddi.label, value: "" + ddi.value }))} />
|
||||
</div>;
|
||||
|
||||
const PlantType = (props: PlantSubCriteriaProps) =>
|
||||
<div className={"plant-type-criteria"}>
|
||||
<p className={"category"}>{t("Type")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"openfarm_slug"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList<string>
|
||||
disabled={props.disabled}
|
||||
pointerType={"Plant"}
|
||||
criteriaKey={"openfarm_slug"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={uniq(props.slugs
|
||||
.concat(props.group.body.criteria.string_eq.openfarm_slug || []))
|
||||
.map(slug =>
|
||||
({ label: capitalize(slug).replace("-", " "), value: slug }))} />
|
||||
</div>;
|
||||
|
||||
/** Criteria specific to map points. */
|
||||
export const PointCriteria = (props: SubCriteriaProps) => {
|
||||
const { group, dispatch, disabled } = props;
|
||||
const commonProps = { group, dispatch, disabled };
|
||||
return <div className={"point-criteria-options"}>
|
||||
<PointType {...commonProps} />
|
||||
<PointSource {...commonProps} />
|
||||
<Color {...commonProps} />
|
||||
<Radius {...commonProps} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PointType = (props: SubCriteriaProps) =>
|
||||
<div className={"point-type-criteria"}>
|
||||
<p className={"category"}>{t("Type")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"meta.type"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList
|
||||
disabled={props.disabled}
|
||||
pointerType={"GenericPointer"}
|
||||
criteriaKey={"meta.type"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={[
|
||||
{ label: t("Weeds"), value: "weed" },
|
||||
{ label: t("Points"), value: "point" },
|
||||
]} />
|
||||
</div>;
|
||||
|
||||
const PointSource = (props: SubCriteriaProps) =>
|
||||
<div className={"point-source-criteria"}>
|
||||
<p className={"category"}>{t("Source")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"meta.created_by"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList
|
||||
disabled={props.disabled}
|
||||
pointerType={"GenericPointer"}
|
||||
criteriaKey={"meta.created_by"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={[
|
||||
{ label: t("Weed Detector"), value: "plant-detection" },
|
||||
{ label: t("Farm Designer"), value: "farm-designer" },
|
||||
]} />
|
||||
</div>;
|
||||
|
||||
const Radius = (props: SubCriteriaProps) =>
|
||||
<div className={"radius-criteria"}>
|
||||
<p className={"category"}>{t("Radius")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["number_gt", "number_lt"]}
|
||||
criteriaKey={"radius"}
|
||||
dispatch={props.dispatch} />
|
||||
<div className={"lt-gt-criteria"}>
|
||||
<NumberLtGtInput
|
||||
disabled={props.disabled}
|
||||
criteriaKey={"radius"}
|
||||
inputWidth={3}
|
||||
labelWidth={2}
|
||||
group={props.group}
|
||||
pointerType={"GenericPointer"}
|
||||
dispatch={props.dispatch} />
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const Color = (props: SubCriteriaProps) =>
|
||||
<div className={"color-criteria"}>
|
||||
<p className={"category"}>{t("Color")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"meta.color"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList
|
||||
disabled={props.disabled}
|
||||
pointerType={"GenericPointer"}
|
||||
criteriaKey={"meta.color"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={[
|
||||
{ label: t("Green"), value: "green" },
|
||||
{ label: t("Red"), value: "red" },
|
||||
{ label: t("Cyan"), value: "cyan" },
|
||||
{ label: t("Blue"), value: "blue" },
|
||||
{ label: t("Yellow"), value: "yellow" },
|
||||
{ label: t("Orange"), value: "orange" },
|
||||
{ label: t("Purple"), value: "purple" },
|
||||
{ label: t("Pink"), value: "pink" },
|
||||
{ label: t("Gray"), value: "gray" },
|
||||
]} />
|
||||
</div>;
|
||||
|
||||
/** Criteria specific to tools. */
|
||||
export const ToolCriteria = (props: SubCriteriaProps) => {
|
||||
const { group, dispatch, disabled } = props;
|
||||
const commonProps = { group, dispatch, disabled };
|
||||
return <div className={"tool-criteria-options"}>
|
||||
<PulloutDirection {...commonProps} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PulloutDirection = (props: SubCriteriaProps) =>
|
||||
<div className={"pullout-direction-criteria"}>
|
||||
<p className={"category"}>{t("Direction")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["number_eq"]}
|
||||
criteriaKey={"pullout_direction"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList<number>
|
||||
disabled={props.disabled}
|
||||
pointerType={"ToolSlot"}
|
||||
criteriaKey={"pullout_direction"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={DIRECTION_CHOICES().map(ddi =>
|
||||
({ label: ddi.label, value: parseInt("" + ddi.value) }))} />
|
||||
</div>;
|
|
@ -24,6 +24,7 @@ interface GroupDetailProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
slugs: string[];
|
||||
hovered: UUID | undefined;
|
||||
editGroupAreaInMap: boolean;
|
||||
}
|
||||
|
||||
/** Find a group from a URL-provided ID. */
|
||||
|
@ -35,6 +36,8 @@ export const findGroupFromUrl = (groups: TaggedPointGroup[]) => {
|
|||
};
|
||||
|
||||
function mapStateToProps(props: Everything): GroupDetailProps {
|
||||
const { hoveredPlantListItem, editGroupAreaInMap } =
|
||||
props.resources.consumers.farm_designer;
|
||||
return {
|
||||
allPoints: selectAllActivePoints(props.resources.index),
|
||||
group: findGroupFromUrl(selectAllPointGroups(props.resources.index)),
|
||||
|
@ -42,7 +45,8 @@ function mapStateToProps(props: Everything): GroupDetailProps {
|
|||
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
|
||||
slugs: uniq(selectAllPlantPointers(props.resources.index)
|
||||
.map(p => p.body.openfarm_slug)),
|
||||
hovered: props.resources.consumers.farm_designer.hoveredPlantListItem,
|
||||
hovered: hoveredPlantListItem,
|
||||
editGroupAreaInMap,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from "./criteria";
|
||||
import { Content } from "../../constants";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import { Help } from "../../ui";
|
||||
|
||||
export interface GroupDetailActiveProps {
|
||||
dispatch: Function;
|
||||
|
@ -22,13 +23,17 @@ export interface GroupDetailActiveProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
slugs: string[];
|
||||
hovered: UUID | undefined;
|
||||
editGroupAreaInMap: boolean;
|
||||
}
|
||||
|
||||
type State = { timerId?: ReturnType<typeof setInterval> };
|
||||
interface GroupDetailActiveState {
|
||||
timerId?: ReturnType<typeof setInterval>;
|
||||
iconDisplay: boolean;
|
||||
}
|
||||
|
||||
export class GroupDetailActive
|
||||
extends React.Component<GroupDetailActiveProps, State> {
|
||||
state: State = {};
|
||||
extends React.Component<GroupDetailActiveProps, GroupDetailActiveState> {
|
||||
state: GroupDetailActiveState = { iconDisplay: true };
|
||||
|
||||
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
this.props.dispatch(edit(this.props.group, { name: currentTarget.value }));
|
||||
|
@ -76,6 +81,8 @@ export class GroupDetailActive
|
|||
(typeof timerId == "number") && clearInterval(timerId);
|
||||
}
|
||||
|
||||
toggleIconShow = () => this.setState({ iconDisplay: !this.state.iconDisplay });
|
||||
|
||||
render() {
|
||||
const { group, dispatch } = this.props;
|
||||
return <ErrorBoundary>
|
||||
|
@ -86,37 +93,18 @@ export class GroupDetailActive
|
|||
defaultValue={group.body.name}
|
||||
onChange={this.update}
|
||||
onBlur={this.saveGroup} />
|
||||
<div className={"group-sort-section"}>
|
||||
<label>
|
||||
{t("SORT BY")}
|
||||
</label>
|
||||
<Paths
|
||||
key={JSON.stringify(this.pointsSelectedByGroup
|
||||
.map(p => p.body.id))}
|
||||
pathPoints={this.pointsSelectedByGroup}
|
||||
dispatch={dispatch}
|
||||
group={group} />
|
||||
<p>
|
||||
{group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)}
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<GroupSortSelection group={group} dispatch={dispatch}
|
||||
pointsSelectedByGroup={this.pointsSelectedByGroup} />
|
||||
<GroupMemberDisplay group={group} dispatch={dispatch}
|
||||
pointsSelectedByGroup={this.pointsSelectedByGroup}
|
||||
icons={this.icons}
|
||||
iconDisplay={this.state.iconDisplay}
|
||||
toggleIconShow={this.toggleIconShow}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
{this.props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupCriteria dispatch={dispatch}
|
||||
group={group} slugs={this.props.slugs} />}
|
||||
group={group} slugs={this.props.slugs}
|
||||
editGroupAreaInMap={this.props.editGroupAreaInMap} />}
|
||||
<DeleteButton
|
||||
className="group-delete-btn"
|
||||
dispatch={dispatch}
|
||||
|
@ -127,3 +115,62 @@ export class GroupDetailActive
|
|||
</ErrorBoundary>;
|
||||
}
|
||||
}
|
||||
|
||||
interface GroupSortSelectionProps {
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
pointsSelectedByGroup: TaggedPoint[];
|
||||
}
|
||||
|
||||
/** Choose and view group point sort method. */
|
||||
const GroupSortSelection = (props: GroupSortSelectionProps) =>
|
||||
<div className={"group-sort-section"}>
|
||||
<label>
|
||||
{t("SORT BY")}
|
||||
</label>
|
||||
{props.group.body.sort_type == "random" &&
|
||||
<Help
|
||||
text={Content.SORT_DESCRIPTION}
|
||||
customIcon={"exclamation-triangle"} />}
|
||||
<Paths
|
||||
key={JSON.stringify(props.pointsSelectedByGroup
|
||||
.map(p => p.body.id))}
|
||||
pathPoints={props.pointsSelectedByGroup}
|
||||
dispatch={props.dispatch}
|
||||
group={props.group} />
|
||||
</div>;
|
||||
|
||||
interface GroupMemberDisplayProps {
|
||||
group: TaggedPointGroup;
|
||||
dispatch: Function;
|
||||
pointsSelectedByGroup: TaggedPoint[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
icons: JSX.Element[];
|
||||
iconDisplay: boolean;
|
||||
toggleIconShow(): void;
|
||||
}
|
||||
|
||||
/** View group point counts and icon list. */
|
||||
const GroupMemberDisplay = (props: GroupMemberDisplayProps) =>
|
||||
<div className="group-member-display">
|
||||
<label>
|
||||
{t("GROUP MEMBERS ({{count}})", { count: props.icons.length })}
|
||||
</label>
|
||||
<Help text={`${t("Click plants in map to add or remove.")} ${(
|
||||
props.shouldDisplay(Feature.criteria_groups) &&
|
||||
props.pointsSelectedByGroup.length != props.group.body.point_ids.length)
|
||||
? t(Content.CRITERIA_SELECTION_COUNT) : ""}`} />
|
||||
<i onClick={props.toggleIconShow}
|
||||
className={`fa fa-caret-${props.iconDisplay ? "up" : "down"}`}
|
||||
title={props.iconDisplay
|
||||
? t("hide icons")
|
||||
: t("show icons")} />
|
||||
{props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupPointCountBreakdown
|
||||
manualCount={props.group.body.point_ids.length}
|
||||
totalCount={props.pointsSelectedByGroup.length} />}
|
||||
{props.iconDisplay &&
|
||||
<div className="groups-list-wrapper">
|
||||
{props.icons}
|
||||
</div>}
|
||||
</div>;
|
||||
|
|
|
@ -23,6 +23,7 @@ export const initialState: DesignerState = {
|
|||
currentPoint: undefined,
|
||||
openedSavedGarden: undefined,
|
||||
tryGroupSortType: undefined,
|
||||
editGroupAreaInMap: false,
|
||||
};
|
||||
|
||||
export const designer = generateReducer<DesignerState>(initialState)
|
||||
|
@ -89,4 +90,8 @@ export const designer = generateReducer<DesignerState>(initialState)
|
|||
.add<PointGroupSortType | undefined>(Actions.TRY_SORT_TYPE, (s, { payload }) => {
|
||||
s.tryGroupSortType = payload;
|
||||
return s;
|
||||
})
|
||||
.add<boolean>(Actions.EDIT_GROUP_AREA_IN_MAP, (s, { payload }) => {
|
||||
s.editGroupAreaInMap = payload;
|
||||
return s;
|
||||
});
|
||||
|
|
|
@ -77,7 +77,7 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
<div className="mounted-tool">
|
||||
<div className="mounted-tool-header">
|
||||
<label>{t("mounted tool")}</label>
|
||||
<Help text={Content.MOUNTED_TOOL} requireClick={true} />
|
||||
<Help text={Content.MOUNTED_TOOL} />
|
||||
</div>
|
||||
<ToolSelection
|
||||
tools={this.props.tools}
|
||||
|
|
|
@ -41,8 +41,8 @@ export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
|
|||
})} />
|
||||
<FBSelect
|
||||
key={props.toolPulloutDirection}
|
||||
list={DIRECTION_CHOICES}
|
||||
selectedItem={DIRECTION_CHOICES_DDI[props.toolPulloutDirection]}
|
||||
list={DIRECTION_CHOICES()}
|
||||
selectedItem={DIRECTION_CHOICES_DDI()[props.toolPulloutDirection]}
|
||||
onChange={ddi => props.onChange({
|
||||
pullout_direction: parseInt("" + ddi.value)
|
||||
})} />
|
||||
|
@ -209,7 +209,7 @@ export const newSlotDirection =
|
|||
export const positionIsDefined = (position: BotPosition): boolean =>
|
||||
isNumber(position.x) && isNumber(position.y) && isNumber(position.z);
|
||||
|
||||
export const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = {
|
||||
export const DIRECTION_CHOICES_DDI = (): { [index: number]: DropDownItem } => ({
|
||||
[ToolPulloutDirection.NONE]:
|
||||
{ label: t("None"), value: ToolPulloutDirection.NONE },
|
||||
[ToolPulloutDirection.POSITIVE_X]:
|
||||
|
@ -220,12 +220,12 @@ export const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = {
|
|||
{ label: t("Positive Y"), value: ToolPulloutDirection.POSITIVE_Y },
|
||||
[ToolPulloutDirection.NEGATIVE_Y]:
|
||||
{ label: t("Negative Y"), value: ToolPulloutDirection.NEGATIVE_Y },
|
||||
};
|
||||
});
|
||||
|
||||
export const DIRECTION_CHOICES: DropDownItem[] = [
|
||||
DIRECTION_CHOICES_DDI[ToolPulloutDirection.NONE],
|
||||
DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_X],
|
||||
DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_X],
|
||||
DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_Y],
|
||||
DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_Y],
|
||||
export const DIRECTION_CHOICES = (): DropDownItem[] => [
|
||||
DIRECTION_CHOICES_DDI()[ToolPulloutDirection.NONE],
|
||||
DIRECTION_CHOICES_DDI()[ToolPulloutDirection.POSITIVE_X],
|
||||
DIRECTION_CHOICES_DDI()[ToolPulloutDirection.NEGATIVE_X],
|
||||
DIRECTION_CHOICES_DDI()[ToolPulloutDirection.POSITIVE_Y],
|
||||
DIRECTION_CHOICES_DDI()[ToolPulloutDirection.NEGATIVE_Y],
|
||||
];
|
||||
|
|
|
@ -53,7 +53,8 @@ export class RawEditZone extends React.Component<EditZoneProps, {}> {
|
|||
<LocationSelection
|
||||
group={zone}
|
||||
criteria={zone.body.criteria}
|
||||
dispatch={this.props.dispatch} />
|
||||
dispatch={this.props.dispatch}
|
||||
editGroupAreaInMap={true} />
|
||||
</div>
|
||||
: <span>{t("Redirecting")}...</span>}
|
||||
</DesignerPanelContent>
|
||||
|
|
|
@ -68,7 +68,7 @@ const LogSetting = (props: LogSettingProps) => {
|
|||
<label>
|
||||
{t(label)}
|
||||
</label>
|
||||
<Help text={t(toolTip)} position={Position.LEFT_TOP} requireClick={true} />
|
||||
<Help text={t(toolTip)} position={Position.LEFT_TOP} />
|
||||
<ToggleButton
|
||||
toggleValue={config.value}
|
||||
dim={!config.consistent}
|
||||
|
|
|
@ -77,7 +77,7 @@ export const SequenceSetting = (props: SequenceSettingProps) => {
|
|||
<label>
|
||||
{t(props.label)}
|
||||
</label>
|
||||
<Help text={t(props.description)} requireClick={true} />
|
||||
<Help text={t(props.description)} />
|
||||
<ToggleButton
|
||||
toggleValue={value}
|
||||
toggleAction={() => proceed() &&
|
||||
|
|
|
@ -41,6 +41,6 @@ export function StepIconGroup(props: StepIconBarProps) {
|
|||
<StepUpDownButtonPopover onMove={onMove} />
|
||||
<i className="fa fa-clone step-control" onClick={onClone} />
|
||||
<i className="fa fa-trash step-control" onClick={onTrash} />
|
||||
<Help text={helpText} requireClick={true} position={Position.TOP} />
|
||||
<Help text={helpText} position={Position.TOP} />
|
||||
</span>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../i18next_wrapper";
|
||||
|
||||
interface CheckboxProps {
|
||||
onChange(): void;
|
||||
checked: boolean;
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
partial?: boolean;
|
||||
onClick?: (e: React.FormEvent) => void;
|
||||
customDisabledText?: string;
|
||||
}
|
||||
|
||||
export const Checkbox = (props: CheckboxProps) =>
|
||||
<div className={["fb-checkbox",
|
||||
props.partial ? "partial" : "",
|
||||
props.disabled ? "disabled" : "",
|
||||
].join(" ")}
|
||||
title={props.disabled ? props.customDisabledText ?? t("incompatible") : ""}
|
||||
onClick={props.onClick}>
|
||||
<input type="checkbox"
|
||||
title={props.title}
|
||||
onChange={props.onChange}
|
||||
checked={props.checked} />
|
||||
</div>;
|
|
@ -1,19 +1,22 @@
|
|||
import * as React from "react";
|
||||
import { Popover, PopoverInteractionKind, PopoverPosition } from "@blueprintjs/core";
|
||||
import {
|
||||
Popover, PopoverInteractionKind, PopoverPosition, Position,
|
||||
} from "@blueprintjs/core";
|
||||
import { t } from "../i18next_wrapper";
|
||||
|
||||
interface HelpProps {
|
||||
text: string;
|
||||
requireClick?: boolean;
|
||||
onHover?: boolean;
|
||||
position?: PopoverPosition;
|
||||
customIcon?: string;
|
||||
}
|
||||
|
||||
export function Help(props: HelpProps) {
|
||||
return <Popover
|
||||
position={props.position}
|
||||
interactionKind={props.requireClick
|
||||
? PopoverInteractionKind.CLICK : PopoverInteractionKind.HOVER}
|
||||
position={props.position || Position.TOP_RIGHT}
|
||||
interactionKind={props.onHover
|
||||
? PopoverInteractionKind.HOVER
|
||||
: PopoverInteractionKind.CLICK}
|
||||
popoverClassName={"help"}>
|
||||
<i className={`fa fa-${props.customIcon || "question-circle"} help-icon`} />
|
||||
<div className={"help-text-content"}>{t(props.text)}</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from "./back_arrow";
|
||||
export * from "./blurable_input";
|
||||
export * from "./center_panel";
|
||||
export * from "./checkbox";
|
||||
export * from "./color_picker";
|
||||
export * from "./colors";
|
||||
export * from "./column";
|
||||
|
|
Loading…
Reference in New Issue