diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index efcf2ba49..8bf3e0c1a 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -19,4 +19,5 @@ export const fakeDesignerState = (): DesignerState => ({ openedSavedGarden: undefined, tryGroupSortType: undefined, editGroupAreaInMap: false, + settingsSearchTerm: "", }); diff --git a/frontend/constants.ts b/frontend/constants.ts index 4153ee19e..e778b7e00 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -923,6 +923,8 @@ export namespace TourContent { } export enum DeviceSetting { + axisHeadingLabels = ``, + // Homing and calibration homingAndCalibration = `Homing and Calibration`, homing = `Homing`, @@ -974,6 +976,11 @@ export enum DeviceSetting { // Pin Guard pinGuard = `Pin Guard`, + pinGuard1 = `Pin Guard 1`, + pinGuard2 = `Pin Guard 2`, + pinGuard3 = `Pin Guard 3`, + pinGuard4 = `Pin Guard 4`, + pinGuard5 = `Pin Guard 5`, // Danger Zone dangerZone = `Danger Zone`, @@ -981,6 +988,8 @@ export enum DeviceSetting { // Pin Bindings pinBindings = `Pin Bindings`, + savedPinBindings = `Saved pin bindings`, + addNewPinBinding = `Add new pin binding`, // FarmBot OS farmbot = `FarmBot`, @@ -1131,6 +1140,7 @@ export enum Actions { SET_DRAWN_WEED_DATA = "SET_DRAWN_WEED_DATA", CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN", TRY_SORT_TYPE = "TRY_SORT_TYPE", + SET_SETTINGS_SEARCH_TERM = "SET_SETTINGS_SEARCH_TERM", EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP", // Regimens diff --git a/frontend/css/colors.scss b/frontend/css/colors.scss index 2f84b9834..e51dc839e 100644 --- a/frontend/css/colors.scss +++ b/frontend/css/colors.scss @@ -38,6 +38,7 @@ $pink: #ebb; $light_red: #e99; $red: #e66; $dark_red: #f00; +$medium_dark_red: #c00; $darkest_red: #900; $panel_green: #35761b; $panel_light_green: #f3f9f1; diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index 4bfb16e78..43e9e87a8 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -158,6 +158,17 @@ left: 1rem; cursor: default !important; } + .fa-times { + position: absolute; + bottom: 0; + right: 0; + padding: 0.5rem; + color: $darkest_red; + font-size: 1.3rem; + &:hover { + color: $medium_dark_red; + } + } } input { background: transparent; diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index 447bf5174..b83256ca3 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -773,19 +773,101 @@ } } +.no-pad { + padding: 0; +} + .settings-panel-content { - max-height: calc(100vh - 15rem); - overflow-y: auto; - overflow-x: hidden; - margin-top: 5rem; + padding: 0; + margin-top: 6rem; padding-bottom: 5rem; - button { - margin-top: 1.75rem; + .section { + margin-bottom: 2rem; } - p { - padding: 0.5rem; + .bulk-expand-controls { margin-left: 1rem; + } + .row:first-child { + margin-right: 0; + margin-top: 1rem; + } + .row:nth-child(2) { + padding-left: 1.5rem; + padding-right: 3rem; + } + .label-headings { + margin-right: 2rem; + label { + line-height: 1rem; + } + } + .release-notes-wrapper { + float: right !important; + } + .network-not-found-timer { + margin-top: 1rem; + } + .pin-guard-input-row { + .row { + margin-left: -15px; + margin-right: -15px; + padding-left: 0; + padding-right: 1rem; + margin-bottom: 1rem; + } + } + .pin-bindings { margin-right: 1rem; + .row { + padding-left: 0; + padding-right: 0; + margin-left: 1rem; + margin-right: 0; + margin-top: 1rem; + } + div[class*=col-] { + padding: 0; + padding-right: 1rem; + } + .bindings-list { + margin-left: -5px; + .binding-action { + font-weight: bold; + font-size: 1.2rem; + } + } + .pin-binding-input-rows { + margin-right: 1rem; + margin-left: -15px; + label { + margin-left: 1rem !important; + } + .green { + float: left; + margin-left: 1rem; + } + .row:last-child { + margin-top: 0; + } + } + .stock-pin-bindings-button { + display: inline; + button { + margin: 0; + margin-top: 0.5rem; + } + } + } + .fb-button { + margin-top: 0.5rem; + } + label { + margin: 0 !important; + line-height: 3rem; + } + .bp3-popover-wrapper { + display: inline; + float: none; } .map-size-inputs { .row { @@ -795,6 +877,31 @@ margin-top: 0.5rem; } } + .help-icon { + margin-left: 1rem; + } + .all-settings-content { + max-height: calc(100vh - 22rem); + overflow-y: auto; + overflow-x: hidden; + margin-top: 1rem; + padding-left: 1rem; + .expandable-header { + margin-top: 1.5rem; + margin-bottom: 0; + } + .section { + margin-bottom: 0; + } + } + .designer-settings { + max-height: calc(100vh - 14rem); + overflow-y: auto; + overflow-x: hidden; + margin-right: -10px; + padding-right: 1rem; + padding-left: 1rem; + } .designer-setting { &.disabled { input { diff --git a/frontend/devices/components/__tests__/maybe_highlight_test.tsx b/frontend/devices/components/__tests__/maybe_highlight_test.tsx index 2bf800e6a..b8243d011 100644 --- a/frontend/devices/components/__tests__/maybe_highlight_test.tsx +++ b/frontend/devices/components/__tests__/maybe_highlight_test.tsx @@ -1,5 +1,12 @@ jest.mock("../../actions", () => ({ toggleControlPanel: jest.fn(), + bulkToggleControlPanel: jest.fn(), +})); + +import { fakeState } from "../../../__test_support__/fake_state"; +const mockState = fakeState(); +jest.mock("../../../redux/store", () => ({ + store: { getState: () => mockState }, })); import * as React from "react"; @@ -9,7 +16,7 @@ import { } from "../maybe_highlight"; import { DeviceSetting } from "../../../constants"; import { panelState } from "../../../__test_support__/control_panel_state"; -import { toggleControlPanel } from "../../actions"; +import { toggleControlPanel, bulkToggleControlPanel } from "../../actions"; describe("", () => { const fakeProps = (): HighlightProps => ({ @@ -25,6 +32,24 @@ describe("", () => { wrapper.instance().componentDidMount(); expect(wrapper.state().className).toEqual("unhighlight"); }); + + it("doesn't hide: no search term", () => { + mockState.resources.consumers.farm_designer.settingsSearchTerm = ""; + const wrapper = mount(); + expect(wrapper.find("div").first().props().hidden).toEqual(false); + }); + + it("doesn't hide: matches search term", () => { + mockState.resources.consumers.farm_designer.settingsSearchTerm = "motor"; + const wrapper = mount(); + expect(wrapper.find("div").first().props().hidden).toEqual(false); + }); + + it("hides", () => { + mockState.resources.consumers.farm_designer.settingsSearchTerm = "encoder"; + const wrapper = mount(); + expect(wrapper.find("div").first().props().hidden).toEqual(true); + }); }); describe("maybeHighlight()", () => { @@ -78,4 +103,11 @@ describe("maybeOpenPanel()", () => { maybeOpenPanel(panelState())(jest.fn()); expect(toggleControlPanel).not.toHaveBeenCalled(); }); + + it("closes other panels", () => { + location.search = "?highlight=motors"; + maybeOpenPanel(panelState(), true)(jest.fn()); + expect(toggleControlPanel).toHaveBeenCalledWith("motors"); + expect(bulkToggleControlPanel).toHaveBeenCalledWith(false, true); + }); }); diff --git a/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx b/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx index bfdce94ea..9142c1215 100644 --- a/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx +++ b/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx @@ -9,11 +9,12 @@ import { settingToggle } from "../../actions"; import { buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; +import { DeviceSetting } from "../../../constants"; describe("", () => { const fakeProps = (): PinGuardMCUInputGroupProps => { return { - label: "Pin Guard 1", + label: DeviceSetting.pinGuard1, pinNumKey: "pin_guard_1_pin_nr", timeoutKey: "pin_guard_1_time_out", activeStateKey: "pin_guard_1_active_state", diff --git a/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx b/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx index d4bdc6bda..1d83e9860 100644 --- a/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx +++ b/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx @@ -13,11 +13,12 @@ import { import { TaggedFirmwareConfig } from "farmbot"; import { FBSelect } from "../../../ui"; import { updateMCU } from "../../actions"; +import { DeviceSetting } from "../../../constants"; describe("", () => { const fakeProps = (firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({ - label: "Pin Guard 1", + label: DeviceSetting.pinGuard1, pinNumKey: "pin_guard_1_pin_nr", timeoutKey: "pin_guard_1_time_out", activeStateKey: "pin_guard_1_active_state", diff --git a/frontend/devices/components/hardware_settings/danger_zone.tsx b/frontend/devices/components/hardware_settings/danger_zone.tsx index 140e08801..afe3b30a2 100644 --- a/frontend/devices/components/hardware_settings/danger_zone.tsx +++ b/frontend/devices/components/hardware_settings/danger_zone.tsx @@ -24,7 +24,7 @@ export function DangerZone(props: DangerZoneProps) { - } - - - - - - - - - - - + + + + + + + + + + + + + ; } diff --git a/frontend/devices/components/interfaces.ts b/frontend/devices/components/interfaces.ts index 823e141f7..404d476bd 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -65,7 +65,7 @@ export interface NumericMCUInputGroupProps { export interface PinGuardMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; - label: string; + label: DeviceSetting; pinNumKey: McuParamName; timeoutKey: McuParamName; activeStateKey: McuParamName; diff --git a/frontend/devices/components/maybe_highlight.tsx b/frontend/devices/components/maybe_highlight.tsx index 3a1dd10e0..87401be00 100644 --- a/frontend/devices/components/maybe_highlight.tsx +++ b/frontend/devices/components/maybe_highlight.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { store } from "../../redux/store"; import { ControlPanelState } from "../interfaces"; import { toggleControlPanel, bulkToggleControlPanel } from "../actions"; import { urlFriendly } from "../../util"; @@ -56,6 +57,11 @@ const ERROR_HANDLING_PANEL = [ ]; const PIN_GUARD_PANEL = [ DeviceSetting.pinGuard, + DeviceSetting.pinGuard1, + DeviceSetting.pinGuard2, + DeviceSetting.pinGuard3, + DeviceSetting.pinGuard4, + DeviceSetting.pinGuard5, ]; const DANGER_ZONE_PANEL = [ DeviceSetting.dangerZone, @@ -63,6 +69,8 @@ const DANGER_ZONE_PANEL = [ ]; const PIN_BINDINGS_PANEL = [ DeviceSetting.pinBindings, + DeviceSetting.savedPinBindings, + DeviceSetting.addNewPinBinding, ]; const POWER_AND_RESET_PANEL = [ DeviceSetting.powerAndReset, @@ -183,6 +191,7 @@ export interface HighlightProps { | (React.ReactChild | false)[] | (React.ReactChild | React.ReactChild[])[]; className?: string; + searchTerm?: string; } interface HighlightState { @@ -200,11 +209,19 @@ export class Highlight extends React.Component { } } + get searchTerm() { + const { resources } = store.getState(); + return resources.consumers.farm_designer.settingsSearchTerm; + } + render() { + const show = !this.searchTerm || + this.props.settingName.toLowerCase().includes(this.searchTerm); return
+ ].join(" ")} + hidden={!show}> {this.props.children}
; } diff --git a/frontend/devices/components/pin_guard_input_group.tsx b/frontend/devices/components/pin_guard_input_group.tsx index ff21796f4..b6c34fed9 100644 --- a/frontend/devices/components/pin_guard_input_group.tsx +++ b/frontend/devices/components/pin_guard_input_group.tsx @@ -10,6 +10,7 @@ import { PinNumberDropdown } from "./pin_number_dropdown"; import { DevSettings } from "../../account/dev/dev_support"; import { ToolTips } from "../../constants"; import { Position } from "@blueprintjs/core"; +import { Highlight } from "./maybe_highlight"; export class PinGuardMCUInputGroup extends React.Component { @@ -50,7 +51,7 @@ export class PinGuardMCUInputGroup ? @@ -63,46 +64,48 @@ export class PinGuardMCUInputGroup - :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
; + : +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
; } } diff --git a/frontend/devices/pin_bindings/pin_binding_input_group.tsx b/frontend/devices/pin_bindings/pin_binding_input_group.tsx index 179f4c6ef..c7aeaabb7 100644 --- a/frontend/devices/pin_bindings/pin_binding_input_group.tsx +++ b/frontend/devices/pin_bindings/pin_binding_input_group.tsx @@ -25,6 +25,7 @@ import { } from "farmbot/dist/resources/api_resources"; import { t } from "../../i18next_wrapper"; import { DevSettings } from "../../account/dev/dev_support"; +import { DeviceSetting } from "../../constants"; export class PinBindingInputGroup extends React.Component { @@ -129,7 +130,7 @@ export class PinBindingInputGroup render() { const newFormat = DevSettings.futureFeaturesEnabled(); return
- {newFormat && } + {newFormat && } {newFormat && } {newFormat && diff --git a/frontend/devices/pin_bindings/pin_bindings.tsx b/frontend/devices/pin_bindings/pin_bindings.tsx index 1dbc4c6d5..2cf1c9e8f 100644 --- a/frontend/devices/pin_bindings/pin_bindings.tsx +++ b/frontend/devices/pin_bindings/pin_bindings.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { Row, Col, Help } from "../../ui"; -import { ToolTips } from "../../constants"; +import { ToolTips, DeviceSetting } from "../../constants"; import { selectAllPinBindings } from "../../resources/selectors"; import { PinBindingsContentProps, PinBindingListItems } from "./interfaces"; import { PinBindingsList } from "./pin_bindings_list"; @@ -17,6 +17,7 @@ import { } from "farmbot/dist/resources/api_resources"; import { t } from "../../i18next_wrapper"; import { DevSettings } from "../../account/dev/dev_support"; +import { Highlight } from "../components/maybe_highlight"; /** Width of UI columns in Pin Bindings widget. */ export enum PinBindingColWidth { @@ -73,32 +74,38 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => { const pinBindings = apiPinBindings(resources); const newFormat = DevSettings.futureFeaturesEnabled(); return
- - {newFormat && } - - - -
- {t(ToolTips.PIN_BINDING_WARNING)} -
-
-
+ + + {newFormat && } + + + +
+ {t(ToolTips.PIN_BINDING_WARNING)} +
+
+
+
{!newFormat && } - - + + + + + +
; }; diff --git a/frontend/devices/pin_bindings/pin_bindings_list.tsx b/frontend/devices/pin_bindings/pin_bindings_list.tsx index 6c5cdf91e..55ccbe341 100644 --- a/frontend/devices/pin_bindings/pin_bindings_list.tsx +++ b/frontend/devices/pin_bindings/pin_bindings_list.tsx @@ -15,6 +15,7 @@ import { DevSettings } from "../../account/dev/dev_support"; import { PinBindingType, PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; +import { DeviceSetting } from "../../constants"; export const PinBindingsList = (props: PinBindingsListProps) => { const { pinBindings, resources, dispatch } = props; @@ -41,7 +42,7 @@ export const PinBindingsList = (props: PinBindingsListProps) => { const newFormat = DevSettings.futureFeaturesEnabled(); return
- {newFormat && } + {newFormat && } {pinBindings .sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number)) .map(x => { diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index 7b733efeb..595de4d80 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -191,6 +191,16 @@ describe("designer reducer", () => { expect(newState.tryGroupSortType).toEqual("random"); }); + it("sets settings search term", () => { + const state = oldState(); + state.settingsSearchTerm = ""; + const action: ReduxAction = { + type: Actions.SET_SETTINGS_SEARCH_TERM, payload: "random" + }; + const newState = designer(state, action); + expect(newState.settingsSearchTerm).toEqual("random"); + }); + it("enables edit group area in map mode", () => { const state = oldState(); state.editGroupAreaInMap = false; diff --git a/frontend/farm_designer/__tests__/settings_test.tsx b/frontend/farm_designer/__tests__/settings_test.tsx deleted file mode 100644 index 82deb77fb..000000000 --- a/frontend/farm_designer/__tests__/settings_test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -jest.mock("../../config_storage/actions", () => ({ - getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }), - setWebAppConfigValue: jest.fn(), -})); - -import * as React from "react"; -import { mount, ReactWrapper } from "enzyme"; -import { - RawDesignerSettings as DesignerSettings, DesignerSettingsProps, - mapStateToProps, -} from "../settings"; -import { fakeState } from "../../__test_support__/fake_state"; -import { BooleanSetting, NumericSetting } from "../../session_keys"; -import { setWebAppConfigValue } from "../../config_storage/actions"; - -const getSetting = - (wrapper: ReactWrapper, position: number, containsString: string) => { - const setting = wrapper.find(".designer-setting").at(position); - expect(setting.text().toLowerCase()) - .toContain(containsString.toLowerCase()); - return setting; - }; - -describe("", () => { - const fakeProps = (): DesignerSettingsProps => ({ - dispatch: jest.fn(), - getConfigValue: jest.fn(), - }); - - it("renders settings", () => { - const wrapper = mount(); - expect(wrapper.text()).toContain("size"); - const settings = wrapper.find(".designer-setting"); - expect(settings.length).toEqual(7); - }); - - it("renders defaultOn setting", () => { - const p = fakeProps(); - p.getConfigValue = () => undefined; - const wrapper = mount(); - const confirmDeletion = getSetting(wrapper, 6, "confirm plant"); - expect(confirmDeletion.find("button").text()).toEqual("on"); - }); - - it("toggles setting", () => { - const wrapper = mount(); - const trailSetting = getSetting(wrapper, 1, "trail"); - trailSetting.find("button").simulate("click"); - expect(setWebAppConfigValue) - .toHaveBeenCalledWith(BooleanSetting.display_trail, true); - }); - - it("changes origin", () => { - const p = fakeProps(); - p.getConfigValue = () => 2; - const wrapper = mount(); - const originSetting = getSetting(wrapper, 5, "origin"); - originSetting.find("div").last().simulate("click"); - expect(setWebAppConfigValue).toHaveBeenCalledWith( - NumericSetting.bot_origin_quadrant, 4); - }); -}); - -describe("mapStateToProps()", () => { - it("returns props", () => { - const props = mapStateToProps(fakeState()); - const value = props.getConfigValue(BooleanSetting.show_plants); - expect(value).toEqual(true); - }); -}); diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index 6ebf62f9f..6543c7e10 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -125,6 +125,7 @@ export interface DesignerState { openedSavedGarden: string | undefined; tryGroupSortType: PointGroupSortType | "nn" | undefined; editGroupAreaInMap: boolean; + settingsSearchTerm: string; } export type TaggedExecutable = TaggedSequence | TaggedRegimen; diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index 81909dee2..02f8a8378 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -27,6 +27,7 @@ export const initialState: DesignerState = { openedSavedGarden: undefined, tryGroupSortType: undefined, editGroupAreaInMap: false, + settingsSearchTerm: "", }; export const designer = generateReducer(initialState) @@ -107,6 +108,10 @@ export const designer = generateReducer(initialState) s.tryGroupSortType = payload; return s; }) + .add(Actions.SET_SETTINGS_SEARCH_TERM, (s, { payload }) => { + s.settingsSearchTerm = payload; + return s; + }) .add(Actions.EDIT_GROUP_AREA_IN_MAP, (s, { payload }) => { s.editGroupAreaInMap = payload; return s; diff --git a/frontend/farm_designer/settings.tsx b/frontend/farm_designer/settings.tsx deleted file mode 100644 index f35516362..000000000 --- a/frontend/farm_designer/settings.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import * as React from "react"; -import { Everything } from "../interfaces"; -import { connect } from "react-redux"; -import { Content } from "../constants"; -import { DesignerPanel, DesignerPanelContent } from "./designer_panel"; -import { t } from "../i18next_wrapper"; -import { - GetWebAppConfigValue, getWebAppConfigValue, setWebAppConfigValue, -} from "../config_storage/actions"; -import { Row, Col } from "../ui"; -import { ToggleButton } from "../controls/toggle_button"; -import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; -import { BooleanSetting, NumericSetting } from "../session_keys"; -import { resetVirtualTrail } from "./map/layers/farmbot/bot_trail"; -import { MapSizeInputs } from "./map_size_setting"; -import { DesignerNavTabs, Panel } from "./panel_header"; -import { isUndefined } from "lodash"; - -export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({ - dispatch: props.dispatch, - getConfigValue: getWebAppConfigValue(() => props), -}); - -export interface DesignerSettingsProps { - dispatch: Function; - getConfigValue: GetWebAppConfigValue; -} - -export class RawDesignerSettings - extends React.Component { - - render() { - const { getConfigValue, dispatch } = this.props; - const settingsProps = { getConfigValue, dispatch }; - return - - - {DESIGNER_SETTINGS(settingsProps).map(setting => - )} - - ; - } -} - -interface SettingDescriptionProps { - setting?: BooleanConfigKey; - title: string; - description: string; - invert?: boolean; - callback?: () => void; - children?: React.ReactChild; - defaultOn?: boolean; - disabled?: boolean; -} - -interface SettingProps - extends DesignerSettingsProps, SettingDescriptionProps { } - -const Setting = (props: SettingProps) => { - const { title, setting, callback, defaultOn } = props; - const raw_value = setting ? props.getConfigValue(setting) : undefined; - const value = (defaultOn && isUndefined(raw_value)) ? true : !!raw_value; - return
- - - - - - {setting && { - props.dispatch(setWebAppConfigValue(setting, !value)); - callback?.(); - }} - title={`${t("toggle")} ${title}`} - customText={{ textFalse: t("off"), textTrue: t("on") }} />} - - - -

{t(props.description)}

-
- {props.children} -
; -}; - -const DESIGNER_SETTINGS = - (settingsProps: DesignerSettingsProps): SettingDescriptionProps[] => ([ - { - title: t("Display plant animations"), - description: t(Content.PLANT_ANIMATIONS), - setting: BooleanSetting.disable_animations, - invert: true - }, - { - title: t("Display virtual FarmBot trail"), - description: t(Content.VIRTUAL_TRAIL), - setting: BooleanSetting.display_trail, - callback: resetVirtualTrail, - }, - { - title: t("Dynamic map size"), - description: t(Content.DYNAMIC_MAP_SIZE), - setting: BooleanSetting.dynamic_map, - }, - { - title: t("Map size"), - description: t(Content.MAP_SIZE), - children: , - disabled: !!settingsProps.getConfigValue(BooleanSetting.dynamic_map), - }, - { - title: t("Rotate map"), - description: t(Content.MAP_SWAP_XY), - setting: BooleanSetting.xy_swap, - }, - { - title: t("Map origin"), - description: t(Content.MAP_ORIGIN), - children: - }, - { - title: t("Confirm plant deletion"), - description: t(Content.CONFIRM_PLANT_DELETION), - setting: BooleanSetting.confirm_plant_deletion, - defaultOn: true, - }, - ]); - -const OriginSelector = (props: DesignerSettingsProps) => { - const quadrant = props.getConfigValue(NumericSetting.bot_origin_quadrant); - const update = (value: number) => () => props.dispatch(setWebAppConfigValue( - NumericSetting.bot_origin_quadrant, value)); - return
-
- {[2, 1, 3, 4].map(q => -
)} -
-
; -}; - -export const DesignerSettings = connect(mapStateToProps)(RawDesignerSettings); diff --git a/frontend/farm_designer/settings/__tests__/farm_designer_settings_test.tsx b/frontend/farm_designer/settings/__tests__/farm_designer_settings_test.tsx new file mode 100644 index 000000000..15d2073ca --- /dev/null +++ b/frontend/farm_designer/settings/__tests__/farm_designer_settings_test.tsx @@ -0,0 +1,35 @@ +jest.mock("../../map/layers/farmbot/bot_trail", () => ({ + resetVirtualTrail: jest.fn(), +})); + +import * as React from "react"; +import { mount } from "enzyme"; +import { PlainDesignerSettings } from "../farm_designer_settings"; +import { DesignerSettingsPropsBase } from "../interfaces"; +import { resetVirtualTrail } from "../../map/layers/farmbot/bot_trail"; + +describe("", () => { + const fakeProps = (): DesignerSettingsPropsBase => ({ + dispatch: jest.fn(), + getConfigValue: jest.fn(), + }); + + it("renders", () => { + const wrapper = mount(
{PlainDesignerSettings(fakeProps())}
); + expect(wrapper.text().toLowerCase()).toContain("plant animations"); + }); + + it("doesn't call callback", () => { + const wrapper = mount(
{PlainDesignerSettings(fakeProps())}
); + expect(wrapper.find("label").at(0).text()).toContain("animations"); + wrapper.find("button").at(0).simulate("click"); + expect(resetVirtualTrail).not.toHaveBeenCalled(); + }); + + it("calls callback", () => { + const wrapper = mount(
{PlainDesignerSettings(fakeProps())}
); + expect(wrapper.find("label").at(1).text()).toContain("trail"); + wrapper.find("button").at(1).simulate("click"); + expect(resetVirtualTrail).toHaveBeenCalled(); + }); +}); diff --git a/frontend/farm_designer/settings/__tests__/index_test.tsx b/frontend/farm_designer/settings/__tests__/index_test.tsx new file mode 100644 index 000000000..334653eec --- /dev/null +++ b/frontend/farm_designer/settings/__tests__/index_test.tsx @@ -0,0 +1,172 @@ +jest.mock("../../../config_storage/actions", () => ({ + getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }), + setWebAppConfigValue: jest.fn(), +})); + +let mockDev = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + overriddenFbosVersion: jest.fn(), + } +})); + +jest.mock("../../../devices/components/maybe_highlight", () => ({ + maybeOpenPanel: jest.fn(), + Highlight: (p: { children: React.ReactChild }) =>
{p.children}
, +})); + +import * as React from "react"; +import { mount, ReactWrapper, shallow } from "enzyme"; +import { RawDesignerSettings as DesignerSettings } from ".."; +import { DesignerSettingsProps } from "../interfaces"; +import { BooleanSetting, NumericSetting } from "../../../session_keys"; +import { setWebAppConfigValue } from "../../../config_storage/actions"; +import { + buildResourceIndex, fakeDevice, +} from "../../../__test_support__/resource_index_builder"; +import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; +import { bot } from "../../../__test_support__/fake_state/bot"; +import { clickButton } from "../../../__test_support__/helpers"; +import { Actions } from "../../../constants"; +import { Motors } from "../hardware_settings"; +import { SearchField } from "../../../ui/search_field"; +import { maybeOpenPanel } from "../../../devices/components/maybe_highlight"; + +const getSetting = + (wrapper: ReactWrapper, position: number, containsString: string) => { + const setting = wrapper.find(".designer-setting").at(position); + expect(setting.text().toLowerCase()) + .toContain(containsString.toLowerCase()); + return setting; + }; + +describe("", () => { + beforeEach(() => { + mockDev = false; + }); + + const fakeProps = (): DesignerSettingsProps => ({ + dispatch: jest.fn(), + getConfigValue: jest.fn(), + firmwareConfig: undefined, + sourceFwConfig: () => ({ value: 10, consistent: true }), + sourceFbosConfig: () => ({ value: 10, consistent: true }), + resources: buildResourceIndex().index, + deviceAccount: fakeDevice(), + env: {}, + alerts: [], + shouldDisplay: jest.fn(), + saveFarmwareEnv: jest.fn(), + timeSettings: fakeTimeSettings(), + bot: bot, + searchTerm: "", + }); + + it("renders settings", () => { + const wrapper = mount(); + expect(wrapper.text()).toContain("size"); + expect(wrapper.text().toLowerCase()).not.toContain("pin"); + const settings = wrapper.find(".designer-setting"); + expect(settings.length).toEqual(7); + }); + + it("renders all settings", () => { + mockDev = true; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("pin"); + }); + + it("mounts", () => { + mount(); + expect(maybeOpenPanel).toHaveBeenCalled(); + }); + + it("unmounts", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.unmount(); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.BULK_TOGGLE_CONTROL_PANEL, + payload: { open: false, all: true }, + }); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_CONTROL_PANEL_OPTION, + payload: "farmbot_os", + }); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_SETTINGS_SEARCH_TERM, + payload: "", + }); + }); + + it("changes search term", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(SearchField).simulate("change", "setting"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.BULK_TOGGLE_CONTROL_PANEL, + payload: { open: true, all: true }, + }); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_SETTINGS_SEARCH_TERM, + payload: "setting", + }); + }); + + it("fetches firmware_hardware", () => { + mockDev = true; + const p = fakeProps(); + p.sourceFbosConfig = () => ({ value: "arduino", consistent: true }); + const wrapper = mount(); + expect(wrapper.find(Motors).props().firmwareHardware).toEqual("arduino"); + }); + + it("expands all", () => { + mockDev = true; + const p = fakeProps(); + const wrapper = mount(); + clickButton(wrapper, 0, "expand all"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.BULK_TOGGLE_CONTROL_PANEL, + payload: { open: true, all: true }, + }); + }); + + it("collapses all", () => { + mockDev = true; + const p = fakeProps(); + const wrapper = mount(); + clickButton(wrapper, 1, "collapse all"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.BULK_TOGGLE_CONTROL_PANEL, + payload: { open: false, all: true }, + }); + }); + + it("renders defaultOn setting", () => { + const p = fakeProps(); + p.getConfigValue = () => undefined; + const wrapper = mount(); + const confirmDeletion = getSetting(wrapper, 6, "confirm plant"); + expect(confirmDeletion.find("button").text()).toEqual("on"); + }); + + it("toggles setting", () => { + const wrapper = mount(); + const trailSetting = getSetting(wrapper, 1, "trail"); + trailSetting.find("button").simulate("click"); + expect(setWebAppConfigValue) + .toHaveBeenCalledWith(BooleanSetting.display_trail, true); + }); + + it("changes origin", () => { + const p = fakeProps(); + p.getConfigValue = () => 2; + const wrapper = mount(); + const originSetting = getSetting(wrapper, 5, "origin"); + originSetting.find("div").last().simulate("click"); + expect(setWebAppConfigValue).toHaveBeenCalledWith( + NumericSetting.bot_origin_quadrant, 4); + }); +}); diff --git a/frontend/farm_designer/settings/__tests__/state_to_props_test.ts b/frontend/farm_designer/settings/__tests__/state_to_props_test.ts new file mode 100644 index 000000000..9bc5e920a --- /dev/null +++ b/frontend/farm_designer/settings/__tests__/state_to_props_test.ts @@ -0,0 +1,16 @@ +jest.mock("../../../config_storage/actions", () => ({ + getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }), + setWebAppConfigValue: jest.fn(), +})); + +import { mapStateToProps } from "../state_to_props"; +import { fakeState } from "../../../__test_support__/fake_state"; +import { BooleanSetting } from "../../../session_keys"; + +describe("mapStateToProps()", () => { + it("returns props", () => { + const props = mapStateToProps(fakeState()); + const value = props.getConfigValue(BooleanSetting.show_plants); + expect(value).toEqual(true); + }); +}); diff --git a/frontend/farm_designer/settings/farm_designer_settings.tsx b/frontend/farm_designer/settings/farm_designer_settings.tsx new file mode 100644 index 000000000..c050b793d --- /dev/null +++ b/frontend/farm_designer/settings/farm_designer_settings.tsx @@ -0,0 +1,125 @@ +import * as React from "react"; +import { Content, DeviceSetting } from "../../constants"; +import { t } from "../../i18next_wrapper"; +import { setWebAppConfigValue } from "../../config_storage/actions"; +import { Row, Col } from "../../ui"; +import { ToggleButton } from "../../controls/toggle_button"; +import { BooleanSetting, NumericSetting } from "../../session_keys"; +import { resetVirtualTrail } from "../map/layers/farmbot/bot_trail"; +import { MapSizeInputs } from "../map_size_setting"; +import { isUndefined } from "lodash"; +import { Collapse } from "@blueprintjs/core"; +import { Header } from "../../devices/components/hardware_settings/header"; +import { Highlight } from "../../devices/components/maybe_highlight"; +import { + DesignerSettingsSectionProps, SettingProps, + DesignerSettingsPropsBase, SettingDescriptionProps, +} from "./interfaces"; + +export const Designer = (props: DesignerSettingsSectionProps) => { + const { getConfigValue, dispatch, controlPanelState } = props; + const settingsProps = { getConfigValue, dispatch }; + return +
+ + {PlainDesignerSettings(settingsProps)} + + ; +}; + +export const PlainDesignerSettings = + (settingsProps: DesignerSettingsPropsBase) => + DESIGNER_SETTINGS(settingsProps).map(setting => + ); + +const Setting = (props: SettingProps) => { + const { title, setting, callback, defaultOn } = props; + const raw_value = setting ? props.getConfigValue(setting) : undefined; + const value = (defaultOn && isUndefined(raw_value)) ? true : !!raw_value; + return +
+ + + + + + {setting && { + props.dispatch(setWebAppConfigValue(setting, !value)); + callback?.(); + }} + title={`${t("toggle")} ${title}`} + customText={{ textFalse: t("off"), textTrue: t("on") }} />} + + + +

