From 426f97ddc26d277bdbaf09ccfdb657d413911475 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 6 May 2020 15:02:53 -0700 Subject: [PATCH 1/2] 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 { From 73e9daed05dcb8ce64a6d19706a24dd3a93a4c70 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 6 May 2020 15:03:15 -0700 Subject: [PATCH 2/2] dep updates (fe) --- frontend/devices/__tests__/reducer_test.ts | 2 + .../__tests__/board_type_test.tsx | 10 -- .../components/fbos_settings/board_type.tsx | 17 +-- .../__tests__/edit_fe_form_test.tsx | 5 +- .../__tests__/images_filter_menu_test.tsx | 122 +++++++++++++++--- .../map/layers/images/image_filter_menu.tsx | 91 +++++++++---- .../map/layers/spread/spread_layer.tsx | 2 +- .../points/__tests__/create_points_test.tsx | 8 +- .../farm_designer/points/create_points.tsx | 2 +- .../points/point_edit_actions.tsx | 6 +- frontend/farmware/index.tsx | 2 +- .../front_page/__tests__/front_page_test.tsx | 2 + .../__tests__/laptop_splash_test.tsx | 10 ++ frontend/regimens/index.tsx | 2 +- frontend/sequences/sequences.tsx | 2 +- package.json | 22 ++-- 16 files changed, 210 insertions(+), 95 deletions(-) create mode 100644 frontend/front_page/__tests__/laptop_splash_test.tsx diff --git a/frontend/devices/__tests__/reducer_test.ts b/frontend/devices/__tests__/reducer_test.ts index 5efd63e14..22ae27a46 100644 --- a/frontend/devices/__tests__/reducer_test.ts +++ b/frontend/devices/__tests__/reducer_test.ts @@ -1,3 +1,5 @@ +jest.mock("../../redux/store", () => ({ store: jest.fn() })); + import { botReducer, initialState } from "../reducer"; import { Actions } from "../../constants"; import { ControlPanelState, BotState } from "../interfaces"; diff --git a/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx b/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx index bc13219bc..4eee54d11 100644 --- a/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx @@ -43,16 +43,6 @@ describe("", () => { expect(wrapper.text()).toContain("Farmduino"); }); - it("sets sending status", () => { - const wrapper = mount(); - expect(wrapper.state().sending).toBeFalsy(); - const p = fakeProps(); - p.sourceFbosConfig = () => ({ value: true, consistent: false }); - wrapper.setProps(p); - wrapper.mount(); - expect(wrapper.state().sending).toBeTruthy(); - }); - it("calls updateConfig", () => { const p = fakeProps(); const wrapper = mount(); diff --git a/frontend/devices/components/fbos_settings/board_type.tsx b/frontend/devices/components/fbos_settings/board_type.tsx index ccf459341..61af67f57 100644 --- a/frontend/devices/components/fbos_settings/board_type.tsx +++ b/frontend/devices/components/fbos_settings/board_type.tsx @@ -13,17 +13,7 @@ import { Highlight } from "../maybe_highlight"; import { DeviceSetting } from "../../../constants"; import { DevSettings } from "../../../account/dev/dev_support"; -interface BoardTypeState { sending: boolean } - -export class BoardType extends React.Component { - state = { - sending: this.sending - }; - - UNSAFE_componentWillReceiveProps() { - this.setState({ sending: this.sending }); - } - +export class BoardType extends React.Component { get sending() { return !this.props.sourceFbosConfig("firmware_hardware").consistent; } @@ -39,15 +29,14 @@ export class BoardType extends React.Component { if (selectedItem && isFwHardwareValue(firmware_hardware)) { info(t("Sending firmware configuration..."), t("Sending")); this.props.dispatch(updateConfig({ firmware_hardware })); - this.setState({ sending: true }); this.forceUpdate(); } } FirmwareSelection = () => diff --git a/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx b/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx index ff37f8017..ee67ac0bb 100644 --- a/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx @@ -545,7 +545,10 @@ describe("", () => { const fakeProps = (): RepeatFormProps => ({ isRegimen: false, fieldGet: jest.fn(key => - "" + ({ endDate: "2017-07-26" } as FarmEventViewModel)[key]), + "" + ({ + endDate: "2017-07-26", endTime: "08:57", + startDate: "2017-07-25", startTime: "08:57" + } as FarmEventViewModel)[key]), fieldSet: jest.fn(), timeSettings: fakeTimeSettings(), }); diff --git a/frontend/farm_designer/map/layers/images/__tests__/images_filter_menu_test.tsx b/frontend/farm_designer/map/layers/images/__tests__/images_filter_menu_test.tsx index c040e80d1..efef4c2aa 100644 --- a/frontend/farm_designer/map/layers/images/__tests__/images_filter_menu_test.tsx +++ b/frontend/farm_designer/map/layers/images/__tests__/images_filter_menu_test.tsx @@ -1,24 +1,30 @@ -import * as React from "react"; -import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu"; -import { shallow, mount } from "enzyme"; +jest.mock("../../../../../api/crud", () => ({ + edit: jest.fn(), + save: jest.fn(), +})); + import { fakeWebAppConfig, } from "../../../../../__test_support__/fake_state/resources"; -import { StringConfigKey } from "farmbot/dist/resources/configs/web_app"; -import { setWebAppConfigValue } from "../../../../../config_storage/actions"; -import { - fakeTimeSettings, -} from "../../../../../__test_support__/fake_time_settings"; - const mockConfig = fakeWebAppConfig(); jest.mock("../../../../../resources/selectors", () => ({ getWebAppConfig: () => mockConfig, assertUuid: jest.fn(), })); -jest.mock("../../../../../config_storage/actions", () => ({ - setWebAppConfigValue: jest.fn(), -})); +import * as React from "react"; +import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu"; +import { shallow, mount } from "enzyme"; + +import { StringConfigKey } from "farmbot/dist/resources/configs/web_app"; +import { + fakeTimeSettings, +} from "../../../../../__test_support__/fake_time_settings"; +import { edit, save } from "../../../../../api/crud"; +import { fakeState } from "../../../../../__test_support__/fake_state"; +import { + buildResourceIndex, +} from "../../../../../__test_support__/resource_index_builder"; describe("", () => { mockConfig.body.photo_filter_begin = ""; @@ -45,13 +51,19 @@ describe("", () => { ["endDate", "photo_filter_end", 2], ])("sets filter: %s", (filter, key, i) => { const p = fakeProps(); + const state = fakeState(); + const config = fakeWebAppConfig(); + state.resources = buildResourceIndex([config]); + p.dispatch = jest.fn(x => x(jest.fn(), () => state)); const wrapper = shallow(); wrapper.find("BlurableInput").at(i).simulate("commit", { currentTarget: { value: "2001-01-03" } }); expect(wrapper.instance().state[filter]).toEqual("2001-01-03"); - expect(setWebAppConfigValue) - .toHaveBeenCalledWith(key, "2001-01-03T00:00:00.000Z"); + expect(edit).toHaveBeenCalledWith(config, { + [key]: "2001-01-03T00:00:00.000Z" + }); + expect(save).toHaveBeenCalledWith(config.uuid); }); it.each<[ @@ -61,14 +73,64 @@ describe("", () => { ["endTime", "photo_filter_end", 3], ])("sets filter: %s", (filter, key, i) => { const p = fakeProps(); + const state = fakeState(); + const config = fakeWebAppConfig(); + state.resources = buildResourceIndex([config]); + p.dispatch = jest.fn(x => x(jest.fn(), () => state)); const wrapper = shallow(); wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" }); wrapper.find("BlurableInput").at(i).simulate("commit", { currentTarget: { value: "05:00" } }); expect(wrapper.instance().state[filter]).toEqual("05:00"); - expect(setWebAppConfigValue) - .toHaveBeenCalledWith(key, "2001-01-03T05:00:00.000Z"); + expect(edit).toHaveBeenCalledWith(config, { + [key]: "2001-01-03T05:00:00.000Z" + }); + expect(save).toHaveBeenCalledWith(config.uuid); + }); + + it.each<[ + "beginDate" | "endDate", + "photo_filter_begin" | "photo_filter_end", + number + ]>([ + ["beginDate", "photo_filter_begin", 0], + ["endDate", "photo_filter_end", 2], + ])("unsets filter: %s", (filter, key, i) => { + const p = fakeProps(); + const state = fakeState(); + const config = fakeWebAppConfig(); + state.resources = buildResourceIndex([config]); + p.dispatch = jest.fn(x => x(jest.fn(), () => state)); + const wrapper = shallow(); + wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" }); + wrapper.find("BlurableInput").at(i).simulate("commit", { + currentTarget: { value: "" } + }); + expect(wrapper.instance().state[filter]).toEqual(undefined); + // tslint:disable-next-line:no-null-keyword + expect(edit).toHaveBeenCalledWith(config, { [key]: null }); + expect(save).toHaveBeenCalledWith(config.uuid); + }); + + it.each<[ + "beginTime" | "endTime", number + ]>([ + ["beginTime", 1], + ["endTime", 3], + ])("doesn't set filter: %s", (filter, i) => { + const p = fakeProps(); + const state = fakeState(); + const config = fakeWebAppConfig(); + state.resources = buildResourceIndex([config]); + p.dispatch = jest.fn(x => x(jest.fn(), () => state)); + const wrapper = shallow(); + wrapper.find("BlurableInput").at(i).simulate("commit", { + currentTarget: { value: "05:00" } + }); + expect(wrapper.instance().state[filter]).toEqual("05:00"); + expect(edit).not.toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); }); it("loads values from config", () => { @@ -83,14 +145,34 @@ describe("", () => { it("changes slider", () => { const p = fakeProps(); + const state = fakeState(); + const config = fakeWebAppConfig(); + state.resources = buildResourceIndex([config]); + p.dispatch = jest.fn(x => x(jest.fn(), () => state)); + p.getConfigValue = () => undefined; p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z"; const wrapper = shallow(); wrapper.instance().sliderChange(1); expect(wrapper.instance().state.slider).toEqual(1); - expect(setWebAppConfigValue) - .toHaveBeenCalledWith("photo_filter_begin", "2001-01-02T00:00:00.000Z"); - expect(setWebAppConfigValue) - .toHaveBeenCalledWith("photo_filter_end", "2001-01-03T00:00:00.000Z"); + expect(edit).toHaveBeenCalledWith(config, { + photo_filter_begin: "2001-01-02T00:00:00.000Z", + photo_filter_end: "2001-01-03T00:00:00.000Z", + }); + expect(save).toHaveBeenCalledWith(config.uuid); + }); + + it("doesn't update config", () => { + const p = fakeProps(); + const state = fakeState(); + state.resources = buildResourceIndex([]); + p.dispatch = jest.fn(x => x(jest.fn(), () => state)); + p.getConfigValue = () => 1; + p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z"; + const wrapper = shallow(); + wrapper.instance().sliderChange(1); + expect(wrapper.instance().state.slider).toEqual(1); + expect(edit).not.toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); }); it("displays slider labels", () => { diff --git a/frontend/farm_designer/map/layers/images/image_filter_menu.tsx b/frontend/farm_designer/map/layers/images/image_filter_menu.tsx index 7357df513..3a988c8bc 100644 --- a/frontend/farm_designer/map/layers/images/image_filter_menu.tsx +++ b/frontend/farm_designer/map/layers/images/image_filter_menu.tsx @@ -1,9 +1,7 @@ import * as React from "react"; import { BlurableInput } from "../../../../ui/index"; import { offsetTime } from "../../../farm_events/edit_fe_form"; -import { - setWebAppConfigValue, GetWebAppConfigValue, -} from "../../../../config_storage/actions"; +import { GetWebAppConfigValue } from "../../../../config_storage/actions"; import moment from "moment"; import { formatDate, formatTime, @@ -11,8 +9,13 @@ import { import { Slider } from "@blueprintjs/core"; import { t } from "../../../../i18next_wrapper"; import { TimeSettings } from "../../../../interfaces"; +import { StringConfigKey } from "farmbot/dist/resources/configs/web_app"; +import { GetState } from "../../../../redux/interfaces"; +import { getWebAppConfig } from "../../../../resources/getters"; +import { edit, save } from "../../../../api/crud"; +import { isString, isUndefined } from "lodash"; -interface ImageFilterMenuState { +interface FullImageFilterMenuState { beginDate: string | undefined; beginTime: string | undefined; endDate: string | undefined; @@ -20,6 +23,8 @@ interface ImageFilterMenuState { slider: number; } +type ImageFilterMenuState = Partial; + export interface ImageFilterMenuProps { timeSettings: TimeSettings; dispatch: Function; @@ -28,26 +33,48 @@ export interface ImageFilterMenuProps { } export class ImageFilterMenu - extends React.Component> { - constructor(props: ImageFilterMenuProps) { - super(props); - this.state = {}; - } + extends React.Component { + state: ImageFilterMenuState = {}; - UNSAFE_componentWillMount() { - const { newestDate, toOldest } = this.props.imageAgeInfo; + componentDidMount() { const beginDatetime = this.props.getConfigValue("photo_filter_begin"); - this.setState({ - slider: toOldest + 1 - (beginDatetime - ? Math.abs(moment(beginDatetime.toString()) - .diff(moment(newestDate).clone(), "days")) : 0) - }); + if (isString(beginDatetime) || isUndefined(beginDatetime)) { + this.updateSliderState(beginDatetime); + } this.updateState(); } - UNSAFE_componentWillReceiveProps() { - this.updateState(); - } + updateSliderState = (begin: string | undefined) => { + const { newestDate, toOldest } = this.props.imageAgeInfo; + const offset = begin ? Math.abs(moment(begin.toString()) + .diff(moment(newestDate).clone(), "days")) : 0; + this.setState({ slider: toOldest + 1 - offset }); + }; + + setValues = (update: StringValueUpdate) => { + Object.entries(update).map(([key, value]) => { + switch (key) { + case "photo_filter_begin": + this.updateSliderState(value); + value + ? this.setState({ + beginDate: formatDate(value.toString(), this.props.timeSettings), + beginTime: formatTime(value.toString(), this.props.timeSettings), + }) + : this.setState({ beginDate: undefined, beginTime: undefined }); + break; + case "photo_filter_end": + value + ? this.setState({ + endDate: formatDate(value.toString(), this.props.timeSettings), + endTime: formatTime(value.toString(), this.props.timeSettings), + }) + : this.setState({ endDate: undefined, endTime: undefined }); + break; + } + }); + this.props.dispatch(setWebAppConfigValues(update)); + }; updateState = () => { const beginDatetime = this.props.getConfigValue("photo_filter_begin"); @@ -70,27 +97,27 @@ export class ImageFilterMenu const input = e.currentTarget.value; this.setState({ [datetime]: input }); const { beginDate, beginTime, endDate, endTime } = this.state; - const { dispatch, timeSettings } = this.props; + const { timeSettings } = this.props; let value = undefined; switch (datetime) { case "beginDate": value = offsetTime(input, beginTime || "00:00", timeSettings); - dispatch(setWebAppConfigValue("photo_filter_begin", value)); + this.setValues({ photo_filter_begin: value }); break; case "beginTime": if (beginDate) { value = offsetTime(beginDate, input, timeSettings); - dispatch(setWebAppConfigValue("photo_filter_begin", value)); + this.setValues({ photo_filter_begin: value }); } break; case "endDate": value = offsetTime(input, endTime || "00:00", timeSettings); - dispatch(setWebAppConfigValue("photo_filter_end", value)); + this.setValues({ photo_filter_end: value }); break; case "endTime": if (endDate) { value = offsetTime(endDate, input, timeSettings); - dispatch(setWebAppConfigValue("photo_filter_end", value)); + this.setValues({ photo_filter_end: value }); } break; } @@ -100,13 +127,12 @@ export class ImageFilterMenu sliderChange = (slider: number) => { const { newestDate, toOldest } = this.props.imageAgeInfo; this.setState({ slider }); - const { dispatch, timeSettings } = this.props; + const { timeSettings } = this.props; const calcDate = (day: number) => moment(newestDate).subtract(toOldest - day, "days").toISOString(); const begin = offsetTime(calcDate(slider - 1), "00:00", timeSettings); const end = offsetTime(calcDate(slider), "00:00", timeSettings); - dispatch(setWebAppConfigValue("photo_filter_begin", begin)); - dispatch(setWebAppConfigValue("photo_filter_end", end)); + this.setValues({ photo_filter_begin: begin, photo_filter_end: end }); } renderLabel = (day: number) => { @@ -191,3 +217,14 @@ export class ImageFilterMenu
; } } + +type StringValueUpdate = Partial>; + +const setWebAppConfigValues = (update: StringValueUpdate) => + (dispatch: Function, getState: GetState) => { + const webAppConfig = getWebAppConfig(getState().resources.index); + if (webAppConfig) { + dispatch(edit(webAppConfig, update)); + dispatch(save(webAppConfig.uuid)); + } + }; diff --git a/frontend/farm_designer/map/layers/spread/spread_layer.tsx b/frontend/farm_designer/map/layers/spread/spread_layer.tsx index b31b9f56b..f186f576d 100644 --- a/frontend/farm_designer/map/layers/spread/spread_layer.tsx +++ b/frontend/farm_designer/map/layers/spread/spread_layer.tsx @@ -73,7 +73,7 @@ export class SpreadCircle extends React.Component { state: SpreadCircleState = { spread: undefined }; - UNSAFE_componentWillMount = () => { + componentDidMount = () => { cachedCrop(this.props.plant.body.openfarm_slug) .then(({ spread }) => this.setState({ spread })); } diff --git a/frontend/farm_designer/points/__tests__/create_points_test.tsx b/frontend/farm_designer/points/__tests__/create_points_test.tsx index f3a4926ba..e900fa27e 100644 --- a/frontend/farm_designer/points/__tests__/create_points_test.tsx +++ b/frontend/farm_designer/points/__tests__/create_points_test.tsx @@ -78,12 +78,12 @@ describe("", () => { it("updates specific fields", () => { const p = fakeProps(); p.drawnPoint = FAKE_POINT; - const i = new CreatePoints(p); - i.updateValue("color")(inputEvent("cheerful hue")); - expect(i.props.drawnPoint).toBeTruthy(); + const wrapper = mount(); + wrapper.instance().updateValue("color")(inputEvent("cheerful hue")); + expect(wrapper.instance().props.drawnPoint).toBeTruthy(); const expected = cloneDeep(FAKE_POINT); expected.color = "cheerful hue"; - expect(i.props.dispatch).toHaveBeenCalledWith({ + expect(wrapper.instance().props.dispatch).toHaveBeenCalledWith({ type: "SET_DRAWN_POINT_DATA", payload: expected, }); diff --git a/frontend/farm_designer/points/create_points.tsx b/frontend/farm_designer/points/create_points.tsx index 417b9ed1d..e5196e4be 100644 --- a/frontend/farm_designer/points/create_points.tsx +++ b/frontend/farm_designer/points/create_points.tsx @@ -122,7 +122,7 @@ export class RawCreatePoints }); } - UNSAFE_componentWillMount() { + componentDidMount() { this.loadDefaultPoint(); } diff --git a/frontend/farm_designer/points/point_edit_actions.tsx b/frontend/farm_designer/points/point_edit_actions.tsx index 123e338a6..bf0d92699 100644 --- a/frontend/farm_designer/points/point_edit_actions.tsx +++ b/frontend/farm_designer/points/point_edit_actions.tsx @@ -73,11 +73,11 @@ export const AdditionalWeedProperties = (props: AdditionalWeedPropertiesProps) = case "type": return
; case "created_by": - return + return {SOURCE_LOOKUP()[value || ""] || t("unknown")} ; case "removal_method": - return + return
{REMOVAL_METHODS.map(method =>
@@ -93,7 +93,7 @@ export const AdditionalWeedProperties = (props: AdditionalWeedPropertiesProps) =
; default: - return + return {value || ""} ; } diff --git a/frontend/farmware/index.tsx b/frontend/farmware/index.tsx index 751d8b558..858ba1175 100644 --- a/frontend/farmware/index.tsx +++ b/frontend/farmware/index.tsx @@ -124,7 +124,7 @@ export class RawFarmwarePage extends React.Component { return isBotOnline(this.props.syncStatus, this.props.botToMqttStatus); } - UNSAFE_componentWillMount() { + componentDidMount() { if (window.innerWidth > 450) { this.props.dispatch({ type: Actions.SELECT_FARMWARE, diff --git a/frontend/front_page/__tests__/front_page_test.tsx b/frontend/front_page/__tests__/front_page_test.tsx index 5a672c6bc..63ee8fe9d 100644 --- a/frontend/front_page/__tests__/front_page_test.tsx +++ b/frontend/front_page/__tests__/front_page_test.tsx @@ -26,6 +26,8 @@ jest.mock("../../api", () => ({ } })); +jest.mock("../laptop_splash", () => ({ LaptopSplash: () =>
})); + import * as React from "react"; import { mount, shallow } from "enzyme"; import { FrontPage, setField, PartialFormEvent } from "../front_page"; diff --git a/frontend/front_page/__tests__/laptop_splash_test.tsx b/frontend/front_page/__tests__/laptop_splash_test.tsx new file mode 100644 index 000000000..00547ecd5 --- /dev/null +++ b/frontend/front_page/__tests__/laptop_splash_test.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; +import { shallow } from "enzyme"; +import { LaptopSplash } from "../laptop_splash"; + +describe("", () => { + it("renders", () => { + const wrapper = shallow(); + expect(wrapper.find("video").length).toEqual(1); + }); +}); diff --git a/frontend/regimens/index.tsx b/frontend/regimens/index.tsx index 14b2f2e72..5d70fba5d 100644 --- a/frontend/regimens/index.tsx +++ b/frontend/regimens/index.tsx @@ -28,7 +28,7 @@ export const RegimenBackButton = (props: RegimenBackButtonProps) => { }; export class RawRegimens extends React.Component { - UNSAFE_componentWillMount() { + componentDidMount() { if (!this.props.current) { setActiveRegimenByName(); } } diff --git a/frontend/sequences/sequences.tsx b/frontend/sequences/sequences.tsx index 05cf74823..0f91f8bfe 100644 --- a/frontend/sequences/sequences.tsx +++ b/frontend/sequences/sequences.tsx @@ -28,7 +28,7 @@ export const SequenceBackButton = (props: SequenceBackButtonProps) => { }; export class RawSequences extends React.Component { - UNSAFE_componentWillMount() { + componentDidMount() { if (!this.props.sequence) { setActiveSequenceByName(); } } diff --git a/package.json b/package.json index c17885701..de510f06e 100644 --- a/package.json +++ b/package.json @@ -24,16 +24,16 @@ "author": "farmbot.io", "license": "MIT", "dependencies": { - "@babel/core": "7.9.0", - "@blueprintjs/core": "3.26.0", - "@blueprintjs/datetime": "3.16.1", - "@blueprintjs/select": "3.12.2", + "@babel/core": "7.9.6", + "@blueprintjs/core": "3.26.1", + "@blueprintjs/datetime": "3.17.0", + "@blueprintjs/select": "3.12.3", "@types/enzyme": "3.10.5", "@types/jest": "25.2.1", "@types/lodash": "4.14.150", "@types/markdown-it": "10.0.1", "@types/moxios": "0.4.9", - "@types/node": "13.13.4", + "@types/node": "13.13.5", "@types/promise-timeout": "1.3.0", "@types/react": "16.9.34", "@types/react-color": "3.0.1", @@ -51,21 +51,21 @@ "lodash": "4.17.15", "markdown-it": "10.0.0", "markdown-it-emoji": "1.4.0", - "moment": "2.24.0", + "moment": "2.25.3", "moxios": "0.4.0", "mqtt": "4.0.0", - "npm": "6.14.4", + "npm": "6.14.5", "parcel-bundler": "1.12.4", "promise-timeout": "1.3.0", "raf": "3.4.1", "react": "16.13.1", "react-addons-test-utils": "15.6.2", - "react-color": "2.18.0", + "react-color": "2.18.1", "react-dom": "16.13.1", "react-joyride": "2.2.1", "react-redux": "7.2.0", "react-test-renderer": "16.13.1", - "react-transition-group": "4.3.0", + "react-transition-group": "4.4.1", "redux": "4.0.5", "redux-immutable-state-invariant": "2.1.0", "redux-thunk": "2.3.0", @@ -78,8 +78,8 @@ "which": "2.0.2" }, "devDependencies": { - "jest": "25.4.0", - "jest-cli": "25.4.0", + "jest": "25.5.4", + "jest-cli": "25.5.4", "jest-junit": "10.0.0", "jest-skipped-reporter": "0.0.5", "jshint": "2.11.0",