resource_update -> update_resource (fe)

pull/1765/head
gabrielburnworth 2020-04-21 14:37:08 -07:00
parent 1014eece5f
commit 281813369e
27 changed files with 515 additions and 229 deletions

View File

@ -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.`);

View File

@ -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 {

View File

@ -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",
}

View File

@ -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("<SequenceEditorMiddleActive/>", () => {
const fakeProps = (): ActiveMiddleProps => {
@ -73,10 +76,13 @@ describe("<SequenceEditorMiddleActive/>", () => {
};
};
it("saves", () => {
const wrapper = mount(<SequenceEditorMiddleActive {...fakeProps()} />);
clickButton(wrapper, 0, "Save * ");
it("saves", async () => {
const p = fakeProps();
p.dispatch = () => Promise.resolve();
const wrapper = mount(<SequenceEditorMiddleActive {...p} />);
await clickButton(wrapper, 0, "Save * ");
expect(save).toHaveBeenCalledWith(expect.stringContaining("Sequence"));
expect(push).toHaveBeenCalledWith("/app/sequences/fake");
});
it("tests", () => {

View File

@ -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;

View File

@ -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) =>
<div className="button-group">
<SaveBtn status={sequence.specialStatus}
onClick={() => dispatch(save(sequence.uuid))} />
onClick={() => dispatch(save(sequence.uuid)).then(() =>
push(`/app/sequences/${urlFriendly(sequence.body.name)}`))} />
<TestButton
syncStatus={syncStatus}
sequence={sequence}

View File

@ -179,16 +179,19 @@ export function StepButtonCluster(props: StepButtonProps) {
{t("ASSERTION")}
</StepButton>);
shouldDisplay(Feature.mark_as_step) && ALL_THE_BUTTONS.push(<StepButton
shouldDisplay(Feature.update_resource) && ALL_THE_BUTTONS.push(<StepButton
{...commonStepProps}
step={{
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 } },
],
}}
color="brown">
{t("Mark As...")}

View File

@ -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 } },

View File

@ -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("<TileOldMarkAs />", () => {
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(<TileOldMarkAs {...fakeProps()} />);
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(<TileOldMarkAs {...p} />);
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(<TileOldMarkAs {...p} />);
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(<TileOldMarkAs {...p} />);
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" }
}],
});
});
});

View File

@ -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 <TileTakePhoto {...props} />;
case "wait": return <TileWait {...props} />;
case "write_pin": return <TileWritePin {...props} />;
case "resource_update": return <MarkAs {...props} />;
case "update_resource": return <MarkAs {...props} />;
case "resource_update" as LegalSequenceKind:
return <TileOldMarkAs {...props} />;
case "set_servo_angle": return <TileSetServoAngle {...props} />;
case "toggle_pin": return <TileTogglePin {...props} />;
case "zero": return <TileSetZero {...props} />;

View File

@ -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<StepParams, MarkAsState> {
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<StepParams, MarkAsState> {
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 <StepWrapper>
@ -54,7 +54,8 @@ export class MarkAs extends React.Component<StepParams, MarkAsState> {
<Col xs={6}>
<label>{t("as")}</label>
<FBSelect
list={actionList(this.state.nextResource, step, this.props.resources)}
list={actionList(this.state.nextResource?.headingId,
step, this.props.resources)}
onChange={this.commitSelection}
key={JSON.stringify(rightSide) + JSON.stringify(this.state)}
selectedItem={this.state.nextResource ? NONE() : rightSide} />

View File

@ -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);
});
});

View File

@ -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");
});
});

View File

@ -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("<MarkAs/>", () => {

View File

@ -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");
});
});

View File

@ -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", () => {

View File

@ -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");
});
});

View File

@ -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<ListBuilder> = {
"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);
};

View File

@ -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;
}
});
};

View File

@ -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" },
];

View File

@ -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<ResourceUpdate["args"]>;
/** 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;
}

View File

@ -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,
}
}]
});

View File

@ -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 =

View File

@ -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);
}
}

View File

@ -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"),

View File

@ -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<LegalArgString, string>;
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<LegalArgString, string>;
const className = "update-resource-step";
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.MARK_AS}
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
index={index}
confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}>
<p>{trim(`Mark ${oldStepArgs.resource_type} ${oldStepArgs.resource_id}
${oldStepArgs.label} as ${oldStepArgs.value}`)}</p>
<hr />
<p>{"This step has been deprecated."}</p>
{props.shouldDisplay?.(Feature.update_resource) &&
<button className="fb-button yellow" style={{ float: "none" }}
onClick={() => props.dispatch(editStep({
step: props.currentStep,
sequence: props.currentSequence,
index: props.index,
executor: convertOldMarkAs(props.currentStep),
}))}>
{"convert"}
</button>}
</StepContent>
</StepWrapper>;
}

View File

@ -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",