Merge pull request #1774 from FarmBot/mark_as

New Mark As UI
point_meta_updates^2
Rick Carlino 2020-05-01 14:29:18 -05:00 committed by GitHub
commit 3b1dbe2209
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1340 additions and 895 deletions

View File

@ -1314,6 +1314,24 @@ ul {
}
}
.update-resource-step {
.custom-meta-field {
position: relative;
.fa-undo {
position: absolute;
top: 0.65rem;
right: 0.5rem;
color: $medium_light_gray;
&:hover {
color: $dark_gray;
}
}
}
.update-resource-pair {
margin-top: 1rem;
}
}
.farmware-name-manual-input {
margin-top: 1rem;
}

View File

@ -60,7 +60,7 @@ export const determineVector =
};
/** Try to find a vector in scope declarations for the variable. */
const maybeFindVariable = (
export const maybeFindVariable = (
label: string, resources: ResourceIndex, uuid?: UUID,
): SequenceMeta | undefined =>
uuid ? findVariableByName(resources, uuid, label) : undefined;

View File

@ -187,15 +187,8 @@ export function StepButtonCluster(props: StepButtonProps) {
{...commonStepProps}
step={{
kind: "update_resource",
args: {
resource: {
kind: "resource",
args: { resource_id: 0, resource_type: "Device" }
}
},
body: [
{ kind: "pair", args: { label: "mounted_tool_id", value: 0 } },
],
args: { resource: NOTHING_SELECTED },
body: [],
}}
color="brown">
{t("Mark As...")}

View File

@ -3,12 +3,16 @@ jest.mock("../../../api/crud", () => ({
}));
import { remove, move, splice, renderCeleryNode } from "../index";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import {
fakeSequence, fakePlant,
} from "../../../__test_support__/fake_state/resources";
import { overwrite } from "../../../api/crud";
import { SequenceBodyItem, Wait } from "farmbot";
import { mount } from "enzyme";
import { StepParams, MessageType } from "../../interfaces";
import { emptyState } from "../../../resources/reducer";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
describe("remove()", () => {
const fakeProps = () => ({
@ -82,12 +86,15 @@ describe("splice()", () => {
describe("renderCeleryNode()", () => {
const currentStep: Wait = { kind: "wait", args: { milliseconds: 100 } };
const plant = fakePlant();
plant.body.id = 23;
const fakeProps = (): StepParams => ({
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
resources: buildResourceIndex([plant]).index,
confirmStepDeletion: false,
});
@ -154,13 +161,13 @@ describe("renderCeleryNode()", () => {
resource: {
kind: "resource",
args: { resource_id: 23, resource_type: "Plant" }
},
body: [
{ kind: "pair", args: { label: "plant_stage", value: "planted" } },
]
}
}
},
body: [
{ kind: "pair", args: { label: "plant_stage", value: "planted" } },
]
},
expected: "markplant 23 as"
expected: "markstrawberry plant 1 (100, 200, 0)fieldplant stageasplanted"
},
{
node: {

View File

@ -2,11 +2,12 @@ import * as React from "react";
import { TileIf } from "../tile_if";
import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { If } from "farmbot/dist";
import { If, Wait } from "farmbot/dist";
import { emptyState } from "../../../resources/reducer";
import { StepParams } from "../../interfaces";
describe("<TileIf/>", () => {
function bootstrapTest() {
describe("<TileIf />", () => {
const fakeProps = (): StepParams => {
const currentStep: If = {
kind: "_if",
args: {
@ -18,35 +19,27 @@ describe("<TileIf/>", () => {
}
};
return {
component: mount(<TileIf
currentSequence={fakeSequence()}
currentStep={currentStep}
dispatch={jest.fn()}
index={0}
resources={emptyState().index}
confirmStepDeletion={false}
showPins={true} />)
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
showPins: true,
};
}
};
it("renders inputs", () => {
const block = bootstrapTest().component;
const inputs = block.find("input");
const labels = block.find("label");
const buttons = block.find("button");
expect(inputs.length).toEqual(2);
expect(labels.length).toEqual(5);
expect(buttons.length).toEqual(4);
expect(inputs.first().props().placeholder).toEqual("If ...");
expect(labels.at(0).text()).toEqual("Variable");
expect(buttons.at(0).text()).toEqual("Pin 0");
expect(labels.at(1).text()).toEqual("Operator");
expect(buttons.at(1).text()).toEqual("is");
expect(labels.at(2).text()).toEqual("Value");
expect(inputs.at(1).props().value).toEqual(0);
expect(labels.at(3).text()).toEqual("Then Execute");
expect(buttons.at(2).text()).toEqual("None");
expect(labels.at(4).text()).toEqual("Else Execute");
expect(buttons.at(3).text()).toEqual("None");
it("renders if step", () => {
const wrapper = mount(<TileIf {...fakeProps()} />);
["Variable", "Operator", "Value", "Then Execute", "Else Execute"]
.map(string => expect(wrapper.text()).toContain(string));
});
it("doesn't render if step", () => {
const p = fakeProps();
const waitStep: Wait = { kind: "wait", args: { milliseconds: 0 } };
p.currentStep = waitStep;
const wrapper = mount(<TileIf {...p} />);
expect(wrapper.text()).toEqual("Expected `_if` node");
});
});

View File

@ -0,0 +1,52 @@
import * as React from "react";
import { TileMarkAs } from "../tile_mark_as";
import { mount } from "enzyme";
import {
fakeSequence, fakePlant,
} from "../../../__test_support__/fake_state/resources";
import { UpdateResource, Wait } from "farmbot/dist";
import { StepParams } from "../../interfaces";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
describe("<TileMarkAs />", () => {
const fakeProps = (): StepParams => {
const currentStep: UpdateResource = {
kind: "update_resource",
args: {
resource: {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
},
},
body: [
{ kind: "pair", args: { label: "some_attr", value: "some_value" } },
],
};
const plant = fakePlant();
plant.body.id = 1;
return {
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: buildResourceIndex([plant]).index,
confirmStepDeletion: false,
};
};
it("renders if step", () => {
const wrapper = mount(<TileMarkAs {...fakeProps()} />);
["Mark", "Strawberry plant 1 (100, 200, 0)", "field", "as"]
.map(string => expect(wrapper.text()).toContain(string));
});
it("doesn't render update_resource step", () => {
const p = fakeProps();
const waitStep: Wait = { kind: "wait", args: { milliseconds: 0 } };
p.currentStep = waitStep;
const wrapper = mount(<TileMarkAs {...p} />);
expect(wrapper.text()).toEqual("Expected `update_resource` node");
});
});

View File

@ -18,7 +18,7 @@ import { TileExecuteScript } from "./tile_execute_script";
import { TileTakePhoto } from "./tile_take_photo";
import { overwrite } from "../../api/crud";
import { TileFindHome } from "./tile_find_home";
import { MarkAs } from "./mark_as";
import { TileMarkAs } from "./tile_mark_as";
import { TileUnknown } from "./tile_unknown";
import { forceSetStepTag } from "../../resources/sequence_tagging";
import { compact, assign } from "lodash";
@ -151,7 +151,7 @@ export function renderCeleryNode(props: StepParams) {
case "take_photo": return <TileTakePhoto {...props} />;
case "wait": return <TileWait {...props} />;
case "write_pin": return <TileWritePin {...props} />;
case "update_resource": return <MarkAs {...props} />;
case "update_resource": return <TileMarkAs {...props} />;
case "resource_update" as LegalSequenceKind:
return <TileOldMarkAs {...props} />;
case "set_servo_angle": return <TileSetServoAngle {...props} />;

View File

@ -1,67 +0,0 @@
import { Row, Col, FBSelect, DropDownItem } from "../../ui/index";
import { StepParams } from "../interfaces";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { ToolTips } from "../../constants";
import * as React from "react";
import { unpackStep } from "./mark_as/unpack_step";
import { UpdateResource } from "farmbot";
import { resourceList } from "./mark_as/resource_list";
import { actionList } from "./mark_as/action_list";
import { commitStepChanges } from "./mark_as/commit_step_changes";
import { t } from "../../i18next_wrapper";
interface MarkAsState { nextResource: DropDownItem | undefined }
const NONE = (): DropDownItem => ({ label: t("Select one"), value: 0 });
export class MarkAs extends React.Component<StepParams, MarkAsState> {
state: MarkAsState = { nextResource: undefined };
className = "update-resource-step";
commitSelection = (nextAction: DropDownItem) => {
this.props.dispatch(commitStepChanges({
index: this.props.index,
nextAction,
nextResource: this.state.nextResource,
sequence: this.props.currentSequence,
step: this.props.currentStep as UpdateResource,
}));
this.setState({ nextResource: undefined });
};
render() {
const step = this.props.currentStep as UpdateResource;
const { rightSide, leftSide } =
unpackStep({ step, resourceIndex: this.props.resources });
return <StepWrapper>
<StepHeader
className={this.className}
helpText={ToolTips.MARK_AS}
currentSequence={this.props.currentSequence}
currentStep={this.props.currentStep}
dispatch={this.props.dispatch}
index={this.props.index}
confirmStepDeletion={this.props.confirmStepDeletion} />
<StepContent className={this.className}>
<Row>
<Col xs={6}>
<label>{t("Mark")}</label>
<FBSelect
list={resourceList(this.props.resources)}
onChange={(nextResource) => this.setState({ nextResource })}
allowEmpty={false}
selectedItem={this.state.nextResource || leftSide} />
</Col>
<Col xs={6}>
<label>{t("as")}</label>
<FBSelect
list={actionList(this.state.nextResource?.headingId,
step, this.props.resources)}
onChange={this.commitSelection}
key={JSON.stringify(rightSide) + JSON.stringify(this.state)}
selectedItem={this.state.nextResource ? NONE() : rightSide} />
</Col>
</Row>
</StepContent>
</StepWrapper>;
}
}

View File

@ -1,65 +0,0 @@
import { actionList } from "../action_list";
import { updateResource, markAsResourceFixture } from "../test_support";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import { PLANT_OPTIONS } from "../constants";
describe("actionList()", () => {
it("uses args.resource_type if DropDownItem is undefined", () => {
const step = updateResource({
kind: "resource",
args: { resource_type: "Plant", resource_id: 0 }
});
const { index } = markAsResourceFixture();
const result = actionList(undefined, step, index);
expect(result).toEqual(PLANT_OPTIONS());
});
it("provides a list of tool mount actions", () => {
const ddi = { label: "test case", value: 1, headingId: "Device" };
const step = updateResource();
const { index } = markAsResourceFixture();
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(3);
const labels = result.map(x => x.label);
expect(labels).toContain("Not Mounted");
expect(labels).toContain("Mounted to: T1");
expect(labels).toContain("Mounted to: T2");
});
it("provides a list of generic pointer actions", () => {
const ddi = { label: "test case", value: 1, headingId: "GenericPointer" };
const step = updateResource();
const { index } = markAsResourceFixture();
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(1);
const labels = result.map(x => x.label);
expect(labels).toContain("Removed");
});
it("provides a list of weed pointer actions", () => {
const ddi = { label: "test case", value: 1, headingId: "Weed" };
const step = updateResource();
const { index } = markAsResourceFixture();
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(1);
const labels = result.map(x => x.label);
expect(labels).toContain("Removed");
});
it("returns an empty list for identifiers", () => {
const ddi = { label: "test case", value: 1, headingId: "USB Cables" };
const step = updateResource();
const { index } = buildResourceIndex([]);
const result = actionList(ddi.headingId, step, index);
expect(result.length).toBe(0);
});
it("returns an empty list for all other options", () => {
const step = updateResource({ kind: "identifier", args: { label: "var" } });
const { index } = buildResourceIndex([]);
const result = actionList("Other", step, index);
expect(result.length).toBe(0);
});
});

View File

@ -1,25 +0,0 @@
import { fakeMarkAsProps } from "../test_support";
import { commitStepChanges } from "../commit_step_changes";
import { UpdateResource, TaggedSequence } from "farmbot";
import { Actions } from "../../../../constants";
import { unpackUUID } from "../../../../util";
describe("commitSelection", () => {
it("commits changes in a <MarkAs/> component", () => {
const p = fakeMarkAsProps();
const results = commitStepChanges({
nextAction: { label: "X", value: "some_action" },
nextResource: undefined,
step: p.currentStep as UpdateResource,
index: p.index,
sequence: p.currentSequence
});
expect(results.type).toBe(Actions.OVERWRITE_RESOURCE);
const { payload } = results;
expect(unpackUUID(payload.uuid).kind).toBe("Sequence");
const s = payload.update as TaggedSequence["body"];
expect(s.kind).toBe("sequence");
const step = (s.body || [])[0] as UpdateResource;
expect(step.body?.[0].args.value).toBe("some_action");
});
});

View File

@ -1,47 +0,0 @@
jest.mock("../commit_step_changes", () => {
return {
commitStepChanges: jest.fn()
};
});
import * as React from "react";
import { shallow, mount } from "enzyme";
import { MarkAs } from "../../mark_as";
import { FBSelect } from "../../../../ui";
import { fakeMarkAsProps } from "../test_support";
import { commitStepChanges } from "../commit_step_changes";
describe("<MarkAs/>", () => {
it("renders the basic parts", () => {
const el = mount(<MarkAs {...fakeMarkAsProps()} />);
const text = el.text();
expect(text).toContain("Tool Mount");
expect(text).toContain("Not Mounted");
});
it("selects a resource", () => {
const el = shallow(<MarkAs {...fakeMarkAsProps()} />);
const wow = el.find(FBSelect).first();
expect(wow).toBeTruthy();
const nextResource = {
label: "fake resource",
value: "fake_resource"
};
wow.simulate("change", nextResource);
expect(el.state()).toEqual({ nextResource });
});
it("triggers callbacks (commitSelection)", () => {
const props = fakeMarkAsProps();
const i = new MarkAs(props);
i.setState = jest.fn((s: typeof i.state) => {
i.state = s;
});
const nextResource = { label: "should be cleared", value: 1 };
i.setState({ nextResource });
expect(i.state.nextResource).toEqual(nextResource);
i.commitSelection({ label: "stub", value: "mock" });
expect(i.state.nextResource).toBe(undefined);
expect(commitStepChanges).toHaveBeenCalled();
expect(i.state.nextResource).toEqual(undefined);
});
});

View File

@ -1,45 +0,0 @@
import { updateResource } from "../test_support";
import { packStep } from "../pack_step";
import { TOP_HALF } from "../constants";
import { Resource, Identifier } from "farmbot";
describe("packStep()", () => {
const plant = updateResource({
kind: "resource",
args: { resource_type: "Plant", resource_id: 6 }
});
it("serializes 'plant_stage' actions", () => {
const actionDDI = { value: "harvested", label: "harvested" };
const { args, body } = packStep(plant, undefined, actionDDI);
expect(body?.[0].args.label).toEqual("plant_stage");
expect(body?.[0].args.value).toEqual("harvested");
expect((args.resource as Resource).args.resource_id).toEqual(6);
expect((args.resource as Resource).args.resource_type).toEqual("Plant");
});
it("serializes 'mounted_tool_id' actions", () => {
const resourceDDI = TOP_HALF[0];
const actionDDI = { value: 23, label: "Mounted to can opener" };
const device = updateResource({
kind: "resource",
args: { resource_type: "Device", resource_id: 7 }
});
const { args, body } = packStep(device, resourceDDI, actionDDI);
expect(body?.[0].args.label).toEqual("mounted_tool_id");
expect((args.resource as Resource).args.resource_type).toEqual("Device");
expect((args.resource as Resource).args.resource_id).toEqual(0);
expect(body?.[0].args.value).toEqual(23);
});
it("serializes 'plant_stage' actions: identifier", () => {
const actionDDI = { value: "harvested", label: "harvested" };
const identifier = updateResource({
kind: "identifier", args: { label: "var" }
});
const { args, body } = packStep(identifier, undefined, actionDDI);
expect(body?.[0].args.label).toEqual("plant_stage");
expect(body?.[0].args.value).toEqual("harvested");
expect((args.resource as Identifier).args.label).toEqual("var");
});
});

View File

@ -1,18 +0,0 @@
import { resourceList } from "../resource_list";
import { markAsResourceFixture } from "../test_support";
describe("resourceList()", () => {
it("lists defaults, plus saved points", () => {
const { index } = markAsResourceFixture();
const result = resourceList(index);
expect(result.length).toBeTruthy();
const headings = result.filter(x => x.heading).map(x => x.label);
expect(headings).toContain("Device");
expect(headings).toContain("Plants");
expect(headings).toContain("Points");
expect(headings).toContain("Weeds");
const weeds = result.filter(x => x.headingId == "Weed");
expect(weeds.length).toEqual(2);
expect(weeds[1].label).toEqual("weed 1 (200, 400, 0)");
});
});

View File

@ -1,144 +0,0 @@
import { fakeResourceIndex } from "../../../locals_list/test_helpers";
import { updateResource } from "../test_support";
import { unpackStep, TOOL_MOUNT, DISMOUNTED } from "../unpack_step";
import {
selectAllPlantPointers,
selectAllTools,
selectAllWeedPointers,
} from "../../../../resources/selectors";
import { DropDownPair } from "../interfaces";
import { fakeTool, fakeWeed } from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
describe("unpackStep()", () => {
function assertGoodness(result: DropDownPair,
action_label: string,
action_value: string,
resource_label: string,
resource_value: string | number): void {
expect(result.rightSide.label).toBe(action_label);
expect(result.rightSide.value).toBe(action_value);
expect(result.leftSide.label).toBe(resource_label);
expect(result.leftSide.value).toBe(resource_value);
}
it("unpacks empty tool_ids", () => {
const result = unpackStep({
step: updateResource(undefined, { label: "mounted_tool_id", value: 0 }),
resourceIndex: fakeResourceIndex()
});
expect(result).toEqual(DISMOUNTED());
});
it("unpacks valid tool_ids", () => {
const resourceIndex = fakeResourceIndex();
const { body } = selectAllTools(resourceIndex)[0];
expect(body).toBeTruthy();
const result = unpackStep({
step: updateResource(undefined,
{ label: "mounted_tool_id", value: body.id || NaN }),
resourceIndex
});
const actionLabel = "Mounted to: Generic Tool";
const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value);
});
it("unpacks valid tool_ids with missing names", () => {
const tool = fakeTool();
tool.body.id = 1;
tool.body.name = undefined;
const resourceIndex = buildResourceIndex([tool]).index;
const { body } = selectAllTools(resourceIndex)[0];
expect(body).toBeTruthy();
const result = unpackStep({
step: updateResource(undefined,
{ label: "mounted_tool_id", value: body.id || NaN }),
resourceIndex
});
const actionLabel = "Mounted to: Untitled Tool";
const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value);
});
it("unpacks invalid tool_ids (that may have been valid previously)", () => {
const result = unpackStep({
step: updateResource(undefined,
{ label: "mounted_tool_id", value: Infinity }),
resourceIndex: fakeResourceIndex()
});
const actionLabel = "Mounted to: an unknown tool";
const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value);
});
it("unpacks plant_stage operations: plants", () => {
const resourceIndex = fakeResourceIndex();
const plant = selectAllPlantPointers(resourceIndex)[1];
expect(plant).toBeTruthy();
const result = unpackStep({
step: updateResource({
kind: "resource",
args: { resource_type: "Plant", resource_id: plant.body.id || -1 }
},
{ label: "plant_stage", value: "wilting" }),
resourceIndex
});
const { body } = plant;
const plantName = `${body.name} (${body.x}, ${body.y}, ${body.z})`;
assertGoodness(result, "wilting", "wilting", plantName, body.id || NaN);
});
it("unpacks plant_stage operations: weeds", () => {
const resourceIndex = fakeResourceIndex([fakeWeed()]);
const weed = selectAllWeedPointers(resourceIndex)[1];
expect(weed).toBeTruthy();
const result = unpackStep({
step: updateResource({
kind: "resource",
args: { resource_type: "Weed", resource_id: weed.body.id || -1 }
},
{ label: "plant_stage", value: "removed" }),
resourceIndex
});
const { body } = weed;
const plantName = `${body.name} (${body.x}, ${body.y}, ${body.z})`;
assertGoodness(result, "Removed", "removed", plantName, body.id || NaN);
});
it("unpacks plant_stage operations: identifier", () => {
const resourceIndex = fakeResourceIndex();
const result = unpackStep({
step: updateResource(
{ kind: "identifier", args: { label: "var" } },
{ label: "plant_stage", value: "removed" }),
resourceIndex
});
assertGoodness(result, "Removed", "removed", "var", "var");
});
it("unpacks unknown resource update_resource steps", () => {
const result = unpackStep({
step: updateResource(),
resourceIndex: fakeResourceIndex()
});
assertGoodness(result,
"some_value", "some_value",
"Other 1 some_attr", "some_attr");
});
it("unpacks unknown identifier update_resource steps", () => {
const result = unpackStep({
step: updateResource({ kind: "identifier", args: { label: "var" } }),
resourceIndex: fakeResourceIndex()
});
assertGoodness(result,
"some_value", "some_value",
"variable 0 some_attr", "some_attr");
});
});

View File

@ -1,45 +0,0 @@
import { Dictionary } from "farmbot";
import { DropDownItem } from "../../../ui";
import { ListBuilder } from "./interfaces";
import { ResourceIndex } from "../../../resources/interfaces";
import { UpdateResource } from "farmbot";
import { selectAllTools } from "../../../resources/selectors";
import {
MOUNTED_TO,
DISMOUNT,
PLANT_OPTIONS,
POINT_OPTIONS,
} from "./constants";
const allToolsAsDDI = (i: ResourceIndex) => {
return selectAllTools(i)
.filter(x => !!x.body.id)
.map(x => {
return {
label: `${MOUNTED_TO()} ${x.body.name}`,
value: x.body.id || 0
};
});
};
const DEFAULT = "Default";
const ACTION_LIST: Dictionary<ListBuilder> = {
"Device": (i) => [DISMOUNT(), ...allToolsAsDDI(i)],
"Plant": () => PLANT_OPTIONS(),
"GenericPointer": () => POINT_OPTIONS(),
"Weed": () => POINT_OPTIONS(),
[DEFAULT]: () => []
};
const getList = (t: string): ListBuilder =>
(ACTION_LIST[t] || ACTION_LIST[DEFAULT]);
export const actionList = (d: string | undefined,
r: UpdateResource,
i: ResourceIndex): DropDownItem[] => {
const resourceType = r.args.resource.kind == "identifier"
? DEFAULT
: r.args.resource.args.resource_type;
return getList(d || resourceType)(i);
};

View File

@ -1,20 +0,0 @@
import { UpdateResource } from "farmbot";
import { editStep } from "../../../api/crud";
import { packStep } from "./pack_step";
import { MarkAsEditProps } from "./interfaces";
/** A wrapper for the `editStep()` action creator.
* Isolated from UI for ease of testing. */
export const commitStepChanges = (p: MarkAsEditProps) => {
const { step, nextResource, nextAction, index, sequence } = p;
return editStep({
step,
index,
sequence,
executor(c: UpdateResource) {
const { args, body } = packStep(step, nextResource, nextAction);
c.args = args;
c.body = body;
}
});
};

View File

@ -1,45 +0,0 @@
import { DropDownItem } from "../../../ui";
import { t } from "../../../i18next_wrapper";
import { PLANT_STAGE_LIST } from "../../../farm_designer/plants/edit_plant_status";
export const MOUNTED_TO = () => t("Mounted to:");
export const DISMOUNT = (): DropDownItem =>
({ label: t("Not Mounted"), value: 0 });
/** Legal "actions" for "Mark As.." block when marking Point resources */
export const POINT_OPTIONS = (): DropDownItem[] => [
{ label: t("Removed"), value: "removed" },
];
/** Legal "actions" in the "Mark As.." block when operating on
* a Plant resource. */
export const PLANT_OPTIONS = PLANT_STAGE_LIST;
const value = 0; // Not used in headings.
export const PLANT_HEADER: DropDownItem = {
headingId: "Plant",
label: t("Plants"),
value,
heading: true
};
export const POINT_HEADER: DropDownItem = {
headingId: "GenericPointer",
label: t("Points"),
value,
heading: true
};
export const WEED_HEADER: DropDownItem = {
headingId: "Weed",
label: t("Weeds"),
value,
heading: true
};
export const TOP_HALF = [
{ headingId: "Device", label: t("Device"), value, heading: true },
{ headingId: "Device", label: t("Tool Mount"), value },
];

View File

@ -1,37 +0,0 @@
import { ResourceIndex } from "../../../resources/interfaces";
import { DropDownItem } from "../../../ui";
import { UpdateResource, TaggedSequence, Resource, Identifier } from "farmbot";
/** Function that converts resources into dropdown selections based on
* use-case specific rules */
export type ListBuilder = (i: ResourceIndex) => DropDownItem[];
/** Input data for calls to commitStepChanges() */
export interface MarkAsEditProps {
nextAction: DropDownItem;
nextResource: DropDownItem | undefined;
step: UpdateResource;
index: number;
sequence: TaggedSequence
}
export interface PackedStepWithResourceIndex {
step: UpdateResource;
resourceIndex: ResourceIndex;
}
export interface UnpackedStepWithResourceIndex {
resource: Resource | Identifier;
field: string;
value: string | number | boolean;
resourceIndex: ResourceIndex;
}
/** A pair of DropDownItems used to render the currently selected items in the
* "Mark As.." block. */
export interface DropDownPair {
/** Left side drop down */
leftSide: DropDownItem;
/** Right side drop down */
rightSide: DropDownItem;
}

View File

@ -1,61 +0,0 @@
import { UpdateResource, Resource, Identifier, resource_type } from "farmbot";
import { DropDownItem } from "../../../ui";
/**
* This is a support function for the <MarkAs/> component.
*
* SCENARIO: You are editing a `Mark As..` sequence step. The user has unsaved
* local changes as well as a copy of older data from the API.
*
* PROBLEM: You need to take the component's local state plus the
* shape of the "update_resource" ("Mark As..") block and merge them
* together so that you can render the form in the editor.
*
* SOLUTION: Use the celery node + pieces of the component's state (resourceDDI,
* actionDDI) to properly populate dropdown menus and determine the
* shape of the new "update_resource" step when it is saved.
* */
export const packStep = (
csNode: UpdateResource,
resourceDDI: DropDownItem | undefined,
actionDDI: DropDownItem,
): UpdateResource => {
const resource = resourceDDI?.headingId
? resourceNode(resourceDDI.headingId, resourceDDI.value)
: csNode.args.resource;
if (resource.kind == "identifier") {
return updateResource(resource, "plant_stage", actionDDI.value);
} else {
switch (resource.args.resource_type) {
case "Device":
/* Scenario I: Changing tool mount */
return updateResource(resource, "mounted_tool_id", actionDDI.value);
default:
/* Scenario II: Changing a point */
return updateResource(resource, "plant_stage", actionDDI.value);
}
}
};
const resourceNode = (type: string, id: string | number): Resource => ({
kind: "resource",
args: {
resource_type: type as resource_type,
resource_id: parseInt("" + id),
}
});
const updateResource = (
resource: Resource | Identifier,
field: string,
value: string | number,
): UpdateResource => ({
kind: "update_resource",
args: { resource },
body: [{
kind: "pair", args: {
label: field,
value: value,
}
}]
});

View File

@ -1,54 +0,0 @@
import { ResourceIndex } from "../../../resources/interfaces";
import { DropDownItem } from "../../../ui/fb_select";
import { selectAllPoints } from "../../../resources/selectors";
import { TaggedPoint } from "farmbot";
import { Point } from "farmbot/dist/resources/api_resources";
import { POINT_HEADER, PLANT_HEADER, TOP_HALF, WEED_HEADER } from "./constants";
/** Filter function to remove resources we don't care about,
* such as ToolSlots and unsaved (Plant|Point)'s */
const isRelevant = (x: TaggedPoint) => {
const saved = !!x.body.id;
const notToolSlot = x.body.pointer_type !== "ToolSlot";
return saved && notToolSlot;
};
/** Format DropDownItem["label"] as "Name (1, 2, 3)" */
const labelStr =
(n: string, x: number, y: number, z: number) => `${n} (${x}, ${y}, ${z})`;
/** Convert a Point to a DropDownItem that is formatted appropriately
* for the "Mark As.." step. */
export const point2ddi = (i: Point): DropDownItem => {
const { x, y, z, name, id, pointer_type } = i;
return {
value: id || 0,
label: labelStr(name, x, y, z),
headingId: pointer_type,
};
};
/** GIVEN: mixed list of *SAVED* point types (ToolSlot, Plant, Pointer)
* RETURNS: list of DropDownItems with proper headers and `headerId`s */
const pointList =
(input: TaggedPoint[]): DropDownItem[] => {
const genericPoints: DropDownItem[] = [POINT_HEADER];
const weeds: DropDownItem[] = [WEED_HEADER];
const plants: DropDownItem[] = [PLANT_HEADER];
input
.map(x => x.body)
.forEach(body => {
switch (body.pointer_type) {
case "GenericPointer": return genericPoints.push(point2ddi(body));
case "Weed": return weeds.push(point2ddi(body));
case "Plant": return plants.push(point2ddi(body));
}
});
return [...plants, ...genericPoints, ...weeds];
};
/** Creates a formatted DropDownItem list for the "Resource" (left hand) side of
* the "Mark As" step. */
export const resourceList = (r: ResourceIndex): DropDownItem[] => {
return [...TOP_HALF, ...pointList(selectAllPoints(r).filter(isRelevant))];
};

View File

@ -1,73 +0,0 @@
import {
UpdateResource, TaggedSequence, resource_type, Pair, Resource, Identifier,
} from "farmbot";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import {
fakeTool,
fakePlant,
fakePoint,
fakeSequence,
fakeWeed,
} from "../../../__test_support__/fake_state/resources";
import { betterMerge } from "../../../util";
import { MarkAs } from "../mark_as";
export function updateResource(
resource?: Resource | Identifier, pairArgs?: Pair["args"]): UpdateResource {
return {
kind: "update_resource",
args: {
resource: resource || {
kind: "resource", args: {
resource_type: "Other" as resource_type,
resource_id: 1,
}
},
},
body: [{
kind: "pair", args: {
label: "some_attr",
value: "some_value",
...pairArgs,
}
}],
};
}
export const markAsResourceFixture = () => buildResourceIndex([
betterMerge(fakeTool(), { body: { name: "T1", id: 1 } }),
fakePlant(),
betterMerge(fakeTool(), { body: { name: "T2", id: 2 } }),
betterMerge(fakePoint(), { body: { name: "my point", id: 7 } }),
betterMerge(fakeWeed(), { body: { name: "weed 1", id: 8 } }),
betterMerge(fakeTool(), { body: { name: "T3", id: undefined } }),
]);
export function fakeMarkAsProps() {
const steps: TaggedSequence["body"]["body"] = [
{
kind: "update_resource",
args: {
resource: {
kind: "resource",
args: { resource_id: 0, resource_type: "Device" }
}
},
body: [{ kind: "pair", args: { label: "mounted_tool_id", value: 0 } }],
},
];
const currentSequence: TaggedSequence =
betterMerge(fakeSequence(), { body: { body: steps } });
const props: MarkAs["props"] = {
currentSequence,
dispatch: jest.fn(),
index: 0,
currentStep: steps[0],
resources: buildResourceIndex([currentSequence]).index,
confirmStepDeletion: false
};
return props;
}

View File

@ -1,94 +0,0 @@
import { DropDownItem } from "../../../ui";
import {
findToolById,
findPointerByTypeAndId,
} from "../../../resources/selectors";
import { point2ddi } from "./resource_list";
import { MOUNTED_TO } from "./constants";
import {
DropDownPair, PackedStepWithResourceIndex, UnpackedStepWithResourceIndex,
} from "./interfaces";
import { t } from "../../../i18next_wrapper";
import {
PLANT_STAGE_DDI_LOOKUP,
} from "../../../farm_designer/plants/edit_plant_status";
export const TOOL_MOUNT = (): DropDownItem => ({
label: t("Tool Mount"), value: "tool_mount"
});
const NOT_IN_USE = (): DropDownItem => ({ label: t("Not Mounted"), value: 0 });
export const DISMOUNTED = (): DropDownPair => ({
leftSide: TOOL_MOUNT(),
rightSide: NOT_IN_USE()
});
const DEFAULT_TOOL_NAME = () => t("Untitled Tool");
const mountedTo = (toolName = DEFAULT_TOOL_NAME()): DropDownItem =>
({ label: `${MOUNTED_TO()} ${toolName}`, value: "mounted" });
/** The user wants to change the `mounted_tool_id` of their Device. */
function mountTool(i: UnpackedStepWithResourceIndex): DropDownPair {
const { value } = i;
if (typeof value === "number" && value > 0) {
try { // Good tool id
const tool = findToolById(i.resourceIndex, value as number);
return { leftSide: TOOL_MOUNT(), rightSide: mountedTo(tool.body.name) };
} catch { // Bad tool ID or app still loading.
return { leftSide: TOOL_MOUNT(), rightSide: mountedTo("an unknown tool") };
}
} else {
// No tool id
return DISMOUNTED();
}
}
/** When we can't properly guess the correct way to to render the screen,
* possibly for legacy reasons or because the user wrote their CeleryScript by
* hand. */
function unknownOption(i: UnpackedStepWithResourceIndex): DropDownPair {
const { resource } = i;
const resource_type =
resource.kind == "resource" ? resource.args.resource_type : "variable";
const resource_id =
resource.kind == "resource" ? resource.args.resource_id : 0;
const { field, value } = i;
const leftLabel = `${resource_type} ${resource_id} ${field}`;
return {
leftSide: { label: leftLabel, value: field },
rightSide: { label: "" + value, value: "" + value }
};
}
/** The user wants to mark a the `plant_stage` attribute of a Plant resource. */
function plantStage(i: UnpackedStepWithResourceIndex): DropDownPair {
const { resource } = i;
const resource_type =
resource.kind == "resource" ? resource.args.resource_type : "";
const resource_id =
resource.kind == "resource" ? resource.args.resource_id : 0;
const { value } = i;
const leftSide = resource.kind == "resource"
? point2ddi(findPointerByTypeAndId(
i.resourceIndex, resource_type, resource_id).body)
: { label: "" + resource.args.label, value: "" + resource.args.label };
return {
leftSide,
rightSide: PLANT_STAGE_DDI_LOOKUP()["" + value]
|| { label: "" + value, value: "" + value },
};
}
/** We can guess how the "Mark As.." UI will be rendered (left and right side
* drop downs) based on the shape of the current step. There are several
* strategies and this function will dispatch the appropriate one. */
export function unpackStep(p: PackedStepWithResourceIndex): DropDownPair {
const { resource } = p.step.args;
const { label, value } = p.step.body?.[0]?.args || { label: "", value: "" };
const field = label;
const unpacked = { resourceIndex: p.resourceIndex, resource, field, value };
switch (field) {
case "mounted_tool_id": return mountTool(unpacked);
case "plant_stage": return plantStage(unpacked);
default: return unknownOption(unpacked);
}
}

View File

@ -14,6 +14,6 @@ export function TileIf(props: StepParams) {
confirmStepDeletion={props.confirmStepDeletion}
showPins={props.showPins} />;
} else {
return <p> Expected "_if" node</p>;
return <p>{"Expected `_if` node"}</p>;
}
}

View File

@ -76,8 +76,23 @@ describe("LHSOptions()", () => {
describe("<InnerIf />", () => {
it("renders", () => {
const wrapper = mount(<InnerIf {...fakeProps()} />);
["Variable", "Operator", "Value", "Then Execute", "Else Execute"].map(string =>
expect(wrapper.text()).toContain(string));
const inputs = wrapper.find("input");
const labels = wrapper.find("label");
const buttons = wrapper.find("button");
expect(inputs.length).toEqual(2);
expect(labels.length).toEqual(5);
expect(buttons.length).toEqual(4);
expect(inputs.first().props().placeholder).toEqual("If ...");
expect(labels.at(0).text()).toEqual("Variable");
expect(buttons.at(0).text()).toEqual("Pin 0");
expect(labels.at(1).text()).toEqual("Operator");
expect(buttons.at(1).text()).toEqual("is");
expect(labels.at(2).text()).toEqual("Value");
expect(inputs.at(1).props().value).toEqual(0);
expect(labels.at(3).text()).toEqual("Then Execute");
expect(buttons.at(2).text()).toEqual("None");
expect(labels.at(4).text()).toEqual("Else Execute");
expect(buttons.at(3).text()).toEqual("None");
});
it("is recursive", () => {

View File

@ -0,0 +1,17 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { MarkAs } from "./tile_mark_as/component";
export function TileMarkAs(props: StepParams) {
if (props.currentStep.kind === "update_resource") {
return <MarkAs
currentSequence={props.currentSequence}
currentStep={props.currentStep}
dispatch={props.dispatch}
index={props.index}
resources={props.resources}
confirmStepDeletion={props.confirmStepDeletion} />;
} else {
return <p>{"Expected `update_resource` node"}</p>;
}
}

View File

@ -0,0 +1,195 @@
const mockEditStep = jest.fn();
jest.mock("../../../../api/crud", () => ({ editStep: mockEditStep }));
import * as React from "react";
import { mount } from "enzyme";
import { MarkAs } from "../component";
import { MarkAsProps, UpdateResourceValue } from "../interfaces";
import { UpdateResource, Identifier, Resource, resource_type } from "farmbot";
import {
fakeSequence, fakePlant, fakeWeed,
} from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import { editStep } from "../../../../api/crud";
import { NOTHING_SELECTED } from "../../../locals_list/handle_select";
describe("<MarkAs/>", () => {
const plant = fakePlant();
plant.body.id = 1;
const weed = fakeWeed();
weed.body.id = 2;
const fakeProps = (): MarkAsProps => ({
currentSequence: fakeSequence(),
dispatch: jest.fn(),
index: 0,
currentStep: ResourceUpdateResourceStep("Device", 1, "mounted_tool_id", 0),
resources: buildResourceIndex([plant, weed]).index,
confirmStepDeletion: false
});
it("renders the basic parts", () => {
const wrapper = mount(<MarkAs {...fakeProps()} />);
["Mark", "Tool Mount", "field", "Mounted Tool", "as", "None"].map(string =>
expect(wrapper.text()).toContain(string));
});
it("resets step", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.instance().resetStep();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual({
kind: "update_resource",
args: { resource: NOTHING_SELECTED },
body: [],
});
});
it("edits step", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
},
fieldsAndValues: [{ field: "plant_stage", value: "planted" }],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual(
ResourceUpdateResourceStep("Plant", 1, "plant_stage", "planted"));
});
it("doesn't edit step", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: { kind: "nothing", args: {} },
fieldsAndValues: [{ field: "plant_stage", value: "planted" }],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual(
ResourceUpdateResourceStep("Device", 1, "mounted_tool_id", 0));
});
it("doesn't save partial pairs", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
},
fieldsAndValues: [
{ field: "plant_stage", value: "planted" },
{ field: "x", value: 1 },
{ field: "y", value: undefined },
],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
const expectedStep =
ResourceUpdateResourceStep("Plant", 1, "plant_stage", "planted");
expectedStep.body && expectedStep.body.push({
kind: "pair", args: { label: "x", value: 1 }
});
expect(p.currentStep).toEqual(expectedStep);
});
it("edits step to use identifier", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
wrapper.setState({
resource: { kind: "identifier", args: { label: "var" } },
fieldsAndValues: [{ field: "plant_stage", value: "planted" }],
});
wrapper.instance().commitSelection();
expect(editStep).toHaveBeenCalled();
mockEditStep.mock.calls[0][0].executor(p.currentStep);
expect(p.currentStep).toEqual(
IdentifierUpdateResourceStep("var", "plant_stage", "planted"));
});
it("updates resource", () => {
const p = fakeProps();
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
expect(wrapper.state().resource).toEqual(p.currentStep.args.resource);
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: "mounted_tool_id", value: 0 }]);
const newResource: Resource =
({ kind: "resource", args: { resource_type: "Weed", resource_id: 2 } });
wrapper.instance().updateResource(newResource);
expect(wrapper.state().resource).toEqual(newResource);
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: undefined, value: undefined }]);
});
it("updates field", () => {
const p = fakeProps();
p.currentStep.body = undefined;
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: undefined, value: undefined }]);
wrapper.instance().updateFieldOrValue(0)({ field: "plant_stage" });
expect(wrapper.state().fieldsAndValues)
.toEqual([{ field: "plant_stage", value: undefined }]);
expect(p.dispatch).toHaveBeenCalled();
});
it("updates value", () => {
const p = fakeProps();
p.currentStep.body && p.currentStep.body.push({
kind: "pair", args: { label: "plant_stage", value: "planned" }
});
const wrapper = mount<MarkAs>(<MarkAs {...p} />);
expect(wrapper.state().fieldsAndValues).toEqual([
{ field: "mounted_tool_id", value: 0 },
{ field: "plant_stage", value: "planned" },
]);
const callback = jest.fn();
wrapper.instance().updateFieldOrValue(1)({ value: "planted" }, callback);
expect(wrapper.state().fieldsAndValues).toEqual([
{ field: "mounted_tool_id", value: 0 },
{ field: "plant_stage", value: "planted" },
]);
expect(callback).toHaveBeenCalled();
expect(p.dispatch).not.toHaveBeenCalled();
});
});
const BaseUpdateResourceStep =
(resource: Resource | Identifier,
field: string,
value: UpdateResourceValue,
): UpdateResource => ({
kind: "update_resource",
args: { resource },
body: [{ kind: "pair", args: { label: field, value } }],
});
const ResourceUpdateResourceStep = (
resourceType: resource_type,
resourceId: number,
field: string,
value: UpdateResourceValue,
): UpdateResource =>
BaseUpdateResourceStep({
kind: "resource",
args: { resource_id: resourceId, resource_type: resourceType }
}, field, value);
const IdentifierUpdateResourceStep = (
label: string,
field: string,
value: UpdateResourceValue,
): UpdateResource =>
BaseUpdateResourceStep({ kind: "identifier", args: { label } }, field, value);

View File

@ -0,0 +1,172 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { FieldSelection, isCustomMetaField } from "../field_selection";
import { FieldSelectionProps } from "../interfaces";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
describe("<FieldSelection />", () => {
const fakeProps = (): FieldSelectionProps => ({
resource: { kind: "nothing", args: {} },
field: undefined,
resources: buildResourceIndex().index,
update: jest.fn(),
});
it("renders disabled none field", () => {
const p = fakeProps();
p.field = undefined;
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Select one");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders none field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
p.field = undefined;
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Plant stage", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Select one");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders custom meta field", () => {
const p = fakeProps();
p.field = "custom";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(0);
expect(wrapper.text()).toContain("field");
expect(wrapper.find("input").props().value).toEqual("custom");
expect(wrapper.find(".reset-custom-field").length).toEqual(1);
});
it("changes custom meta field", () => {
const p = fakeProps();
p.field = "custom_field";
const wrapper = mount(<FieldSelection {...p} />);
const input = shallow(wrapper.find("input").getElement());
input.simulate("change", { currentTarget: { value: "1" } });
input.simulate("blur", { currentTarget: { value: "1" } });
expect(p.update).toHaveBeenCalledWith({ field: "1" });
});
it("clears custom meta field", () => {
const p = fakeProps();
p.field = "custom_field";
const wrapper = mount(<FieldSelection {...p} />);
wrapper.find(".reset-custom-field").simulate("click");
expect(p.update).toHaveBeenCalledWith({
field: undefined, value: undefined
});
});
it("renders field list for identifier", () => {
const p = fakeProps();
p.resource = { kind: "identifier", args: { label: "var" } };
p.field = "plant_stage";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Status", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders known weed field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = "plant_stage";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Weed status", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Weed status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("renders known point field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "GenericPointer", resource_id: 3 }
};
p.field = "plant_stage";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Status", value: "plant_stage" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
it("changes known weed field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = undefined;
const wrapper = mount(<FieldSelection {...p} />);
const select = shallow(<div>{wrapper.find("FBSelect").getElement()}</div>);
select.find("FBSelect").simulate("change", {
label: "", value: "plant_stage"
});
expect(p.update).toHaveBeenCalledWith({ field: "plant_stage" });
});
it("renders known device field", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Device", resource_id: 1 }
};
p.field = "mounted_tool_id";
const wrapper = mount(<FieldSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Mounted Tool", value: "mounted_tool_id" },
{ label: "Custom Meta Field", value: "" },
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Mounted Tool");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
});
describe("isCustomMetaField()", () => {
it("is custom meta field", () => {
expect(isCustomMetaField("")).toBeTruthy();
expect(isCustomMetaField("custom")).toBeTruthy();
});
it("is not custom meta field", () => {
expect(isCustomMetaField(undefined)).toBeFalsy();
expect(isCustomMetaField("plant_stage")).toBeFalsy();
expect(isCustomMetaField("mounted_tool_id")).toBeFalsy();
});
});

View File

@ -0,0 +1,101 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { ResourceSelection } from "../resource_selection";
import { ResourceSelectionProps } from "../interfaces";
import {
buildResourceIndex, fakeDevice,
} from "../../../../__test_support__/resource_index_builder";
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
describe("<ResourceSelection />", () => {
const plant = fakePlant();
plant.body.id = 1;
const fakeProps = (): ResourceSelectionProps => ({
resource: { kind: "nothing", args: {} },
resources: buildResourceIndex([plant]).index,
updateResource: jest.fn(),
sequenceUuid: "fake Sequence UUID",
});
it("renders", () => {
const p = fakeProps();
const device = fakeDevice();
device.body.id = 1;
p.resources = buildResourceIndex([device]).index;
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Select one");
});
it("renders resource", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Strawberry plant 1 (100, 200, 0)");
});
it("renders identifier", () => {
const p = fakeProps();
p.resource = {
kind: "identifier",
args: { label: "var" }
};
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Variable - Add new");
});
it("renders identifier with label", () => {
const p = fakeProps();
p.resources.sequenceMetas["fake uuid"] = {
parent: {
celeryNode: {
kind: "parameter_declaration", args: {
label: "parent", default_value: {
kind: "coordinate", args: { x: 0, y: 0, z: 0 }
}
}
},
dropdown: { label: "Parent", value: "parent" },
vector: undefined,
}
};
p.sequenceUuid = "fake uuid";
p.resource = {
kind: "identifier",
args: { label: "parent" }
};
const wrapper = mount(<ResourceSelection {...p} />);
expect(wrapper.text()).toContain("Mark");
expect(wrapper.text()).toContain("Variable - Parent");
});
it("changes resource", () => {
const p = fakeProps();
const wrapper = shallow(<ResourceSelection {...p} />);
wrapper.find("FBSelect").simulate("change", {
label: "", value: "1", headingId: "Plant",
});
expect(p.updateResource).toHaveBeenCalledWith({
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
});
});
it("changes resource to identifier", () => {
const p = fakeProps();
const wrapper = shallow(<ResourceSelection {...p} />);
wrapper.find("FBSelect").simulate("change", {
label: "Variable", value: "parent", headingId: "Identifier",
});
expect(p.updateResource).toHaveBeenCalledWith({
kind: "identifier",
args: { label: "parent" }
});
});
});

View File

@ -0,0 +1,256 @@
let mockDev = false;
jest.mock("../../../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { ValueSelection } from "../value_selection";
import { ValueSelectionProps } from "../interfaces";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import {
PLANT_STAGE_LIST,
} from "../../../../farm_designer/plants/edit_plant_status";
import { fakeTool } from "../../../../__test_support__/fake_state/resources";
import { resource_type, Resource } from "farmbot";
describe("<ValueSelection />", () => {
const fakeProps = (): ValueSelectionProps => ({
resource: { kind: "nothing", args: {} },
field: undefined,
value: undefined,
resources: buildResourceIndex().index,
update: jest.fn(),
add: jest.fn(),
commitSelection: jest.fn(),
});
it("renders none value", () => {
const p = fakeProps();
p.field = undefined;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Select one");
});
it("renders custom meta value", () => {
const p = fakeProps();
p.field = "custom_field";
p.value = "custom_value";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(0);
expect(wrapper.text()).toContain("as");
expect(wrapper.find("input").props().value).toEqual("custom_value");
});
it("renders missing custom meta value", () => {
const p = fakeProps();
p.field = "custom_field";
p.value = undefined;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(0);
expect(wrapper.text()).toContain("as");
expect(wrapper.find("input").props().value).toEqual("");
});
it("changes custom meta value", () => {
const p = fakeProps();
p.field = "custom_field";
p.value = "custom_value";
const wrapper = mount(<ValueSelection {...p} />);
const input = shallow(wrapper.find("input").getElement());
input.simulate("change", { currentTarget: { value: "1" } });
input.simulate("blur", { currentTarget: { value: "1" } });
expect(p.update).toHaveBeenCalledWith({ value: "1" },
expect.any(Function));
});
it("adds row", () => {
const p = fakeProps();
const wrapper = mount(<ValueSelection {...p} />);
wrapper.find("label").simulate("click");
expect(p.add).not.toHaveBeenCalled();
mockDev = true;
wrapper.find("label").simulate("click");
expect(p.add).toHaveBeenCalledWith({});
});
it("renders known plant value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "planted";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Planted");
});
it("renders plant value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Plant", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "other";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("other");
});
it("renders known weed value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "removed";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Removed", value: "removed" },
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
});
it("changes known weed value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Weed", resource_id: 1 }
};
p.field = "plant_stage";
p.value = undefined;
const wrapper = mount(<ValueSelection {...p} />);
const select = shallow(<div>{wrapper.find("FBSelect").getElement()}</div>);
select.find("FBSelect").simulate("change", {
label: "", value: "removed"
});
expect(p.update).toHaveBeenCalledWith({ value: "removed" },
expect.any(Function));
});
it("renders known point value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "GenericPointer", resource_id: 1 }
};
p.field = "plant_stage";
p.value = "removed";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Removed", value: "removed" },
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
});
it("renders other value", () => {
const p = fakeProps();
p.resource = {
kind: "resource",
args: { resource_type: "Other" as resource_type, resource_id: 1 }
};
p.field = "plant_stage";
p.value = "removed";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
});
const TOOL_OPTIONS = [
{ label: "None", value: 0 },
{ label: "Trench Digging Tool", value: 14 },
{ label: "Berry Picking Tool", value: 15 },
];
const DeviceResource: Resource = {
kind: "resource",
args: { resource_type: "Device", resource_id: 1 }
};
it("renders known tool value: not mounted", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 0;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(TOOL_OPTIONS);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("None");
});
it("renders known tool value: mounted", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 14;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(TOOL_OPTIONS);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Trench Digging Tool");
});
it("renders known tool value: unknown tool", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 123;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(TOOL_OPTIONS);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Unknown tool");
});
it("renders known tool value: untitled tool", () => {
const p = fakeProps();
p.resource = DeviceResource;
p.field = "mounted_tool_id";
p.value = 1;
const tool = fakeTool();
tool.body.id = 1;
tool.body.name = undefined;
p.resources = buildResourceIndex([tool]).index;
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "None", value: 0 },
{ label: "Untitled tool", value: 1 },
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Untitled tool");
});
it("renders known identifier value", () => {
const p = fakeProps();
p.resource = {
kind: "identifier", args: { label: "var" }
};
p.field = "plant_stage";
p.value = "planted";
const wrapper = mount(<ValueSelection {...p} />);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual(PLANT_STAGE_LIST());
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Planted");
});
});

