diff --git a/frontend/constants.ts b/frontend/constants.ts index e778b7e00..49bc3ffeb 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -346,7 +346,7 @@ export namespace ToolTips { trim(`The Mark As step allows FarmBot to programmatically edit the properties of the UTM, plants, and weeds from within a sequence. For example, you can mark a plant as "planted" during a seeding - sequence or delete a weed after removing it.`); + sequence or mark a weed as "removed" after removing it.`); export const REBOOT = trim(`Power cycle FarmBot's onboard computer.`); diff --git a/frontend/css/steps.scss b/frontend/css/steps.scss index 1d91cd6eb..d8dc65365 100644 --- a/frontend/css/steps.scss +++ b/frontend/css/steps.scss @@ -106,7 +106,7 @@ &.take-photo-step { background: $brown; } - &.resource-update-step { + &.update-resource-step { background: $brown; } &.set-servo-angle-step { @@ -226,7 +226,7 @@ &.take-photo-step a { color: $dark_brown; } - &.resource-update-step { + &.update-resource-step { background: $light_brown; } &.set-servo-angle-step { diff --git a/frontend/devices/interfaces.ts b/frontend/devices/interfaces.ts index b644b80a9..55320c32d 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -81,6 +81,7 @@ export enum Feature { ota_update_hour = "ota_update_hour", rpi_led_control = "rpi_led_control", sensors = "sensors", + update_resource = "update_resource", use_update_channel = "use_update_channel", variables = "variables", } diff --git a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx index 1e16408d6..df6162da9 100644 --- a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx +++ b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx @@ -1,3 +1,5 @@ +jest.mock("../../history", () => ({ push: jest.fn() })); + jest.mock("../../api/crud", () => ({ destroy: jest.fn(), save: jest.fn(), @@ -55,6 +57,7 @@ import { DropAreaProps } from "../../draggable/interfaces"; import { Actions } from "../../constants"; import { setWebAppConfigValue } from "../../config_storage/actions"; import { BooleanSetting } from "../../session_keys"; +import { push } from "../../history"; describe("", () => { const fakeProps = (): ActiveMiddleProps => { @@ -73,10 +76,13 @@ describe("", () => { }; }; - it("saves", () => { - const wrapper = mount(); - clickButton(wrapper, 0, "Save * "); + it("saves", async () => { + const p = fakeProps(); + p.dispatch = () => Promise.resolve(); + const wrapper = mount(); + await clickButton(wrapper, 0, "Save * "); expect(save).toHaveBeenCalledWith(expect.stringContaining("Sequence")); + expect(push).toHaveBeenCalledWith("/app/sequences/fake"); }); it("tests", () => { diff --git a/frontend/sequences/locals_list/handle_select.ts b/frontend/sequences/locals_list/handle_select.ts index 1f03ac1a6..1a6023eb4 100644 --- a/frontend/sequences/locals_list/handle_select.ts +++ b/frontend/sequences/locals_list/handle_select.ts @@ -66,32 +66,45 @@ interface NewVarProps { newVarLabel?: string; } -const nothingVar = - ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue => - createVariableNode(allowedVariableNodes)(label, NOTHING_SELECTED); +const nothingVar = ({ + identifierLabel: label, allowedVariableNodes +}: NewVarProps): VariableWithAValue => + createVariableNode(allowedVariableNodes)(label, NOTHING_SELECTED); -const toolVar = (value: string | number) => - ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue => - createVariableNode(allowedVariableNodes)(label, { - kind: "tool", - args: { tool_id: parseInt("" + value) } - }); +const toolVar = (value: string | number) => ({ + identifierLabel: label, allowedVariableNodes +}: NewVarProps): VariableWithAValue => + createVariableNode(allowedVariableNodes)(label, { + kind: "tool", + args: { tool_id: parseInt("" + value) } + }); const pointVar = ( pointer_type: "Plant" | "GenericPointer" | "Weed", value: string | number, -) => ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue => +) => ({ + identifierLabel: label, allowedVariableNodes +}: NewVarProps): VariableWithAValue => createVariableNode(allowedVariableNodes)(label, { kind: "point", args: { pointer_type, pointer_id: parseInt("" + value) } }); -const manualEntry = (value: string | number) => - ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue => - createVariableNode(allowedVariableNodes)(label, { - kind: "coordinate", - args: value ? JSON.parse("" + value) : { x: 0, y: 0, z: 0 } - }); +const groupVar = (value: string | number) => ({ + identifierLabel: label, allowedVariableNodes +}: NewVarProps): VariableWithAValue => + createVariableNode(allowedVariableNodes)(label, { + kind: "point_group", + args: { point_group_id: parseInt("" + value) } + }); + +const manualEntry = (value: string | number) => ({ + identifierLabel: label, allowedVariableNodes +}: NewVarProps): VariableWithAValue => + createVariableNode(allowedVariableNodes)(label, { + kind: "coordinate", + args: value ? JSON.parse("" + value) : { x: 0, y: 0, z: 0 } + }); /** * Create a parameter declaration or a parameter application containing an @@ -129,15 +142,7 @@ const createNewVariable = (props: NewVarProps): VariableNode | undefined => { case "Tool": return toolVar(ddi.value)(props); case "parameter": return newParameter(props); case "Coordinate": return manualEntry(ddi.value)(props); - case "PointGroup": - const point_group_id = parseInt("" + ddi.value, 10); - return { - kind: "parameter_application", - args: { - label: props.identifierLabel, - data_value: { kind: "point_group", args: { point_group_id } } - } - }; + case "PointGroup": return groupVar(ddi.value)(props); } console.error("WARNING: Don't know how to handle " + (ddi.headingId || "NA")); return undefined; diff --git a/frontend/sequences/sequence_editor_middle_active.tsx b/frontend/sequences/sequence_editor_middle_active.tsx index 66695a79f..479b7656b 100644 --- a/frontend/sequences/sequence_editor_middle_active.tsx +++ b/frontend/sequences/sequence_editor_middle_active.tsx @@ -12,7 +12,7 @@ import { save, edit, destroy } from "../api/crud"; import { TestButton } from "./test_button"; import { AllSteps } from "./all_steps"; import { LocalsList, localListCallback } from "./locals_list/locals_list"; -import { betterCompact } from "../util"; +import { betterCompact, urlFriendly } from "../util"; import { AllowedVariableNodes } from "./locals_list/locals_list_support"; import { ResourceIndex } from "../resources/interfaces"; import { ShouldDisplay } from "../devices/interfaces"; @@ -135,7 +135,8 @@ const SequenceBtnGroup = ({ }: SequenceBtnGroupProps) =>
dispatch(save(sequence.uuid))} /> + onClick={() => dispatch(save(sequence.uuid)).then(() => + push(`/app/sequences/${urlFriendly(sequence.body.name)}`))} /> ); - shouldDisplay(Feature.mark_as_step) && ALL_THE_BUTTONS.push( {t("Mark As...")} diff --git a/frontend/sequences/step_tiles/__tests__/index_test.ts b/frontend/sequences/step_tiles/__tests__/index_test.ts index 4515b18e5..7201d313c 100644 --- a/frontend/sequences/step_tiles/__tests__/index_test.ts +++ b/frontend/sequences/step_tiles/__tests__/index_test.ts @@ -147,17 +147,32 @@ describe("renderCeleryNode()", () => { node: { kind: "wait", args: { milliseconds: 100 } }, expected: "milliseconds" }, + { + node: { + kind: "update_resource", + args: { + resource: { + kind: "resource", + args: { resource_id: 23, resource_type: "Plant" } + }, + body: [ + { kind: "pair", args: { label: "plant_stage", value: "planted" } }, + ] + } + }, + expected: "markplant 23 as" + }, { node: { kind: "resource_update", args: { resource_id: 23, resource_type: "Plant", - label: "x", - value: 300 + label: "plant_stage", + value: "planted", } }, - expected: "MarkPlantasx = 300" + expected: "mark plant 23 plant_stage as plantedthis step has been deprecated." }, { node: { kind: "set_servo_angle", args: { pin_number: 4, pin_value: 90 } }, diff --git a/frontend/sequences/step_tiles/__tests__/tile_old_mark_as_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_old_mark_as_test.tsx new file mode 100644 index 000000000..91737bf58 --- /dev/null +++ b/frontend/sequences/step_tiles/__tests__/tile_old_mark_as_test.tsx @@ -0,0 +1,106 @@ +const mockEditStep = jest.fn(); +jest.mock("../../../api/crud", () => ({ + editStep: mockEditStep +})); + +import * as React from "react"; +import { mount } from "enzyme"; +import { TileOldMarkAs } from "../tile_old_mark_as"; +import { fakeSequence } from "../../../__test_support__/fake_state/resources"; +import { emptyState } from "../../../resources/reducer"; +import { StepParams } from "../../interfaces"; +import { editStep } from "../../../api/crud"; +import { SequenceBodyItem } from "farmbot"; +import { cloneDeep } from "lodash"; + +describe("", () => { + const currentStep = { + kind: "resource_update", + args: { + resource_type: "Device", + resource_id: 0, + label: "mounted_tool_id", + value: 0, + } + } as unknown as SequenceBodyItem; + + const fakeProps = (): StepParams => ({ + currentSequence: fakeSequence(), + currentStep: currentStep, + dispatch: jest.fn(), + index: 0, + resources: emptyState().index, + confirmStepDeletion: false, + }); + + it("renders deprecation notice", () => { + const block = mount(); + expect(block.text()).toContain("deprecated"); + expect(block.text()).not.toContain("convert"); + }); + + it("renders deprecation notice and convert button", () => { + const p = fakeProps(); + p.shouldDisplay = () => true; + const block = mount(); + expect(block.text()).toContain("deprecated"); + expect(block.text()).toContain("convert"); + }); + + it("converts set mounted tool step", () => { + const p = fakeProps(); + p.shouldDisplay = () => true; + const block = mount(); + expect(block.text()).toContain("deprecated"); + expect(block.text()).toContain("convert"); + block.find("button").last().simulate("click"); + expect(editStep).toHaveBeenCalled(); + const step = cloneDeep(p.currentStep); + mockEditStep.mock.calls[0][0].executor(step); + expect(step).toEqual({ + kind: "update_resource", + args: { + resource: { + kind: "resource", + args: { resource_type: "Device", resource_id: 0 } + } + }, + body: [{ + kind: "pair", args: { label: "mounted_tool_id", value: 0 } + }], + }); + }); + + it("converts remove weed step", () => { + const p = fakeProps(); + p.currentStep = { + kind: "resource_update", + args: { + resource_type: "Weed", + resource_id: 123, + label: "discarded_at", + value: "?", + } + } as unknown as SequenceBodyItem; + p.shouldDisplay = () => true; + const block = mount(); + expect(block.text()).toContain("deprecated"); + expect(block.text()).toContain("convert"); + block.find("button").last().simulate("click"); + expect(editStep).toHaveBeenCalled(); + const step = cloneDeep(p.currentStep); + mockEditStep.mock.calls[0][0].executor(step); + expect(step).toEqual({ + kind: "update_resource", + args: { + resource: { + kind: "resource", + args: { resource_type: "Weed", resource_id: 123 } + } + }, + body: [{ + kind: "pair", args: { label: "plant_stage", value: "removed" } + }], + }); + }); +}); diff --git a/frontend/sequences/step_tiles/index.tsx b/frontend/sequences/step_tiles/index.tsx index fc68fea1f..09e0442cf 100644 --- a/frontend/sequences/step_tiles/index.tsx +++ b/frontend/sequences/step_tiles/index.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { CeleryNode, LegalArgString, If, Execute, Nothing, - SequenceBodyItem as Step, TaggedSequence, + SequenceBodyItem as Step, TaggedSequence, LegalSequenceKind, } from "farmbot"; import { FLOAT_NUMERIC_FIELDS, NUMERIC_FIELDS } from "../interfaces"; import { ExecuteBlock } from "./tile_execute"; @@ -33,6 +33,7 @@ import { t } from "../../i18next_wrapper"; import { TileAssertion } from "./tile_assertion"; import { TileEmergencyStop } from "./tile_emergency_stop"; import { TileReboot } from "./tile_reboot"; +import { TileOldMarkAs } from "./tile_old_mark_as"; interface MoveParams { step: Step; @@ -149,7 +150,9 @@ export function renderCeleryNode(props: StepParams) { case "take_photo": return ; case "wait": return ; case "write_pin": return ; - case "resource_update": return ; + case "update_resource": return ; + case "resource_update" as LegalSequenceKind: + return ; case "set_servo_angle": return ; case "toggle_pin": return ; case "zero": return ; diff --git a/frontend/sequences/step_tiles/mark_as.tsx b/frontend/sequences/step_tiles/mark_as.tsx index 598efeb07..305ef2cd4 100644 --- a/frontend/sequences/step_tiles/mark_as.tsx +++ b/frontend/sequences/step_tiles/mark_as.tsx @@ -4,7 +4,7 @@ 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 { ResourceUpdate } from "farmbot"; +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"; @@ -15,7 +15,7 @@ const NONE = (): DropDownItem => ({ label: t("Select one"), value: 0 }); export class MarkAs extends React.Component { state: MarkAsState = { nextResource: undefined }; - className = "resource-update-step"; + className = "update-resource-step"; commitSelection = (nextAction: DropDownItem) => { this.props.dispatch(commitStepChanges({ @@ -23,13 +23,13 @@ export class MarkAs extends React.Component { nextAction, nextResource: this.state.nextResource, sequence: this.props.currentSequence, - step: this.props.currentStep as ResourceUpdate, + step: this.props.currentStep as UpdateResource, })); this.setState({ nextResource: undefined }); }; render() { - const step = this.props.currentStep as ResourceUpdate; + const step = this.props.currentStep as UpdateResource; const { rightSide, leftSide } = unpackStep({ step, resourceIndex: this.props.resources }); return @@ -54,7 +54,8 @@ export class MarkAs extends React.Component { 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 index 04d6d33c8..df3524d88 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts @@ -1,5 +1,5 @@ import { actionList } from "../action_list"; -import { resourceUpdate, markAsResourceFixture } from "../assertion_support"; +import { updateResource, markAsResourceFixture } from "../test_support"; import { buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; @@ -7,7 +7,10 @@ import { PLANT_OPTIONS } from "../constants"; describe("actionList()", () => { it("uses args.resource_type if DropDownItem is undefined", () => { - const step = resourceUpdate({ resource_type: "Plant" }); + 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()); @@ -15,9 +18,9 @@ describe("actionList()", () => { it("provides a list of tool mount actions", () => { const ddi = { label: "test case", value: 1, headingId: "Device" }; - const step = resourceUpdate({}); + const step = updateResource(); const { index } = markAsResourceFixture(); - const result = actionList(ddi, step, index); + const result = actionList(ddi.headingId, step, index); expect(result.length).toBe(3); const labels = result.map(x => x.label); expect(labels).toContain("Not Mounted"); @@ -27,9 +30,9 @@ describe("actionList()", () => { it("provides a list of generic pointer actions", () => { const ddi = { label: "test case", value: 1, headingId: "GenericPointer" }; - const step = resourceUpdate({}); + const step = updateResource(); const { index } = markAsResourceFixture(); - const result = actionList(ddi, step, index); + const result = actionList(ddi.headingId, step, index); expect(result.length).toBe(1); const labels = result.map(x => x.label); expect(labels).toContain("Removed"); @@ -37,19 +40,26 @@ describe("actionList()", () => { it("provides a list of weed pointer actions", () => { const ddi = { label: "test case", value: 1, headingId: "Weed" }; - const step = resourceUpdate({}); + const step = updateResource(); const { index } = markAsResourceFixture(); - const result = actionList(ddi, step, index); + 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 all other options", () => { + it("returns an empty list for identifiers", () => { const ddi = { label: "test case", value: 1, headingId: "USB Cables" }; - const step = resourceUpdate({}); + const step = updateResource(); const { index } = buildResourceIndex([]); - const result = actionList(ddi, step, index); + 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 index 6b537f3bf..8e79ebbdb 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/commit_selection_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/commit_selection_test.ts @@ -1,6 +1,6 @@ -import { fakeMarkAsProps } from "../assertion_support"; +import { fakeMarkAsProps } from "../test_support"; import { commitStepChanges } from "../commit_step_changes"; -import { ResourceUpdate, TaggedSequence } from "farmbot"; +import { UpdateResource, TaggedSequence } from "farmbot"; import { Actions } from "../../../../constants"; import { unpackUUID } from "../../../../util"; @@ -10,7 +10,7 @@ describe("commitSelection", () => { const results = commitStepChanges({ nextAction: { label: "X", value: "some_action" }, nextResource: undefined, - step: p.currentStep as ResourceUpdate, + step: p.currentStep as UpdateResource, index: p.index, sequence: p.currentSequence }); @@ -19,7 +19,7 @@ describe("commitSelection", () => { 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 ResourceUpdate; - expect(step.args.value).toBe("some_action"); + 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 index c3d645d10..52c76f866 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/component_test.tsx +++ b/frontend/sequences/step_tiles/mark_as/__tests__/component_test.tsx @@ -7,7 +7,7 @@ import * as React from "react"; import { shallow, mount } from "enzyme"; import { MarkAs } from "../../mark_as"; import { FBSelect } from "../../../../ui"; -import { fakeMarkAsProps } from "../assertion_support"; +import { fakeMarkAsProps } from "../test_support"; import { commitStepChanges } from "../commit_step_changes"; describe("", () => { 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 index 9b425cc60..1e4e03f8b 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/pack_step_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/pack_step_test.ts @@ -1,36 +1,45 @@ -import { resourceUpdate } from "../assertion_support"; +import { updateResource } from "../test_support"; import { packStep } from "../pack_step"; import { TOP_HALF } from "../constants"; +import { Resource, Identifier } from "farmbot"; describe("packStep()", () => { - const plant = resourceUpdate({ resource_type: "Plant", resource_id: 6 }); - - it("serializes 'discard' actions", () => { - const actionDDI = { value: "removed", label: "Removed" }; - const { args } = packStep(plant, undefined, actionDDI); - expect(args.label).toEqual("discarded_at"); - expect(args.value).toEqual("{{ Time.now }}"); - expect(args.resource_id).toEqual(6); - expect(args.resource_type).toEqual("Plant"); + 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 } = packStep(plant, undefined, actionDDI); - expect(args.label).toEqual("plant_stage"); - expect(args.value).toEqual("harvested"); - expect(args.resource_id).toEqual(6); - expect(args.resource_type).toEqual("Plant"); + 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 = resourceUpdate({ resource_type: "Device", resource_id: 7 }); - const { args } = packStep(device, resourceDDI, actionDDI); - expect(args.label).toEqual("mounted_tool_id"); - expect(args.resource_type).toEqual("Device"); - expect(args.resource_id).toEqual(0); - expect(args.value).toEqual(23); + 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 index 41e882a62..1ba6019bb 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts @@ -1,5 +1,5 @@ import { resourceList } from "../resource_list"; -import { markAsResourceFixture } from "../assertion_support"; +import { markAsResourceFixture } from "../test_support"; describe("resourceList()", () => { it("lists defaults, plus saved points", () => { 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 index cc2375c36..9f95449c4 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts @@ -1,13 +1,13 @@ import { fakeResourceIndex } from "../../../locals_list/test_helpers"; -import { resourceUpdate } from "../assertion_support"; +import { updateResource } from "../test_support"; import { unpackStep, TOOL_MOUNT, DISMOUNTED } from "../unpack_step"; import { selectAllPlantPointers, selectAllTools, - selectAllGenericPointers, + selectAllWeedPointers, } from "../../../../resources/selectors"; import { DropDownPair } from "../interfaces"; -import { fakeTool } from "../../../../__test_support__/fake_state/resources"; +import { fakeTool, fakeWeed } from "../../../../__test_support__/fake_state/resources"; import { buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; @@ -25,7 +25,7 @@ describe("unpackStep()", () => { it("unpacks empty tool_ids", () => { const result = unpackStep({ - step: resourceUpdate({ label: "mounted_tool_id", value: 0 }), + step: updateResource(undefined, { label: "mounted_tool_id", value: 0 }), resourceIndex: fakeResourceIndex() }); expect(result).toEqual(DISMOUNTED()); @@ -37,7 +37,8 @@ describe("unpackStep()", () => { expect(body).toBeTruthy(); const result = unpackStep({ - step: resourceUpdate({ label: "mounted_tool_id", value: body.id || NaN }), + step: updateResource(undefined, + { label: "mounted_tool_id", value: body.id || NaN }), resourceIndex }); const actionLabel = "Mounted to: Generic Tool"; @@ -54,7 +55,8 @@ describe("unpackStep()", () => { expect(body).toBeTruthy(); const result = unpackStep({ - step: resourceUpdate({ label: "mounted_tool_id", value: body.id || NaN }), + step: updateResource(undefined, + { label: "mounted_tool_id", value: body.id || NaN }), resourceIndex }); const actionLabel = "Mounted to: Untitled Tool"; @@ -64,7 +66,8 @@ describe("unpackStep()", () => { it("unpacks invalid tool_ids (that may have been valid previously)", () => { const result = unpackStep({ - step: resourceUpdate({ label: "mounted_tool_id", value: Infinity }), + step: updateResource(undefined, + { label: "mounted_tool_id", value: Infinity }), resourceIndex: fakeResourceIndex() }); const actionLabel = "Mounted to: an unknown tool"; @@ -72,49 +75,70 @@ describe("unpackStep()", () => { assertGoodness(result, actionLabel, "mounted", label, value); }); - it("unpacks discarded_at operations", () => { - const resourceIndex = fakeResourceIndex(); - const { body } = selectAllGenericPointers(resourceIndex)[0]; - expect(body.pointer_type).toBe("GenericPointer"); - - const result = unpackStep({ - step: resourceUpdate({ - resource_type: "GenericPointer", - resource_id: body.id || -1, - label: "discarded_at", - value: "non-configurable" - }), resourceIndex - }); - assertGoodness(result, - "Removed", - "removed", - `${body.name} (${body.x}, ${body.y}, ${body.z})`, - body.id || NaN); - }); - - it("unpacks plant_stage operations", () => { + it("unpacks plant_stage operations: plants", () => { const resourceIndex = fakeResourceIndex(); const plant = selectAllPlantPointers(resourceIndex)[1]; expect(plant).toBeTruthy(); const result = unpackStep({ - step: resourceUpdate({ - resource_type: "Plant", - resource_id: plant.body.id || -1, - label: "plant_stage", - value: "wilting" - }), resourceIndex + 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 unknown resource_update steps", () => { + it("unpacks plant_stage operations: weeds", () => { + const resourceIndex = fakeResourceIndex([fakeWeed()]); + const weed = selectAllWeedPointers(resourceIndex)[1]; + expect(weed).toBeTruthy(); + const result = unpackStep({ - step: resourceUpdate({}), + 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_attr = some_value", "some_value", "Other", 1); + 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 index 60225c797..f19bf6d43 100644 --- a/frontend/sequences/step_tiles/mark_as/action_list.ts +++ b/frontend/sequences/step_tiles/mark_as/action_list.ts @@ -2,7 +2,7 @@ import { Dictionary } from "farmbot"; import { DropDownItem } from "../../../ui"; import { ListBuilder } from "./interfaces"; import { ResourceIndex } from "../../../resources/interfaces"; -import { ResourceUpdate } from "farmbot"; +import { UpdateResource } from "farmbot"; import { selectAllTools } from "../../../resources/selectors"; import { MOUNTED_TO, @@ -27,16 +27,19 @@ const DEFAULT = "Default"; const ACTION_LIST: Dictionary = { "Device": (i) => [DISMOUNT(), ...allToolsAsDDI(i)], "Plant": () => PLANT_OPTIONS(), - "GenericPointer": () => POINT_OPTIONS, - "Weed": () => POINT_OPTIONS, + "GenericPointer": () => POINT_OPTIONS(), + "Weed": () => POINT_OPTIONS(), [DEFAULT]: () => [] }; -const getList = - (t = DEFAULT): ListBuilder => (ACTION_LIST[t] || ACTION_LIST[DEFAULT]); +const getList = (t: string): ListBuilder => + (ACTION_LIST[t] || ACTION_LIST[DEFAULT]); -export const actionList = (d: DropDownItem | undefined, - r: ResourceUpdate, +export const actionList = (d: string | undefined, + r: UpdateResource, i: ResourceIndex): DropDownItem[] => { - return getList(d ? d.headingId : r.args.resource_type)(i); + 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 index 1817060d6..2813cedbe 100644 --- a/frontend/sequences/step_tiles/mark_as/commit_step_changes.ts +++ b/frontend/sequences/step_tiles/mark_as/commit_step_changes.ts @@ -1,4 +1,4 @@ -import { ResourceUpdate } from "farmbot"; +import { UpdateResource } from "farmbot"; import { editStep } from "../../../api/crud"; import { packStep } from "./pack_step"; import { MarkAsEditProps } from "./interfaces"; @@ -11,8 +11,10 @@ export const commitStepChanges = (p: MarkAsEditProps) => { step, index, sequence, - executor(c: ResourceUpdate) { - c.args = packStep(step, nextResource, nextAction).args; + 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 index 256ca247d..4216df74f 100644 --- a/frontend/sequences/step_tiles/mark_as/constants.ts +++ b/frontend/sequences/step_tiles/mark_as/constants.ts @@ -8,7 +8,7 @@ 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[] = [ +export const POINT_OPTIONS = (): DropDownItem[] => [ { label: t("Removed"), value: "removed" }, ]; diff --git a/frontend/sequences/step_tiles/mark_as/interfaces.ts b/frontend/sequences/step_tiles/mark_as/interfaces.ts index f312c50a7..e8bf92745 100644 --- a/frontend/sequences/step_tiles/mark_as/interfaces.ts +++ b/frontend/sequences/step_tiles/mark_as/interfaces.ts @@ -1,25 +1,29 @@ import { ResourceIndex } from "../../../resources/interfaces"; import { DropDownItem } from "../../../ui"; -import { ResourceUpdate, TaggedSequence } from "farmbot"; +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[]; -/** Shape of step.args when step.kind = "resource_update" */ -export type ResourceUpdateArgs = Partial; - /** Input data for calls to commitStepChanges() */ export interface MarkAsEditProps { nextAction: DropDownItem; nextResource: DropDownItem | undefined; - step: ResourceUpdate; + step: UpdateResource; index: number; sequence: TaggedSequence } -export interface StepWithResourceIndex { - step: ResourceUpdate; +export interface PackedStepWithResourceIndex { + step: UpdateResource; + resourceIndex: ResourceIndex; +} + +export interface UnpackedStepWithResourceIndex { + resource: Resource | Identifier; + field: string; + value: string | number | boolean; resourceIndex: ResourceIndex; } diff --git a/frontend/sequences/step_tiles/mark_as/pack_step.ts b/frontend/sequences/step_tiles/mark_as/pack_step.ts index 5f657312b..e4121e858 100644 --- a/frontend/sequences/step_tiles/mark_as/pack_step.ts +++ b/frontend/sequences/step_tiles/mark_as/pack_step.ts @@ -1,4 +1,4 @@ -import { ResourceUpdate, resource_type as RESOURCE_TYPE } from "farmbot"; +import { UpdateResource, Resource, Identifier, resource_type } from "farmbot"; import { DropDownItem } from "../../../ui"; /** @@ -8,43 +8,54 @@ import { DropDownItem } from "../../../ui"; * 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 "resource_update" ("Mark As..") block and merge them + * 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 "resource_update" step when it is saved. + * shape of the new "update_resource" step when it is saved. * */ -export const packStep = - (csNode: ResourceUpdate, - resourceDDI: DropDownItem | undefined, - actionDDI: DropDownItem): ResourceUpdate => { - const resource_type = (resourceDDI ? - resourceDDI.headingId : csNode.args.resource_type) as RESOURCE_TYPE; - const resource_id = (resourceDDI ? - resourceDDI.value : csNode.args.resource_id) as number; - switch (resource_type) { +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 { - kind: "resource_update", - args: { - resource_id, - resource_type, - label: "mounted_tool_id", - value: actionDDI.value - } - }; - + return updateResource(resource, "mounted_tool_id", actionDDI.value); default: /* Scenario II: Changing a point */ - const label = "" + - (actionDDI.value == "removed" ? "discarded_at" : "plant_stage"); - const value = "" + - (label === "discarded_at" ? "{{ Time.now }}" : actionDDI.value); - return { - kind: "resource_update", - args: { resource_id, resource_type, label, value } - }; + 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/assertion_support.ts b/frontend/sequences/step_tiles/mark_as/test_support.ts similarity index 60% rename from frontend/sequences/step_tiles/mark_as/assertion_support.ts rename to frontend/sequences/step_tiles/mark_as/test_support.ts index 289b3be68..e618c0e72 100644 --- a/frontend/sequences/step_tiles/mark_as/assertion_support.ts +++ b/frontend/sequences/step_tiles/mark_as/test_support.ts @@ -1,4 +1,6 @@ -import { ResourceUpdate, TaggedSequence, resource_type } from "farmbot"; +import { + UpdateResource, TaggedSequence, resource_type, Pair, Resource, Identifier, +} from "farmbot"; import { buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; @@ -11,18 +13,26 @@ import { } from "../../../__test_support__/fake_state/resources"; import { betterMerge } from "../../../util"; import { MarkAs } from "../mark_as"; -import { ResourceUpdateArgs } from "./interfaces"; -export function resourceUpdate(i: ResourceUpdateArgs): ResourceUpdate { +export function updateResource( + resource?: Resource | Identifier, pairArgs?: Pair["args"]): UpdateResource { return { - kind: "resource_update", + kind: "update_resource", args: { - resource_type: "Other" as resource_type, - resource_id: 1, - label: "some_attr", - value: "some_value", - ...i - } + 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, + } + }], }; } @@ -38,13 +48,14 @@ export const markAsResourceFixture = () => buildResourceIndex([ export function fakeMarkAsProps() { const steps: TaggedSequence["body"]["body"] = [ { - kind: "resource_update", + kind: "update_resource", args: { - resource_type: "Device", - resource_id: 0, - label: "mounted_tool_id", - value: 0 - } + resource: { + kind: "resource", + args: { resource_id: 0, resource_type: "Device" } + } + }, + body: [{ kind: "pair", args: { label: "mounted_tool_id", value: 0 } }], }, ]; const currentSequence: TaggedSequence = diff --git a/frontend/sequences/step_tiles/mark_as/unpack_step.ts b/frontend/sequences/step_tiles/mark_as/unpack_step.ts index fe04769b7..e0e702082 100644 --- a/frontend/sequences/step_tiles/mark_as/unpack_step.ts +++ b/frontend/sequences/step_tiles/mark_as/unpack_step.ts @@ -1,12 +1,17 @@ import { DropDownItem } from "../../../ui"; import { - findToolById, findPointerByTypeAndId, + findToolById, + findPointerByTypeAndId, } from "../../../resources/selectors"; import { point2ddi } from "./resource_list"; import { MOUNTED_TO } from "./constants"; -import { DropDownPair, StepWithResourceIndex } from "./interfaces"; +import { + DropDownPair, PackedStepWithResourceIndex, UnpackedStepWithResourceIndex, +} from "./interfaces"; import { t } from "../../../i18next_wrapper"; -import { PLANT_STAGE_DDI_LOOKUP } from "../../../farm_designer/plants/edit_plant_status"; +import { + PLANT_STAGE_DDI_LOOKUP, +} from "../../../farm_designer/plants/edit_plant_status"; export const TOOL_MOUNT = (): DropDownItem => ({ label: t("Tool Mount"), value: "tool_mount" @@ -17,14 +22,13 @@ export const DISMOUNTED = (): DropDownPair => ({ rightSide: NOT_IN_USE() }); const DEFAULT_TOOL_NAME = () => t("Untitled Tool"); -const REMOVED_ACTION = () => ({ label: t("Removed"), value: "removed" }); 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: StepWithResourceIndex): DropDownPair { - const { value } = i.step.args; +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); @@ -41,33 +45,34 @@ function mountTool(i: StepWithResourceIndex): DropDownPair { /** 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: StepWithResourceIndex): DropDownPair { - const { resource_type, resource_id, label, value } = i.step.args; - +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: resource_type, value: resource_id }, - rightSide: { label: `${label} = ${value}`, value: "" + value } - }; -} - -/** The user wants to mark a the `discarded_at` attribute of a Point. */ -function discardPoint(i: StepWithResourceIndex): DropDownPair { - const { resource_id, resource_type } = i.step.args; - const pointerBody = - findPointerByTypeAndId(i.resourceIndex, resource_type, resource_id).body; - return { - leftSide: point2ddi(pointerBody), - rightSide: REMOVED_ACTION(), + 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: StepWithResourceIndex): DropDownPair { - const { resource_id, resource_type, value } = i.step.args; - const pointerBody = - findPointerByTypeAndId(i.resourceIndex, resource_type, resource_id).body; +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: point2ddi(pointerBody), + leftSide, rightSide: PLANT_STAGE_DDI_LOOKUP()["" + value] || { label: "" + value, value: "" + value }, }; @@ -76,12 +81,14 @@ function plantStage(i: StepWithResourceIndex): DropDownPair { /** 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(i: StepWithResourceIndex): DropDownPair { - const { label } = i.step.args; - switch (label) { - case "mounted_tool_id": return mountTool(i); - case "discarded_at": return discardPoint(i); - case "plant_stage": return plantStage(i); - default: return unknownOption(i); +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/step_title_bar.tsx b/frontend/sequences/step_tiles/step_title_bar.tsx index d77bdc8b9..cec15d60d 100644 --- a/frontend/sequences/step_tiles/step_title_bar.tsx +++ b/frontend/sequences/step_tiles/step_title_bar.tsx @@ -1,5 +1,7 @@ import * as React from "react"; -import { SequenceBodyItem as Step, SequenceBodyItem } from "farmbot"; +import { + SequenceBodyItem as Step, SequenceBodyItem, LegalSequenceKind, +} from "farmbot"; import { StepTitleBarProps } from "../interfaces"; import { BlurableInput } from "../../ui/index"; import { updateStepTitle } from "./index"; @@ -19,7 +21,8 @@ function translate(input: Step): string { "read_pin": t("Read Sensor"), "send_message": t("Send Message"), "take_photo": t("Take a Photo"), - "resource_update": t("Mark As"), + "update_resource": t("Mark As"), + ["resource_update" as LegalSequenceKind]: t("Deprecated Mark As"), "assertion": t("Assertion"), "set_servo_angle": t("Control Servo"), "wait": t("Wait"), diff --git a/frontend/sequences/step_tiles/tile_old_mark_as.tsx b/frontend/sequences/step_tiles/tile_old_mark_as.tsx new file mode 100644 index 000000000..bfe7e5512 --- /dev/null +++ b/frontend/sequences/step_tiles/tile_old_mark_as.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import { StepParams } from "../interfaces"; +import { ToolTips } from "../../constants"; +import { StepWrapper, StepHeader, StepContent } from "../step_ui/index"; +import { editStep } from "../../api/crud"; +import { SequenceBodyItem, LegalArgString } from "farmbot"; +import { Feature } from "../../devices/interfaces"; +import { trim } from "../../util"; + +const convertOldMarkAs = (oldStep: SequenceBodyItem) => + (step: SequenceBodyItem) => { + const stepArgs = oldStep.args as Record; + step.kind = "update_resource"; + step.args = { + resource: { + kind: "resource", args: { + resource_type: stepArgs.resource_type, + resource_id: stepArgs.resource_id, + } + } + }; + const field = stepArgs.label; + step.body = [{ + kind: "pair", args: { + label: field == "discarded_at" ? "plant_stage" : field, + value: field == "discarded_at" ? "removed" : stepArgs.value, + } + }]; + }; + +export function TileOldMarkAs(props: StepParams) { + const { dispatch, currentStep, index, currentSequence } = props; + const oldStepArgs = currentStep.args as Record; + const className = "update-resource-step"; + return + + +

{trim(`Mark ${oldStepArgs.resource_type} ${oldStepArgs.resource_id} + ${oldStepArgs.label} as ${oldStepArgs.value}`)}

+
+

{"This step has been deprecated."}

+ {props.shouldDisplay?.(Feature.update_resource) && + } +
+
; +} diff --git a/package.json b/package.json index d6409e772..b570b17fd 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "coveralls": "3.0.11", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", - "farmbot": "9.2.3", + "farmbot": "10.0.0-rc1", "i18next": "19.4.1", "install": "0.13.0", "lodash": "4.17.15",