commit
0bd6d9a967
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")}
|
||||
<a className="blinking" href="/app/device?highlight=farmbot_os">
|
||||
<u>{t("upgrade FarmBot OS")}</u>
|
||||
</a>
|
||||
{t("before troubleshooting.")}
|
||||
</p>
|
||||
<p>
|
||||
{diagnose(props)}
|
||||
</p>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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" });
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
})} />;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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"] };
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -161,7 +161,6 @@ describe("<NumberLtGtInput />", () => {
|
|||
p.group,
|
||||
"number_gt",
|
||||
"x",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -175,7 +174,6 @@ describe("<NumberLtGtInput />", () => {
|
|||
p.group,
|
||||
"number_lt",
|
||||
"x",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in New Issue