View File

@ -0,0 +1,119 @@
import * as React from "react";
import { editStep } from "../../../api/crud";
import { Row, Col } from "../../../ui";
import { StepWrapper, StepHeader, StepContent } from "../../step_ui/index";
import { ToolTips } from "../../../constants";
import { UpdateResource, Resource, Identifier } from "farmbot";
import { MarkAsState, MarkAsProps, FieldAndValue } from "./interfaces";
import { ResourceSelection } from "./resource_selection";
import { FieldSelection } from "./field_selection";
import { ValueSelection } from "./value_selection";
import { isUndefined } from "lodash";
import { NOTHING_SELECTED } from "../../locals_list/handle_select";
export class MarkAs extends React.Component<MarkAsProps, MarkAsState> {
state: MarkAsState = {
resource: this.step.args.resource,
fieldsAndValues: this.step.body?.length
? this.step.body.map(pair =>
({ field: pair.args.label, value: pair.args.value }))
: [{ field: undefined, value: undefined }],
};
get step() { return this.props.currentStep; }
editStep = (executor: (s: UpdateResource) => void) =>
this.props.dispatch(editStep({
step: this.step,
index: this.props.index,
sequence: this.props.currentSequence,
executor,
}));
resetStep = () =>
this.editStep(s => {
s.args = { resource: NOTHING_SELECTED };
s.body = [];
});
commitSelection = () => {
const { resource, fieldsAndValues } = this.state;
this.editStep(s => {
if (fieldsAndValues.length > 0 && resource.kind != "nothing") {
s.args = { resource };
s.body = [];
fieldsAndValues.map(({ field, value }) => {
if (s.body && !isUndefined(field) && !isUndefined(value)) {
s.body.push({ kind: "pair", args: { label: field, value: value } });
}
});
}
});
};
updateResource = (resource: Resource | Identifier) => {
this.setState({
resource,
fieldsAndValues: [{ field: undefined, value: undefined }],
});
this.resetStep();
};
updateFieldOrValue = (index: number) =>
(update: Partial<FieldAndValue>, callback?: () => void) => {
const { fieldsAndValues } = this.state;
const old = fieldsAndValues[index];
fieldsAndValues[index] = { ...old, ...update };
this.setState({ fieldsAndValues: fieldsAndValues }, callback);
if (isUndefined(update.value) && fieldsAndValues.length < 2) {
this.resetStep();
}
};
render() {
const commonProps = {
key: JSON.stringify(this.state)
+ JSON.stringify(this.props.currentSequence.body.args.locals),
resource: this.state.resource,
resources: this.props.resources,
};
const className = "update-resource-step";
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.MARK_AS}
currentSequence={this.props.currentSequence}
currentStep={this.props.currentStep}
dispatch={this.props.dispatch}
index={this.props.index}
confirmStepDeletion={this.props.confirmStepDeletion} />
<StepContent className={className}>
<Row>
<Col xs={12}>
<ResourceSelection {...commonProps}
sequenceUuid={this.props.currentSequence.uuid}
updateResource={this.updateResource} />
</Col>
</Row>
{this.state.fieldsAndValues.map((fieldAndValue, index) =>
<div className={"update-resource-pair"} key={index}>
<Row>
<Col xs={6}>
<FieldSelection {...commonProps}
field={fieldAndValue.field}
update={this.updateFieldOrValue(index)} />
</Col>
<Col xs={6}>
<ValueSelection {...commonProps}
field={fieldAndValue.field}
value={fieldAndValue.value}
update={this.updateFieldOrValue(index)}
add={this.updateFieldOrValue(this.state.fieldsAndValues.length)}
commitSelection={this.commitSelection} />
</Col>
</Row>
</div>)}
</StepContent>
</StepWrapper>;
}
}

