designer settings menu

pull/1230/head
gabrielburnworth 2019-06-10 14:43:11 -07:00
parent cba5646e23
commit 9e8ae68da9
13 changed files with 240 additions and 45 deletions

View File

@ -37,10 +37,8 @@ const LengthInput = (props: LengthInputProps) =>
</Row>;
export const MapSizeSetting =
({ dispatch, getConfigValue }: MapSizeSettingProps) => {
const mapSizeX = parseInt("" + getConfigValue(NumericSetting.map_size_x));
const mapSizeY = parseInt("" + getConfigValue(NumericSetting.map_size_y));
return <div className={"map-size-inputs"}>
({ dispatch, getConfigValue }: MapSizeSettingProps) =>
<div className={"map-size-setting"}>
<Row>
<Col xs={4}>
<label>{t("garden map size")}</label>
@ -49,17 +47,28 @@ export const MapSizeSetting =
<p>{t(Content.MAP_SIZE)}</p>
</Col>
<Col xs={4}>
<LengthInput
value={mapSizeX}
label={t("x (mm)")}
setting={NumericSetting.map_size_x}
dispatch={dispatch} />
<LengthInput
value={mapSizeY}
label={t("y (mm)")}
setting={NumericSetting.map_size_y}
<MapSizeInputs
getConfigValue={getConfigValue}
dispatch={dispatch} />
</Col>
</Row>
</div>;
};
interface MapSizeInputsProps {
dispatch: Function;
getConfigValue: GetWebAppConfigValue;
}
export const MapSizeInputs = (props: MapSizeInputsProps) =>
<div className="map-size-inputs">
<LengthInput
value={parseInt("" + props.getConfigValue(NumericSetting.map_size_x))}
label={t("x (mm)")}
setting={NumericSetting.map_size_x}
dispatch={props.dispatch} />
<LengthInput
value={parseInt("" + props.getConfigValue(NumericSetting.map_size_y))}
label={t("y (mm)")}
setting={NumericSetting.map_size_y}
dispatch={props.dispatch} />
</div>;

View File

