From 11f349ac89441e61a7e2e4724f3c3b9fd1945785 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 13 Mar 2020 14:06:02 -0700 Subject: [PATCH 1/4] dep updates (fe) --- package.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 58a807c65..590c32b66 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,18 @@ "author": "farmbot.io", "license": "MIT", "dependencies": { - "@babel/core": "7.8.4", - "@blueprintjs/core": "3.23.1", + "@babel/core": "7.8.7", + "@blueprintjs/core": "3.24.0", "@blueprintjs/datetime": "3.15.2", - "@blueprintjs/select": "3.11.2", + "@blueprintjs/select": "3.12.0", "@types/enzyme": "3.10.5", - "@types/jest": "25.1.3", + "@types/jest": "25.1.4", "@types/lodash": "4.14.149", "@types/markdown-it": "0.0.9", "@types/moxios": "0.4.9", - "@types/node": "13.7.4", + "@types/node": "13.9.1", "@types/promise-timeout": "1.3.0", - "@types/react": "16.9.22", + "@types/react": "16.9.23", "@types/react-color": "3.0.1", "@types/react-dom": "16.9.5", "@types/react-redux": "7.1.7", @@ -46,7 +46,7 @@ "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", "farmbot": "9.1.2", - "i18next": "19.3.1", + "i18next": "19.3.2", "install": "0.13.0", "lodash": "4.17.15", "markdown-it": "10.0.0", @@ -54,17 +54,17 @@ "moment": "2.24.0", "moxios": "0.4.0", "mqtt": "3.0.0", - "npm": "6.13.7", + "npm": "6.14.2", "parcel-bundler": "1.12.4", "promise-timeout": "1.3.0", "raf": "3.4.1", - "react": "16.12.0", + "react": "16.13.0", "react-addons-test-utils": "15.6.2", "react-color": "2.18.0", - "react-dom": "16.12.0", + "react-dom": "16.13.0", "react-joyride": "2.2.1", "react-redux": "7.2.0", - "react-test-renderer": "16.12.0", + "react-test-renderer": "16.13.0", "react-transition-group": "4.3.0", "redux": "4.0.5", "redux-immutable-state-invariant": "2.1.0", @@ -73,8 +73,8 @@ "takeme": "0.11.3", "ts-jest": "25.2.1", "ts-lint": "4.5.1", - "tslint": "6.0.0", - "typescript": "3.8.2", + "tslint": "6.1.0", + "typescript": "3.8.3", "which": "2.0.2" }, "devDependencies": { @@ -83,7 +83,7 @@ "jest-junit": "10.0.0", "jest-skipped-reporter": "0.0.5", "jshint": "2.11.0", - "madge": "3.7.0", - "sass": "1.25.0" + "madge": "3.8.0", + "sass": "1.26.3" } } From b1c2b36a37432deffef4a28c0e2490481deabdd7 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 13 Mar 2020 14:06:40 -0700 Subject: [PATCH 2/4] settings refactoring --- frontend/app.tsx | 10 +- frontend/auth/__tests__/actions_test.ts | 5 +- frontend/auth/actions.ts | 5 +- frontend/connectivity/reducer_support.ts | 2 +- frontend/constants.ts | 2 + frontend/controls/__tests__/controls_test.tsx | 14 +- frontend/controls/controls.tsx | 7 +- frontend/controls/interfaces.ts | 3 +- .../controls/move/__tests__/move_test.tsx | 1 - frontend/controls/move/interfaces.ts | 2 - frontend/controls/move/move.tsx | 8 +- frontend/controls/state_to_props.ts | 2 - frontend/controls/toggle_button.tsx | 10 +- frontend/css/global.scss | 33 +-- frontend/devices/__tests__/actions_test.ts | 49 +++- frontend/devices/__tests__/devices_test.tsx | 29 --- frontend/devices/__tests__/reducer_test.ts | 30 ++- frontend/devices/actions.ts | 34 ++- .../__tests__/farmbot_os_settings_test.tsx | 137 ++++------- .../__tests__/hardware_settings_test.tsx | 27 ++- .../__tests__/lockable_button_test.tsx | 32 ++- .../__tests__/mcu_input_box_test.tsx | 23 +- .../components/boolean_mcu_input_group.tsx | 70 +++--- .../components/farmbot_os_settings.tsx | 229 +++++++----------- .../__tests__/board_type_test.tsx | 33 ++- .../__tests__/boot_sequence_selector_test.tsx | 16 +- .../__tests__/farmbot_os_row_test.tsx | 14 +- .../__tests__/fbos_details_test.tsx | 4 +- .../firmware_hardware_status_test.tsx | 16 -- .../fbos_settings/__tests__/firmware_test.tsx | 57 +++++ .../__tests__/last_seen_row_test.tsx | 44 ++-- .../fbos_settings/__tests__/name_row_test.tsx | 34 +++ .../__tests__/ota_time_selector_test.tsx | 78 ++++-- .../__tests__/power_and_reset_test.tsx | 34 +-- .../__tests__/timezone_row_test.tsx | 34 +++ .../fbos_settings/auto_sync_row.tsx | 26 +- .../fbos_settings/auto_update_row.tsx | 27 ++- .../components/fbos_settings/board_type.tsx | 48 ++-- .../fbos_settings/boot_sequence_selector.tsx | 36 ++- .../fbos_settings/camera_selection.tsx | 12 +- .../fbos_settings/factory_reset_row.tsx | 78 +++--- .../fbos_settings/farmbot_os_row.tsx | 135 +++++++---- .../fbos_settings/fbos_button_row.tsx | 26 +- .../components/fbos_settings/firmware.tsx | 49 ++++ .../firmware_hardware_status.tsx | 21 -- .../fbos_settings/flash_firmware_row.tsx | 42 ++++ .../components/fbos_settings/interfaces.ts | 31 ++- .../fbos_settings/last_seen_row.tsx | 8 + .../components/fbos_settings/name_row.tsx | 34 +++ .../fbos_settings/ota_time_selector.tsx | 19 +- .../fbos_settings/power_and_reset.tsx | 11 +- .../components/fbos_settings/timezone_row.tsx | 51 ++++ .../devices/components/hardware_settings.tsx | 16 +- .../__tests__/calibration_row_test.tsx | 2 +- .../__tests__/homing_and_calibration_test.tsx | 2 +- .../hardware_settings/calibration_row.tsx | 50 ++-- .../hardware_settings/danger_zone.tsx | 32 +-- .../components/hardware_settings/encoders.tsx | 4 +- .../components/hardware_settings/endstops.tsx | 4 +- .../hardware_settings/error_handling.tsx | 4 +- .../homing_and_calibration.tsx | 12 +- .../components/hardware_settings/motors.tsx | 6 +- .../hardware_settings/pin_guard.tsx | 43 ++-- .../hardware_settings/single_setting_row.tsx | 23 +- .../hardware_settings/space_panel_header.tsx | 42 ++-- frontend/devices/components/interfaces.ts | 6 +- .../devices/components/lockable_button.tsx | 5 +- .../devices/components/maybe_highlight.tsx | 14 +- .../components/numeric_mcu_input_group.tsx | 55 +++-- .../components/pin_guard_input_group.tsx | 134 +++++++--- frontend/devices/devices.tsx | 18 +- frontend/devices/interfaces.ts | 30 ++- frontend/devices/must_be_online.tsx | 8 + .../pin_bindings/pin_binding_input_group.tsx | 96 +++++--- .../devices/pin_bindings/pin_bindings.tsx | 9 +- .../pin_bindings/pin_bindings_list.tsx | 33 ++- frontend/devices/reducer.ts | 41 ++-- frontend/devices/state_to_props.ts | 13 +- .../__tests__/search_selectors_test.ts | 2 +- frontend/farm_designer/move_to.tsx | 7 +- .../plants/__tests__/crop_info_test.tsx | 6 +- frontend/farm_designer/plants/crop_info.tsx | 2 +- frontend/farm_designer/search_selectors.ts | 15 +- .../tools/__tests__/index_test.tsx | 5 +- frontend/farm_designer/tools/index.tsx | 12 +- frontend/messages/alerts.tsx | 5 +- 86 files changed, 1533 insertions(+), 1005 deletions(-) create mode 100644 frontend/devices/components/fbos_settings/__tests__/firmware_test.tsx create mode 100644 frontend/devices/components/fbos_settings/__tests__/name_row_test.tsx create mode 100644 frontend/devices/components/fbos_settings/__tests__/timezone_row_test.tsx create mode 100644 frontend/devices/components/fbos_settings/firmware.tsx create mode 100644 frontend/devices/components/fbos_settings/flash_firmware_row.tsx create mode 100644 frontend/devices/components/fbos_settings/name_row.tsx create mode 100644 frontend/devices/components/fbos_settings/timezone_row.tsx diff --git a/frontend/app.tsx b/frontend/app.tsx index cdf2c1519..6a32eb57d 100644 --- a/frontend/app.tsx +++ b/frontend/app.tsx @@ -26,11 +26,11 @@ import { getFirmwareConfig, getFbosConfig } from "./resources/getters"; import { intersection } from "lodash"; import { t } from "./i18next_wrapper"; import { ResourceIndex } from "./resources/interfaces"; -import { isBotOnline } from "./devices/must_be_online"; -import { getStatus } from "./connectivity/reducer_support"; +import { isBotOnlineFromState } from "./devices/must_be_online"; import { getAllAlerts } from "./messages/state_to_props"; import { PingDictionary } from "./devices/connectivity/qos"; import { getEnv, getShouldDisplayFn } from "./farmware/state_to_props"; +import { filterAlerts } from "./messages/alerts"; /** For the logger module */ init(); @@ -81,7 +81,7 @@ export function mapStateToProps(props: Everything): AppProps { tour: props.resources.consumers.help.currentTour, resources: props.resources.index, autoSync: !!(fbosConfig && fbosConfig.auto_sync), - alertCount: getAllAlerts(props.resources).length, + alertCount: getAllAlerts(props.resources).filter(filterAlerts).length, pings: props.bot.connectivity.pings, env, }; @@ -124,8 +124,6 @@ export class RawApp extends React.Component { const syncLoaded = this.isLoaded; const currentPage = getPathArray()[2]; const { location_data, mcu_params } = this.props.bot.hardware; - const { sync_status } = this.props.bot.hardware.informational_settings; - const bot2mqtt = this.props.bot.connectivity.uptime["bot.mqtt"]; return
{!syncLoaded && } @@ -151,7 +149,7 @@ export class RawApp extends React.Component { firmwareSettings={this.props.firmwareConfig || mcu_params} xySwap={this.props.xySwap} arduinoBusy={!!this.props.bot.hardware.informational_settings.busy} - botOnline={isBotOnline(sync_status, getStatus(bot2mqtt))} + botOnline={isBotOnlineFromState(this.props.bot)} env={this.props.env} stepSize={this.props.bot.stepSize} />}
; diff --git a/frontend/auth/__tests__/actions_test.ts b/frontend/auth/__tests__/actions_test.ts index 29d9aa9f6..c3e7bb9a4 100644 --- a/frontend/auth/__tests__/actions_test.ts +++ b/frontend/auth/__tests__/actions_test.ts @@ -3,8 +3,8 @@ jest.mock("axios", () => ({ response: { use: jest.fn() }, request: { use: jest.fn() } }, - post: jest.fn(() => { return Promise.resolve({ data: { foo: "bar" } }); }), - get: jest.fn(() => { return Promise.resolve({ data: { foo: "bar" } }); }), + post: jest.fn(() => Promise.resolve({ data: { foo: "bar" } })), + get: jest.fn(() => Promise.resolve({ data: { foo: "bar" } })), })); jest.mock("../../api/api", () => ({ @@ -22,6 +22,7 @@ jest.mock("../../devices/actions", () => ({ fetchReleases: jest.fn(), fetchLatestGHBetaRelease: jest.fn(), fetchMinOsFeatureData: jest.fn(), + fetchOsReleaseNotes: jest.fn(), })); import { didLogin } from "../actions"; diff --git a/frontend/auth/actions.ts b/frontend/auth/actions.ts index f8d417137..10199c84f 100644 --- a/frontend/auth/actions.ts +++ b/frontend/auth/actions.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { fetchReleases, fetchMinOsFeatureData, fetchLatestGHBetaRelease, + fetchOsReleaseNotes, } from "../devices/actions"; import { AuthState } from "./interfaces"; import { ReduxAction } from "../redux/interfaces"; @@ -16,7 +17,6 @@ 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); @@ -25,7 +25,8 @@ 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(ExternalUrl.featureMinVersions)); + dispatch(fetchMinOsFeatureData()); + dispatch(fetchOsReleaseNotes()); dispatch(setToken(authState)); Sync.fetchSyncData(dispatch); dispatch(connectDevice(authState)); diff --git a/frontend/connectivity/reducer_support.ts b/frontend/connectivity/reducer_support.ts index 41db3fdee..d86e0434a 100644 --- a/frontend/connectivity/reducer_support.ts +++ b/frontend/connectivity/reducer_support.ts @@ -1,5 +1,5 @@ import { ConnectionStatus } from "./interfaces"; export function getStatus(cs: ConnectionStatus | undefined): "up" | "down" { - return (cs && cs.state) || "down"; + return cs?.state || "down"; } diff --git a/frontend/constants.ts b/frontend/constants.ts index 15b21f9a1..b0abe47c0 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -1127,6 +1127,8 @@ export enum Actions { FETCH_BETA_OS_UPDATE_INFO_ERROR = "FETCH_BETA_OS_UPDATE_INFO_ERROR", FETCH_MIN_OS_FEATURE_INFO_OK = "FETCH_MIN_OS_FEATURE_INFO_OK", FETCH_MIN_OS_FEATURE_INFO_ERROR = "FETCH_MIN_OS_FEATURE_INFO_ERROR", + FETCH_OS_RELEASE_NOTES_OK = "FETCH_OS_RELEASE_NOTES_OK", + FETCH_OS_RELEASE_NOTES_ERROR = "FETCH_OS_RELEASE_NOTES_ERROR", INVERT_JOG_BUTTON = "INVERT_JOG_BUTTON", DISPLAY_ENCODER_DATA = "DISPLAY_ENCODER_DATA", STASH_STATUS = "STASH_STATUS", diff --git a/frontend/controls/__tests__/controls_test.tsx b/frontend/controls/__tests__/controls_test.tsx index aba980723..396de7375 100644 --- a/frontend/controls/__tests__/controls_test.tsx +++ b/frontend/controls/__tests__/controls_test.tsx @@ -18,10 +18,9 @@ describe("", () => { feeds: [fakeWebcamFeed()], peripherals: [fakePeripheral()], sensors: [fakeSensor()], - botToMqttStatus: "up", firmwareSettings: bot.hardware.mcu_params, shouldDisplay: () => true, - getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), + getWebAppConfigVal: jest.fn(key => mockConfig[key]), sensorReadings: [], timeSettings: fakeTimeSettings(), env: {}, @@ -65,6 +64,17 @@ describe("", () => { .map(string => expect(txt).not.toContain(string)); }); + it("hides sensors widget based on model", () => { + mockConfig.hide_sensors = false; + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + const txt = wrapper.text().toLowerCase(); + ["move", "peripherals"] + .map(string => expect(txt).toContain(string)); + ["sensors"].map(string => expect(txt).not.toContain(string)); + }); + it("doesn't show sensor readings widget", () => { const p = fakeProps(); mockConfig.hide_sensors = true; diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index 5149af504..969e34256 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -9,7 +9,7 @@ import { Props } from "./interfaces"; import { Move } from "./move/move"; import { BooleanSetting } from "../session_keys"; import { SensorReadings } from "./sensor_readings/sensor_readings"; -import { isBotOnline } from "../devices/must_be_online"; +import { isBotOnlineFromState } from "../devices/must_be_online"; import { hasSensors } from "../devices/components/firmware_hardware_support"; /** Controls page. */ @@ -19,9 +19,7 @@ export class RawControls extends React.Component { } get botOnline() { - return isBotOnline( - this.props.bot.hardware.informational_settings.sync_status, - this.props.botToMqttStatus); + return isBotOnlineFromState(this.props.bot); } get hideSensors() { @@ -34,7 +32,6 @@ export class RawControls extends React.Component { env={this.props.env} dispatch={this.props.dispatch} arduinoBusy={this.arduinoBusy} - botToMqttStatus={this.props.botToMqttStatus} firmwareSettings={this.props.firmwareSettings} firmwareHardware={this.props.firmwareHardware} getWebAppConfigVal={this.props.getWebAppConfigVal} /> diff --git a/frontend/controls/interfaces.ts b/frontend/controls/interfaces.ts index 038dda123..93f1e8069 100644 --- a/frontend/controls/interfaces.ts +++ b/frontend/controls/interfaces.ts @@ -8,7 +8,6 @@ import { TaggedSensor, TaggedSensorReading, } from "farmbot"; -import { NetworkState } from "../connectivity/interfaces"; import { GetWebAppConfigValue } from "../config_storage/actions"; import { TimeSettings } from "../interfaces"; @@ -18,7 +17,6 @@ export interface Props { feeds: TaggedWebcamFeed[]; peripherals: TaggedPeripheral[]; sensors: TaggedSensor[]; - botToMqttStatus: NetworkState; firmwareSettings: McuParams; shouldDisplay: ShouldDisplay; getWebAppConfigVal: GetWebAppConfigValue; @@ -60,4 +58,5 @@ export interface ToggleButtonProps { dim?: boolean; grayscale?: boolean; title?: string; + className?: string; } diff --git a/frontend/controls/move/__tests__/move_test.tsx b/frontend/controls/move/__tests__/move_test.tsx index eae4f86f4..7ef748009 100644 --- a/frontend/controls/move/__tests__/move_test.tsx +++ b/frontend/controls/move/__tests__/move_test.tsx @@ -29,7 +29,6 @@ describe("", () => { dispatch: jest.fn(), bot: bot, arduinoBusy: false, - botToMqttStatus: "up", firmwareSettings: bot.hardware.mcu_params, getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), env: {}, diff --git a/frontend/controls/move/interfaces.ts b/frontend/controls/move/interfaces.ts index 6d549b46e..914006f4e 100644 --- a/frontend/controls/move/interfaces.ts +++ b/frontend/controls/move/interfaces.ts @@ -1,6 +1,5 @@ import { BotPosition, BotState, UserEnv } from "../../devices/interfaces"; import { McuParams, Xyz, FirmwareHardware } from "farmbot"; -import { NetworkState } from "../../connectivity/interfaces"; import { GetWebAppConfigValue } from "../../config_storage/actions"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; @@ -11,7 +10,6 @@ export interface MoveProps { dispatch: Function; bot: BotState; arduinoBusy: boolean; - botToMqttStatus: NetworkState; firmwareSettings: McuParams; getWebAppConfigVal: GetWebAppConfigValue; env: UserEnv; diff --git a/frontend/controls/move/move.tsx b/frontend/controls/move/move.tsx index dd3916082..f752a6406 100644 --- a/frontend/controls/move/move.tsx +++ b/frontend/controls/move/move.tsx @@ -13,6 +13,7 @@ import { MotorPositionPlot } from "./motor_position_plot"; import { Popover, Position } from "@blueprintjs/core"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { t } from "../../i18next_wrapper"; +import { getStatus } from "../../connectivity/reducer_support"; export class Move extends React.Component { @@ -23,7 +24,8 @@ export class Move extends React.Component { !!this.props.getWebAppConfigVal(BooleanSetting[key]); render() { - const { location_data, informational_settings } = this.props.bot.hardware; + const { bot } = this.props; + const { location_data, informational_settings } = bot.hardware; const locationData = validBotLocationData(location_data); return { props), shouldDisplay, diff --git a/frontend/controls/toggle_button.tsx b/frontend/controls/toggle_button.tsx index a724ee954..cf2c24f2a 100644 --- a/frontend/controls/toggle_button.tsx +++ b/frontend/controls/toggle_button.tsx @@ -40,12 +40,16 @@ export class ToggleButton extends React.Component { } render() { - const addCss = (this.props.dim ? " dim" : "") - + (this.props.grayscale ? " grayscale" : ""); + const allCss = [ + this.css(), + this.props.className, + this.props.dim ? "dim" : "", + this.props.grayscale ? "grayscale" : "", + ].join(" "); const cb = () => !this.props.disabled && this.props.toggleAction(); return - - ; + + {newFormat &&

{t(props.description)}

} + ; }; diff --git a/frontend/devices/components/fbos_settings/firmware.tsx b/frontend/devices/components/fbos_settings/firmware.tsx new file mode 100644 index 000000000..001138c4a --- /dev/null +++ b/frontend/devices/components/fbos_settings/firmware.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { Header } from "../hardware_settings/header"; +import { Collapse } from "@blueprintjs/core"; +import { FirmwareProps } from "./interfaces"; +import { FbosButtonRow } from "./fbos_button_row"; +import { Content, DeviceSetting } from "../../../constants"; +import { restartFirmware } from "../../actions"; +import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; +import { BoardType } from "./board_type"; +import { isFwHardwareValue } from "../firmware_hardware_support"; +import { FlashFirmwareRow } from "./flash_firmware_row"; + +export function Firmware(props: FirmwareProps) { + const { dispatch, sourceFbosConfig, botOnline } = props; + const { firmware } = props.bot.controlPanelState; + + const { value } = props.sourceFbosConfig("firmware_hardware"); + const firmwareHardware = isFwHardwareValue(value) ? value : undefined; + return +
+ + + + + + ; +} diff --git a/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx b/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx index b85c3b3f0..48f859534 100644 --- a/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx +++ b/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx @@ -6,7 +6,6 @@ import { t } from "../../../i18next_wrapper"; import { BotState } from "../../interfaces"; import { FirmwareAlerts } from "../../../messages/alerts"; import { TimeSettings } from "../../../interfaces"; -import { trim } from "../../../util"; import { Alert } from "farmbot"; import { isFwHardwareValue, boardType } from "../firmware_hardware_support"; @@ -56,22 +55,6 @@ export const FlashFirmwareBtn = (props: FlashFirmwareBtnProps) => { ; }; -export interface FirmwareActionsProps { - apiFirmwareValue: string | undefined; - botOnline: boolean; -} - -export const FirmwareActions = (props: FirmwareActionsProps) => { - const { apiFirmwareValue } = props; - return
-

- {trim(`${t("Flash the")} ${lookup(apiFirmwareValue) || ""} - ${t("firmware to your device")}:`)} -

- -
; -}; - export const FirmwareHardwareStatusDetails = (props: FirmwareHardwareStatusDetailsProps) => { return
@@ -81,10 +64,6 @@ export const FirmwareHardwareStatusDetails =

{lookup(props.botFirmwareValue) || t("unknown")}

{lookup(props.mcuFirmwareValue) || t("unknown")}

- - { + + Description = () => +

+ {trim(`${t("Flash the")} ${lookup(this.props.firmwareHardware) || ""} + ${t("firmware to your device")}:`)} +

; + + render() { + const newFormat = DevSettings.futureFeaturesEnabled(); + + return + + + + + {!newFormat && + + } + + + + + {newFormat && } + ; + } +} diff --git a/frontend/devices/components/fbos_settings/interfaces.ts b/frontend/devices/components/fbos_settings/interfaces.ts index 7cf855a65..ce7dc6413 100644 --- a/frontend/devices/components/fbos_settings/interfaces.ts +++ b/frontend/devices/components/fbos_settings/interfaces.ts @@ -10,9 +10,21 @@ import { Alert, InformationalSettings, TaggedDevice, + FirmwareHardware, } from "farmbot"; import { TimeSettings } from "../../../interfaces"; +export interface NameRowProps { + dispatch: Function; + device: TaggedDevice; + widget?: boolean; +} + +export interface TimezoneRowProps { + dispatch: Function; + device: TaggedDevice; +} + export interface AutoSyncRowProps { dispatch: Function; sourceFbosConfig: SourceFbosConfig; @@ -50,6 +62,22 @@ export interface BoardTypeProps { shouldDisplay: ShouldDisplay; timeSettings: TimeSettings; sourceFbosConfig: SourceFbosConfig; + firmwareHardware: FirmwareHardware | undefined; +} + +export interface FirmwareProps { + botOnline: boolean; + bot: BotState; + alerts: Alert[]; + dispatch: Function; + shouldDisplay: ShouldDisplay; + timeSettings: TimeSettings; + sourceFbosConfig: SourceFbosConfig; +} + +export interface FlashFirmwareRowProps { + botOnline: boolean; + firmwareHardware: FirmwareHardware | undefined; } export interface PowerAndResetProps { @@ -67,13 +95,10 @@ export interface FactoryResetRowsProps { export interface FarmbotOsRowProps { bot: BotState; - osReleaseNotesHeading: string; - osReleaseNotes: string; dispatch: Function; sourceFbosConfig: SourceFbosConfig; shouldDisplay: ShouldDisplay; botOnline: boolean; - botToMqttLastSeen: number; timeSettings: TimeSettings; deviceAccount: TaggedDevice; } diff --git a/frontend/devices/components/fbos_settings/last_seen_row.tsx b/frontend/devices/components/fbos_settings/last_seen_row.tsx index 607d6476e..71aee37a4 100644 --- a/frontend/devices/components/fbos_settings/last_seen_row.tsx +++ b/frontend/devices/components/fbos_settings/last_seen_row.tsx @@ -6,6 +6,14 @@ import { t } from "../../../i18next_wrapper"; import { TimeSettings } from "../../../interfaces"; import { timeFormatString } from "../../../util"; import { refresh } from "../../../api/crud"; +import { BotState } from "../../interfaces"; + +export const getLastSeenNumber = (bot: BotState): number => { + const { uptime } = bot.connectivity; + const bot2Mqtt = uptime["bot.mqtt"]; + const botToMqttLastSeen = bot2Mqtt?.state === "up" ? bot2Mqtt.at : ""; + return new Date(botToMqttLastSeen).getTime(); +}; export interface LastSeenProps { dispatch: Function; diff --git a/frontend/devices/components/fbos_settings/name_row.tsx b/frontend/devices/components/fbos_settings/name_row.tsx new file mode 100644 index 000000000..8dfb16313 --- /dev/null +++ b/frontend/devices/components/fbos_settings/name_row.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { Row, Col } from "../../../ui/index"; +import { DeviceSetting } from "../../../constants"; +import { ColWidth } from "../farmbot_os_settings"; +import { NameRowProps } from "./interfaces"; +import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; +import { edit, save } from "../../../api/crud"; +import { DevSettings } from "../../../account/dev/dev_support"; + +export class NameRow extends React.Component { + NameInput = () => + this.props.dispatch(edit(this.props.device, { + name: e.currentTarget.value + }))} + onBlur={() => this.props.dispatch(save(this.props.device.uuid))} + value={this.props.device.body.name} />; + + render() { + const newFormat = DevSettings.futureFeaturesEnabled(); + return + + + + + {!newFormat && } + + {newFormat && } + ; + } +} diff --git a/frontend/devices/components/fbos_settings/ota_time_selector.tsx b/frontend/devices/components/fbos_settings/ota_time_selector.tsx index 425579545..d11b11e9e 100644 --- a/frontend/devices/components/fbos_settings/ota_time_selector.tsx +++ b/frontend/devices/components/fbos_settings/ota_time_selector.tsx @@ -7,6 +7,7 @@ import { ColWidth } from "../farmbot_os_settings"; import { DeviceSetting } from "../../../constants"; import { Highlight } from "../maybe_highlight"; import { OtaTimeSelectorRowProps } from "./interfaces"; +import { DevSettings } from "../../../account/dev/dev_support"; // tslint:disable-next-line:no-null-keyword const UNDEFINED = null as unknown as undefined; @@ -40,7 +41,7 @@ type HOUR = | 23; type TimeTable = Record; type EveryTimeTable = Record; -const ASAP = () => t("As soon as possible"); +export const ASAP = () => t("As soon as possible"); const TIME_TABLE_12H = (): TimeTable => ({ 0: { label: t("Midnight"), value: 0 }, 1: { label: "1:00 AM", value: 1 }, @@ -102,7 +103,7 @@ const TIME_FORMATS = (): EveryTimeTable => ({ "24h": TIME_TABLE_24H() }); -interface OtaTimeSelectorProps { +export interface OtaTimeSelectorProps { disabled: boolean; timeFormat: PreferredHourFormat; onChange(hour24: number | undefined): void; @@ -116,7 +117,8 @@ export const changeOtaHour = dispatch(save(device.uuid)); }; -export function assertIsHour(val: number | undefined): asserts val is (HOUR | undefined) { +export function assertIsHour( + val: number | undefined): asserts val is (HOUR | undefined) { if ((val === null) || (val === undefined)) { return; } @@ -147,9 +149,10 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => { .sort((_x, _y) => (_x.value > _y.value) ? 1 : -1); const selectedItem = (typeof value == "number") ? theTimeTable[value as HOUR] : theTimeTable[DEFAULT_HOUR]; - return - - + const newFormat = DevSettings.futureFeaturesEnabled(); + return + + @@ -161,8 +164,8 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => { list={list} extraClass={disabled ? "disabled" : ""} /> - - ; + + ; }; export function OtaTimeSelectorRow(props: OtaTimeSelectorRowProps) { diff --git a/frontend/devices/components/fbos_settings/power_and_reset.tsx b/frontend/devices/components/fbos_settings/power_and_reset.tsx index 5464fc31c..670dbaa63 100644 --- a/frontend/devices/components/fbos_settings/power_and_reset.tsx +++ b/frontend/devices/components/fbos_settings/power_and_reset.tsx @@ -6,10 +6,9 @@ import { PowerAndResetProps } from "./interfaces"; import { ChangeOwnershipForm } from "./change_ownership_form"; import { FbosButtonRow } from "./fbos_button_row"; import { Content, DeviceSetting } from "../../../constants"; -import { reboot, powerOff, restartFirmware } from "../../actions"; +import { reboot, powerOff } from "../../actions"; import { t } from "../../../i18next_wrapper"; import { Highlight } from "../maybe_highlight"; -import { DevSettings } from "../../../account/dev/dev_support"; export function PowerAndReset(props: PowerAndResetProps) { const { dispatch, sourceFbosConfig, botOnline } = props; @@ -36,14 +35,6 @@ export function PowerAndReset(props: PowerAndResetProps) { buttonText={t("SHUTDOWN")} color={"red"} action={powerOff} /> - {!DevSettings.futureFeaturesEnabled() && - } { + + Note = () => +
+ {timezoneMismatch(this.props.device.body.timezone) + ? t(Content.DIFFERENT_TZ_WARNING) : ""} +
; + + Selector = () => + { + this.props.dispatch(edit(this.props.device, { timezone })); + this.props.dispatch(save(this.props.device.uuid)); + }} />; + + render() { + const newFormat = DevSettings.futureFeaturesEnabled(); + + return + + + + + {!newFormat && + + + + } + + {newFormat && + + + } + ; + } +} diff --git a/frontend/devices/components/hardware_settings.tsx b/frontend/devices/components/hardware_settings.tsx index 06be4f1a0..0bd20fe45 100644 --- a/frontend/devices/components/hardware_settings.tsx +++ b/frontend/devices/components/hardware_settings.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { MCUFactoryReset, bulkToggleControlPanel } from "../actions"; import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index"; import { HardwareSettingsProps, SourceFwConfig } from "../interfaces"; -import { isBotOnline } from "../must_be_online"; +import { isBotOnlineFromState } from "../must_be_online"; import { ToolTips } from "../../constants"; import { DangerZone } from "./hardware_settings/danger_zone"; import { PinGuard } from "./hardware_settings/pin_guard"; @@ -17,24 +17,18 @@ 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"; import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import type { McuParamName } from "farmbot"; export class HardwareSettings extends React.Component { - componentDidMount = () => - this.props.dispatch(maybeOpenPanel(this.props.controlPanelState)); - render() { const { bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig, - botToMqttStatus, firmwareHardware, resources + firmwareHardware, resources } = this.props; - const { informational_settings } = this.props.bot.hardware; - const { sync_status } = informational_settings; - const botDisconnected = !isBotOnline(sync_status, botToMqttStatus); + const botOnline = !isBotOnlineFromState(bot); const commonProps = { dispatch, controlPanelState }; return @@ -63,7 +57,7 @@ export class HardwareSettings extends sourceFwConfig={sourceFwConfig} firmwareConfig={firmwareConfig} firmwareHardware={firmwareHardware} - botDisconnected={botDisconnected} /> + botOnline={botOnline} /> @@ -82,7 +76,7 @@ export class HardwareSettings extends sourceFwConfig={sourceFwConfig} /> + botOnline={botOnline} /> ; } 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 e7c47b59b..34673fcd7 100644 --- a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx @@ -9,7 +9,7 @@ describe("", () => { const fakeProps = (): CalibrationRowProps => ({ type: "calibrate", hardware: bot.hardware.mcu_params, - botDisconnected: false, + botOnline: true, action: jest.fn(), toolTip: "calibrate", title: DeviceSetting.calibration, 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 2b951aacf..0c272b531 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 @@ -33,7 +33,7 @@ describe("", () => { value: bot.hardware.mcu_params[x], consistent: true }), firmwareConfig: fakeFirmwareConfig().body, - botDisconnected: false, + botOnline: true, firmwareHardware: undefined, }); diff --git a/frontend/devices/components/hardware_settings/calibration_row.tsx b/frontend/devices/components/hardware_settings/calibration_row.tsx index 145020de3..10963de0f 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -6,33 +6,45 @@ import { CalibrationRowProps } from "../interfaces"; import { t } from "../../../i18next_wrapper"; import { Position } from "@blueprintjs/core"; import { Highlight } from "../maybe_highlight"; +import { DevSettings } from "../../../account/dev/dev_support"; -export function CalibrationRow(props: CalibrationRowProps) { +export class CalibrationRow extends React.Component { - const { hardware, botDisconnected } = props; + get newFormat() { return DevSettings.futureFeaturesEnabled(); } - return - - - - - + Axes = () => { + const { type, botOnline, axisTitle, hardware, action } = this.props; + return
{axisTrackingStatus(hardware) .map(row => { const { axis } = row; - const hardwareDisabled = props.type == "zero" ? false : row.disabled; - return + const hardwareDisabled = type == "zero" ? false : row.disabled; + return props.action(axis)}> - {`${t(props.axisTitle)} ${axis}`} + disabled={hardwareDisabled || !botOnline} + title={t(axisTitle)} + onClick={() => action(axis)}> + {`${t(axisTitle)} ${axis}`} ; })} - - ; +
; + } + + render() { + return + + + + + + {!this.newFormat && } + + {this.newFormat && } + ; + } } diff --git a/frontend/devices/components/hardware_settings/danger_zone.tsx b/frontend/devices/components/hardware_settings/danger_zone.tsx index 0d0d941eb..140e08801 100644 --- a/frontend/devices/components/hardware_settings/danger_zone.tsx +++ b/frontend/devices/components/hardware_settings/danger_zone.tsx @@ -6,12 +6,13 @@ import { Collapse } from "@blueprintjs/core"; import { Content, DeviceSetting } from "../../../constants"; import { t } from "../../../i18next_wrapper"; import { Highlight } from "../maybe_highlight"; +import { DevSettings } from "../../../account/dev/dev_support"; export function DangerZone(props: DangerZoneProps) { - const { dispatch, onReset, botDisconnected } = props; + const { dispatch, onReset, botOnline } = props; const { danger_zone } = props.controlPanelState; - + const newFormat = DevSettings.futureFeaturesEnabled(); return
- - - + + + - -

- {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} -

- - + {!newFormat && + +

+ {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} +

+ } + -
-
+ + {newFormat && +

{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 40a7fa219..60f9d6213 100644 --- a/frontend/devices/components/hardware_settings/encoders.tsx +++ b/frontend/devices/components/hardware_settings/encoders.tsx @@ -31,9 +31,7 @@ export function Encoders(props: EncodersProps) { panel={"encoders"} dispatch={dispatch} /> -
- -
+ -
- -
+ -
- -
+ -
- -
+ + botOnline={botOnline} /> getDevice().calibrate({ axis }) .catch(commandErr("Calibration"))} hardware={hardware} - botDisconnected={botDisconnected} /> + botOnline={botOnline} /> getDevice().setZero(axis) .catch(commandErr("Zeroing"))} hardware={hardware} - botDisconnected={botDisconnected} /> + botOnline={botOnline} /> -
- -
+ dispatch( @@ -149,6 +148,7 @@ export function Motors(props: MotorsProps) { label={DeviceSetting.invert2ndXMotor} tooltip={ToolTips.INVERT_MOTORS}>
- - - - - - - - - - - - + {!newFormat && + + + + + + + + + + + + } - - - + ({ label, tooltip, settingType, children }: SingleSettingRowProps) => { + const newFormat = DevSettings.futureFeaturesEnabled(); + return + + - + {settingType === "button" - ? {children} - : {children}} - - ; + ? + {children} + + : {children}} + + ; + }; diff --git a/frontend/devices/components/hardware_settings/space_panel_header.tsx b/frontend/devices/components/hardware_settings/space_panel_header.tsx index dfb70cedd..b9b052abc 100644 --- a/frontend/devices/components/hardware_settings/space_panel_header.tsx +++ b/frontend/devices/components/hardware_settings/space_panel_header.tsx @@ -1,23 +1,29 @@ import * as React from "react"; import { Row, Col } from "../../../ui/index"; import { t } from "../../../i18next_wrapper"; +import { DevSettings } from "../../../account/dev/dev_support"; -export function SpacePanelHeader(_: {}) { - return - - - - - - - - - - ; +export function SpacePanelHeader() { + const newFormat = DevSettings.futureFeaturesEnabled(); + const width = newFormat ? 4 : 2; + const offset = newFormat ? 0 : 6; + return
+ + + + + + + + + + + +
; } diff --git a/frontend/devices/components/interfaces.ts b/frontend/devices/components/interfaces.ts index cfb683fd7..823e141f7 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -18,7 +18,7 @@ export interface HomingAndCalibrationProps { controlPanelState: ControlPanelState; sourceFwConfig: SourceFwConfig; firmwareConfig: FirmwareConfig | undefined; - botDisconnected: boolean; + botOnline: boolean; firmwareHardware: FirmwareHardware | undefined; } @@ -39,7 +39,7 @@ export interface BooleanMCUInputGroupProps { export interface CalibrationRowProps { type: "find_home" | "calibrate" | "zero"; hardware: McuParams; - botDisconnected: boolean; + botOnline: boolean; action(axis: Axis): void; toolTip: string; title: DeviceSetting; @@ -116,5 +116,5 @@ export interface DangerZoneProps { dispatch: Function; controlPanelState: ControlPanelState; onReset(): void; - botDisconnected: boolean; + botOnline: boolean; } diff --git a/frontend/devices/components/lockable_button.tsx b/frontend/devices/components/lockable_button.tsx index 1bcd74f5c..c45ad1059 100644 --- a/frontend/devices/components/lockable_button.tsx +++ b/frontend/devices/components/lockable_button.tsx @@ -1,13 +1,14 @@ import * as React from "react"; -interface Props { +export interface LockableButtonProps { onClick: Function; disabled: boolean; children?: React.ReactNode; title?: string; } -export function LockableButton({ onClick, disabled, children, title }: Props) { +export function LockableButton(props: LockableButtonProps) { + const { onClick, disabled, children, title } = props; const className = disabled ? "gray" : "yellow"; return - - ; + Type = () => + + + Action = () => + this.state.bindingType == PinBindingType.special + ? + : + + render() { + const newFormat = DevSettings.futureFeaturesEnabled(); + return
+ {newFormat && } + {newFormat && } + {newFormat && + + + + + + + } + + {!newFormat && + + + } + {!newFormat && + + + } + + + + +
; } } @@ -153,9 +175,9 @@ export const PinNumberInputGroup = (props: { label: generatePinLabel(pinNumberInput), value: "" + pinNumberInput } : undefined; - + const newFormat = DevSettings.futureFeaturesEnabled(); return - + - + diff --git a/frontend/devices/pin_bindings/pin_bindings.tsx b/frontend/devices/pin_bindings/pin_bindings.tsx index d009211b2..14992a42c 100644 --- a/frontend/devices/pin_bindings/pin_bindings.tsx +++ b/frontend/devices/pin_bindings/pin_bindings.tsx @@ -16,6 +16,7 @@ import { PinBinding, } from "farmbot/dist/resources/api_resources"; import { t } from "../../i18next_wrapper"; +import { DevSettings } from "../../account/dev/dev_support"; /** Width of UI columns in Pin Bindings widget. */ export enum PinBindingColWidth { @@ -70,13 +71,15 @@ const PinBindingsListHeader = () => export const PinBindingsContent = (props: PinBindingsContentProps) => { const { dispatch, resources, firmwareHardware } = props; const pinBindings = apiPinBindings(resources); - + const newFormat = DevSettings.futureFeaturesEnabled(); return
+ {newFormat && } @@ -87,7 +90,7 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
- + {!newFormat && } { const { pinBindings, resources, dispatch } = props; @@ -26,22 +30,33 @@ export const PinBindingsList = (props: PinBindingsListProps) => { const delBtnColor = (pin: number) => sysBtnBindings.includes(pin) ? "pseudo-disabled" : "red"; + const bindingText = ( + sequence_id: number | undefined, + binding_type: PinBindingType | undefined, + special_action: PinBindingSpecialAction | undefined, + ) => + `${t(bindingTypeLabelLookup[binding_type || ""])}: ${(sequence_id + ? findSequenceById(resources, sequence_id).body.name + : t(getSpecialActionLabel(special_action)))}`; + + const newFormat = DevSettings.futureFeaturesEnabled(); return
+ {newFormat && } {pinBindings .sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number)) .map(x => { const { pin_number, sequence_id, binding_type, special_action } = x; + const binding = bindingText(sequence_id, binding_type, special_action); return - - {generatePinLabel(pin_number)} + +

{generatePinLabel(pin_number)}

+

{newFormat && binding}

- - {t(bindingTypeLabelLookup[binding_type || ""])}:  - {sequence_id - ? findSequenceById(resources, sequence_id).body.name - : t(getSpecialActionLabel(special_action))} - - + {!newFormat && + + {binding} + } + -
+
; + } render() { const panelName = "add-tool"; diff --git a/frontend/farm_designer/tools/add_tool_slot.tsx b/frontend/farm_designer/tools/add_tool_slot.tsx index 874e4199e..dd069a2c2 100644 --- a/frontend/farm_designer/tools/add_tool_slot.tsx +++ b/frontend/farm_designer/tools/add_tool_slot.tsx @@ -11,15 +11,9 @@ import { Panel } from "../panel_header"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { history } from "../../history"; import { SlotEditRows } from "./tool_slot_edit_components"; -import { UUID } from "../../resources/interfaces"; -import { - isExpressBoard, -} from "../../devices/components/firmware_hardware_support"; -import { AddToolSlotProps, mapStateToPropsAdd } from "./map_to_props_add_edit"; - -export interface AddToolSlotState { - uuid: UUID | undefined; -} +import { hasUTM } from "../../devices/components/firmware_hardware_support"; +import { mapStateToPropsAdd } from "./state_to_props"; +import { AddToolSlotState, AddToolSlotProps } from "./interfaces"; export class RawAddToolSlot extends React.Component { @@ -30,7 +24,7 @@ export class RawAddToolSlot pointer_type: "ToolSlot", name: t("Slot"), radius: 0, meta: {}, x: 0, y: 0, z: 0, tool_id: undefined, pullout_direction: ToolPulloutDirection.NONE, - gantry_mounted: isExpressBoard(this.props.firmwareHardware) ? true : false, + gantry_mounted: !hasUTM(this.props.firmwareHardware) ? true : false, }); this.setState({ uuid: action.payload.uuid }); this.props.dispatch(action); @@ -74,7 +68,7 @@ export class RawAddToolSlot {this.toolSlot ? (toolId: number | undefined) => !!(toolId && toolSlots.map(x => x.body.tool_id).includes(toolId)); -export interface EditToolProps { - findTool(id: string): TaggedTool | undefined; - dispatch: Function; - mountedToolId: number | undefined; - isActive(id: number | undefined): boolean; -} - -export interface EditToolState { - toolName: string; -} - export const mapStateToProps = (props: Everything): EditToolProps => ({ findTool: (id: string) => maybeFindToolById(props.resources.index, parseInt(id)), diff --git a/frontend/farm_designer/tools/edit_tool_slot.tsx b/frontend/farm_designer/tools/edit_tool_slot.tsx index 18698f96e..29772b1c4 100644 --- a/frontend/farm_designer/tools/edit_tool_slot.tsx +++ b/frontend/farm_designer/tools/edit_tool_slot.tsx @@ -11,10 +11,9 @@ import { history } from "../../history"; import { Panel } from "../panel_header"; import { SlotEditRows } from "./tool_slot_edit_components"; import { moveAbs } from "../../devices/actions"; -import { - isExpressBoard, -} from "../../devices/components/firmware_hardware_support"; -import { EditToolSlotProps, mapStateToPropsEdit } from "./map_to_props_add_edit"; +import { hasUTM } from "../../devices/components/firmware_hardware_support"; +import { mapStateToPropsEdit } from "./state_to_props"; +import { EditToolSlotProps } from "./interfaces"; export class RawEditToolSlot extends React.Component { @@ -44,7 +43,7 @@ export class RawEditToolSlot extends React.Component { {toolSlot ?
({ - tools: selectAllTools(props.resources.index), - toolSlots: selectAllToolSlotPointers(props.resources.index), - dispatch: props.dispatch, - findTool: (id: number) => maybeFindToolById(props.resources.index, id), - device: getDeviceAccountSettings(props.resources.index), - sensors: selectAllSensors(props.resources.index), - bot: props.bot, - hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot, - firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), - isActive: isActive(selectAllToolSlotPointers(props.resources.index)), -}); +import { hasUTM } from "../../devices/components/firmware_hardware_support"; +import { ToolsProps, ToolsState } from "./interfaces"; +import { mapStateToProps } from "./state_to_props"; +import { BotOriginQuadrant } from "../interfaces"; const toolStatus = (value: number | undefined): string => { switch (value) { @@ -112,7 +71,7 @@ export class RawTools extends React.Component { get botOnline() { return isBotOnlineFromState(this.props.bot); } - get isExpress() { return isExpressBoard(this.props.firmwareHardware); } + get noUTM() { return !hasUTM(this.props.firmwareHardware); } MountedToolInfo = () =>
@@ -165,7 +124,9 @@ export class RawTools extends React.Component { dispatch={this.props.dispatch} toolSlot={toolSlot} isActive={this.props.isActive} - tools={this.props.tools} />)} + tools={this.props.tools} + xySwap={this.props.xySwap} + quadrant={this.props.quadrant} />)}
Tools = () => @@ -192,16 +153,16 @@ export class RawTools extends React.Component { get strings() { return { - placeholder: this.isExpress + placeholder: this.noUTM ? t("Search your seed containers...") : t("Search your tools..."), - titleText: this.isExpress + titleText: this.noUTM ? t("Add a seed container") : t("Add a tool or seed container"), - emptyStateText: this.isExpress + emptyStateText: this.noUTM ? Content.NO_SEED_CONTAINERS : Content.NO_TOOLS, - tools: this.isExpress + tools: this.noUTM ? t("seed containers") : t("tools and seed containers"), toolSlots: t("slots"), @@ -228,8 +189,7 @@ export class RawTools extends React.Component { title={this.strings.titleText} text={this.strings.emptyStateText} colorScheme={"tools"}> - {!this.isExpress && - } + {!this.noUTM && } @@ -244,6 +204,8 @@ export interface ToolSlotInventoryItemProps { hovered: boolean; dispatch: Function; isActive(id: number | undefined): boolean; + xySwap: boolean; + quadrant: BotOriginQuadrant; } export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { @@ -260,7 +222,7 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { + xySwap={props.xySwap} quadrant={props.quadrant} />
{ + const getWebAppConfig = getWebAppConfigValue(() => props); + const xySwap = !!getWebAppConfig(BooleanSetting.xy_swap); + const rawQuadrant = getWebAppConfig(NumericSetting.bot_origin_quadrant); + const quadrant = isBotOriginQuadrant(rawQuadrant) ? rawQuadrant : 2; + return { + tools: selectAllTools(props.resources.index), + toolSlots: selectAllToolSlotPointers(props.resources.index), + dispatch: props.dispatch, + findTool: (id: number) => maybeFindToolById(props.resources.index, id), + device: getDeviceAccountSettings(props.resources.index), + sensors: selectAllSensors(props.resources.index), + bot: props.bot, + hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot, + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), + isActive: isActive(selectAllToolSlotPointers(props.resources.index)), + xySwap, + quadrant, + }; +}; -export const mapStateToPropsBase = (props: Everything): AddEditToolSlotPropsBase => { +export const mapStateToPropsAddEditBase = (props: Everything): + AddEditToolSlotPropsBase => { const getWebAppConfig = getWebAppConfigValue(() => props); const xySwap = !!getWebAppConfig(BooleanSetting.xy_swap); const rawQuadrant = getWebAppConfig(NumericSetting.bot_origin_quadrant); @@ -44,24 +58,16 @@ export const mapStateToPropsBase = (props: Everything): AddEditToolSlotPropsBase }; }; -export interface AddToolSlotProps extends AddEditToolSlotPropsBase { - findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined; -} - export const mapStateToPropsAdd = (props: Everything): AddToolSlotProps => { - const mapStateToProps = mapStateToPropsBase(props) as AddToolSlotProps; - mapStateToProps.findToolSlot = (uuid: UUID | undefined) => + const stateToProps = mapStateToPropsAddEditBase(props) as AddToolSlotProps; + stateToProps.findToolSlot = (uuid: UUID | undefined) => maybeGetToolSlot(props.resources.index, uuid); - return mapStateToProps; + return stateToProps; }; -export interface EditToolSlotProps extends AddEditToolSlotPropsBase { - findToolSlot(id: string): TaggedToolSlotPointer | undefined; -} - export const mapStateToPropsEdit = (props: Everything): EditToolSlotProps => { - const mapStateToProps = mapStateToPropsBase(props) as EditToolSlotProps; - mapStateToProps.findToolSlot = (id: string) => + const stateToProps = mapStateToPropsAddEditBase(props) as EditToolSlotProps; + stateToProps.findToolSlot = (id: string) => maybeFindToolSlotById(props.resources.index, parseInt(id)); - return mapStateToProps; + return stateToProps; }; diff --git a/frontend/farm_designer/tools/tool_slot_edit_components.tsx b/frontend/farm_designer/tools/tool_slot_edit_components.tsx index 702537376..666150b2b 100644 --- a/frontend/farm_designer/tools/tool_slot_edit_components.tsx +++ b/frontend/farm_designer/tools/tool_slot_edit_components.tsx @@ -8,8 +8,8 @@ import { BotPosition } from "../../devices/interfaces"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { Popover } from "@blueprintjs/core"; import { ToolSlotSVG } from "../map/layers/tool_slots/tool_graphics"; -import { BotOriginQuadrant } from "../interfaces"; import { isNumber } from "lodash"; +import { BotOriginQuadrant } from "../interfaces"; export interface GantryMountedInputProps { gantryMounted: boolean; @@ -32,7 +32,7 @@ export interface SlotDirectionInputRowProps { export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
@@ -155,7 +155,7 @@ export interface SlotEditRowsProps { tool: TaggedTool | undefined; botPosition: BotPosition; updateToolSlot(update: Partial): void; - isExpress: boolean; + noUTM: boolean; xySwap: boolean; quadrant: BotOriginQuadrant; isActive(id: number | undefined): boolean; @@ -165,14 +165,14 @@ export const SlotEditRows = (props: SlotEditRowsProps) =>
+ xySwap={props.xySwap} quadrant={props.quadrant} /> } - {!props.isExpress && + {!props.noUTM && } From fe9ff346a876e82440a733dfc9422c130dc7b024 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 13 Mar 2020 14:21:44 -0700 Subject: [PATCH 4/4] group panel updates --- .../__test_support__/fake_designer_state.ts | 1 + frontend/constants.ts | 3 +- frontend/css/farm_designer/farm_designer.scss | 10 - .../farm_designer/farm_designer_panels.scss | 190 ++++++++++++--- frontend/css/global.scss | 9 - frontend/css/inputs.scss | 13 +- .../components/boolean_mcu_input_group.tsx | 2 +- .../hardware_settings/calibration_row.tsx | 3 +- .../hardware_settings/pin_guard.tsx | 2 +- .../hardware_settings/single_setting_row.tsx | 2 +- .../components/numeric_mcu_input_group.tsx | 2 +- .../components/pin_guard_input_group.tsx | 2 +- .../devices/pin_bindings/pin_bindings.tsx | 2 +- frontend/farm_designer/interfaces.ts | 1 + .../map/__tests__/garden_map_test.tsx | 14 +- .../__tests__/selection_box_actions_test.tsx | 50 +++- .../map/background/selection_box_actions.tsx | 32 ++- frontend/farm_designer/map/garden_map.tsx | 12 +- .../zones/__tests__/zones_layer_test.tsx | 2 - .../map/layers/zones/__tests__/zones_test.tsx | 8 +- .../farm_designer/map/layers/zones/zones.tsx | 22 +- .../map/legend/garden_map_legend.tsx | 2 +- .../__tests__/group_detail_active_test.tsx | 29 ++- .../__tests__/group_list_panel_test.tsx | 1 + .../criteria/__tests__/add_test.tsx | 118 +-------- .../criteria/__tests__/apply_test.ts | 8 +- .../criteria/__tests__/component_test.tsx | 13 +- .../criteria/__tests__/edit_test.ts | 203 +++++++++++++--- .../criteria/__tests__/presets_test.tsx | 93 ++++--- .../criteria/__tests__/selected_test.ts | 70 ++++++ .../criteria/__tests__/show_test.tsx | 190 +++++++-------- .../criteria/__tests__/subcriteria_test.tsx | 32 +++ .../point_groups/criteria/add.tsx | 120 +-------- .../point_groups/criteria/apply.ts | 18 +- .../point_groups/criteria/component.tsx | 85 ++++--- .../point_groups/criteria/edit.ts | 159 +++++++++--- .../point_groups/criteria/index.tsx | 2 + .../point_groups/criteria/interfaces.ts | 88 +++++-- .../point_groups/criteria/presets.tsx | 123 ++++++---- .../point_groups/criteria/selected.ts | 76 ++++++ .../point_groups/criteria/show.tsx | 206 +++++++--------- .../point_groups/criteria/subcriteria.tsx | 230 ++++++++++++++++++ .../point_groups/group_detail.tsx | 6 +- .../point_groups/group_detail_active.tsx | 111 ++++++--- frontend/farm_designer/reducer.ts | 5 + frontend/farm_designer/tools/index.tsx | 2 +- .../tools/tool_slot_edit_components.tsx | 20 +- frontend/farm_designer/zones/edit_zone.tsx | 3 +- frontend/logs/components/settings_menu.tsx | 2 +- .../sequence_editor_middle_active.tsx | 2 +- frontend/sequences/step_icon_group.tsx | 2 +- frontend/ui/checkbox.tsx | 25 ++ frontend/ui/help.tsx | 13 +- frontend/ui/index.ts | 1 + 54 files changed, 1609 insertions(+), 831 deletions(-) create mode 100644 frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts create mode 100644 frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx create mode 100644 frontend/farm_designer/point_groups/criteria/selected.ts create mode 100644 frontend/farm_designer/point_groups/criteria/subcriteria.tsx create mode 100644 frontend/ui/checkbox.tsx diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index bb6f411a4..e122a9dfd 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -16,4 +16,5 @@ export const fakeDesignerState = (): DesignerState => ({ currentPoint: undefined, openedSavedGarden: undefined, tryGroupSortType: undefined, + editGroupAreaInMap: false, }); diff --git a/frontend/constants.ts b/frontend/constants.ts index b0abe47c0..4639d223e 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -858,7 +858,7 @@ export namespace Content { export const CRITERIA_SELECTION_COUNT = trim(`Criteria additions can only be removed by changing criteria. - Click and drag in the map to modify zone selection criteria. + Click and drag in the map to modify selection criteria. Criteria will be applied at the time of sequence execution. The final selection at that time may differ from the selection currently displayed.`); @@ -1151,6 +1151,7 @@ export enum Actions { SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA", CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN", TRY_SORT_TYPE = "TRY_SORT_TYPE", + EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP", // Regimens PUSH_WEEK = "PUSH_WEEK", diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index bb34b097a..44fe1f4e1 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -210,16 +210,6 @@ @extend %panel-item-base; padding-top: 0.6rem; } - .groups-panel-content { - padding: 0px; - } - .groups-list-wrapper { - padding: 0.5em 0em; - } - .group-delete-btn { - float: left; - margin-top: 1em; - } .plant-search-item-name { display: inline-block; vertical-align: middle; diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index 9abc1ccb9..997266800 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -809,7 +809,6 @@ .weeds-inventory-panel, .zones-inventory-panel, -.group-detail-panel, .groups-panel { .panel-content { max-height: calc(100vh - 19rem); @@ -821,6 +820,31 @@ .group-detail-panel { .panel-content { + max-height: calc(100vh - 14rem); + overflow-y: auto; + overflow-x: hidden; + padding-bottom: 5rem; + .group-member-display { + i[class*=fa-caret-] { + float: right; + font-size: 2rem; + margin-top: 1.5rem; + } + .groups-list-wrapper { + padding: 0.5em 0em; + } + } + .group-member-display, + .group-sort-section { + .bp3-popover-wrapper { + display: inline; + margin-left: 1rem; + } + } + .group-delete-btn { + float: left; + margin-top: 1em; + } .group-criteria { margin-top: 1rem; .criteria-heading { @@ -829,7 +853,61 @@ .fb-button { margin-top: 0.5rem; } - .group-criteria-presets { + .point-type-checkboxes { + .point-type-section { + .fb-checkbox { + display: inline; + margin-right: 1rem; + vertical-align: top; + } + p { + display: inline; + text-transform: uppercase; + } + .point-type-checkbox { + position: relative; + height: 2rem; + margin-top: 0.75rem; + cursor: pointer; + .fb-checkbox { + display: inline-block; + height: 2rem; + } + i[class*=fa-caret-] { + position: absolute; + right: -0.5rem; + width: 3rem; + font-size: 2rem; + padding-left: 1rem; + } + } + .plant-criteria-options, + .point-criteria-options, + .tool-criteria-options { + margin-left: 3rem; + p { + &.category { + display: block; + padding-top: 1rem; + padding-bottom: 1rem; + text-transform: none; + font-size: 1.2rem; + font-weight: bold; + } + } + hr { + margin: 0.5rem; + } + .lt-gt-criteria { + margin-bottom: 1rem; + .row { + margin-left: 0 !important; + } + } + } + } + } + .criteria-radio-presets { input[type="radio"] { width: auto; margin-right: 1rem; @@ -845,29 +923,28 @@ .criteria-slug { margin-top: 1rem; } - .location-criteria { - .row { - margin-top: 1rem; - p { - font-size: 1.4rem; - font-weight: bold; - } - label { - margin-top: 0; - } - } - } .day-criteria { p { display: inline; vertical-align: bottom; } + input { + line-height: 1.75rem; + } } .string-eq-criteria { margin-top: 1rem; .row { margin-top: 1rem; } + code { + display: inline-block; + margin-top: 2rem; + font-size: 1.2rem; + font-weight: bold; + color: $black; + background: none; + } } .number-eq-criteria, .number-gt-lt-criteria { @@ -877,11 +954,49 @@ } p { text-align: center; - margin-top: 0.5rem; + line-height: 2.75rem; + font-size: 1.2rem; } } - .expandable-header { - margin-top: 3rem; + .fb-toggle-button { + width: 85px; + margin-top: 0; + &.red { + background: $dark_gray !important; + } + } + .clear-criteria { + margin-top: 2rem; + } + .basic, + .advanced { + margin-left: 1rem; + .day-criteria { + .row { + margin-left: 0; + } + div[class*=col-] { + padding: 0; + padding-right: 0.75rem; + } + } + } + .advanced { + .row { + margin-left: 0; + } + div[class*=col-] { + padding: 0; + } + .col-xs-9 { + margin-right: 0.5rem; + } + .col-xs-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; + margin-top: 0.4rem; + text-align: center; + } } } .criteria-point-count-breakdown { @@ -910,19 +1025,34 @@ } } -.zone-info-panel { - .panel-content { - .location-criteria { - .row { - margin-top: 1rem; - p { - font-size: 1.4rem; - font-weight: bold; - } - label { - margin-top: 0; - } - } +.lt-gt-criteria, +.location-criteria { + display: inline-block; + .row { + margin-left: 0; + div[class*=col-] { + padding: 0; + text-align: center; + } + margin-top: 1rem; + p { + display: block !important; + text-transform: uppercase; + font-size: 1.1rem; + margin-top: 0.75rem; + } + label { + margin-top: 0.5rem; + } + } + .edit-in-map { + float: right; + button { + margin: 1rem !important; + width: 5rem !important; + } + label { + margin-top: 1.1rem !important; } } } diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 94ff8595f..8cd473bce 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -1309,15 +1309,6 @@ ul { display: inline; position: relative; margin-right: 1rem; - &.partial:after { - content: ""; - position: absolute; - left: 0.75rem; - bottom: 1.2rem; - border: solid $dark_gray; - border-width: 0 0 3px 0; - padding: 0.6rem 0.3rem; - } } .bp3-popover-wrapper, .bp3-popover-target { diff --git a/frontend/css/inputs.scss b/frontend/css/inputs.scss index 9326c1a44..b7ee32649 100644 --- a/frontend/css/inputs.scss +++ b/frontend/css/inputs.scss @@ -138,6 +138,15 @@ select { padding: 0.6rem 0.3rem; } } + &.partial:after { + content: ""; + position: absolute; + left: 0.75rem; + bottom: 1.2rem; + border: solid $dark_gray; + border-width: 0 0 3px 0; + padding: 0.6rem 0.3rem; + } &.large { input[type="checkbox"] { width: 3rem; @@ -155,8 +164,10 @@ select { } } &.disabled { + cursor: not-allowed; input[type="checkbox"] { - cursor: not-allowed; + background: $light_gray; + pointer-events: none; &:checked:after { border-color: $gray; } diff --git a/frontend/devices/components/boolean_mcu_input_group.tsx b/frontend/devices/components/boolean_mcu_input_group.tsx index 28ad9ac06..b6b8ae0ca 100644 --- a/frontend/devices/components/boolean_mcu_input_group.tsx +++ b/frontend/devices/components/boolean_mcu_input_group.tsx @@ -63,7 +63,7 @@ export class BooleanMCUInputGroup {caution && } - + {!this.newFormat && } diff --git a/frontend/devices/components/hardware_settings/calibration_row.tsx b/frontend/devices/components/hardware_settings/calibration_row.tsx index 10963de0f..989e7a91b 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -39,8 +39,7 @@ export class CalibrationRow extends React.Component { - + {!this.newFormat && } diff --git a/frontend/devices/components/hardware_settings/pin_guard.tsx b/frontend/devices/components/hardware_settings/pin_guard.tsx index 5f78684ea..d417b709d 100644 --- a/frontend/devices/components/hardware_settings/pin_guard.tsx +++ b/frontend/devices/components/hardware_settings/pin_guard.tsx @@ -29,7 +29,7 @@ export function PinGuard(props: PinGuardProps) { - diff --git a/frontend/devices/components/hardware_settings/single_setting_row.tsx b/frontend/devices/components/hardware_settings/single_setting_row.tsx index a2f8f727e..74a19567e 100644 --- a/frontend/devices/components/hardware_settings/single_setting_row.tsx +++ b/frontend/devices/components/hardware_settings/single_setting_row.tsx @@ -20,7 +20,7 @@ export const SingleSettingRow = - + {settingType === "button" ? diff --git a/frontend/devices/components/numeric_mcu_input_group.tsx b/frontend/devices/components/numeric_mcu_input_group.tsx index 2dd28fe4b..22f3b6907 100644 --- a/frontend/devices/components/numeric_mcu_input_group.tsx +++ b/frontend/devices/components/numeric_mcu_input_group.tsx @@ -59,7 +59,7 @@ export class NumericMCUInputGroup - + {!this.newFormat && } diff --git a/frontend/devices/components/pin_guard_input_group.tsx b/frontend/devices/components/pin_guard_input_group.tsx index b00bc82f1..ff21796f4 100644 --- a/frontend/devices/components/pin_guard_input_group.tsx +++ b/frontend/devices/components/pin_guard_input_group.tsx @@ -76,7 +76,7 @@ export class PinGuardMCUInputGroup - diff --git a/frontend/devices/pin_bindings/pin_bindings.tsx b/frontend/devices/pin_bindings/pin_bindings.tsx index 14992a42c..1dbc4c6d5 100644 --- a/frontend/devices/pin_bindings/pin_bindings.tsx +++ b/frontend/devices/pin_bindings/pin_bindings.tsx @@ -75,7 +75,7 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => { return
{newFormat && } + position={Position.TOP_RIGHT} />} ({ jest.mock("../background/selection_box_actions", () => ({ startNewSelectionBox: jest.fn(), resizeBox: jest.fn(), - maybeUpdateGroupCriteria: jest.fn(), + maybeUpdateGroup: jest.fn(), })); jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() })); @@ -61,7 +61,7 @@ import { dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant, } from "../layers/plants/plant_actions"; import { - startNewSelectionBox, resizeBox, maybeUpdateGroupCriteria, + startNewSelectionBox, resizeBox, maybeUpdateGroup, } from "../background/selection_box_actions"; import { getGardenCoordinates } from "../util"; import { chooseLocation } from "../../move_to"; @@ -158,7 +158,7 @@ describe("", () => { wrapper.setState({ isDragging: true }); wrapper.find(".drop-area-svg").simulate("mouseUp", DEFAULT_EVENT); expect(maybeSavePlantLocation).toHaveBeenCalled(); - expect(maybeUpdateGroupCriteria).toHaveBeenCalled(); + expect(maybeUpdateGroup).toHaveBeenCalled(); expect(wrapper.instance().state.isDragging).toBeFalsy(); }); @@ -224,7 +224,9 @@ describe("", () => { }); it("starts drag on background: selecting zone", () => { - const wrapper = mount(); + const p = fakeProps(); + p.designer.editGroupAreaInMap = true; + const wrapper = mount(); mockMode = Mode.editGroup; const e = { pageX: 1000, pageY: 2000 }; wrapper.find(".drop-area-background").simulate("mouseDown", e); @@ -255,7 +257,9 @@ describe("", () => { }); it("drags: selecting zone", () => { - const wrapper = shallow(); + const p = fakeProps(); + p.designer.editGroupAreaInMap = true; + const wrapper = shallow(); mockMode = Mode.editGroup; const e = { pageX: 2000, pageY: 2000 }; wrapper.find(".drop-area-svg").simulate("mouseMove", e); diff --git a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx index 04dc92199..cae610a41 100644 --- a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx +++ b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx @@ -8,18 +8,25 @@ jest.mock("../../../point_groups/criteria", () => ({ editGtLtCriteria: jest.fn(), })); +jest.mock("../../../../api/crud", () => ({ + overwrite: jest.fn(), + save: jest.fn(), +})); + import { fakePlant, fakePointGroup, } from "../../../../__test_support__/fake_state/resources"; import { getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps, StartNewSelectionBoxProps, - maybeUpdateGroupCriteria, - MaybeUpdateGroupCriteriaProps, + maybeUpdateGroup, + MaybeUpdateGroupProps, } from "../selection_box_actions"; import { Actions } from "../../../../constants"; import { history } from "../../../../history"; import { editGtLtCriteria } from "../../../point_groups/criteria"; +import { overwrite, save } from "../../../../api/crud"; +import { cloneDeep } from "lodash"; describe("getSelected", () => { it("returns some", () => { @@ -156,24 +163,55 @@ describe("startNewSelectionBox", () => { }); }); -describe("maybeUpdateGroupCriteria()", () => { - const fakeProps = (): MaybeUpdateGroupCriteriaProps => ({ +describe("maybeUpdateGroup()", () => { + const fakeProps = (): MaybeUpdateGroupProps => ({ selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined }, dispatch: jest.fn(), group: fakePointGroup(), shouldDisplay: () => true, + editGroupAreaInMap: false, + boxSelected: undefined, + }); + + it("updates group", () => { + const p = fakeProps(); + p.editGroupAreaInMap = false; + const plant1 = fakePlant(); + const plant2 = fakePlant(); + p.boxSelected = [plant1.uuid, plant2.uuid]; + p.group && (p.group.body.point_ids = [plant1.body.id || 0]); + maybeUpdateGroup(p); + expect(editGtLtCriteria).not.toHaveBeenCalled(); + const expectedBody = cloneDeep(p.group?.body); + expectedBody && (expectedBody.point_ids = [ + plant1.body.id || 0, plant2.body.id || 0, + ]); + expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); + expect(save).not.toHaveBeenCalled(); }); it("updates criteria", () => { const p = fakeProps(); - maybeUpdateGroupCriteria(p); + p.editGroupAreaInMap = true; + maybeUpdateGroup(p); expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox); }); it("doesn't update criteria", () => { const p = fakeProps(); p.shouldDisplay = () => false; - maybeUpdateGroupCriteria(p); + maybeUpdateGroup(p); expect(editGtLtCriteria).not.toHaveBeenCalled(); }); + + it("handles missing group or box", () => { + const p = fakeProps(); + p.group = undefined; + p.selectionBox = undefined; + maybeUpdateGroup(p); + expect(p.dispatch).not.toHaveBeenCalled(); + expect(editGtLtCriteria).not.toHaveBeenCalled(); + expect(overwrite).not.toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/farm_designer/map/background/selection_box_actions.tsx b/frontend/farm_designer/map/background/selection_box_actions.tsx index c44f7b250..9fda2f484 100644 --- a/frontend/farm_designer/map/background/selection_box_actions.tsx +++ b/frontend/farm_designer/map/background/selection_box_actions.tsx @@ -1,4 +1,4 @@ -import { isNumber } from "lodash"; +import { isNumber, uniq, cloneDeep, isEqual } from "lodash"; import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces"; import { SelectionBoxData } from "./selection_box"; import { GardenMapState } from "../../interfaces"; @@ -8,6 +8,9 @@ import { getMode } from "../util"; import { editGtLtCriteria } from "../../point_groups/criteria"; import { TaggedPointGroup } from "farmbot"; import { ShouldDisplay, Feature } from "../../../devices/interfaces"; +import { overwrite } from "../../../api/crud"; +import { unpackUUID } from "../../../util"; +import { UUID } from "../../../resources/interfaces"; /** Return all plants within the selection box. */ export const getSelected = ( @@ -85,17 +88,32 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => { } }; -export interface MaybeUpdateGroupCriteriaProps { +export interface MaybeUpdateGroupProps { selectionBox: SelectionBoxData | undefined; dispatch: Function; group: TaggedPointGroup | undefined; shouldDisplay: ShouldDisplay; + editGroupAreaInMap: boolean; + boxSelected: UUID[] | undefined; } -export const maybeUpdateGroupCriteria = - (props: MaybeUpdateGroupCriteriaProps) => { - if (props.selectionBox && props.group && - props.shouldDisplay(Feature.criteria_groups)) { - props.dispatch(editGtLtCriteria(props.group, props.selectionBox)); +export const maybeUpdateGroup = + (props: MaybeUpdateGroupProps) => { + if (props.selectionBox && props.group) { + if (props.editGroupAreaInMap + && props.shouldDisplay(Feature.criteria_groups)) { + props.dispatch(editGtLtCriteria(props.group, props.selectionBox)); + } else { + const nextGroupBody = cloneDeep(props.group.body); + props.boxSelected?.map(uuid => { + const { kind, remoteId } = unpackUUID(uuid); + remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId); + }); + nextGroupBody.point_ids = uniq(nextGroupBody.point_ids); + if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) { + props.dispatch(overwrite(props.group, nextGroupBody)); + props.dispatch(selectPlant(undefined)); + } + } } }; diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 0551a0af5..cce11c0cf 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -11,7 +11,7 @@ import { import { Grid, MapBackground, TargetCoordinate, - SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroupCriteria, + SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroup, } from "./background"; import { PlantLayer, @@ -88,11 +88,13 @@ export class GardenMap extends isDragging: this.state.isDragging, dispatch: this.props.dispatch, }); - maybeUpdateGroupCriteria({ + maybeUpdateGroup({ selectionBox: this.state.selectionBox, group: this.group, dispatch: this.props.dispatch, shouldDisplay: this.props.shouldDisplay, + editGroupAreaInMap: this.props.designer.editGroupAreaInMap, + boxSelected: this.props.designer.selectedPlants, }); this.setState({ isDragging: false, qPageX: 0, qPageY: 0, @@ -142,7 +144,7 @@ export class GardenMap extends gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, - plantActions: false, + plantActions: !this.props.designer.editGroupAreaInMap, }); break; case Mode.createPoint: @@ -179,7 +181,7 @@ export class GardenMap extends gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, - plantActions: false, + plantActions: !this.props.designer.editGroupAreaInMap, }); break; default: @@ -283,7 +285,7 @@ export class GardenMap extends gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, - plantActions: false, + plantActions: !this.props.designer.editGroupAreaInMap, }); break; case Mode.boxSelect: diff --git a/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx b/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx index fab0bfc97..a812e4f53 100644 --- a/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx +++ b/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx @@ -7,7 +7,6 @@ import { import { fakeMapTransformProps, } from "../../../../../__test_support__/map_transform_props"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; describe("", () => { const fakeProps = (): ZonesLayerProps => ({ @@ -69,7 +68,6 @@ describe("", () => { const p = fakeProps(); p.visible = false; p.groups[0].body.id = 1; - p.groups[0].body.criteria = undefined as unknown as PointGroup["criteria"]; p.currentGroup = p.groups[0].uuid; const wrapper = svgMount(); expect(wrapper.html()) diff --git a/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx b/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx index 1d017627d..0ca53f5c8 100644 --- a/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx +++ b/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx @@ -9,7 +9,7 @@ import { import { fakeMapTransformProps, } from "../../../../../__test_support__/map_transform_props"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; +import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces"; const fakeProps = (): ZonesProps => ({ group: fakePointGroup(), @@ -25,7 +25,7 @@ describe("", () => { it("renders none: no data", () => { const p = fakeProps(); p.group.body.id = 1; - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; + p.group.body.criteria = DEFAULT_CRITERIA; const wrapper = svgMount(); expect(wrapper.find("#zones-0D-1").length).toEqual(1); expect(wrapper.find("circle").length).toEqual(0); @@ -63,7 +63,7 @@ describe("", () => { it("renders none: no data", () => { const p = fakeProps(); p.group.body.id = 1; - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; + p.group.body.criteria = DEFAULT_CRITERIA; const wrapper = svgMount(); expect(wrapper.find("#zones-1D-1").length).toEqual(1); expect(wrapper.find("line").length).toEqual(0); @@ -110,7 +110,7 @@ describe("", () => { it("renders none", () => { const p = fakeProps(); p.group.body.id = 1; - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; + p.group.body.criteria = DEFAULT_CRITERIA; const wrapper = svgMount(); expect(wrapper.find("#zones-2D-1").length).toEqual(1); expect(wrapper.find("rect").length).toEqual(0); diff --git a/frontend/farm_designer/map/layers/zones/zones.tsx b/frontend/farm_designer/map/layers/zones/zones.tsx index 0adbcee67..1d04f353c 100644 --- a/frontend/farm_designer/map/layers/zones/zones.tsx +++ b/frontend/farm_designer/map/layers/zones/zones.tsx @@ -26,9 +26,9 @@ type Point = { x: number, y: number }; export enum ZoneType { points, lines, area, none } export const getZoneType = (group: TaggedPointGroup): ZoneType => { - const numEq = group.body.criteria?.number_eq || {}; - const numGt = group.body.criteria?.number_gt || {}; - const numLt = group.body.criteria?.number_lt || {}; + const numEq = group.body.criteria.number_eq; + const numGt = group.body.criteria.number_gt; + const numLt = group.body.criteria.number_lt; const hasXEq = !!numEq.x?.length; const hasYEq = !!numEq.y?.length; if (hasXEq && hasYEq) { @@ -46,8 +46,8 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => { /** Bounds for area selected by criteria or bot extents. */ const getBoundary = (props: GetBoundaryProps): Boundary => { const { criteria } = props.group.body; - const gt = criteria?.number_gt || {}; - const lt = criteria?.number_lt || {}; + const gt = criteria.number_gt; + const lt = criteria.number_lt; const x1 = gt.x || 0; const x2 = lt.x || props.botSize.x.value; const y1 = gt.y || 0; @@ -67,8 +67,8 @@ const filter: ( /** Coordinates selected by both x and y number equal values. */ const getPoints = (boundary: Boundary, group: TaggedPointGroup): Point[] => { - const xs = group.body.criteria?.number_eq.x; - const ys = group.body.criteria?.number_eq.y; + const xs = group.body.criteria.number_eq.x; + const ys = group.body.criteria.number_eq.y; const points: Point[] = []; xs?.map(x => ys?.map(y => points.push({ x, y }))); return filter(boundary, points); @@ -95,12 +95,12 @@ export const Zones0D = (props: ZonesProps) => { /** Lines selected by an x or y number equal value. */ const getLines = (boundary: Boundary, group: TaggedPointGroup): Line[] => { - const xs = group.body.criteria?.number_eq.x; - const ys = group.body.criteria?.number_eq.y; + const xs = group.body.criteria.number_eq.x; + const ys = group.body.criteria.number_eq.y; const onlyXs = !!xs?.length && !ys?.length; const onlyYs = !!ys?.length && !xs?.length; - const xLineData = onlyXs ? xs?.map(x => ({ x })) : undefined; - const yLineData = onlyYs ? ys?.map(y => ({ y })) : undefined; + const xLineData = (onlyXs && xs) ? xs.map(x => ({ x })) : undefined; + const yLineData = (onlyYs && ys) ? ys.map(y => ({ y })) : undefined; return filter(boundary, xLineData || yLineData); }; diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx index 7fbef1b7e..4b8a2561d 100644 --- a/frontend/farm_designer/map/legend/garden_map_legend.tsx +++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx @@ -80,7 +80,7 @@ const LayerToggles = (props: GardenMapLegendProps) => { {DevSettings.futureFeaturesEnabled() && } {DevSettings.futureFeaturesEnabled() && props.hasSensorReadings && ", () => { const fakeProps = (): GroupDetailActiveProps => { const plant = fakePlant(); plant.body.id = 1; const group = fakePointGroup(); + group.body.criteria = DEFAULT_CRITERIA; group.specialStatus = SpecialStatus.DIRTY; group.body.name = "XYZ"; group.body.point_ids = [plant.body.id]; @@ -34,6 +34,7 @@ describe("", () => { shouldDisplay: () => true, slugs: [], hovered: undefined, + editGroupAreaInMap: false, }; }; @@ -45,11 +46,29 @@ describe("", () => { expect(save).toHaveBeenCalledWith(p.group.uuid); }); + it("is already saved", () => { + const p = fakeProps(); + p.group.specialStatus = SpecialStatus.SAVED; + const el = new GroupDetailActive(p); + el.saveGroup(); + expect(p.dispatch).not.toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); + }); + + it("toggles icon view", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.state().iconDisplay).toBeTruthy(); + wrapper.instance().toggleIconShow(); + expect(wrapper.state().iconDisplay).toBeFalsy(); + }); + it("renders", () => { const p = fakeProps(); p.group.specialStatus = SpecialStatus.SAVED; const wrapper = mount(); expect(wrapper.find("input").first().prop("defaultValue")).toContain("XYZ"); + expect(wrapper.find(".groups-list-wrapper").length).toEqual(1); expect(wrapper.text()).not.toContain("saving"); }); @@ -109,6 +128,12 @@ describe("", () => { const p = fakeProps(); p.group.body.sort_type = "random"; const wrapper = mount(); - expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION); + expect(wrapper.html()).toContain("exclamation-triangle"); + }); + + it("doesn't show icons", () => { + const wrapper = mount(); + wrapper.setState({ iconDisplay: false }); + expect(wrapper.find(".groups-list-wrapper").length).toEqual(0); }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx index b359b22ba..70ba6bce3 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx @@ -25,6 +25,7 @@ describe("", () => { group1.body.point_ids = [1, 2, 3]; const group2 = fakePointGroup(); group2.body.name = "two"; + group2.body.criteria.day.days_ago = -1; const point1 = fakePlant(); point1.body.id = 1; const point2 = fakePlant(); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx index d6574c378..a7d93fe15 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx +++ b/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx @@ -5,27 +5,20 @@ jest.mock("../edit", () => ({ import React from "react"; import { mount, shallow } from "enzyme"; -import { - AddEqCriteria, AddNumberCriteria, editCriteria, AddStringCriteria, - toggleStringCriteria, - POINTER_TYPE_LIST, -} from ".."; +import { AddEqCriteria, AddNumberCriteria, editCriteria } from ".."; import { AddEqCriteriaProps, NumberCriteriaProps, DEFAULT_CRITERIA, - AddStringCriteriaProps, } from "../interfaces"; import { fakePointGroup, } from "../../../../__test_support__/fake_state/resources"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; -import { PLANT_STAGE_LIST } from "../../../plants/edit_plant_status"; describe(" />", () => { const fakeProps = (): AddEqCriteriaProps => ({ dispatch: jest.fn(), group: fakePointGroup(), type: "string", - criteriaField: undefined, + eqCriteria: {}, criteriaKey: "string_eq", }); @@ -68,7 +61,7 @@ describe(" />", () => { dispatch: jest.fn(), group: fakePointGroup(), type: "number", - criteriaField: {}, + eqCriteria: {}, criteriaKey: "number_eq", }); @@ -106,104 +99,6 @@ describe(" />", () => { }); }); -describe("", () => { - const fakeProps = (): AddStringCriteriaProps => ({ - dispatch: jest.fn(), - group: fakePointGroup(), - slugs: ["apple", "orange"], - }); - - it("renders", () => { - const wrapper = mount(); - expect(wrapper.text()).toContain("None"); - }); - - it("changes key", () => { - const wrapper = shallow( - ); - wrapper.find("FBSelect").first().simulate("change", { - value: "openfarm_slug", label: "" - }); - expect(wrapper.state().key).toEqual("openfarm_slug"); - }); - - it("changes value", () => { - const wrapper = shallow( - ); - wrapper.setState({ key: "openfarm_slug" }); - wrapper.find("FBSelect").last().simulate("change", { - label: "", value: "slug" - }); - expect(wrapper.state().value).toEqual("slug"); - }); - - it("renders slug list", () => { - const wrapper = shallow( - ); - wrapper.setState({ key: "openfarm_slug", value: "pear" }); - expect(wrapper.find("FBSelect").last().props().list).toEqual([ - { label: "Apple", value: "apple" }, - { label: "Orange", value: "orange" }, - ]); - expect(wrapper.instance().selected).toEqual({ - label: "Pear", value: "pear" - }); - }); - - it("returns selected point type", () => { - const wrapper = shallow( - ); - wrapper.setState({ key: "pointer_type", value: "" }); - expect(wrapper.instance().selected).toEqual(undefined); - }); - - it("renders point type list", () => { - const wrapper = shallow( - ); - wrapper.setState({ key: "pointer_type", value: "Plant" }); - expect(wrapper.find("FBSelect").last().props().list) - .toEqual(POINTER_TYPE_LIST()); - expect(wrapper.instance().selected).toEqual({ - label: "Plants", value: "Plant" - }); - }); - - it("returns selected plant stage", () => { - const wrapper = shallow( - ); - wrapper.setState({ key: "plant_stage", value: "" }); - expect(wrapper.instance().selected).toEqual(undefined); - }); - - it("renders plant stage list", () => { - const wrapper = shallow( - ); - wrapper.setState({ key: "plant_stage", value: "planted" }); - expect(wrapper.find("FBSelect").last().props().list) - .toEqual(PLANT_STAGE_LIST()); - expect(wrapper.instance().selected).toEqual({ - label: "Planted", value: "planted" - }); - }); - - it("updates criteria", () => { - const p = fakeProps(); - const wrapper = mount(); - wrapper.setState({ key: "openfarm_slug", value: "slug" }); - wrapper.find("button").last().simulate("click"); - expect(toggleStringCriteria).toHaveBeenCalledWith( - p.group, "openfarm_slug", "slug"); - }); - - it("doesn't update criteria", () => { - const p = fakeProps(); - const wrapper = mount(); - wrapper.setState({ key: "openfarm_slug", value: "" }); - wrapper.find("button").last().simulate("click"); - expect(toggleStringCriteria).not.toHaveBeenCalled(); - }); -}); - describe("", () => { const fakeProps = (): NumberCriteriaProps => ({ dispatch: jest.fn(), @@ -242,11 +137,4 @@ describe("", () => { wrapper.find("button").last().simulate("click"); expect(editCriteria).toHaveBeenCalledWith(p.group, { number_lt: { x: 1 } }); }); - - it("handles missing criteria", () => { - const p = fakeProps(); - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; - const wrapper = mount(); - expect(wrapper.text()).toContain("<"); - }); }); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts index e5da28f4f..4e6165023 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts +++ b/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts @@ -2,13 +2,12 @@ import { selectPointsByCriteria, pointsSelectedByGroup } from ".."; import { fakePoint, fakePlant, fakePointGroup, } from "../../../../__test_support__/fake_state/resources"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; import moment from "moment"; -import { DEFAULT_CRITERIA } from "../interfaces"; +import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces"; import { cloneDeep } from "lodash"; describe("selectPointsByCriteria()", () => { - const fakeCriteria = (): PointGroup["criteria"] => + const fakeCriteria = (): PointGroupCriteria => cloneDeep(DEFAULT_CRITERIA); it("matches color", () => { @@ -44,6 +43,7 @@ describe("selectPointsByCriteria()", () => { it("matches positions: gt/lt", () => { const criteria = fakeCriteria(); + criteria.string_eq = {}; criteria.number_gt = { x: 100 }; criteria.number_lt = { x: 500 }; const matchingPoint = fakePoint(); @@ -57,6 +57,7 @@ describe("selectPointsByCriteria()", () => { it("matches age greater than 1 day old", () => { const criteria = fakeCriteria(); + criteria.string_eq = {}; criteria.day = { days_ago: 1, op: ">" }; const matchingPoint = fakePoint(); matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z"; @@ -70,6 +71,7 @@ describe("selectPointsByCriteria()", () => { it("matches age less than 1 day old", () => { const criteria = fakeCriteria(); + criteria.string_eq = {}; criteria.day = { days_ago: 1, op: "<" }; const matchingPoint = fakePoint(); matchingPoint.body.created_at = "2020-02-20T20:00:00.000Z"; diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx index 26ed1d2ed..0f13b9c73 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx +++ b/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx @@ -14,20 +14,17 @@ import { } from "../../../../__test_support__/fake_state/resources"; import { cloneDeep } from "lodash"; import { overwrite, save } from "../../../../api/crud"; -import { ExpandableHeader } from "../../../../ui"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; describe("", () => { const fakeProps = (): GroupCriteriaProps => ({ dispatch: jest.fn(), group: fakePointGroup(), slugs: [], + editGroupAreaInMap: false, }); it("renders", () => { - const p = fakeProps(); - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; - const wrapper = mount(); + const wrapper = mount(); ["criteria", "age selection"].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); @@ -35,17 +32,17 @@ describe("", () => { it("clears criteria", () => { const p = fakeProps(); const wrapper = mount(); - wrapper.find("button").first().simulate("click"); + wrapper.find("button").last().simulate("click"); const expectedBody = cloneDeep(p.group.body); expectedBody.criteria = DEFAULT_CRITERIA; expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); expect(save).toHaveBeenCalledWith(p.group.uuid); }); - it("expands section", () => { + it("toggles advanced view", () => { const wrapper = mount(); expect(wrapper.text()).not.toContain("number criteria"); - wrapper.find(ExpandableHeader).simulate("click"); + wrapper.find("ToggleButton").first().simulate("click"); expect(wrapper.text()).toContain("number criteria"); }); }); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts index a8bdbb156..3bcebcef8 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts +++ b/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts @@ -5,20 +5,25 @@ jest.mock("../../../../api/crud", () => ({ import { editCriteria, toggleEqCriteria, - togglePointSelection, toggleStringCriteria, editGtLtCriteria, + editGtLtCriteria, + togglePointTypeCriteria, + toggleAndEditEqCriteria, + clearCriteriaField, + removeEqCriteriaValue, + editGtLtCriteriaField, } from ".."; import { fakePointGroup, } from "../../../../__test_support__/fake_state/resources"; import { overwrite, save } from "../../../../api/crud"; import { cloneDeep } from "lodash"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; -import { DEFAULT_CRITERIA } from "../interfaces"; +import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces"; +import { inputEvent } from "../../../../__test_support__/fake_html_events"; describe("editCriteria()", () => { it("edits criteria: all empty", () => { const group = fakePointGroup(); - group.body.criteria = undefined as unknown as PointGroup["criteria"]; + group.body.criteria = DEFAULT_CRITERIA; editCriteria(group, {})(jest.fn()); const expectedBody = cloneDeep(group.body); expectedBody.criteria = DEFAULT_CRITERIA; @@ -35,7 +40,7 @@ describe("editCriteria()", () => { it("edits criteria: full update", () => { const group = fakePointGroup(); - const criteria: PointGroup["criteria"] = { + const criteria: PointGroupCriteria = { day: { days_ago: 1, op: "<" }, string_eq: { openfarm_slug: ["slug"] }, number_eq: { x: [0] }, @@ -52,47 +57,162 @@ describe("editCriteria()", () => { describe("toggleEqCriteria()", () => { it("adds criteria", () => { - const result = toggleEqCriteria({})("openfarm_slug", "slug"); - expect(result).toEqual({ openfarm_slug: ["slug"] }); + const eqCriteria = {}; + toggleEqCriteria(eqCriteria)("openfarm_slug", "slug"); + expect(eqCriteria).toEqual({ openfarm_slug: ["slug"] }); }); it("removes criteria", () => { - const result = toggleEqCriteria({ openfarm_slug: ["slug"] })( + const eqCriteria = { openfarm_slug: ["slug"] }; + toggleEqCriteria(eqCriteria)( "openfarm_slug", "slug"); - expect(result).toEqual({}); + expect(eqCriteria).toEqual({}); + }); + + it("toggles on", () => { + const eqCriteria = { openfarm_slug: ["slug"] }; + toggleEqCriteria(eqCriteria, "on")( + "openfarm_slug", "slug"); + expect(eqCriteria).toEqual({ openfarm_slug: ["slug"] }); + }); + + it("toggles off", () => { + const eqCriteria = {}; + toggleEqCriteria(eqCriteria, "off")("openfarm_slug", "slug"); + expect(eqCriteria).toEqual({}); }); }); const dispatch = jest.fn(x => x(jest.fn())); -describe("togglePointSelection()", () => { - it("adds criteria", () => { +describe("toggleAndEditEqCriteria()", () => { + it("toggles criteria on", () => { const group = fakePointGroup(); - togglePointSelection(group)({ openfarm_slug: "slug" })(dispatch); const expectedBody = cloneDeep(group.body); - expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] }; + expectedBody.criteria.string_eq = { openfarm_slug: ["mint"] }; + toggleAndEditEqCriteria(group, "openfarm_slug", "mint")(dispatch); + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); + + it("toggles criteria on for point type", () => { + const group = fakePointGroup(); + const expectedBody = cloneDeep(group.body); + group.body.criteria.string_eq = { + pointer_type: ["GenericPointer", "Plant", "ToolSlot"], + openfarm_slug: ["apple"], + "meta.color": ["red"], + }; + group.body.criteria.number_eq = { + pullout_direction: [0] + }; + expectedBody.criteria.string_eq = { + pointer_type: ["Plant"], + openfarm_slug: ["apple", "mint"], + }; + expectedBody.criteria.number_eq = {}; + toggleAndEditEqCriteria(group, "openfarm_slug", "mint", "Plant")(dispatch); + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); + + it("toggles off", () => { + const group = fakePointGroup(); + group.body.criteria.string_eq = { + pointer_type: ["GenericPointer", "Plant", "ToolSlot"], + openfarm_slug: ["mint"], + "meta.color": ["red"], + }; + group.body.criteria.number_eq = { + pullout_direction: [0], + }; + const expectedBody = cloneDeep(group.body); + delete expectedBody.criteria.string_eq.openfarm_slug; + toggleAndEditEqCriteria(group, "openfarm_slug", "mint", "Plant")(dispatch); + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); + + it("toggles on: empty criteria", () => { + const group = fakePointGroup(); + group.body.criteria.string_eq = { + openfarm_slug: undefined, + "meta.color": undefined, + }; + group.body.criteria.number_eq = { + pullout_direction: undefined, + }; + const expectedBody = cloneDeep(group.body); + expectedBody.criteria.number_eq = { pullout_direction: [0] }; + toggleAndEditEqCriteria(group, "pullout_direction", 0, "ToolSlot")(dispatch); expect(overwrite).toHaveBeenCalledWith(group, expectedBody); expect(save).toHaveBeenCalledWith(group.uuid); }); }); -describe("toggleStringCriteria()", () => { - it("adds criteria", () => { +describe("togglePointTypeCriteria()", () => { + it("toggles on", () => { const group = fakePointGroup(); - toggleStringCriteria(group, "openfarm_slug", "slug")(dispatch); + group.body.criteria.string_eq = { + pointer_type: ["GenericPointer"], + openfarm_slug: ["mint"], + "meta.color": ["red"], + }; const expectedBody = cloneDeep(group.body); - expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] }; + expectedBody.criteria.string_eq.pointer_type?.push("Plant"); + togglePointTypeCriteria(group, "Plant")(dispatch); expect(overwrite).toHaveBeenCalledWith(group, expectedBody); expect(save).toHaveBeenCalledWith(group.uuid); }); - it("handles missing criteria", () => { + it("toggles off", () => { const group = fakePointGroup(); - group.body.criteria = undefined as unknown as PointGroup["criteria"]; - toggleStringCriteria(group, "openfarm_slug", "slug")(dispatch); const expectedBody = cloneDeep(group.body); - expectedBody.criteria = cloneDeep(DEFAULT_CRITERIA); - expectedBody.criteria.string_eq = { openfarm_slug: ["slug"] }; + group.body.criteria.string_eq = { + pointer_type: ["GenericPointer", "Plant"], + openfarm_slug: ["mint"], + "meta.color": ["red"], + }; + expectedBody.criteria.string_eq = { + pointer_type: ["GenericPointer"], + "meta.color": ["red"], + }; + togglePointTypeCriteria(group, "Plant")(dispatch); + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); + + it("toggles on: empty criteria", () => { + const group = fakePointGroup(); + group.body.criteria.string_eq = {}; + const expectedBody = cloneDeep(group.body); + expectedBody.criteria.string_eq = { pointer_type: ["Plant"] }; + togglePointTypeCriteria(group, "Plant")(dispatch); + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); + + it("toggles off: empty criteria", () => { + const group = fakePointGroup(); + group.body.criteria.string_eq = { pointer_type: ["ToolSlot"] }; + group.body.criteria.number_eq = { + pullout_direction: undefined, + }; + const expectedBody = cloneDeep(group.body); + expectedBody.criteria.string_eq = {}; + togglePointTypeCriteria(group, "ToolSlot")(dispatch); + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); +}); + +describe("clearCriteriaField()", () => { + it("clears field", () => { + const group = fakePointGroup(); + const expectedBody = cloneDeep(group.body); + group.body.criteria.string_eq = { plant_stage: ["planted"] }; + expectedBody.criteria.string_eq = {}; + clearCriteriaField(group, ["string_eq"], "plant_stage")(dispatch); expect(overwrite).toHaveBeenCalledWith(group, expectedBody); expect(save).toHaveBeenCalledWith(group.uuid); }); @@ -117,16 +237,41 @@ describe("editGtLtCriteria()", () => { expect(overwrite).not.toHaveBeenCalled(); expect(save).not.toHaveBeenCalled(); }); +}); - it("handles missing criteria", () => { +describe("removeEqCriteriaValue()", () => { + it("removes value", () => { const group = fakePointGroup(); - group.body.criteria = undefined as unknown as PointGroup["criteria"]; - const box = { x0: 1, y0: 2, x1: 3, y1: 4 }; - editGtLtCriteria(group, box)(dispatch); + group.body.criteria.string_eq = { plant_stage: ["planted", "planned"] }; + removeEqCriteriaValue(group, group.body.criteria.string_eq, + "string_eq", "plant_stage", "planned")(dispatch); const expectedBody = cloneDeep(group.body); - expectedBody.criteria = cloneDeep(DEFAULT_CRITERIA); - expectedBody.criteria.number_gt = { x: 1, y: 2 }; - expectedBody.criteria.number_lt = { x: 3, y: 4 }; + expectedBody.criteria.string_eq = { plant_stage: ["planted"] }; + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); +}); + +describe("editGtLtCriteriaField()", () => { + it("changes value", () => { + const group = fakePointGroup(); + const e = inputEvent("1"); + editGtLtCriteriaField(group, "number_lt", "radius")(e)(dispatch); + const expectedBody = cloneDeep(group.body); + expectedBody.criteria.number_lt = { radius: 1 }; + expect(overwrite).toHaveBeenCalledWith(group, expectedBody); + expect(save).toHaveBeenCalledWith(group.uuid); + }); + + it("clears incompatible criteria", () => { + const group = fakePointGroup(); + const expectedBody = cloneDeep(group.body); + group.body.criteria.string_eq = { plant_stage: ["planted"] }; + const e = inputEvent("1"); + editGtLtCriteriaField( + group, "number_lt", "radius", "GenericPointer", + )(e)(dispatch); + expectedBody.criteria.number_lt = { radius: 1 }; expect(overwrite).toHaveBeenCalledWith(group, expectedBody); expect(save).toHaveBeenCalledWith(group.uuid); }); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx index aca3e83db..de5c4eecf 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx +++ b/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx @@ -1,56 +1,93 @@ -const mockToggle = jest.fn(); jest.mock("../edit", () => ({ - togglePointSelection: jest.fn(() => mockToggle), - toggleStringCriteria: jest.fn(), + togglePointTypeCriteria: jest.fn(), + toggleAndEditEqCriteria: jest.fn(), + clearCriteriaField: jest.fn(), })); import React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { - CheckboxSelections, togglePointSelection, criteriaSelected, + CheckboxSelections, togglePointTypeCriteria, clearCriteriaField, } from ".."; import { CheckboxSelectionsProps } from "../interfaces"; import { fakePointGroup, } from "../../../../__test_support__/fake_state/resources"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; +import { Checkbox } from "../../../../ui"; describe("", () => { const fakeProps = (): CheckboxSelectionsProps => ({ dispatch: jest.fn(), group: fakePointGroup(), + slugs: ["mint"], }); - it("renders", () => { + it("renders all criteria", () => { + const STRINGS = [ + "planted", "mint", + "farm designer", "radius", "green", + "positive x", + ]; + const wrapper = mount(); + STRINGS.map(string => + expect(wrapper.text().toLowerCase()).not.toContain(string.toLowerCase())); + wrapper.setState({ Plant: true, GenericPointer: true, ToolSlot: true }); + STRINGS.map(string => + expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); + }); + + it("clears sub criteria", () => { const p = fakeProps(); - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; + p.group.body.criteria.string_eq = { plant_stage: ["planned"] }; const wrapper = mount(); - ["planted plants", "detected weeds", "created points", "created weeds", - ].map(string => - expect(wrapper.text()).toContain(string)); + wrapper.setState({ Plant: true, GenericPointer: false, ToolSlot: false }); + wrapper.find(".plant-criteria-options") + .find("input").first().simulate("change"); + expect(clearCriteriaField).toHaveBeenCalledWith( + p.group, ["string_eq"], "plant_stage", + ); }); - it("changes criteria", () => { + it("toggles section", () => { + const wrapper = + shallow(); + expect(wrapper.state().Plant).toBeFalsy(); + wrapper.instance().toggleMore("Plant")(); + expect(wrapper.state().Plant).toBeTruthy(); + }); + + it("toggles point type", () => { const p = fakeProps(); const wrapper = mount(); wrapper.find("input").first().simulate("change"); - expect(togglePointSelection).toHaveBeenCalledWith(p.group); - expect(mockToggle).toHaveBeenCalledWith({ - plant_stage: "planted", pointer_type: "Plant" - }); - }); -}); - -describe("criteriaSelected()", () => { - it("returns selection state: false", () => { - const result = criteriaSelected(undefined)({ pointer_type: "Plant" }); - expect(result).toEqual(false); + expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant"); }); - it("returns selection state: true", () => { - const result = criteriaSelected({ - pointer_type: ["Plant"] - })({ pointer_type: "Plant" }); - expect(result).toEqual(true); + it("stops propagation", () => { + const wrapper = mount(); + const e = { stopPropagation: jest.fn() }; + wrapper.find(".fb-checkbox").first().simulate("click", e); + expect(e.stopPropagation).toHaveBeenCalled(); + }); + + it("is not disabled", () => { + const p = fakeProps(); + const wrapper = mount(); + const pointTypeBoxes = wrapper.find(".point-type-checkbox").find("input"); + expect(pointTypeBoxes.first().props().disabled).toBeFalsy(); + expect(pointTypeBoxes.at(1).props().disabled).toBeFalsy(); + expect(pointTypeBoxes.last().props().disabled).toBeFalsy(); + }); + + it("is disabled", () => { + const p = fakeProps(); + p.group.body.criteria.string_eq = { plant_stage: ["planted"] }; + p.group.body.criteria.number_eq = { pullout_direction: [0] }; + p.group.body.criteria.number_gt = { radius: 0 }; + const wrapper = mount(); + const pointTypeBoxes = wrapper.find(".point-type-checkbox").find(Checkbox); + expect(pointTypeBoxes.first().props().disabled).toBeTruthy(); + expect(pointTypeBoxes.at(1).props().disabled).toBeTruthy(); + expect(pointTypeBoxes.last().props().disabled).toBeTruthy(); }); }); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts new file mode 100644 index 000000000..7592f8cc9 --- /dev/null +++ b/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts @@ -0,0 +1,70 @@ +import { eqCriteriaSelected, criteriaHasKey, hasSubCriteria, typeDisabled } from ".."; +import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces"; +import { cloneDeep } from "lodash"; + +const fakeCriteria = (): PointGroupCriteria => + cloneDeep(DEFAULT_CRITERIA); + +describe("eqCriteriaSelected()", () => { + it("returns selected", () => { + const criteria = fakeCriteria(); + criteria.number_eq = { pullout_direction: [0] }; + const result = eqCriteriaSelected(criteria)("pullout_direction", 0); + expect(result).toEqual(true); + }); + + it("returns not selected", () => { + const criteria = fakeCriteria(); + const result = eqCriteriaSelected(criteria)( + "pullout_direction", false as unknown as string); + expect(result).toEqual(false); + }); +}); + +describe("criteriaHasKey()", () => { + it("has key", () => { + const criteria = fakeCriteria(); + criteria.string_eq = { plant_stage: ["planted"] }; + const result = criteriaHasKey(criteria, ["string_eq"], "plant_stage"); + expect(result).toBeTruthy(); + }); + + it("doesn't have key", () => { + const criteria = fakeCriteria(); + criteria.string_eq = {}; + const result = criteriaHasKey(criteria, ["string_eq"], "plant_stage"); + expect(result).toBeFalsy(); + }); +}); + +describe("hasSubCriteria()", () => { + it("has criteria", () => { + const criteria = fakeCriteria(); + criteria.string_eq = { plant_stage: ["planted"] }; + const result = hasSubCriteria(criteria)("Plant"); + expect(result).toBeTruthy(); + }); + + it("doesn't have criteria", () => { + const criteria = fakeCriteria(); + criteria.string_eq = { "meta.color": ["red"] }; + const result = hasSubCriteria(criteria)("Plant"); + expect(result).toBeFalsy(); + }); +}); + +describe("typeDisabled()", () => { + it("is disabled", () => { + const criteria = fakeCriteria(); + criteria.string_eq = { "meta.color": ["red"] }; + const result = typeDisabled(criteria, "Plant"); + expect(result).toBeTruthy(); + }); + + it("isn't disabled", () => { + const criteria = fakeCriteria(); + criteria.string_eq = { plant_stage: ["planted"] }; + const result = typeDisabled(criteria, "Plant"); + expect(result).toBeFalsy(); + }); +}); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx index 98828c3b3..9a315dd6d 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx +++ b/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx @@ -1,13 +1,22 @@ -jest.mock("../../../../api/crud", () => ({ - overwrite: jest.fn(), - save: jest.fn(), +jest.mock("../edit", () => ({ + editCriteria: jest.fn(), + editGtLtCriteriaField: jest.fn(() => jest.fn()), + removeEqCriteriaValue: jest.fn(), + clearCriteriaField: jest.fn(), })); import React from "react"; import { mount, shallow } from "enzyme"; import { EqCriteriaSelection, - NumberCriteriaSelection, DaySelection, LocationSelection, AddCriteria, + NumberCriteriaSelection, + DaySelection, + LocationSelection, + NumberLtGtInput, + removeEqCriteriaValue, + clearCriteriaField, + editCriteria, + editGtLtCriteriaField, } from ".."; import { EqCriteriaSelectionProps, @@ -15,23 +24,21 @@ import { CriteriaSelectionProps, DEFAULT_CRITERIA, LocationSelectionProps, - GroupCriteriaProps, + NumberLtGtInputProps, } from "../interfaces"; import { fakePointGroup, } from "../../../../__test_support__/fake_state/resources"; -import { overwrite } from "../../../../api/crud"; -import { cloneDeep } from "lodash"; import { FBSelect } from "../../../../ui"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; +import { Actions } from "../../../../constants"; describe(" />", () => { const fakeProps = (): EqCriteriaSelectionProps => ({ criteria: DEFAULT_CRITERIA, group: fakePointGroup(), - dispatch: jest.fn(x => x(jest.fn())), + dispatch: jest.fn(), type: "string", - criteriaField: {}, + eqCriteria: {}, criteriaKey: "string_eq", }); @@ -43,12 +50,16 @@ describe(" />", () => { it("removes criteria", () => { const p = fakeProps(); - p.criteriaField = { openfarm_slug: ["slug"] }; + p.eqCriteria = { openfarm_slug: ["slug"] }; const wrapper = mount( {...p} />); wrapper.find("button").last().simulate("click"); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.string_eq = {}; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); + expect(removeEqCriteriaValue).toHaveBeenCalledWith( + p.group, + { openfarm_slug: ["slug"] }, + "string_eq", + "openfarm_slug", + "slug", + ); }); }); @@ -56,24 +67,29 @@ describe("", () => { const fakeProps = (): NumberCriteriaProps => ({ criteria: DEFAULT_CRITERIA, group: fakePointGroup(), - dispatch: jest.fn(x => x(jest.fn())), + dispatch: jest.fn(), criteriaKey: "number_lt", }); it("renders", () => { const p = fakeProps(); + p.criteria.number_lt = { x: 1 }; const wrapper = mount(); expect(wrapper.text()).toContain("<"); }); it("removes criteria", () => { const p = fakeProps(); - p.criteria.number_lt = { x: 1 }; + p.criteriaKey = "number_gt"; + p.criteria.number_gt = { x: 1 }; const wrapper = mount(); + expect(wrapper.text()).toContain(">"); wrapper.find("button").last().simulate("click"); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.number_lt = {}; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); + expect(clearCriteriaField).toHaveBeenCalledWith( + p.group, + ["number_gt"], + "x", + ); }); }); @@ -81,16 +97,17 @@ describe("", () => { const fakeProps = (): CriteriaSelectionProps => ({ criteria: DEFAULT_CRITERIA, group: fakePointGroup(), - dispatch: jest.fn(x => x(jest.fn())), + dispatch: jest.fn(), }); it("changes operator", () => { const p = fakeProps(); const wrapper = shallow(); wrapper.find(FBSelect).simulate("change", { label: "", value: "<" }); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.day.op = "<"; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); + expect(editCriteria).toHaveBeenCalledWith( + p.group, + { day: { days_ago: 0, op: "<" } }, + ); }); it("changes day value", () => { @@ -99,16 +116,46 @@ describe("", () => { wrapper.find("input").last().simulate("change", { currentTarget: { value: "1" } }); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.day.days_ago = 1; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); + expect(editCriteria).toHaveBeenCalledWith( + p.group, + { day: { days_ago: 1, op: "<" } }, + ); + }); +}); + +describe("", () => { + const fakeProps = (): NumberLtGtInputProps => ({ + criteriaKey: "x", + group: fakePointGroup(), + dispatch: jest.fn(), }); - it("handles missing criteria", () => { + it("changes number_gt", () => { const p = fakeProps(); - p.criteria = {} as PointGroup["criteria"]; - const wrapper = shallow(); - expect(wrapper.find("input").last().props().value).toEqual(0); + const wrapper = shallow(); + wrapper.find("input").first().simulate("blur", { + currentTarget: { value: "1" } + }); + expect(editGtLtCriteriaField).toHaveBeenCalledWith( + p.group, + "number_gt", + "x", + undefined, + ); + }); + + it("changes number_lt", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("input").last().simulate("blur", { + currentTarget: { value: "1" } + }); + expect(editGtLtCriteriaField).toHaveBeenCalledWith( + p.group, + "number_lt", + "x", + undefined, + ); }); }); @@ -116,84 +163,17 @@ describe("", () => { const fakeProps = (): LocationSelectionProps => ({ criteria: DEFAULT_CRITERIA, group: fakePointGroup(), - dispatch: jest.fn(x => x(jest.fn())), + dispatch: jest.fn(), + editGroupAreaInMap: false, }); - it("changes number_gt", () => { + it("toggles selection box behavior", () => { const p = fakeProps(); - const wrapper = shallow(); - wrapper.find("input").first().simulate("blur", { - currentTarget: { value: "1" } + const wrapper = mount(); + wrapper.find("button").first().simulate("click"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.EDIT_GROUP_AREA_IN_MAP, + payload: true }); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.number_gt = { x: 1 }; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); - }); - - it("changes number_lt", () => { - const p = fakeProps(); - const wrapper = shallow(); - wrapper.find("input").last().simulate("blur", { - currentTarget: { value: "1" } - }); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.number_lt = { x: 1, y: 1 }; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); - }); - - it("handles missing criteria", () => { - const p = fakeProps(); - p.criteria = {} as PointGroup["criteria"]; - const wrapper = shallow(); - expect(wrapper.find("input").first().props().defaultValue).toEqual(undefined); - expect(wrapper.find("input").last().props().defaultValue).toEqual(undefined); - }); -}); - -describe("", () => { - const fakeProps = (): GroupCriteriaProps => ({ - slugs: [], - group: fakePointGroup(), - dispatch: jest.fn(x => x(jest.fn(y => y(jest.fn())))), - }); - - it("renders", () => { - const p = fakeProps(); - p.group.body.criteria.string_eq = { - openfarm_slug: ["slug"], - pointer_type: ["Plant"], - plant_stage: ["planted"], - }; - const wrapper = mount(); - expect(wrapper.find("input").at(0).props().value).toEqual("Plant Type"); - expect(wrapper.find("input").at(1).props().value).toEqual("Slug"); - expect(wrapper.find("input").at(2).props().value).toEqual("Point Type"); - expect(wrapper.find("input").at(3).props().value).toEqual("Plants"); - expect(wrapper.find("input").at(4).props().value).toEqual("Plant Status"); - expect(wrapper.find("input").at(5).props().value).toEqual("Planted"); - }); - - it("removes criteria", () => { - const p = fakeProps(); - p.group.body.criteria.string_eq = { - openfarm_slug: ["slug"], - pointer_type: ["Plant"], - plant_stage: ["planted"], - }; - const wrapper = mount(); - wrapper.find("button").last().simulate("click"); - const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.string_eq = { - openfarm_slug: ["slug"], - pointer_type: ["Plant"], - }; - expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); - }); - - it("handles missing criteria", () => { - const p = fakeProps(); - p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; - const wrapper = mount(); - expect(wrapper.text()).toEqual("SelectNone"); }); }); diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx new file mode 100644 index 000000000..7bd37dfdf --- /dev/null +++ b/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx @@ -0,0 +1,32 @@ +jest.mock("../edit", () => ({ + toggleAndEditEqCriteria: jest.fn(), +})); + +import React from "react"; +import { mount } from "enzyme"; +import { toggleAndEditEqCriteria } from ".."; +import { CheckboxListProps } from "../interfaces"; +import { + fakePointGroup, +} from "../../../../__test_support__/fake_state/resources"; +import { CheckboxList } from "../subcriteria"; + +describe("", () => { + const fakeProps = (): CheckboxListProps => ({ + criteriaKey: "openfarm_slug", + list: [{ label: "label", value: "value" }], + dispatch: jest.fn(), + group: fakePointGroup(), + pointerType: "Plant", + disabled: false, + }); + + it("toggles criteria", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text()).toContain("label"); + wrapper.find("input").first().simulate("change"); + expect(toggleAndEditEqCriteria).toHaveBeenCalledWith( + p.group, "openfarm_slug", "value", "Plant"); + }); +}); diff --git a/frontend/farm_designer/point_groups/criteria/add.tsx b/frontend/farm_designer/point_groups/criteria/add.tsx index 0f4159973..7ecefb3b8 100644 --- a/frontend/farm_designer/point_groups/criteria/add.tsx +++ b/frontend/farm_designer/point_groups/criteria/add.tsx @@ -1,32 +1,28 @@ import * as React from "react"; import { t } from "../../../i18next_wrapper"; -import { cloneDeep, capitalize } from "lodash"; -import { Row, Col, FBSelect, DropDownItem } from "../../../ui"; -import { editCriteria, toggleStringCriteria } from "."; +import { cloneDeep, uniq } from "lodash"; +import { Row, Col } from "../../../ui"; +import { editCriteria } from "."; import { AddEqCriteriaProps, AddEqCriteriaState, NumberCriteriaProps, AddNumberCriteriaState, - AddStringCriteriaProps, } from "./interfaces"; -import { - PLANT_STAGE_DDI_LOOKUP, PLANT_STAGE_LIST, -} from "../../plants/edit_plant_status"; export class AddEqCriteria extends React.Component, AddEqCriteriaState> { state: AddEqCriteriaState = { key: "", value: "" }; commit = () => { - const { dispatch, group, criteriaKey, criteriaField } = this.props; - const tempEqCriteria = cloneDeep(criteriaField || {}); + const { dispatch, group, criteriaKey, eqCriteria } = this.props; + const tempEqCriteria = cloneDeep(eqCriteria); const tempValues = tempEqCriteria[this.state.key] || []; const value = this.props.type == "number" ? parseInt(this.state.value) : this.state.value; this.state.value && tempValues.push(value as T); - tempEqCriteria[this.state.key] = tempValues; + tempEqCriteria[this.state.key] = uniq(tempValues); dispatch(editCriteria(group, { [criteriaKey]: tempEqCriteria })); this.setState({ key: "", value: "" }); } @@ -62,115 +58,13 @@ export class AddEqCriteria } } -export const CRITERIA_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ - pointer_type: { label: t("Point Type"), value: "pointer_type" }, - plant_stage: { label: t("Plant Status"), value: "plant_stage" }, - openfarm_slug: { label: t("Plant Type"), value: "openfarm_slug" }, -}); -export const CRITERIA_TYPE_LIST = () => [ - CRITERIA_TYPE_DDI_LOOKUP().pointer_type, - CRITERIA_TYPE_DDI_LOOKUP().plant_stage, - CRITERIA_TYPE_DDI_LOOKUP().openfarm_slug, -]; - -export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ - Plant: { label: t("Plants"), value: "Plant" }, - GenericPointer: { label: t("Points"), value: "GenericPointer" }, - ToolSlot: { label: t("Slots"), value: "ToolSlot" }, -}); -export const POINTER_TYPE_LIST = () => [ - POINTER_TYPE_DDI_LOOKUP().Plant, - POINTER_TYPE_DDI_LOOKUP().GenericPointer, - POINTER_TYPE_DDI_LOOKUP().ToolSlot, -]; - -export class AddStringCriteria - extends React.Component { - state: AddEqCriteriaState = { key: "", value: "" }; - - commit = () => { - if (this.state.key && this.state.value) { - this.props.dispatch(toggleStringCriteria(this.props.group, - this.state.key, this.state.value)); - this.setState({ key: "", value: "" }); - } - } - - get key() { return JSON.stringify(this.props.group.body.criteria || {}); } - - change = (ddi: DropDownItem) => this.setState({ value: "" + ddi.value }); - - get selected() { - switch (this.state.key) { - case "openfarm_slug": - return this.state.value - ? { label: t(capitalize(this.state.value)), value: this.state.value } - : undefined; - case "pointer_type": - return this.state.value - ? POINTER_TYPE_DDI_LOOKUP()[this.state.value] - : undefined; - case "plant_stage": - return this.state.value - ? PLANT_STAGE_DDI_LOOKUP()[this.state.value] - : undefined; - default: - return undefined; - } - } - - get options() { - switch (this.state.key) { - case "openfarm_slug": - return this.props.slugs.map(slug => - ({ label: t(capitalize(slug)), value: slug })); - case "pointer_type": - return POINTER_TYPE_LIST(); - case "plant_stage": - return PLANT_STAGE_LIST(); - default: - return []; - } - } - - render() { - const noKey = this.options.length < 1; - return
- - - this.setState({ key: "" + ddi.value })} /> - - - - - - - - -
; - } -} - export class AddNumberCriteria extends React.Component { state: AddNumberCriteriaState = { key: "", value: 0 }; commit = () => { const { dispatch, group, criteriaKey } = this.props; - const tempNumberCriteria = - cloneDeep(group.body.criteria?.[criteriaKey] || {}); + const tempNumberCriteria = cloneDeep(group.body.criteria[criteriaKey]); tempNumberCriteria[this.state.key] = this.state.value; dispatch(editCriteria(group, { [criteriaKey]: tempNumberCriteria })); this.setState({ key: "", value: 0 }); diff --git a/frontend/farm_designer/point_groups/criteria/apply.ts b/frontend/farm_designer/point_groups/criteria/apply.ts index 10aa45403..530871890 100644 --- a/frontend/farm_designer/point_groups/criteria/apply.ts +++ b/frontend/farm_designer/point_groups/criteria/apply.ts @@ -1,16 +1,17 @@ -import { every, get, isEqual, uniq, gt, lt, isNumber } from "lodash"; +import { every, get, uniq, gt, lt, isNumber } from "lodash"; import { TaggedPoint, TaggedPointGroup } from "farmbot"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; import moment from "moment"; -import { DEFAULT_CRITERIA } from "./interfaces"; +import { PointGroupCriteria } from "./interfaces"; +/** Check if a string or number criteria field is empty. */ const eqCriteriaEmpty = (eqCriteria: Record) => every(Object.values(eqCriteria).map(values => !values?.length)); +/** Check if a point matches the criteria in the provided category. */ const checkCriteria = - (criteria: PointGroup["criteria"], now: moment.Moment) => - (point: TaggedPoint, criteriaKey: keyof PointGroup["criteria"]) => { + (criteria: PointGroupCriteria, now: moment.Moment) => + (point: TaggedPoint, criteriaKey: keyof PointGroupCriteria) => { switch (criteriaKey) { case "string_eq": case "number_eq": @@ -38,18 +39,19 @@ const checkCriteria = } }; +/** Check if a point matches all criteria provided. */ export const selectPointsByCriteria = ( - criteria: PointGroup["criteria"] | undefined, + criteria: PointGroupCriteria, allPoints: TaggedPoint[], now = moment(), ): TaggedPoint[] => { - if (!criteria || isEqual(criteria, DEFAULT_CRITERIA)) { return []; } const check = checkCriteria(criteria, now); return allPoints.filter(point => - every(Object.keys(criteria).map((key: keyof PointGroup["criteria"]) => + every(Object.keys(criteria).map((key: keyof PointGroupCriteria) => check(point, key)))); }; +/** Return all points selected by group manual additions and criteria. */ export const pointsSelectedByGroup = (group: TaggedPointGroup, allPoints: TaggedPoint[]) => uniq(allPoints diff --git a/frontend/farm_designer/point_groups/criteria/component.tsx b/frontend/farm_designer/point_groups/criteria/component.tsx index 18d9f006f..5e0432ecd 100644 --- a/frontend/farm_designer/point_groups/criteria/component.tsx +++ b/frontend/farm_designer/point_groups/criteria/component.tsx @@ -2,63 +2,68 @@ import * as React from "react"; import { t } from "../../../i18next_wrapper"; import { overwrite, save } from "../../../api/crud"; import { - CheckboxSelections, DaySelection, EqCriteriaSelection, - NumberCriteriaSelection, LocationSelection, AddCriteria, + DaySelection, EqCriteriaSelection, + NumberCriteriaSelection, LocationSelection, CheckboxSelections, } from "."; import { GroupCriteriaProps, GroupPointCountBreakdownProps, GroupCriteriaState, - DEFAULT_CRITERIA, + DEFAULT_CRITERIA, ClearCriteriaProps, } from "./interfaces"; -import { ExpandableHeader } from "../../../ui"; -import { Collapse } from "@blueprintjs/core"; +import { ToggleButton } from "../../../controls/toggle_button"; export class GroupCriteria extends React.Component { state: GroupCriteriaState = { advanced: false, clearCount: 0 }; render() { const { group, dispatch, slugs } = this.props; - const criteria = group.body.criteria || {}; + const criteria = group.body.criteria; const commonProps = { group, criteria, dispatch }; return
- -
- - -
- - - - - this.setState({ advanced: !this.state.advanced })} /> - - - {...commonProps} - type={"string"} criteriaField={criteria.string_eq} - criteriaKey={"string_eq"} /> - - {...commonProps} - type={"number"} criteriaField={criteria.number_eq} - criteriaKey={"number_eq"} /> - - - + this.setState({ advanced: !this.state.advanced })} /> + {!this.state.advanced + ?
+ + + +
+ :
+ + + {...commonProps} + type={"string"} eqCriteria={criteria.string_eq} + criteriaKey={"string_eq"} /> + + {...commonProps} + type={"number"} eqCriteria={criteria.number_eq} + criteriaKey={"number_eq"} /> + + +
} +
; } } +/** Reset all group criteria to defaults. */ +const ClearCriteria = (props: ClearCriteriaProps) => + ; + +/** Show counts of manual and criteria selections. */ export const GroupPointCountBreakdown = (props: GroupPointCountBreakdownProps) =>
diff --git a/frontend/farm_designer/point_groups/criteria/edit.ts b/frontend/farm_designer/point_groups/criteria/edit.ts index 88917c8e4..551f7ed1c 100644 --- a/frontend/farm_designer/point_groups/criteria/edit.ts +++ b/frontend/farm_designer/point_groups/criteria/edit.ts @@ -1,64 +1,130 @@ import { overwrite, save } from "../../../api/crud"; import { TaggedPointGroup } from "farmbot"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; import { cloneDeep, isNumber } from "lodash"; import { SelectionBoxData } from "../../map/background"; -import { DEFAULT_CRITERIA } from "./interfaces"; +import { + PointGroupCriteria, POINTER_TYPES, EqCriteria, PointerType, + StrAndNumCriteriaKeys, +} from "./interfaces"; +/** Update and save group criteria. */ export const editCriteria = - (group: TaggedPointGroup, update: Partial) => + (group: TaggedPointGroup, update: Partial) => (dispatch: Function) => { const criteria = { - string_eq: update.string_eq || group.body.criteria?.string_eq || {}, - day: update.day || group.body.criteria?.day || DEFAULT_CRITERIA.day, - number_eq: update.number_eq || group.body.criteria?.number_eq || {}, - number_gt: update.number_gt || group.body.criteria?.number_gt || {}, - number_lt: update.number_lt || group.body.criteria?.number_lt || {}, + string_eq: update.string_eq || group.body.criteria.string_eq, + day: update.day || group.body.criteria.day, + number_eq: update.number_eq || group.body.criteria.number_eq, + number_gt: update.number_gt || group.body.criteria.number_gt, + number_lt: update.number_lt || group.body.criteria.number_lt, }; dispatch(overwrite(group, { ...group.body, criteria })); dispatch(save(group.uuid)); }; +/** Toggle string or number equal criteria. */ export const toggleEqCriteria = ( - eqCriteria: Record, -) => - (key: string, value: T): Record => { - const values: T[] = eqCriteria[key] || []; - if (values.includes(value)) { + eqCriteria: EqCriteria, + direction?: "on" | "off", +) => (key: string, value: T) => { + const values: T[] = eqCriteria[key] || []; + if (values.includes(value)) { + if (direction != "on") { const newValues = values.filter(s => s != value); eqCriteria[key] = newValues; !newValues.length && delete eqCriteria[key]; - } else { + } + } else { + if (direction != "off") { values.push(value); eqCriteria[key] = values; } - return eqCriteria; + } +}; + +/** + * Toggle and save string or number equal criteria. + * When adding criteria with a pointerType provided, clear incompatible criteria. + */ +export const toggleAndEditEqCriteria = ( + group: TaggedPointGroup, + key: string, + value: T, + pointerType?: PointerType, +) => + (dispatch: Function) => { + const tempCriteria = cloneDeep(group.body.criteria); + const criteriaField = typeof value == "string" ? "string_eq" : "number_eq"; + const tempEqCriteria = tempCriteria[criteriaField] as EqCriteria; + const wasOff = !tempEqCriteria[key]?.includes(value); + toggleEqCriteria(tempEqCriteria)(key, value); + pointerType && wasOff && clearSubCriteria( + POINTER_TYPES.filter(x => x != pointerType), tempCriteria); + dispatch(editCriteria(group, tempCriteria)); }; -export const togglePointSelection = - (group: TaggedPointGroup) => (toggleCriteria: Record) => +/** Clear incompatible criteria. */ +const clearSubCriteria = ( + pointerTypes: PointerType[], + tempCriteria: PointGroupCriteria, +) => { + const toggleStrEq = toggleEqCriteria(tempCriteria.string_eq, "off"); + const toggleNumEq = toggleEqCriteria(tempCriteria.number_eq, "off"); + if (pointerTypes.includes("Plant")) { + Object.entries(tempCriteria.string_eq).map(([key, values]) => + ["openfarm_slug", "plant_stage"].includes(key) + && values?.map(v => toggleStrEq(key, v))); + toggleStrEq("pointer_type", "Plant"); + } + if (pointerTypes.includes("GenericPointer")) { + Object.entries(tempCriteria.string_eq).map(([key, values]) => + key.includes("meta") && values?.map(v => toggleStrEq(key, v))); + delete tempCriteria.number_lt.radius; + delete tempCriteria.number_gt.radius; + toggleStrEq("pointer_type", "GenericPointer"); + } + if (pointerTypes.includes("ToolSlot")) { + tempCriteria.number_eq.pullout_direction?.map(value => + toggleNumEq("pullout_direction", value)); + toggleStrEq("pointer_type", "ToolSlot"); + } +}; + +/** + * Toggle and save pointer_type string equal criteria. + * When removing pointer_type criteria, clear pointer_type-specific criteria. + */ +export const togglePointTypeCriteria = + (group: TaggedPointGroup, pointerType: PointerType) => (dispatch: Function) => { - const stringCriteria = {}; - const toggle = toggleEqCriteria(stringCriteria); - Object.entries(toggleCriteria).map(([key, value]) => toggle(key, value)); - dispatch(editCriteria(group, { string_eq: stringCriteria })); + const tempCriteria = cloneDeep(group.body.criteria); + const wasOn = tempCriteria.string_eq.pointer_type?.includes(pointerType); + const toggle = toggleEqCriteria(tempCriteria.string_eq); + toggle("pointer_type", pointerType); + wasOn && clearSubCriteria([pointerType], tempCriteria); + dispatch(editCriteria(group, tempCriteria)); }; -export const toggleStringCriteria = - (group: TaggedPointGroup, key: string, value: string) => - (dispatch: Function) => { - const tempStringCriteria = cloneDeep(group.body.criteria?.string_eq || {}); - toggleEqCriteria(tempStringCriteria)(key, value); - dispatch(editCriteria(group, { string_eq: tempStringCriteria })); - }; +/** Clear and save all fields in the provided criteria categories. */ +export const clearCriteriaField = ( + group: TaggedPointGroup, + categories: StrAndNumCriteriaKeys, + field: string, +) => + (dispatch: Function) => { + const tempCriteria = cloneDeep(group.body.criteria); + categories.map(category => delete tempCriteria[category][field]); + dispatch(editCriteria(group, tempCriteria)); + }; +/** For map selection box actions maybeUpdateGroup. */ export const editGtLtCriteria = (group: TaggedPointGroup, box: SelectionBoxData) => (dispatch: Function) => { if (!(isNumber(box.x0) && isNumber(box.y0) && isNumber(box.x1) && isNumber(box.y1))) { return; } - const tempGtCriteria = cloneDeep(group.body.criteria?.number_gt || {}); - const tempLtCriteria = cloneDeep(group.body.criteria?.number_lt || {}); + const tempGtCriteria = cloneDeep(group.body.criteria.number_gt); + const tempLtCriteria = cloneDeep(group.body.criteria.number_lt); tempGtCriteria.x = Math.min(box.x0, box.x1); tempGtCriteria.y = Math.min(box.y0, box.y1); tempLtCriteria.x = Math.max(box.x0, box.x1); @@ -68,3 +134,36 @@ export const editGtLtCriteria = number_lt: tempLtCriteria, })); }; + +/** For EqCriteriaSelection form. */ +export const removeEqCriteriaValue = ( + group: TaggedPointGroup, + eqCriteria: EqCriteria, + eqCriteriaName: string, + key: string, + value: T, +) => (dispatch: Function) => { + const tempCriteriaField = cloneDeep(eqCriteria); + toggleEqCriteria(tempCriteriaField, "off")(key, value); + dispatch(editCriteria(group, { [eqCriteriaName]: tempCriteriaField })); +}; + +/** + * For criteria form NumberLtGtInput. + * Clear incompatible criteria if pointer_type is provided. + */ +export const editGtLtCriteriaField = ( + group: TaggedPointGroup, + criteriaField: "number_gt" | "number_lt", + criteriaKey: string, + pointerType?: PointerType, +) => + (e: React.FormEvent) => + (dispatch: Function) => { + const tempCriteria = cloneDeep(group.body.criteria); + pointerType && clearSubCriteria( + POINTER_TYPES.filter(x => x != pointerType), tempCriteria); + tempCriteria[criteriaField][criteriaKey] = + parseInt(e.currentTarget.value); + dispatch(editCriteria(group, tempCriteria)); + }; diff --git a/frontend/farm_designer/point_groups/criteria/index.tsx b/frontend/farm_designer/point_groups/criteria/index.tsx index 002cc0ac2..de7ffac22 100644 --- a/frontend/farm_designer/point_groups/criteria/index.tsx +++ b/frontend/farm_designer/point_groups/criteria/index.tsx @@ -3,4 +3,6 @@ export * from "./apply"; export * from "./component"; export * from "./edit"; export * from "./presets"; +export * from "./selected"; export * from "./show"; +export * from "./subcriteria"; diff --git a/frontend/farm_designer/point_groups/criteria/interfaces.ts b/frontend/farm_designer/point_groups/criteria/interfaces.ts index 4ca2bd4fc..403454f67 100644 --- a/frontend/farm_designer/point_groups/criteria/interfaces.ts +++ b/frontend/farm_designer/point_groups/criteria/interfaces.ts @@ -1,21 +1,28 @@ import { TaggedPointGroup } from "farmbot"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; +import { PointGroup, Point } from "farmbot/dist/resources/api_resources"; -export const DEFAULT_CRITERIA: Readonly = { +export type PointGroupCriteria = PointGroup["criteria"]; +export type StringEqCriteria = PointGroupCriteria["string_eq"]; +export type PointerType = Point["pointer_type"]; +export type StrAndNumCriteriaKeys = (keyof Omit)[]; +export type EqCriteria = Record; + +export const POINTER_TYPES: PointerType[] = + ["Plant", "GenericPointer", "ToolSlot"]; + +export const DEFAULT_CRITERIA: Readonly = { day: { op: "<", days_ago: 0 }, number_eq: {}, number_gt: {}, number_lt: {}, - string_eq: {}, + string_eq: { pointer_type: ["Plant"] }, }; -export type EqCriteria = Record | undefined; -export type StringEqCriteria = PointGroup["criteria"]["string_eq"] | undefined; - export interface GroupCriteriaProps { dispatch: Function; group: TaggedPointGroup; slugs: string[]; + editGroupAreaInMap: boolean; } export interface GroupCriteriaState { @@ -23,24 +30,30 @@ export interface GroupCriteriaState { clearCount: number; } +export interface ClearCriteriaProps { + dispatch: Function; + group: TaggedPointGroup; +} + export interface GroupPointCountBreakdownProps { manualCount: number; totalCount: number; } export interface CriteriaSelectionProps { - criteria: PointGroup["criteria"]; + criteria: PointGroupCriteria; group: TaggedPointGroup; dispatch: Function; } export interface LocationSelectionProps extends CriteriaSelectionProps { + editGroupAreaInMap: boolean; } export interface EqCriteriaSelectionProps extends CriteriaSelectionProps { type: "string" | "number"; - criteriaField: Record | undefined; - criteriaKey: keyof PointGroup["criteria"]; + eqCriteria: EqCriteria; + criteriaKey: keyof PointGroupCriteria; } export interface NumberCriteriaProps extends CriteriaSelectionProps { @@ -51,31 +64,64 @@ export interface AddEqCriteriaProps { dispatch: Function; group: TaggedPointGroup; type: "string" | "number"; - criteriaField: Record | undefined; - criteriaKey: keyof PointGroup["criteria"]; + eqCriteria: EqCriteria; + criteriaKey: keyof PointGroupCriteria; } export interface AddEqCriteriaState { key: string; value: string; } -export interface AddCriteriaState { - key: string; - value: string; -} - -export interface AddStringCriteriaProps { - group: TaggedPointGroup; - dispatch: Function; - slugs: string[]; -} export interface AddNumberCriteriaState { key: string; value: number; } +export interface SubCriteriaProps { + dispatch: Function; + group: TaggedPointGroup; + disabled: boolean; +} + +export interface PlantSubCriteriaProps extends SubCriteriaProps { + slugs: string[]; +} + export interface CheckboxSelectionsProps { dispatch: Function; group: TaggedPointGroup; + slugs: string[]; +} + +export interface CheckboxSelectionsState { + Plant: boolean; + GenericPointer: boolean; + ToolSlot: boolean; +} + +export interface NumberLtGtInputProps { + criteriaKey: "x" | "y" | "radius"; + group: TaggedPointGroup; + dispatch: Function; + inputWidth?: number; + labelWidth?: number; + disabled?: boolean; + pointerType?: PointerType; +} + +export interface ClearCategoryProps { + group: TaggedPointGroup; + criteriaCategories: StrAndNumCriteriaKeys; + criteriaKey: string; + dispatch: Function; +} + +export interface CheckboxListProps { + criteriaKey: string; + list: { label: string, value: T }[]; + dispatch: Function; + group: TaggedPointGroup; + pointerType: PointerType; + disabled?: boolean; } diff --git a/frontend/farm_designer/point_groups/criteria/presets.tsx b/frontend/farm_designer/point_groups/criteria/presets.tsx index fc9821167..f0bd83b44 100644 --- a/frontend/farm_designer/point_groups/criteria/presets.tsx +++ b/frontend/farm_designer/point_groups/criteria/presets.tsx @@ -1,58 +1,77 @@ import * as React from "react"; import { t } from "../../../i18next_wrapper"; -import { every } from "lodash"; -import { togglePointSelection } from "."; -import { CheckboxSelectionsProps, StringEqCriteria } from "./interfaces"; +import { + togglePointTypeCriteria, + eqCriteriaSelected, + hasSubCriteria, + typeDisabled, + PlantCriteria, + PointCriteria, + ToolCriteria, +} from "."; +import { + CheckboxSelectionsProps, + CheckboxSelectionsState, + PointerType, +} from "./interfaces"; +import { Checkbox } from "../../../ui"; -const CRITERIA_PRESETS = (): { - description: string, criteria: Record -}[] => [ - { - description: t("planted plants"), - criteria: { - "pointer_type": "Plant", - "plant_stage": "planted", - } - }, - { - description: t("detected weeds"), - criteria: { - "meta.created_by": "plant-detection", - "meta.color": "red", - } - }, - { - description: t("created points"), - criteria: { - "meta.created_by": "farm-designer", - "meta.type": "point", - } - }, - { - description: t("created weeds"), - criteria: { - "meta.created_by": "farm-designer", - "meta.type": "weed", - } - }, +const CRITERIA_POINT_TYPES = + (): { label: string, pointerType: PointerType }[] => [ + { label: t("Plants"), pointerType: "Plant" }, + { label: t("Points and Weeds"), pointerType: "GenericPointer" }, + { label: t("Slots"), pointerType: "ToolSlot" }, ]; -export const CheckboxSelections = (props: CheckboxSelectionsProps) => { - const toggle = togglePointSelection(props.group); - const stringCriteria = props.group.body.criteria?.string_eq; - const selected = criteriaSelected(stringCriteria); - return
- {CRITERIA_PRESETS().map((selector, index) => -
- props.dispatch(toggle(selector.criteria))} - checked={selected(selector.criteria)} /> -

{selector.description}

-
)} -
; -}; +export class CheckboxSelections extends React.Component + > { + state: CheckboxSelectionsState = { + Plant: false, GenericPointer: false, ToolSlot: false + }; -export const criteriaSelected = (stringCriteria: StringEqCriteria) => - (selectionCriteria: Record) => - every(Object.entries(selectionCriteria).map(([key, value]) => - stringCriteria?.[key]?.includes(value))); + toggleMore = (section: keyof CheckboxSelectionsState) => () => + this.setState({ [section]: !this.state[section] }); + + render() { + const { group, dispatch, slugs } = this.props; + const { criteria } = group.body; + const selected = eqCriteriaSelected(criteria); + return
+ {CRITERIA_POINT_TYPES().map(({ label, pointerType }, index) => { + const typeSelected = selected("pointer_type", pointerType); + const partial = hasSubCriteria(criteria)(pointerType) && !typeSelected; + return
+
+ + dispatch(togglePointTypeCriteria(group, pointerType))} + checked={typeSelected} + partial={partial} + title={t(label)} + disabled={typeDisabled(criteria, pointerType)} + onClick={e => e.stopPropagation()} /> +

{label}

+ +
+ {this.state.Plant && pointerType == "Plant" && + } + {this.state.GenericPointer && pointerType == "GenericPointer" && + } + {this.state.ToolSlot && pointerType == "ToolSlot" && + } +
; + })} +
; + } +} diff --git a/frontend/farm_designer/point_groups/criteria/selected.ts b/frontend/farm_designer/point_groups/criteria/selected.ts new file mode 100644 index 000000000..6a0c84a49 --- /dev/null +++ b/frontend/farm_designer/point_groups/criteria/selected.ts @@ -0,0 +1,76 @@ +import { isNumber, some } from "lodash"; +import { + StringEqCriteria, + PointerType, + PointGroupCriteria, + StrAndNumCriteriaKeys, + POINTER_TYPES, +} from "./interfaces"; + +/** Check if equal criteria values exist. */ +export const eqCriteriaSelected = + (criteria: PointGroupCriteria) => + (key: string, value: T): boolean => { + if (typeof value == "string") { + return !!criteria.string_eq[key]?.includes(value); + } + if (typeof value == "number") { + return !!criteria.number_eq[key]?.includes(value); + } + return false; + }; + +/** Check if string equal criteria fields exist. */ +const strCriteriaHasKey = (stringCriteria: StringEqCriteria) => + (key: string) => (stringCriteria[key]?.length || 0) > 0; + +/** Check if number criteria fields exist. */ +const numCriteriaHasKey = (criteria: PointGroupCriteria) => + (key: string) => (criteria.number_eq[key]?.length || 0) > 0 + || isNumber(criteria.number_lt[key]) + || isNumber(criteria.number_gt[key]); + +/** Check if a string or number criteria field exists. */ +export const criteriaHasKey = ( + criteria: PointGroupCriteria, + categories: StrAndNumCriteriaKeys, + key: string, +) => + some(categories.map(category => { + if (category == "string_eq") { + return strCriteriaHasKey(criteria.string_eq)(key); + } else { + return numCriteriaHasKey(criteria)(key); + } + })); + +/** Check for point type specific sub criteria. */ +export const hasSubCriteria = (criteria: PointGroupCriteria) => + (pointerType: PointerType) => { + const selected = strCriteriaHasKey(criteria.string_eq); + const numSelected = numCriteriaHasKey(criteria); + switch (pointerType) { + case "GenericPointer": + return !!( + selected("meta.type") + || selected("meta.color") + || selected("meta.created_by") + || numSelected("radius")); + case "Plant": + return !!( + selected("openfarm_slug") + || selected("plant_stage")); + case "ToolSlot": + return !!( + numSelected("tool_id") + || numSelected("pullout_direction") + || numSelected("gantry_mounted")); + } + }; + +/** Check for criteria specific to other point types. */ +export const typeDisabled = + (criteria: PointGroupCriteria, pointerType: PointerType): boolean => + some(POINTER_TYPES + .filter(x => x != pointerType) + .map(hasSubCriteria(criteria))); diff --git a/frontend/farm_designer/point_groups/criteria/show.tsx b/frontend/farm_designer/point_groups/criteria/show.tsx index bab1fc483..ba4fd005f 100644 --- a/frontend/farm_designer/point_groups/criteria/show.tsx +++ b/frontend/farm_designer/point_groups/criteria/show.tsx @@ -1,34 +1,35 @@ import * as React from "react"; -import { cloneDeep, capitalize } from "lodash"; import { Row, Col, FBSelect, DropDownItem } from "../../../ui"; import { - AddEqCriteria, toggleEqCriteria, editCriteria, AddNumberCriteria, - POINTER_TYPE_DDI_LOOKUP, AddStringCriteria, - CRITERIA_TYPE_DDI_LOOKUP, toggleStringCriteria, + AddEqCriteria, editCriteria, AddNumberCriteria, + editGtLtCriteriaField, + removeEqCriteriaValue, + clearCriteriaField, } from "."; import { EqCriteriaSelectionProps, NumberCriteriaProps, - CriteriaSelectionProps, LocationSelectionProps, GroupCriteriaProps, - AddCriteriaState, - DEFAULT_CRITERIA, + CriteriaSelectionProps, LocationSelectionProps, + NumberLtGtInputProps, + PointGroupCriteria, } from "./interfaces"; import { t } from "../../../i18next_wrapper"; -import { PointGroup } from "farmbot/dist/resources/api_resources"; -import { PLANT_STAGE_DDI_LOOKUP } from "../../plants/edit_plant_status"; +import { ToggleButton } from "../../../controls/toggle_button"; +import { Actions } from "../../../constants"; +/** Add and view string or number equal criteria. */ export class EqCriteriaSelection extends React.Component> { render() { - const { criteriaField, criteriaKey, group, dispatch } = this.props; + const { eqCriteria, criteriaKey, group, dispatch } = this.props; return
group={group} dispatch={dispatch} - type={this.props.type} criteriaField={criteriaField} + type={this.props.type} eqCriteria={eqCriteria} criteriaKey={criteriaKey} /> - {criteriaField && Object.entries(criteriaField) + {eqCriteria && Object.entries(eqCriteria) .map(([key, values]: [string, T[]], keyIndex) => values && values.length > 0 &&
- + {key} {values.map((value, valueIndex) => @@ -39,13 +40,8 @@ export class EqCriteriaSelection @@ -55,6 +51,7 @@ export class EqCriteriaSelection } } +/** Add and view > or < number criteria. */ export const NumberCriteriaSelection = (props: NumberCriteriaProps) => { const criteriaField = props.criteria[props.criteriaKey]; return
@@ -70,21 +67,13 @@ export const NumberCriteriaSelection = (props: NumberCriteriaProps) => { {props.criteriaKey == "number_gt" ? ">" : "<"} - +

{value}

@@ -98,9 +87,10 @@ const DAY_OPERATOR_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ [">"]: { label: t("greater than"), value: ">" }, }); +/** Edit and view day criteria. */ export const DaySelection = (props: CriteriaSelectionProps) => { const { group, criteria, dispatch } = props; - const dayCriteria = criteria.day || cloneDeep(DEFAULT_CRITERIA.day); + const dayCriteria = criteria.day; return
@@ -112,7 +102,7 @@ export const DaySelection = (props: CriteriaSelectionProps) => { onChange={ddi => dispatch(editCriteria(group, { day: { days_ago: dayCriteria.days_ago, - op: ddi.value as PointGroup["criteria"]["day"]["op"] + op: ddi.value as PointGroupCriteria["day"]["op"] } }))} /> @@ -131,96 +121,64 @@ export const DaySelection = (props: CriteriaSelectionProps) => {
; }; -export const LocationSelection = (props: LocationSelectionProps) => { - const { group, criteria, dispatch } = props; - const gtCriteria = criteria.number_gt || {}; - const ltCriteria = criteria.number_lt || {}; - return
- - {["x", "y"].map(axis => - - - { - const tempGtCriteria = cloneDeep(gtCriteria); - tempGtCriteria[axis] = parseInt(e.currentTarget.value); - dispatch(editCriteria(group, { number_gt: tempGtCriteria })); - }} /> - - -

{"<"}

- - - - - -

{"<"}

- - - { - const tempLtCriteria = cloneDeep(ltCriteria); - tempLtCriteria[axis] = parseInt(e.currentTarget.value); - dispatch(editCriteria(group, { number_lt: tempLtCriteria })); - }} /> - -
)} -
; +/** Edit number < and > criteria. */ +export const NumberLtGtInput = (props: NumberLtGtInputProps) => { + const { group, dispatch, criteriaKey, pointerType } = props; + const gtCriteria = props.group.body.criteria.number_gt; + const ltCriteria = props.group.body.criteria.number_lt; + return + + dispatch(editGtLtCriteriaField( + group, "number_gt", criteriaKey, pointerType)(e))} /> + + +

{"<"}

+ + +

{criteriaKey}

+ + +

{"<"}

+ + + dispatch(editGtLtCriteriaField( + group, "number_lt", criteriaKey, pointerType)(e))} /> + +
; }; -export class AddCriteria - extends React.Component { - - labelLookup = (key: string, value: string) => { - switch (key) { - case "openfarm_slug": - return capitalize(value); - case "pointer_type": - return POINTER_TYPE_DDI_LOOKUP()[value].label; - case "plant_stage": - return PLANT_STAGE_DDI_LOOKUP()[value].label; - } - } - - render() { - const { props } = this; - const stringCriteria = this.props.group.body.criteria?.string_eq || {}; - const displayedCriteria = Object.entries(stringCriteria) - .filter(([key, _values]) => - ["openfarm_slug", "pointer_type", "plant_stage"].includes(key)); - return
- - {displayedCriteria.map(([key, values]) => - values && values.map((value, index) => -
- - - - - - - - - - - -
))} -
; - } -} +/** Form inputs to define a 2D group criteria area. */ +export const LocationSelection = (props: LocationSelectionProps) => +
+ + {["x", "y"].map((axis: "x" | "y") => + )} +
+ + props.dispatch({ + type: Actions.EDIT_GROUP_AREA_IN_MAP, + payload: !props.editGroupAreaInMap + })} /> + +
+
; diff --git a/frontend/farm_designer/point_groups/criteria/subcriteria.tsx b/frontend/farm_designer/point_groups/criteria/subcriteria.tsx new file mode 100644 index 000000000..ac3db6b82 --- /dev/null +++ b/frontend/farm_designer/point_groups/criteria/subcriteria.tsx @@ -0,0 +1,230 @@ +import * as React from "react"; +import { t } from "../../../i18next_wrapper"; +import { capitalize, uniq } from "lodash"; +import { + NumberLtGtInput, + toggleAndEditEqCriteria, + clearCriteriaField, + eqCriteriaSelected, + criteriaHasKey, +} from "."; +import { + CheckboxListProps, + SubCriteriaProps, + PlantSubCriteriaProps, + ClearCategoryProps, +} from "./interfaces"; +import { PLANT_STAGE_LIST } from "../../plants/edit_plant_status"; +import { DIRECTION_CHOICES } from "../../tools/tool_slot_edit_components"; +import { Checkbox } from "../../../ui"; + +/** "All" (any) checkbox to show or choose state of criteria subcategory. */ +const ClearCategory = (props: ClearCategoryProps) => { + const { group, criteriaCategories, criteriaKey, dispatch } = props; + const all = + !criteriaHasKey(group.body.criteria, criteriaCategories, criteriaKey); + return
+ + dispatch(clearCriteriaField(group, criteriaCategories, criteriaKey))} + checked={all} + disabled={all} + title={t("clear selections")} + customDisabledText={t("selections empty")} /> +

{t("all")}

+
; +}; + +/** List of criteria toggle checkboxes. */ +export const CheckboxList = + (props: CheckboxListProps) => { + const { criteria } = props.group.body; + const selected = eqCriteriaSelected(criteria); + const toggle = toggleAndEditEqCriteria; + return
+ {props.list.map(({ label, value }: { label: string, value: T }, index) => +
+ props.dispatch(toggle( + props.group, props.criteriaKey, value, props.pointerType))} + checked={selected(props.criteriaKey, value)} + title={t(label)} + disabled={props.disabled} /> +

{label}

+
)} +
; + }; + +/** Criteria specific to plants. */ +export const PlantCriteria = (props: PlantSubCriteriaProps) => { + const { group, dispatch, disabled } = props; + const commonProps = { group, dispatch, disabled }; + return
+ + +
; +}; + +const PlantStage = (props: SubCriteriaProps) => +
+

{t("Stage")}

+ + + disabled={props.disabled} + pointerType={"Plant"} + criteriaKey={"plant_stage"} + group={props.group} + dispatch={props.dispatch} + list={PLANT_STAGE_LIST().map(ddi => + ({ label: ddi.label, value: "" + ddi.value }))} /> +
; + +const PlantType = (props: PlantSubCriteriaProps) => +
+

{t("Type")}

+ + + disabled={props.disabled} + pointerType={"Plant"} + criteriaKey={"openfarm_slug"} + group={props.group} + dispatch={props.dispatch} + list={uniq(props.slugs + .concat(props.group.body.criteria.string_eq.openfarm_slug || [])) + .map(slug => + ({ label: capitalize(slug).replace("-", " "), value: slug }))} /> +
; + +/** Criteria specific to map points. */ +export const PointCriteria = (props: SubCriteriaProps) => { + const { group, dispatch, disabled } = props; + const commonProps = { group, dispatch, disabled }; + return
+ + + + +
; +}; + +const PointType = (props: SubCriteriaProps) => +
+

{t("Type")}

+ + +
; + +const PointSource = (props: SubCriteriaProps) => +
+

{t("Source")}

+ + +
; + +const Radius = (props: SubCriteriaProps) => +
+

{t("Radius")}

+ +
+ +
+
; + +const Color = (props: SubCriteriaProps) => +
+

{t("Color")}

+ + +
; + +/** Criteria specific to tools. */ +export const ToolCriteria = (props: SubCriteriaProps) => { + const { group, dispatch, disabled } = props; + const commonProps = { group, dispatch, disabled }; + return
+ +
; +}; + +const PulloutDirection = (props: SubCriteriaProps) => +
+

{t("Direction")}

+ + + disabled={props.disabled} + pointerType={"ToolSlot"} + criteriaKey={"pullout_direction"} + group={props.group} + dispatch={props.dispatch} + list={DIRECTION_CHOICES().map(ddi => + ({ label: ddi.label, value: parseInt("" + ddi.value) }))} /> +
; diff --git a/frontend/farm_designer/point_groups/group_detail.tsx b/frontend/farm_designer/point_groups/group_detail.tsx index 6e37ef884..4ea507907 100644 --- a/frontend/farm_designer/point_groups/group_detail.tsx +++ b/frontend/farm_designer/point_groups/group_detail.tsx @@ -24,6 +24,7 @@ interface GroupDetailProps { shouldDisplay: ShouldDisplay; slugs: string[]; hovered: UUID | undefined; + editGroupAreaInMap: boolean; } /** Find a group from a URL-provided ID. */ @@ -35,6 +36,8 @@ export const findGroupFromUrl = (groups: TaggedPointGroup[]) => { }; function mapStateToProps(props: Everything): GroupDetailProps { + const { hoveredPlantListItem, editGroupAreaInMap } = + props.resources.consumers.farm_designer; return { allPoints: selectAllActivePoints(props.resources.index), group: findGroupFromUrl(selectAllPointGroups(props.resources.index)), @@ -42,7 +45,8 @@ 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, + hovered: hoveredPlantListItem, + editGroupAreaInMap, }; } diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index 893df98af..3818cc2a6 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -14,6 +14,7 @@ import { } from "./criteria"; import { Content } from "../../constants"; import { UUID } from "../../resources/interfaces"; +import { Help } from "../../ui"; export interface GroupDetailActiveProps { dispatch: Function; @@ -22,13 +23,17 @@ export interface GroupDetailActiveProps { shouldDisplay: ShouldDisplay; slugs: string[]; hovered: UUID | undefined; + editGroupAreaInMap: boolean; } -type State = { timerId?: ReturnType }; +interface GroupDetailActiveState { + timerId?: ReturnType; + iconDisplay: boolean; +} export class GroupDetailActive - extends React.Component { - state: State = {}; + extends React.Component { + state: GroupDetailActiveState = { iconDisplay: true }; update = ({ currentTarget }: React.SyntheticEvent) => { this.props.dispatch(edit(this.props.group, { name: currentTarget.value })); @@ -76,6 +81,8 @@ export class GroupDetailActive (typeof timerId == "number") && clearInterval(timerId); } + toggleIconShow = () => this.setState({ iconDisplay: !this.state.iconDisplay }); + render() { const { group, dispatch } = this.props; return @@ -86,37 +93,18 @@ export class GroupDetailActive defaultValue={group.body.name} onChange={this.update} onBlur={this.saveGroup} /> -
- - p.body.id))} - pathPoints={this.pointsSelectedByGroup} - dispatch={dispatch} - group={group} /> -

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

-
- - {this.props.shouldDisplay(Feature.criteria_groups) && - } -

{t("Click plants in map to add or remove.")}

- {this.props.shouldDisplay(Feature.criteria_groups) && - this.pointsSelectedByGroup.length != group.body.point_ids.length && -

{t(Content.CRITERIA_SELECTION_COUNT)}

} -
- {this.icons} -
+ + {this.props.shouldDisplay(Feature.criteria_groups) && } + group={group} slugs={this.props.slugs} + editGroupAreaInMap={this.props.editGroupAreaInMap} />} ; } } + +interface GroupSortSelectionProps { + group: TaggedPointGroup; + dispatch: Function; + pointsSelectedByGroup: TaggedPoint[]; +} + +/** Choose and view group point sort method. */ +const GroupSortSelection = (props: GroupSortSelectionProps) => +
+ + {props.group.body.sort_type == "random" && + } + p.body.id))} + pathPoints={props.pointsSelectedByGroup} + dispatch={props.dispatch} + group={props.group} /> +
; + +interface GroupMemberDisplayProps { + group: TaggedPointGroup; + dispatch: Function; + pointsSelectedByGroup: TaggedPoint[]; + shouldDisplay: ShouldDisplay; + icons: JSX.Element[]; + iconDisplay: boolean; + toggleIconShow(): void; +} + +/** View group point counts and icon list. */ +const GroupMemberDisplay = (props: GroupMemberDisplayProps) => +
+ + + + {props.shouldDisplay(Feature.criteria_groups) && + } + {props.iconDisplay && +
+ {props.icons} +
} +
; diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index 4fb5a5c33..0c601afb1 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -23,6 +23,7 @@ export const initialState: DesignerState = { currentPoint: undefined, openedSavedGarden: undefined, tryGroupSortType: undefined, + editGroupAreaInMap: false, }; export const designer = generateReducer(initialState) @@ -89,4 +90,8 @@ export const designer = generateReducer(initialState) .add(Actions.TRY_SORT_TYPE, (s, { payload }) => { s.tryGroupSortType = payload; return s; + }) + .add(Actions.EDIT_GROUP_AREA_IN_MAP, (s, { payload }) => { + s.editGroupAreaInMap = payload; + return s; }); diff --git a/frontend/farm_designer/tools/index.tsx b/frontend/farm_designer/tools/index.tsx index 9b296b8ef..56b0713ed 100644 --- a/frontend/farm_designer/tools/index.tsx +++ b/frontend/farm_designer/tools/index.tsx @@ -77,7 +77,7 @@ export class RawTools extends React.Component {
- +
})} /> props.onChange({ pullout_direction: parseInt("" + ddi.value) })} /> @@ -209,7 +209,7 @@ export const newSlotDirection = export const positionIsDefined = (position: BotPosition): boolean => isNumber(position.x) && isNumber(position.y) && isNumber(position.z); -export const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = { +export const DIRECTION_CHOICES_DDI = (): { [index: number]: DropDownItem } => ({ [ToolPulloutDirection.NONE]: { label: t("None"), value: ToolPulloutDirection.NONE }, [ToolPulloutDirection.POSITIVE_X]: @@ -220,12 +220,12 @@ export const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = { { label: t("Positive Y"), value: ToolPulloutDirection.POSITIVE_Y }, [ToolPulloutDirection.NEGATIVE_Y]: { label: t("Negative Y"), value: ToolPulloutDirection.NEGATIVE_Y }, -}; +}); -export const DIRECTION_CHOICES: DropDownItem[] = [ - DIRECTION_CHOICES_DDI[ToolPulloutDirection.NONE], - DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_X], - DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_X], - DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_Y], - DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_Y], +export const DIRECTION_CHOICES = (): DropDownItem[] => [ + DIRECTION_CHOICES_DDI()[ToolPulloutDirection.NONE], + DIRECTION_CHOICES_DDI()[ToolPulloutDirection.POSITIVE_X], + DIRECTION_CHOICES_DDI()[ToolPulloutDirection.NEGATIVE_X], + DIRECTION_CHOICES_DDI()[ToolPulloutDirection.POSITIVE_Y], + DIRECTION_CHOICES_DDI()[ToolPulloutDirection.NEGATIVE_Y], ]; diff --git a/frontend/farm_designer/zones/edit_zone.tsx b/frontend/farm_designer/zones/edit_zone.tsx index 3ecd18bae..33a1e84a7 100644 --- a/frontend/farm_designer/zones/edit_zone.tsx +++ b/frontend/farm_designer/zones/edit_zone.tsx @@ -53,7 +53,8 @@ export class RawEditZone extends React.Component { + dispatch={this.props.dispatch} + editGroupAreaInMap={true} />
: {t("Redirecting")}...} diff --git a/frontend/logs/components/settings_menu.tsx b/frontend/logs/components/settings_menu.tsx index 38605cd92..aa54657c6 100644 --- a/frontend/logs/components/settings_menu.tsx +++ b/frontend/logs/components/settings_menu.tsx @@ -68,7 +68,7 @@ const LogSetting = (props: LogSettingProps) => { - + { - + proceed() && diff --git a/frontend/sequences/step_icon_group.tsx b/frontend/sequences/step_icon_group.tsx index 47c2cda96..ceec9f33c 100644 --- a/frontend/sequences/step_icon_group.tsx +++ b/frontend/sequences/step_icon_group.tsx @@ -41,6 +41,6 @@ export function StepIconGroup(props: StepIconBarProps) { - + ; } diff --git a/frontend/ui/checkbox.tsx b/frontend/ui/checkbox.tsx new file mode 100644 index 000000000..70c146d92 --- /dev/null +++ b/frontend/ui/checkbox.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { t } from "../i18next_wrapper"; + +interface CheckboxProps { + onChange(): void; + checked: boolean; + title: string; + disabled?: boolean; + partial?: boolean; + onClick?: (e: React.FormEvent) => void; + customDisabledText?: string; +} + +export const Checkbox = (props: CheckboxProps) => +
+ +
; diff --git a/frontend/ui/help.tsx b/frontend/ui/help.tsx index 68aba52fa..d697c4529 100644 --- a/frontend/ui/help.tsx +++ b/frontend/ui/help.tsx @@ -1,19 +1,22 @@ import * as React from "react"; -import { Popover, PopoverInteractionKind, PopoverPosition } from "@blueprintjs/core"; +import { + Popover, PopoverInteractionKind, PopoverPosition, Position, +} from "@blueprintjs/core"; import { t } from "../i18next_wrapper"; interface HelpProps { text: string; - requireClick?: boolean; + onHover?: boolean; position?: PopoverPosition; customIcon?: string; } export function Help(props: HelpProps) { return
{t(props.text)}
diff --git a/frontend/ui/index.ts b/frontend/ui/index.ts index 208d4f6d6..f7d19f4af 100644 --- a/frontend/ui/index.ts +++ b/frontend/ui/index.ts @@ -1,6 +1,7 @@ export * from "./back_arrow"; export * from "./blurable_input"; export * from "./center_panel"; +export * from "./checkbox"; export * from "./color_picker"; export * from "./colors"; export * from "./column";