View File

@ -0,0 +1,99 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
import { FBSelect, DropDownItem, BlurableInput } from "../../../ui";
import { Resource, Identifier, Nothing } from "farmbot";
import { isUndefined } from "lodash";
import { FieldSelectionProps, CustomFieldSelectionProps } from "./interfaces";
export const FieldSelection = (props: FieldSelectionProps) =>
<div className={"update-resource-step-field"}>
<label>{t("field")}</label>
{(isCustomMetaField(props.field) && !isUndefined(props.field))
? <CustomMetaField {...props} field={props.field} />
: <KnownFieldSelection {...props} />}
</div>;
const KnownFieldSelection = (props: FieldSelectionProps) =>
<FBSelect
extraClass={props.resource.kind == "nothing" ? "disabled" : ""}
list={props.resource.kind == "nothing"
? []
: fieldList(props.resource)
.concat([{ label: t("Custom Meta Field"), value: "" }])}
onChange={ddi => props.update({
field: "" + ddi.value,
value: undefined
})}
allowEmpty={false}
selectedItem={getSelectedField(
props.resource, knownField(props.field))} />;
const CustomMetaField = (props: CustomFieldSelectionProps) =>
<div className="custom-meta-field">
<BlurableInput type="text" name="field"
onCommit={e => props.update({
field: e.currentTarget.value,
value: undefined
})}
allowEmpty={true}
value={props.field} />
<i className={"reset-custom-field fa fa-undo"}
title={t("reset")}
onClick={() => props.update({ field: undefined, value: undefined })} />
</div>;
export enum KnownField {
plant_stage = "plant_stage",
mounted_tool_id = "mounted_tool_id",
}
const isKnownField = (x: string | undefined): x is KnownField =>
!!(x && Object.keys(KnownField).includes(x));
export const knownField =
(field: string | undefined): KnownField | undefined =>
isKnownField(field) ? field : undefined;
export const isCustomMetaField = (field: string | undefined): boolean =>
!(isUndefined(field) || knownField(field));
const fieldList = (resource: Resource | Identifier) => {
if (resource.kind == "identifier") {
return [{ label: t("Status"), value: "plant_stage" }];
}
switch (resource.args.resource_type) {
case "Device":
return [{ label: t("Mounted Tool"), value: "mounted_tool_id" }];
case "Weed":
return [{ label: t("Weed status"), value: "plant_stage" }];
case "GenericPointer":
return [{ label: t("Status"), value: "plant_stage" }];
default:
return [{ label: t("Plant stage"), value: "plant_stage" }];
}
};
const getSelectedField = (
resource: Resource | Identifier | Nothing,
field: KnownField | undefined,
): DropDownItem => {
if (isUndefined(field) || resource.kind == "nothing") {
return { label: t("Select one"), value: "" };
}
if (resource.kind == "identifier") {
return { label: t("Status"), value: "plant_stage" };
}
const resourceType = resource.args.resource_type;
switch (field) {
case KnownField.mounted_tool_id:
return { label: t("Mounted Tool"), value: "tool" };
case KnownField.plant_stage:
if (resourceType == "Weed") {
return { label: t("Weed status"), value: "plant_stage" };
}
if (resourceType == "GenericPointer") {
return { label: t("Status"), value: "plant_stage" };
}
return { label: t("Plant stage"), value: "plant_stage" };
}
};

