From 426f97ddc26d277bdbaf09ccfdb657d413911475 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 6 May 2020 15:02:53 -0700 Subject: [PATCH] minor step changes --- frontend/css/global.scss | 50 ++++++++--- .../step_tiles/tile_if/then_else.tsx | 4 +- .../__tests__/field_selection_test.tsx | 27 +++--- .../__tests__/field_warning_test.tsx | 82 +++++++++++++++++++ .../__tests__/value_selection_test.tsx | 7 +- .../step_tiles/tile_mark_as/component.tsx | 42 ++++++---- .../tile_mark_as/field_selection.tsx | 53 ++++++------ .../step_tiles/tile_mark_as/field_warning.tsx | 70 ++++++++++++++++ .../step_tiles/tile_mark_as/interfaces.ts | 6 ++ .../tile_mark_as/resource_selection.tsx | 3 +- .../tile_mark_as/value_selection.tsx | 19 +++-- 11 files changed, 281 insertions(+), 82 deletions(-) create mode 100644 frontend/sequences/step_tiles/tile_mark_as/__tests__/field_warning_test.tsx create mode 100644 frontend/sequences/step_tiles/tile_mark_as/field_warning.tsx 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 + 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 {