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