View File

@ -0,0 +1,64 @@
import { ResourceIndex, UUID } from "../../../resources/interfaces";
import {
UpdateResource, TaggedSequence, Resource, Identifier, Nothing, Pair,
} from "farmbot";
import { KnownField } from "./field_selection";
export interface MarkAsProps {
currentSequence: TaggedSequence;
currentStep: UpdateResource;
dispatch: Function;
index: number;
resources: ResourceIndex;
confirmStepDeletion: boolean;
}
export type UpdateResourceValue = Pair["args"]["value"];
export interface FieldAndValue {
field: string | undefined;
value: UpdateResourceValue | undefined;
}
export interface MarkAsState {
resource: Resource | Identifier | Nothing;
fieldsAndValues: FieldAndValue[];
}
export interface GetSelectedValueProps {
resource: Resource | Identifier | Nothing;
field: KnownField | undefined;
value: UpdateResourceValue | undefined;
resourceIndex: ResourceIndex;
}
interface SelectionPropsBase {
resource: Resource | Identifier | Nothing;
resources: ResourceIndex;
}
export interface ResourceSelectionProps extends SelectionPropsBase {
updateResource(resource: Resource | Identifier): void;
sequenceUuid: UUID;
}
type UpdateFieldOrValue =
(update: Partial<FieldAndValue>, callback?: () => void) => void;
export interface FieldSelectionProps extends SelectionPropsBase {
field: string | undefined;
update: UpdateFieldOrValue;
}
export interface CustomFieldSelectionProps extends SelectionPropsBase {
field: string;
update: UpdateFieldOrValue;
}
export interface ValueSelectionProps extends SelectionPropsBase {
field: string | undefined;
value: UpdateResourceValue | undefined;
update: UpdateFieldOrValue;
add: UpdateFieldOrValue;
commitSelection(): void;
}

