settings panel updates

pull/1757/head
gabrielburnworth 2020-04-14 15:46:39 -07:00
parent 4375a935f0
commit bee1e0e074
33 changed files with 911 additions and 320 deletions

View File

@ -19,4 +19,5 @@ export const fakeDesignerState = (): DesignerState => ({
openedSavedGarden: undefined,
tryGroupSortType: undefined,
editGroupAreaInMap: false,
settingsSearchTerm: "",
});

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -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("<Highlight />", () => {
const fakeProps = (): HighlightProps => ({
@ -25,6 +32,24 @@ describe("<Highlight />", () => {
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(<Highlight {...fakeProps()} />);
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(<Highlight {...fakeProps()} />);
expect(wrapper.find("div").first().props().hidden).toEqual(false);
});
it("hides", () => {
mockState.resources.consumers.farm_designer.settingsSearchTerm = "encoder";
const wrapper = mount(<Highlight {...fakeProps()} />);
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);
});
});

View File

@ -9,11 +9,12 @@ import { settingToggle } from "../../actions";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { DeviceSetting } from "../../../constants";
describe("<PinGuardMCUInputGroup/>", () => {
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",

View File

@ -13,11 +13,12 @@ import {
import { TaggedFirmwareConfig } from "farmbot";
import { FBSelect } from "../../../ui";
import { updateMCU } from "../../actions";
import { DeviceSetting } from "../../../constants";
describe("<PinNumberDropdown />", () => {
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",

View File

@ -24,7 +24,7 @@ export function DangerZone(props: DangerZoneProps) {
<Highlight settingName={DeviceSetting.resetHardwareParams}>
<Row>
<Col xs={newFormat ? 8 : 4}>
<label>
<label style={{ lineHeight: "1.5rem" }}>
{t(DeviceSetting.resetHardwareParams)}
</label>
</Col>

View File

@ -44,7 +44,7 @@ export function PinGuard(props: PinGuardProps) {
</Col>
</Row>}
<PinGuardMCUInputGroup
label={t("Pin Guard {{ num }}", { num: 1 })}
label={DeviceSetting.pinGuard1}
pinNumKey={"pin_guard_1_pin_nr"}
timeoutKey={"pin_guard_1_time_out"}
activeStateKey={"pin_guard_1_active_state"}
@ -52,7 +52,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={t("Pin Guard {{ num }}", { num: 2 })}
label={DeviceSetting.pinGuard2}
pinNumKey={"pin_guard_2_pin_nr"}
timeoutKey={"pin_guard_2_time_out"}
activeStateKey={"pin_guard_2_active_state"}
@ -60,7 +60,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={t("Pin Guard {{ num }}", { num: 3 })}
label={DeviceSetting.pinGuard3}
pinNumKey={"pin_guard_3_pin_nr"}
timeoutKey={"pin_guard_3_time_out"}
activeStateKey={"pin_guard_3_active_state"}
@ -68,7 +68,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={t("Pin Guard {{ num }}", { num: 4 })}
label={DeviceSetting.pinGuard4}
pinNumKey={"pin_guard_4_pin_nr"}
timeoutKey={"pin_guard_4_time_out"}
activeStateKey={"pin_guard_4_active_state"}
@ -76,7 +76,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={t("Pin Guard {{ num }}", { num: 5 })}
label={DeviceSetting.pinGuard5}
pinNumKey={"pin_guard_5_pin_nr"}
timeoutKey={"pin_guard_5_time_out"}
activeStateKey={"pin_guard_5_active_state"}

View File

@ -2,28 +2,32 @@ import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { t } from "../../../i18next_wrapper";
import { DevSettings } from "../../../account/dev/dev_support";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
export function SpacePanelHeader() {
const newFormat = DevSettings.futureFeaturesEnabled();
const width = newFormat ? 4 : 2;
const offset = newFormat ? 0 : 6;
return <div className="label-headings">
<Row>
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
<label>
{t("X AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Y AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Z AXIS")}
</label>
</Col>
</Row>
<Highlight settingName={DeviceSetting.axisHeadingLabels}>
<Row>
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
<label>
{t("X AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Y AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Z AXIS")}
</label>
</Col>
</Row>
</Highlight>
</div>;
}

View File

@ -65,7 +65,7 @@ export interface NumericMCUInputGroupProps {
export interface PinGuardMCUInputGroupProps {
sourceFwConfig: SourceFwConfig;
dispatch: Function;
label: string;
label: DeviceSetting;
pinNumKey: McuParamName;
timeoutKey: McuParamName;
activeStateKey: McuParamName;

View File

@ -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<HighlightProps, HighlightState> {
}
}
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 <div className={[
this.props.className,
this.state.className,
].join(" ")}>
].join(" ")}
hidden={!show}>
{this.props.children}
</div>;
}

View File

@ -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<PinGuardMCUInputGroupProps> {
@ -50,7 +51,7 @@ export class PinGuardMCUInputGroup
? <Row>
<Col xs={3}>
<label>
{label}
{t(label)}
</label>
</Col>
<Col xs={3}>
@ -63,46 +64,48 @@ export class PinGuardMCUInputGroup
<this.State />
</Col>
</Row>
: <div className={"pin-guard-input-row"}>
<Row>
<Col xs={12}>
<label>
{label}
</label>
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Pin Number")}
</label>
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
position={Position.TOP_RIGHT} />
</Col>
<Col xs={5} className="no-pad">
<this.Number />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Timeout (sec)")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.Timeout />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("To State")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.State />
</Col>
</Row>
</div>;
: <Highlight settingName={label}>
<div className={"pin-guard-input-row"}>
<Row>
<Col xs={12}>
<label>
{t(label)}
</label>
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Pin Number")}
</label>
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
position={Position.TOP_RIGHT} />
</Col>
<Col xs={5} className="no-pad">
<this.Number />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Timeout (sec)")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.Timeout />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("To State")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.State />
</Col>
</Row>
</div>
</Highlight>;
}
}

View File

@ -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<PinBindingInputGroupProps, PinBindingInputGroupState> {
@ -129,7 +130,7 @@ export class PinBindingInputGroup
render() {
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className="pin-binding-input-rows">
{newFormat && <Row><label>{t("add new pin binding")}</label></Row>}
{newFormat && <Row><label>{t(DeviceSetting.addNewPinBinding)}</label></Row>}
{newFormat && <this.Number />}
{newFormat && <Row>
<Col xs={5}>

View File

@ -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 <div className="pin-bindings">
<Row>
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
position={Position.TOP_RIGHT} />}
<StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover
position={Position.TOP_RIGHT}
interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" />
<div className={"pin-binding-warning"}>
{t(ToolTips.PIN_BINDING_WARNING)}
</div>
</Popover>
</Row>
<Highlight settingName={DeviceSetting.pinBindings}>
<Row>
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
position={Position.TOP_RIGHT} />}
<StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover
position={Position.TOP_RIGHT}
interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" />
<div className={"pin-binding-warning"}>
{t(ToolTips.PIN_BINDING_WARNING)}
</div>
</Popover>
</Row>
</Highlight>
<div className={"pin-bindings-list-and-input"}>
{!newFormat && <PinBindingsListHeader />}
<PinBindingsList
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
<PinBindingInputGroup
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
<Highlight settingName={DeviceSetting.savedPinBindings}>
<PinBindingsList
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
</Highlight>
<Highlight settingName={DeviceSetting.addNewPinBinding}>
<PinBindingInputGroup
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
</Highlight>
</div>
</div>;
};

View File

@ -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 <div className={"bindings-list"}>
{newFormat && <Row><label>{t("saved pin bindings")}</label></Row>}
{newFormat && <Row><label>{t(DeviceSetting.savedPinBindings)}</label></Row>}
{pinBindings
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
.map(x => {

View File

@ -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<string> = {
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;

View File

@ -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("<DesignerSettings />", () => {
const fakeProps = (): DesignerSettingsProps => ({
dispatch: jest.fn(),
getConfigValue: jest.fn(),
});
it("renders settings", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
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(<DesignerSettings {...p} />);
const confirmDeletion = getSetting(wrapper, 6, "confirm plant");
expect(confirmDeletion.find("button").text()).toEqual("on");
});
it("toggles setting", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
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(<DesignerSettings {...p} />);
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);
});
});

View File

@ -125,6 +125,7 @@ export interface DesignerState {
openedSavedGarden: string | undefined;
tryGroupSortType: PointGroupSortType | "nn" | undefined;
editGroupAreaInMap: boolean;
settingsSearchTerm: string;
}
export type TaggedExecutable = TaggedSequence | TaggedRegimen;

View File

@ -27,6 +27,7 @@ export const initialState: DesignerState = {
openedSavedGarden: undefined,
tryGroupSortType: undefined,
editGroupAreaInMap: false,
settingsSearchTerm: "",
};
export const designer = generateReducer<DesignerState>(initialState)
@ -107,6 +108,10 @@ export const designer = generateReducer<DesignerState>(initialState)
s.tryGroupSortType = payload;
return s;
})
.add<string>(Actions.SET_SETTINGS_SEARCH_TERM, (s, { payload }) => {
s.settingsSearchTerm = payload;
return s;
})
.add<boolean>(Actions.EDIT_GROUP_AREA_IN_MAP, (s, { payload }) => {
s.editGroupAreaInMap = payload;
return s;

View File

@ -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<DesignerSettingsProps, {}> {
render() {
const { getConfigValue, dispatch } = this.props;
const settingsProps = { getConfigValue, dispatch };
return <DesignerPanel panelName={"settings"} panel={Panel.Settings}>
<DesignerNavTabs />
<DesignerPanelContent panelName={"settings"}>
{DESIGNER_SETTINGS(settingsProps).map(setting =>
<Setting key={setting.title} {...setting} {...settingsProps} />)}
</DesignerPanelContent>
</DesignerPanel>;
}
}
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 <div
className={`designer-setting ${props.disabled ? "disabled" : ""}`}>
<Row>
<Col xs={9}>
<label>{t(title)}</label>
</Col>
<Col xs={3}>
{setting && <ToggleButton
toggleValue={props.invert ? !value : value}
toggleAction={() => {
props.dispatch(setWebAppConfigValue(setting, !value));
callback?.();
}}
title={`${t("toggle")} ${title}`}
customText={{ textFalse: t("off"), textTrue: t("on") }} />}
</Col>
</Row>
<Row>
<p>{t(props.description)}</p>
</Row>
{props.children}
</div>;
};
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: <MapSizeInputs {...settingsProps} />,
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: <OriginSelector {...settingsProps} />
},
{
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 <div className="farmbot-origin">
<div className="quadrants">
{[2, 1, 3, 4].map(q =>
<div key={"quadrant_" + q}
className={`quadrant ${quadrant === q ? "selected" : ""}`}
onClick={update(q)} />)}
</div>
</div>;
};
export const DesignerSettings = connect(mapStateToProps)(RawDesignerSettings);

View File

@ -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("<PlainDesignerSettings />", () => {
const fakeProps = (): DesignerSettingsPropsBase => ({
dispatch: jest.fn(),
getConfigValue: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<div>{PlainDesignerSettings(fakeProps())}</div>);
expect(wrapper.text().toLowerCase()).toContain("plant animations");
});
it("doesn't call callback", () => {
const wrapper = mount(<div>{PlainDesignerSettings(fakeProps())}</div>);
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(<div>{PlainDesignerSettings(fakeProps())}</div>);
expect(wrapper.find("label").at(1).text()).toContain("trail");
wrapper.find("button").at(1).simulate("click");
expect(resetVirtualTrail).toHaveBeenCalled();
});
});

View File

@ -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 }) => <div>{p.children}</div>,
}));
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("<DesignerSettings />", () => {
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(<DesignerSettings {...fakeProps()} />);
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(<DesignerSettings {...fakeProps()} />);
expect(wrapper.text().toLowerCase()).toContain("pin");
});
it("mounts", () => {
mount(<DesignerSettings {...fakeProps()} />);
expect(maybeOpenPanel).toHaveBeenCalled();
});
it("unmounts", () => {
const p = fakeProps();
const wrapper = mount(<DesignerSettings {...p} />);
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(<DesignerSettings {...p} />);
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(<DesignerSettings {...p} />);
expect(wrapper.find(Motors).props().firmwareHardware).toEqual("arduino");
});
it("expands all", () => {
mockDev = true;
const p = fakeProps();
const wrapper = mount(<DesignerSettings {...p} />);
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(<DesignerSettings {...p} />);
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(<DesignerSettings {...p} />);
const confirmDeletion = getSetting(wrapper, 6, "confirm plant");
expect(confirmDeletion.find("button").text()).toEqual("on");
});
it("toggles setting", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
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(<DesignerSettings {...p} />);
const originSetting = getSetting(wrapper, 5, "origin");
originSetting.find("div").last().simulate("click");
expect(setWebAppConfigValue).toHaveBeenCalledWith(
NumericSetting.bot_origin_quadrant, 4);
});
});

