Merge pull request #1763 from FarmBot/mark_as

Add weed status for Mark As step
pull/1764/head
Rick Carlino 2020-04-20 09:26:28 -05:00 committed by GitHub
commit 0bd6d9a967
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 352 additions and 132 deletions

View File

@ -564,10 +564,10 @@
cursor: pointer;
}
.more-bugs,
.select-mode,
.move-to-mode {
button {
margin-right: 1rem;
}
margin: auto;
margin-top: 1rem;
p {
text-align: center;
padding-top: 2rem;

View File

@ -291,7 +291,7 @@
.panel-action-buttons {
position: absolute;
z-index: 9;
height: 19rem;
height: 25rem;
width: 100%;
background: $panel_medium_light_gray;
padding: 0.5rem;
@ -303,6 +303,7 @@
min-width: -webkit-fill-available;
margin-bottom: 0px;
margin-left: .5rem;
margin-top: 1rem;
}
.button-row {
float: left;
@ -325,7 +326,7 @@
}
}
.panel-content {
padding-top: 19rem;
padding-top: 25rem;
padding-right: 0;
padding-left: 0;
padding-bottom: 5rem;

View File

@ -41,6 +41,13 @@ export function Diagnosis(props: DiagnosisProps) {
<div className={"saucer-connector last " + diagnosisColor} />
</Col>
<Col xs={10} className={"connectivity-diagnosis"}>
<p className="blinking">
{t("Always")}&nbsp;
<a className="blinking" href="/app/device?highlight=farmbot_os">
<u>{t("upgrade FarmBot OS")}</u>
</a>
&nbsp;{t("before troubleshooting.")}
</p>
<p>
{diagnose(props)}
</p>

View File

@ -11,6 +11,7 @@ import { BooleanSetting } from "../../../session_keys";
import { DevSettings } from "../../../account/dev/dev_support";
import { t } from "../../../i18next_wrapper";
import { Feature } from "../../../devices/interfaces";
import { SelectModeLink } from "../../plants/select_plants";
export const ZoomControls = ({ zoom, getConfigValue }: {
zoom: (value: number) => () => void,
@ -109,6 +110,7 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
<ZoomControls zoom={props.zoom} getConfigValue={props.getConfigValue} />
<LayerToggles {...props} />
<MoveModeLink />
<SelectModeLink />
<BugsControls />
</div>
</div>;

View File

@ -0,0 +1,121 @@
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import { EditPlantStatusProps } from "../plant_panel";
import { shallow } from "enzyme";
import {
fakePlant, fakeWeed,
} from "../../../__test_support__/fake_state/resources";
import { edit } from "../../../api/crud";
import {
EditPlantStatus, PlantStatusBulkUpdateProps, PlantStatusBulkUpdate,
EditWeedStatus, EditWeedStatusProps,
} from "../edit_plant_status";
describe("<EditPlantStatus />", () => {
const fakeProps = (): EditPlantStatusProps => ({
uuid: "Plant.0.0",
plantStatus: "planned",
updatePlant: jest.fn(),
});
it("changes stage to planted", () => {
const p = fakeProps();
const wrapper = shallow(<EditPlantStatus {...p} />);
wrapper.find("FBSelect").simulate("change", { value: "planted" });
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
plant_stage: "planted",
planted_at: expect.stringContaining("Z")
});
});
it("changes stage to planned", () => {
const p = fakeProps();
const wrapper = shallow(<EditPlantStatus {...p} />);
wrapper.find("FBSelect").simulate("change", { value: "planned" });
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
plant_stage: "planned",
planted_at: undefined
});
});
});
describe("<PlantStatusBulkUpdate />", () => {
const fakeProps = (): PlantStatusBulkUpdateProps => ({
allPoints: [],
selected: [],
dispatch: jest.fn(),
pointerType: "Plant",
});
it("doesn't update plant statuses", () => {
const p = fakeProps();
const plant1 = fakePlant();
const plant2 = fakePlant();
p.allPoints = [plant1, plant2];
p.selected = [plant1.uuid];
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
window.confirm = jest.fn(() => false);
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
expect(window.confirm).toHaveBeenCalled();
expect(edit).not.toHaveBeenCalled();
});
it("updates plant statuses", () => {
const p = fakeProps();
const plant1 = fakePlant();
const plant2 = fakePlant();
const plant3 = fakePlant();
p.allPoints = [plant1, plant2, plant3];
p.selected = [plant1.uuid, plant2.uuid];
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
window.confirm = jest.fn(() => true);
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
expect(window.confirm).toHaveBeenCalledWith(
"Change status to 'planted' for 2 items?");
expect(edit).toHaveBeenCalledTimes(2);
expect(edit).toHaveBeenCalledWith(plant1, {
plant_stage: "planted",
planted_at: expect.stringContaining("Z"),
});
expect(edit).toHaveBeenCalledWith(plant2, {
plant_stage: "planted",
planted_at: expect.stringContaining("Z"),
});
});
it("updates weed statuses", () => {
const p = fakeProps();
p.pointerType = "Weed";
const weed1 = fakeWeed();
const weed2 = fakeWeed();
const weed3 = fakeWeed();
p.allPoints = [weed1, weed2, weed3];
p.selected = [weed1.uuid, weed2.uuid];
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
window.confirm = jest.fn(() => true);
wrapper.find("FBSelect").simulate("change", { label: "", value: "removed" });
expect(window.confirm).toHaveBeenCalledWith(
"Change status to 'removed' for 2 items?");
expect(edit).toHaveBeenCalledTimes(2);
expect(edit).toHaveBeenCalledWith(weed1, { plant_stage: "removed" });
expect(edit).toHaveBeenCalledWith(weed2, { plant_stage: "removed" });
});
});
describe("<EditWeedStatus />", () => {
const fakeProps = (): EditWeedStatusProps => ({
weed: fakeWeed(),
updateWeed: jest.fn(),
});
it("updates weed status", () => {
const p = fakeProps();
const wrapper = shallow(<EditWeedStatus {...p} />);
wrapper.find("FBSelect").simulate("change", { label: "", value: "removed" });
expect(p.updateWeed).toHaveBeenCalledWith({ plant_stage: "removed" });
});
});

View File

@ -1,13 +1,8 @@
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import {
PlantPanel, PlantPanelProps, EditPlantStatusProps,
PlantPanel, PlantPanelProps,
EditDatePlantedProps, EditDatePlanted, EditPlantLocationProps,
EditPlantLocation,
} from "../plant_panel";
@ -18,11 +13,6 @@ import { clickButton } from "../../../__test_support__/helpers";
import { history } from "../../../history";
import moment from "moment";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { edit } from "../../../api/crud";
import {
EditPlantStatus, PlantStatusBulkUpdateProps, PlantStatusBulkUpdate,
} from "../edit_plant_status";
describe("<PlantPanel/>", () => {
const info: FormattedPlantInfo = {
@ -106,70 +96,6 @@ describe("<PlantPanel/>", () => {
});
});
describe("<EditPlantStatus />", () => {
const fakeProps = (): EditPlantStatusProps => ({
uuid: "Plant.0.0",
plantStatus: "planned",
updatePlant: jest.fn(),
});
it("changes stage to planted", () => {
const p = fakeProps();
const wrapper = shallow(<EditPlantStatus {...p} />);
wrapper.find("FBSelect").simulate("change", { value: "planted" });
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
plant_stage: "planted",
planted_at: expect.stringContaining("Z")
});
});
it("changes stage to planned", () => {
const p = fakeProps();
const wrapper = shallow(<EditPlantStatus {...p} />);
wrapper.find("FBSelect").simulate("change", { value: "planned" });
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
plant_stage: "planned",
planted_at: undefined
});
});
});
describe("<PlantStatusBulkUpdate />", () => {
const fakeProps = (): PlantStatusBulkUpdateProps => ({
plants: [],
selected: [],
dispatch: jest.fn(),
});
it("doesn't update plant statuses", () => {
const p = fakeProps();
const plant1 = fakePlant();
const plant2 = fakePlant();
p.plants = [plant1, plant2];
p.selected = [plant1.uuid];
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
window.confirm = jest.fn(() => false);
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
expect(window.confirm).toHaveBeenCalled();
expect(edit).not.toHaveBeenCalled();
});
it("updates plant statuses", () => {
const p = fakeProps();
const plant1 = fakePlant();
const plant2 = fakePlant();
const plant3 = fakePlant();
p.plants = [plant1, plant2, plant3];
p.selected = [plant1.uuid, plant2.uuid];
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
window.confirm = jest.fn(() => true);
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
expect(window.confirm).toHaveBeenCalledWith(
"Change the plant status to 'planted' for 2 plants?");
expect(edit).toHaveBeenCalledTimes(2);
});
});
describe("<EditDatePlanted />", () => {
const fakeProps = (): EditDatePlantedProps => ({
uuid: "Plant.0.0",

View File

@ -18,12 +18,13 @@ import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps,
getFilteredPoints, GetFilteredPointsProps, validPointTypes,
getFilteredPoints, GetFilteredPointsProps, validPointTypes, SelectModeLink,
} from "../select_plants";
import {
fakePlant, fakePoint, fakeWeed, fakeToolSlot, fakeTool,
fakePlantTemplate,
fakeWebAppConfig,
fakePointGroup,
} from "../../../__test_support__/fake_state/resources";
import { Actions, Content } from "../../../constants";
import { clickButton } from "../../../__test_support__/helpers";
@ -35,6 +36,8 @@ import { mockDispatch } from "../../../__test_support__/fake_dispatch";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { history } from "../../../history";
import { POINTER_TYPES } from "../../point_groups/criteria/interfaces";
describe("<SelectPlants />", () => {
beforeEach(function () {
@ -60,6 +63,7 @@ describe("<SelectPlants />", () => {
quadrant: 2,
isActive: () => false,
tools: [],
groups: [],
};
}
@ -211,7 +215,57 @@ describe("<SelectPlants />", () => {
{ payload: undefined, type: Actions.SELECT_POINT });
});
const DELETE_BTN_INDEX = 3;
it("selects group items", () => {
const p = fakeProps();
p.selected = undefined;
const group = fakePointGroup();
group.body.id = 1;
const plant = fakePlant();
plant.body.id = 1;
group.body.point_ids = [1];
p.groups = [group];
p.allPoints = [plant];
const dispatch = jest.fn();
p.dispatch = mockDispatch(dispatch);
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
expect(wrapper.state().group_id).toEqual(undefined);
actionsWrapper.find("FBSelect").at(1).simulate("change", {
label: "", value: 1
});
expect(wrapper.state().group_id).toEqual(1);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SET_SELECTION_POINT_TYPE,
payload: POINTER_TYPES,
});
expect(p.dispatch).toHaveBeenLastCalledWith({
type: Actions.SELECT_POINT,
payload: [plant.uuid],
});
});
it("selects selection type", () => {
const p = fakeProps();
const group0 = fakePointGroup();
group0.body.id = 0;
const group1 = fakePointGroup();
group1.body.id = 1;
group1.body.criteria.string_eq = { pointer_type: ["Plant"] };
p.groups = [group0, group1];
const dispatch = jest.fn();
p.dispatch = mockDispatch(dispatch);
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
actionsWrapper.find("FBSelect").at(1).simulate("change", {
label: "", value: 1
});
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SET_SELECTION_POINT_TYPE,
payload: ["Plant"],
});
});
const DELETE_BTN_INDEX = 4;
it("confirms deletion of selected plants", () => {
const p = fakeProps();
@ -344,3 +398,11 @@ describe("validPointTypes()", () => {
expect(validPointTypes(["nope"])).toEqual(undefined);
});
});
describe("<SelectModeLink />", () => {
it("navigates to panel", () => {
const wrapper = shallow(<SelectModeLink />);
wrapper.find("button").simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
});
});

View File

@ -1,27 +1,34 @@
import * as React from "react";
import { FBSelect, DropDownItem } from "../../ui";
import { PlantOptions } from "../interfaces";
import { PlantStage } from "farmbot";
import { PlantStage, TaggedWeedPointer, PointType, TaggedPoint } from "farmbot";
import moment from "moment";
import { t } from "../../i18next_wrapper";
import { TaggedPlant } from "../map/interfaces";
import { UUID } from "../../resources/interfaces";
import { edit, save } from "../../api/crud";
import { EditPlantStatusProps } from "./plant_panel";
import { PlantPointer } from "farmbot/dist/resources/api_resources";
export const PLANT_STAGE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
planned: { label: t("Planned"), value: "planned" },
planted: { label: t("Planted"), value: "planted" },
sprouted: { label: t("Sprouted"), value: "sprouted" },
harvested: { label: t("Harvested"), value: "harvested" },
removed: { label: t("Removed"), value: "removed" },
});
export const PLANT_STAGE_LIST = () => [
PLANT_STAGE_DDI_LOOKUP().planned,
PLANT_STAGE_DDI_LOOKUP().planted,
PLANT_STAGE_DDI_LOOKUP().sprouted,
PLANT_STAGE_DDI_LOOKUP().harvested,
PLANT_STAGE_DDI_LOOKUP().removed,
];
export const WEED_STATUSES = ["removed"];
const WEED_STAGE_DDI_LOOKUP = (): Record<string, DropDownItem> => ({
removed: PLANT_STAGE_DDI_LOOKUP().removed,
});
/** Change `planted_at` value based on `plant_stage` update. */
const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => {
const update: PlantOptions = { plant_stage };
@ -46,33 +53,55 @@ export function EditPlantStatus(props: EditPlantStatusProps) {
}
export interface PlantStatusBulkUpdateProps {
plants: TaggedPlant[];
allPoints: TaggedPoint[];
selected: UUID[];
dispatch: Function;
pointerType: PointType;
}
/** Update `plant_stage` for multiple plants at once. */
export const PlantStatusBulkUpdate = (props: PlantStatusBulkUpdateProps) =>
<div className="plant-status-bulk-update">
<p>{t("update plant status to")}</p>
<p>{t("update status to")}</p>
<FBSelect
key={JSON.stringify(props.selected)}
list={PLANT_STAGE_LIST()}
list={PLANT_STAGE_LIST().filter(ddi =>
props.pointerType == "Plant" || WEED_STATUSES.includes("" + ddi.value))}
selectedItem={undefined}
customNullLabel={t("Select a status")}
onChange={ddi => {
const plant_stage = ddi.value as PlantStage;
const update = getUpdateByPlantStage(plant_stage);
const plants = props.plants.filter(plant =>
props.selected.includes(plant.uuid)
&& plant.kind === "Point"
&& plant.body.plant_stage != plant_stage);
plants.length > 0 && confirm(
t("Change the plant status to '{{ status }}' for {{ num }} plants?",
{ status: plant_stage, num: plants.length }))
&& plants.map(plant => {
props.dispatch(edit(plant, update));
props.dispatch(save(plant.uuid));
const update = props.pointerType == "Plant"
? getUpdateByPlantStage(plant_stage)
: { plant_stage };
const points = props.allPoints.filter(point =>
props.selected.includes(point.uuid)
&& point.kind === "Point"
&& ["Plant", "Weed"].includes(point.body.pointer_type)
&& (point.body as unknown as PlantPointer).plant_stage != plant_stage);
points.length > 0 && confirm(
t("Change status to '{{ status }}' for {{ num }} items?",
{ status: plant_stage, num: points.length }))
&& points.map(point => {
props.dispatch(edit(point, update));
props.dispatch(save(point.uuid));
});
}} />
</div>;
export interface EditWeedStatusProps {
weed: TaggedWeedPointer;
updateWeed(update: Partial<TaggedWeedPointer["body"]>): void;
}
/** Select a `plant_stage` for a weed. */
export const EditWeedStatus = (props: EditWeedStatusProps) =>
<FBSelect
list={PLANT_STAGE_LIST().filter(ddi => WEED_STATUSES.includes("" + ddi.value))}
selectedItem={WEED_STAGE_DDI_LOOKUP()[(
props.weed.body as unknown as PlantPointer).plant_stage]}
onChange={ddi =>
props.updateWeed({
["plant_stage" as keyof TaggedWeedPointer["body"]]:
ddi.value as PlantStage
})} />;

View File

@ -21,10 +21,11 @@ import {
PointType, TaggedPoint, TaggedGenericPointer, TaggedToolSlotPointer,
TaggedTool,
TaggedWeedPointer,
TaggedPointGroup,
} from "farmbot";
import { UUID } from "../../resources/interfaces";
import {
selectAllActivePoints, selectAllToolSlotPointers, selectAllTools,
selectAllActivePoints, selectAllToolSlotPointers, selectAllTools, selectAllPointGroups,
} from "../../resources/selectors";
import { PointInventoryItem } from "../points/point_inventory_item";
import { ToolSlotInventoryItem } from "../tools";
@ -37,6 +38,7 @@ import { isActive } from "../tools/edit_tool";
import { uniq } from "lodash";
import { POINTER_TYPES } from "../point_groups/criteria/interfaces";
import { WeedInventoryItem } from "../weeds/weed_inventory_item";
import { pointsSelectedByGroup } from "../point_groups/criteria";
// tslint:disable-next-line:no-any
export const isPointType = (x: any): x is PointType => POINTER_TYPES.includes(x);
@ -82,6 +84,7 @@ export const mapStateToProps = (props: Everything): SelectPlantsProps => {
dispatch: props.dispatch,
gardenOpen: props.resources.consumers.farm_designer.openedSavedGarden,
tools: selectAllTools(props.resources.index),
groups: selectAllPointGroups(props.resources.index),
isActive: isActive(selectAllToolSlotPointers(props.resources.index)),
xySwap,
quadrant,
@ -100,9 +103,17 @@ export interface SelectPlantsProps {
quadrant: BotOriginQuadrant;
isActive(id: number | undefined): boolean;
tools: TaggedTool[];
groups: TaggedPointGroup[];
}
export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
interface SelectPlantsState {
group_id: number | undefined;
}
export class RawSelectPlants
extends React.Component<SelectPlantsProps, SelectPlantsState> {
state: SelectPlantsState = { group_id: undefined };
componentDidMount() {
const { dispatch, selected } = this.props;
if (selected && selected.length == 1) {
@ -135,13 +146,39 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
return selectionPointTypes.length > 1 ? "All" : selectionPointTypes[0];
}
get groupDDILookup(): Record<number, DropDownItem> {
const lookup: Record<number, DropDownItem> = {};
this.props.groups.map(group => {
const { id } = group.body;
const groupName = group.body.name;
const count = pointsSelectedByGroup(group, this.props.allPoints).length;
const label = `${groupName} (${t("{{count}} items", { count })})`;
id && (lookup[id] = { label, value: id });
});
return lookup;
}
selectGroup = (ddi: DropDownItem) => {
const group_id = parseInt("" + ddi.value);
this.setState({ group_id });
const group = this.props.groups
.filter(pg => pg.body.id == group_id)[0];
const pointUuids = pointsSelectedByGroup(group, this.props.allPoints)
.map(p => p.uuid);
const pointerTypes =
group.body.criteria.string_eq.pointer_type as PointType[] | undefined;
this.props.dispatch(setSelectionPointType(pointerTypes || POINTER_TYPES));
this.props.dispatch(selectPoint(pointUuids));
}
ActionButtons = () =>
<div className="panel-action-buttons">
<FBSelect
<FBSelect key={this.selectionPointType}
list={POINTER_TYPE_LIST()}
selectedItem={POINTER_TYPE_DDI_LOOKUP()[this.selectionPointType]}
onChange={ddi => {
this.props.dispatch(selectPoint(undefined));
this.setState({ group_id: undefined });
this.props.dispatch(setSelectionPointType(
ddi.value == "All" ? POINTER_TYPES : validPointTypes([ddi.value])));
}} />
@ -156,6 +193,14 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
onClick={() => this.props.dispatch(selectPoint(this.allPointUuids))}>
{t("Select all")}
</button>
<label>{t("select all in group")}</label>
<FBSelect key={this.selectionPointType}
list={Object.values(this.groupDDILookup)}
selectedItem={this.state.group_id
? this.groupDDILookup[this.state.group_id]
: undefined}
customNullLabel={t("Select a group")}
onChange={this.selectGroup} />
</div>
<label>{t("SELECTION ACTIONS")}</label>
<div className="button-row">
@ -171,9 +216,10 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>
{t("Create group")}
</button>
{this.selectionPointType == "Plant" &&
{(this.selectionPointType == "Plant" || this.selectionPointType == "Weed") &&
<PlantStatusBulkUpdate
plants={this.props.plants}
pointerType={this.selectionPointType}
allPoints={this.props.allPoints}
selected={this.selected}
dispatch={this.props.dispatch} />}
</div>
@ -320,3 +366,13 @@ const getVisibleLayers = (getConfigValue: GetWebAppConfigValue): PointType[] =>
...(showFarmbot ? [PointerType.ToolSlot] : []),
];
};
export const SelectModeLink = () =>
<div className="select-mode">
<button
className="fb-button gray"
title={t("open point select panel")}
onClick={() => history.push("/app/designer/plants/select")}>
{t("select")}
</button>
</div>;

View File

@ -199,7 +199,7 @@ describe("togglePointTypeCriteria()", () => {
const group = fakePointGroup();
group.body.criteria.string_eq = {
pointer_type: ["Plant", "ToolSlot"],
"plant_stage": ["planned"],
"openfarm_slug": ["mint"],
};
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.string_eq = { pointer_type: ["Weed"] };

View File

@ -107,7 +107,7 @@ describe("typeDisabled()", () => {
it("isn't disabled", () => {
const criteria = fakeCriteria();
criteria.string_eq = { plant_stage: ["planted"] };
criteria.string_eq = { openfarm_slug: ["mint"] };
const result = typeDisabled(criteria, "Plant");
expect(result).toBeFalsy();
});

View File

@ -161,7 +161,6 @@ describe("<NumberLtGtInput />", () => {
p.group,
"number_gt",
"x",
undefined,
);
});
@ -175,7 +174,6 @@ describe("<NumberLtGtInput />", () => {
p.group,
"number_lt",
"x",
undefined,
);
});
});

View File

@ -81,6 +81,6 @@ describe("<CheckboxList />", () => {
expect(wrapper.text()).toContain("label");
wrapper.find("input").first().simulate("change");
expect(toggleAndEditEqCriteria).toHaveBeenCalledWith(
p.group, "openfarm_slug", "value", "Plant");
p.group, "openfarm_slug", "value");
});
});

View File

@ -60,7 +60,7 @@ export const toggleAndEditEqCriteria = <T extends string | number>(
const wasOff = !tempEqCriteria[key]?.includes(value);
toggleEqCriteria<T>(tempEqCriteria)(key, value);
pointerType && wasOff && clearSubCriteria(
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
POINTER_TYPES.filter(x => x != pointerType), tempCriteria, key);
dispatch(editCriteria(group, tempCriteria));
};
@ -68,16 +68,17 @@ export const toggleAndEditEqCriteria = <T extends string | number>(
export const clearSubCriteria = (
pointerTypes: PointerType[],
tempCriteria: PointGroupCriteria,
keepKey: string,
) => {
const toggleStrEq = toggleEqCriteria<string>(tempCriteria.string_eq, "off");
const toggleNumEq = toggleEqCriteria<number>(tempCriteria.number_eq, "off");
const toggleStrEqMapper = (key: string) =>
const toggleStrEqMapper = (key: string) => key != keepKey &&
tempCriteria.string_eq[key]?.map(value => toggleStrEq(key, value));
if (pointerTypes.includes("Plant")) {
["openfarm_slug", "plant_stage"].map(toggleStrEqMapper);
}
if (pointerTypes.includes("Weed")) {
["meta.created_by"].map(toggleStrEqMapper);
["meta.created_by", "plant_stage"].map(toggleStrEqMapper);
}
if (pointerTypes.includes("GenericPointer") && pointerTypes.includes("Weed")) {
["meta.color"].map(toggleStrEqMapper);
@ -101,8 +102,8 @@ export const togglePointTypeCriteria =
const toggle = toggleEqCriteria<string>(tempCriteria.string_eq);
clear && (tempCriteria.string_eq.pointer_type = []);
toggle("pointer_type", pointerType);
clearSubCriteria(
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
clearSubCriteria(POINTER_TYPES.filter(x => x != pointerType),
tempCriteria, "pointer_type");
dispatch(editCriteria(group, tempCriteria));
};
@ -164,7 +165,7 @@ export const editGtLtCriteriaField = (
(dispatch: Function) => {
const tempCriteria = cloneDeep(group.body.criteria);
pointerType && clearSubCriteria(
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
POINTER_TYPES.filter(x => x != pointerType), tempCriteria, criteriaKey);
const value = e.currentTarget.value != ""
? parseInt(e.currentTarget.value)
: undefined;

View File

@ -62,6 +62,7 @@ export const hasSubCriteria = (criteria: PointGroupCriteria) =>
case "Weed":
return !!(
selected("meta.created_by")
|| selected("plant_stage")
|| selected("meta.color")
|| numSelected("radius"));
case "Plant":

View File

@ -153,7 +153,7 @@ export const DaySelection = (props: DaySelectionProps) => {
/** Edit number < and > criteria. */
export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
const { group, dispatch, criteriaKey, pointerType } = props;
const { group, dispatch, criteriaKey } = props;
const gtCriteria = props.group.body.criteria.number_gt;
const ltCriteria = props.group.body.criteria.number_lt;
return <Row>
@ -164,7 +164,7 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
defaultValue={gtCriteria[criteriaKey]}
disabled={props.disabled}
onBlur={e => dispatch(editGtLtCriteriaField(
group, "number_gt", criteriaKey, pointerType)(e))} />
group, "number_gt", criteriaKey)(e))} />
</Col>
<Col xs={1}>
<p>{"<"}</p>
@ -182,7 +182,7 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
defaultValue={ltCriteria[criteriaKey]}
disabled={props.disabled}
onBlur={e => dispatch(editGtLtCriteriaField(
group, "number_lt", criteriaKey, pointerType)(e))} />
group, "number_lt", criteriaKey)(e))} />
</Col>
</Row>;
};

View File

@ -17,7 +17,7 @@ import {
SubCriteriaSectionProps,
CheckboxListItem,
} from "./interfaces";
import { PLANT_STAGE_LIST } from "../../plants/edit_plant_status";
import { PLANT_STAGE_LIST, WEED_STATUSES } from "../../plants/edit_plant_status";
import { DIRECTION_CHOICES } from "../../tools/tool_slot_edit_components";
import { Checkbox } from "../../../ui";
import { PointType } from "farmbot";
@ -80,7 +80,7 @@ export const CheckboxList =
<div className="criteria-checkbox-list-item" key={index}>
<Checkbox
onChange={() => props.dispatch(toggle<T>(
props.group, props.criteriaKey, value, props.pointerType))}
props.group, props.criteriaKey, value))}
checked={selected(props.criteriaKey, value)}
title={t(label)}
color={color}
@ -95,14 +95,16 @@ export const PlantCriteria = (props: PlantSubCriteriaProps) => {
const { group, dispatch, disabled } = props;
const commonProps = { group, dispatch, disabled };
return <div className={"plant-criteria-options"}>
<PlantStage {...commonProps} />
<PlantStage {...commonProps} pointerType={"Plant"} />
<PlantType {...commonProps} slugs={props.slugs} />
</div>;
};
const PlantStage = (props: SubCriteriaProps) =>
const PlantStage = (props: PointSubCriteriaProps) =>
<div className={"plant-stage-criteria"}>
<p className={"category"}>{t("Stage")}</p>
<p className={"category"}>
{props.pointerType == "Plant" ? t("Stage") : t("Status")}
</p>
<ClearCategory
group={props.group}
criteriaCategories={["string_eq"]}
@ -110,12 +112,15 @@ const PlantStage = (props: SubCriteriaProps) =>
dispatch={props.dispatch} />
<CheckboxList<string>
disabled={props.disabled}
pointerType={"Plant"}
pointerType={props.pointerType}
criteriaKey={"plant_stage"}
group={props.group}
dispatch={props.dispatch}
list={PLANT_STAGE_LIST().map(ddi =>
({ label: ddi.label, value: "" + ddi.value }))} />
list={PLANT_STAGE_LIST().filter(ddi =>
props.pointerType == "Plant" || WEED_STATUSES.includes("" + ddi.value))
.map(ddi => ({ label: ddi.label, value: "" + ddi.value }))
.concat(props.pointerType == "Weed"
? [{ label: t("Remaining"), value: "planned" }] : [])} />
</div>;
const PlantType = (props: PlantSubCriteriaProps) =>
@ -145,6 +150,7 @@ export const WeedCriteria = (props: SubCriteriaProps) => {
const commonProps = { group, dispatch, disabled, pointerType };
return <div className={"weed-criteria-options"}>
<PointSource {...commonProps} />
<PlantStage {...commonProps} />
<Color {...commonProps} />
<Radius {...commonProps} />
</div>;

View File

@ -11,9 +11,11 @@ import {
EditPointColor, EditPointColorProps, updatePoint, EditPointName,
EditPointNameProps,
AdditionalWeedProperties,
EditPointPropertiesProps,
AdditionalWeedPropertiesProps,
} from "../point_edit_actions";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import {
fakePoint, fakeWeed,
} from "../../../__test_support__/fake_state/resources";
import { edit, save } from "../../../api/crud";
describe("updatePoint()", () => {
@ -94,8 +96,8 @@ describe("<EditPointColor />", () => {
});
describe("<AdditionalWeedProperties />", () => {
const fakeProps = (): EditPointPropertiesProps => ({
point: fakePoint(),
const fakeProps = (): AdditionalWeedPropertiesProps => ({
point: fakeWeed(),
updatePoint: jest.fn(),
});

View File

@ -10,6 +10,7 @@ import { Row, Col, BlurableInput, ColorPicker } from "../../ui";
import { parseIntInput } from "../../util";
import { UUID } from "../../resources/interfaces";
import { plantAge } from "../plants/map_state_to_props";
import { EditWeedStatus } from "../plants/edit_plant_status";
type PointUpdate =
Partial<TaggedGenericPointer["body"] | TaggedWeedPointer["body"]>;
@ -29,6 +30,11 @@ export interface EditPointPropertiesProps {
updatePoint(update: PointUpdate): void;
}
export interface AdditionalWeedPropertiesProps {
point: TaggedWeedPointer;
updatePoint(update: PointUpdate): void;
}
export const EditPointProperties = (props: EditPointPropertiesProps) =>
<ul>
<li>
@ -53,11 +59,14 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
</ListItem>
</ul>;
export const AdditionalWeedProperties = (props: EditPointPropertiesProps) =>
export const AdditionalWeedProperties = (props: AdditionalWeedPropertiesProps) =>
<ul className="additional-weed-properties">
<ListItem name={t("Age")}>
{`${plantAge(props.point)} ${t("days old")}`}
</ListItem>
<ListItem name={t("Status")}>
<EditWeedStatus weed={props.point} updateWeed={props.updatePoint} />
</ListItem>
{Object.entries(props.point.body.meta).map(([key, value]) => {
switch (key) {
case "color":

View File

@ -158,7 +158,7 @@ export function renderCeleryNode(props: StepParams) {
case "reboot": return <TileReboot {...props} />;
case "emergency_lock": return <TileEmergencyStop {...props} />;
case "assertion": return <TileAssertion {...props} />;
case "sync": case "dump_info": case "power_off": case "read_status":
case "sync": case "power_off": case "read_status":
case "emergency_unlock": case "install_first_party_farmware":
return <TileSystemAction {...props} />;
case "check_updates": case "factory_reset":

View File

@ -25,7 +25,6 @@ function translate(input: Step): string {
"wait": t("Wait"),
"write_pin": t("Control Peripheral"),
"sync": t("Sync"),
"dump_info": t("Diagnostic Report"),
"power_off": t("Shutdown"),
"read_status": t("Read status"),
"emergency_unlock": t("UNLOCK"),