@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => {
Object.defineProperty(window.location, "reload", { value: jest.fn() });
it("basically just initializes stuff", () => {
const val = fetchLabFeatures(jest.fn());
expect(val.length).toBe(10);
expect(val.length).toBe(7);
expect(val[0].value).toBeFalsy();
const { callback } = val[0];
if (callback) {

View File

@ -5,7 +5,6 @@ import { maybeToggleFeature } from "./labs_features_list_data";
import { ToolTips } from "../../constants";
import { GetWebAppConfigValue } from "../../config_storage/actions";
import { t } from "../../i18next_wrapper";
import { MapSizeSetting } from "../components/map_size_setting";
interface LabsFeaturesProps {
getConfigValue: GetWebAppConfigValue;
@ -28,9 +27,6 @@ export class LabsFeatures extends React.Component<LabsFeaturesProps, {}> {
maybeToggleFeature(getConfigValue, dispatch)(x);
this.forceUpdate();
}} />
<MapSizeSetting
dispatch={this.props.dispatch}
getConfigValue={this.props.getConfigValue} />
</WidgetBody>
</Widget>;
}

View File

@ -1,7 +1,8 @@
import { BooleanSetting } from "../../session_keys";
import { Content } from "../../constants";
import { VirtualTrail } from "../../farm_designer/map/layers/farmbot/bot_trail";
import { GetWebAppConfigValue, setWebAppConfigValue } from "../../config_storage/actions";
import {
GetWebAppConfigValue, setWebAppConfigValue
} from "../../config_storage/actions";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { t } from "../../i18next_wrapper";
@ -44,13 +45,6 @@ export const fetchLabFeatures =
storageKey: BooleanSetting.hide_sensors,
value: false
},
{
name: t("Display plant animations"),
description: t(Content.PLANT_ANIMATIONS),
storageKey: BooleanSetting.disable_animations,
value: false,
displayInvert: true
},
{
name: t("Read speak logs in browser"),
description: t(Content.BROWSER_SPEAK_LOGS),
@ -64,13 +58,6 @@ export const fetchLabFeatures =
value: false,
confirmationMessage: t(Content.DISCARD_UNSAVED_CHANGES_CONFIRM)
},
{
name: t("Display virtual FarmBot trail"),
description: t(Content.VIRTUAL_TRAIL),
storageKey: BooleanSetting.display_trail,
value: false,
callback: () => sessionStorage.setItem(VirtualTrail.records, "[]")
},
{
name: t("Use 24-hour time format"),
description: t(Content.TIME_FORMAT_24_HOUR),
@ -85,12 +72,6 @@ export const fetchLabFeatures =
value: false,
displayInvert: true,
},
{
name: t("Dynamic map size"),
description: t(Content.DYNAMIC_MAP_SIZE),
storageKey: BooleanSetting.dynamic_map,
value: false
},
].map(fetchSettingValue(getConfigValue)));
/** Always allow toggling from true => false (deactivate).

View File

@ -381,6 +381,25 @@
}
}
.settings-panel-content {
margin-top: 5rem;
button {
margin-top: 1.75rem;
}
p {
padding: 1rem;
margin-left: 1rem;
}
.map-size-inputs {
.row {
margin-bottom: 1rem;
}
label {
margin-top: 0.5rem;
}
}
}
.saved-garden-panel-content {
&.with-nav {
margin-top: 6rem;

View File

@ -989,7 +989,7 @@ ul {
}
}
.map-size-inputs {
.map-size-setting {
margin-top: 1rem;
}

View File

@ -43,4 +43,12 @@ describe("<DesignerNavTabs />", () => {
expect(wrapper.hasClass("green-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});
it("renders for settings", () => {
mockPath = "/app/designer/settings";
mockDev = true;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("gray-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});
});

View File

@ -0,0 +1,42 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
jest.mock("../../config_storage/actions", () => ({
getWebAppConfigValue: jest.fn(() => jest.fn(() => true)),
setWebAppConfigValue: jest.fn(),
}));
import * as React from "react";
import { mount } from "enzyme";
import {
DesignerSettings, DesignerSettingsProps, mapStateToProps
} from "../settings";
import { fakeState } from "../../__test_support__/fake_state";
import { BooleanSetting } from "../../session_keys";
import { setWebAppConfigValue } from "../../config_storage/actions";
describe("<DesignerSettings />", () => {
const fakeProps = (): DesignerSettingsProps => ({
dispatch: jest.fn(),
getConfigValue: jest.fn(),
});
it("renders settings", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
expect(wrapper.text()).toContain("size");
});
it("toggles setting", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
wrapper.find("button").at(1).simulate("click");
expect(setWebAppConfigValue)
.toHaveBeenCalledWith(BooleanSetting.display_trail, true);
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const props = mapStateToProps(fakeState());
const value = props.getConfigValue(BooleanSetting.show_plants);
expect(value).toEqual(true);
});
});

View File

@ -1,6 +1,8 @@
import * as React from "react";
import { shallow } from "enzyme";
import { BotTrail, BotTrailProps, VirtualTrail } from "../bot_trail";
import {
BotTrail, BotTrailProps, VirtualTrail, resetVirtualTrail
} from "../bot_trail";
import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";
@ -75,3 +77,11 @@ describe("<BotTrail/>", () => {
});
});
describe("resetVirtualTrail()", () => {
it("clears data", () => {
sessionStorage.setItem(VirtualTrail.records, "[1]");
resetVirtualTrail();
expect(sessionStorage.getItem(VirtualTrail.records)).toEqual("[]");
});
});

View File

@ -70,3 +70,6 @@ export function BotTrail(props: BotTrailProps) {
})}
</g>;
}
export const resetVirtualTrail = () =>
sessionStorage.setItem(VirtualTrail.records, "[]");

View File

@ -9,6 +9,7 @@ export enum Panel {
Plants = "Plants",
FarmEvents = "FarmEvents",
SavedGardens = "SavedGardens",
Settings = "Settings",
}
type Tabs = keyof typeof Panel;
@ -18,6 +19,7 @@ export const TAB_COLOR: { [key in Panel]: string } = {
[Panel.Plants]: "green",
[Panel.FarmEvents]: "yellow",
[Panel.SavedGardens]: "green",
[Panel.Settings]: "gray",
};
const iconFile = (icon: string) => `/app-resources/img/icons/${icon}.svg`;
@ -27,6 +29,7 @@ export const TAB_ICON: { [key in Panel]: string } = {
[Panel.Plants]: iconFile("plant"),
[Panel.FarmEvents]: iconFile("calendar"),
[Panel.SavedGardens]: iconFile("gardens"),
[Panel.Settings]: iconFile("gardens"),
};
const getCurrentTab = (): Tabs => {
@ -37,6 +40,8 @@ const getCurrentTab = (): Tabs => {
return Panel.FarmEvents;
} else if (pathArray.includes("saved_gardens")) {
return Panel.SavedGardens;
} else if (pathArray.includes("settings")) {
return Panel.Settings;
} else {
return Panel.Plants;
}
@ -48,15 +53,16 @@ interface NavTabProps {
panel: Panel;
linkTo: string;
title: string;
icon?: string;
}
const NavTab = (props: NavTabProps) =>
<Link to={props.linkTo}
<Link to={props.linkTo} style={props.icon ? { flex: 0.3 } : {}}
className={getCurrentTab() === props.panel ? "active" : ""}>
{DevSettings.futureFeaturesEnabled()
? <img {...common}
src={TAB_ICON[props.panel]} title={props.title} />
: props.title}
: props.icon ? <i className={props.icon} /> : props.title}
</Link>;
export function DesignerNavTabs(props: { hidden?: boolean }) {
@ -73,6 +79,8 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
{DevSettings.futureFeaturesEnabled() &&
<NavTab panel={Panel.SavedGardens}
linkTo={"/app/designer/saved_gardens"} title={t("Gardens")} />}
<NavTab panel={Panel.Settings} icon={"fa fa-gear"}
linkTo={"/app/designer/settings"} title={t("Settings")} />
</div>
</div>;
}

View File

@ -0,0 +1,111 @@
import * as React from "react";
import { Everything } from "../interfaces";
import { connect } from "react-redux";
import { Content } from "../constants";
import { DesignerPanel, DesignerPanelContent } from "./plants/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 } from "../session_keys";
import { resetVirtualTrail } from "./map/layers/farmbot/bot_trail";
import { MapSizeInputs } from "../account/components/map_size_setting";
import { DesignerNavTabs } from "./panel_header";
export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({
dispatch: props.dispatch,
getConfigValue: getWebAppConfigValue(() => props),
});
export interface DesignerSettingsProps {
dispatch: Function;
getConfigValue: GetWebAppConfigValue;
}
@connect(mapStateToProps)
export class DesignerSettings
extends React.Component<DesignerSettingsProps, {}> {
render() {
return <DesignerPanel panelName={"settings"} panelColor={"gray"}>
<DesignerNavTabs />
<DesignerPanelContent panelName={"settings"}>
{DESIGNER_SETTINGS.map(setting =>
<Setting key={setting.title}
dispatch={this.props.dispatch}
getConfigValue={this.props.getConfigValue}
setting={setting.setting}
title={setting.title}
description={setting.description}
invert={setting.invert}
callback={setting.callback} />)}
<MapSizeInputs
getConfigValue={this.props.getConfigValue}
dispatch={this.props.dispatch} />
</DesignerPanelContent>
</DesignerPanel>;
}
}
interface SettingDescriptionProps {
setting?: BooleanConfigKey;
title: string;
description: string;
invert?: boolean;
callback?: () => void;
}
interface SettingProps
extends DesignerSettingsProps, SettingDescriptionProps { }
const Setting = (props: SettingProps) => {
const { title, setting, callback } = props;
const value = setting ? !!props.getConfigValue(setting) : undefined;
return <div className="designer-setting">
<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 && callback();
}}
title={`${t("toggle")} ${title}`}
customText={{ textFalse: t("off"), textTrue: t("on") }} />}
</Col>
</Row>
<Row>
<p>{t(props.description)}</p>
</Row>
</div>;
};
const DESIGNER_SETTINGS: 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),
},
];

View File

@ -304,4 +304,12 @@ export const UNBOUND_ROUTES = [
getChild: () => import("./farm_designer/saved_gardens/saved_gardens"),
childKey: "SavedGardens"
}),
route({
children: true,
$: "/designer/settings",
getModule,
key,
getChild: () => import("./farm_designer/settings"),
childKey: "DesignerSettings"
}),
].concat([NOT_FOUND_ROUTE]);