diff --git a/frontend/css/global.scss b/frontend/css/global.scss
index 468e830dc..3510fa00a 100644
--- a/frontend/css/global.scss
+++ b/frontend/css/global.scss
@@ -1315,20 +1315,46 @@ 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-step-resource {
+ margin-bottom: 1rem;
}
.update-resource-pair {
- margin-top: 1rem;
+ margin-top: 0;
+ margin-right: -2rem;
+ div[class*=col-] {
+ padding: 0;
+ padding-right: 2rem;
+ }
+ .custom-meta-field {
+ position: relative;
+ input {
+ height: 3rem;
+ }
+ .fa-undo {
+ position: absolute;
+ top: 0.65rem;
+ right: 0.5rem;
+ color: $medium_light_gray;
+ &:hover {
+ color: $dark_gray;
+ }
+ }
+ }
+ .custom-field-warning {
+ display: inline-block;
+ margin-top: 0.5rem;
+ i,
+ p {
+ display: inline;
+ cursor: default !important;
+ margin-right: 0.5rem;
+ color: $darkest_red;
+ }
+ .did-you-mean {
+ cursor: pointer !important;
+ font-weight: bold;
+ }
+ }
}
}
diff --git a/frontend/sequences/step_tiles/tile_if/then_else.tsx b/frontend/sequences/step_tiles/tile_if/then_else.tsx
index 784332152..a6c959eab 100644
--- a/frontend/sequences/step_tiles/tile_if/then_else.tsx
+++ b/frontend/sequences/step_tiles/tile_if/then_else.tsx
@@ -10,7 +10,7 @@ export function ThenElse(props: ThenElseParams) {
onChange, selectedItem, calledSequenceVariableData, assignVariable
} = IfBlockDropDownHandler(props);
const { body } = props.currentStep.args[props.thenElseKey];
- return
@@ -22,7 +22,7 @@ export function ThenElse(props: ThenElseParams) {
onChange={onChange}
selectedItem={selectedItem()} />
{!!calledSequenceVariableData &&
-
+
", () => {
const fakeProps = (): FieldSelectionProps => ({
resource: { kind: "nothing", args: {} },
@@ -35,8 +39,8 @@ describe("
", () => {
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: "" },
+ DDI.PLANT_STAGE,
+ DDI.CUSTOM_META_FIELD,
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Select one");
@@ -80,8 +84,8 @@ describe("
", () => {
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: "" },
+ DDI.STATUS,
+ DDI.CUSTOM_META_FIELD,
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Status");
@@ -98,8 +102,8 @@ describe("
", () => {
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: "" },
+ DDI.WEED_STATUS,
+ DDI.CUSTOM_META_FIELD,
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Weed status");
@@ -116,11 +120,10 @@ describe("
", () => {
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: "" },
+ DDI.CUSTOM_META_FIELD,
]);
expect(wrapper.text()).toContain("field");
- expect(wrapper.text()).toContain("Status");
+ expect(wrapper.text()).toContain("Point status");
expect(wrapper.find(".reset-custom-field").length).toEqual(0);
});
@@ -149,8 +152,8 @@ describe("
", () => {
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: "" },
+ DDI.MOUNTED_TOOL,
+ DDI.CUSTOM_META_FIELD,
]);
expect(wrapper.text()).toContain("field");
expect(wrapper.text()).toContain("Mounted Tool");
diff --git a/frontend/sequences/step_tiles/tile_mark_as/__tests__/field_warning_test.tsx b/frontend/sequences/step_tiles/tile_mark_as/__tests__/field_warning_test.tsx
new file mode 100644
index 000000000..f230533a3
--- /dev/null
+++ b/frontend/sequences/step_tiles/tile_mark_as/__tests__/field_warning_test.tsx
@@ -0,0 +1,82 @@
+import * as React from "react";
+import { mount } from "enzyme";
+import { CustomFieldWarning } from "../field_warning";
+import { CustomFieldWarningProps } from "../interfaces";
+
+describe("
", () => {
+ const fakeProps = (): CustomFieldWarningProps => ({
+ resource: { kind: "nothing", args: {} },
+ field: "",
+ update: jest.fn(),
+ });
+
+ it("doesn't display warning", () => {
+ const p = fakeProps();
+ p.field = "";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).not.toContain("invalid field");
+ });
+
+ it("displays warning", () => {
+ const p = fakeProps();
+ p.field = "nope";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).toContain("invalid field");
+ expect(wrapper.text().toLowerCase()).toContain("meta");
+ wrapper.find(".did-you-mean").simulate("click");
+ expect(p.update).toHaveBeenCalledWith({ field: "meta.nope" });
+ });
+
+ it("displays warning: Device resource", () => {
+ const p = fakeProps();
+ p.resource = {
+ kind: "resource", args: { resource_type: "Device", resource_id: 1 }
+ };
+ p.field = "x";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).toContain("invalid field");
+ expect(wrapper.text().toLowerCase()).not.toContain("meta");
+ });
+
+ it("displays warning: GenericPointer resource", () => {
+ const p = fakeProps();
+ p.resource = {
+ kind: "resource", args: { resource_type: "GenericPointer", resource_id: 1 }
+ };
+ p.field = "openfarm_slug";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).toContain("invalid field");
+ expect(wrapper.text().toLowerCase()).toContain("meta");
+ });
+
+ it("doesn't display warning: Plant resource", () => {
+ const p = fakeProps();
+ p.resource = {
+ kind: "resource", args: { resource_type: "Plant", resource_id: 1 }
+ };
+ p.field = "openfarm_slug";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).not.toContain("invalid field");
+ expect(wrapper.text().toLowerCase()).not.toContain("meta");
+ });
+
+ it("displays warning: Weed resource", () => {
+ const p = fakeProps();
+ p.resource = {
+ kind: "resource", args: { resource_type: "Weed", resource_id: 1 }
+ };
+ p.field = "openfarm_slug";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).toContain("invalid field");
+ expect(wrapper.text().toLowerCase()).toContain("meta");
+ });
+
+ it("displays warning: identifier", () => {
+ const p = fakeProps();
+ p.resource = { kind: "identifier", args: { label: "var" } };
+ p.field = "mounted_tool_id";
+ const wrapper = mount(
);
+ expect(wrapper.text().toLowerCase()).toContain("invalid field");
+ expect(wrapper.text().toLowerCase()).toContain("meta");
+ });
+});
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
index 9f4051bf8..38937d637 100644
--- 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
@@ -15,6 +15,9 @@ import {
} from "../../../../farm_designer/plants/edit_plant_status";
import { fakeTool } from "../../../../__test_support__/fake_state/resources";
import { resource_type, Resource } from "farmbot";
+import { UPDATE_RESOURCE_DDIS } from "../field_selection";
+
+const DDI = UPDATE_RESOURCE_DDIS();
describe("
", () => {
const fakeProps = (): ValueSelectionProps => ({
@@ -119,7 +122,7 @@ describe("
", () => {
const wrapper = mount(
);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
- { label: "Removed", value: "removed" },
+ DDI.REMOVED,
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
@@ -153,7 +156,7 @@ describe("
", () => {
const wrapper = mount(
);
expect(wrapper.find("FBSelect").length).toEqual(1);
expect(wrapper.find("FBSelect").props().list).toEqual([
- { label: "Removed", value: "removed" },
+ DDI.REMOVED,
]);
expect(wrapper.text()).toContain("as");
expect(wrapper.text()).toContain("Removed");
diff --git a/frontend/sequences/step_tiles/tile_mark_as/component.tsx b/frontend/sequences/step_tiles/tile_mark_as/component.tsx
index 0e0357326..620b5faf9 100644
--- a/frontend/sequences/step_tiles/tile_mark_as/component.tsx
+++ b/frontend/sequences/step_tiles/tile_mark_as/component.tsx
@@ -10,6 +10,7 @@ import { FieldSelection } from "./field_selection";
import { ValueSelection } from "./value_selection";
import { isUndefined } from "lodash";
import { NOTHING_SELECTED } from "../../locals_list/handle_select";
+import { CustomFieldWarning } from "./field_warning";
export class MarkAs extends React.Component
{
state: MarkAsState = {
@@ -89,30 +90,35 @@ export class MarkAs extends React.Component {
confirmStepDeletion={this.props.confirmStepDeletion} />
-
+
-
- {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
index 6fb201e4f..d4ea60944 100644
--- a/frontend/sequences/step_tiles/tile_mark_as/field_selection.tsx
+++ b/frontend/sequences/step_tiles/tile_mark_as/field_selection.tsx
@@ -19,7 +19,7 @@ const KnownFieldSelection = (props: FieldSelectionProps) =>
list={props.resource.kind == "nothing"
? []
: fieldList(props.resource)
- .concat([{ label: t("Custom Meta Field"), value: "" }])}
+ .concat([UPDATE_RESOURCE_DDIS().CUSTOM_META_FIELD])}
onChange={ddi => props.update({
field: "" + ddi.value,
value: undefined
@@ -58,18 +58,13 @@ 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" }];
- }
+ const DDI = UPDATE_RESOURCE_DDIS();
+ if (resource.kind == "identifier") { return [DDI.STATUS]; }
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" }];
+ case "Device": return [DDI.MOUNTED_TOOL];
+ case "Weed": return [DDI.WEED_STATUS];
+ case "GenericPointer": return [];
+ default: return [DDI.PLANT_STAGE];
}
};
@@ -77,23 +72,27 @@ 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 DDI = UPDATE_RESOURCE_DDIS();
+ if (isUndefined(field) || resource.kind == "nothing") { return DDI.SELECT_ONE; }
+ if (resource.kind == "identifier") { return DDI.STATUS; }
const resourceType = resource.args.resource_type;
switch (field) {
- case KnownField.mounted_tool_id:
- return { label: t("Mounted Tool"), value: "tool" };
+ case KnownField.mounted_tool_id: return DDI.MOUNTED_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" };
+ if (resourceType == "Weed") { return DDI.WEED_STATUS; }
+ if (resourceType == "GenericPointer") { return DDI.POINT_STATUS; }
+ return DDI.PLANT_STAGE;
}
};
+
+export const UPDATE_RESOURCE_DDIS = (): Record => ({
+ SELECT_ONE: { label: t("Select one"), value: "" },
+ CUSTOM_META_FIELD: { label: t("Custom field"), value: "" },
+ STATUS: { label: t("Status"), value: "plant_stage" },
+ MOUNTED_TOOL: { label: t("Mounted Tool"), value: "mounted_tool_id" },
+ WEED_STATUS: { label: t("Weed status"), value: "plant_stage" },
+ POINT_STATUS: { label: t("Point status"), value: "plant_stage" },
+ PLANT_STAGE: { label: t("Plant stage"), value: "plant_stage" },
+ NONE: { label: t("None"), value: 0 },
+ REMOVED: { label: t("Removed"), value: "removed" },
+});
diff --git a/frontend/sequences/step_tiles/tile_mark_as/field_warning.tsx b/frontend/sequences/step_tiles/tile_mark_as/field_warning.tsx
new file mode 100644
index 000000000..1ea9bc166
--- /dev/null
+++ b/frontend/sequences/step_tiles/tile_mark_as/field_warning.tsx
@@ -0,0 +1,70 @@
+import * as React from "react";
+import { t } from "../../../i18next_wrapper";
+import { Resource, Identifier, Nothing } from "farmbot";
+import {
+ PlantPointer, ToolSlotPointer, WeedPointer, GenericPointer,
+ DeviceAccountSettings, Point,
+} from "farmbot/dist/resources/api_resources";
+import { CustomFieldWarningProps } from "./interfaces";
+
+export const CustomFieldWarning = (props: CustomFieldWarningProps) =>
+ props.field && !validFields(props.resource).includes(props.field)
+ && !props.field.includes("meta.")
+ ?
+
+
+ {t("Invalid field for resource.")}
+
+ {!(props.resource.kind == "resource" &&
+ props.resource.args.resource_type == "Device") &&
+
props.update({
+ field: "meta." + props.field,
+ value: undefined
+ })}>
+ {t("Did you mean meta.{{field}}?", { field: props.field })}
+
}
+
+ : ;
+
+const validFields = (resource: Resource | Identifier | Nothing): string[] => {
+ if (resource.kind == "identifier" || resource.kind == "nothing") {
+ return POINT_FIELDS;
+ }
+ switch (resource.args.resource_type) {
+ case "Device": return DEVICE_FIELDS;
+ case "Weed": return WEED_FIELDS;
+ case "GenericPointer": return GENERIC_POINTER_FIELDS;
+ default: return PLANT_FIELDS;
+ }
+};
+
+type BaseFields = (keyof Point)[];
+type PlantFields = (keyof PlantPointer)[];
+type ToolSlotFields = (keyof ToolSlotPointer)[];
+type GenericPointerFields = (keyof GenericPointer)[];
+type WeedFields = (keyof WeedPointer)[];
+type PointFields = (
+ keyof PlantPointer
+ | keyof ToolSlotPointer
+ | keyof GenericPointer
+ | keyof WeedPointer
+)[];
+
+const BASE_FIELDS: BaseFields =
+ ["name", "pointer_type", "x", "y", "z", "meta"];
+const PLANT_FIELDS: PlantFields = (BASE_FIELDS as PlantFields)
+ .concat(["openfarm_slug", "plant_stage", "planted_at", "radius"]);
+const TOOL_SLOT_FIELDS: ToolSlotFields = (BASE_FIELDS as ToolSlotFields)
+ .concat(["tool_id", "pullout_direction", "gantry_mounted"]);
+const GENERIC_POINTER_FIELDS: GenericPointerFields =
+ (BASE_FIELDS as GenericPointerFields).concat(["radius"]);
+const WEED_FIELDS: WeedFields = (BASE_FIELDS as PlantFields)
+ .concat(["plant_stage", "radius"]) as WeedFields;
+const POINT_FIELDS: PointFields = (BASE_FIELDS as PointFields)
+ .concat(PLANT_FIELDS)
+ .concat(TOOL_SLOT_FIELDS)
+ .concat(GENERIC_POINTER_FIELDS)
+ .concat(WEED_FIELDS);
+const DEVICE_FIELDS: (keyof DeviceAccountSettings)[] =
+ ["name", "mounted_tool_id", "ota_hour", "timezone"];
diff --git a/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts b/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts
index 7a109b091..6378e4eb7 100644
--- a/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts
+++ b/frontend/sequences/step_tiles/tile_mark_as/interfaces.ts
@@ -55,6 +55,12 @@ export interface CustomFieldSelectionProps extends SelectionPropsBase {
update: UpdateFieldOrValue;
}
+export interface CustomFieldWarningProps {
+ resource: Resource | Identifier | Nothing;
+ field: string | undefined;
+ update: UpdateFieldOrValue;
+}
+
export interface ValueSelectionProps extends SelectionPropsBase {
field: string | undefined;
value: UpdateResourceValue | undefined;
diff --git a/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx b/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx
index 345a3583c..cae54b9ac 100644
--- a/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx
+++ b/frontend/sequences/step_tiles/tile_mark_as/resource_selection.tsx
@@ -14,6 +14,7 @@ import { formatPoint } from "../../locals_list/location_form_list";
import {
maybeFindVariable, SequenceMeta,
} from "../../../resources/sequence_meta";
+import { UPDATE_RESOURCE_DDIS } from "./field_selection";
export const ResourceSelection = (props: ResourceSelectionProps) =>
@@ -83,7 +84,7 @@ const getSelectedResource = (
label: resourceVariableLabel(variable),
value: resource.args.label,
};
- case "nothing": return { label: t("Select one"), value: "" };
+ case "nothing": return UPDATE_RESOURCE_DDIS().SELECT_ONE;
}
};
diff --git a/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx b/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx
index 19dac0360..59539263c 100644
--- a/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx
+++ b/frontend/sequences/step_tiles/tile_mark_as/value_selection.tsx
@@ -10,7 +10,9 @@ 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 {
+ isCustomMetaField, KnownField, knownField, UPDATE_RESOURCE_DDIS,
+} from "./field_selection";
import { DevSettings } from "../../../account/dev/dev_support";
export const ValueSelection = (props: ValueSelectionProps) =>
@@ -43,6 +45,7 @@ const KnownValue = (props: ValueSelectionProps) =>
const CustomMetaValue = (props: ValueSelectionProps) =>
{
props.update({ value: e.currentTarget.value },
@@ -53,33 +56,33 @@ const CustomMetaValue = (props: ValueSelectionProps) =>
const valuesList = (
resource: Resource | Identifier,
resources: ResourceIndex): DropDownItem[] => {
+ const DDI = UPDATE_RESOURCE_DDIS();
const stepResourceType =
resource.kind == "identifier" ? undefined : resource.args.resource_type;
switch (stepResourceType) {
case "Device": return [
- { label: t("None"), value: 0 },
+ DDI.NONE,
...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 "GenericPointer": return [DDI.REMOVED];
+ case "Weed": return [DDI.REMOVED];
case "Plant":
default: return PLANT_STAGE_LIST();
}
};
const getSelectedValue = (props: GetSelectedValueProps): DropDownItem => {
+ const DDI = UPDATE_RESOURCE_DDIS();
if (isUndefined(props.field) || isUndefined(props.value)
- || props.resource.kind == "nothing") {
- return { label: t("Select one"), value: "" };
- }
+ || props.resource.kind == "nothing") { return DDI.SELECT_ONE; }
switch (props.field) {
case KnownField.mounted_tool_id:
const toolId = parseInt("" + props.value);
- if (toolId == 0) { return { label: t("None"), value: 0 }; }
+ if (toolId == 0) { return DDI.NONE; }
const tool = maybeFindToolById(props.resourceIndex, toolId);
if (!tool) { return { label: t("Unknown tool"), value: toolId }; }
return {