From 9dab0c4bc531600fe0e87ad108b0d885ed8779f3 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Sat, 15 Feb 2020 10:29:09 -0800 Subject: [PATCH 1/4] hardware settings ui updates --- .../__test_support__/control_panel_state.ts | 5 +- frontend/__test_support__/fake_state/bot.ts | 15 ++-- frontend/constants.ts | 86 ++++++++++++------- frontend/css/global.scss | 22 ++++- frontend/devices/__tests__/actions_test.ts | 2 +- .../components/farmbot_os_settings.tsx | 1 - .../__tests__/power_and_reset_test.tsx | 30 +++---- .../components/fbos_settings/interfaces.ts | 1 - .../fbos_settings/power_and_reset.tsx | 30 +++---- .../devices/components/hardware_settings.tsx | 35 ++++---- .../__tests__/calibration_row_test.tsx | 44 +++++++--- .../__tests__/encoders_and_endstops_test.tsx | 47 ---------- .../__tests__/encoders_test.tsx | 32 +++++++ .../__tests__/endstops_test.tsx | 21 +++++ .../__tests__/error_handling_tests.tsx | 47 ++++++++++ .../__tests__/homing_and_calibration_test.tsx | 71 ++++++++++++--- .../__tests__/homing_row_test.tsx | 33 ------- .../__tests__/motors_test.tsx | 21 +---- .../__tests__/pin_bindings_test.tsx | 22 +++++ .../__tests__/zero_row_test.tsx | 19 ---- .../hardware_settings/calibration_row.tsx | 22 ++--- ...encoders_and_endstops.tsx => encoders.tsx} | 84 +++++++----------- .../components/hardware_settings/endstops.tsx | 57 ++++++++++++ .../hardware_settings/error_handling.tsx | 53 ++++++++++++ .../homing_and_calibration.tsx | 57 ++++++++---- .../hardware_settings/homing_row.tsx | 41 --------- .../components/hardware_settings/motors.tsx | 58 +------------ .../hardware_settings/pin_bindings.tsx | 22 +++++ .../hardware_settings/single_setting_row.tsx | 20 +++++ .../components/hardware_settings/zero_row.tsx | 40 --------- frontend/devices/components/interfaces.ts | 33 +++++-- .../components/pin_number_dropdown.tsx | 6 +- frontend/devices/devices.tsx | 4 - frontend/devices/interfaces.ts | 5 +- .../__tests__/list_and_label_support_test.tsx | 11 ++- .../__tests__/pin_bindings_test.tsx | 12 +-- frontend/devices/pin_bindings/interfaces.ts | 2 +- .../pin_bindings/list_and_label_support.tsx | 5 ++ .../pin_bindings/pin_binding_input_group.tsx | 20 ++--- .../devices/pin_bindings/pin_bindings.tsx | 34 +++----- .../pin_bindings/pin_bindings_list.tsx | 10 +-- frontend/devices/reducer.ts | 10 ++- .../sequences/step_tiles/tile_calibrate.tsx | 2 +- 43 files changed, 680 insertions(+), 512 deletions(-) delete mode 100644 frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx delete mode 100644 frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx delete mode 100644 frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx rename frontend/devices/components/hardware_settings/{encoders_and_endstops.tsx => encoders.tsx} (55%) create mode 100644 frontend/devices/components/hardware_settings/endstops.tsx create mode 100644 frontend/devices/components/hardware_settings/error_handling.tsx delete mode 100644 frontend/devices/components/hardware_settings/homing_row.tsx create mode 100644 frontend/devices/components/hardware_settings/pin_bindings.tsx create mode 100644 frontend/devices/components/hardware_settings/single_setting_row.tsx delete mode 100644 frontend/devices/components/hardware_settings/zero_row.tsx diff --git a/frontend/__test_support__/control_panel_state.ts b/frontend/__test_support__/control_panel_state.ts index 13eddf61a..30fdacdeb 100644 --- a/frontend/__test_support__/control_panel_state.ts +++ b/frontend/__test_support__/control_panel_state.ts @@ -4,7 +4,10 @@ export const panelState = (): ControlPanelState => { return { homing_and_calibration: false, motors: false, - encoders_and_endstops: false, + encoders: false, + endstops: false, + error_handling: false, + pin_bindings: false, danger_zone: false, power_and_reset: false, pin_guard: false diff --git a/frontend/__test_support__/fake_state/bot.ts b/frontend/__test_support__/fake_state/bot.ts index ec6c82df5..98151048c 100644 --- a/frontend/__test_support__/fake_state/bot.ts +++ b/frontend/__test_support__/fake_state/bot.ts @@ -4,12 +4,15 @@ export const bot: Everything["bot"] = { "consistent": true, "stepSize": 100, "controlPanelState": { - "homing_and_calibration": false, - "motors": false, - "encoders_and_endstops": false, - "danger_zone": false, - "power_and_reset": false, - "pin_guard": false, + homing_and_calibration: false, + motors: false, + encoders: false, + endstops: false, + error_handling: false, + pin_bindings: false, + danger_zone: false, + power_and_reset: false, + pin_guard: false, }, "hardware": { "gpio_registry": {}, diff --git a/frontend/constants.ts b/frontend/constants.ts index 71388071f..7dd95c219 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -39,8 +39,8 @@ export namespace ToolTips { few sequences to verify that everything works as expected.`); export const PIN_BINDINGS = - trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is - activated.`); + trim(`Assign an action or sequence to execute when a Raspberry Pi + GPIO pin is activated.`); export const PIN_BINDING_WARNING = trim(`Warning: Binding to a pin without a physical button and @@ -51,24 +51,38 @@ export namespace ToolTips { trim(`Diagnose connectivity issues with FarmBot and the browser.`); // Hardware Settings: Homing and Calibration - export const HOMING = + export const HOMING_ENCODERS = trim(`If encoders or end-stops are enabled, home axis (find zero).`); - export const CALIBRATION = + export const HOMING_STALL_DETECTION = + trim(`If stall detection or end-stops are enabled, home axis + (find zero).`); + + export const CALIBRATION_ENCODERS = trim(`If encoders or end-stops are enabled, home axis and determine maximum.`); + export const CALIBRATION_STALL_DETECTION = + trim(`If stall detection or end-stops are enabled, home axis and + determine maximum.`); + export const SET_ZERO_POSITION = trim(`Set the current location as zero.`); - export const FIND_HOME_ON_BOOT = + export const FIND_HOME_ON_BOOT_ENCODERS = trim(`If encoders or end-stops are enabled, find the home position - when the device powers on. - Warning! This will perform homing on all axes when the - device powers on. Encoders or endstops must be enabled. + when the device powers on. Warning! This will perform homing on all + axes when the device powers on. Encoders or endstops must be enabled. It is recommended to make sure homing works properly before enabling this feature. (default: disabled)`); + export const FIND_HOME_ON_BOOT_STALL_DETECTION = + trim(`If stall detection or end-stops are enabled, find the home + position when the device powers on. Warning! This will perform homing + on all axes when the device powers on. Stall detection or endstops + must be enabled. It is recommended to make sure homing works properly + before enabling this feature. (default: disabled)`); + export const STOP_AT_HOME = trim(`Stop at the home location of the axis. (default: disabled)`); @@ -85,18 +99,7 @@ export namespace ToolTips { trim(`Set the length of each axis to provide software limits. Used only if STOP AT MAX is enabled. (default: 0 (disabled))`); - export const TIMEOUT_AFTER = - trim(`Amount of time to wait for a command to execute before stopping. - (default: 120s)`); - // Hardware Settings: Motors - export const MAX_MOVEMENT_RETRIES = - trim(`Number of times to retry a movement before stopping. (default: 3)`); - - export const E_STOP_ON_MOV_ERR = - trim(`Emergency stop if movement is not complete after the maximum - number of retries. (default: disabled)`); - export const MAX_SPEED = trim(`Maximum travel speed after acceleration in millimeters per second. (default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`); @@ -132,18 +135,22 @@ export namespace ToolTips { export const MOTOR_CURRENT = trim(`Motor current in milliamps. (default: 600)`); - export const STALL_SENSITIVITY = - trim(`Motor stall sensitivity. (default: 30)`); - export const ENABLE_X2_MOTOR = trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS. (default: enabled)`); - // Hardware Settings: Encoders and Endstops + // Hardware Settings: Encoders / Stall Detection export const ENABLE_ENCODERS = trim(`Enable use of rotary encoders for stall detection, calibration and homing. (default: enabled)`); + export const ENABLE_STALL_DETECTION = + trim(`Enable use of motor stall detection for detecting missed steps, + calibration and homing. (default: enabled)`); + + export const STALL_SENSITIVITY = + trim(`Motor stall sensitivity. (default: 30)`); + export const ENCODER_POSITIONING = trim(`Use encoders for positioning. (default: disabled)`); @@ -151,17 +158,22 @@ export namespace ToolTips { trim(`Reverse the direction of encoder position reading. (default: disabled)`); - export const MAX_MISSED_STEPS = + export const MAX_MISSED_STEPS_ENCODERS = trim(`Number of steps missed (determined by encoder) before motor is considered to have stalled. (default: 5)`); - export const ENCODER_MISSED_STEP_DECAY = + export const MAX_MISSED_STEPS_STALL_DETECTION = + trim(`Number of steps missed (determined by motor stall detection) before + motor is considered to have stalled. (default: 5)`); + + export const MISSED_STEP_DECAY = trim(`Reduction to missed step total for every good step. (default: 5)`); export const ENCODER_SCALING = trim(`encoder scaling factor = 10000 * (motor resolution * microsteps) / (encoder resolution). (default: 5556 (10000*200/360))`); + // Hardware Settings: Endstops export const ENABLE_ENDSTOPS = trim(`Enable use of electronic end-stops for end detection, calibration and homing. (default: disabled)`); @@ -173,6 +185,18 @@ export namespace ToolTips { trim(`Invert axis end-stops. Enable for normally closed (NC), disable for normally open (NO). (default: disabled)`); + // Hardware Settings: Error Handling + export const TIMEOUT_AFTER = + trim(`Amount of time to wait for a command to execute before stopping. + (default: 120s)`); + + export const MAX_MOVEMENT_RETRIES = + trim(`Number of times to retry a movement before stopping. (default: 3)`); + + export const E_STOP_ON_MOV_ERR = + trim(`Emergency stop if movement is not complete after the maximum + number of retries. (default: disabled)`); + // Hardware Settings: Pin Guard export const PIN_GUARD_PIN_NUMBER = trim(`The number of the pin to guard. This pin will be set to the specified @@ -263,8 +287,12 @@ export namespace ToolTips { export const FIND_HOME = trim(`The Find Home step instructs the device to perform a homing - command (using encoders or endstops) to find and set zero for - the chosen axis or axes.`); + command (using encoders, stall detection, or endstops) to find and set + zero for the chosen axis or axes.`); + + export const CALIBRATE = + trim(`If encoders, stall detection, or end-stops are enabled, + home axis and determine maximum.`); export const IF = trim(`Execute a sequence if a condition is satisfied. If the condition @@ -715,8 +743,8 @@ export namespace Content { export const END_DETECTION_DISABLED = trim(`This command will not execute correctly because you do not have - encoders or endstops enabled for the chosen axis. Enable endstops or - encoders from the Device page for: `); + encoders, stall detection, or endstops enabled for the chosen axis. + Enable endstops, encoders, or stall detection from the Device page for: `); export const IN_USE = trim(`Used in another resource. Protected from deletion.`); diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 8e320263b..59274676d 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -433,14 +433,17 @@ a { } } -.pin-bindings-widget { +.pin-bindings { .fa-exclamation-triangle { color: $orange; + margin-left: 1rem; + margin-top: 0.75rem; } .fa-th-large { + position: absolute; + top: 0.75rem; + left: 0.5rem; color: $dark_gray; - margin-top: 0.5rem; - margin-left: 0.5rem; } .fb-button { &.green { @@ -449,16 +452,27 @@ a { } .bindings-list { margin-bottom: 1rem; + margin-left: 1rem; font-size: 1.2rem; } + .binding-type-dropdown { + margin-bottom: 1.5rem; + } .stock-pin-bindings-button { button { - margin: 0 !important; + margin: 1rem; + float: left; + margin-left: 2rem; } i { margin-right: 0.5rem; } } + .bp3-popover-wrapper { + display: inline; + float: none !important; + margin-left: 1rem; + } } .sensor-history-widget { diff --git a/frontend/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts index ad82f57be..2d0e5a1cd 100644 --- a/frontend/devices/__tests__/actions_test.ts +++ b/frontend/devices/__tests__/actions_test.ts @@ -307,7 +307,7 @@ describe("commandErr()", () => { }); }); -describe("toggleControlPanel()", function () { +describe("toggleControlPanel()", () => { it("toggles", () => { const action = actions.toggleControlPanel("homing_and_calibration"); expect(action.payload).toEqual("homing_and_calibration"); diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index 9fceb9231..9dac417d3 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -161,7 +161,6 @@ export class FarmbotOsSettings controlPanelState={this.props.bot.controlPanelState} dispatch={this.props.dispatch} sourceFbosConfig={sourceFbosConfig} - shouldDisplay={this.props.shouldDisplay} botOnline={botOnline} /> diff --git a/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx b/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx index d993e3dc0..147e2be34 100644 --- a/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx @@ -25,30 +25,25 @@ describe("", () => { const state = fakeState(); state.resources = buildResourceIndex([fakeConfig]); - const fakeProps = (): PowerAndResetProps => { - return { - controlPanelState: panelState(), - dispatch: jest.fn(x => x(jest.fn(), () => state)), - sourceFbosConfig: () => ({ value: true, consistent: true }), - shouldDisplay: jest.fn(), - botOnline: true, - }; - }; + const fakeProps = (): PowerAndResetProps => ({ + controlPanelState: panelState(), + dispatch: jest.fn(x => x(jest.fn(), () => state)), + sourceFbosConfig: () => ({ value: true, consistent: true }), + botOnline: true, + }); - it("open", () => { + it("renders in open state", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = true; const wrapper = mount(); - ["Power and Reset", "Restart", "Shutdown", "Factory Reset", - "Automatic Factory Reset", "Connection Attempt Period", "Change Ownership"] + ["Power and Reset", "Restart", "Shutdown", "Restart Firmware", + "Factory Reset", "Automatic Factory Reset", + "Connection Attempt Period", "Change Ownership"] .map(string => expect(wrapper.text().toLowerCase()) .toContain(string.toLowerCase())); - ["Restart Firmware"] - .map(string => expect(wrapper.text().toLowerCase()) - .not.toContain(string.toLowerCase())); }); - it("closed", () => { + it("renders as closed", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = false; const wrapper = mount(); @@ -73,7 +68,7 @@ describe("", () => { p.sourceFbosConfig = () => ({ value: false, consistent: true }); p.controlPanelState.power_and_reset = true; const wrapper = mount(); - clickButton(wrapper, 3, "yes"); + clickButton(wrapper, 4, "yes"); expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true }); expect(save).toHaveBeenCalledWith(fakeConfig.uuid); }); @@ -81,7 +76,6 @@ describe("", () => { it("restarts firmware", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = true; - p.shouldDisplay = () => true; const wrapper = mount(); expect(wrapper.text().toLowerCase()) .toContain("Restart Firmware".toLowerCase()); diff --git a/frontend/devices/components/fbos_settings/interfaces.ts b/frontend/devices/components/fbos_settings/interfaces.ts index b3b5047e9..ad5714335 100644 --- a/frontend/devices/components/fbos_settings/interfaces.ts +++ b/frontend/devices/components/fbos_settings/interfaces.ts @@ -53,7 +53,6 @@ export interface PowerAndResetProps { controlPanelState: ControlPanelState; dispatch: Function; sourceFbosConfig: SourceFbosConfig; - shouldDisplay: ShouldDisplay; botOnline: boolean; } diff --git a/frontend/devices/components/fbos_settings/power_and_reset.tsx b/frontend/devices/components/fbos_settings/power_and_reset.tsx index f67061e12..e6ac2d437 100644 --- a/frontend/devices/components/fbos_settings/power_and_reset.tsx +++ b/frontend/devices/components/fbos_settings/power_and_reset.tsx @@ -4,23 +4,20 @@ import { Collapse, Popover, Position } from "@blueprintjs/core"; import { FactoryResetRow } from "./factory_reset_row"; import { PowerAndResetProps } from "./interfaces"; import { ChangeOwnershipForm } from "./change_ownership_form"; -import { Feature } from "../../interfaces"; import { FbosButtonRow } from "./fbos_button_row"; import { Content } from "../../../constants"; import { reboot, powerOff, restartFirmware } from "../../actions"; import { t } from "../../../i18next_wrapper"; export function PowerAndReset(props: PowerAndResetProps) { - const { dispatch, sourceFbosConfig, shouldDisplay, botOnline } = props; + const { dispatch, sourceFbosConfig, botOnline } = props; const { power_and_reset } = props.controlPanelState; return
-
-
-
+
- {shouldDisplay(Feature.firmware_restart) && - } + { @@ -27,6 +30,7 @@ export class HardwareSettings extends const { informational_settings } = this.props.bot.hardware; const { sync_status } = informational_settings; const botDisconnected = !isBotOnline(sync_status, botToMqttStatus); + const commonProps = { dispatch, controlPanelState }; return - - - - - + + + ; diff --git a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx index 9d384af5c..f1475c44a 100644 --- a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx @@ -1,22 +1,40 @@ -const mockDevice = { - calibrate: jest.fn(() => Promise.resolve({})) -}; -jest.mock("../../../../device", () => ({ - getDevice: () => (mockDevice) -})); import * as React from "react"; import { mount } from "enzyme"; import { CalibrationRow } from "../calibration_row"; import { bot } from "../../../../__test_support__/fake_state/bot"; +import { CalibrationRowProps } from "../../interfaces"; + +describe("", () => { + const fakeProps = (): CalibrationRowProps => ({ + type: "calibrate", + hardware: bot.hardware.mcu_params, + botDisconnected: false, + action: jest.fn(), + toolTip: "calibrate", + title: "calibrate", + axisTitle: "calibrate", + }); -describe("", () => { it("calls device", () => { - const result = mount(); + const p = fakeProps(); + const result = mount(); + p.hardware.encoder_enabled_x = 1; + p.hardware.encoder_enabled_y = 1; + p.hardware.encoder_enabled_z = 0; [0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click")); - expect(mockDevice.calibrate).toHaveBeenCalledTimes(2); - [{ axis: "y" }, { axis: "x" }].map(x => - expect(mockDevice.calibrate).toHaveBeenCalledWith(x)); + expect(p.action).toHaveBeenCalledTimes(2); + ["y", "x"].map(x => expect(p.action).toHaveBeenCalledWith(x)); + }); + + it("is not disabled", () => { + const p = fakeProps(); + p.type = "zero"; + const result = mount(); + p.hardware.encoder_enabled_x = 0; + p.hardware.encoder_enabled_y = 1; + p.hardware.encoder_enabled_z = 0; + [0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click")); + expect(p.action).toHaveBeenCalledTimes(3); + ["x", "y", "z"].map(x => expect(p.action).toHaveBeenCalledWith(x)); }); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx b/frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx deleted file mode 100644 index 6c70916d5..000000000 --- a/frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react"; -import { mount, shallow } from "enzyme"; -import { EncodersAndEndStops } from "../encoders_and_endstops"; -import { EncodersProps, NumericMCUInputGroupProps } from "../../interfaces"; -import { panelState } from "../../../../__test_support__/control_panel_state"; -import { bot } from "../../../../__test_support__/fake_state/bot"; -import { Dictionary } from "farmbot"; - -describe("", () => { - const mockFeatures: Dictionary = {}; - const fakeProps = (): EncodersProps => ({ - dispatch: jest.fn(), - controlPanelState: panelState(), - sourceFwConfig: x => - ({ value: bot.hardware.mcu_params[x], consistent: true }), - shouldDisplay: jest.fn(key => mockFeatures[key]), - firmwareHardware: undefined, - }); - - it("shows encoder labels", () => { - const p = fakeProps(); - p.firmwareHardware = undefined; - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("encoder"); - expect(wrapper.text().toLowerCase()).not.toContain("stall"); - }); - - it("shows stall labels", () => { - const p = fakeProps(); - p.firmwareHardware = "express_k10"; - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("encoder"); - expect(wrapper.text().toLowerCase()).toContain("stall"); - }); - - it.each<["short" | "long"]>([ - ["short"], - ["long"], - ])("uses %s int scaling factor", (size) => { - mockFeatures.long_scaling_factor = size === "short" ? false : true; - const wrapper = shallow(); - const sfProps = wrapper.find("NumericMCUInputGroup").at(2) - .props() as NumericMCUInputGroupProps; - expect(sfProps.name).toEqual("Encoder Scaling"); - expect(sfProps.intSize).toEqual(size); - }); -}); diff --git a/frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx b/frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx new file mode 100644 index 000000000..d06e176d6 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { Encoders } from "../encoders"; +import { EncodersProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { bot } from "../../../../__test_support__/fake_state/bot"; + +describe("", () => { + const fakeProps = (): EncodersProps => ({ + dispatch: jest.fn(), + controlPanelState: panelState(), + sourceFwConfig: x => + ({ value: bot.hardware.mcu_params[x], consistent: true }), + firmwareHardware: undefined, + }); + + it("shows encoder labels", () => { + const p = fakeProps(); + p.firmwareHardware = undefined; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("encoder"); + expect(wrapper.text().toLowerCase()).not.toContain("stall"); + }); + + it("shows stall labels", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("encoder"); + expect(wrapper.text().toLowerCase()).toContain("stall"); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx b/frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx new file mode 100644 index 000000000..96a2a5b69 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { EndStops } from "../endstops"; +import { EndStopsProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { bot } from "../../../../__test_support__/fake_state/bot"; + +describe("", () => { + const fakeProps = (): EndStopsProps => ({ + dispatch: jest.fn(), + controlPanelState: panelState(), + sourceFwConfig: x => + ({ value: bot.hardware.mcu_params[x], consistent: true }), + }); + + it("shows endstop labels", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("endstop"); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx b/frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx new file mode 100644 index 000000000..aa18b27f2 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx @@ -0,0 +1,47 @@ +jest.mock("../../../../api/crud", () => ({ + edit: jest.fn(), + save: jest.fn(), +})); + +import * as React from "react"; +import { mount } from "enzyme"; +import { ErrorHandling } from "../error_handling"; +import { ErrorHandlingProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { bot } from "../../../../__test_support__/fake_state/bot"; +import { edit, save } from "../../../../api/crud"; +import { fakeState } from "../../../../__test_support__/fake_state"; +import { + fakeFirmwareConfig +} from "../../../../__test_support__/fake_state/resources"; +import { + buildResourceIndex +} from "../../../../__test_support__/resource_index_builder"; + +describe("", () => { + const fakeConfig = fakeFirmwareConfig(); + const state = fakeState(); + state.resources = buildResourceIndex([fakeConfig]); + const fakeProps = (): ErrorHandlingProps => ({ + dispatch: jest.fn(x => x(jest.fn(), () => state)), + controlPanelState: panelState(), + sourceFwConfig: x => + ({ value: bot.hardware.mcu_params[x], consistent: true }), + }); + + it("shows error handling labels", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("error handling"); + }); + + it("toggles retries e-stop parameter", () => { + const p = fakeProps(); + p.controlPanelState.error_handling = true; + p.sourceFwConfig = () => ({ value: 1, consistent: true }); + const wrapper = mount(); + wrapper.find("button").at(0).simulate("click"); + expect(edit).toHaveBeenCalledWith(fakeConfig, { param_e_stop_on_mov_err: 0 }); + expect(save).toHaveBeenCalledWith(fakeConfig.uuid); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx index 6fd42b781..53425a84b 100644 --- a/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx @@ -1,7 +1,17 @@ -jest.mock("../../../actions", () => ({ updateMCU: jest.fn() })); +jest.mock("../../../actions", () => ({ + updateMCU: jest.fn(), + commandErr: jest.fn(), +})); + +const mockDevice = { + calibrate: jest.fn(() => Promise.resolve({})), + findHome: jest.fn(() => Promise.resolve({})), + setZero: jest.fn(() => Promise.resolve({})), +}; +jest.mock("../../../../device", () => ({ getDevice: () => mockDevice })); import * as React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { HomingAndCalibration } from "../homing_and_calibration"; import { bot } from "../../../../__test_support__/fake_state/bot"; import { updateMCU } from "../../../actions"; @@ -10,20 +20,28 @@ import { } from "../../../../__test_support__/fake_state/resources"; import { error, warning } from "../../../../toast/toast"; import { inputEvent } from "../../../../__test_support__/fake_html_events"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { HomingAndCalibrationProps } from "../../interfaces"; +import { CalibrationRow } from "../calibration_row"; describe("", () => { + const fakeProps = (): HomingAndCalibrationProps => ({ + dispatch: jest.fn(), + bot, + controlPanelState: panelState(), + sourceFwConfig: x => ({ + value: bot.hardware.mcu_params[x], consistent: true + }), + firmwareConfig: fakeFirmwareConfig().body, + botDisconnected: false, + firmwareHardware: undefined, + }); + function testAxisLengthInput( provided: string, expected: string | undefined) { - const dispatch = jest.fn(); - bot.controlPanelState.homing_and_calibration = true; - const result = mount( ({ - value: bot.hardware.mcu_params[x], consistent: true - })} - botDisconnected={false} />); + const p = fakeProps(); + p.bot.controlPanelState.homing_and_calibration = true; + const result = mount(); const e = inputEvent(provided); const input = result.find("input").first().props(); input.onChange && input.onChange(e); @@ -45,4 +63,33 @@ describe("", () => { expect(warning).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); }); + + it("finds home", () => { + const wrapper = shallow(); + wrapper.find(CalibrationRow).first().props().action("x"); + expect(mockDevice.findHome).toHaveBeenCalledWith({ + axis: "x", speed: 100 + }); + }); + + it("calibrates", () => { + const wrapper = shallow(); + wrapper.find(CalibrationRow).at(1).props().action("all"); + expect(mockDevice.calibrate).toHaveBeenCalledWith({ axis: "all" }); + }); + + it("sets zero", () => { + const wrapper = shallow(); + wrapper.find(CalibrationRow).last().props().action("all"); + expect(mockDevice.setZero).toHaveBeenCalledWith("all"); + }); + + it("shows express board related labels", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + p.controlPanelState.homing_and_calibration = true; + const wrapper = shallow(); + expect(wrapper.find(CalibrationRow).first().props().toolTip) + .toContain("stall detection"); + }); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx deleted file mode 100644 index 4287bc45f..000000000 --- a/frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -const mockDevice = { - findHome: jest.fn(() => Promise.resolve({})) -}; - -jest.mock("../../../../device", () => ({ - getDevice: () => (mockDevice) -})); -import * as React from "react"; -import { mount } from "enzyme"; -import { HomingRow } from "../homing_row"; -import { bot } from "../../../../__test_support__/fake_state/bot"; - -describe("", () => { - it("renders three buttons", () => { - const wrapper = mount(); - const txt = wrapper.text().toUpperCase(); - ["X", "Y", "Z"].map(function (axis) { - expect(txt).toContain(`HOME ${axis}`); - }); - }); - - it("calls device", () => { - const result = mount(); - [0, 1, 2].map(i => - result.find("LockableButton").at(i).simulate("click")); - [{ axis: "x", speed: 100 }, { axis: "y", speed: 100 }].map(x => - expect(mockDevice.findHome).toHaveBeenCalledWith(x)); - }); -}); diff --git a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx index fb2df2174..7b8032843 100644 --- a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx @@ -37,8 +37,6 @@ describe("", () => { it("renders the base case", () => { const wrapper = render(); ["Enable 2nd X Motor", - "Max Retries", - "E-Stop on Movement Error", "Max Speed (mm/s)" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); @@ -48,16 +46,14 @@ describe("", () => { const p = fakeProps(); p.firmwareHardware = "express_k10"; const wrapper = render(); - expect(wrapper.text()).toContain("Stall"); - expect(wrapper.text()).toContain("Current"); + expect(wrapper.text()).toContain("Motor Current"); }); it("doesn't show TMC parameters", () => { const p = fakeProps(); p.firmwareHardware = "farmduino"; const wrapper = render(); - expect(wrapper.text()).not.toContain("Stall"); - expect(wrapper.text()).not.toContain("Current"); + expect(wrapper.text()).not.toContain("Motor Current"); }); const testParamToggle = ( @@ -72,15 +68,6 @@ describe("", () => { expect(save).toHaveBeenCalledWith(fakeConfig.uuid); }); }; - testParamToggle("toggles retries e-stop parameter", "param_e_stop_on_mov_err", 0); - testParamToggle("toggles enable X2", "movement_secondary_motor_x", 7); - testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 8); - - it("renders TMC params", () => { - const p = fakeProps(); - p.firmwareHardware = "express_k10"; - const wrapper = render(); - expect(wrapper.text()).toContain("Motor Current"); - expect(wrapper.text()).toContain("Stall Sensitivity"); - }); + testParamToggle("toggles enable X2", "movement_secondary_motor_x", 6); + testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 7); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx b/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx new file mode 100644 index 000000000..63b63d18d --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { PinBindings } from "../pin_bindings"; +import { PinBindingsProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { + buildResourceIndex +} from "../../../../__test_support__/resource_index_builder"; + +describe("", () => { + const fakeProps = (): PinBindingsProps => ({ + dispatch: jest.fn(), + controlPanelState: panelState(), + resources: buildResourceIndex([]).index, + }); + + it("shows pin binding labels", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("pin bindings"); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx deleted file mode 100644 index 280578bc4..000000000 --- a/frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -const mockDevice = { - setZero: jest.fn(() => Promise.resolve()) -}; -jest.mock("../../../../device", () => ({ - getDevice: () => (mockDevice) -})); -import * as React from "react"; -import { mount } from "enzyme"; -import { ZeroRow } from "../zero_row"; - -describe("", () => { - it("calls device", () => { - const result = mount(); - [0, 1, 2].map(i => result.find("ZeroButton").at(i).simulate("click")); - ["x", "y", "z"].map(x => - expect(mockDevice.setZero).toHaveBeenCalledWith(x)); - expect(mockDevice.setZero).toHaveBeenCalledTimes(3); - }); -}); diff --git a/frontend/devices/components/hardware_settings/calibration_row.tsx b/frontend/devices/components/hardware_settings/calibration_row.tsx index 62c7cb31c..ab986afec 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -1,19 +1,11 @@ import * as React from "react"; -import { getDevice } from "../../../device"; -import { Axis } from "../../interfaces"; import { LockableButton } from "../lockable_button"; import { axisTrackingStatus } from "../axis_tracking_status"; -import { ToolTips } from "../../../constants"; import { Row, Col, Help } from "../../../ui/index"; import { CalibrationRowProps } from "../interfaces"; -import { commandErr } from "../../actions"; import { t } from "../../../i18next_wrapper"; import { Position } from "@blueprintjs/core"; -const calibrate = (axis: Axis) => getDevice() - .calibrate({ axis }) - .catch(commandErr("Calibration")); - export function CalibrationRow(props: CalibrationRowProps) { const { hardware, botDisconnected } = props; @@ -21,18 +13,20 @@ export function CalibrationRow(props: CalibrationRowProps) { return - + {axisTrackingStatus(hardware) .map(row => { - const { axis, disabled } = row; + const { axis } = row; + const hardwareDisabled = props.type == "zero" ? false : row.disabled; return calibrate(axis)}> - {t("CALIBRATE {{axis}}", { axis })} + disabled={hardwareDisabled || botDisconnected} + onClick={() => props.action(axis)}> + {`${t(props.axisTitle)} ${axis}`} ; })} diff --git a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx b/frontend/devices/components/hardware_settings/encoders.tsx similarity index 55% rename from frontend/devices/components/hardware_settings/encoders_and_endstops.tsx rename to frontend/devices/components/hardware_settings/encoders.tsx index 72bdce981..7addae3ae 100644 --- a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx +++ b/frontend/devices/components/hardware_settings/encoders.tsx @@ -5,41 +5,53 @@ import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { EncodersProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { Feature } from "../../interfaces"; import { t } from "../../../i18next_wrapper"; import { isExpressBoard } from "../firmware_hardware_support"; -export function EncodersAndEndStops(props: EncodersProps) { +export function Encoders(props: EncodersProps) { - const { encoders_and_endstops } = props.controlPanelState; - const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props; + const { encoders } = props.controlPanelState; + const { dispatch, sourceFwConfig, firmwareHardware } = props; const encodersDisabled = { x: !sourceFwConfig("encoder_enabled_x").value, y: !sourceFwConfig("encoder_enabled_y").value, z: !sourceFwConfig("encoder_enabled_z").value }; + const isExpress = isExpressBoard(firmwareHardware); return
- + - {!isExpressBoard(firmwareHardware) && + {isExpress && + } + {!isExpress && } - {!isExpressBoard(firmwareHardware) && + {!isExpress && } - {!isExpressBoard(firmwareHardware) && + {!isExpress && } - - -
; } diff --git a/frontend/devices/components/hardware_settings/endstops.tsx b/frontend/devices/components/hardware_settings/endstops.tsx new file mode 100644 index 000000000..ffa1b5f3d --- /dev/null +++ b/frontend/devices/components/hardware_settings/endstops.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; +import { ToolTips } from "../../../constants"; +import { EndStopsProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { t } from "../../../i18next_wrapper"; + +export function EndStops(props: EndStopsProps) { + + const { endstops } = props.controlPanelState; + const { dispatch, sourceFwConfig } = props; + + return
+
+ + + + + +
; +} diff --git a/frontend/devices/components/hardware_settings/error_handling.tsx b/frontend/devices/components/hardware_settings/error_handling.tsx new file mode 100644 index 000000000..e8d4142f6 --- /dev/null +++ b/frontend/devices/components/hardware_settings/error_handling.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; +import { ToolTips } from "../../../constants"; +import { ErrorHandlingProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { t } from "../../../i18next_wrapper"; +import { McuInputBox } from "../mcu_input_box"; +import { settingToggle } from "../../actions"; +import { SingleSettingRow } from "./single_setting_row"; +import { ToggleButton } from "../../../controls/toggle_button"; + +export function ErrorHandling(props: ErrorHandlingProps) { + + const { error_handling } = props.controlPanelState; + const { dispatch, sourceFwConfig } = props; + const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); + + return
+
+ + + + + + + dispatch( + settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} /> + + +
; +} diff --git a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx index 7775539a8..1602fa351 100644 --- a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx +++ b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx @@ -2,19 +2,23 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { ToolTips } from "../../../constants"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; -import { HomingRow } from "./homing_row"; import { CalibrationRow } from "./calibration_row"; -import { ZeroRow } from "./zero_row"; import { disabledAxisMap } from "../axis_tracking_status"; import { HomingAndCalibrationProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; import { t } from "../../../i18next_wrapper"; import { calculateScale } from "./motors"; +import { isExpressBoard } from "../firmware_hardware_support"; +import { getDevice } from "../../../device"; +import { commandErr } from "../../actions"; +import { CONFIG_DEFAULTS } from "farmbot/dist/config"; export function HomingAndCalibration(props: HomingAndCalibrationProps) { - const { dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected + const { + dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected, + firmwareHardware } = props; const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params; const { homing_and_calibration } = props.bot.controlPanelState; @@ -34,12 +38,43 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { dispatch={dispatch} expanded={homing_and_calibration} /> - - - + getDevice() + .findHome({ speed: CONFIG_DEFAULTS.speed, axis }) + .catch(commandErr("'Find Home' request"))} + hardware={hardware} + botDisconnected={botDisconnected} /> + getDevice().calibrate({ axis }) + .catch(commandErr("Calibration"))} + hardware={hardware} + botDisconnected={botDisconnected} /> + getDevice().setZero(axis) + .catch(commandErr("Zeroing"))} + hardware={hardware} + botDisconnected={botDisconnected} /> -
; } diff --git a/frontend/devices/components/hardware_settings/homing_row.tsx b/frontend/devices/components/hardware_settings/homing_row.tsx deleted file mode 100644 index bac27807c..000000000 --- a/frontend/devices/components/hardware_settings/homing_row.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; -import { HomingRowProps } from "../interfaces"; -import { LockableButton } from "../lockable_button"; -import { axisTrackingStatus } from "../axis_tracking_status"; -import { ToolTips } from "../../../constants"; -import { Row, Col, Help } from "../../../ui/index"; -import { CONFIG_DEFAULTS } from "farmbot/dist/config"; -import { commandErr } from "../../actions"; -import { Axis } from "../../interfaces"; -import { getDevice } from "../../../device"; -import { t } from "../../../i18next_wrapper"; -import { Position } from "@blueprintjs/core"; - -const speed = CONFIG_DEFAULTS.speed; -const findHome = (axis: Axis) => getDevice() - .findHome({ speed, axis }) - .catch(commandErr("'Find Home' request")); - -export function HomingRow(props: HomingRowProps) { - const { hardware, botDisconnected } = props; - - return - - - - - {axisTrackingStatus(hardware) - .map((row) => { - const { axis, disabled } = row; - return - findHome(axis)}> - {t("FIND HOME {{axis}}", { axis })} - - ; - })} - ; -} diff --git a/frontend/devices/components/hardware_settings/motors.tsx b/frontend/devices/components/hardware_settings/motors.tsx index d76d6987e..b6281e751 100644 --- a/frontend/devices/components/hardware_settings/motors.tsx +++ b/frontend/devices/components/hardware_settings/motors.tsx @@ -5,32 +5,14 @@ import { ToggleButton } from "../../../controls/toggle_button"; import { settingToggle } from "../../actions"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { MotorsProps } from "../interfaces"; -import { Row, Col, Help } from "../../../ui/index"; import { Header } from "./header"; -import { Collapse, Position } from "@blueprintjs/core"; -import { McuInputBox } from "../mcu_input_box"; +import { Collapse } from "@blueprintjs/core"; import { t } from "../../../i18next_wrapper"; import { Xyz, McuParamName } from "farmbot"; import { SourceFwConfig } from "../../interfaces"; import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props"; -import { isTMCBoard, isExpressBoard } from "../firmware_hardware_support"; - -const SingleSettingRow = - ({ label, tooltip, settingType, children }: { - label: string, - tooltip: string, - children: React.ReactChild, - settingType: "button" | "input", - }) => - - - - - - {settingType === "button" - ? {children} - : {children}} - ; +import { isTMCBoard } from "../firmware_hardware_support"; +import { SingleSettingRow } from "./single_setting_row"; export const calculateScale = (sourceFwConfig: SourceFwConfig): Record => { @@ -51,13 +33,8 @@ export function Motors(props: MotorsProps) { } = props; const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x"); const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); - const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); const scale = calculateScale(sourceFwConfig); - const encodersDisabled = { - x: !sourceFwConfig("encoder_enabled_x").value, - y: !sourceFwConfig("encoder_enabled_y").value, - z: !sourceFwConfig("encoder_enabled_z").value, - }; + return
- - - - - dispatch( - settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} /> - } - {isExpressBoard(firmwareHardware) && - } diff --git a/frontend/devices/components/hardware_settings/pin_bindings.tsx b/frontend/devices/components/hardware_settings/pin_bindings.tsx new file mode 100644 index 000000000..e7e716c0c --- /dev/null +++ b/frontend/devices/components/hardware_settings/pin_bindings.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { PinBindingsProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { PinBindingsContent } from "../../pin_bindings/pin_bindings"; + +export function PinBindings(props: PinBindingsProps) { + + const { pin_bindings } = props.controlPanelState; + const { dispatch, resources } = props; + + return
+
+ + + +
; +} diff --git a/frontend/devices/components/hardware_settings/single_setting_row.tsx b/frontend/devices/components/hardware_settings/single_setting_row.tsx new file mode 100644 index 000000000..02075c813 --- /dev/null +++ b/frontend/devices/components/hardware_settings/single_setting_row.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { Row, Col, Help } from "../../../ui/index"; +import { Position } from "@blueprintjs/core"; + +export const SingleSettingRow = + ({ label, tooltip, settingType, children }: { + label: string, + tooltip: string, + children: React.ReactChild, + settingType: "button" | "input", + }) => + + + + + + {settingType === "button" + ? {children} + : {children}} + ; diff --git a/frontend/devices/components/hardware_settings/zero_row.tsx b/frontend/devices/components/hardware_settings/zero_row.tsx deleted file mode 100644 index 809fffed5..000000000 --- a/frontend/devices/components/hardware_settings/zero_row.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { getDevice } from "../../../device"; -import { Axis } from "../../interfaces"; -import { ToolTips } from "../../../constants"; -import { Row, Col, Help } from "../../../ui/index"; -import { ZeroRowProps } from "../interfaces"; -import { commandErr } from "../../actions"; -import { t } from "../../../i18next_wrapper"; -import { Position } from "@blueprintjs/core"; - -const zero = - (axis: Axis) => getDevice().setZero(axis).catch(commandErr("Zeroing")); -const AXES: Axis[] = ["x", "y", "z"]; - -export function ZeroButton(props: { axis: Axis; disabled: boolean; }) { - const { axis, disabled } = props; - return ; -} - -export function ZeroRow({ botDisconnected }: ZeroRowProps) { - return - - - - - {AXES.map((axis) => { - return - - ; - })} - ; -} diff --git a/frontend/devices/components/interfaces.ts b/frontend/devices/components/interfaces.ts index 75fd5141e..a76dd30ec 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -1,17 +1,12 @@ import { BotState, Xyz, SourceFwConfig, - ControlPanelState, ShouldDisplay + ControlPanelState, Axis } from "../interfaces"; import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist"; import { IntegerSize } from "../../util"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { ResourceIndex } from "../../resources/interfaces"; -export interface HomingRowProps { - hardware: McuParams; - botDisconnected: boolean; -} - export interface ZeroRowProps { botDisconnected: boolean; } @@ -19,9 +14,11 @@ export interface ZeroRowProps { export interface HomingAndCalibrationProps { dispatch: Function; bot: BotState; + controlPanelState: ControlPanelState; sourceFwConfig: SourceFwConfig; firmwareConfig: FirmwareConfig | undefined; botDisconnected: boolean; + firmwareHardware: FirmwareHardware | undefined; } export interface BooleanMCUInputGroupProps { @@ -39,8 +36,13 @@ export interface BooleanMCUInputGroupProps { } export interface CalibrationRowProps { + type: "find_home" | "calibrate" | "zero"; hardware: McuParams; botDisconnected: boolean; + action(axis: Axis): void; + toolTip: string; + title: string; + axisTitle: string; } export interface NumericMCUInputGroupProps { @@ -85,12 +87,29 @@ export interface MotorsProps { export interface EncodersProps { dispatch: Function; - shouldDisplay: ShouldDisplay; controlPanelState: ControlPanelState; sourceFwConfig: SourceFwConfig; firmwareHardware: FirmwareHardware | undefined; } +export interface EndStopsProps { + dispatch: Function; + controlPanelState: ControlPanelState; + sourceFwConfig: SourceFwConfig; +} + +export interface ErrorHandlingProps { + dispatch: Function; + controlPanelState: ControlPanelState; + sourceFwConfig: SourceFwConfig; +} + +export interface PinBindingsProps { + dispatch: Function; + controlPanelState: ControlPanelState; + resources: ResourceIndex; +} + export interface DangerZoneProps { dispatch: Function; controlPanelState: ControlPanelState; diff --git a/frontend/devices/components/pin_number_dropdown.tsx b/frontend/devices/components/pin_number_dropdown.tsx index a706c1f36..7cb4a454e 100644 --- a/frontend/devices/components/pin_number_dropdown.tsx +++ b/frontend/devices/components/pin_number_dropdown.tsx @@ -56,8 +56,12 @@ const pinNumOrNamedPin = } : pin; +const DISABLE_DDI = (): DropDownItem => ({ + label: t("None"), value: 0 +}); + const listItems = (resources: ResourceIndex): DropDownItem[] => - [...peripheralItems(resources), ...pinDropdowns(n => n)]; + [DISABLE_DDI(), ...peripheralItems(resources), ...pinDropdowns(n => n)]; const peripheralItems = (resources: ResourceIndex): DropDownItem[] => { const list = selectAllSavedPeripherals(resources) diff --git a/frontend/devices/devices.tsx b/frontend/devices/devices.tsx index 1d45a8e46..a0970b240 100644 --- a/frontend/devices/devices.tsx +++ b/frontend/devices/devices.tsx @@ -5,7 +5,6 @@ import { FarmbotOsSettings } from "./components/farmbot_os_settings"; import { Page, Col, Row } from "../ui/index"; import { mapStateToProps } from "./state_to_props"; import { Props } from "./interfaces"; -import { PinBindings } from "./pin_bindings/pin_bindings"; import { getStatus } from "../connectivity/reducer_support"; import { isFwHardwareValue } from "./components/firmware_hardware_support"; @@ -48,9 +47,6 @@ export class RawDevices extends React.Component { firmwareHardware={firmwareHardware} sourceFwConfig={this.props.sourceFwConfig} firmwareConfig={this.props.firmwareConfig} /> - ; diff --git a/frontend/devices/interfaces.ts b/frontend/devices/interfaces.ts index dd24b9a58..8963b9f86 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -245,7 +245,10 @@ export interface HardwareSettingsProps { export interface ControlPanelState { homing_and_calibration: boolean; motors: boolean; - encoders_and_endstops: boolean; + encoders: boolean; + endstops: boolean; + error_handling: boolean; + pin_bindings: boolean; danger_zone: boolean; power_and_reset: boolean; pin_guard: boolean; diff --git a/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx b/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx index 542871140..6742019cf 100644 --- a/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx @@ -1,4 +1,5 @@ -import { sortByNameAndPin, ButtonPin } from "../list_and_label_support"; +import { sortByNameAndPin, ButtonPin, getSpecialActionLabel } from "../list_and_label_support"; +import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources"; describe("sortByNameAndPin()", () => { @@ -26,3 +27,11 @@ describe("sortByNameAndPin()", () => { sortTest(1, 1, Order.equal); // GPIO 1 == GPIO 1 }); }); + +describe("getSpecialActionLabel()", () => { + it("handles undefined values", () => { + expect(getSpecialActionLabel(undefined)).toEqual("None"); + expect(getSpecialActionLabel("wrong" as PinBindingSpecialAction)) + .toEqual(""); + }); +}); diff --git a/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx b/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx index 4d0ac230c..70dcc2781 100644 --- a/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { PinBindings } from "../pin_bindings"; +import { PinBindingsContent } from "../pin_bindings"; import { mount } from "enzyme"; import { bot } from "../../../__test_support__/fake_state/bot"; import { @@ -8,15 +8,15 @@ import { import { fakeSequence, fakePinBinding } from "../../../__test_support__/fake_state/resources"; -import { PinBindingsProps } from "../interfaces"; +import { PinBindingsContentProps } from "../interfaces"; import { SpecialPinBinding, PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources"; -describe("", () => { - function fakeProps(): PinBindingsProps { +describe("", () => { + function fakeProps(): PinBindingsContentProps { const fakeSequence1 = fakeSequence(); fakeSequence1.body.id = 1; fakeSequence1.body.name = "Sequence 1"; @@ -51,8 +51,8 @@ describe("", () => { it("renders", () => { const p = fakeProps(); - const wrapper = mount(); - ["pin bindings", "pin number", "none", "bind", "stock bindings"] + const wrapper = mount(); + ["pin number", "none", "bind", "stock bindings"] .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); ["26", "action"].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); diff --git a/frontend/devices/pin_bindings/interfaces.ts b/frontend/devices/pin_bindings/interfaces.ts index 1370327f2..9f8c195e4 100644 --- a/frontend/devices/pin_bindings/interfaces.ts +++ b/frontend/devices/pin_bindings/interfaces.ts @@ -4,7 +4,7 @@ import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources"; -export interface PinBindingsProps { +export interface PinBindingsContentProps { dispatch: Function; resources: ResourceIndex; } diff --git a/frontend/devices/pin_bindings/list_and_label_support.tsx b/frontend/devices/pin_bindings/list_and_label_support.tsx index 3e6e503e3..09ea49a71 100644 --- a/frontend/devices/pin_bindings/list_and_label_support.tsx +++ b/frontend/devices/pin_bindings/list_and_label_support.tsx @@ -32,9 +32,14 @@ export const specialActionLabelLookup: { [x: string]: string } = { export const specialActionList: DropDownItem[] = Object.values(PinBindingSpecialAction) + .filter(action => action != PinBindingSpecialAction.dump_info) .map((action: PinBindingSpecialAction) => ({ label: specialActionLabelLookup[action], value: action })); +export const getSpecialActionLabel = + (action: PinBindingSpecialAction | undefined) => + specialActionLabelLookup[action || ""] || ""; + /** Pin numbers for standard buttons. */ export enum ButtonPin { estop = 16, diff --git a/frontend/devices/pin_bindings/pin_binding_input_group.tsx b/frontend/devices/pin_bindings/pin_binding_input_group.tsx index 7eb87c550..684318430 100644 --- a/frontend/devices/pin_bindings/pin_binding_input_group.tsx +++ b/frontend/devices/pin_bindings/pin_binding_input_group.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { Row, Col, FBSelect, NULL_CHOICE, DropDownItem } from "../../ui"; +import { Row, Col, FBSelect, DropDownItem } from "../../ui"; import { PinBindingColWidth } from "./pin_bindings"; import { Popover, Position } from "@blueprintjs/core"; import { RpiGpioDiagram } from "./rpi_gpio_diagram"; @@ -13,9 +13,10 @@ import { pinBindingBody } from "./tagged_pin_binding_init"; import { error, warning } from "../../toast/toast"; import { validGpioPins, sysBindings, generatePinLabel, RpiPinList, - bindingTypeLabelLookup, specialActionLabelLookup, specialActionList, + bindingTypeLabelLookup, specialActionList, reservedPiGPIO, - bindingTypeList + bindingTypeList, + getSpecialActionLabel } from "./list_and_label_support"; import { SequenceSelectBox } from "../../sequences/sequence_select_box"; import { ResourceIndex } from "../../resources/interfaces"; @@ -119,8 +120,6 @@ export class PinBindingInputGroup - - {bindingType == PinBindingType.special ? - + void, }) => { const { bindingType, setBindingType } = props; - return ; diff --git a/frontend/devices/pin_bindings/pin_bindings.tsx b/frontend/devices/pin_bindings/pin_bindings.tsx index 76a085c77..f64cb3d88 100644 --- a/frontend/devices/pin_bindings/pin_bindings.tsx +++ b/frontend/devices/pin_bindings/pin_bindings.tsx @@ -1,8 +1,8 @@ import * as React from "react"; -import { Widget, WidgetBody, WidgetHeader, Row, Col } from "../../ui"; +import { Row, Col, Help } from "../../ui"; import { ToolTips } from "../../constants"; import { selectAllPinBindings } from "../../resources/selectors"; -import { PinBindingsProps, PinBindingListItems } from "./interfaces"; +import { PinBindingsContentProps, PinBindingListItems } from "./interfaces"; import { PinBindingsList } from "./pin_bindings_list"; import { PinBindingInputGroup } from "./pin_binding_input_group"; import { @@ -20,9 +20,8 @@ import { t } from "../../i18next_wrapper"; /** Width of UI columns in Pin Bindings widget. */ export enum PinBindingColWidth { pin = 4, - type = 3, - target = 4, - button = 1 + type = 6, + button = 2 } /** Use binding type to return a sequence ID or a special action. */ @@ -64,34 +63,29 @@ const PinBindingsListHeader = () => - - - + ; -export const PinBindings = (props: PinBindingsProps) => { +export const PinBindingsContent = (props: PinBindingsContentProps) => { const { dispatch, resources } = props; const pinBindings = apiPinBindings(resources); - return - + return
+ +
{t(ToolTips.PIN_BINDING_WARNING)}
- - - +
+
{ pinBindings={pinBindings} dispatch={dispatch} resources={resources} /> - - ; +
+
; }; diff --git a/frontend/devices/pin_bindings/pin_bindings_list.tsx b/frontend/devices/pin_bindings/pin_bindings_list.tsx index bee8c844c..431a89388 100644 --- a/frontend/devices/pin_bindings/pin_bindings_list.tsx +++ b/frontend/devices/pin_bindings/pin_bindings_list.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { - bindingTypeLabelLookup, specialActionLabelLookup, - generatePinLabel, sortByNameAndPin + bindingTypeLabelLookup, + generatePinLabel, sortByNameAndPin, getSpecialActionLabel } from "./list_and_label_support"; import { destroy } from "../../api/crud"; import { error } from "../../toast/toast"; @@ -36,12 +36,10 @@ export const PinBindingsList = (props: PinBindingsListProps) => { {generatePinLabel(pin_number)} - {t(bindingTypeLabelLookup[binding_type || ""])} - - + {t(bindingTypeLabelLookup[binding_type || ""])}:  {sequence_id ? findSequenceById(resources, sequence_id).body.name - : t(specialActionLabelLookup[special_action || ""])} + : t(getSpecialActionLabel(special_action))} - {firmwareConfig && - - - - } - -
- -
- - - - - - - - -
+ + + + +
+ +
+ + + + + + + +
; } diff --git a/frontend/devices/components/hardware_settings/export_menu.tsx b/frontend/devices/components/hardware_settings/export_menu.tsx index 67546b166..512e9e2de 100644 --- a/frontend/devices/components/hardware_settings/export_menu.tsx +++ b/frontend/devices/components/hardware_settings/export_menu.tsx @@ -25,7 +25,7 @@ const getSubKeyName = (key: string) => { }; export const FwParamExportMenu = - ({ firmwareConfig }: { firmwareConfig: FirmwareConfig }) => { + ({ firmwareConfig }: { firmwareConfig: FirmwareConfig | undefined }) => { /** Filter out unnecessary parameters. */ const filteredConfig = pickBy(firmwareConfig, (_, key) => !["id", "device_id", "api_migrated", "created_at", "updated_at", diff --git a/frontend/devices/reducer.ts b/frontend/devices/reducer.ts index 2da18b512..256bc1911 100644 --- a/frontend/devices/reducer.ts +++ b/frontend/devices/reducer.ts @@ -11,7 +11,6 @@ import { maybeNegateStatus } from "../connectivity/maybe_negate_status"; import { ReduxAction } from "../redux/interfaces"; import { connectivityReducer, PingResultPayload } from "../connectivity/reducer"; import { versionOK } from "../util"; -import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions"; import { DeepPartial } from "redux"; import { incomingLegacyStatus } from "../connectivity/connect_device"; import { merge } from "lodash"; @@ -205,8 +204,7 @@ function legacyStatusHandler(state: BotState, const nextSyncStatus = maybeNegateStatus(info); - versionOK(informational_settings.controller_version, - EXPECTED_MAJOR, EXPECTED_MINOR); + versionOK(informational_settings.controller_version); state.hardware.informational_settings.sync_status = nextSyncStatus; return state; } diff --git a/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx b/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx index 087b919b1..96df099ea 100644 --- a/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx +++ b/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx @@ -8,7 +8,7 @@ export function FarmBotLayer(props: FarmBotLayerProps) { visible, stopAtHome, botSize, plantAreaOffset, mapTransformProps, peripherals, eStopStatus, botLocationData, getConfigValue } = props; - return visible ? + return visible ? ", () => { const wrapper = svgMount(); expect(wrapper.find("use").props().transform).toEqual(expected); }); + + it("handles bad data", () => { + const p = fakeProps(); + p.pulloutDirection = 1.1; + p.quadrant = 1.1; + const wrapper = svgMount(); + expect(wrapper.find("use").props().transform).toEqual("rotate(0, 10, 20)"); + }); }); describe("", () => { diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts index 130c77f37..41323925d 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts @@ -49,4 +49,8 @@ describe("textAnchorPosition()", () => { expect(textAnchorPosition(4, 3, true)).toEqual(END); expect(textAnchorPosition(4, 4, true)).toEqual(START); }); + + it("handles bad data", () => { + expect(textAnchorPosition(1.1, 1.1, false)).toEqual(START); + }); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx index 4a06683a0..5788588ad 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx @@ -4,6 +4,11 @@ jest.mock("../../../../../history", () => ({ getPathArray: jest.fn(() => { return mockPath.split("/"); }) })); +let mockDev = false; +jest.mock("../../../../../account/dev/dev_support", () => ({ + DevSettings: { futureFeaturesEnabled: () => mockDev } +})); + import * as React from "react"; import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer"; import { @@ -53,6 +58,7 @@ describe("", () => { }); it("navigates to tools page", async () => { + mockDev = true; mockPath = "/app/designer/plants"; const p = fakeProps(); const wrapper = shallow(); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index 1f203c2a1..75451aefc 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -48,10 +48,10 @@ describe("", () => { const p = fakeProps(); p.slot.toolSlot.body.id = 1; const wrapper = svgMount(); - mockDev = false; + mockDev = true; wrapper.find("g").first().simulate("click"); expect(history.push).not.toHaveBeenCalled(); - mockDev = true; + mockDev = false; wrapper.find("g").first().simulate("click"); expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1"); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx index b9a6bb6de..b87288589 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx @@ -180,23 +180,30 @@ export interface GantryToolSlotGraphicProps { xySwap: boolean; } +/** dimensions */ +enum Trough { + width = 20, + length = 45, + wall = 4, +} + export const GantryToolSlot = (props: GantryToolSlotGraphicProps) => { const { x, y, xySwap } = props; - const slotLengthX = xySwap ? 24 : 49; - const slotLengthY = xySwap ? 49 : 24; + const slotLengthX = Trough.wall + (xySwap ? Trough.width : Trough.length); + const slotLengthY = Trough.wall + (xySwap ? Trough.length : Trough.width); return ; }; const SeedTrough = (props: ToolGraphicProps) => { const { x, y, hovered, dispatch, uuid, xySwap } = props; - const slotLengthX = xySwap ? 20 : 45; - const slotLengthY = xySwap ? 45 : 20; + const slotLengthX = xySwap ? Trough.width : Trough.length; + const slotLengthY = xySwap ? Trough.length : Trough.width; return dispatch(setToolHover(uuid))} onMouseLeave={() => dispatch(setToolHover(undefined))}> diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx index 9b12293c8..18d9ff713 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx @@ -39,7 +39,7 @@ export const textAnchorPosition = ( case Anchor.end: return { anchor: "end", x: -40, y: 10 }; case Anchor.middleTop: return { anchor: "middle", x: 0, y: 60 }; case Anchor.middleBottom: return { anchor: "middle", x: 0, y: -40 }; - default: return { anchor: "start", x: 40, y: 10 }; + default: throw new Error("https://xkcd.com/2200"); } }; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx index d222e4dae..d03dcef25 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx @@ -19,7 +19,7 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) { const pathArray = getPathArray(); const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4); const goToToolsPage = () => canClickTool && - !DevSettings.futureFeaturesEnabled() && history.push("/app/tools"); + DevSettings.futureFeaturesEnabled() && history.push("/app/tools"); const { slots, visible, mapTransformProps } = props; const cursor = canClickTool ? "pointer" : "default"; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx index d63554c8c..ddb889b00 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx @@ -43,7 +43,7 @@ export const ToolSlotPoint = (props: TSPProps) => { xySwap, }; return DevSettings.futureFeaturesEnabled() && + onClick={() => !DevSettings.futureFeaturesEnabled() && history.push(`/app/designer/tool-slots/${id}`)}> {pullout_direction && - {DevSettings.futureFeaturesEnabled() && + {!DevSettings.futureFeaturesEnabled() && { function fakeProps() { @@ -39,8 +39,11 @@ describe("PlantGrid", () => { it("saves a grid", async () => { const props = fakeProps(); const pg = mount().instance(); + const oldId = pg.state.gridId; await pg.saveGrid(); - expect(saveGrid).toHaveBeenCalledWith(pg.state.gridId); + expect(saveGrid).toHaveBeenCalledWith(oldId); + expect(success).toHaveBeenCalledWith("16 plants added."); + expect(pg.state.gridId).not.toEqual(oldId); }); it("stashes a grid", async () => { diff --git a/frontend/farm_designer/plants/grid/plant_grid.tsx b/frontend/farm_designer/plants/grid/plant_grid.tsx index 6878640a7..9d83b41f3 100644 --- a/frontend/farm_designer/plants/grid/plant_grid.tsx +++ b/frontend/farm_designer/plants/grid/plant_grid.tsx @@ -9,13 +9,18 @@ import { initPlantGrid } from "./generate_grid"; import { init } from "../../../api/crud"; import { uuid } from "farmbot"; import { saveGrid, stashGrid } from "./thunks"; -import { error } from "../../../toast/toast"; +import { error, success } from "../../../toast/toast"; import { t } from "../../../i18next_wrapper"; import { GridInput } from "./grid_input"; export class PlantGrid extends React.Component { state: PlantGridState = { ...EMPTY_PLANT_GRID, gridId: uuid() }; + get plantCount() { + const { numPlantsH, numPlantsV } = this.state.grid; + return numPlantsH * numPlantsV; + } + onchange = (key: PlantGridKey, val: number) => { const grid = { ...this.state.grid, [key]: val }; this.setState({ grid }); @@ -33,9 +38,7 @@ export class PlantGrid extends React.Component { } performPreview = () => { - const { numPlantsH, numPlantsV } = this.state.grid; - const total = numPlantsH * numPlantsV; - if (total > 100) { + if (this.plantCount > 100) { error(t("Please make a grid with less than 100 plants")); return; } @@ -57,7 +60,10 @@ export class PlantGrid extends React.Component { saveGrid = () => { const p: Promise<{}> = this.props.dispatch(saveGrid(this.state.gridId)); - return p.then(() => this.setState(EMPTY_PLANT_GRID)); + return p.then(() => { + success(t("{{ count }} plants added.", { count: this.plantCount })); + this.setState({ ...EMPTY_PLANT_GRID, gridId: uuid() }); + }); } inputs = () => { @@ -73,16 +79,16 @@ export class PlantGrid extends React.Component { case "clean": return ; case "dirty": return ; } diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx index 19ffd8c27..2d7909c5b 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx @@ -24,6 +24,7 @@ import { import { save, edit } from "../../../api/crud"; import { SpecialStatus } from "farmbot"; import { DEFAULT_CRITERIA } from "../criteria/interfaces"; +import { Content } from "../../../constants"; describe("", () => { const fakeProps = (): GroupDetailActiveProps => { @@ -105,16 +106,23 @@ describe("", () => { }); it("shows paths", () => { - mockDev = true; - const p = fakeProps(); - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("optimized"); - }); - - it("doesn't show paths", () => { mockDev = false; const p = fakeProps(); const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("optimized"); + expect(wrapper.text().toLowerCase()).toContain("0m"); + }); + + it("doesn't show paths", () => { + mockDev = true; + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("0m"); + }); + + it("shows random warning text", () => { + const p = fakeProps(); + p.group.body.sort_type = "random"; + const wrapper = mount(); + expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION); }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx index ffa057ad0..31cecd0ba 100644 --- a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx @@ -1,5 +1,12 @@ jest.mock("../../../api/crud", () => ({ edit: jest.fn() })); +let mockDev = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + import * as React from "react"; import { shallow, mount } from "enzyme"; import { @@ -141,6 +148,7 @@ describe("", () => { p.pathPoints = cases.order.xy_ascending; const wrapper = mount(); expect(wrapper.state().pathData).toEqual(cases.distance); + expect(wrapper.text().toLowerCase()).not.toContain("optimized"); }); it.each<[PointGroupSortType]>([ @@ -154,4 +162,24 @@ describe("", () => { expect(SORT_OPTIONS[sortType](cases.order.xy_ascending)) .toEqual(cases.order[sortType]); }); + + it("renders new sort type", () => { + mockDev = true; + const p = fakeProps(); + const cases = pathTestCases(); + p.pathPoints = cases.order.xy_ascending; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("optimized"); + }); + + it("doesn't generate data twice", () => { + const p = fakeProps(); + const cases = pathTestCases(); + p.pathPoints = cases.order.xy_ascending; + const wrapper = mount(); + expect(wrapper.state().pathData).toEqual(cases.distance); + wrapper.setState({ pathData: { nn: 0 } }); + wrapper.update(); + expect(wrapper.state().pathData).toEqual({ nn: 0 }); + }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx index f87d6cb79..90eba3063 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx @@ -45,7 +45,6 @@ describe("", () => { const p = fakeProps(); p.point = fakePlant(); const i = new PointGroupItem(p); - i.setState = jest.fn(); const fakeImgEvent = imgEvent(); await i.maybeGetCachedIcon(fakeImgEvent); const slug = i.props.point.body.pointer_type === "Plant" ? @@ -55,11 +54,17 @@ describe("", () => { expect(setImgSrc).not.toHaveBeenCalled(); }); + it("sets icon in state", () => { + const i = new PointGroupItem(fakeProps()); + i.setState = jest.fn(); + i.setIconState("fake icon"); + expect(i.setState).toHaveBeenCalledWith({ icon: "fake icon" }); + }); + it("fetches point icon", () => { const p = fakeProps(); p.point = fakePoint(); const i = new PointGroupItem(p); - i.setState = jest.fn(); const fakeImgEvent = imgEvent(); i.maybeGetCachedIcon(fakeImgEvent); expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled(); @@ -71,7 +76,6 @@ describe("", () => { const p = fakeProps(); p.point = fakeToolSlot(); const i = new PointGroupItem(p); - i.setState = jest.fn(); const fakeImgEvent = imgEvent(); i.maybeGetCachedIcon(fakeImgEvent); expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled(); diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx index 9c761fe55..bfd1f1d60 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx @@ -1,14 +1,10 @@ -import * as React from "react"; import { - isSortType, sortTypeChange, SORT_OPTIONS, PointGroupSortSelector, - PointGroupSortSelectorProps + isSortType, sortTypeChange, SORT_OPTIONS } from "../point_group_sort_selector"; import { DropDownItem } from "../../../ui"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { TaggedPoint } from "farmbot"; import { fakePlant } from "../../../__test_support__/fake_state/resources"; -import { mount } from "enzyme"; -import { Content } from "../../../constants"; const tests: [string, boolean][] = [ ["", false], @@ -89,15 +85,3 @@ describe("sort()", () => { expect(results).toEqual(["C", "D", "B", "A"]); }); }); - -describe("", () => { - const fakeProps = (): PointGroupSortSelectorProps => ({ - onChange: jest.fn(), - value: "random", - }); - - it("shows random warning text", () => { - const wrapper = mount(); - expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION); - }); -}); diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index b7b9251c4..b56ed3e71 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -97,9 +97,24 @@ export class GroupDetailActive defaultValue={group.body.name} onChange={this.update} onBlur={this.saveGroup} /> - +
+ + {!DevSettings.futureFeaturesEnabled() + ? p.body.id))} + pathPoints={this.pointsSelectedByGroup} + dispatch={dispatch} + group={group} /> + : } +

+ {group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)} +

+
@@ -117,11 +132,6 @@ export class GroupDetailActive {this.props.shouldDisplay(Feature.criteria_groups) && } - {DevSettings.futureFeaturesEnabled() && - } ({ x: point.body.x, y: point.body.y }); @@ -66,7 +67,8 @@ export const PathInfoBar = (props: PathInfoBarProps) => { const normalizedLength = pathLength / maxLength * 100; const sortLabel = sortTypeKey == "nn" ? "Optimized" : sortOptionsTable()[sortTypeKey]; - return
dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })} onMouseLeave={() => @@ -74,9 +76,11 @@ export const PathInfoBar = (props: PathInfoBarProps) => { onClick={() => sortTypeKey == "nn" ? error(t("Not supported yet.")) - : dispatch(edit(group, { sort_type: sortTypeKey }))} - style={{ width: `${normalizedLength}%` }}> - {`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`} + : dispatch(edit(group, { sort_type: sortTypeKey }))}> +
+ {`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`} +
; }; @@ -101,15 +105,17 @@ export class Paths extends React.Component { }; render() { - if (!this.state.pathData.nn) { this.generatePathData(this.props.pathPoints); } - return
- - {SORT_TYPES.concat("nn").map(st => - )} + if (!isNumber(this.state.pathData.nn)) { + this.generatePathData(this.props.pathPoints); + } + return
+ {SORT_TYPES.concat(DevSettings.futureFeaturesEnabled() ? "nn" : []) + .map(sortType => + )}
; } } diff --git a/frontend/farm_designer/point_groups/point_group_item.tsx b/frontend/farm_designer/point_groups/point_group_item.tsx index 8d6d7b101..cee43c163 100644 --- a/frontend/farm_designer/point_groups/point_group_item.tsx +++ b/frontend/farm_designer/point_groups/point_group_item.tsx @@ -57,6 +57,8 @@ export class PointGroupItem this.leave(); } + setIconState = (icon: string) => this.setState({ icon }); + get criteriaIcon() { return !this.props.group.body.point_ids .includes(this.props.point.body.id || 0); @@ -67,7 +69,7 @@ export class PointGroupItem switch (this.props.point.body.pointer_type) { case "Plant": const slug = this.props.point.body.openfarm_slug; - maybeGetCachedPlantIcon(slug, img, icon => this.setState({ icon })); + maybeGetCachedPlantIcon(slug, img, this.setIconState); break; case "GenericPointer": const { color } = this.props.point.body.meta; diff --git a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx b/frontend/farm_designer/point_groups/point_group_sort_selector.tsx index 5c1d6a586..e1c335dd9 100644 --- a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx +++ b/frontend/farm_designer/point_groups/point_group_sort_selector.tsx @@ -3,7 +3,6 @@ import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { FBSelect, DropDownItem } from "../../ui"; import { t } from "../../i18next_wrapper"; import { shuffle, sortBy } from "lodash"; -import { Content } from "../../constants"; import { TaggedPoint } from "farmbot"; export interface PointGroupSortSelectorProps { @@ -42,22 +41,11 @@ export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => { }; export function PointGroupSortSelector(p: PointGroupSortSelectorProps) { - - return
-
- -
- -

- {(p.value == "random") ? t(Content.SORT_DESCRIPTION) : ""} -

-
; + return ; } type Sorter = (p: TaggedPoint[]) => TaggedPoint[]; diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx index 3f6e93f7c..e982afc55 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx @@ -11,6 +11,7 @@ import { fakeState } from "../../../__test_support__/fake_state"; import { SaveBtn } from "../../../ui"; import { initSave } from "../../../api/crud"; import { history } from "../../../history"; +import { error } from "../../../toast/toast"; describe("", () => { const fakeProps = (): AddToolProps => ({ @@ -37,10 +38,19 @@ describe("", () => { expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" }); }); - it("adds stock tools", () => { + it("doesn't add stock tools", () => { const wrapper = mount(); wrapper.find("button").last().simulate("click"); - expect(initSave).toHaveBeenCalledTimes(6); + expect(error).toHaveBeenCalledWith("Please choose a FarmBot model."); + expect(initSave).not.toHaveBeenCalledTimes(6); + expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools"); + }); + + it("adds stock tools", () => { + const wrapper = mount(); + wrapper.setState({ model: "express" }); + wrapper.find("button").last().simulate("click"); + expect(initSave).toHaveBeenCalledTimes(2); expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); }); diff --git a/frontend/farm_designer/tools/add_tool.tsx b/frontend/farm_designer/tools/add_tool.tsx index b95508245..2eba033c9 100644 --- a/frontend/farm_designer/tools/add_tool.tsx +++ b/frontend/farm_designer/tools/add_tool.tsx @@ -5,11 +5,20 @@ import { } from "../designer_panel"; import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; -import { SaveBtn } from "../../ui"; +import { SaveBtn, FBSelect, DropDownItem } from "../../ui"; import { SpecialStatus } from "farmbot"; import { initSave } from "../../api/crud"; import { Panel } from "../panel_header"; import { history } from "../../history"; +import { error } from "../../toast/toast"; + +enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" } + +const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ + [Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 }, + [Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 }, + [Model.express]: { label: t("Express"), value: Model.express }, +}); export interface AddToolProps { dispatch: Function; @@ -17,6 +26,7 @@ export interface AddToolProps { export interface AddToolState { toolName: string; + model: Model | undefined; } export const mapStateToProps = (props: Everything): AddToolProps => ({ @@ -24,7 +34,7 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({ }); export class RawAddTool extends React.Component { - state: AddToolState = { toolName: "" }; + state: AddToolState = { toolName: "", model: undefined }; newTool = (name: string) => { this.props.dispatch(initSave("Tool", { name })); @@ -35,28 +45,60 @@ export class RawAddTool extends React.Component { history.push("/app/designer/tools"); } - get stockToolNames() { - return [ - t("Seeder"), - t("Watering Nozzle"), - t("Weeder"), - t("Soil Sensor"), - t("Seed Bin"), - t("Seed Tray"), - ]; + stockToolNames = (model: Model) => { + switch (model) { + case Model.genesis14: + return [ + t("Seeder"), + t("Watering Nozzle"), + t("Weeder"), + t("Soil Sensor"), + t("Seed Bin"), + t("Seed Tray"), + ]; + case Model.genesis15: + return [ + t("Seeder"), + t("Watering Nozzle"), + t("Weeder"), + t("Soil Sensor"), + t("Seed Bin"), + t("Seed Tray"), + t("Seed Trough 1"), + t("Seed Trough 2"), + ]; + case Model.express: + return [ + t("Seed Trough 1"), + t("Seed Trough 2"), + ]; + } } AddStockTools = () =>
-
    - {this.stockToolNames.map(n =>
  • {n}
  • )} -
+ this.setState({ model: ddi.value as Model })} + /> + {this.state.model && +
    + {this.stockToolNames(this.state.model).map(n =>
  • {n}
  • )} +
}
- InactiveTools = () => -
- + Tools = () => +
+
+ + +
+ +
+ +
{this.props.tools .filter(tool => !tool.body.name || tool.body.name && tool.body.name.toLowerCase() .includes(this.state.searchTerm.toLowerCase())) - .filter(tool => tool.body.status === "inactive") .map(tool => { render() { const panelName = "tools"; + const hasTools = this.props.tools.length > 0; return + linkTo={!hasTools ? "/app/designer/tools/add" : undefined} + title={!hasTools ? t("Add tool") : undefined}> 0} + notEmpty={hasTools} graphic={EmptyStateGraphic.tools} title={t("Add a tool")} text={Content.NO_TOOLS} colorScheme={"tools"}> - + ; diff --git a/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx b/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx index 577f65133..afdfe119e 100644 --- a/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx +++ b/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx @@ -3,6 +3,13 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); jest.mock("../actions", () => ({ scanImage: jest.fn() })); jest.mock("../../images/actions", () => ({ selectImage: jest.fn() })); +let mockDev = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + import * as React from "react"; import { mount, shallow } from "enzyme"; import { CameraCalibration } from "../camera_calibration"; @@ -12,6 +19,7 @@ import { selectImage } from "../../images/actions"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { error } from "../../../toast/toast"; import { Content, ToolTips } from "../../../constants"; +import { SPECIAL_VALUES } from "../../weed_detector/remote_env/constants"; describe("", () => { const fakeProps = (): CameraCalibrationProps => ({ @@ -116,4 +124,21 @@ describe("", () => { expect(error).toHaveBeenCalledWith( ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED); }); + + it("toggles simple version", () => { + mockDev = true; + const p = fakeProps(); + const wrapper = mount(); + wrapper.find("input").first().simulate("change"); + expect(mockDevice.setUserEnv).toHaveBeenCalledWith({ + CAMERA_CALIBRATION_easy_calibration: "\"FALSE\"" + }); + }); + + it("renders simple version", () => { + const p = fakeProps(); + p.wDEnv = { CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.TRUE }; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("blur"); + }); }); diff --git a/frontend/farmware/camera_calibration/camera_calibration.tsx b/frontend/farmware/camera_calibration/camera_calibration.tsx index 6654eac51..515a7066c 100644 --- a/frontend/farmware/camera_calibration/camera_calibration.tsx +++ b/frontend/farmware/camera_calibration/camera_calibration.tsx @@ -8,7 +8,7 @@ import { selectImage } from "../images/actions"; import { calibrate, scanImage } from "./actions"; import { envGet } from "../weed_detector/remote_env/selectors"; import { MustBeOnline, isBotOnline } from "../../devices/must_be_online"; -import { WeedDetectorConfig } from "../weed_detector/config"; +import { WeedDetectorConfig, BoolConfig } from "../weed_detector/config"; import { Feature } from "../../devices/interfaces"; import { namespace } from "../weed_detector"; import { t } from "../../i18next_wrapper"; @@ -16,6 +16,10 @@ import { formatEnvKey } from "../weed_detector/remote_env/translators"; import { cameraBtnProps } from "../../devices/components/fbos_settings/camera_selection"; +import { ImageFlipper } from "../images/image_flipper"; +import { PhotoFooter } from "../images/photos"; +import { UUID } from "../../resources/interfaces"; +import { DevSettings } from "../../account/dev/dev_support"; export class CameraCalibration extends React.Component { @@ -31,9 +35,11 @@ export class CameraCalibration extends key, JSON.stringify(formatEnvKey(key, value)))) : envSave(key, value) + onFlip = (uuid: UUID) => this.props.dispatch(selectImage(uuid)); + render() { const camDisabled = cameraBtnProps(this.props.env); - return
+ return
- - } + {!!envGet(this.namespace("easy_calibration"), this.props.wDEnv) + ?
+ + +
+ : this.props.dispatch(scanImage(id))} - onFlip={uuid => this.props.dispatch(selectImage(uuid))} + onFlip={this.onFlip} images={this.props.images} currentImage={this.props.currentImage} onChange={this.change} @@ -73,11 +91,10 @@ export class CameraCalibration extends S_HI={this.props.S_HI} V_HI={this.props.V_HI} invertHue={!!envGet(this.namespace("invert_hue_selection"), - this.props.wDEnv)} /> - -
+ this.props.wDEnv)} />} +
; diff --git a/frontend/farmware/weed_detector/__tests__/config_test.tsx b/frontend/farmware/weed_detector/__tests__/config_test.tsx index 19495ebfb..1bd300793 100644 --- a/frontend/farmware/weed_detector/__tests__/config_test.tsx +++ b/frontend/farmware/weed_detector/__tests__/config_test.tsx @@ -31,18 +31,6 @@ describe("", () => { expect(badChange).toThrow("Weed detector got a non-numeric value"); }); - it("changes hue invert value", () => { - const p = fakeProps(); - const wrapper = shallow(); - const input = wrapper.find("input").first(); - input.simulate("change", { currentTarget: { checked: true } }); - expect(p.onChange).toHaveBeenCalledWith( - "CAMERA_CALIBRATION_invert_hue_selection", 1); - input.simulate("change", { currentTarget: { checked: false } }); - expect(p.onChange).toHaveBeenCalledWith( - "CAMERA_CALIBRATION_invert_hue_selection", 0); - }); - it("changes number value", () => { const p = fakeProps(); const wrapper = shallow(); diff --git a/frontend/farmware/weed_detector/config.tsx b/frontend/farmware/weed_detector/config.tsx index 9d775c606..3383c3b32 100644 --- a/frontend/farmware/weed_detector/config.tsx +++ b/frontend/farmware/weed_detector/config.tsx @@ -15,6 +15,9 @@ import { isNumber } from "lodash"; import { t } from "../../i18next_wrapper"; export class WeedDetectorConfig extends React.Component { + getValue(conf: keyof WD_ENV) { return envGet(conf, this.props.values); } + get simple() { return !!this.getValue("CAMERA_CALIBRATION_easy_calibration"); } + NumberBox = ({ conf, label }: { conf: keyof WD_ENV; label: string; @@ -25,7 +28,7 @@ export class WeedDetectorConfig extends React.Component { this.props.onChange(conf, parseFloat(e.currentTarget.value))} placeholder={label} /> @@ -40,57 +43,48 @@ export class WeedDetectorConfig extends React.Component { } }; - find = (needle: keyof WD_ENV): DropDownItem => { - const wow = envGet(needle, this.props.values); - const ok = SPECIAL_VALUE_DDI[wow]; - return ok || NULL_CHOICE; - }; + find = (conf: keyof WD_ENV): DropDownItem => + SPECIAL_VALUE_DDI[this.getValue(conf)] || NULL_CHOICE render() { - return
- -
- - this.props.onChange("CAMERA_CALIBRATION_invert_hue_selection", - e.currentTarget.checked ? - SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} /> -
- - - - - + return
+ {!this.simple && +
+ - - - - - - - + conf={"CAMERA_CALIBRATION_calibration_object_separation"} + label={t(`Calibration Object Separation`)} /> + + + + + + + + + + + + +
} {
; } } + +export interface BoolConfigProps { + configKey: keyof WD_ENV; + label: string; + wDEnv: Partial; + onChange(key: keyof WD_ENV, value: number): void; +} + +export const BoolConfig = (props: BoolConfigProps) => +
+ + + props.onChange(props.configKey, + e.currentTarget.checked ? + SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} /> +
; diff --git a/frontend/farmware/weed_detector/index.tsx b/frontend/farmware/weed_detector/index.tsx index 23da0c179..aa4d8c8db 100644 --- a/frontend/farmware/weed_detector/index.tsx +++ b/frontend/farmware/weed_detector/index.tsx @@ -79,29 +79,24 @@ export class WeedDetector
- - this.props.dispatch(scanImage(id))} - onFlip={uuid => this.props.dispatch(selectImage(uuid))} - currentImage={this.props.currentImage} - images={this.props.images} - onChange={this.change} - timeSettings={this.props.timeSettings} - iteration={wDEnvGet(this.namespace("iteration"))} - morph={wDEnvGet(this.namespace("morph"))} - blur={wDEnvGet(this.namespace("blur"))} - H_LO={wDEnvGet(this.namespace("H_LO"))} - H_HI={wDEnvGet(this.namespace("H_HI"))} - S_LO={wDEnvGet(this.namespace("S_LO"))} - S_HI={wDEnvGet(this.namespace("S_HI"))} - V_LO={wDEnvGet(this.namespace("V_LO"))} - V_HI={wDEnvGet(this.namespace("V_HI"))} /> - + this.props.dispatch(scanImage(id))} + onFlip={uuid => this.props.dispatch(selectImage(uuid))} + currentImage={this.props.currentImage} + images={this.props.images} + onChange={this.change} + timeSettings={this.props.timeSettings} + iteration={wDEnvGet(this.namespace("iteration"))} + morph={wDEnvGet(this.namespace("morph"))} + blur={wDEnvGet(this.namespace("blur"))} + H_LO={wDEnvGet(this.namespace("H_LO"))} + H_HI={wDEnvGet(this.namespace("H_HI"))} + S_LO={wDEnvGet(this.namespace("S_LO"))} + S_HI={wDEnvGet(this.namespace("S_HI"))} + V_LO={wDEnvGet(this.namespace("V_LO"))} + V_HI={wDEnvGet(this.namespace("V_HI"))} />
; diff --git a/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts b/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts index 3e9696200..825ee5f38 100644 --- a/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts +++ b/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts @@ -40,6 +40,11 @@ describe("formatEnvKey()", () => { v: SPECIAL_VALUES.FALSE, r: "FALSE" }, + { + k: "CAMERA_CALIBRATION_easy_calibration", + v: SPECIAL_VALUES.FALSE, + r: "FALSE" + }, { k: "CAMERA_CALIBRATION_calibration_along_axis", v: SPECIAL_VALUES.X, diff --git a/frontend/farmware/weed_detector/remote_env/constants.ts b/frontend/farmware/weed_detector/remote_env/constants.ts index 762fbb480..1c9dced3c 100644 --- a/frontend/farmware/weed_detector/remote_env/constants.ts +++ b/frontend/farmware/weed_detector/remote_env/constants.ts @@ -25,6 +25,7 @@ export const WD_KEY_DEFAULTS = { CAMERA_CALIBRATION_calibration_along_axis: SPECIAL_VALUES.X, CAMERA_CALIBRATION_image_bot_origin_location: SPECIAL_VALUES.BOTTOM_LEFT, CAMERA_CALIBRATION_invert_hue_selection: SPECIAL_VALUES.TRUE, + CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.FALSE, CAMERA_CALIBRATION_blur: 5, CAMERA_CALIBRATION_calibration_object_separation: 100, CAMERA_CALIBRATION_camera_offset_x: 50, @@ -61,6 +62,7 @@ export const DEFAULT_FORMATTER: Translation = { case "CAMERA_CALIBRATION_calibration_along_axis": case "CAMERA_CALIBRATION_image_bot_origin_location": case "CAMERA_CALIBRATION_invert_hue_selection": + case "CAMERA_CALIBRATION_easy_calibration": return ("" + (SPECIAL_VALUES[val] || val)); default: return val; diff --git a/frontend/logs/__tests__/state_to_props_test.ts b/frontend/logs/__tests__/state_to_props_test.ts index 96d72a6a2..646a76860 100644 --- a/frontend/logs/__tests__/state_to_props_test.ts +++ b/frontend/logs/__tests__/state_to_props_test.ts @@ -24,24 +24,10 @@ describe("mapStateToProps()", () => { state.bot.hardware.configuration.sequence_init_log = false; const fakeApiConfig = fakeFbosConfig(); fakeApiConfig.body.sequence_init_log = true; - fakeApiConfig.body.api_migrated = true; state.resources = buildResourceIndex([fakeApiConfig]); const props = mapStateToProps(state); expect(props.sourceFbosConfig("sequence_init_log")).toEqual({ value: true, consistent: false }); }); - - it("bot source of FBOS settings", () => { - const state = fakeState(); - state.bot.hardware.configuration.sequence_init_log = false; - const fakeApiConfig = fakeFbosConfig(); - fakeApiConfig.body.sequence_init_log = true; - fakeApiConfig.body.api_migrated = false; - state.resources = buildResourceIndex([fakeApiConfig]); - const props = mapStateToProps(state); - expect(props.sourceFbosConfig("sequence_init_log")).toEqual({ - value: false, consistent: true - }); - }); }); diff --git a/frontend/messages/__tests__/state_to_props_test.ts b/frontend/messages/__tests__/state_to_props_test.ts index 9c38170cf..bb509a482 100644 --- a/frontend/messages/__tests__/state_to_props_test.ts +++ b/frontend/messages/__tests__/state_to_props_test.ts @@ -18,7 +18,6 @@ describe("mapStateToProps()", () => { it("returns firmware value", () => { const state = fakeState(); const fbosConfig = fakeFbosConfig(); - fbosConfig.body.api_migrated = true; fbosConfig.body.firmware_hardware = "arduino"; state.resources = buildResourceIndex([fbosConfig]); const props = mapStateToProps(state); diff --git a/frontend/nav/__tests__/nav_links_test.tsx b/frontend/nav/__tests__/nav_links_test.tsx index 31d7eeb71..3550df51c 100644 --- a/frontend/nav/__tests__/nav_links_test.tsx +++ b/frontend/nav/__tests__/nav_links_test.tsx @@ -28,7 +28,7 @@ describe("", () => { }); it("shows links", () => { - mockDev = true; + mockDev = false; const wrapper = mount(); expect(wrapper.text().toLowerCase()).not.toContain("tools"); }); diff --git a/frontend/nav/nav_links.tsx b/frontend/nav/nav_links.tsx index e05777dab..e4d7bc76c 100644 --- a/frontend/nav/nav_links.tsx +++ b/frontend/nav/nav_links.tsx @@ -37,7 +37,7 @@ export const getLinks = (): NavLinkParams[] => betterCompact([ name: "Regimens", icon: "calendar-check-o", slug: "regimens", computeHref: computeEditorUrlFromState("Regimen") }, - DevSettings.futureFeaturesEnabled() ? undefined : + !DevSettings.futureFeaturesEnabled() ? undefined : { name: "Tools", icon: "wrench", slug: "tools" }, { name: "Farmware", icon: "crosshairs", slug: "farmware", diff --git a/frontend/redux/upgrade_reminder.ts b/frontend/redux/upgrade_reminder.ts index f74a2d542..7f97d0d2f 100644 --- a/frontend/redux/upgrade_reminder.ts +++ b/frontend/redux/upgrade_reminder.ts @@ -1,10 +1,10 @@ import { info } from "../toast/toast"; -import { semverCompare, SemverResult, MinVersionOverride } from "../util"; +import { semverCompare, SemverResult, FbosVersionFallback } from "../util"; import { Content } from "../constants"; import { Dictionary } from "lodash"; const IDEAL_VERSION = - globalConfig.FBOS_END_OF_LIFE_VERSION || MinVersionOverride.ALWAYS; + globalConfig.FBOS_END_OF_LIFE_VERSION || FbosVersionFallback.NULL; /** Returns a function that, when given a version string, (possibly) warns the * user to upgrade FBOS versions before it hits end of life. */ @@ -12,8 +12,8 @@ export function createReminderFn() { /** FBOS Version can change during the app lifecycle. We only want one * reminder per FBOS version change. */ const alreadyChecked: Dictionary = { - // Dont bother when the user is offline. - [MinVersionOverride.ALWAYS]: true + // Don't bother when the user is offline. + [FbosVersionFallback.NULL]: true }; return function reminder(version: string) { diff --git a/frontend/redux/version_tracker_middleware.ts b/frontend/redux/version_tracker_middleware.ts index 4af0b8aed..1187b4fe5 100644 --- a/frontend/redux/version_tracker_middleware.ts +++ b/frontend/redux/version_tracker_middleware.ts @@ -1,5 +1,5 @@ import { EnvName } from "./interfaces"; -import { determineInstalledOsVersion, MinVersionOverride } from "../util/index"; +import { determineInstalledOsVersion, FbosVersionFallback } from "../util/index"; import { maybeGetDevice } from "../resources/selectors"; import { MW } from "./middlewares"; import { Everything } from "../interfaces"; @@ -11,10 +11,10 @@ const maybeRemindUserToUpdate = createReminderFn(); function getVersionFromState(state: Everything) { const device = maybeGetDevice(state.resources.index); - const v = - determineInstalledOsVersion(state.bot, device) || MinVersionOverride.ALWAYS; - maybeRemindUserToUpdate(v); - return v; + const version = determineInstalledOsVersion(state.bot, device) + || FbosVersionFallback.NULL; + maybeRemindUserToUpdate(version); + return version; } const fn: MW = diff --git a/frontend/util/__tests__/version_test.ts b/frontend/util/__tests__/version_test.ts index 28e9a1e56..e58346c44 100644 --- a/frontend/util/__tests__/version_test.ts +++ b/frontend/util/__tests__/version_test.ts @@ -5,6 +5,7 @@ import { createShouldDisplayFn, determineInstalledOsVersion, versionOK, + MinVersionOverride, } from "../version"; import { bot } from "../../__test_support__/fake_state/bot"; import { fakeDevice } from "../../__test_support__/resource_index_builder"; @@ -128,6 +129,10 @@ describe("shouldDisplay()", () => { expect(createShouldDisplayFn("10.0.0", { jest_feature: "1.0.0" }, undefined)( Feature.jest_feature)).toBeTruthy(); + globalConfig.FBOS_END_OF_LIFE_VERSION = MinVersionOverride.NEVER; + expect(createShouldDisplayFn(undefined, fakeMinOsData, undefined)( + Feature.jest_feature)).toBeTruthy(); + delete globalConfig.FBOS_END_OF_LIFE_VERSION; }); it("shouldn't display", () => { @@ -179,14 +184,20 @@ describe("determineInstalledOsVersion()", () => { describe("versionOK()", () => { it("checks if major/minor version meets min requirement", () => { - expect(versionOK("9.1.9-rc99", 3, 0)).toBeTruthy(); - expect(versionOK("3.0.9-rc99", 3, 0)).toBeTruthy(); - expect(versionOK("4.0.0", 3, 0)).toBeTruthy(); - expect(versionOK("4.0.0", 3, 1)).toBeTruthy(); - expect(versionOK("3.1.0", 3, 0)).toBeTruthy(); - expect(versionOK("2.0.-", 3, 0)).toBeFalsy(); - expect(versionOK("2.9.4", 3, 0)).toBeFalsy(); - expect(versionOK("1.9.6", 3, 0)).toBeFalsy(); - expect(versionOK("3.1.6", 4, 0)).toBeFalsy(); + globalConfig.MINIMUM_FBOS_VERSION = "3.0.0"; + expect(versionOK("9.1.9-rc99")).toBeTruthy(); + expect(versionOK("3.0.9-rc99")).toBeTruthy(); + expect(versionOK("4.0.0")).toBeTruthy(); + expect(versionOK("3.1.0")).toBeTruthy(); + expect(versionOK("2.0.-")).toBeFalsy(); + expect(versionOK("2.9.4")).toBeFalsy(); + expect(versionOK("1.9.6")).toBeFalsy(); + globalConfig.MINIMUM_FBOS_VERSION = "3.1.0"; + expect(versionOK("4.0.0")).toBeTruthy(); + globalConfig.MINIMUM_FBOS_VERSION = "4.0.0"; + expect(versionOK("3.1.6")).toBeFalsy(); + delete globalConfig.MINIMUM_FBOS_VERSION; + expect(versionOK("5.0.0")).toBeFalsy(); + expect(versionOK("7.0.0")).toBeTruthy(); }); }); diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 247a785e1..33357bb03 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -183,9 +183,7 @@ export function validBotLocationData( */ export function validFwConfig(config: TaggedFirmwareConfig | undefined): TaggedFirmwareConfig["body"] | undefined { - return (config?.body.api_migrated) - ? config.body - : undefined; + return config ? config.body : undefined; } /** @@ -193,9 +191,7 @@ export function validFwConfig(config: TaggedFirmwareConfig | undefined): */ export function validFbosConfig( config: TaggedFbosConfig | undefined): TaggedFbosConfig["body"] | undefined { - return (config?.body.api_migrated) - ? config.body - : undefined; + return config ? config.body : undefined; } interface BetterUUID { diff --git a/frontend/util/version.ts b/frontend/util/version.ts index 1a4c6082d..b6178b7d3 100644 --- a/frontend/util/version.ts +++ b/frontend/util/version.ts @@ -97,10 +97,13 @@ export function minFwVersionCheck(current: string | undefined, min: string) { * for shouldDisplay() */ export enum MinVersionOverride { - ALWAYS = "0.0.0", NEVER = "999.999.999", } +export enum FbosVersionFallback { + NULL = "0.0.0", +} + /** * Determine whether a feature should be displayed based on * the user's current FBOS version. Min FBOS version feature data is pulled @@ -114,19 +117,18 @@ export function createShouldDisplayFn( lookupData: MinOsFeatureLookup | undefined, override: string | undefined) { return function (feature: Feature): boolean { - const target = override || current; - if (isString(target)) { - const table = lookupData || {}; - const min = table[feature] || MinVersionOverride.NEVER; - switch (semverCompare(target, min)) { - case SemverResult.LEFT_IS_GREATER: - case SemverResult.EQUAL: - return true; - default: - return false; - } + const fallback = globalConfig.FBOS_END_OF_LIFE_VERSION || + FbosVersionFallback.NULL; + const target = override || current || fallback; + const table = lookupData || {}; + const min = table[feature] || MinVersionOverride.NEVER; + switch (semverCompare(target, min)) { + case SemverResult.LEFT_IS_GREATER: + case SemverResult.EQUAL: + return true; + default: + return false; } - return false; }; } @@ -147,6 +149,9 @@ export function determineInstalledOsVersion( } } +const parseVersion = (version: string) => + version.split(".").map(x => parseInt(x, 10)); + /** * Compare installed FBOS version against the lowest version compatible * with the web app to lock out incompatible FBOS versions from the App. @@ -155,20 +160,16 @@ export function determineInstalledOsVersion( * identifiers. * * @param stringyVersion version string to check ("0.0.0") - * @param _EXPECTED_MAJOR minimum required major version number - * @param _EXPECTED_MINOR minimum required minor version number */ -export function versionOK(stringyVersion = "0.0.0", - _EXPECTED_MAJOR: number, - _EXPECTED_MINOR: number) { - const [actual_major, actual_minor] = stringyVersion - .split(".") - .map(x => parseInt(x, 10)); - if (actual_major > _EXPECTED_MAJOR) { +export function versionOK(stringyVersion = "0.0.0") { + const [actual_major, actual_minor] = parseVersion(stringyVersion); + const [EXPECTED_MAJOR, EXPECTED_MINOR] = + parseVersion(globalConfig.MINIMUM_FBOS_VERSION || "6.0.0"); + if (actual_major > EXPECTED_MAJOR) { return true; } else { - const majorOK = (actual_major == _EXPECTED_MAJOR); - const minorOK = (actual_minor >= _EXPECTED_MINOR); + const majorOK = (actual_major == EXPECTED_MAJOR); + const minorOK = (actual_minor >= EXPECTED_MINOR); return (majorOK && minorOK); } } diff --git a/spec/controllers/api/devices/devices_controller_seed_spec.rb b/spec/controllers/api/devices/devices_controller_seed_spec.rb index 259e07667..f8a71103c 100644 --- a/spec/controllers/api/devices/devices_controller_seed_spec.rb +++ b/spec/controllers/api/devices/devices_controller_seed_spec.rb @@ -101,6 +101,14 @@ describe Api::DevicesController do device.tool_slots.order(id: :asc)[5] end + def tool_slots_slot_7?(device) + device.tool_slots.order(id: :asc)[6] + end + + def tool_slots_slot_8?(device) + device.tool_slots.order(id: :asc)[7] + end + def tools_seed_bin?(device) device.tools.find_by(name: "Seed Bin") end @@ -117,10 +125,6 @@ describe Api::DevicesController do device.tools.find_by(name: "Seed Trough 2") end - def tools_seed_trough_3?(device) - device.tools.find_by(name: "Seed Trough 3") - end - def tools_seeder?(device) device.tools.find_by(name: "Seeder") end @@ -230,17 +234,20 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -280,6 +287,8 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") @@ -287,11 +296,11 @@ describe Api::DevicesController do check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -331,11 +340,20 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") + check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") + check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") + check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") + check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") + check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -375,11 +393,22 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1") + expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2") + + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") + check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") + check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") + check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") + check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") + check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1") + check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be - expect(tools_seed_trough_1?(device)).to_not be - expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be + expect(tools_seed_trough_1?(device)).to be + expect(tools_seed_trough_2?(device)).to be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -419,6 +448,8 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") @@ -431,7 +462,6 @@ describe Api::DevicesController do expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -471,6 +501,8 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1") + expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2") check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") @@ -478,12 +510,13 @@ describe Api::DevicesController do check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1") + check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2") expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be - expect(tools_seed_trough_1?(device)).to_not be - expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be + expect(tools_seed_trough_1?(device)).to be + expect(tools_seed_trough_2?(device)).to be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -519,18 +552,20 @@ describe Api::DevicesController do expect(settings_hide_sensors?(device)).to be(true) expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1") expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2") - expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3") + expect(tool_slots_slot_3?(device)).to_not be expect(tool_slots_slot_4?(device)).to_not be expect(tool_slots_slot_5?(device)).to_not be expect(tool_slots_slot_6?(device)).to_not be + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1") check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2") - check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3") + expect(tools_seed_bin?(device)).to_not be expect(tools_seed_tray?(device)).to_not be expect(tools_seed_trough_1?(device)).to be expect(tools_seed_trough_2?(device)).to be - expect(tools_seed_trough_3?(device)).to be expect(tools_seeder?(device)).to_not be expect(tools_soil_sensor?(device)).to_not be expect(tools_watering_nozzle?(device)).to_not be @@ -566,18 +601,20 @@ describe Api::DevicesController do expect(settings_hide_sensors?(device)).to be(true) expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1") expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2") - expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3") + expect(tool_slots_slot_3?(device)).to_not be expect(tool_slots_slot_4?(device)).to_not be expect(tool_slots_slot_5?(device)).to_not be expect(tool_slots_slot_6?(device)).to_not be + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1") check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2") - check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3") + expect(tools_seed_bin?(device)).to_not be expect(tools_seed_tray?(device)).to_not be expect(tools_seed_trough_1?(device)).to be expect(tools_seed_trough_2?(device)).to be - expect(tools_seed_trough_3?(device)).to be expect(tools_seeder?(device)).to_not be expect(tools_soil_sensor?(device)).to_not be expect(tools_watering_nozzle?(device)).to_not be From 66b5e3c9620a99ee397725b4902083db1f38eb1d Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Sat, 15 Feb 2020 10:30:23 -0800 Subject: [PATCH 3/4] refactor external urls --- frontend/__tests__/external_urls_test.ts | 33 ++++++++++++ frontend/api/api.ts | 4 ++ frontend/apology.tsx | 3 +- frontend/auth/actions.ts | 5 +- frontend/constants.ts | 3 +- frontend/crash_page.tsx | 3 +- frontend/demo/demo_iframe.tsx | 9 ++-- frontend/devices/actions.ts | 3 -- .../components/farmbot_os_settings.tsx | 6 +-- .../components/fbos_settings/fbos_details.tsx | 10 ++-- frontend/devices/connectivity/truth_table.ts | 14 +++-- frontend/devices/interfaces.ts | 2 +- frontend/external_urls.ts | 51 +++++++++++++++++++ frontend/farm_designer/openfarm.ts | 3 -- frontend/farm_designer/plants/crop_info.tsx | 3 +- .../plants/openfarm_search_results.tsx | 3 +- frontend/farm_designer/util.ts | 4 +- frontend/farmware/__tests__/actions_test.ts | 4 +- frontend/farmware/actions.ts | 11 ++-- frontend/farmware/interfaces.ts | 2 - frontend/front_page/laptop_splash.tsx | 5 +- frontend/nav/additional_menu.tsx | 3 +- frontend/open_farm/__tests__/icons_test.ts | 8 +-- frontend/open_farm/cached_crop.ts | 5 +- frontend/open_farm/icons.ts | 5 -- frontend/os_download/content.tsx | 6 +-- frontend/tos_update/component.tsx | 3 +- frontend/ui/__tests__/doc_link_test.ts | 7 +-- frontend/ui/doc_link.ts | 8 +-- 29 files changed, 152 insertions(+), 74 deletions(-) create mode 100644 frontend/__tests__/external_urls_test.ts create mode 100644 frontend/external_urls.ts diff --git a/frontend/__tests__/external_urls_test.ts b/frontend/__tests__/external_urls_test.ts new file mode 100644 index 000000000..156e1932b --- /dev/null +++ b/frontend/__tests__/external_urls_test.ts @@ -0,0 +1,33 @@ +jest.unmock("../external_urls"); +import { ExternalUrl } from "../external_urls"; + +/* tslint:disable:max-line-length */ + +describe("ExternalUrl", () => { + it("returns urls", () => { + expect(ExternalUrl.featureMinVersions) + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/FEATURE_MIN_VERSIONS.json"); + expect(ExternalUrl.osReleaseNotes) + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/RELEASE_NOTES.md"); + expect(ExternalUrl.latestRelease) + .toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest"); + expect(ExternalUrl.webAppRepo) + .toEqual("https://github.com/FarmBot/Farmbot-Web-App"); + expect(ExternalUrl.gitHubFarmBot) + .toEqual("https://github.com/FarmBot"); + expect(ExternalUrl.softwareDocs) + .toEqual("https://software.farm.bot/docs"); + expect(ExternalUrl.softwareForum) + .toEqual("http://forum.farmbot.org/c/software"); + expect(ExternalUrl.OpenFarm.cropApi) + .toEqual("https://openfarm.cc/api/v1/crops/"); + expect(ExternalUrl.OpenFarm.cropBrowse) + .toEqual("https://openfarm.cc/crops/"); + expect(ExternalUrl.OpenFarm.newCrop) + .toEqual("https://openfarm.cc/en/crops/new"); + expect(ExternalUrl.Videos.desktop) + .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"); + expect(ExternalUrl.Videos.mobile) + .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"); + }); +}); diff --git a/frontend/api/api.ts b/frontend/api/api.ts index 03d5a55d0..264242e62 100644 --- a/frontend/api/api.ts +++ b/frontend/api/api.ts @@ -158,6 +158,10 @@ export class API { get farmwareInstallationPath() { return `${this.baseUrl}/api/farmware_installations/`; } + /** /api/first_party_farmwares */ + get firstPartyFarmwarePath() { + return `${this.baseUrl}/api/first_party_farmwares`; + } /** /api/alerts/:id */ get alertPath() { return `${this.baseUrl}/api/alerts/`; } /** /api/global_bulletins/:id */ diff --git a/frontend/apology.tsx b/frontend/apology.tsx index a00871358..bd2ac6328 100644 --- a/frontend/apology.tsx +++ b/frontend/apology.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { Session } from "./session"; +import { ExternalUrl } from "./external_urls"; const OUTER_STYLE: React.CSSProperties = { borderRadius: "10px", @@ -47,7 +48,7 @@ export function Apology(_: {}) {
  • Send a report to our developer team via the  - FarmBot software + FarmBot software forum. Including additional information (such as steps leading up to the error) helps us identify solutions more quickly. diff --git a/frontend/auth/actions.ts b/frontend/auth/actions.ts index 76126927c..9527939f0 100644 --- a/frontend/auth/actions.ts +++ b/frontend/auth/actions.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { - fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL, + fetchReleases, fetchMinOsFeatureData, fetchLatestGHBetaRelease } from "../devices/actions"; import { AuthState } from "./interfaces"; @@ -16,6 +16,7 @@ import { Actions } from "../constants"; import { connectDevice } from "../connectivity/connect_device"; import { getFirstPartyFarmwareList } from "../farmware/actions"; import { readOnlyInterceptor } from "../read_only_mode"; +import { ExternalUrl } from "../external_urls"; export function didLogin(authState: AuthState, dispatch: Function) { API.setBaseUrl(authState.token.unencoded.iss); @@ -24,7 +25,7 @@ export function didLogin(authState: AuthState, dispatch: Function) { beta_os_update_server && beta_os_update_server != "NOT_SET" && dispatch(fetchLatestGHBetaRelease(beta_os_update_server)); dispatch(getFirstPartyFarmwareList()); - dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL)); + dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions)); dispatch(setToken(authState)); Sync.fetchSyncData(dispatch); dispatch(connectDevice(authState)); diff --git a/frontend/constants.ts b/frontend/constants.ts index 7dd95c219..c76339cb7 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -952,8 +952,7 @@ export namespace DiagnosticMessages { but we have no recent record of FarmBot connecting to the internet. This usually happens because of poor WiFi connectivity in the garden, a bad password during configuration, a very long power outage, or - blocked ports on FarmBot's local network. Please refer IT staff to - https://software.farm.bot/docs/for-it-security-professionals`); + blocked ports on FarmBot's local network. Please refer IT staff to:`); export const NO_WS_AVAILABLE = trim(`You are either offline, using a web browser that does not support WebSockets, or are behind a firewall that diff --git a/frontend/crash_page.tsx b/frontend/crash_page.tsx index 031ff12ad..7f6b92dd7 100644 --- a/frontend/crash_page.tsx +++ b/frontend/crash_page.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { get } from "lodash"; import { Page } from "./ui/index"; import { Session } from "./session"; +import { ExternalUrl } from "./external_urls"; /** Use currying to pass down `error` object for now. */ export function crashPage(error: object) { @@ -24,7 +25,7 @@ export function crashPage(error: object) {
  • Perform a "hard refresh" (CTRL + SHIFT + R on most machines).
  • Session.clear()}>Log out by clicking here.
  • Send the error information (below) to our developer team via the - FarmBot software + FarmBot software forum. Including additional information (such as steps leading up to the error) help us identify solutions more quickly.
  • diff --git a/frontend/demo/demo_iframe.tsx b/frontend/demo/demo_iframe.tsx index e0f5ba72c..16681552f 100644 --- a/frontend/demo/demo_iframe.tsx +++ b/frontend/demo/demo_iframe.tsx @@ -2,16 +2,13 @@ import { connect, MqttClient } from "mqtt"; import React from "react"; import { uuid } from "farmbot"; import axios from "axios"; +import { ExternalUrl } from "../external_urls"; interface State { error: Error | undefined; stage: string; } -const VIDEO_URL = - "https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"; -const PHONE_URL = - "https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"; const WS_CONFIG = { username: "farmbot_demo", password: "required, but not used.", @@ -63,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> { return
    - + diff --git a/frontend/devices/actions.ts b/frontend/devices/actions.ts index 67be2d969..edea6ce0f 100644 --- a/frontend/devices/actions.ts +++ b/frontend/devices/actions.ts @@ -26,9 +26,6 @@ import { t } from "../i18next_wrapper"; const ON = 1, OFF = 0; export type ConfigKey = keyof McuParams; -export const FEATURE_MIN_VERSIONS_URL = - "https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" + - "FEATURE_MIN_VERSIONS.json"; // Already filtering messages in FarmBot OS and the API- this is just for // an additional layer of safety. const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"]; diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index cb4b9f0de..cbfa9d10f 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -15,6 +15,7 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row"; import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector"; +import { ExternalUrl } from "../../external_urls"; export enum ColWidth { label = 3, @@ -22,15 +23,12 @@ export enum ColWidth { button = 2 } -const OS_RELEASE_NOTES_URL = - "https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md"; - export class FarmbotOsSettings extends React.Component { state: FarmbotOsState = { allOsReleaseNotes: "" }; componentDidMount() { - this.fetchReleaseNotes(OS_RELEASE_NOTES_URL); + this.fetchReleaseNotes(ExternalUrl.osReleaseNotes); } get osMajorVersion() { diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index 43ad6a0c7..c06a4b627 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -14,6 +14,7 @@ import { timeFormatString } from "../../../util"; import { TimeSettings } from "../../../interfaces"; import { StringConfigKey } from "farmbot/dist/resources/configs/fbos"; import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; +import { ExternalUrl, FarmBotRepo } from "../../../external_urls"; /** Return an indicator color for the given temperature (C). */ export const colorFromTemp = (temp: number | undefined): string => { @@ -170,7 +171,7 @@ const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8); interface CommitDisplayProps { title: string; - repo: string; + repo: FarmBotRepo; commit: string; } @@ -184,7 +185,7 @@ const CommitDisplay = ( {shortCommit === "---" ? shortCommit : {shortCommit} } @@ -270,14 +271,15 @@ export function FbosDetails(props: FbosDetailsProps) { timeSettings={props.timeSettings} device={props.deviceAccount} />

    {t("Environment")}: {env}

    - +

    {t("Target")}: {target}

    {t("Node name")}: {last((node_name || "").split("@"))}

    {t("Device ID")}: {props.deviceAccount.body.id}

    {isString(private_ip) &&

    {t("Local IP address")}: {private_ip}

    }

    {t("Firmware")}: {reformatFwVersion(firmware_version)}

    + repo={FarmBotRepo.FarmBotArduinoFirmware} commit={firmwareCommit} />

    {t("Firmware code")}: {firmware_version}

    {isNumber(uptime) && } {isNumber(memory_usage) && diff --git a/frontend/devices/connectivity/truth_table.ts b/frontend/devices/connectivity/truth_table.ts index 2a95f5eaf..3ff3ebc08 100644 --- a/frontend/devices/connectivity/truth_table.ts +++ b/frontend/devices/connectivity/truth_table.ts @@ -1,5 +1,11 @@ import { Dictionary } from "farmbot"; import { DiagnosticMessages } from "../../constants"; +import { docLink } from "../../ui/doc_link"; +import { trim } from "../../util/util"; + +const DiagnosticMessagesWiFiOrConfig = + trim(`${DiagnosticMessages.WIFI_OR_CONFIG} + ${docLink("for-it-security-professionals")}`); // I don't like this at all. // If anyone has a cleaner solution, I'd love to hear it. @@ -16,13 +22,13 @@ export const TRUTH_TABLE: Readonly> = { // 17: No MQTT connections. [0b10001]: DiagnosticMessages.NO_WS_AVAILABLE, // 24: Browser is connected to API and MQTT. - [0b11000]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b11000]: DiagnosticMessagesWiFiOrConfig, // 9: At least the browser is connected to MQTT. - [0b01001]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b01001]: DiagnosticMessagesWiFiOrConfig, // 8: At least the browser is connected to MQTT. - [0b01000]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b01000]: DiagnosticMessagesWiFiOrConfig, // 25: Farmbot offline. - [0b11001]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b11001]: DiagnosticMessagesWiFiOrConfig, // 2: Browser offline. Farmbot last seen by the API recently. [0b00010]: DiagnosticMessages.NO_WS_AVAILABLE, // 18: Farmbot last seen by the API recently. diff --git a/frontend/devices/interfaces.ts b/frontend/devices/interfaces.ts index 8963b9f86..767ea8bd5 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -93,7 +93,7 @@ export enum Feature { variables = "variables", } -/** Object fetched from FEATURE_MIN_VERSIONS_URL. */ +/** Object fetched from ExternalUrl.featureMinVersions. */ export type MinOsFeatureLookup = Partial>; export interface BotState { diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts new file mode 100644 index 000000000..47e123e98 --- /dev/null +++ b/frontend/external_urls.ts @@ -0,0 +1,51 @@ +enum Org { + FarmBot = "FarmBot", + FarmBotLabs = "FarmBot-Labs", +} + +export enum FarmBotRepo { + FarmBotWebApp = "Farmbot-Web-App", + FarmBotOS = "farmbot_os", + FarmBotArduinoFirmware = "farmbot-arduino-firmware", +} + +enum FbosFile { + featureMinVersions = "FEATURE_MIN_VERSIONS.json", + osReleaseNotes = "RELEASE_NOTES.md", +} + +export namespace ExternalUrl { + const GITHUB = "https://github.com"; + const GITHUB_RAW = "https://raw.githubusercontent.com"; + const GITHUB_API = "https://api.github.com"; + const OPENFARM = "https://openfarm.cc"; + const SOFTWARE_DOCS = "https://software.farm.bot"; + const FORUM = "http://forum.farmbot.org"; + const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files"; + + const FBOS_RAW = `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}`; + export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`; + export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`; + + export const latestRelease = + `${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`; + + export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`; + export const webAppRepo = + `${GITHUB}/${Org.FarmBot}/${FarmBotRepo.FarmBotWebApp}`; + + export const softwareDocs = `${SOFTWARE_DOCS}/docs`; + export const softwareForum = `${FORUM}/c/software`; + + export namespace OpenFarm { + export const cropApi = `${OPENFARM}/api/v1/crops/`; + export const cropBrowse = `${OPENFARM}/crops/`; + export const newCrop = `${OPENFARM}/en/crops/new`; + } + + export namespace Videos { + export const desktop = + `${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`; + export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`; + } +} diff --git a/frontend/farm_designer/openfarm.ts b/frontend/farm_designer/openfarm.ts index 10899a263..4b07b09d9 100644 --- a/frontend/farm_designer/openfarm.ts +++ b/frontend/farm_designer/openfarm.ts @@ -67,9 +67,6 @@ export namespace OpenFarm { type: string; attributes: ImageAttrs; } - - export const cropUrl = "https://openfarm.cc/api/v1/crops"; - export const browsingCropUrl = "https://openfarm.cc/crops/"; } /** Returned by https://openfarm.cc/api/v1/crops?filter=q */ export interface CropSearchResult { diff --git a/frontend/farm_designer/plants/crop_info.tsx b/frontend/farm_designer/plants/crop_info.tsx index e6bc6f63b..e56c69f50 100644 --- a/frontend/farm_designer/plants/crop_info.tsx +++ b/frontend/farm_designer/plants/crop_info.tsx @@ -24,6 +24,7 @@ import { import { startCase, isArray, chain, isNumber } from "lodash"; import { t } from "../../i18next_wrapper"; import { Panel } from "../panel_header"; +import { ExternalUrl } from "../../external_urls"; interface InfoFieldProps { title: string; @@ -170,7 +171,7 @@ const CropDragInfoTile = const EditOnOpenFarm = ({ slug }: { slug: string }) =>
    {t("Edit on")}  - {"OpenFarm"} diff --git a/frontend/farm_designer/plants/openfarm_search_results.tsx b/frontend/farm_designer/plants/openfarm_search_results.tsx index 2efddc935..d6040a26f 100644 --- a/frontend/farm_designer/plants/openfarm_search_results.tsx +++ b/frontend/farm_designer/plants/openfarm_search_results.tsx @@ -5,6 +5,7 @@ import { } from "../../ui/empty_state_wrapper"; import { Content } from "../../constants"; import { t } from "../../i18next_wrapper"; +import { ExternalUrl } from "../../external_urls"; /** A stripped down version of OFSearchResult */ interface Result { @@ -24,7 +25,7 @@ export class OpenFarmResults extends React.Component { get text(): JSX.Element { return

    {`${t(Content.CROP_NOT_FOUND_INTRO)} `} - + {t(Content.CROP_NOT_FOUND_LINK)}

    ; diff --git a/frontend/farm_designer/util.ts b/frontend/farm_designer/util.ts index 40500c821..425fece84 100644 --- a/frontend/farm_designer/util.ts +++ b/frontend/farm_designer/util.ts @@ -4,8 +4,10 @@ import { DEFAULT_ICON } from "../open_farm/icons"; import { Actions } from "../constants"; import { ExecutableType } from "farmbot/dist/resources/api_resources"; import { get } from "lodash"; +import { ExternalUrl } from "../external_urls"; -const url = (q: string) => `${OpenFarm.cropUrl}?include=pictures&filter=${q}`; +const url = (q: string) => + `${ExternalUrl.OpenFarm.cropApi}?include=pictures&filter=${q}`; const openFarmSearchQuery = (q: string): AxiosPromise => axios.get(url(q)); diff --git a/frontend/farmware/__tests__/actions_test.ts b/frontend/farmware/__tests__/actions_test.ts index a32929d45..d4bfde0b4 100644 --- a/frontend/farmware/__tests__/actions_test.ts +++ b/frontend/farmware/__tests__/actions_test.ts @@ -2,8 +2,8 @@ jest.mock("axios", () => ({ get: jest.fn(() => { return Promise.resolve({ data: [ - { manifest: "url", name: "farmware0" }, - { manifest: "url", name: "farmware1" } + { package: "farmware0" }, + { package: "farmware1" } ] }); }), diff --git a/frontend/farmware/actions.ts b/frontend/farmware/actions.ts index 0453e9d7f..c10135119 100644 --- a/frontend/farmware/actions.ts +++ b/frontend/farmware/actions.ts @@ -1,17 +1,14 @@ import axios from "axios"; -import { FarmwareManifestEntry } from "./interfaces"; import { Actions } from "../constants"; import { urlFor } from "../api/crud"; - -const farmwareManifestUrl = - "https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests" + - "/master/manifest.json"; +import { API } from "../api"; +import { FarmwareManifest } from "farmbot"; export const getFirstPartyFarmwareList = () => { return (dispatch: Function) => { - axios.get(farmwareManifestUrl) + axios.get(API.current.firstPartyFarmwarePath) .then(r => { - const names = r.data.map((fw: FarmwareManifestEntry) => fw.name); + const names = r.data.map(fw => fw.package); dispatch({ type: Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK, payload: names diff --git a/frontend/farmware/interfaces.ts b/frontend/farmware/interfaces.ts index 1a9cfe4d1..32d7cb132 100644 --- a/frontend/farmware/interfaces.ts +++ b/frontend/farmware/interfaces.ts @@ -24,8 +24,6 @@ export interface FarmwareState { infoOpen: boolean; } -export type FarmwareManifestEntry = Record<"name" | "manifest", string>; - export interface FarmwareConfigMenuProps { show: boolean | undefined; dispatch: Function; diff --git a/frontend/front_page/laptop_splash.tsx b/frontend/front_page/laptop_splash.tsx index ec4308bfa..bc9ddb94f 100644 --- a/frontend/front_page/laptop_splash.tsx +++ b/frontend/front_page/laptop_splash.tsx @@ -1,6 +1,5 @@ import * as React from "react"; -const VIDEO_URL = "https://cdn.shopify.com/s/files/1/2040/0289/files/" + - "Farm_Designer_Loop.mp4?9552037556691879018"; +import { ExternalUrl } from "../external_urls"; export const LaptopSplash = ({ className }: { className: string }) =>
    @@ -8,7 +7,7 @@ export const LaptopSplash = ({ className }: { className: string }) =>
    diff --git a/frontend/nav/additional_menu.tsx b/frontend/nav/additional_menu.tsx index 83a15f9c8..4e0b1290f 100644 --- a/frontend/nav/additional_menu.tsx +++ b/frontend/nav/additional_menu.tsx @@ -3,6 +3,7 @@ import { AccountMenuProps } from "./interfaces"; import { Link } from "../link"; import { shortRevision } from "../util"; import { t } from "../i18next_wrapper"; +import { ExternalUrl } from "../external_urls"; export const AdditionalMenu = (props: AccountMenuProps) => { return
    @@ -30,7 +31,7 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
    diff --git a/frontend/open_farm/__tests__/icons_test.ts b/frontend/open_farm/__tests__/icons_test.ts index 499c8e618..153fd1e74 100644 --- a/frontend/open_farm/__tests__/icons_test.ts +++ b/frontend/open_farm/__tests__/icons_test.ts @@ -1,10 +1,4 @@ -import { OpenFarmAPI, svgToUrl } from "../icons"; - -describe("OpenFarmAPI", () => { - it("has a base URL", () => { - expect(OpenFarmAPI.OFBaseURL).toContain("openfarm.cc"); - }); -}); +import { svgToUrl } from "../icons"; describe("svgToUrl()", () => { it("returns svg url", () => { diff --git a/frontend/open_farm/cached_crop.ts b/frontend/open_farm/cached_crop.ts index 24dc069ab..21d166b6a 100644 --- a/frontend/open_farm/cached_crop.ts +++ b/frontend/open_farm/cached_crop.ts @@ -1,7 +1,8 @@ import axios, { AxiosResponse } from "axios"; import { Dictionary } from "farmbot"; import { isObject } from "lodash"; -import { OFCropAttrs, OFCropResponse, OpenFarmAPI, svgToUrl } from "./icons"; +import { OFCropAttrs, OFCropResponse, svgToUrl } from "./icons"; +import { ExternalUrl } from "../external_urls"; export type OFIcon = Readonly; type IconDictionary = Dictionary; @@ -57,7 +58,7 @@ const cacheTheIcon = (slug: string) => }; function HTTPIconFetch(slug: string) { - const url = OpenFarmAPI.OFBaseURL + slug; + const url = ExternalUrl.OpenFarm.cropApi + slug; // Avoid duplicate requests. if (promiseCache[url]) { return promiseCache[url]; } promiseCache[url] = axios diff --git a/frontend/open_farm/icons.ts b/frontend/open_farm/icons.ts index c79f93549..b2ed9f940 100644 --- a/frontend/open_farm/icons.ts +++ b/frontend/open_farm/icons.ts @@ -1,4 +1,3 @@ -const BASE = "https://openfarm.cc/api/v1/crops/"; export const DATA_URI = "data:image/svg+xml;utf8,"; export const DEFAULT_ICON = "/app-resources/img/generic-plant.svg"; @@ -20,10 +19,6 @@ export interface OFCropResponse { }; } -export namespace OpenFarmAPI { - export const OFBaseURL = BASE; -} - export function svgToUrl(xml: string | undefined): string { return xml ? (DATA_URI + encodeURIComponent(xml)) : DEFAULT_ICON; diff --git a/frontend/os_download/content.tsx b/frontend/os_download/content.tsx index c30d5c016..f9d0e66f9 100644 --- a/frontend/os_download/content.tsx +++ b/frontend/os_download/content.tsx @@ -3,9 +3,7 @@ import axios from "axios"; import { t } from "../i18next_wrapper"; import { GithubRelease } from "../devices/interfaces"; import { Content } from "../constants"; - -const LATEST_RELEASE_URL = - "https://api.github.com/repos/farmbot/farmbot_os/releases/latest"; +import { ExternalUrl } from "../external_urls"; interface OsDownloadState { tagName: string; @@ -49,7 +47,7 @@ export class OsDownload extends React.Component<{}, OsDownloadState> { } fetchLatestRelease = () => - axios.get(LATEST_RELEASE_URL) + axios.get(ExternalUrl.latestRelease) .then(resp => this.setState({ tagName: resp.data.tag_name, diff --git a/frontend/tos_update/component.tsx b/frontend/tos_update/component.tsx index df93f2639..92d9a9336 100644 --- a/frontend/tos_update/component.tsx +++ b/frontend/tos_update/component.tsx @@ -8,6 +8,7 @@ import { API } from "../api"; import { Row, Col, Widget, WidgetHeader, WidgetBody } from "../ui"; import { TermsCheckbox } from "../front_page/terms_checkbox"; import { t } from "../i18next_wrapper"; +import { ExternalUrl } from "../external_urls"; interface Props { } interface State { @@ -86,7 +87,7 @@ export class TosUpdate extends React.Component> {

    {t("Please send us an email at contact@farm.bot or see the ")} - + {t("FarmBot forum.")}

    diff --git a/frontend/ui/__tests__/doc_link_test.ts b/frontend/ui/__tests__/doc_link_test.ts index 5a1c61d06..bcb65f595 100644 --- a/frontend/ui/__tests__/doc_link_test.ts +++ b/frontend/ui/__tests__/doc_link_test.ts @@ -1,8 +1,9 @@ -import { docLink, BASE_URL } from "../doc_link"; +import { docLink } from "../doc_link"; +import { ExternalUrl } from "../../external_urls"; describe("docLink", () => { it("creates doc links", () => { - expect(docLink()).toEqual(BASE_URL); - expect(docLink("farmware")).toEqual(BASE_URL + "farmware"); + expect(docLink()).toEqual(ExternalUrl.softwareDocs + "/"); + expect(docLink("farmware")).toEqual(ExternalUrl.softwareDocs + "/farmware"); }); }); diff --git a/frontend/ui/doc_link.ts b/frontend/ui/doc_link.ts index a65b5958c..8cbcc2edd 100644 --- a/frontend/ui/doc_link.ts +++ b/frontend/ui/doc_link.ts @@ -1,4 +1,4 @@ -export const BASE_URL = "https://software.farm.bot/docs/"; +import { ExternalUrl } from "../external_urls"; /** A centralized list of all documentation slugs in the app makes it easier to * rename / move links in the future. */ @@ -7,11 +7,13 @@ export const DOC_SLUGS = { "camera-calibration": "Camera Calibration", "the-farmbot-web-app": "Web App", "farmware": "Farmware", - "connecting-farmbot-to-the-internet": "Connecting FarmBot to the Internet" + "connecting-farmbot-to-the-internet": "Connecting FarmBot to the Internet", + "for-it-security-professionals": "For IT Security Professionals", }; export type DocSlug = keyof typeof DOC_SLUGS; /** WHY?: The function keeps things DRY. It also makes life easier when the * documentation URL / slug name changes. */ -export const docLink = (slug?: DocSlug) => BASE_URL + (slug || ""); +export const docLink = (slug?: DocSlug) => + `${ExternalUrl.softwareDocs}/${slug || ""}`; From a04ec59ba55ad8f1c3f6b745265735e40c3caa6e Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Tue, 18 Feb 2020 11:21:09 -0800 Subject: [PATCH 4/4] model and version updates part 2 --- frontend/__tests__/attach_app_to_dom_test.ts | 13 +- frontend/__tests__/external_urls_test.ts | 10 +- frontend/constants.ts | 107 +++++++++++- frontend/controls/controls.tsx | 2 + .../peripherals/__tests__/index_test.tsx | 18 +- frontend/controls/peripherals/index.tsx | 38 +++- .../controls/sensors/__tests__/index_test.tsx | 11 +- .../sensors/__tests__/sensor_list_test.tsx | 7 + frontend/controls/sensors/index.tsx | 18 +- frontend/css/_blueprint_overrides.scss | 1 + frontend/css/global.scss | 20 +++ frontend/demo/demo_iframe.tsx | 4 +- .../boolean_mcu_input_group_test.tsx | 3 +- .../__tests__/farmbot_os_settings_test.tsx | 20 ++- .../firmware_hardware_support_test.ts | 18 +- .../__tests__/maybe_highlight_test.tsx | 81 +++++++++ .../components/boolean_mcu_input_group.tsx | 76 ++++---- .../components/farmbot_os_settings.tsx | 59 ++++--- .../fbos_settings/auto_sync_row.tsx | 41 +++-- .../fbos_settings/auto_update_row.tsx | 39 +++-- .../components/fbos_settings/board_type.tsx | 52 +++--- .../fbos_settings/boot_sequence_selector.tsx | 28 +-- .../fbos_settings/camera_selection.tsx | 35 ++-- .../fbos_settings/factory_reset_row.tsx | 121 +++++++------ .../fbos_settings/farmbot_os_row.tsx | 88 +++++----- .../fbos_settings/fbos_button_row.tsx | 44 ++--- .../fbos_settings/power_and_reset.tsx | 35 ++-- .../components/firmware_hardware_support.ts | 8 +- .../devices/components/hardware_settings.tsx | 4 + .../__tests__/calibration_row_test.tsx | 3 +- .../__tests__/header_test.tsx | 7 +- .../hardware_settings/calibration_row.tsx | 41 +++-- .../hardware_settings/danger_zone.tsx | 50 +++--- .../components/hardware_settings/encoders.tsx | 33 ++-- .../components/hardware_settings/endstops.tsx | 19 +- .../hardware_settings/error_handling.tsx | 19 +- .../components/hardware_settings/header.tsx | 12 +- .../homing_and_calibration.tsx | 28 +-- .../components/hardware_settings/motors.tsx | 35 ++-- .../hardware_settings/pin_bindings.tsx | 11 +- .../hardware_settings/pin_guard.tsx | 12 +- .../hardware_settings/single_setting_row.tsx | 33 ++-- frontend/devices/components/interfaces.ts | 7 +- .../devices/components/maybe_highlight.tsx | 162 ++++++++++++++++++ .../components/numeric_mcu_input_group.tsx | 78 +++++---- frontend/devices/interfaces.ts | 8 +- .../pin_bindings/list_and_label_support.tsx | 18 +- frontend/external_urls.ts | 10 +- frontend/farm_designer/map/garden_map.tsx | 1 + frontend/farm_designer/map/interfaces.ts | 3 + .../farmbot/__tests__/bot_figure_test.tsx | 18 +- .../map/layers/farmbot/bot_figure.tsx | 2 +- .../plants/__tests__/garden_plant_test.tsx | 21 +++ .../plants/__tests__/plant_layer_test.tsx | 16 +- .../map/layers/plants/garden_plant.tsx | 12 +- .../map/layers/plants/plant_layer.tsx | 10 +- .../__tests__/tool_slot_point_test.tsx | 4 +- .../map/layers/tool_slots/tool_slot_point.tsx | 2 +- .../__tests__/group_detail_active_test.tsx | 1 + .../point_groups/group_detail.tsx | 3 + .../point_groups/group_detail_active.tsx | 4 +- .../point_groups/group_order_visual.tsx | 2 +- .../point_groups/point_group_item.tsx | 1 + .../tools/__tests__/add_tool_slot_test.tsx | 24 ++- .../tools/__tests__/add_tool_test.tsx | 30 +++- .../tools/__tests__/edit_tool_slot_test.tsx | 3 +- .../tools/__tests__/index_test.tsx | 11 +- .../tool_slot_edit_components_test.tsx | 8 + frontend/farm_designer/tools/add_tool.tsx | 75 ++++---- .../farm_designer/tools/add_tool_slot.tsx | 20 ++- frontend/farm_designer/tools/edit_tool.tsx | 2 +- .../farm_designer/tools/edit_tool_slot.tsx | 9 +- frontend/farm_designer/tools/index.tsx | 55 ++++-- .../tools/tool_slot_edit_components.tsx | 23 ++- frontend/folders/actions.ts | 8 +- frontend/front_page/laptop_splash.tsx | 2 +- frontend/help/__tests__/tour_test.tsx | 2 +- frontend/help/__tests__/tours_test.ts | 56 +++++- frontend/help/tour.tsx | 21 ++- frontend/help/tours.ts | 80 +++++++-- frontend/messages/__tests__/alerts_test.tsx | 4 +- frontend/messages/alerts.tsx | 1 + frontend/redux/upgrade_reminder.ts | 3 +- .../mark_as/__tests__/unpack_step_test.ts | 6 +- .../step_tiles/mark_as/unpack_step.ts | 27 +-- public/app-resources/languages/_helper.js | 3 +- .../languages/translation_metrics.md | 22 +-- 87 files changed, 1481 insertions(+), 701 deletions(-) create mode 100644 frontend/devices/components/__tests__/maybe_highlight_test.tsx create mode 100644 frontend/devices/components/maybe_highlight.tsx diff --git a/frontend/__tests__/attach_app_to_dom_test.ts b/frontend/__tests__/attach_app_to_dom_test.ts index 1a2d69daa..2c18907d9 100644 --- a/frontend/__tests__/attach_app_to_dom_test.ts +++ b/frontend/__tests__/attach_app_to_dom_test.ts @@ -1,10 +1,9 @@ -jest.mock("../util", () => { - return { - attachToRoot: jest.fn(), - // Incidental mock. Can be removed if errors go away. - trim: jest.fn(x => x) - }; -}); +jest.mock("../util", () => ({ + attachToRoot: jest.fn(), + // Incidental mock. Can be removed if errors go away. + trim: jest.fn(x => x), + urlFriendly: jest.fn(), +})); jest.mock("../redux/store", () => { return { store: { dispatch: jest.fn() } }; diff --git a/frontend/__tests__/external_urls_test.ts b/frontend/__tests__/external_urls_test.ts index 156e1932b..99f23cea5 100644 --- a/frontend/__tests__/external_urls_test.ts +++ b/frontend/__tests__/external_urls_test.ts @@ -6,9 +6,9 @@ import { ExternalUrl } from "../external_urls"; describe("ExternalUrl", () => { it("returns urls", () => { expect(ExternalUrl.featureMinVersions) - .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/FEATURE_MIN_VERSIONS.json"); + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/FEATURE_MIN_VERSIONS.json"); expect(ExternalUrl.osReleaseNotes) - .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/RELEASE_NOTES.md"); + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md"); expect(ExternalUrl.latestRelease) .toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest"); expect(ExternalUrl.webAppRepo) @@ -18,16 +18,16 @@ describe("ExternalUrl", () => { expect(ExternalUrl.softwareDocs) .toEqual("https://software.farm.bot/docs"); expect(ExternalUrl.softwareForum) - .toEqual("http://forum.farmbot.org/c/software"); + .toEqual("https://forum.farmbot.org/c/software"); expect(ExternalUrl.OpenFarm.cropApi) .toEqual("https://openfarm.cc/api/v1/crops/"); expect(ExternalUrl.OpenFarm.cropBrowse) .toEqual("https://openfarm.cc/crops/"); expect(ExternalUrl.OpenFarm.newCrop) .toEqual("https://openfarm.cc/en/crops/new"); - expect(ExternalUrl.Videos.desktop) + expect(ExternalUrl.Video.desktop) .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"); - expect(ExternalUrl.Videos.mobile) + expect(ExternalUrl.Video.mobile) .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"); }); }); diff --git a/frontend/constants.ts b/frontend/constants.ts index c76339cb7..7adb0a80f 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -702,9 +702,9 @@ export namespace Content { trim(`FarmBot sent a malformed message. You may need to upgrade FarmBot OS. Please upgrade FarmBot OS and log back in.`); - export const OLD_FBOS_REC_UPGRADE = trim(`Your version of FarmBot OS is - outdated and will soon no longer be supported. Please update your device as - soon as possible.`); + export const OLD_FBOS_REC_UPGRADE = + trim(`Your version of FarmBot OS is outdated and will soon no longer + be supported. Please update your device as soon as possible.`); export const EXPERIMENTAL_WARNING = trim(`Warning! This is an EXPERIMENTAL feature. This feature may be @@ -812,7 +812,10 @@ export namespace Content { trim(`add this crop on OpenFarm?`); export const NO_TOOLS = - trim(`Press "+" to add a new tool.`); + trim(`Press "+" to add a new tool or seed container.`); + + export const NO_SEED_CONTAINERS = + trim(`Press "+" to add a seed container.`); export const MOUNTED_TOOL = trim(`The tool currently mounted to the UTM can be set here or by using @@ -887,12 +890,23 @@ export namespace TourContent { selecting one, and dragging it into the garden.`); export const ADD_TOOLS = - trim(`Press edit and then the + button to add tools and seed containers.`); + trim(`Press the + button to add tools and seed containers.`); + + export const ADD_SEED_CONTAINERS = + trim(`Press the + button to add seed containers.`); + + export const ADD_TOOLS_AND_SLOTS = + trim(`Press the + button to add tools and seed containers. Then create + tool slots for them to by pressing the tool slot + button.`); + + export const ADD_SEED_CONTAINERS_AND_SLOTS = + trim(`Press the + button to add seed containers. Then create + slots for them to by pressing the seed container slot + button.`); export const ADD_TOOLS_SLOTS = trim(`Add the newly created tools and seed containers to the corresponding tool slots on FarmBot: - press edit and then + to create a tool slot.`); + press the + button to create a tool slot.`); export const ADD_PERIPHERALS = trim(`Press edit and then the + button to add peripherals.`); @@ -930,6 +944,87 @@ export namespace TourContent { trim(`Toggle various settings to customize your web app experience.`); } +export enum DeviceSetting { + // Homing and calibration + homingAndCalibration = `Homing and Calibration`, + homing = `Homing`, + calibration = `Calibration`, + setZeroPosition = `Set Zero Position`, + findHomeOnBoot = `Find Home on Boot`, + stopAtHome = `Stop at Home`, + stopAtMax = `Stop at Max`, + negativeCoordinatesOnly = `Negative Coordinates Only`, + axisLength = `Axis Length (mm)`, + + // Motors + motors = `Motors`, + maxSpeed = `Max Speed (mm/s)`, + homingSpeed = `Homing Speed (mm/s)`, + minimumSpeed = `Minimum Speed (mm/s)`, + accelerateFor = `Accelerate for (mm)`, + stepsPerMm = `Steps per MM`, + microstepsPerStep = `Microsteps per step`, + alwaysPowerMotors = `Always Power Motors`, + invertMotors = `Invert Motors`, + motorCurrent = `Motor Current`, + enable2ndXMotor = `Enable 2nd X Motor`, + invert2ndXMotor = `Invert 2nd X Motor`, + + // Encoders / Stall Detection + encoders = `Encoders`, + stallDetection = `Stall Detection`, + enableEncoders = `Enable Encoders`, + enableStallDetection = `Enable Stall Detection`, + stallSensitivity = `Stall Sensitivity`, + useEncodersForPositioning = `Use Encoders for Positioning`, + invertEncoders = `Invert Encoders`, + maxMissedSteps = `Max Missed Steps`, + missedStepDecay = `Missed Step Decay`, + encoderScaling = `Encoder Scaling`, + + // Endstops + endstops = `Endstops`, + enableEndstops = `Enable Endstops`, + swapEndstops = `Swap Endstops`, + invertEndstops = `Invert Endstops`, + + // Error handling + errorHandling = `Error Handling`, + timeoutAfter = `Timeout after (seconds)`, + maxRetries = `Max Retries`, + estopOnMovementError = `E-Stop on Movement Error`, + + // Pin Guard + pinGuard = `Pin Guard`, + + // Danger Zone + dangerZone = `dangerZone`, + resetHardwareParams = `Reset hardware parameter defaults`, + + // Pin Bindings + pinBindings = `Pin Bindings`, + + // FarmBot OS + name = `name`, + timezone = `timezone`, + camera = `camera`, + firmware = `firmware`, + farmbotOSAutoUpdate = `Farmbot OS Auto Update`, + farmbotOS = `Farmbot OS`, + autoSync = `Auto Sync`, + bootSequence = `Boot Sequence`, + + // Power and Reset + powerAndReset = `Power and Reset`, + restartFarmbot = `Restart Farmbot`, + shutdownFarmbot = `Shutdown Farmbot`, + restartFirmware = `Restart Firmware`, + factoryReset = `Factory Reset`, + autoFactoryReset = `Automatic Factory Reset`, + connectionAttemptPeriod = `Connection Attempt Period`, + changeOwnership = `Change Ownership`, +} + export namespace DiagnosticMessages { export const OK = trim(`All systems nominal.`); diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index bcd98afa0..5195c58ba 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -38,6 +38,7 @@ export class RawControls extends React.Component { getWebAppConfigVal={this.props.getWebAppConfigVal} /> peripherals = () => { sensors = () => this.hideSensors ?
    : ", () => { @@ -14,7 +14,8 @@ describe("", () => { bot, peripherals: [fakePeripheral()], dispatch: jest.fn(), - disabled: false + disabled: false, + firmwareHardware: undefined, }; } @@ -73,11 +74,18 @@ describe("", () => { expect(p.dispatch).toHaveBeenCalled(); }); - it("adds farmduino peripherals", () => { + it.each<[FirmwareHardware, number]>([ + ["arduino", 2], + ["farmduino", 5], + ["farmduino_k14", 5], + ["farmduino_k15", 5], + ["express_k10", 3], + ])("adds peripherals: %s", (firmware, expectedAdds) => { const p = fakeProps(); + p.firmwareHardware = firmware; const wrapper = mount(); wrapper.setState({ isEditing: true }); - clickButton(wrapper, 3, "farmduino"); - expect(p.dispatch).toHaveBeenCalledTimes(5); + clickButton(wrapper, 3, "stock"); + expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds); }); }); diff --git a/frontend/controls/peripherals/index.tsx b/frontend/controls/peripherals/index.tsx index cad85b669..69decd905 100644 --- a/frontend/controls/peripherals/index.tsx +++ b/frontend/controls/peripherals/index.tsx @@ -56,12 +56,31 @@ export class Peripherals this.props.dispatch(init("Peripheral", { pin, label })); }; - farmduinoPeripherals = () => { - this.newPeripheral(7, t("Lighting")); - this.newPeripheral(8, t("Water")); - this.newPeripheral(9, t("Vacuum")); - this.newPeripheral(10, t("Peripheral ") + "4"); - this.newPeripheral(12, t("Peripheral ") + "5"); + get stockPeripherals() { + switch (this.props.firmwareHardware) { + case "arduino": + return [ + { pin: 8, label: t("Water") }, + { pin: 9, label: t("Vacuum") }, + ]; + case "farmduino": + case "farmduino_k14": + case "farmduino_k15": + default: + return [ + { pin: 7, label: t("Lighting") }, + { pin: 8, label: t("Water") }, + { pin: 9, label: t("Vacuum") }, + { pin: 10, label: t("Peripheral ") + "4" }, + { pin: 12, label: t("Peripheral ") + "5" }, + ]; + case "express_k10": + return [ + { pin: 7, label: t("Lighting") }, + { pin: 8, label: t("Water") }, + { pin: 9, label: t("Vacuum") }, + ]; + } } render() { @@ -92,10 +111,11 @@ export class Peripherals hidden={!isEditing} className="fb-button green" type="button" - onClick={this.farmduinoPeripherals}> + onClick={() => this.stockPeripherals.map(p => + this.newPeripheral(p.pin, p.label))}> - Farmduino - + {t("Stock")} + {this.showPins()} diff --git a/frontend/controls/sensors/__tests__/index_test.tsx b/frontend/controls/sensors/__tests__/index_test.tsx index 23d13d778..569beee99 100644 --- a/frontend/controls/sensors/__tests__/index_test.tsx +++ b/frontend/controls/sensors/__tests__/index_test.tsx @@ -18,7 +18,8 @@ describe("", () => { bot, sensors: [fakeSensor1, fakeSensor2], dispatch: jest.fn(), - disabled: false + disabled: false, + firmwareHardware: undefined, }; } @@ -68,8 +69,16 @@ describe("", () => { it("adds stock sensors", () => { const p = fakeProps(); const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("stock sensors"); wrapper.setState({ isEditing: true }); clickButton(wrapper, 3, "stock sensors"); expect(p.dispatch).toHaveBeenCalledTimes(2); }); + + it("doesn't display + stock button", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("stock sensors"); + }); }); diff --git a/frontend/controls/sensors/__tests__/sensor_list_test.tsx b/frontend/controls/sensors/__tests__/sensor_list_test.tsx index 112280990..791f9fe81 100644 --- a/frontend/controls/sensors/__tests__/sensor_list_test.tsx +++ b/frontend/controls/sensors/__tests__/sensor_list_test.tsx @@ -100,4 +100,11 @@ describe("", function () { readSensorBtn.last().simulate("click"); expect(mockDevice.readPin).not.toHaveBeenCalled(); }); + + it("renders analog reading", () => { + const p = fakeProps(); + p.pins[50] && (p.pins[50].value = 600); + const wrapper = mount(); + expect(wrapper.html()).toContain("margin-left: -3.5rem"); + }); }); diff --git a/frontend/controls/sensors/index.tsx b/frontend/controls/sensors/index.tsx index 3ed6e9c82..77bb35249 100644 --- a/frontend/controls/sensors/index.tsx +++ b/frontend/controls/sensors/index.tsx @@ -10,6 +10,7 @@ import { saveAll, init } from "../../api/crud"; import { ToolTips } from "../../constants"; import { uniq } from "lodash"; import { t } from "../../i18next_wrapper"; +import { isExpressBoard } from "../../devices/components/firmware_hardware_support"; export class Sensors extends React.Component { constructor(props: SensorsProps) { @@ -79,14 +80,15 @@ export class Sensors extends React.Component { onClick={() => this.newSensor()}> - + {!isExpressBoard(this.props.firmwareHardware) && + } {this.showPins()} diff --git a/frontend/css/_blueprint_overrides.scss b/frontend/css/_blueprint_overrides.scss index 391d11aa9..039e5add0 100644 --- a/frontend/css/_blueprint_overrides.scss +++ b/frontend/css/_blueprint_overrides.scss @@ -1,5 +1,6 @@ // Padding for the popups. .bp3-popover-content { + z-index: 999; padding: 1rem; } diff --git a/frontend/css/global.scss b/frontend/css/global.scss index b719f3d1c..02ff86b62 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -1629,3 +1629,23 @@ textarea:focus { } } } + +.section { + display: block !important; +} + +.highlight, +.unhighlight { + display: flex; +} + +.highlight { + background-color: $light_yellow; + box-shadow: 0px 0px 7px 4px $light_yellow; +} + +.unhighlight { + transition: background-color 10s linear, box-shadow 10s linear; + background-color: transparent; + box-shadow: none; +} diff --git a/frontend/demo/demo_iframe.tsx b/frontend/demo/demo_iframe.tsx index 16681552f..1fe9181ed 100644 --- a/frontend/demo/demo_iframe.tsx +++ b/frontend/demo/demo_iframe.tsx @@ -60,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> { return
    - + diff --git a/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx b/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx index eaf610a0b..505650d38 100644 --- a/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx +++ b/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx @@ -7,13 +7,14 @@ import { ToggleButton } from "../../../controls/toggle_button"; import { settingToggle } from "../../actions"; import { bot } from "../../../__test_support__/fake_state/bot"; import { BooleanMCUInputGroupProps } from "../interfaces"; +import { DeviceSetting } from "../../../constants"; describe("BooleanMCUInputGroup", () => { const fakeProps = (): BooleanMCUInputGroupProps => ({ sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }), dispatch: jest.fn(), tooltip: "Tooltip", - name: "Name", + label: DeviceSetting.invertEncoders, x: "encoder_invert_x", y: "encoder_invert_y", z: "encoder_invert_z", diff --git a/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx b/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx index 917caaead..757cc3b56 100644 --- a/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx +++ b/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx @@ -22,6 +22,8 @@ import axios from "axios"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { edit } from "../../../api/crud"; import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources"; +import { formEvent } from "../../../__test_support__/fake_html_events"; +import { Content } from "../../../constants"; describe("", () => { beforeEach(() => { @@ -54,8 +56,8 @@ describe("", () => { const osSettings = mount(); expect(osSettings.find("input").length).toBe(1); expect(osSettings.find("button").length).toBe(7); - ["NAME", "TIME ZONE", "FARMBOT OS", "CAMERA", "FIRMWARE"] - .map(string => expect(osSettings.text()).toContain(string)); + ["name", "time zone", "farmbot os", "camera", "firmware"] + .map(string => expect(osSettings.text().toLowerCase()).toContain(string)); }); it("fetches OS release notes", async () => { @@ -115,4 +117,18 @@ describe("", () => { const osSettings = shallow(); expect(osSettings.find("BootSequenceSelector").length).toEqual(1); }); + + it("prevents default form submit action", () => { + const osSettings = shallow(); + const e = formEvent(); + osSettings.find("form").simulate("submit", e); + expect(e.preventDefault).toHaveBeenCalled(); + }); + + it("warns about timezone mismatch", () => { + const p = fakeProps(); + p.deviceAccount.body.timezone = "different"; + const osSettings = mount(); + expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING); + }); }); diff --git a/frontend/devices/components/__tests__/firmware_hardware_support_test.ts b/frontend/devices/components/__tests__/firmware_hardware_support_test.ts index e7f5e2a4a..46ddb33f8 100644 --- a/frontend/devices/components/__tests__/firmware_hardware_support_test.ts +++ b/frontend/devices/components/__tests__/firmware_hardware_support_test.ts @@ -1,4 +1,5 @@ -import { boardType } from "../firmware_hardware_support"; +import { boardType, getFwHardwareValue } from "../firmware_hardware_support"; +import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources"; describe("boardType()", () => { it("returns Farmduino", () => { @@ -32,3 +33,18 @@ describe("boardType()", () => { expect(boardType("none")).toEqual("none"); }); }); + +describe("getFwHardwareValue()", () => { + it("returns undefined", () => { + const fbosConfig = fakeFbosConfig(); + fbosConfig.body.firmware_hardware = "wrong"; + expect(getFwHardwareValue(fbosConfig)).toEqual(undefined); + expect(getFwHardwareValue(undefined)).toEqual(undefined); + }); + + it("returns real value", () => { + const fbosConfig = fakeFbosConfig(); + fbosConfig.body.firmware_hardware = "express_k10"; + expect(getFwHardwareValue(fbosConfig)).toEqual("express_k10"); + }); +}); diff --git a/frontend/devices/components/__tests__/maybe_highlight_test.tsx b/frontend/devices/components/__tests__/maybe_highlight_test.tsx new file mode 100644 index 000000000..e748d7574 --- /dev/null +++ b/frontend/devices/components/__tests__/maybe_highlight_test.tsx @@ -0,0 +1,81 @@ +jest.mock("../../actions", () => ({ + toggleControlPanel: jest.fn(), +})); + +import * as React from "react"; +import { mount } from "enzyme"; +import { + Highlight, HighlightProps, maybeHighlight, maybeOpenPanel, highlight +} from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; +import { panelState } from "../../../__test_support__/control_panel_state"; +import { toggleControlPanel } from "../../actions"; + +describe("", () => { + const fakeProps = (): HighlightProps => ({ + settingName: DeviceSetting.motors, + children:
    , + className: "section", + }); + + it("fades highlight", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.setState({ className: "highlight" }); + wrapper.instance().componentDidMount(); + expect(wrapper.state().className).toEqual("unhighlight"); + }); +}); + +describe("maybeHighlight()", () => { + beforeEach(() => { + highlight.opened = false; + highlight.highlighted = false; + }); + + it("highlights only once", () => { + location.search = "?highlight=motors"; + expect(maybeHighlight(DeviceSetting.motors)).toEqual("highlight"); + expect(maybeHighlight(DeviceSetting.motors)).toEqual(""); + }); + + it("doesn't highlight: different setting", () => { + location.search = "?highlight=name"; + expect(maybeHighlight(DeviceSetting.motors)).toEqual(""); + }); + + it("doesn't highlight: no matches", () => { + location.search = "?highlight=na"; + expect(maybeHighlight(DeviceSetting.motors)).toEqual(""); + }); +}); + +describe("maybeOpenPanel()", () => { + beforeEach(() => { + highlight.opened = false; + highlight.highlighted = false; + }); + + it("opens panel only once", () => { + location.search = "?highlight=motors"; + maybeOpenPanel(panelState())(jest.fn()); + expect(toggleControlPanel).toHaveBeenCalledWith("motors"); + jest.resetAllMocks(); + maybeOpenPanel(panelState())(jest.fn()); + expect(toggleControlPanel).not.toHaveBeenCalled(); + }); + + it("doesn't open panel: already open", () => { + location.search = "?highlight=motors"; + const panels = panelState(); + panels.motors = true; + maybeOpenPanel(panels)(jest.fn()); + expect(toggleControlPanel).not.toHaveBeenCalled(); + }); + + it("doesn't open panel: no search term", () => { + location.search = ""; + maybeOpenPanel(panelState())(jest.fn()); + expect(toggleControlPanel).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/devices/components/boolean_mcu_input_group.tsx b/frontend/devices/components/boolean_mcu_input_group.tsx index cd223864c..710fafa31 100644 --- a/frontend/devices/components/boolean_mcu_input_group.tsx +++ b/frontend/devices/components/boolean_mcu_input_group.tsx @@ -4,12 +4,14 @@ import { settingToggle } from "../actions"; import { Row, Col, Help } from "../../ui/index"; import { BooleanMCUInputGroupProps } from "./interfaces"; import { Position } from "@blueprintjs/core"; +import { t } from "../../i18next_wrapper"; +import { Highlight } from "./maybe_highlight"; export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { const { tooltip, - name, + label, x, y, z, @@ -26,40 +28,42 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { const zParam = sourceFwConfig(z); return - - - - - - - dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> - - - - dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> - - - - dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> - + + + + + + + + dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> + + + + dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> + + + + dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> + + ; } diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index cbfa9d10f..1498f5868 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -5,7 +5,7 @@ import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces"; import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui"; import { save, edit } from "../../api/crud"; import { isBotOnline } from "../must_be_online"; -import { Content } from "../../constants"; +import { Content, DeviceSetting } from "../../constants"; import { TimezoneSelector } from "../timezones/timezone_selector"; import { timezoneMismatch } from "../timezones/guess_timezone"; import { CameraSelection } from "./fbos_settings/camera_selection"; @@ -16,6 +16,7 @@ import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector"; import { ExternalUrl } from "../../external_urls"; +import { Highlight } from "./maybe_highlight"; export enum ColWidth { label = 3, @@ -85,34 +86,38 @@ export class FarmbotOsSettings - - - - - - + + + + + + + + - - - - -
    - {this.maybeWarnTz()} -
    -
    - -
    - + + + + + +
    + {this.maybeWarnTz()} +
    +
    + +
    + +
    - - - - -

    - {t(Content.AUTO_SYNC)} -

    - - - { - props.dispatch(updateConfig({ auto_sync: !autoSync.value })); - }} /> - + + + + + +

    + {t(Content.AUTO_SYNC)} +

    + + + { + props.dispatch(updateConfig({ auto_sync: !autoSync.value })); + }} /> + +
    ; } diff --git a/frontend/devices/components/fbos_settings/auto_update_row.tsx b/frontend/devices/components/fbos_settings/auto_update_row.tsx index f99e553f1..2540daecf 100644 --- a/frontend/devices/components/fbos_settings/auto_update_row.tsx +++ b/frontend/devices/components/fbos_settings/auto_update_row.tsx @@ -3,10 +3,11 @@ import { Row, Col } from "../../../ui/index"; import { ColWidth } from "../farmbot_os_settings"; import { ToggleButton } from "../../../controls/toggle_button"; import { updateConfig } from "../../actions"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { AutoUpdateRowProps } from "./interfaces"; import { t } from "../../../i18next_wrapper"; import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector"; +import { Highlight } from "../maybe_highlight"; export function AutoUpdateRow(props: AutoUpdateRowProps) { const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); @@ -18,23 +19,25 @@ export function AutoUpdateRow(props: AutoUpdateRowProps) { value={props.device.body.ota_hour} onChange={changeOtaHour(props.dispatch, props.device)} /> - - - - -

    - {t(Content.OS_AUTO_UPDATE)} -

    - - - props.dispatch(updateConfig({ - os_auto_update: !osAutoUpdate.value - }))} /> - + + + + + +

    + {t(Content.OS_AUTO_UPDATE)} +

    + + + props.dispatch(updateConfig({ + os_auto_update: !osAutoUpdate.value + }))} /> + +
    ; } diff --git a/frontend/devices/components/fbos_settings/board_type.tsx b/frontend/devices/components/fbos_settings/board_type.tsx index f5fc2fe23..9334f2a0d 100644 --- a/frontend/devices/components/fbos_settings/board_type.tsx +++ b/frontend/devices/components/fbos_settings/board_type.tsx @@ -10,6 +10,8 @@ import { FirmwareHardwareStatus } from "./firmware_hardware_status"; import { isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; interface BoardTypeState { sending: boolean } @@ -47,30 +49,32 @@ export class BoardType extends React.Component { render() { return - - - - -
    - -
    - - - - + + + + + +
    + +
    + + + + +
    ; } } diff --git a/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx b/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx index 6c3c9342b..1a0f4d460 100644 --- a/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx +++ b/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx @@ -9,6 +9,8 @@ import { selectAllSequences, findSequenceById } from "../../../resources/selecto import { betterCompact } from "../../../util"; import { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; interface Props { list: DropDownItem[]; @@ -56,18 +58,20 @@ export class RawBootSequenceSelector extends React.Component { render() { return - - - - - - + + + + + + + + ; } } diff --git a/frontend/devices/components/fbos_settings/camera_selection.tsx b/frontend/devices/components/fbos_settings/camera_selection.tsx index 46f3d895b..34adca94a 100644 --- a/frontend/devices/components/fbos_settings/camera_selection.tsx +++ b/frontend/devices/components/fbos_settings/camera_selection.tsx @@ -8,7 +8,8 @@ import { getDevice } from "../../../device"; import { ColWidth } from "../farmbot_os_settings"; import { Feature, UserEnv } from "../../interfaces"; import { t } from "../../../i18next_wrapper"; -import { Content, ToolTips } from "../../../constants"; +import { Content, ToolTips, DeviceSetting } from "../../../constants"; +import { Highlight } from "../maybe_highlight"; /** Check if the camera has been disabled. */ export const cameraDisabled = (env: UserEnv): boolean => @@ -84,21 +85,23 @@ export class CameraSelection render() { return - - - - -
    - -
    - + + + + + +
    + +
    + +
    ; } } diff --git a/frontend/devices/components/fbos_settings/factory_reset_row.tsx b/frontend/devices/components/fbos_settings/factory_reset_row.tsx index 0a42a8f3a..273313502 100644 --- a/frontend/devices/components/fbos_settings/factory_reset_row.tsx +++ b/frontend/devices/components/fbos_settings/factory_reset_row.tsx @@ -1,12 +1,13 @@ import * as React from "react"; import { Row, Col } from "../../../ui/index"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { factoryReset, updateConfig } from "../../actions"; import { ToggleButton } from "../../../controls/toggle_button"; import { BotConfigInputBox } from "../bot_config_input_box"; import { FactoryResetRowProps } from "./interfaces"; import { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function FactoryResetRow(props: FactoryResetRowProps) { const { dispatch, sourceFbosConfig, botOnline } = props; @@ -14,66 +15,72 @@ export function FactoryResetRow(props: FactoryResetRowProps) { const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {}; return
    - - - - -

    - {t(Content.FACTORY_RESET_WARNING)} -

    - - - - + + + + + +

    + {t(Content.FACTORY_RESET_WARNING)} +

    + + + + +
    - - - - -

    - {t(Content.AUTO_FACTORY_RESET)} -

    - - - { - dispatch(updateConfig({ - disable_factory_reset: !disableFactoryReset.value - })); - }} /> - + + + + + +

    + {t(Content.AUTO_FACTORY_RESET)} +

    + + + { + dispatch(updateConfig({ + disable_factory_reset: !disableFactoryReset.value + })); + }} /> + +
    - - - - -

    - {t(Content.AUTO_FACTORY_RESET_PERIOD)} -

    - - - - + + + + + +

    + {t(Content.AUTO_FACTORY_RESET_PERIOD)} +

    + + + + +
    ; } diff --git a/frontend/devices/components/fbos_settings/farmbot_os_row.tsx b/frontend/devices/components/fbos_settings/farmbot_os_row.tsx index 2f1cc8e04..2bcc6fd15 100644 --- a/frontend/devices/components/fbos_settings/farmbot_os_row.tsx +++ b/frontend/devices/components/fbos_settings/farmbot_os_row.tsx @@ -7,6 +7,8 @@ import { FarmbotOsRowProps } from "./interfaces"; import { FbosDetails } from "./fbos_details"; import { t } from "../../../i18next_wrapper"; import { ErrorBoundary } from "../../../error_boundary"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; const getVersionString = (fbosVersion: string | undefined, onBeta: boolean | undefined): string => { @@ -21,48 +23,50 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) { } = bot.hardware.informational_settings; const version = getVersionString(controller_version, currently_on_beta); return - - - - - -

    - {t("Version {{ version }}", { version })} -

    - - - -
    - - - -

    - {t("Release Notes")}  + + + + + + +

    + {t("Version {{ version }}", { version })} +

    + + + +
    + + + +

    + {t("Release Notes")}  -

    -
    -

    {props.osReleaseNotesHeading}

    - - {osReleaseNotes} - -
    -
    - - - - +

    +
    +

    {props.osReleaseNotesHeading}

    + + {osReleaseNotes} + +
    + + + + + +
    ; } diff --git a/frontend/devices/components/fbos_settings/fbos_button_row.tsx b/frontend/devices/components/fbos_settings/fbos_button_row.tsx index 1cf1d3a58..5ef9ec8f9 100644 --- a/frontend/devices/components/fbos_settings/fbos_button_row.tsx +++ b/frontend/devices/components/fbos_settings/fbos_button_row.tsx @@ -2,10 +2,12 @@ import * as React from "react"; import { Row, Col } from "../../../ui"; import { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; export interface FbosButtonRowProps { botOnline: boolean; - label: string; + label: DeviceSetting; description: string; buttonText: string; color: string; @@ -14,24 +16,26 @@ export interface FbosButtonRowProps { export const FbosButtonRow = (props: FbosButtonRowProps) => { return - - - - -

    - {t(props.description)} -

    - - - - + + + + + +

    + {t(props.description)} +

    + + + + +
    ; }; diff --git a/frontend/devices/components/fbos_settings/power_and_reset.tsx b/frontend/devices/components/fbos_settings/power_and_reset.tsx index e6ac2d437..1a33fa116 100644 --- a/frontend/devices/components/fbos_settings/power_and_reset.tsx +++ b/frontend/devices/components/fbos_settings/power_and_reset.tsx @@ -5,37 +5,39 @@ import { FactoryResetRow } from "./factory_reset_row"; import { PowerAndResetProps } from "./interfaces"; import { ChangeOwnershipForm } from "./change_ownership_form"; import { FbosButtonRow } from "./fbos_button_row"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { reboot, powerOff, restartFirmware } from "../../actions"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function PowerAndReset(props: PowerAndResetProps) { const { dispatch, sourceFbosConfig, botOnline } = props; const { power_and_reset } = props.controlPanelState; - return
    + return
    {botOnline && - -

    - {t("Change Ownership")}  - -

    - -
    - } + + +

    + {t(DeviceSetting.changeOwnership)}  + +

    + +
    +
    }
    -
    ; + ; } diff --git a/frontend/devices/components/firmware_hardware_support.ts b/frontend/devices/components/firmware_hardware_support.ts index 56e111276..71756e221 100644 --- a/frontend/devices/components/firmware_hardware_support.ts +++ b/frontend/devices/components/firmware_hardware_support.ts @@ -1,4 +1,4 @@ -import { FirmwareHardware } from "farmbot"; +import { FirmwareHardware, TaggedFbosConfig } from "farmbot"; export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { const values: FirmwareHardware[] = [ @@ -10,6 +10,12 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { return !!values.includes(x as FirmwareHardware); }; +export const getFwHardwareValue = + (fbosConfig: TaggedFbosConfig | undefined) => { + const value = fbosConfig?.body.firmware_hardware; + return isFwHardwareValue(value) ? value : undefined; + }; + const TMC_BOARDS = ["express_k10", "farmduino_k15"]; const EXPRESS_BOARDS = ["express_k10"]; diff --git a/frontend/devices/components/hardware_settings.tsx b/frontend/devices/components/hardware_settings.tsx index 0ad58b3cd..1f12f68fe 100644 --- a/frontend/devices/components/hardware_settings.tsx +++ b/frontend/devices/components/hardware_settings.tsx @@ -18,10 +18,14 @@ import { FwParamExportMenu } from "./hardware_settings/export_menu"; import { t } from "../../i18next_wrapper"; import { PinBindings } from "./hardware_settings/pin_bindings"; import { ErrorHandling } from "./hardware_settings/error_handling"; +import { maybeOpenPanel } from "./maybe_highlight"; export class HardwareSettings extends React.Component { + componentDidMount = () => + this.props.dispatch(maybeOpenPanel(this.props.controlPanelState)); + render() { const { bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig, diff --git a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx index f1475c44a..e7c47b59b 100644 --- a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx @@ -3,6 +3,7 @@ import { mount } from "enzyme"; import { CalibrationRow } from "../calibration_row"; import { bot } from "../../../../__test_support__/fake_state/bot"; import { CalibrationRowProps } from "../../interfaces"; +import { DeviceSetting } from "../../../../constants"; describe("", () => { const fakeProps = (): CalibrationRowProps => ({ @@ -11,7 +12,7 @@ describe("", () => { botDisconnected: false, action: jest.fn(), toolTip: "calibrate", - title: "calibrate", + title: DeviceSetting.calibration, axisTitle: "calibrate", }); diff --git a/frontend/devices/components/hardware_settings/__tests__/header_test.tsx b/frontend/devices/components/hardware_settings/__tests__/header_test.tsx index 902fee1b6..8b63d6d11 100644 --- a/frontend/devices/components/hardware_settings/__tests__/header_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/header_test.tsx @@ -1,16 +1,17 @@ import * as React from "react"; import { Header } from "../header"; import { mount } from "enzyme"; +import { DeviceSetting } from "../../../../constants"; describe("
    ", () => { it("renders", () => { const fn = jest.fn(); const el = mount(
    ); - expect(el.text()).toContain("FOO"); + expect(el.text().toLowerCase()).toContain("motors"); expect(el.find(".fa-minus").length).toBe(1); }); }); diff --git a/frontend/devices/components/hardware_settings/calibration_row.tsx b/frontend/devices/components/hardware_settings/calibration_row.tsx index ab986afec..5be2448c2 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -5,30 +5,33 @@ import { Row, Col, Help } from "../../../ui/index"; import { CalibrationRowProps } from "../interfaces"; import { t } from "../../../i18next_wrapper"; import { Position } from "@blueprintjs/core"; +import { Highlight } from "../maybe_highlight"; export function CalibrationRow(props: CalibrationRowProps) { const { hardware, botDisconnected } = props; return - - - - - {axisTrackingStatus(hardware) - .map(row => { - const { axis } = row; - const hardwareDisabled = props.type == "zero" ? false : row.disabled; - return - props.action(axis)}> - {`${t(props.axisTitle)} ${axis}`} - - ; - })} + + + + + + {axisTrackingStatus(hardware) + .map(row => { + const { axis } = row; + const hardwareDisabled = props.type == "zero" ? false : row.disabled; + return + props.action(axis)}> + {`${t(props.axisTitle)} ${axis}`} + + ; + })} + ; } diff --git a/frontend/devices/components/hardware_settings/danger_zone.tsx b/frontend/devices/components/hardware_settings/danger_zone.tsx index cd78e7f23..a7ce7940c 100644 --- a/frontend/devices/components/hardware_settings/danger_zone.tsx +++ b/frontend/devices/components/hardware_settings/danger_zone.tsx @@ -3,41 +3,45 @@ import { DangerZoneProps } from "../interfaces"; import { Row, Col } from "../../../ui/index"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function DangerZone(props: DangerZoneProps) { const { dispatch, onReset, botDisconnected } = props; const { danger_zone } = props.controlPanelState; - return
    + return
    - - - - -

    - {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} -

    - - - - + + + + + +

    + {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} +

    + + + + +
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/encoders.tsx b/frontend/devices/components/hardware_settings/encoders.tsx index 7addae3ae..2f5d817aa 100644 --- a/frontend/devices/components/hardware_settings/encoders.tsx +++ b/frontend/devices/components/hardware_settings/encoders.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { EncodersProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; import { isExpressBoard } from "../firmware_hardware_support"; +import { Highlight } from "../maybe_highlight"; export function Encoders(props: EncodersProps) { @@ -20,19 +20,20 @@ export function Encoders(props: EncodersProps) { }; const isExpress = isExpressBoard(firmwareHardware); - return
    + return
    {isExpress && } {!isExpress && } {!isExpress && } {!isExpress && } -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/endstops.tsx b/frontend/devices/components/hardware_settings/endstops.tsx index ffa1b5f3d..c2b55ed65 100644 --- a/frontend/devices/components/hardware_settings/endstops.tsx +++ b/frontend/devices/components/hardware_settings/endstops.tsx @@ -1,25 +1,26 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { EndStopsProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function EndStops(props: EndStopsProps) { const { endstops } = props.controlPanelState; const { dispatch, sourceFwConfig } = props; - return
    + return
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/error_handling.tsx b/frontend/devices/components/hardware_settings/error_handling.tsx index e8d4142f6..c0dccc7a0 100644 --- a/frontend/devices/components/hardware_settings/error_handling.tsx +++ b/frontend/devices/components/hardware_settings/error_handling.tsx @@ -1,14 +1,14 @@ import * as React from "react"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { ErrorHandlingProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; import { McuInputBox } from "../mcu_input_box"; import { settingToggle } from "../../actions"; import { SingleSettingRow } from "./single_setting_row"; import { ToggleButton } from "../../../controls/toggle_button"; +import { Highlight } from "../maybe_highlight"; export function ErrorHandling(props: ErrorHandlingProps) { @@ -16,15 +16,16 @@ export function ErrorHandling(props: ErrorHandlingProps) { const { dispatch, sourceFwConfig } = props; const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); - return
    + return
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/header.tsx b/frontend/devices/components/hardware_settings/header.tsx index 3536d115a..f072cad13 100644 --- a/frontend/devices/components/hardware_settings/header.tsx +++ b/frontend/devices/components/hardware_settings/header.tsx @@ -2,18 +2,20 @@ import * as React from "react"; import { ControlPanelState } from "../../interfaces"; import { toggleControlPanel } from "../../actions"; import { ExpandableHeader } from "../../../ui/expandable_header"; +import { t } from "../../../i18next_wrapper"; +import { DeviceSetting } from "../../../constants"; interface Props { dispatch: Function; - name: keyof ControlPanelState; - title: string; + panel: keyof ControlPanelState; + title: DeviceSetting; expanded: boolean; } export const Header = (props: Props) => { - const { dispatch, name, title, expanded } = props; + const { dispatch, panel, title, expanded } = props; return dispatch(toggleControlPanel(name))} />; + title={t(title)} + onClick={() => dispatch(toggleControlPanel(panel))} />; }; diff --git a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx index 1602fa351..0fa2c4374 100644 --- a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx +++ b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { CalibrationRow } from "./calibration_row"; import { disabledAxisMap } from "../axis_tracking_status"; @@ -13,6 +13,7 @@ import { isExpressBoard } from "../firmware_hardware_support"; import { getDevice } from "../../../device"; import { commandErr } from "../../actions"; import { CONFIG_DEFAULTS } from "farmbot/dist/config"; +import { Highlight } from "../maybe_highlight"; export function HomingAndCalibration(props: HomingAndCalibrationProps) { @@ -31,16 +32,17 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { const scale = calculateScale(sourceFwConfig); - return
    + return
    getDevice().setZero(axis) @@ -71,7 +73,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { hardware={hardware} botDisconnected={botDisconnected} /> -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/motors.tsx b/frontend/devices/components/hardware_settings/motors.tsx index b6281e751..39cdbabd6 100644 --- a/frontend/devices/components/hardware_settings/motors.tsx +++ b/frontend/devices/components/hardware_settings/motors.tsx @@ -1,18 +1,18 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { ToggleButton } from "../../../controls/toggle_button"; import { settingToggle } from "../../actions"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { MotorsProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; import { Xyz, McuParamName } from "farmbot"; import { SourceFwConfig } from "../../interfaces"; import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props"; import { isTMCBoard } from "../firmware_hardware_support"; import { SingleSettingRow } from "./single_setting_row"; +import { Highlight } from "../maybe_highlight"; export const calculateScale = (sourceFwConfig: SourceFwConfig): Record => { @@ -35,15 +35,16 @@ export function Motors(props: MotorsProps) { const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); const scale = calculateScale(sourceFwConfig); - return
    + return
    {isTMCBoard(firmwareHardware) && } -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/pin_bindings.tsx b/frontend/devices/components/hardware_settings/pin_bindings.tsx index e7e716c0c..fa258ce42 100644 --- a/frontend/devices/components/hardware_settings/pin_bindings.tsx +++ b/frontend/devices/components/hardware_settings/pin_bindings.tsx @@ -3,20 +3,23 @@ import { PinBindingsProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; import { PinBindingsContent } from "../../pin_bindings/pin_bindings"; +import { DeviceSetting } from "../../../constants"; +import { Highlight } from "../maybe_highlight"; export function PinBindings(props: PinBindingsProps) { const { pin_bindings } = props.controlPanelState; const { dispatch, resources } = props; - return
    + return
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/pin_guard.tsx b/frontend/devices/components/hardware_settings/pin_guard.tsx index 9bd49c83b..1886382fc 100644 --- a/frontend/devices/components/hardware_settings/pin_guard.tsx +++ b/frontend/devices/components/hardware_settings/pin_guard.tsx @@ -4,19 +4,21 @@ import { PinGuardProps } from "../interfaces"; import { Header } from "./header"; import { Collapse, Position } from "@blueprintjs/core"; import { Row, Col, Help } from "../../../ui/index"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function PinGuard(props: PinGuardProps) { const { pin_guard } = props.controlPanelState; const { dispatch, sourceFwConfig, resources } = props; - return
    + return
    @@ -79,5 +81,5 @@ export function PinGuard(props: PinGuardProps) { resources={resources} sourceFwConfig={sourceFwConfig} /> -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/single_setting_row.tsx b/frontend/devices/components/hardware_settings/single_setting_row.tsx index 02075c813..0ab1c4bba 100644 --- a/frontend/devices/components/hardware_settings/single_setting_row.tsx +++ b/frontend/devices/components/hardware_settings/single_setting_row.tsx @@ -1,20 +1,27 @@ import * as React from "react"; import { Row, Col, Help } from "../../../ui/index"; import { Position } from "@blueprintjs/core"; +import { DeviceSetting } from "../../../constants"; +import { Highlight } from "../maybe_highlight"; +import { t } from "../../../i18next_wrapper"; + +export interface SingleSettingRowProps { + label: DeviceSetting; + tooltip: string; + children: React.ReactChild; + settingType: "button" | "input"; +} export const SingleSettingRow = - ({ label, tooltip, settingType, children }: { - label: string, - tooltip: string, - children: React.ReactChild, - settingType: "button" | "input", - }) => + ({ label, tooltip, settingType, children }: SingleSettingRowProps) => - - - - - {settingType === "button" - ? {children} - : {children}} + + + + + + {settingType === "button" + ? {children} + : {children}} + ; diff --git a/frontend/devices/components/interfaces.ts b/frontend/devices/components/interfaces.ts index a76dd30ec..4142d3284 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -6,6 +6,7 @@ import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist"; import { IntegerSize } from "../../util"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { ResourceIndex } from "../../resources/interfaces"; +import { DeviceSetting } from "../../constants"; export interface ZeroRowProps { botDisconnected: boolean; @@ -25,7 +26,7 @@ export interface BooleanMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; tooltip: string; - name: string; + label: DeviceSetting; x: McuParamName; y: McuParamName; z: McuParamName; @@ -41,7 +42,7 @@ export interface CalibrationRowProps { botDisconnected: boolean; action(axis: Axis): void; toolTip: string; - title: string; + title: DeviceSetting; axisTitle: string; } @@ -49,7 +50,7 @@ export interface NumericMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; tooltip: string; - name: string; + label: DeviceSetting; x: McuParamName; xScale?: number; y: McuParamName; diff --git a/frontend/devices/components/maybe_highlight.tsx b/frontend/devices/components/maybe_highlight.tsx new file mode 100644 index 000000000..eab806b0f --- /dev/null +++ b/frontend/devices/components/maybe_highlight.tsx @@ -0,0 +1,162 @@ +import * as React from "react"; +import { ControlPanelState } from "../interfaces"; +import { toggleControlPanel } from "../actions"; +import { urlFriendly } from "../../util"; +import { DeviceSetting } from "../../constants"; + +const HOMING_PANEL = [ + DeviceSetting.homingAndCalibration, + DeviceSetting.homing, + DeviceSetting.calibration, + DeviceSetting.setZeroPosition, + DeviceSetting.findHomeOnBoot, + DeviceSetting.stopAtHome, + DeviceSetting.stopAtMax, + DeviceSetting.negativeCoordinatesOnly, + DeviceSetting.axisLength, +]; +const MOTORS_PANEL = [ + DeviceSetting.motors, + DeviceSetting.maxSpeed, + DeviceSetting.homingSpeed, + DeviceSetting.minimumSpeed, + DeviceSetting.accelerateFor, + DeviceSetting.stepsPerMm, + DeviceSetting.microstepsPerStep, + DeviceSetting.alwaysPowerMotors, + DeviceSetting.invertMotors, + DeviceSetting.motorCurrent, + DeviceSetting.enable2ndXMotor, + DeviceSetting.invert2ndXMotor, +]; +const ENCODERS_PANEL = [ + DeviceSetting.encoders, + DeviceSetting.stallDetection, + DeviceSetting.enableEncoders, + DeviceSetting.enableStallDetection, + DeviceSetting.stallSensitivity, + DeviceSetting.useEncodersForPositioning, + DeviceSetting.invertEncoders, + DeviceSetting.maxMissedSteps, + DeviceSetting.missedStepDecay, + DeviceSetting.encoderScaling, +]; +const ENDSTOPS_PANEL = [ + DeviceSetting.endstops, + DeviceSetting.enableEndstops, + DeviceSetting.swapEndstops, + DeviceSetting.invertEndstops, +]; +const ERROR_HANDLING_PANEL = [ + DeviceSetting.errorHandling, + DeviceSetting.timeoutAfter, + DeviceSetting.maxRetries, + DeviceSetting.estopOnMovementError, +]; +const PIN_GUARD_PANEL = [ + DeviceSetting.pinGuard, +]; +const DANGER_ZONE_PANEL = [ + DeviceSetting.dangerZone, + DeviceSetting.resetHardwareParams, +]; +const PIN_BINDINGS_PANEL = [ + DeviceSetting.pinBindings, +]; +const POWER_AND_RESET_PANEL = [ + DeviceSetting.powerAndReset, + DeviceSetting.restartFarmbot, + DeviceSetting.shutdownFarmbot, + DeviceSetting.restartFirmware, + DeviceSetting.factoryReset, + DeviceSetting.autoFactoryReset, + DeviceSetting.connectionAttemptPeriod, + DeviceSetting.changeOwnership, +]; + +/** Look up parent panels for settings. */ +const SETTING_PANEL_LOOKUP = {} as Record; +HOMING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "homing_and_calibration"); +MOTORS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "motors"); +ENCODERS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "encoders"); +ENDSTOPS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "endstops"); +ERROR_HANDLING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "error_handling"); +PIN_GUARD_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_guard"); +DANGER_ZONE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "danger_zone"); +PIN_BINDINGS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_bindings"); +POWER_AND_RESET_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "power_and_reset"); + +/** Look up parent panels for settings using URL-friendly names. */ +const URL_FRIENDLY_LOOKUP: Record = {}; +Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) => + URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel); + +/** Look up all relevant names for the same setting. */ +const ALTERNATE_NAMES = + Object.values(DeviceSetting).reduce((acc, s) => { acc[s] = [s]; return acc; }, + {} as Record); +ALTERNATE_NAMES[DeviceSetting.encoders].push(DeviceSetting.stallDetection); +ALTERNATE_NAMES[DeviceSetting.stallDetection].push(DeviceSetting.encoders); + +/** Generate array of names for the same setting. Most only have one. */ +const compareValues = (settingName: DeviceSetting) => + (ALTERNATE_NAMES[settingName]).map(s => urlFriendly(s)); + +/** Retrieve a highlight search term. */ +const getHighlightName = () => location.search.split("?highlight=").pop(); + +/** Only open panel and highlight once per app load. Exported for tests. */ +export const highlight = { opened: false, highlighted: false }; + +/** Open a panel if a setting in that panel is highlighted. */ +export const maybeOpenPanel = (panelState: ControlPanelState) => + (dispatch: Function) => { + if (highlight.opened) { return; } + const urlFriendlySettingName = urlFriendly(getHighlightName() || ""); + if (!urlFriendlySettingName) { return; } + const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName]; + const panelIsOpen = panelState[panel]; + if (panelIsOpen) { return; } + dispatch(toggleControlPanel(panel)); + highlight.opened = true; + }; + +/** Highlight a setting if provided as a search term. */ +export const maybeHighlight = (settingName: DeviceSetting) => { + const item = getHighlightName(); + if (highlight.highlighted || !item) { return ""; } + const isCurrentSetting = compareValues(settingName).includes(item); + if (!isCurrentSetting) { return ""; } + highlight.highlighted = true; + return "highlight"; +}; + +export interface HighlightProps { + settingName: DeviceSetting; + children: React.ReactChild + | React.ReactChild[] + | (React.ReactChild | React.ReactChild[])[]; + className?: string; +} + +interface HighlightState { + className: string; +} + +/** Wrap highlight-able settings. */ +export class Highlight extends React.Component { + state: HighlightState = { className: maybeHighlight(this.props.settingName) }; + + componentDidMount = () => { + if (this.state.className == "highlight") { + /** Slowly fades highlight. */ + this.setState({ className: "unhighlight" }); + } + } + + render() { + return
    + {this.props.children} +
    ; + } +} diff --git a/frontend/devices/components/numeric_mcu_input_group.tsx b/frontend/devices/components/numeric_mcu_input_group.tsx index 13397af32..c76b94041 100644 --- a/frontend/devices/components/numeric_mcu_input_group.tsx +++ b/frontend/devices/components/numeric_mcu_input_group.tsx @@ -3,48 +3,52 @@ import { McuInputBox } from "./mcu_input_box"; import { NumericMCUInputGroupProps } from "./interfaces"; import { Row, Col, Help } from "../../ui/index"; import { Position } from "@blueprintjs/core"; +import { Highlight } from "./maybe_highlight"; +import { t } from "../../i18next_wrapper"; export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) { const { - sourceFwConfig, dispatch, tooltip, name, x, y, z, intSize, gray, float, + sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float, } = props; return - - - - - - - - - - - - - + + + + + + + + + + + + + + + ; } diff --git a/frontend/devices/interfaces.ts b/frontend/devices/interfaces.ts index 767ea8bd5..c74ff5e9f 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -201,6 +201,7 @@ export interface PeripheralsProps { peripherals: TaggedPeripheral[]; dispatch: Function; disabled: boolean | undefined; + firmwareHardware: FirmwareHardware | undefined; } export interface SensorsProps { @@ -208,6 +209,7 @@ export interface SensorsProps { sensors: TaggedSensor[]; dispatch: Function; disabled: boolean | undefined; + firmwareHardware: FirmwareHardware | undefined; } export interface FarmwareProps { @@ -248,8 +250,8 @@ export interface ControlPanelState { encoders: boolean; endstops: boolean; error_handling: boolean; - pin_bindings: boolean; - danger_zone: boolean; - power_and_reset: boolean; pin_guard: boolean; + danger_zone: boolean; + pin_bindings: boolean; + power_and_reset: boolean; } diff --git a/frontend/devices/pin_bindings/list_and_label_support.tsx b/frontend/devices/pin_bindings/list_and_label_support.tsx index 09ea49a71..ddc7be7c6 100644 --- a/frontend/devices/pin_bindings/list_and_label_support.tsx +++ b/frontend/devices/pin_bindings/list_and_label_support.tsx @@ -89,17 +89,17 @@ export const piSpi1Pins = [16, 17, 18, 19, 20, 21]; /** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */ export const reservedPiGPIO = piI2c0Pins; -const LabeledGpioPins: { [x: number]: string } = { - [ButtonPin.estop]: "Button 1: E-STOP", - [ButtonPin.unlock]: "Button 2: UNLOCK", - [ButtonPin.btn3]: "Button 3", - [ButtonPin.btn4]: "Button 4", - [ButtonPin.btn5]: "Button 5", -}; +const GPIO_PIN_LABELS = (): { [x: number]: string } => ({ + [ButtonPin.estop]: t("Button {{ num }}: E-STOP", { num: 1 }), + [ButtonPin.unlock]: t("Button {{ num }}: UNLOCK", { num: 2 }), + [ButtonPin.btn3]: t("Button {{ num }})", { num: 3 }), + [ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }), + [ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }), +}); export const generatePinLabel = (pin: number) => - LabeledGpioPins[pin] - ? `${LabeledGpioPins[pin]} (Pi ${pin})` + GPIO_PIN_LABELS()[pin] + ? `${t(GPIO_PIN_LABELS()[pin])} (Pi ${pin})` : `Pi GPIO ${pin}`; /** Raspberry Pi GPIO pin numbers. */ diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts index 47e123e98..6caa90822 100644 --- a/frontend/external_urls.ts +++ b/frontend/external_urls.ts @@ -20,10 +20,11 @@ export namespace ExternalUrl { const GITHUB_API = "https://api.github.com"; const OPENFARM = "https://openfarm.cc"; const SOFTWARE_DOCS = "https://software.farm.bot"; - const FORUM = "http://forum.farmbot.org"; + const FORUM = "https://forum.farmbot.org"; const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files"; - const FBOS_RAW = `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}`; + const FBOS_RAW = + `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/staging`; export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`; export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`; @@ -31,8 +32,7 @@ export namespace ExternalUrl { `${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`; export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`; - export const webAppRepo = - `${GITHUB}/${Org.FarmBot}/${FarmBotRepo.FarmBotWebApp}`; + export const webAppRepo = `${gitHubFarmBot}/${FarmBotRepo.FarmBotWebApp}`; export const softwareDocs = `${SOFTWARE_DOCS}/docs`; export const softwareForum = `${FORUM}/c/software`; @@ -43,7 +43,7 @@ export namespace ExternalUrl { export const newCrop = `${OPENFARM}/en/crops/new`; } - export namespace Videos { + export namespace Video { export const desktop = `${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`; export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`; diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 90c76eafb..063c5ccbf 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -386,6 +386,7 @@ export class GardenMap extends visible={!!this.props.showPlants} plants={this.props.plants} currentPlant={this.getPlant()} + hoveredPlant={this.props.hoveredPlant} dragging={!!this.state.isDragging} editing={this.isEditing} boxSelected={this.props.designer.selectedPlants} diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts index 8b647c2fc..aca542188 100644 --- a/frontend/farm_designer/map/interfaces.ts +++ b/frontend/farm_designer/map/interfaces.ts @@ -14,6 +14,7 @@ export type TaggedPlant = TaggedPlantPointer | TaggedPlantTemplate; export interface PlantLayerProps { plants: TaggedPlant[]; currentPlant: TaggedPlant | undefined; + hoveredPlant: TaggedPlant | undefined; dragging: boolean; editing: boolean; visible: boolean; @@ -57,12 +58,14 @@ export interface GardenPlantProps { dispatch: Function; plant: Readonly; selected: boolean; + current: boolean; editing: boolean; dragging: boolean; zoomLvl: number; activeDragXY: BotPosition | undefined; uuid: string; animate: boolean; + hovered: boolean; } export interface GardenPlantState { diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx index b17fa5345..d523f8f1e 100644 --- a/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx +++ b/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx @@ -15,17 +15,19 @@ describe("", () => { plantAreaOffset: { x: 100, y: 100 }, }); + const EXPECTED_MOTORS_OPACITY = 0.5; + it.each<[ string, BotOriginQuadrant, Record<"x" | "y", number>, boolean, number ]>([ - ["motors", 1, { x: 3000, y: 0 }, false, 0.75], - ["motors", 2, { x: 0, y: 0 }, false, 0.75], - ["motors", 3, { x: 0, y: 1500 }, false, 0.75], - ["motors", 4, { x: 3000, y: 1500 }, false, 0.75], - ["motors", 1, { x: 0, y: 1500 }, true, 0.75], - ["motors", 2, { x: 0, y: 0 }, true, 0.75], - ["motors", 3, { x: 3000, y: 0 }, true, 0.75], - ["motors", 4, { x: 3000, y: 1500 }, true, 0.75], + ["motors", 1, { x: 3000, y: 0 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 2, { x: 0, y: 0 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 3, { x: 0, y: 1500 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 4, { x: 3000, y: 1500 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 1, { x: 0, y: 1500 }, true, EXPECTED_MOTORS_OPACITY], + ["motors", 2, { x: 0, y: 0 }, true, EXPECTED_MOTORS_OPACITY], + ["motors", 3, { x: 3000, y: 0 }, true, EXPECTED_MOTORS_OPACITY], + ["motors", 4, { x: 3000, y: 1500 }, true, EXPECTED_MOTORS_OPACITY], ["encoders", 2, { x: 0, y: 0 }, false, 0.25], ])("shows %s in correct location for quadrant %i", (name, quadrant, expected, xySwap, opacity) => { diff --git a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx index bf07f7de2..59d699041 100644 --- a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx +++ b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx @@ -31,7 +31,7 @@ export class BotFigure extends const positionQ = transformXY( (position.x || 0), (position.y || 0), mapTransformProps); const color = eStopStatus ? Color.virtualRed : Color.darkGray; - const opacity = name.includes("encoder") ? 0.25 : 0.75; + const opacity = name.includes("encoder") ? 0.25 : 0.5; return ", () => { return { mapTransformProps: fakeMapTransformProps(), plant: fakePlant(), + current: false, selected: false, editing: false, dragging: false, @@ -21,6 +22,7 @@ describe("", () => { activeDragXY: { x: undefined, y: undefined, z: undefined }, uuid: "plantUuid", animate: false, + hovered: false, }; } @@ -31,6 +33,8 @@ describe("", () => { const wrapper = shallow(); expect(wrapper.find("image").length).toEqual(1); expect(wrapper.find("image").props().opacity).toEqual(1); + expect(wrapper.find("image").props().visibility).toEqual("visible"); + expect(wrapper.find("image").props().opacity).toEqual(1.0); expect(wrapper.find("text").length).toEqual(0); expect(wrapper.find("rect").length).toBeLessThanOrEqual(1); expect(wrapper.find("use").length).toEqual(0); @@ -88,4 +92,21 @@ describe("", () => { expect(wrapper.find(".plant-indicator").length).toEqual(1); expect(wrapper.find("Circle").length).toEqual(1); }); + + it("doesn't render indicator circle twice", () => { + const p = fakeProps(); + p.selected = true; + p.hovered = true; + const wrapper = shallow(); + expect(wrapper.find(".plant-indicator").length).toEqual(0); + expect(wrapper.find("Circle").length).toEqual(0); + }); + + it("renders while dragging", () => { + const p = fakeProps(); + p.dragging = true; + const wrapper = shallow(); + expect(wrapper.find("image").props().visibility).toEqual("hidden"); + expect(wrapper.find("image").props().opacity).toEqual(0.4); + }); }); diff --git a/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx b/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx index b981856b5..09dc7f1c1 100644 --- a/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx +++ b/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx @@ -8,11 +8,13 @@ import { PlantLayer } from "../plant_layer"; import { fakePlant, fakePlantTemplate } from "../../../../../__test_support__/fake_state/resources"; -import { PlantLayerProps, GardenPlantProps } from "../../../interfaces"; +import { PlantLayerProps } from "../../../interfaces"; import { fakeMapTransformProps } from "../../../../../__test_support__/map_transform_props"; import { svgMount } from "../../../../../__test_support__/svg_mount"; +import { shallow } from "enzyme"; +import { GardenPlant } from "../garden_plant"; describe("", () => { const fakeProps = (): PlantLayerProps => ({ @@ -28,6 +30,7 @@ describe("", () => { zoomLvl: 1, activeDragXY: { x: undefined, y: undefined, z: undefined }, animate: true, + hoveredPlant: undefined, }); it("shows plants", () => { @@ -88,14 +91,14 @@ describe("", () => { .toEqual("/app/designer/gardens/templates/5"); }); - it("has selected plant", () => { + it("has hovered plant", () => { mockPath = "/app/designer/plants"; const p = fakeProps(); const plant = fakePlant(); p.plants = [plant]; - p.currentPlant = plant; - const wrapper = svgMount(); - expect(wrapper.find("GardenPlant").props().selected).toEqual(true); + p.hoveredPlant = plant; + const wrapper = shallow(); + expect(wrapper.find(GardenPlant).props().hovered).toEqual(true); }); it("has plant selected by selection box", () => { @@ -105,8 +108,7 @@ describe("", () => { p.plants = [plant]; p.boxSelected = [plant.uuid]; const wrapper = svgMount(); - expect((wrapper.find("GardenPlant").props() as GardenPlantProps).selected) - .toEqual(true); + expect(wrapper.find("GardenPlant").props().selected).toEqual(true); }); it("allows clicking of unsaved plants", () => { diff --git a/frontend/farm_designer/map/layers/plants/garden_plant.tsx b/frontend/farm_designer/map/layers/plants/garden_plant.tsx index cc3a2b112..8bdec572e 100644 --- a/frontend/farm_designer/map/layers/plants/garden_plant.tsx +++ b/frontend/farm_designer/map/layers/plants/garden_plant.tsx @@ -36,22 +36,22 @@ export class GardenPlant extends }; get radius() { - const { selected, plant } = this.props; + const { plant } = this.props; const { hover } = this.state; const { radius } = plant.body; - return (hover && !selected) ? radius * 1.1 : radius; + return hover ? radius * 1.1 : radius; } render() { - const { selected, dragging, plant, mapTransformProps, - activeDragXY, zoomLvl, animate, editing } = this.props; + const { current, selected, dragging, plant, mapTransformProps, + activeDragXY, zoomLvl, animate, editing, hovered } = this.props; const { id, radius, x, y } = plant.body; const { icon } = this.state; const { qx, qy } = transformXY(round(x), round(y), mapTransformProps); const alpha = dragging ? 0.4 : 1.0; const className = [ - "plant-image", `is-chosen-${selected}`, animate ? "animate" : "" + "plant-image", `is-chosen-${current || selected}`, animate ? "animate" : "" ].join(" "); return @@ -65,7 +65,7 @@ export class GardenPlant extends fill={Color.soilCloud} fillOpacity={0} />} - {selected && !editing && + {(current || selected) && !editing && !hovered && {visible && plants.map(p => { - const selected = !!(p.uuid === currentPlant?.uuid); + const current = p.uuid === currentPlant?.uuid; + const hovered = p.uuid === hoveredPlant?.uuid; const selectedByBox = !!boxSelected?.includes(p.uuid); const selectedByGroup = groupSelected.includes(p.uuid); const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate" @@ -33,12 +35,14 @@ export function PlantLayer(props: PlantLayerProps) { uuid={p.uuid} mapTransformProps={mapTransformProps} plant={p} - selected={selected || selectedByBox || selectedByGroup} + selected={selectedByBox || selectedByGroup} + current={current} editing={editing} - dragging={selected && dragging && editing} + dragging={current && dragging && editing} dispatch={dispatch} zoomLvl={zoomLvl} activeDragXY={activeDragXY} + hovered={hovered} animate={animate} />; const wrapperProps = { className: "plant-link-wrapper", diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index 75451aefc..f47a485eb 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -66,12 +66,12 @@ describe("", () => { expect(wrapper.find("text").props().dx).toEqual(-40); }); - it("displays 'no tool'", () => { + it("displays 'empty'", () => { const p = fakeProps(); p.slot.tool = undefined; p.hoveredToolSlot = p.slot.toolSlot.uuid; const wrapper = svgMount(); - expect(wrapper.find("text").text()).toEqual("no tool"); + expect(wrapper.find("text").text()).toEqual("empty"); expect(wrapper.find("text").props().dx).toEqual(40); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx index ddb889b00..e34858e6d 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx @@ -32,7 +32,7 @@ export const ToolSlotPoint = (props: TSPProps) => { const { quadrant, xySwap } = mapTransformProps; const xPosition = gantry_mounted ? (botPositionX || 0) : x; const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps); - const toolName = props.slot.tool ? props.slot.tool.body.name : "no tool"; + const toolName = props.slot.tool ? props.slot.tool.body.name : "empty"; const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot; const toolProps = { x: qx, diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx index 2d7909c5b..1054d9806 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx @@ -40,6 +40,7 @@ describe("", () => { allPoints: [], shouldDisplay: () => true, slugs: [], + hovered: undefined, }; }; diff --git a/frontend/farm_designer/point_groups/group_detail.tsx b/frontend/farm_designer/point_groups/group_detail.tsx index c9bf393d9..506c5a0ed 100644 --- a/frontend/farm_designer/point_groups/group_detail.tsx +++ b/frontend/farm_designer/point_groups/group_detail.tsx @@ -10,6 +10,7 @@ import { GroupDetailActive } from "./group_detail_active"; import { ShouldDisplay } from "../../devices/interfaces"; import { getShouldDisplayFn } from "../../farmware/state_to_props"; import { uniq } from "lodash"; +import { UUID } from "../../resources/interfaces"; interface GroupDetailProps { dispatch: Function; @@ -17,6 +18,7 @@ interface GroupDetailProps { allPoints: TaggedPoint[]; shouldDisplay: ShouldDisplay; slugs: string[]; + hovered: UUID | undefined; } /** Find a group from a URL-provided ID. */ @@ -35,6 +37,7 @@ function mapStateToProps(props: Everything): GroupDetailProps { shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot), slugs: uniq(selectAllPlantPointers(props.resources.index) .map(p => p.body.openfarm_slug)), + hovered: props.resources.consumers.farm_designer.hoveredPlantListItem, }; } diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index b56ed3e71..5c50e92a3 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -18,6 +18,7 @@ import { GroupCriteria, GroupPointCountBreakdown, pointsSelectedByGroup } from "./criteria"; import { Content } from "../../constants"; +import { UUID } from "../../resources/interfaces"; export interface GroupDetailActiveProps { dispatch: Function; @@ -25,6 +26,7 @@ export interface GroupDetailActiveProps { allPoints: TaggedPoint[]; shouldDisplay: ShouldDisplay; slugs: string[]; + hovered: UUID | undefined; } type State = { timerId?: ReturnType }; @@ -47,7 +49,7 @@ export class GroupDetailActive return sortedPoints.map(point => { return ; diff --git a/frontend/farm_designer/point_groups/group_order_visual.tsx b/frontend/farm_designer/point_groups/group_order_visual.tsx index 7f4aead09..cdf9f21cc 100644 --- a/frontend/farm_designer/point_groups/group_order_visual.tsx +++ b/frontend/farm_designer/point_groups/group_order_visual.tsx @@ -36,7 +36,7 @@ export interface PointsPathLineProps { } export const PointsPathLine = (props: PointsPathLineProps) => - diff --git a/frontend/farm_designer/point_groups/point_group_item.tsx b/frontend/farm_designer/point_groups/point_group_item.tsx index cee43c163..830c25bbf 100644 --- a/frontend/farm_designer/point_groups/point_group_item.tsx +++ b/frontend/farm_designer/point_groups/point_group_item.tsx @@ -93,6 +93,7 @@ export class PointGroupItem style={{ border: this.criteriaIcon ? "1px solid gray" : "none", borderRadius: "5px", + background: this.props.hovered ? "lightgray" : "none", }} src={DEFAULT_ICON} onLoad={this.maybeGetCachedIcon} diff --git a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx index 616f081d9..76a5c991b 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx @@ -22,6 +22,7 @@ import { import { init, save, edit, destroy } from "../../../api/crud"; import { history } from "../../../history"; import { SpecialStatus } from "farmbot"; +import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; describe("", () => { const fakeProps = (): AddToolSlotProps => ({ @@ -30,14 +31,20 @@ describe("", () => { botPosition: { x: undefined, y: undefined, z: undefined }, dispatch: jest.fn(), findToolSlot: fakeToolSlot, + firmwareHardware: undefined, }); it("renders", () => { const wrapper = mount(); - ["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone", + ["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", "change slot direction", "use current location", "gantry-mounted" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); - expect(init).toHaveBeenCalled(); + expect(init).toHaveBeenCalledWith("Point", { + pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + x: 0, y: 0, z: 0, tool_id: undefined, + pullout_direction: ToolPulloutDirection.NONE, + gantry_mounted: false, + }); }); it("renders while loading", () => { @@ -102,6 +109,19 @@ describe("", () => { const wrapper = mount(); expect(wrapper.instance().tool).toEqual(undefined); }); + + it("renders for express bots", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("tool"); + expect(init).toHaveBeenCalledWith("Point", { + pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + x: 0, y: 0, z: 0, tool_id: undefined, + pullout_direction: ToolPulloutDirection.NONE, + gantry_mounted: true, + }); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx index e982afc55..c8d06b478 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx @@ -11,11 +11,13 @@ import { fakeState } from "../../../__test_support__/fake_state"; import { SaveBtn } from "../../../ui"; import { initSave } from "../../../api/crud"; import { history } from "../../../history"; -import { error } from "../../../toast/toast"; +import { FirmwareHardware } from "farmbot"; describe("", () => { const fakeProps = (): AddToolProps => ({ dispatch: jest.fn(), + existingToolNames: [], + firmwareHardware: undefined, }); it("renders", () => { @@ -38,19 +40,29 @@ describe("", () => { expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" }); }); - it("doesn't add stock tools", () => { - const wrapper = mount(); + it.each<[FirmwareHardware, number]>([ + ["arduino", 6], + ["farmduino", 6], + ["farmduino_k14", 6], + ["farmduino_k15", 8], + ["express_k10", 2], + ])("adds peripherals: %s", (firmware, expectedAdds) => { + const p = fakeProps(); + p.firmwareHardware = firmware; + const wrapper = mount(); wrapper.find("button").last().simulate("click"); - expect(error).toHaveBeenCalledWith("Please choose a FarmBot model."); - expect(initSave).not.toHaveBeenCalledTimes(6); - expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools"); + expect(initSave).toHaveBeenCalledTimes(expectedAdds); + expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); - it("adds stock tools", () => { - const wrapper = mount(); + it("doesn't add stock tools twice", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + p.existingToolNames = ["Seed Trough 1"]; + const wrapper = mount(); wrapper.setState({ model: "express" }); wrapper.find("button").last().simulate("click"); - expect(initSave).toHaveBeenCalledTimes(2); + expect(initSave).toHaveBeenCalledTimes(1); expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); }); diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx index f251c62e0..7057f20dd 100644 --- a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx @@ -28,6 +28,7 @@ describe("", () => { findTool: jest.fn(), botPosition: { x: undefined, y: undefined, z: undefined }, dispatch: jest.fn(), + firmwareHardware: undefined, }); it("redirects", () => { @@ -39,7 +40,7 @@ describe("", () => { const p = fakeProps(); p.findToolSlot = () => fakeToolSlot(); const wrapper = mount(); - ["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone", + ["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", "change slot direction", "use current location", "gantry-mounted" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); diff --git a/frontend/farm_designer/tools/__tests__/index_test.tsx b/frontend/farm_designer/tools/__tests__/index_test.tsx index 39e83f38a..c4584f781 100644 --- a/frontend/farm_designer/tools/__tests__/index_test.tsx +++ b/frontend/farm_designer/tools/__tests__/index_test.tsx @@ -39,6 +39,7 @@ describe("", () => { bot, botToMqttStatus: "down", hoveredToolSlot: undefined, + firmwareHardware: undefined, }); it("renders with no tools", () => { @@ -64,7 +65,7 @@ describe("", () => { p.toolSlots[1].body.y = 2; const wrapper = mount(); [ - "foo", "my tool", "unnamed tool", "(1, 0, 0)", "unknown", "(gantry, 2, 0)" + "foo", "my tool", "unnamed", "(1, 0, 0)", "unknown", "(gantry, 2, 0)" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); @@ -158,6 +159,7 @@ describe("", () => { p.bot.hardware.informational_settings.sync_status = "synced"; p.botToMqttStatus = "up"; const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("mounted tool"); wrapper.find(".yellow").first().simulate("click"); expect(mockDevice.readPin).toHaveBeenCalledWith({ label: "pin63", pin_mode: 0, pin_number: 63 @@ -173,6 +175,13 @@ describe("", () => { expect(mockDevice.readPin).not.toHaveBeenCalled(); expect(error).toHaveBeenCalledWith(Content.NOT_AVAILABLE_WHEN_OFFLINE); }); + + it("doesn't display mounted tool on express models", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("mounted tool"); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx index 7a58f2c93..861c26b3f 100644 --- a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx +++ b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx @@ -136,6 +136,7 @@ describe("", () => { tools: [], selectedTool: undefined, onChange: jest.fn(), + isExpress: false, }); it("renders", () => { @@ -149,6 +150,13 @@ describe("", () => { const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("foo"); }); + + it("renders for express bots", () => { + const p = fakeProps(); + p.isExpress = true; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("seed container"); + }); }); describe("", () => { diff --git a/frontend/farm_designer/tools/add_tool.tsx b/frontend/farm_designer/tools/add_tool.tsx index 2eba033c9..0eb08d86d 100644 --- a/frontend/farm_designer/tools/add_tool.tsx +++ b/frontend/farm_designer/tools/add_tool.tsx @@ -5,36 +5,37 @@ import { } from "../designer_panel"; import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; -import { SaveBtn, FBSelect, DropDownItem } from "../../ui"; -import { SpecialStatus } from "farmbot"; +import { SaveBtn } from "../../ui"; +import { SpecialStatus, FirmwareHardware } from "farmbot"; import { initSave } from "../../api/crud"; import { Panel } from "../panel_header"; import { history } from "../../history"; -import { error } from "../../toast/toast"; - -enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" } - -const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ - [Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 }, - [Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 }, - [Model.express]: { label: t("Express"), value: Model.express }, -}); +import { selectAllTools } from "../../resources/selectors"; +import { betterCompact } from "../../util"; +import { + isExpressBoard, getFwHardwareValue +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; export interface AddToolProps { dispatch: Function; + existingToolNames: string[]; + firmwareHardware: FirmwareHardware | undefined; } export interface AddToolState { toolName: string; - model: Model | undefined; } export const mapStateToProps = (props: Everything): AddToolProps => ({ dispatch: props.dispatch, + existingToolNames: betterCompact(selectAllTools(props.resources.index) + .map(tool => tool.body.name)), + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); export class RawAddTool extends React.Component { - state: AddToolState = { toolName: "", model: undefined }; + state: AddToolState = { toolName: "" }; newTool = (name: string) => { this.props.dispatch(initSave("Tool", { name })); @@ -45,9 +46,12 @@ export class RawAddTool extends React.Component { history.push("/app/designer/tools"); } - stockToolNames = (model: Model) => { - switch (model) { - case Model.genesis14: + stockToolNames = () => { + switch (this.props.firmwareHardware) { + case "arduino": + case "farmduino": + case "farmduino_k14": + default: return [ t("Seeder"), t("Watering Nozzle"), @@ -56,7 +60,7 @@ export class RawAddTool extends React.Component { t("Seed Bin"), t("Seed Tray"), ]; - case Model.genesis15: + case "farmduino_k15": return [ t("Seeder"), t("Watering Nozzle"), @@ -67,7 +71,7 @@ export class RawAddTool extends React.Component { t("Seed Trough 1"), t("Seed Trough 2"), ]; - case Model.express: + case "express_k10": return [ t("Seed Trough 1"), t("Seed Trough 2"), @@ -77,31 +81,20 @@ export class RawAddTool extends React.Component { AddStockTools = () =>
    - - this.setState({ model: ddi.value as Model })} - /> - {this.state.model && -
      - {this.stockToolNames(this.state.model).map(n =>
    • {n}
    • )} -
    } + +
      + {this.stockToolNames().map(n =>
    • {n}
    • )} +
    @@ -110,12 +103,14 @@ export class RawAddTool extends React.Component { return
    - + this.setState({ toolName: e.currentTarget.value })} /> diff --git a/frontend/farm_designer/tools/add_tool_slot.tsx b/frontend/farm_designer/tools/add_tool_slot.tsx index 23de61eb3..ffd4062d8 100644 --- a/frontend/farm_designer/tools/add_tool_slot.tsx +++ b/frontend/farm_designer/tools/add_tool_slot.tsx @@ -6,7 +6,9 @@ import { import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { SaveBtn } from "../../ui"; -import { SpecialStatus, TaggedTool, TaggedToolSlotPointer } from "farmbot"; +import { + SpecialStatus, TaggedTool, TaggedToolSlotPointer, FirmwareHardware +} from "farmbot"; import { init, save, edit, destroy } from "../../api/crud"; import { Panel } from "../panel_header"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; @@ -18,6 +20,10 @@ import { validBotLocationData } from "../../util"; import { history } from "../../history"; import { SlotEditRows } from "./tool_slot_edit_components"; import { UUID } from "../../resources/interfaces"; +import { + isExpressBoard, getFwHardwareValue +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; export interface AddToolSlotProps { tools: TaggedTool[]; @@ -25,6 +31,7 @@ export interface AddToolSlotProps { botPosition: BotPosition; findTool(id: number): TaggedTool | undefined; findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined; + firmwareHardware: FirmwareHardware | undefined; } export interface AddToolSlotState { @@ -38,6 +45,7 @@ export const mapStateToProps = (props: Everything): AddToolSlotProps => ({ findTool: (id: number) => maybeFindToolById(props.resources.index, id), findToolSlot: (uuid: UUID | undefined) => maybeGetToolSlot(props.resources.index, uuid), + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); export class RawAddToolSlot @@ -48,7 +56,8 @@ export class RawAddToolSlot const action = init("Point", { pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, x: 0, y: 0, z: 0, tool_id: undefined, - pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: false + pullout_direction: ToolPulloutDirection.NONE, + gantry_mounted: isExpressBoard(this.props.firmwareHardware) ? true : false, }); this.setState({ uuid: action.payload.uuid }); this.props.dispatch(action); @@ -57,7 +66,7 @@ export class RawAddToolSlot componentWillUnmount() { if (this.state.uuid && this.toolSlot && this.toolSlot.specialStatus == SpecialStatus.DIRTY) { - confirm(t("Save new tool?")) + confirm(t("Save new slot?")) ? this.props.dispatch(save(this.state.uuid)) : this.props.dispatch(destroy(this.state.uuid, true)); } @@ -86,12 +95,15 @@ export class RawAddToolSlot return {this.toolSlot ? { backTo={"/app/designer/tools"} panel={Panel.Tools} /> - + this.setState({ toolName: e.currentTarget.value })} /> diff --git a/frontend/farm_designer/tools/edit_tool_slot.tsx b/frontend/farm_designer/tools/edit_tool_slot.tsx index 21a114be3..210b283d3 100644 --- a/frontend/farm_designer/tools/edit_tool_slot.tsx +++ b/frontend/farm_designer/tools/edit_tool_slot.tsx @@ -6,7 +6,7 @@ import { import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { getPathArray } from "../../history"; -import { TaggedToolSlotPointer, TaggedTool } from "farmbot"; +import { TaggedToolSlotPointer, TaggedTool, FirmwareHardware } from "farmbot"; import { edit, save, destroy } from "../../api/crud"; import { history } from "../../history"; import { Panel } from "../panel_header"; @@ -17,6 +17,10 @@ import { BotPosition } from "../../devices/interfaces"; import { validBotLocationData } from "../../util"; import { SlotEditRows } from "./tool_slot_edit_components"; import { moveAbs } from "../../devices/actions"; +import { + getFwHardwareValue, isExpressBoard +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; export interface EditToolSlotProps { findToolSlot(id: string): TaggedToolSlotPointer | undefined; @@ -24,6 +28,7 @@ export interface EditToolSlotProps { findTool(id: number): TaggedTool | undefined; dispatch: Function; botPosition: BotPosition; + firmwareHardware: FirmwareHardware | undefined; } export const mapStateToProps = (props: Everything): EditToolSlotProps => ({ @@ -33,6 +38,7 @@ export const mapStateToProps = (props: Everything): EditToolSlotProps => ({ findTool: (id: number) => maybeFindToolById(props.resources.index, id), dispatch: props.dispatch, botPosition: validBotLocationData(props.bot.hardware.location_data).position, + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); export class RawEditToolSlot extends React.Component { @@ -64,6 +70,7 @@ export class RawEditToolSlot extends React.Component { panel={Panel.Tools} /> ({ bot: props.bot, botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]), hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot, + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); const toolStatus = (value: number | undefined): string => { @@ -108,6 +115,8 @@ export class RawTools extends React.Component { this.props.botToMqttStatus); } + get isExpress() { return isExpressBoard(this.props.firmwareHardware); } + MountedToolInfo = () =>
    @@ -141,10 +150,10 @@ export class RawTools extends React.Component { ToolSlots = () =>
    - +
    - +
    @@ -162,10 +171,10 @@ export class RawTools extends React.Component { Tools = () =>
    - +
    - +
    @@ -176,9 +185,32 @@ export class RawTools extends React.Component { .map(tool => )} + toolName={tool.body.name || t("Unnamed")} />)}
    + get strings() { + return { + placeholder: this.isExpress + ? t("Search your seed containers...") + : t("Search your tools..."), + titleText: this.isExpress + ? t("Add a seed container") + : t("Add a tool or seed container"), + emptyStateText: this.isExpress + ? Content.NO_SEED_CONTAINERS + : Content.NO_TOOLS, + tools: this.isExpress + ? t("seed containers") + : t("tools and seed containers"), + toolSlots: this.isExpress + ? t("seed container slots") + : t("tool slots"), + addSlot: this.isExpress + ? t("Add slot") + : t("Add tool slot"), + }; + } + render() { const panelName = "tools"; const hasTools = this.props.tools.length > 0; @@ -187,18 +219,19 @@ export class RawTools extends React.Component { + title={!hasTools ? this.strings.titleText : undefined}> + placeholder={this.strings.placeholder} /> - + {!this.isExpress && + } @@ -223,7 +256,7 @@ const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { onMouseLeave={() => props.dispatch(setToolHover(undefined))}> -

    {props.getToolName(tool_id) || t("No tool")}

    +

    {props.getToolName(tool_id) || t("Empty")}

    diff --git a/frontend/farm_designer/tools/tool_slot_edit_components.tsx b/frontend/farm_designer/tools/tool_slot_edit_components.tsx index 9a2fc0f86..c1e58ccd0 100644 --- a/frontend/farm_designer/tools/tool_slot_edit_components.tsx +++ b/frontend/farm_designer/tools/tool_slot_edit_components.tsx @@ -97,13 +97,18 @@ export interface ToolInputRowProps { tools: TaggedTool[]; selectedTool: TaggedTool | undefined; onChange(update: { tool_id: number }): void; + isExpress: boolean; } export const ToolInputRow = (props: ToolInputRowProps) =>

    - + ): void; + isExpress: boolean; } export const SlotEditRows = (props: SlotEditRowsProps) => @@ -153,16 +159,19 @@ export const SlotEditRows = (props: SlotEditRowsProps) => gantryMounted={props.toolSlot.body.gantry_mounted} onChange={props.updateToolSlot} /> - + {!props.toolSlot.body.gantry_mounted && + } - + {!props.isExpress && + }
    ; diff --git a/frontend/folders/actions.ts b/frontend/folders/actions.ts index 449501dfb..eac3583da 100644 --- a/frontend/folders/actions.ts +++ b/frontend/folders/actions.ts @@ -40,11 +40,11 @@ export const setFolderName = (id: number, name: string) => { return d(save(folder.uuid)) as Promise<{}>; }; -const DEFAULTS: Folder = { - name: "New Folder", +const DEFAULTS = (): Folder => ({ + name: t("New Folder"), color: "gray", parent_id: 0, -}; +}); export const addNewSequenceToFolder = (folder_id?: number) => { const uuidMap = store.getState().resources.index.byKind["Sequence"]; @@ -67,7 +67,7 @@ export const addNewSequenceToFolder = (folder_id?: number) => { export const createFolder = (config: DeepPartial = {}) => { const d: Function = store.dispatch; - const folder: Folder = { ...DEFAULTS, ...config }; + const folder: Folder = { ...DEFAULTS(), ...config }; const action = initSave("Folder", folder); // tslint:disable-next-line:no-any const p: Promise<{}> = d(action); diff --git a/frontend/front_page/laptop_splash.tsx b/frontend/front_page/laptop_splash.tsx index bc9ddb94f..dc81416fa 100644 --- a/frontend/front_page/laptop_splash.tsx +++ b/frontend/front_page/laptop_splash.tsx @@ -7,7 +7,7 @@ export const LaptopSplash = ({ className }: { className: string }) =>
    diff --git a/frontend/help/__tests__/tour_test.tsx b/frontend/help/__tests__/tour_test.tsx index a3c5908ad..38be5e14e 100644 --- a/frontend/help/__tests__/tour_test.tsx +++ b/frontend/help/__tests__/tour_test.tsx @@ -56,7 +56,7 @@ describe("", () => { expect(wrapper.state()).toEqual({ run: true, index: 1, returnPath: "/app/messages" }); - expect(history.push).toHaveBeenCalledWith("/app/tools"); + expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); it("navigates through tour: other", () => { diff --git a/frontend/help/__tests__/tours_test.ts b/frontend/help/__tests__/tours_test.ts index 2ab27cf74..98be2687a 100644 --- a/frontend/help/__tests__/tours_test.ts +++ b/frontend/help/__tests__/tours_test.ts @@ -1,7 +1,22 @@ jest.mock("../../history", () => ({ history: { push: jest.fn() } })); -import { tourPageNavigation } from "../tours"; +let mockDev = false; +jest.mock("../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + +import { fakeState } from "../../__test_support__/fake_state"; +const mockState = fakeState(); +jest.mock("../../redux/store", () => ({ + store: { getState: () => mockState }, +})); + +import { tourPageNavigation, TOUR_STEPS, Tours } from "../tours"; import { history } from "../../history"; +import { fakeTool, fakeFbosConfig } from "../../__test_support__/fake_state/resources"; +import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; describe("tourPageNavigation()", () => { const testCase = (el: string) => { @@ -20,8 +35,47 @@ describe("tourPageNavigation()", () => { testCase(".regimen-list-panel"); testCase(".tool-list"); testCase(".toolbay-list"); + testCase(".tools"); + testCase(".tool-slots"); + testCase(".tools-panel"); testCase(".photos"); testCase(".logs-table"); testCase(".app-settings-widget"); }); + + it("includes steps based on tool count", () => { + const getTargets = () => + Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target); + mockDev = false; + mockState.resources = buildResourceIndex([]); + expect(getTargets()).not.toContain(".tool-slots"); + mockState.resources = buildResourceIndex([fakeTool()]); + expect(getTargets()).toContain(".tool-slots"); + }); + + it("has correct content based on board version", () => { + const getTitles = () => + Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.title); + mockDev = false; + mockState.resources = buildResourceIndex([]); + expect(getTitles()).toContain("Add tools and tool slots"); + expect(getTitles()).not.toContain("Add seed containers"); + const fbosConfig = fakeFbosConfig(); + fbosConfig.body.firmware_hardware = "express_k10"; + mockState.resources = buildResourceIndex([fbosConfig]); + expect(getTitles()).toContain("Add seed containers and slots"); + expect(getTitles()).not.toContain("Add seed containers"); + mockState.resources = buildResourceIndex([fbosConfig, fakeTool()]); + expect(getTitles()).not.toContain("Add seed containers and slots"); + expect(getTitles()).toContain("Add seed containers"); + }); + + it("includes correct tour steps", () => { + mockDev = true; + const targets = + Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target); + expect(targets).not.toContain(".tools"); + expect(targets).toContain(".tool-list"); + expect(targets).toContain(".toolbay-list"); + }); }); diff --git a/frontend/help/tour.tsx b/frontend/help/tour.tsx index ece8f5064..3f2e6408f 100644 --- a/frontend/help/tour.tsx +++ b/frontend/help/tour.tsx @@ -6,6 +6,7 @@ import { TOUR_STEPS, tourPageNavigation } from "./tours"; import { t } from "../i18next_wrapper"; import { Actions } from "../constants"; import { store } from "../redux/store"; +import { ErrorBoundary } from "../error_boundary"; const strings = () => ({ back: t("Back"), @@ -65,15 +66,17 @@ export class Tour extends React.Component { return step; }); return
    - + + +
    ; } } diff --git a/frontend/help/tours.ts b/frontend/help/tours.ts index f01fe0dec..fc4339da5 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -2,6 +2,13 @@ import { history } from "../history"; import { Step as TourStep } from "react-joyride"; import { TourContent } from "../constants"; import { t } from "../i18next_wrapper"; +import { DevSettings } from "../account/dev/dev_support"; +import { selectAllTools } from "../resources/selectors"; +import { store } from "../redux/store"; +import { getFbosConfig } from "../resources/getters"; +import { + isExpressBoard, getFwHardwareValue +} from "../devices/components/firmware_hardware_support"; export enum Tours { gettingStarted = "gettingStarted", @@ -15,70 +22,105 @@ export const tourNames = () => [ { name: Tours.funStuff, description: t("find new features") }, ]; +const hasTools = () => + selectAllTools(store.getState().resources.index).length > 0; + +const isExpress = () => + isExpressBoard(getFwHardwareValue( + getFbosConfig(store.getState().resources.index))); + +const toolsStep = () => hasTools() + ? [{ + target: ".tools", + content: isExpress() + ? t(TourContent.ADD_SEED_CONTAINERS) + : t(TourContent.ADD_TOOLS), + title: isExpress() + ? t("Add seed containers") + : t("Add tools and seed containers"), + }] + : [{ + target: ".tools", + content: isExpress() + ? t(TourContent.ADD_SEED_CONTAINERS_AND_SLOTS) + : t(TourContent.ADD_TOOLS_AND_SLOTS), + title: isExpress() + ? t("Add seed containers and slots") + : t("Add tools and tool slots"), + }]; + +const toolSlotsStep = () => hasTools() + ? [{ + target: ".tool-slots", + content: t(TourContent.ADD_TOOLS_AND_SLOTS), + title: t("Add tool slots"), + }] + : []; + export const TOUR_STEPS = (): { [x: string]: TourStep[] } => ({ [Tours.gettingStarted]: [ { target: ".plant-inventory-panel", - content: TourContent.ADD_PLANTS, + content: t(TourContent.ADD_PLANTS), title: t("Add plants"), }, - { + ...(DevSettings.futureFeaturesEnabled() ? [{ target: ".tool-list", - content: TourContent.ADD_TOOLS, - title: t("Add tools"), - }, - { + content: t(TourContent.ADD_TOOLS), + title: t("Add tools and seed containers"), + }] : toolsStep()), + ...(DevSettings.futureFeaturesEnabled() ? [{ target: ".toolbay-list", - content: TourContent.ADD_TOOLS_SLOTS, + content: t(TourContent.ADD_TOOLS_SLOTS), title: t("Add tools to tool bay"), - }, + }] : toolSlotsStep()), { target: ".peripherals-widget", - content: TourContent.ADD_PERIPHERALS, + content: t(TourContent.ADD_PERIPHERALS), title: t("Add peripherals"), }, { target: ".sequence-list-panel", - content: TourContent.ADD_SEQUENCES, + content: t(TourContent.ADD_SEQUENCES), title: t("Create sequences"), }, { target: ".regimen-list-panel", - content: TourContent.ADD_REGIMENS, + content: t(TourContent.ADD_REGIMENS), title: t("Create regimens"), }, { target: ".farm-event-panel", - content: TourContent.ADD_FARM_EVENTS, + content: t(TourContent.ADD_FARM_EVENTS), title: t("Create events"), }, ], [Tours.monitoring]: [ { target: ".move-widget", - content: TourContent.LOCATION_GRID, + content: t(TourContent.LOCATION_GRID), title: t("View current location"), }, { target: ".farm-designer", - content: TourContent.VIRTUAL_FARMBOT, + content: t(TourContent.VIRTUAL_FARMBOT), title: t("View current location"), }, { target: ".logs-table", - content: TourContent.LOGS_TABLE, + content: t(TourContent.LOGS_TABLE), title: t("View log messages"), }, { target: ".photos", - content: TourContent.PHOTOS, + content: t(TourContent.PHOTOS), title: t("Take and view photos"), }, ], [Tours.funStuff]: [ { target: ".app-settings-widget", - content: TourContent.APP_SETTINGS, + content: t(TourContent.APP_SETTINGS), title: t("Customize your web app experience"), }, ], @@ -112,6 +154,10 @@ export const tourPageNavigation = (nextStepTarget: string | HTMLElement) => { case ".toolbay-list": history.push("/app/tools"); break; + case ".tools": + case ".tool-slots": + history.push("/app/designer/tools"); + break; case ".photos": history.push("/app/farmware"); break; diff --git a/frontend/messages/__tests__/alerts_test.tsx b/frontend/messages/__tests__/alerts_test.tsx index 08a61add7..8f85c4f3b 100644 --- a/frontend/messages/__tests__/alerts_test.tsx +++ b/frontend/messages/__tests__/alerts_test.tsx @@ -52,8 +52,7 @@ describe("", () => { const p = fakeProps(); p.alerts = [FIRMWARE_MISSING_ALERT, SEED_DATA_MISSING_ALERT]; const wrapper = mount(); - expect(wrapper.text()).toContain("2"); - expect(wrapper.text()).toContain("Your device has no firmware"); + expect(wrapper.text()).not.toContain("Your device has no firmware"); expect(wrapper.text()).toContain("Choose your FarmBot"); }); @@ -61,7 +60,6 @@ describe("", () => { const p = fakeProps(); p.alerts = [FIRMWARE_MISSING_ALERT, UNKNOWN_ALERT]; const wrapper = mount(); - expect(wrapper.text()).toContain("1"); expect(wrapper.text()).toContain("firmware: alert"); }); }); diff --git a/frontend/messages/alerts.tsx b/frontend/messages/alerts.tsx index 30ed30b09..5a1141819 100644 --- a/frontend/messages/alerts.tsx +++ b/frontend/messages/alerts.tsx @@ -34,6 +34,7 @@ export const Alerts = (props: AlertsProps) =>
    {sortAlerts(props.alerts) .filter(filterIncompleteAlerts) + .filter(x => x.problem_tag != "farmbot_os.firmware.missing") .map(x => { step: resourceUpdate({ label: "mounted_tool_id", value: 0 }), resourceIndex: fakeResourceIndex() }); - expect(result).toEqual(DISMOUNTED); + expect(result).toEqual(DISMOUNTED()); }); it("unpacks valid tool_ids", () => { @@ -37,7 +37,7 @@ describe("unpackStep()", () => { resourceIndex }); const actionLabel = "Mounted to: Generic Tool"; - const { label, value } = TOOL_MOUNT; + const { label, value } = TOOL_MOUNT(); assertGoodness(result, actionLabel, "mounted", label, value); }); @@ -47,7 +47,7 @@ describe("unpackStep()", () => { resourceIndex: fakeResourceIndex() }); const actionLabel = "Mounted to: an unknown tool"; - const { label, value } = TOOL_MOUNT; + const { label, value } = TOOL_MOUNT(); assertGoodness(result, actionLabel, "mounted", label, value); }); diff --git a/frontend/sequences/step_tiles/mark_as/unpack_step.ts b/frontend/sequences/step_tiles/mark_as/unpack_step.ts index ccf7a59f0..b4d5f7da8 100644 --- a/frontend/sequences/step_tiles/mark_as/unpack_step.ts +++ b/frontend/sequences/step_tiles/mark_as/unpack_step.ts @@ -9,15 +9,16 @@ import { GenericPointer } from "farmbot/dist/resources/api_resources"; import { MOUNTED_TO } from "./constants"; import { DropDownPair, StepWithResourceIndex } from "./interfaces"; import { TaggedPoint, TaggedPlantPointer } from "farmbot"; +import { t } from "../../../i18next_wrapper"; -export const TOOL_MOUNT: DropDownItem = { - label: "Tool Mount", value: "tool_mount" -}; -const NOT_IN_USE: DropDownItem = { label: "Not Mounted", value: 0 }; -export const DISMOUNTED: DropDownPair = { - leftSide: TOOL_MOUNT, - rightSide: NOT_IN_USE -}; +export const TOOL_MOUNT = (): DropDownItem => ({ + label: t("Tool Mount"), value: "tool_mount" +}); +const NOT_IN_USE = (): DropDownItem => ({ label: t("Not Mounted"), value: 0 }); +export const DISMOUNTED = (): DropDownPair => ({ + leftSide: TOOL_MOUNT(), + rightSide: NOT_IN_USE() +}); const DEFAULT_TOOL_NAME = "Untitled Tool"; const REMOVED_ACTION = { label: "Removed", value: "removed" }; @@ -30,13 +31,13 @@ function mountTool(i: StepWithResourceIndex): DropDownPair { if (typeof value === "number" && value > 0) { try { // Good tool id const tool = findToolById(i.resourceIndex, value as number); - return { leftSide: TOOL_MOUNT, rightSide: mountedTo(tool.body.name) }; + return { leftSide: TOOL_MOUNT(), rightSide: mountedTo(tool.body.name) }; } catch { // Bad tool ID or app still loading. - return { leftSide: TOOL_MOUNT, rightSide: mountedTo("an unknown tool") }; + return { leftSide: TOOL_MOUNT(), rightSide: mountedTo("an unknown tool") }; } } else { // No tool id - return DISMOUNTED; + return DISMOUNTED(); } } @@ -55,10 +56,10 @@ function unknownOption(i: StepWithResourceIndex): DropDownPair { /** The user wants to mark a the `discarded_at` attribute of a Point. */ function discardPoint(i: StepWithResourceIndex): DropDownPair { const { resource_id } = i.step.args; - const t = + const genericPointerBody = findPointerByTypeAndId(i.resourceIndex, "GenericPointer", resource_id).body; return { - leftSide: pointer2ddi(t as GenericPointer), + leftSide: pointer2ddi(genericPointerBody as GenericPointer), rightSide: REMOVED_ACTION }; } diff --git a/public/app-resources/languages/_helper.js b/public/app-resources/languages/_helper.js index c01b7d3b9..ae133fc46 100644 --- a/public/app-resources/languages/_helper.js +++ b/public/app-resources/languages/_helper.js @@ -48,7 +48,7 @@ var HelperNamespace = (function () { var T_REGEX = /[.{[(\s]t\(["`]([\w\s{}().,:'\-=\\?\/%!]*)["`],*\s*.*\)/g; // '``' - var C_REGEX = /[`]([\w\s{}().,:'\-=\\?"+!]*)[`].*/g; + var C_REGEX = /[`]([\w\s{}().,:'\-=\/\\?"+!]*)[`].*/g; /** Some additional phrases the regex can't find. */ var EXTRA_TAGS = [ @@ -60,6 +60,7 @@ var HelperNamespace = (function () { "Else Execute", "Connecting FarmBot to the Internet", "move to home", "emergency stop", "SYNC ERROR", "inactive", "error", "No messages.", "back to regimens", "back to sequences", "back to farmware list", + "Verify Password", ]; /** diff --git a/public/app-resources/languages/translation_metrics.md b/public/app-resources/languages/translation_metrics.md index e55daf997..08b1e8de3 100644 --- a/public/app-resources/languages/translation_metrics.md +++ b/public/app-resources/languages/translation_metrics.md @@ -21,20 +21,20 @@ For example, `sudo docker-compose run web npm run translation-check`._ See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app-into-your-language) for contribution instructions. -Total number of phrases identified by the language helper for translation: __1139__ +Total number of phrases identified by the language helper for translation: __1238__ |Language|Percent translated|Translated|Untranslated|Other Translations| |:---:|---:|---:|---:|---:| -|da|10%|109|1030|44| -|de|36%|413|726|141| -|es|88%|1002|137|173| -|fr|90%|1022|117|198| -|it|8%|91|1048|189| -|nl|7%|79|1060|161| -|pt|6%|71|1068|180| -|ru|52%|596|543|221| -|th|0%|0|1139|0| -|zh|8%|86|1053|161| +|da|8%|105|1133|77| +|de|32%|397|841|168| +|es|78%|965|273|210| +|fr|80%|985|253|242| +|it|7%|87|1151|215| +|nl|6%|75|1163|187| +|pt|5%|66|1172|207| +|ru|46%|575|663|246| +|th|0%|0|1238|0| +|zh|7%|82|1156|187| **Percent translated** refers to the percent of phrases identified by the language helper that have been translated. Additional phrases not identified