View File

@ -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);
});
});

View File

@ -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 <Highlight className={"section"}
settingName={DeviceSetting.farmDesigner}>
<Header
title={DeviceSetting.farmDesigner}
panel={"farm_designer"}
dispatch={dispatch}
expanded={controlPanelState.farm_designer} />
<Collapse isOpen={!!controlPanelState.farm_designer}>
{PlainDesignerSettings(settingsProps)}
</Collapse>
</Highlight>;
};
export const PlainDesignerSettings =
(settingsProps: DesignerSettingsPropsBase) =>
DESIGNER_SETTINGS(settingsProps).map(setting =>
<Setting key={setting.title} {...setting} {...settingsProps} />);
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 <Highlight settingName={title}>
<div
className={`designer-setting ${props.disabled ? "disabled" : ""}`}>
<Row>
<Col xs={9}>
<label>{t(title)}</label>
</Col>
<Col xs={3}>
{setting && <ToggleButton
toggleValue={props.invert ? !value : value}
toggleAction={() => {
props.dispatch(setWebAppConfigValue(setting, !value));
callback?.();
}}
title={`${t("toggle")} ${title}`}
customText={{ textFalse: t("off"), textTrue: t("on") }} />}
</Col>
</Row>
<Row>
<p>{t(props.description)}</p>
</Row>
{props.children}
</div>
</Highlight>;
};
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: <MapSizeInputs {...settingsProps} />,
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: <OriginSelector {...settingsProps} />
},
{
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 <div className="farmbot-origin">
<div className="quadrants">
{[2, 1, 3, 4].map(q =>
<div key={"quadrant_" + q}
className={`quadrant ${quadrant === q ? "selected" : ""}`}
onClick={update(q)} />)}
</div>
</div>;
};