View File

@ -0,0 +1,91 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
import { FBSelect } from "../../../ui";
import {
resource_type as RESOURCE_TYPE, Identifier, Resource, Nothing,
} from "farmbot";
import { ResourceSelectionProps } from "./interfaces";
import { ResourceIndex, UUID } from "../../../resources/interfaces";
import { DropDownItem } from "../../../ui/fb_select";
import {
selectAllPoints, maybeGetDevice, findPointerByTypeAndId,
} from "../../../resources/selectors";
import { formatPoint } from "../../locals_list/location_form_list";
import {
maybeFindVariable, SequenceMeta,
} from "../../../resources/sequence_meta";
export const ResourceSelection = (props: ResourceSelectionProps) =>
<div className={"update-resource-step-resource"}>
<label>{t("Mark")}</label>
<FBSelect
list={resourceList(props.resources, props.sequenceUuid)}
onChange={ddi => props.updateResource(prepareResource(ddi))}
selectedItem={getSelectedResource(
props.resource, props.resources, props.sequenceUuid)} />
</div>;
const prepareResource = (ddi: DropDownItem): Resource | Identifier => {
switch (ddi.headingId) {
case "Identifier":
return { kind: "identifier", args: { label: "" + ddi.value } };
default:
return {
kind: "resource",
args: {
resource_type: ddi.headingId as RESOURCE_TYPE,
resource_id: parseInt("" + ddi.value)
}
};
}
};
const resourceList =
(resources: ResourceIndex, sequenceUuid: UUID): DropDownItem[] => {
const deviceId = maybeGetDevice(resources)?.body.id || 0;
const points = selectAllPoints(resources).filter(p => !!p.body.id);
const mapPoints = points.filter(p => p.body.pointer_type == "GenericPointer");
const weeds = points.filter(p => p.body.pointer_type == "Weed");
const plants = points.filter(p => p.body.pointer_type == "Plant");
const headingCommon = { heading: true, value: 0 };
const varLabel = resourceVariableLabel(maybeFindVariable(
"parent", resources, sequenceUuid));
return [
{ headingId: "Identifier", label: varLabel, value: "parent" },
{ headingId: "Device", label: t("Device"), ...headingCommon },
{ headingId: "Device", label: t("Tool Mount"), value: deviceId },
{ headingId: "Plant", label: t("Plants"), ...headingCommon },
...plants.map(formatPoint),
{ headingId: "GenericPointer", label: t("Points"), ...headingCommon },
...mapPoints.map(formatPoint),
{ headingId: "Weed", label: t("Weeds"), ...headingCommon },
...weeds.map(formatPoint),
];
};
const getSelectedResource = (
resource: Resource | Identifier | Nothing,
resources: ResourceIndex,
sequenceUuid: UUID,
): DropDownItem => {
switch (resource.kind) {
case "resource":
const { resource_type, resource_id } = resource.args;
if (resource_type == "Device") {
return { label: t("Tool Mount"), value: resource_id };
}
return formatPoint(
findPointerByTypeAndId(resources, resource_type, resource_id));
case "identifier":
const variable =
maybeFindVariable(resource.args.label, resources, sequenceUuid);
return {
label: resourceVariableLabel(variable),
value: resource.args.label,
};
case "nothing": return { label: t("Select one"), value: "" };
}
};
const resourceVariableLabel = (variable: SequenceMeta | undefined) =>
`${t("Variable")} - ${variable?.dropdown.label || t("Add new")}`;

