From 980d39f70df89227ea2ec9a125faa772e3921ed4 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 30 Apr 2020 16:55:14 -0700 Subject: [PATCH] new update_resource ui --- frontend/css/global.scss | 18 ++ frontend/resources/sequence_meta.ts | 2 +- frontend/sequences/step_button_cluster.tsx | 11 +- .../step_tiles/__tests__/index_test.ts | 25 +- .../step_tiles/__tests__/tile_if_test.tsx | 55 ++-- .../__tests__/tile_mark_as_test.tsx | 52 ++++ frontend/sequences/step_tiles/index.tsx | 4 +- frontend/sequences/step_tiles/mark_as.tsx | 67 ----- .../mark_as/__tests__/action_list_test.ts | 65 ----- .../__tests__/commit_selection_test.ts | 25 -- .../mark_as/__tests__/component_test.tsx | 47 ---- .../mark_as/__tests__/pack_step_test.ts | 45 --- .../mark_as/__tests__/resource_list_test.ts | 18 -- .../mark_as/__tests__/unpack_step_test.ts | 144 ---------- .../step_tiles/mark_as/action_list.ts | 45 --- .../step_tiles/mark_as/commit_step_changes.ts | 20 -- .../sequences/step_tiles/mark_as/constants.ts | 45 --- .../step_tiles/mark_as/interfaces.ts | 37 --- .../sequences/step_tiles/mark_as/pack_step.ts | 61 ----- .../step_tiles/mark_as/resource_list.ts | 54 ---- .../step_tiles/mark_as/test_support.ts | 73 ----- .../step_tiles/mark_as/unpack_step.ts | 94 ------- frontend/sequences/step_tiles/tile_if.tsx | 2 +- .../tile_if/__tests__/index_test.tsx | 19 +- .../sequences/step_tiles/tile_mark_as.tsx | 17 ++ .../tile_mark_as/__tests__/component_test.tsx | 195 +++++++++++++ .../__tests__/field_selection_test.tsx | 172 ++++++++++++ .../__tests__/resource_selection_test.tsx | 101 +++++++ .../__tests__/value_selection_test.tsx | 256 ++++++++++++++++++ .../step_tiles/tile_mark_as/component.tsx | 119 ++++++++ .../tile_mark_as/field_selection.tsx | 99 +++++++ .../step_tiles/tile_mark_as/interfaces.ts | 64 +++++ .../tile_mark_as/resource_selection.tsx | 91 +++++++ .../tile_mark_as/value_selection.tsx | 93 +++++++ 34 files changed, 1340 insertions(+), 895 deletions(-) create mode 100644 frontend/sequences/step_tiles/__tests__/tile_mark_as_test.tsx delete mode 100644 frontend/sequences/step_tiles/mark_as.tsx delete mode 100644 frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/__tests__/commit_selection_test.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/__tests__/component_test.tsx delete mode 100644 frontend/sequences/step_tiles/mark_as/__tests__/pack_step_test.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/action_list.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/commit_step_changes.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/constants.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/interfaces.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/pack_step.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/resource_list.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/test_support.ts delete mode 100644 frontend/sequences/step_tiles/mark_as/unpack_step.ts create mode 100644 frontend/sequences/step_tiles/tile_mark_as.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/__tests__/field_selection_test.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/__tests__/resource_selection_test.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/__tests__/value_selection_test.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/component.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/field_selection.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/interfaces.ts create mode 100644 frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 9e1e23ae6..468e830dc 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -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; } diff --git a/frontend/resources/sequence_meta.ts b/frontend/resources/sequence_meta.ts index 75c8c530e..4e464db4a 100644 --- a/frontend/resources/sequence_meta.ts +++ b/frontend/resources/sequence_meta.ts @@ -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; diff --git a/frontend/sequences/step_button_cluster.tsx b/frontend/sequences/step_button_cluster.tsx index b2c21e283..a7c7c12f9 100644 --- a/frontend/sequences/step_button_cluster.tsx +++ b/frontend/sequences/step_button_cluster.tsx @@ -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...")} diff --git a/frontend/sequences/step_tiles/__tests__/index_test.ts b/frontend/sequences/step_tiles/__tests__/index_test.ts index 7201d313c..02589013a 100644 --- a/frontend/sequences/step_tiles/__tests__/index_test.ts +++ b/frontend/sequences/step_tiles/__tests__/index_test.ts @@ -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: { diff --git a/frontend/sequences/step_tiles/__tests__/tile_if_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_if_test.tsx index d747f2d32..b9f0bed90 100644 --- a/frontend/sequences/step_tiles/__tests__/tile_if_test.tsx +++ b/frontend/sequences/step_tiles/__tests__/tile_if_test.tsx @@ -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("", () => { - function bootstrapTest() { +describe("", () => { + const fakeProps = (): StepParams => { const currentStep: If = { kind: "_if", args: { @@ -18,35 +19,27 @@ describe("", () => { } }; return { - component: mount() + 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(); + ["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(); + expect(wrapper.text()).toEqual("Expected `_if` node"); }); }); diff --git a/frontend/sequences/step_tiles/__tests__/tile_mark_as_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_mark_as_test.tsx new file mode 100644 index 000000000..231460e38 --- /dev/null +++ b/frontend/sequences/step_tiles/__tests__/tile_mark_as_test.tsx @@ -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("", () => { + 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(); + ["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(); + expect(wrapper.text()).toEqual("Expected `update_resource` node"); + }); +}); diff --git a/frontend/sequences/step_tiles/index.tsx b/frontend/sequences/step_tiles/index.tsx index 2adb54227..3654ad594 100644 --- a/frontend/sequences/step_tiles/index.tsx +++ b/frontend/sequences/step_tiles/index.tsx @@ -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 ; case "wait": return ; case "write_pin": return ; - case "update_resource": return ; + case "update_resource": return ; case "resource_update" as LegalSequenceKind: return ; case "set_servo_angle": return ; diff --git a/frontend/sequences/step_tiles/mark_as.tsx b/frontend/sequences/step_tiles/mark_as.tsx deleted file mode 100644 index 305ef2cd4..000000000 --- a/frontend/sequences/step_tiles/mark_as.tsx +++ /dev/null @@ -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 { - 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 - - - - - - this.setState({ nextResource })} - allowEmpty={false} - selectedItem={this.state.nextResource || leftSide} /> - - - - - - - - ; - } -} diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts deleted file mode 100644 index df3524d88..000000000 --- a/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/commit_selection_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/commit_selection_test.ts deleted file mode 100644 index 8e79ebbdb..000000000 --- a/frontend/sequences/step_tiles/mark_as/__tests__/commit_selection_test.ts +++ /dev/null @@ -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 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"); - }); -}); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/component_test.tsx b/frontend/sequences/step_tiles/mark_as/__tests__/component_test.tsx deleted file mode 100644 index 52c76f866..000000000 --- a/frontend/sequences/step_tiles/mark_as/__tests__/component_test.tsx +++ /dev/null @@ -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("", () => { - it("renders the basic parts", () => { - const el = mount(); - const text = el.text(); - expect(text).toContain("Tool Mount"); - expect(text).toContain("Not Mounted"); - }); - - it("selects a resource", () => { - const el = shallow(); - 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); - }); -}); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/pack_step_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/pack_step_test.ts deleted file mode 100644 index 1e4e03f8b..000000000 --- a/frontend/sequences/step_tiles/mark_as/__tests__/pack_step_test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts deleted file mode 100644 index 1ba6019bb..000000000 --- a/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts +++ /dev/null @@ -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)"); - }); -}); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts deleted file mode 100644 index 9f95449c4..000000000 --- a/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/frontend/sequences/step_tiles/mark_as/action_list.ts b/frontend/sequences/step_tiles/mark_as/action_list.ts deleted file mode 100644 index f19bf6d43..000000000 --- a/frontend/sequences/step_tiles/mark_as/action_list.ts +++ /dev/null @@ -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 = { - "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); -}; diff --git a/frontend/sequences/step_tiles/mark_as/commit_step_changes.ts b/frontend/sequences/step_tiles/mark_as/commit_step_changes.ts deleted file mode 100644 index 2813cedbe..000000000 --- a/frontend/sequences/step_tiles/mark_as/commit_step_changes.ts +++ /dev/null @@ -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; - } - }); -}; diff --git a/frontend/sequences/step_tiles/mark_as/constants.ts b/frontend/sequences/step_tiles/mark_as/constants.ts deleted file mode 100644 index 4216df74f..000000000 --- a/frontend/sequences/step_tiles/mark_as/constants.ts +++ /dev/null @@ -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 }, -]; diff --git a/frontend/sequences/step_tiles/mark_as/interfaces.ts b/frontend/sequences/step_tiles/mark_as/interfaces.ts deleted file mode 100644 index e8bf92745..000000000 --- a/frontend/sequences/step_tiles/mark_as/interfaces.ts +++ /dev/null @@ -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; -} diff --git a/frontend/sequences/step_tiles/mark_as/pack_step.ts b/frontend/sequences/step_tiles/mark_as/pack_step.ts deleted file mode 100644 index e4121e858..000000000 --- a/frontend/sequences/step_tiles/mark_as/pack_step.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { UpdateResource, Resource, Identifier, resource_type } from "farmbot"; -import { DropDownItem } from "../../../ui"; - -/** - * This is a support function for the 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, - } - }] -}); diff --git a/frontend/sequences/step_tiles/mark_as/resource_list.ts b/frontend/sequences/step_tiles/mark_as/resource_list.ts deleted file mode 100644 index ecfb606b2..000000000 --- a/frontend/sequences/step_tiles/mark_as/resource_list.ts +++ /dev/null @@ -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))]; -}; diff --git a/frontend/sequences/step_tiles/mark_as/test_support.ts b/frontend/sequences/step_tiles/mark_as/test_support.ts deleted file mode 100644 index e618c0e72..000000000 --- a/frontend/sequences/step_tiles/mark_as/test_support.ts +++ /dev/null @@ -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; -} diff --git a/frontend/sequences/step_tiles/mark_as/unpack_step.ts b/frontend/sequences/step_tiles/mark_as/unpack_step.ts deleted file mode 100644 index e0e702082..000000000 --- a/frontend/sequences/step_tiles/mark_as/unpack_step.ts +++ /dev/null @@ -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); - } -} diff --git a/frontend/sequences/step_tiles/tile_if.tsx b/frontend/sequences/step_tiles/tile_if.tsx index 2f856377f..1b25bb895 100644 --- a/frontend/sequences/step_tiles/tile_if.tsx +++ b/frontend/sequences/step_tiles/tile_if.tsx @@ -14,6 +14,6 @@ export function TileIf(props: StepParams) { confirmStepDeletion={props.confirmStepDeletion} showPins={props.showPins} />; } else { - return

Expected "_if" node

; + return

{"Expected `_if` node"}

; } } diff --git a/frontend/sequences/step_tiles/tile_if/__tests__/index_test.tsx b/frontend/sequences/step_tiles/tile_if/__tests__/index_test.tsx index 72a1322df..c0d25b071 100644 --- a/frontend/sequences/step_tiles/tile_if/__tests__/index_test.tsx +++ b/frontend/sequences/step_tiles/tile_if/__tests__/index_test.tsx @@ -76,8 +76,23 @@ describe("LHSOptions()", () => { describe("", () => { it("renders", () => { const wrapper = mount(); - ["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", () => { diff --git a/frontend/sequences/step_tiles/tile_mark_as.tsx b/frontend/sequences/step_tiles/tile_mark_as.tsx new file mode 100644 index 000000000..c950ce22e --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as.tsx @@ -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 ; + } else { + return

{"Expected `update_resource` node"}

; + } +} diff --git a/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx b/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx new file mode 100644 index 000000000..aee9b9c2d --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx @@ -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("", () => { + 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(); + ["Mark", "Tool Mount", "field", "Mounted Tool", "as", "None"].map(string => + expect(wrapper.text()).toContain(string)); + }); + + it("resets step", () => { + const p = fakeProps(); + const wrapper = mount(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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); diff --git a/frontend/sequences/step_tiles/tile_mark_as/__tests__/field_selection_test.tsx b/frontend/sequences/step_tiles/tile_mark_as/__tests__/field_selection_test.tsx new file mode 100644 index 000000000..23225a067 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/__tests__/field_selection_test.tsx @@ -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("", () => { + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + const select = shallow(
{wrapper.find("FBSelect").getElement()}
); + 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(); + 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(); + }); +}); diff --git a/frontend/sequences/step_tiles/tile_mark_as/__tests__/resource_selection_test.tsx b/frontend/sequences/step_tiles/tile_mark_as/__tests__/resource_selection_test.tsx new file mode 100644 index 000000000..7d8d67700 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/__tests__/resource_selection_test.tsx @@ -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("", () => { + 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(); + 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(); + 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(); + 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(); + expect(wrapper.text()).toContain("Mark"); + expect(wrapper.text()).toContain("Variable - Parent"); + }); + + it("changes resource", () => { + const p = fakeProps(); + const wrapper = shallow(); + 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(); + wrapper.find("FBSelect").simulate("change", { + label: "Variable", value: "parent", headingId: "Identifier", + }); + expect(p.updateResource).toHaveBeenCalledWith({ + kind: "identifier", + args: { label: "parent" } + }); + }); +}); diff --git a/frontend/sequences/step_tiles/tile_mark_as/__tests__/value_selection_test.tsx b/frontend/sequences/step_tiles/tile_mark_as/__tests__/value_selection_test.tsx new file mode 100644 index 000000000..9f4051bf8 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/__tests__/value_selection_test.tsx @@ -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("", () => { + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + const select = shallow(
{wrapper.find("FBSelect").getElement()}
); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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"); + }); +}); diff --git a/frontend/sequences/step_tiles/tile_mark_as/component.tsx b/frontend/sequences/step_tiles/tile_mark_as/component.tsx new file mode 100644 index 000000000..0e0357326 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/component.tsx @@ -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 { + 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, 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 + + + + + + + + {this.state.fieldsAndValues.map((fieldAndValue, index) => +
+ + + + + + + + +
)} +
+
; + } +} diff --git a/frontend/sequences/step_tiles/tile_mark_as/field_selection.tsx b/frontend/sequences/step_tiles/tile_mark_as/field_selection.tsx new file mode 100644 index 000000000..6fb201e4f --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/field_selection.tsx @@ -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) => +
+ + {(isCustomMetaField(props.field) && !isUndefined(props.field)) + ? + : } +
; + +const KnownFieldSelection = (props: FieldSelectionProps) => + props.update({ + field: "" + ddi.value, + value: undefined + })} + allowEmpty={false} + selectedItem={getSelectedField( + props.resource, knownField(props.field))} />; + +const CustomMetaField = (props: CustomFieldSelectionProps) => +
+ props.update({ + field: e.currentTarget.value, + value: undefined + })} + allowEmpty={true} + value={props.field} /> + props.update({ field: undefined, value: undefined })} /> +
; + +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" }; + } +}; diff --git a/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts b/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts new file mode 100644 index 000000000..7a109b091 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts @@ -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, 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; +} diff --git a/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx b/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx new file mode 100644 index 000000000..345a3583c --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx @@ -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) => +
+ + props.updateResource(prepareResource(ddi))} + selectedItem={getSelectedResource( + props.resource, props.resources, props.sequenceUuid)} /> +
; + +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")}`; diff --git a/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx b/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx new file mode 100644 index 000000000..19dac0360 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx @@ -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) => +
+ + {isCustomMetaField(props.field) + ? + : } +
; + +const KnownValue = (props: ValueSelectionProps) => + { + 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) => +
+ { + props.update({ value: e.currentTarget.value }, + props.commitSelection); + }} /> +
; + +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 }; + } +};