{t(props.description)}

+
+ {props.children} +
+
; +}; + +const DESIGNER_SETTINGS = + (settingsProps: DesignerSettingsPropsBase): SettingDescriptionProps[] => ([ + { + title: DeviceSetting.animations, + description: t(Content.PLANT_ANIMATIONS), + setting: BooleanSetting.disable_animations, + invert: true + }, + { + title: DeviceSetting.trail, + description: t(Content.VIRTUAL_TRAIL), + setting: BooleanSetting.display_trail, + callback: resetVirtualTrail, + }, + { + title: DeviceSetting.dynamicMap, + description: t(Content.DYNAMIC_MAP_SIZE), + setting: BooleanSetting.dynamic_map, + }, + { + title: DeviceSetting.mapSize, + description: t(Content.MAP_SIZE), + children: , + disabled: !!settingsProps.getConfigValue(BooleanSetting.dynamic_map), + }, + { + title: DeviceSetting.rotateMap, + description: t(Content.MAP_SWAP_XY), + setting: BooleanSetting.xy_swap, + }, + { + title: DeviceSetting.mapOrigin, + description: t(Content.MAP_ORIGIN), + children: + }, + { + title: DeviceSetting.confirmPlantDeletion, + description: t(Content.CONFIRM_PLANT_DELETION), + setting: BooleanSetting.confirm_plant_deletion, + defaultOn: true, + }, + ]); + +const OriginSelector = (props: DesignerSettingsPropsBase) => { + const quadrant = props.getConfigValue(NumericSetting.bot_origin_quadrant); + const update = (value: number) => () => props.dispatch(setWebAppConfigValue( + NumericSetting.bot_origin_quadrant, value)); + return
+
+ {[2, 1, 3, 4].map(q => +
)} +
+
; +}; diff --git a/frontend/farm_designer/settings/fbos_settings.ts b/frontend/farm_designer/settings/fbos_settings.ts new file mode 100644 index 000000000..65152600c --- /dev/null +++ b/frontend/farm_designer/settings/fbos_settings.ts @@ -0,0 +1,3 @@ +export * from "../../devices/components/fbos_settings/power_and_reset"; +export * from "../../devices/components/fbos_settings/firmware"; +export * from "../../devices/components/farmbot_os_settings"; diff --git a/frontend/farm_designer/settings/hardware_settings.ts b/frontend/farm_designer/settings/hardware_settings.ts new file mode 100644 index 000000000..deb7a46e7 --- /dev/null +++ b/frontend/farm_designer/settings/hardware_settings.ts @@ -0,0 +1,9 @@ +export * from "../../devices/components/hardware_settings/homing_and_calibration"; +export * from "../../devices/components/hardware_settings/motors"; +export * from "../../devices/components/hardware_settings/encoders"; +export * from "../../devices/components/hardware_settings/endstops"; +export * from "../../devices/components/hardware_settings/error_handling"; +export * from "../../devices/components/hardware_settings/pin_bindings"; +export * from "../../devices/components/hardware_settings/pin_guard"; +export * from "../../devices/components/hardware_settings/danger_zone"; +export * from "../../devices/components/firmware_hardware_support"; diff --git a/frontend/farm_designer/settings/index.tsx b/frontend/farm_designer/settings/index.tsx new file mode 100644 index 000000000..6b69f9f92 --- /dev/null +++ b/frontend/farm_designer/settings/index.tsx @@ -0,0 +1,135 @@ +import * as React from "react"; +import { connect } from "react-redux"; +import { DesignerPanel, DesignerPanelContent } from "../designer_panel"; +import { t } from "../../i18next_wrapper"; +import { DesignerNavTabs, Panel } from "../panel_header"; +import { + bulkToggleControlPanel, MCUFactoryReset, toggleControlPanel, +} from "../../devices/actions"; +import { FarmBotSettings, Firmware, PowerAndReset } from "./fbos_settings"; +import { + HomingAndCalibration, Motors, Encoders, EndStops, ErrorHandling, + PinGuard, DangerZone, PinBindings, isFwHardwareValue, +} from "./hardware_settings"; +import { DevSettings } from "../../account/dev/dev_support"; +import { maybeOpenPanel } from "../../devices/components/maybe_highlight"; +import { isBotOnlineFromState } from "../../devices/must_be_online"; +import { DesignerSettingsProps } from "./interfaces"; +import { Designer, PlainDesignerSettings } from "./farm_designer_settings"; +import { SearchField } from "../../ui/search_field"; +import { mapStateToProps } from "./state_to_props"; +import { Actions } from "../../constants"; + +export class RawDesignerSettings + extends React.Component { + + componentDidMount = () => + this.props.dispatch(maybeOpenPanel(this.props.bot.controlPanelState, true)); + + componentWillUnmount = () => { + this.props.dispatch(bulkToggleControlPanel(false, true)); + this.props.dispatch(toggleControlPanel("farmbot_os")); + this.props.dispatch({ + type: Actions.SET_SETTINGS_SEARCH_TERM, + payload: "" + }); + } + + render() { + const { getConfigValue, dispatch, firmwareConfig, + sourceFwConfig, sourceFbosConfig, resources, + } = this.props; + const { controlPanelState } = this.props.bot; + const settingsProps = { getConfigValue, dispatch }; + const commonProps = { dispatch, controlPanelState }; + const { value } = this.props.sourceFbosConfig("firmware_hardware"); + const firmwareHardware = isFwHardwareValue(value) ? value : undefined; + const botOnline = isBotOnlineFromState(this.props.bot); + return + + + { + dispatch(bulkToggleControlPanel(true, true)); + dispatch({ + type: Actions.SET_SETTINGS_SEARCH_TERM, + payload: searchTerm + }); + }} /> + {DevSettings.futureFeaturesEnabled() ? +
+
+ + +
+
+ + + + + + + + + + + + +
+
+ :
+ {PlainDesignerSettings(settingsProps)} +
} +
+
; + } +} + +export const DesignerSettings = connect(mapStateToProps)(RawDesignerSettings); diff --git a/frontend/farm_designer/settings/interfaces.ts b/frontend/farm_designer/settings/interfaces.ts new file mode 100644 index 000000000..3123408ef --- /dev/null +++ b/frontend/farm_designer/settings/interfaces.ts @@ -0,0 +1,53 @@ +import { GetWebAppConfigValue } from "../../config_storage/actions"; +import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; +import { + SourceFwConfig, SourceFbosConfig, UserEnv, ShouldDisplay, + SaveFarmwareEnv, BotState, ControlPanelState, +} from "../../devices/interfaces"; +import { ResourceIndex } from "../../resources/interfaces"; +import { TaggedDevice, Alert } from "farmbot"; +import { TimeSettings } from "../../interfaces"; +import { DeviceSetting } from "../../constants"; +import { + BooleanConfigKey as WebAppBooleanConfigKey, +} from "farmbot/dist/resources/configs/web_app"; + +export interface DesignerSettingsPropsBase { + dispatch: Function; + getConfigValue: GetWebAppConfigValue; +} + +export interface DesignerSettingsProps extends DesignerSettingsPropsBase { + firmwareConfig: FirmwareConfig | undefined; + sourceFwConfig: SourceFwConfig; + sourceFbosConfig: SourceFbosConfig; + resources: ResourceIndex; + deviceAccount: TaggedDevice; + env: UserEnv; + alerts: Alert[]; + shouldDisplay: ShouldDisplay; + saveFarmwareEnv: SaveFarmwareEnv; + timeSettings: TimeSettings; + bot: BotState; + searchTerm: string; +} + +export interface DesignerSettingsSectionProps { + dispatch: Function; + controlPanelState: ControlPanelState; + getConfigValue: GetWebAppConfigValue; +} + +export interface SettingDescriptionProps { + setting?: WebAppBooleanConfigKey; + title: DeviceSetting; + description: string; + invert?: boolean; + callback?: () => void; + children?: React.ReactChild; + defaultOn?: boolean; + disabled?: boolean; +} + +export interface SettingProps + extends DesignerSettingsPropsBase, SettingDescriptionProps { } diff --git a/frontend/farm_designer/settings/state_to_props.ts b/frontend/farm_designer/settings/state_to_props.ts new file mode 100644 index 000000000..4c0742d2e --- /dev/null +++ b/frontend/farm_designer/settings/state_to_props.ts @@ -0,0 +1,35 @@ +import { Everything } from "../../interfaces"; +import { getWebAppConfigValue } from "../../config_storage/actions"; +import { validFwConfig, validFbosConfig } from "../../util"; +import { getFirmwareConfig, getFbosConfig } from "../../resources/getters"; +import { + sourceFwConfigValue, sourceFbosConfigValue, +} from "../../devices/components/source_config_value"; +import { + getDeviceAccountSettings, maybeGetTimeSettings, +} from "../../resources/selectors"; +import { + saveOrEditFarmwareEnv, getShouldDisplayFn, getEnv, +} from "../../farmware/state_to_props"; +import { getAllAlerts } from "../../messages/state_to_props"; +import { DesignerSettingsProps } from "./interfaces"; + +export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({ + dispatch: props.dispatch, + getConfigValue: getWebAppConfigValue(() => props), + firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index)), + sourceFwConfig: sourceFwConfigValue(validFwConfig(getFirmwareConfig( + props.resources.index)), props.bot.hardware.mcu_params), + sourceFbosConfig: sourceFbosConfigValue(validFbosConfig(getFbosConfig( + props.resources.index)), props.bot.hardware.configuration), + resources: props.resources.index, + deviceAccount: getDeviceAccountSettings(props.resources.index), + shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot), + env: getEnv(props.resources.index, getShouldDisplayFn( + props.resources.index, props.bot), props.bot), + saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index), + timeSettings: maybeGetTimeSettings(props.resources.index), + alerts: getAllAlerts(props.resources), + bot: props.bot, + searchTerm: props.resources.consumers.farm_designer.settingsSearchTerm, +}); diff --git a/frontend/ui/__tests__/search_field_test.tsx b/frontend/ui/__tests__/search_field_test.tsx index aeb70072a..43f2823cc 100644 --- a/frontend/ui/__tests__/search_field_test.tsx +++ b/frontend/ui/__tests__/search_field_test.tsx @@ -40,4 +40,12 @@ describe("", () => { wrapper.find("input").simulate("KeyPress", e); expect(p.onChange).not.toHaveBeenCalled(); }); + + it("clears search term", () => { + const p = fakeProps(); + p.searchTerm = "old"; + const wrapper = shallow(); + wrapper.find("i").last().simulate("click"); + expect(p.onChange).toHaveBeenCalledWith(""); + }); }); diff --git a/frontend/ui/search_field.tsx b/frontend/ui/search_field.tsx index b87ce9342..cd84567b7 100644 --- a/frontend/ui/search_field.tsx +++ b/frontend/ui/search_field.tsx @@ -23,7 +23,8 @@ export const SearchField = (props: SearchFieldProps) => onChange={e => props.onChange(e.currentTarget.value)} onKeyPress={e => props.onKeyPress?.(e.currentTarget.value)} placeholder={props.placeholder} /> - {props.searchTerm && props.customRightIcon} + {props.searchTerm && (props.customRightIcon || + props.onChange("")} />)}