View File

@ -0,0 +1,93 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
import { FBSelect, BlurableInput } from "../../../ui";
import { isUndefined } from "lodash";
import { ValueSelectionProps, GetSelectedValueProps } from "./interfaces";
import { Identifier, Resource } from "farmbot";
import { DropDownItem } from "../../../ui";
import { ResourceIndex } from "../../../resources/interfaces";
import { selectAllTools, maybeFindToolById } from "../../../resources/selectors";
import {
PLANT_STAGE_LIST, PLANT_STAGE_DDI_LOOKUP,
} from "../../../farm_designer/plants/edit_plant_status";
import { isCustomMetaField, KnownField, knownField } from "./field_selection";
import { DevSettings } from "../../../account/dev/dev_support";
export const ValueSelection = (props: ValueSelectionProps) =>
<div className={"update-resource-step-value"}>
<label onClick={() => DevSettings.futureFeaturesEnabled() && props.add({})}>
{t("as")}
</label>
{isCustomMetaField(props.field)
? <CustomMetaValue {...props} />
: <KnownValue {...props} />}
</div>;
const KnownValue = (props: ValueSelectionProps) =>
<FBSelect
extraClass={isUndefined(props.field) ? "disabled" : ""}
list={props.resource.kind == "nothing"
? []
: valuesList(props.resource, props.resources)}
onChange={ddi => {
props.update({ value: ddi.value },
props.commitSelection);
}}
selectedItem={getSelectedValue({
resourceIndex: props.resources,
resource: props.resource,
field: knownField(props.field),
value: props.value,
})} />;
const CustomMetaValue = (props: ValueSelectionProps) =>
<div className="custom-meta-field">
<BlurableInput type="text" name="value"
value={isUndefined(props.value) ? "" : "" + props.value}
onCommit={e => {
props.update({ value: e.currentTarget.value },
props.commitSelection);
}} />
</div>;
const valuesList = (
resource: Resource | Identifier,
resources: ResourceIndex): DropDownItem[] => {
const stepResourceType =
resource.kind == "identifier" ? undefined : resource.args.resource_type;
switch (stepResourceType) {
case "Device": return [
{ label: t("None"), value: 0 },
...selectAllTools(resources).filter(x => !!x.body.id)
.map(x => ({ toolName: x.body.name, toolId: x.body.id }))
.map(({ toolName, toolId }:
{ toolName: string | undefined, toolId: number }) =>
({ label: toolName || t("Untitled tool"), value: toolId })),
];
case "GenericPointer": return [{ label: t("Removed"), value: "removed" }];
case "Weed": return [{ label: t("Removed"), value: "removed" }];
case "Plant":
default: return PLANT_STAGE_LIST();
}
};
const getSelectedValue = (props: GetSelectedValueProps): DropDownItem => {
if (isUndefined(props.field) || isUndefined(props.value)
|| props.resource.kind == "nothing") {
return { label: t("Select one"), value: "" };
}
switch (props.field) {
case KnownField.mounted_tool_id:
const toolId = parseInt("" + props.value);
if (toolId == 0) { return { label: t("None"), value: 0 }; }
const tool = maybeFindToolById(props.resourceIndex, toolId);
if (!tool) { return { label: t("Unknown tool"), value: toolId }; }
return {
label: tool.body.name || t("Untitled tool"),
value: toolId
};
case KnownField.plant_stage:
return PLANT_STAGE_DDI_LOOKUP()["" + props.value]
|| { label: "" + props.value, value: "" + props.value };
}
};