View File

@ -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";

View File

@ -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";

View File

@ -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<DesignerSettingsProps, {}> {
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 <DesignerPanel panelName={"settings"} panel={Panel.Settings}>
<DesignerNavTabs />
<DesignerPanelContent panelName={"settings"}>
<SearchField
placeholder={t("Search settings...")}
searchTerm={this.props.searchTerm}
onChange={searchTerm => {
dispatch(bulkToggleControlPanel(true, true));
dispatch({
type: Actions.SET_SETTINGS_SEARCH_TERM,
payload: searchTerm
});
}} />
{DevSettings.futureFeaturesEnabled() ?
<div className="all-settings">
<div className="bulk-expand-controls">
<button
className={"fb-button gray no-float"}
onClick={() => dispatch(bulkToggleControlPanel(true, true))}>
{t("Expand All")}
</button>
<button
className={"fb-button gray no-float"}
onClick={() => dispatch(bulkToggleControlPanel(false, true))}>
{t("Collapse All")}
</button>
</div>
<div className="all-settings-content">
<FarmBotSettings
bot={this.props.bot}
env={this.props.env}
alerts={this.props.alerts}
saveFarmwareEnv={this.props.saveFarmwareEnv}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
timeSettings={this.props.timeSettings}
device={this.props.deviceAccount} />
<Firmware
bot={this.props.bot}
alerts={this.props.alerts}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
timeSettings={this.props.timeSettings} />
<PowerAndReset {...commonProps}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
<HomingAndCalibration {...commonProps}
bot={this.props.bot}
sourceFwConfig={sourceFwConfig}
firmwareConfig={firmwareConfig}
firmwareHardware={firmwareHardware}
botOnline={botOnline} />
<Motors {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<Encoders {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<EndStops {...commonProps}
sourceFwConfig={sourceFwConfig} />
<ErrorHandling {...commonProps}
sourceFwConfig={sourceFwConfig} />
<PinBindings {...commonProps}
resources={resources}
firmwareHardware={firmwareHardware} />
<PinGuard {...commonProps}
resources={resources}
sourceFwConfig={sourceFwConfig} />
<DangerZone {...commonProps}
onReset={MCUFactoryReset}
botOnline={botOnline} />
<Designer {...commonProps}
getConfigValue={getConfigValue} />
</div>
</div>
: <div className="designer-settings">
{PlainDesignerSettings(settingsProps)}
</div>}
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const DesignerSettings = connect(mapStateToProps)(RawDesignerSettings);

View File

@ -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 { }

View File

@ -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,
});

View File

@ -40,4 +40,12 @@ describe("<SearchField />", () => {
wrapper.find("input").simulate("KeyPress", e);
expect(p.onChange).not.toHaveBeenCalled();
});
it("clears search term", () => {
const p = fakeProps();
p.searchTerm = "old";
const wrapper = shallow(<SearchField {...p} />);
wrapper.find("i").last().simulate("click");
expect(p.onChange).toHaveBeenCalledWith("");
});
});

View File

@ -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 ||
<i className="fa fa-times" onClick={() => props.onChange("")} />)}
</ErrorBoundary>
</div>
</div>