model and version updates part 2

pull/1699/head
gabrielburnworth 2020-02-18 11:21:09 -08:00
parent 310686508f
commit a04ec59ba5
87 changed files with 1481 additions and 701 deletions

View File

@ -1,10 +1,9 @@
jest.mock("../util", () => { jest.mock("../util", () => ({
return { attachToRoot: jest.fn(),
attachToRoot: jest.fn(), // Incidental mock. Can be removed if errors go away.
// Incidental mock. Can be removed if errors go away. trim: jest.fn(x => x),
trim: jest.fn(x => x) urlFriendly: jest.fn(),
}; }));
});
jest.mock("../redux/store", () => { jest.mock("../redux/store", () => {
return { store: { dispatch: jest.fn() } }; return { store: { dispatch: jest.fn() } };

View File

@ -6,9 +6,9 @@ import { ExternalUrl } from "../external_urls";
describe("ExternalUrl", () => { describe("ExternalUrl", () => {
it("returns urls", () => { it("returns urls", () => {
expect(ExternalUrl.featureMinVersions) expect(ExternalUrl.featureMinVersions)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/FEATURE_MIN_VERSIONS.json"); .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/FEATURE_MIN_VERSIONS.json");
expect(ExternalUrl.osReleaseNotes) expect(ExternalUrl.osReleaseNotes)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/RELEASE_NOTES.md"); .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md");
expect(ExternalUrl.latestRelease) expect(ExternalUrl.latestRelease)
.toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest"); .toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest");
expect(ExternalUrl.webAppRepo) expect(ExternalUrl.webAppRepo)
@ -18,16 +18,16 @@ describe("ExternalUrl", () => {
expect(ExternalUrl.softwareDocs) expect(ExternalUrl.softwareDocs)
.toEqual("https://software.farm.bot/docs"); .toEqual("https://software.farm.bot/docs");
expect(ExternalUrl.softwareForum) expect(ExternalUrl.softwareForum)
.toEqual("http://forum.farmbot.org/c/software"); .toEqual("https://forum.farmbot.org/c/software");
expect(ExternalUrl.OpenFarm.cropApi) expect(ExternalUrl.OpenFarm.cropApi)
.toEqual("https://openfarm.cc/api/v1/crops/"); .toEqual("https://openfarm.cc/api/v1/crops/");
expect(ExternalUrl.OpenFarm.cropBrowse) expect(ExternalUrl.OpenFarm.cropBrowse)
.toEqual("https://openfarm.cc/crops/"); .toEqual("https://openfarm.cc/crops/");
expect(ExternalUrl.OpenFarm.newCrop) expect(ExternalUrl.OpenFarm.newCrop)
.toEqual("https://openfarm.cc/en/crops/new"); .toEqual("https://openfarm.cc/en/crops/new");
expect(ExternalUrl.Videos.desktop) expect(ExternalUrl.Video.desktop)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"); .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018");
expect(ExternalUrl.Videos.mobile) expect(ExternalUrl.Video.mobile)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"); .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097");
}); });
}); });

View File

@ -702,9 +702,9 @@ export namespace Content {
trim(`FarmBot sent a malformed message. You may need to upgrade trim(`FarmBot sent a malformed message. You may need to upgrade
FarmBot OS. Please upgrade FarmBot OS and log back in.`); FarmBot OS. Please upgrade FarmBot OS and log back in.`);
export const OLD_FBOS_REC_UPGRADE = trim(`Your version of FarmBot OS is export const OLD_FBOS_REC_UPGRADE =
outdated and will soon no longer be supported. Please update your device as trim(`Your version of FarmBot OS is outdated and will soon no longer
soon as possible.`); be supported. Please update your device as soon as possible.`);
export const EXPERIMENTAL_WARNING = export const EXPERIMENTAL_WARNING =
trim(`Warning! This is an EXPERIMENTAL feature. This feature may be trim(`Warning! This is an EXPERIMENTAL feature. This feature may be
@ -812,7 +812,10 @@ export namespace Content {
trim(`add this crop on OpenFarm?`); trim(`add this crop on OpenFarm?`);
export const NO_TOOLS = export const NO_TOOLS =
trim(`Press "+" to add a new tool.`); trim(`Press "+" to add a new tool or seed container.`);
export const NO_SEED_CONTAINERS =
trim(`Press "+" to add a seed container.`);
export const MOUNTED_TOOL = export const MOUNTED_TOOL =
trim(`The tool currently mounted to the UTM can be set here or by using trim(`The tool currently mounted to the UTM can be set here or by using
@ -887,12 +890,23 @@ export namespace TourContent {
selecting one, and dragging it into the garden.`); selecting one, and dragging it into the garden.`);
export const ADD_TOOLS = export const ADD_TOOLS =
trim(`Press edit and then the + button to add tools and seed containers.`); trim(`Press the + button to add tools and seed containers.`);
export const ADD_SEED_CONTAINERS =
trim(`Press the + button to add seed containers.`);
export const ADD_TOOLS_AND_SLOTS =
trim(`Press the + button to add tools and seed containers. Then create
tool slots for them to by pressing the tool slot + button.`);
export const ADD_SEED_CONTAINERS_AND_SLOTS =
trim(`Press the + button to add seed containers. Then create
slots for them to by pressing the seed container slot + button.`);
export const ADD_TOOLS_SLOTS = export const ADD_TOOLS_SLOTS =
trim(`Add the newly created tools and seed containers to the trim(`Add the newly created tools and seed containers to the
corresponding tool slots on FarmBot: corresponding tool slots on FarmBot:
press edit and then + to create a tool slot.`); press the + button to create a tool slot.`);
export const ADD_PERIPHERALS = export const ADD_PERIPHERALS =
trim(`Press edit and then the + button to add peripherals.`); trim(`Press edit and then the + button to add peripherals.`);
@ -930,6 +944,87 @@ export namespace TourContent {
trim(`Toggle various settings to customize your web app experience.`); trim(`Toggle various settings to customize your web app experience.`);
} }
export enum DeviceSetting {
// Homing and calibration
homingAndCalibration = `Homing and Calibration`,
homing = `Homing`,
calibration = `Calibration`,
setZeroPosition = `Set Zero Position`,
findHomeOnBoot = `Find Home on Boot`,
stopAtHome = `Stop at Home`,
stopAtMax = `Stop at Max`,
negativeCoordinatesOnly = `Negative Coordinates Only`,
axisLength = `Axis Length (mm)`,
// Motors
motors = `Motors`,
maxSpeed = `Max Speed (mm/s)`,
homingSpeed = `Homing Speed (mm/s)`,
minimumSpeed = `Minimum Speed (mm/s)`,
accelerateFor = `Accelerate for (mm)`,
stepsPerMm = `Steps per MM`,
microstepsPerStep = `Microsteps per step`,
alwaysPowerMotors = `Always Power Motors`,
invertMotors = `Invert Motors`,
motorCurrent = `Motor Current`,
enable2ndXMotor = `Enable 2nd X Motor`,
invert2ndXMotor = `Invert 2nd X Motor`,
// Encoders / Stall Detection
encoders = `Encoders`,
stallDetection = `Stall Detection`,
enableEncoders = `Enable Encoders`,
enableStallDetection = `Enable Stall Detection`,
stallSensitivity = `Stall Sensitivity`,
useEncodersForPositioning = `Use Encoders for Positioning`,
invertEncoders = `Invert Encoders`,
maxMissedSteps = `Max Missed Steps`,
missedStepDecay = `Missed Step Decay`,
encoderScaling = `Encoder Scaling`,
// Endstops
endstops = `Endstops`,
enableEndstops = `Enable Endstops`,
swapEndstops = `Swap Endstops`,
invertEndstops = `Invert Endstops`,
// Error handling
errorHandling = `Error Handling`,
timeoutAfter = `Timeout after (seconds)`,
maxRetries = `Max Retries`,
estopOnMovementError = `E-Stop on Movement Error`,
// Pin Guard
pinGuard = `Pin Guard`,
// Danger Zone
dangerZone = `dangerZone`,
resetHardwareParams = `Reset hardware parameter defaults`,
// Pin Bindings
pinBindings = `Pin Bindings`,
// FarmBot OS
name = `name`,
timezone = `timezone`,
camera = `camera`,
firmware = `firmware`,
farmbotOSAutoUpdate = `Farmbot OS Auto Update`,
farmbotOS = `Farmbot OS`,
autoSync = `Auto Sync`,
bootSequence = `Boot Sequence`,
// Power and Reset
powerAndReset = `Power and Reset`,
restartFarmbot = `Restart Farmbot`,
shutdownFarmbot = `Shutdown Farmbot`,
restartFirmware = `Restart Firmware`,
factoryReset = `Factory Reset`,
autoFactoryReset = `Automatic Factory Reset`,
connectionAttemptPeriod = `Connection Attempt Period`,
changeOwnership = `Change Ownership`,
}
export namespace DiagnosticMessages { export namespace DiagnosticMessages {
export const OK = trim(`All systems nominal.`); export const OK = trim(`All systems nominal.`);

View File

@ -38,6 +38,7 @@ export class RawControls extends React.Component<Props, {}> {
getWebAppConfigVal={this.props.getWebAppConfigVal} /> getWebAppConfigVal={this.props.getWebAppConfigVal} />
peripherals = () => <Peripherals peripherals = () => <Peripherals
firmwareHardware={this.props.firmwareHardware}
bot={this.props.bot} bot={this.props.bot}
peripherals={this.props.peripherals} peripherals={this.props.peripherals}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
@ -50,6 +51,7 @@ export class RawControls extends React.Component<Props, {}> {
sensors = () => this.hideSensors sensors = () => this.hideSensors
? <div id="hidden-sensors-widget" /> ? <div id="hidden-sensors-widget" />
: <Sensors : <Sensors
firmwareHardware={this.props.firmwareHardware}
bot={this.props.bot} bot={this.props.bot}
sensors={this.props.sensors} sensors={this.props.sensors}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}

View File

@ -5,7 +5,7 @@ import { bot } from "../../../__test_support__/fake_state/bot";
import { PeripheralsProps } from "../../../devices/interfaces"; import { PeripheralsProps } from "../../../devices/interfaces";
import { fakePeripheral } from "../../../__test_support__/fake_state/resources"; import { fakePeripheral } from "../../../__test_support__/fake_state/resources";
import { clickButton } from "../../../__test_support__/helpers"; import { clickButton } from "../../../__test_support__/helpers";
import { SpecialStatus } from "farmbot"; import { SpecialStatus, FirmwareHardware } from "farmbot";
import { error } from "../../../toast/toast"; import { error } from "../../../toast/toast";
describe("<Peripherals />", () => { describe("<Peripherals />", () => {
@ -14,7 +14,8 @@ describe("<Peripherals />", () => {
bot, bot,
peripherals: [fakePeripheral()], peripherals: [fakePeripheral()],
dispatch: jest.fn(), dispatch: jest.fn(),
disabled: false disabled: false,
firmwareHardware: undefined,
}; };
} }
@ -73,11 +74,18 @@ describe("<Peripherals />", () => {
expect(p.dispatch).toHaveBeenCalled(); expect(p.dispatch).toHaveBeenCalled();
}); });
it("adds farmduino peripherals", () => { it.each<[FirmwareHardware, number]>([
["arduino", 2],
["farmduino", 5],
["farmduino_k14", 5],
["farmduino_k15", 5],
["express_k10", 3],
])("adds peripherals: %s", (firmware, expectedAdds) => {
const p = fakeProps(); const p = fakeProps();
p.firmwareHardware = firmware;
const wrapper = mount(<Peripherals {...p} />); const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true }); wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "farmduino"); clickButton(wrapper, 3, "stock");
expect(p.dispatch).toHaveBeenCalledTimes(5); expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
}); });
}); });

View File

@ -56,12 +56,31 @@ export class Peripherals
this.props.dispatch(init("Peripheral", { pin, label })); this.props.dispatch(init("Peripheral", { pin, label }));
}; };
farmduinoPeripherals = () => { get stockPeripherals() {
this.newPeripheral(7, t("Lighting")); switch (this.props.firmwareHardware) {
this.newPeripheral(8, t("Water")); case "arduino":
this.newPeripheral(9, t("Vacuum")); return [
this.newPeripheral(10, t("Peripheral ") + "4"); { pin: 8, label: t("Water") },
this.newPeripheral(12, t("Peripheral ") + "5"); { pin: 9, label: t("Vacuum") },
];
case "farmduino":
case "farmduino_k14":
case "farmduino_k15":
default:
return [
{ pin: 7, label: t("Lighting") },
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
{ pin: 10, label: t("Peripheral ") + "4" },
{ pin: 12, label: t("Peripheral ") + "5" },
];
case "express_k10":
return [
{ pin: 7, label: t("Lighting") },
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
];
}
} }
render() { render() {
@ -92,10 +111,11 @@ export class Peripherals
hidden={!isEditing} hidden={!isEditing}
className="fb-button green" className="fb-button green"
type="button" type="button"
onClick={this.farmduinoPeripherals}> onClick={() => this.stockPeripherals.map(p =>
this.newPeripheral(p.pin, p.label))}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} /> <i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
Farmduino {t("Stock")}
</button> </button>
</WidgetHeader> </WidgetHeader>
<WidgetBody> <WidgetBody>
{this.showPins()} {this.showPins()}

View File

@ -18,7 +18,8 @@ describe("<Sensors />", () => {
bot, bot,
sensors: [fakeSensor1, fakeSensor2], sensors: [fakeSensor1, fakeSensor2],
dispatch: jest.fn(), dispatch: jest.fn(),
disabled: false disabled: false,
firmwareHardware: undefined,
}; };
} }
@ -68,8 +69,16 @@ describe("<Sensors />", () => {
it("adds stock sensors", () => { it("adds stock sensors", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = mount(<Sensors {...p} />); const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
wrapper.setState({ isEditing: true }); wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "stock sensors"); clickButton(wrapper, 3, "stock sensors");
expect(p.dispatch).toHaveBeenCalledTimes(2); expect(p.dispatch).toHaveBeenCalledTimes(2);
}); });
it("doesn't display + stock button", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
});
}); });

View File

@ -100,4 +100,11 @@ describe("<SensorList/>", function () {
readSensorBtn.last().simulate("click"); readSensorBtn.last().simulate("click");
expect(mockDevice.readPin).not.toHaveBeenCalled(); expect(mockDevice.readPin).not.toHaveBeenCalled();
}); });
it("renders analog reading", () => {
const p = fakeProps();
p.pins[50] && (p.pins[50].value = 600);
const wrapper = mount(<SensorList {...p} />);
expect(wrapper.html()).toContain("margin-left: -3.5rem");
});
}); });

View File

@ -10,6 +10,7 @@ import { saveAll, init } from "../../api/crud";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { uniq } from "lodash"; import { uniq } from "lodash";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
export class Sensors extends React.Component<SensorsProps, SensorState> { export class Sensors extends React.Component<SensorsProps, SensorState> {
constructor(props: SensorsProps) { constructor(props: SensorsProps) {
@ -79,14 +80,15 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
onClick={() => this.newSensor()}> onClick={() => this.newSensor()}>
<i className="fa fa-plus" /> <i className="fa fa-plus" />
</button> </button>
<button {!isExpressBoard(this.props.firmwareHardware) &&
hidden={!isEditing} <button
className="fb-button green" hidden={!isEditing}
type="button" className="fb-button green"
onClick={this.stockSensors}> type="button"
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} /> onClick={this.stockSensors}>
{t("Stock sensors")} <i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
</button> {t("Stock sensors")}
</button>}
</WidgetHeader> </WidgetHeader>
<WidgetBody> <WidgetBody>
{this.showPins()} {this.showPins()}

View File

@ -1,5 +1,6 @@
// Padding for the popups. // Padding for the popups.
.bp3-popover-content { .bp3-popover-content {
z-index: 999;
padding: 1rem; padding: 1rem;
} }

View File

@ -1629,3 +1629,23 @@ textarea:focus {
} }
} }
} }
.section {
display: block !important;
}
.highlight,
.unhighlight {
display: flex;
}
.highlight {
background-color: $light_yellow;
box-shadow: 0px 0px 7px 4px $light_yellow;
}
.unhighlight {
transition: background-color 10s linear, box-shadow 10s linear;
background-color: transparent;
box-shadow: none;
}

View File

@ -60,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> {
return <div className="demo-container"> return <div className="demo-container">
<video muted={true} autoPlay={true} loop={true} className="demo-video"> <video muted={true} autoPlay={true} loop={true} className="demo-video">
<source src={ExternalUrl.Videos.desktop} type="video/mp4" /> <source src={ExternalUrl.Video.desktop} type="video/mp4" />
</video> </video>
<img className="demo-phone" src={ExternalUrl.Videos.mobile} /> <img className="demo-phone" src={ExternalUrl.Video.mobile} />
<button className="demo-button" onClick={this.requestAccount}> <button className="demo-button" onClick={this.requestAccount}>
{this.state.stage} {this.state.stage}
</button> </button>

View File

@ -7,13 +7,14 @@ import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions"; import { settingToggle } from "../../actions";
import { bot } from "../../../__test_support__/fake_state/bot"; import { bot } from "../../../__test_support__/fake_state/bot";
import { BooleanMCUInputGroupProps } from "../interfaces"; import { BooleanMCUInputGroupProps } from "../interfaces";
import { DeviceSetting } from "../../../constants";
describe("BooleanMCUInputGroup", () => { describe("BooleanMCUInputGroup", () => {
const fakeProps = (): BooleanMCUInputGroupProps => ({ const fakeProps = (): BooleanMCUInputGroupProps => ({
sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }), sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }),
dispatch: jest.fn(), dispatch: jest.fn(),
tooltip: "Tooltip", tooltip: "Tooltip",
name: "Name", label: DeviceSetting.invertEncoders,
x: "encoder_invert_x", x: "encoder_invert_x",
y: "encoder_invert_y", y: "encoder_invert_y",
z: "encoder_invert_z", z: "encoder_invert_z",

View File

@ -22,6 +22,8 @@ import axios from "axios";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { edit } from "../../../api/crud"; import { edit } from "../../../api/crud";
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources"; import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
import { formEvent } from "../../../__test_support__/fake_html_events";
import { Content } from "../../../constants";
describe("<FarmbotOsSettings />", () => { describe("<FarmbotOsSettings />", () => {
beforeEach(() => { beforeEach(() => {
@ -54,8 +56,8 @@ describe("<FarmbotOsSettings />", () => {
const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />); const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />);
expect(osSettings.find("input").length).toBe(1); expect(osSettings.find("input").length).toBe(1);
expect(osSettings.find("button").length).toBe(7); expect(osSettings.find("button").length).toBe(7);
["NAME", "TIME ZONE", "FARMBOT OS", "CAMERA", "FIRMWARE"] ["name", "time zone", "farmbot os", "camera", "firmware"]
.map(string => expect(osSettings.text()).toContain(string)); .map(string => expect(osSettings.text().toLowerCase()).toContain(string));
}); });
it("fetches OS release notes", async () => { it("fetches OS release notes", async () => {
@ -115,4 +117,18 @@ describe("<FarmbotOsSettings />", () => {
const osSettings = shallow(<FarmbotOsSettings {...p} />); const osSettings = shallow(<FarmbotOsSettings {...p} />);
expect(osSettings.find("BootSequenceSelector").length).toEqual(1); expect(osSettings.find("BootSequenceSelector").length).toEqual(1);
}); });
it("prevents default form submit action", () => {
const osSettings = shallow(<FarmbotOsSettings {...fakeProps()} />);
const e = formEvent();
osSettings.find("form").simulate("submit", e);
expect(e.preventDefault).toHaveBeenCalled();
});
it("warns about timezone mismatch", () => {
const p = fakeProps();
p.deviceAccount.body.timezone = "different";
const osSettings = mount(<FarmbotOsSettings {...p} />);
expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING);
});
}); });

View File

@ -1,4 +1,5 @@
import { boardType } from "../firmware_hardware_support"; import { boardType, getFwHardwareValue } from "../firmware_hardware_support";
import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources";
describe("boardType()", () => { describe("boardType()", () => {
it("returns Farmduino", () => { it("returns Farmduino", () => {
@ -32,3 +33,18 @@ describe("boardType()", () => {
expect(boardType("none")).toEqual("none"); expect(boardType("none")).toEqual("none");
}); });
}); });
describe("getFwHardwareValue()", () => {
it("returns undefined", () => {
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "wrong";
expect(getFwHardwareValue(fbosConfig)).toEqual(undefined);
expect(getFwHardwareValue(undefined)).toEqual(undefined);
});
it("returns real value", () => {
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "express_k10";
expect(getFwHardwareValue(fbosConfig)).toEqual("express_k10");
});
});

View File

@ -0,0 +1,81 @@
jest.mock("../../actions", () => ({
toggleControlPanel: jest.fn(),
}));
import * as React from "react";
import { mount } from "enzyme";
import {
Highlight, HighlightProps, maybeHighlight, maybeOpenPanel, highlight
} from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { panelState } from "../../../__test_support__/control_panel_state";
import { toggleControlPanel } from "../../actions";
describe("<Highlight />", () => {
const fakeProps = (): HighlightProps => ({
settingName: DeviceSetting.motors,
children: <div />,
className: "section",
});
it("fades highlight", () => {
const p = fakeProps();
const wrapper = mount<Highlight>(<Highlight {...p} />);
wrapper.setState({ className: "highlight" });
wrapper.instance().componentDidMount();
expect(wrapper.state().className).toEqual("unhighlight");
});
});
describe("maybeHighlight()", () => {
beforeEach(() => {
highlight.opened = false;
highlight.highlighted = false;
});
it("highlights only once", () => {
location.search = "?highlight=motors";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("highlight");
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
it("doesn't highlight: different setting", () => {
location.search = "?highlight=name";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
it("doesn't highlight: no matches", () => {
location.search = "?highlight=na";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
});
describe("maybeOpenPanel()", () => {
beforeEach(() => {
highlight.opened = false;
highlight.highlighted = false;
});
it("opens panel only once", () => {
location.search = "?highlight=motors";
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).toHaveBeenCalledWith("motors");
jest.resetAllMocks();
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("doesn't open panel: already open", () => {
location.search = "?highlight=motors";
const panels = panelState();
panels.motors = true;
maybeOpenPanel(panels)(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("doesn't open panel: no search term", () => {
location.search = "";
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
});

View File

@ -4,12 +4,14 @@ import { settingToggle } from "../actions";
import { Row, Col, Help } from "../../ui/index"; import { Row, Col, Help } from "../../ui/index";
import { BooleanMCUInputGroupProps } from "./interfaces"; import { BooleanMCUInputGroupProps } from "./interfaces";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { t } from "../../i18next_wrapper";
import { Highlight } from "./maybe_highlight";
export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const { const {
tooltip, tooltip,
name, label,
x, x,
y, y,
z, z,
@ -26,40 +28,42 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const zParam = sourceFwConfig(z); const zParam = sourceFwConfig(z);
return <Row> return <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={label}>
<label> <Col xs={6} className={"widget-body-tooltips"}>
{name} <label>
{caution && {t(label)}
<i className="fa fa-exclamation-triangle caution-icon" />} {caution &&
</label> <i className="fa fa-exclamation-triangle caution-icon" />}
<Help text={tooltip} requireClick={true} position={Position.RIGHT} /> </label>
</Col> <Help text={tooltip} requireClick={true} position={Position.RIGHT} />
<Col xs={2} className={"centered-button-div"}> </Col>
<ToggleButton <Col xs={2} className={"centered-button-div"}>
grayscale={grayscale?.x} <ToggleButton
disabled={disable?.x} grayscale={grayscale?.x}
dim={!xParam.consistent} disabled={disable?.x}
toggleValue={xParam.value} dim={!xParam.consistent}
toggleAction={() => toggleValue={xParam.value}
dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> toggleAction={() =>
</Col> dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
<Col xs={2} className={"centered-button-div"}> </Col>
<ToggleButton <Col xs={2} className={"centered-button-div"}>
grayscale={grayscale?.y} <ToggleButton
disabled={disable?.y} grayscale={grayscale?.y}
dim={!yParam.consistent} disabled={disable?.y}
toggleValue={yParam.value} dim={!yParam.consistent}
toggleAction={() => toggleValue={yParam.value}
dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> toggleAction={() =>
</Col> dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
<Col xs={2} className={"centered-button-div"}> </Col>
<ToggleButton <Col xs={2} className={"centered-button-div"}>
grayscale={grayscale?.z} <ToggleButton
disabled={disable?.z} grayscale={grayscale?.z}
dim={!zParam.consistent} disabled={disable?.z}
toggleValue={zParam.value} dim={!zParam.consistent}
toggleAction={() => toggleValue={zParam.value}
dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> toggleAction={() =>
</Col> dispatch(settingToggle(z, sourceFwConfig, displayAlert))} />
</Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -5,7 +5,7 @@ import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui"; import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
import { save, edit } from "../../api/crud"; import { save, edit } from "../../api/crud";
import { isBotOnline } from "../must_be_online"; import { isBotOnline } from "../must_be_online";
import { Content } from "../../constants"; import { Content, DeviceSetting } from "../../constants";
import { TimezoneSelector } from "../timezones/timezone_selector"; import { TimezoneSelector } from "../timezones/timezone_selector";
import { timezoneMismatch } from "../timezones/guess_timezone"; import { timezoneMismatch } from "../timezones/guess_timezone";
import { CameraSelection } from "./fbos_settings/camera_selection"; import { CameraSelection } from "./fbos_settings/camera_selection";
@ -16,6 +16,7 @@ import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector"; import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector";
import { ExternalUrl } from "../../external_urls"; import { ExternalUrl } from "../../external_urls";
import { Highlight } from "./maybe_highlight";
export enum ColWidth { export enum ColWidth {
label = 3, label = 3,
@ -85,34 +86,38 @@ export class FarmbotOsSettings
</WidgetHeader> </WidgetHeader>
<WidgetBody> <WidgetBody>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.name}>
<label> <Col xs={ColWidth.label}>
{t("NAME")} <label>
</label> {t(DeviceSetting.name)}
</Col> </label>
<Col xs={9}> </Col>
<input name="name" <Col xs={9}>
onChange={this.changeBot} <input name="name"
onBlur={this.updateBot} onChange={this.changeBot}
value={this.props.deviceAccount.body.name} /> onBlur={this.updateBot}
</Col> value={this.props.deviceAccount.body.name} />
</Col>
</Highlight>
</Row> </Row>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.timezone}>
<label> <Col xs={ColWidth.label}>
{t("TIME ZONE")} <label>
</label> {t("TIME ZONE")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<div className="note"> <Col xs={ColWidth.description}>
{this.maybeWarnTz()} <div className="note">
</div> {this.maybeWarnTz()}
<div> </div>
<TimezoneSelector <div>
currentTimezone={this.props.deviceAccount.body.timezone} <TimezoneSelector
onUpdate={this.handleTimezone} /> currentTimezone={this.props.deviceAccount.body.timezone}
</div> onUpdate={this.handleTimezone} />
</Col> </div>
</Col>
</Highlight>
</Row> </Row>
<CameraSelection <CameraSelection
env={this.props.env} env={this.props.env}

View File

@ -1,32 +1,35 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { updateConfig } from "../../actions"; import { updateConfig } from "../../actions";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { AutoSyncRowProps } from "./interfaces"; import { AutoSyncRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function AutoSyncRow(props: AutoSyncRowProps) { export function AutoSyncRow(props: AutoSyncRowProps) {
const autoSync = props.sourceFbosConfig("auto_sync"); const autoSync = props.sourceFbosConfig("auto_sync");
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.autoSync}>
<label> <Col xs={ColWidth.label}>
{t("AUTO SYNC")} <label>
</label> {t("AUTO SYNC")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.AUTO_SYNC)} <p>
</p> {t(Content.AUTO_SYNC)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<ToggleButton <Col xs={ColWidth.button}>
toggleValue={autoSync.value} <ToggleButton
dim={!autoSync.consistent} toggleValue={autoSync.value}
toggleAction={() => { dim={!autoSync.consistent}
props.dispatch(updateConfig({ auto_sync: !autoSync.value })); toggleAction={() => {
}} /> props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
</Col> }} />
</Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -3,10 +3,11 @@ import { Row, Col } from "../../../ui/index";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { updateConfig } from "../../actions"; import { updateConfig } from "../../actions";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { AutoUpdateRowProps } from "./interfaces"; import { AutoUpdateRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector"; import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector";
import { Highlight } from "../maybe_highlight";
export function AutoUpdateRow(props: AutoUpdateRowProps) { export function AutoUpdateRow(props: AutoUpdateRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
@ -18,23 +19,25 @@ export function AutoUpdateRow(props: AutoUpdateRowProps) {
value={props.device.body.ota_hour} value={props.device.body.ota_hour}
onChange={changeOtaHour(props.dispatch, props.device)} /> onChange={changeOtaHour(props.dispatch, props.device)} />
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<label> <Col xs={ColWidth.label}>
{t("FARMBOT OS AUTO UPDATE")} <label>
</label> {t(DeviceSetting.farmbotOSAutoUpdate)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.OS_AUTO_UPDATE)} <p>
</p> {t(Content.OS_AUTO_UPDATE)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<ToggleButton toggleValue={osAutoUpdate.value} <Col xs={ColWidth.button}>
dim={!osAutoUpdate.consistent} <ToggleButton toggleValue={osAutoUpdate.value}
toggleAction={() => props.dispatch(updateConfig({ dim={!osAutoUpdate.consistent}
os_auto_update: !osAutoUpdate.value toggleAction={() => props.dispatch(updateConfig({
}))} /> os_auto_update: !osAutoUpdate.value
</Col> }))} />
</Col>
</Highlight>
</Row> </Row>
</div>; </div>;
} }

View File

@ -10,6 +10,8 @@ import { FirmwareHardwareStatus } from "./firmware_hardware_status";
import { import {
isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI
} from "../firmware_hardware_support"; } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
interface BoardTypeState { sending: boolean } interface BoardTypeState { sending: boolean }
@ -47,30 +49,32 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
render() { render() {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.firmware}>
<label> <Col xs={ColWidth.label}>
{t("FIRMWARE")} <label>
</label> {t("FIRMWARE")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<div> <Col xs={ColWidth.description}>
<FBSelect <div>
key={this.apiValue} <FBSelect
extraClass={this.state.sending ? "dim" : ""} key={this.apiValue}
list={getFirmwareChoices()} extraClass={this.state.sending ? "dim" : ""}
selectedItem={this.selectedBoard} list={getFirmwareChoices()}
onChange={this.sendOffConfig} /> selectedItem={this.selectedBoard}
</div> onChange={this.sendOffConfig} />
</Col> </div>
<Col xs={ColWidth.button}> </Col>
<FirmwareHardwareStatus <Col xs={ColWidth.button}>
botOnline={this.props.botOnline} <FirmwareHardwareStatus
apiFirmwareValue={this.apiValue} botOnline={this.props.botOnline}
alerts={this.props.alerts} apiFirmwareValue={this.apiValue}
bot={this.props.bot} alerts={this.props.alerts}
dispatch={this.props.dispatch} bot={this.props.bot}
timeSettings={this.props.timeSettings} /> dispatch={this.props.dispatch}
</Col> timeSettings={this.props.timeSettings} />
</Col>
</Highlight>
</Row>; </Row>;
} }
} }

View File

@ -9,6 +9,8 @@ import { selectAllSequences, findSequenceById } from "../../../resources/selecto
import { betterCompact } from "../../../util"; import { betterCompact } from "../../../util";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
interface Props { interface Props {
list: DropDownItem[]; list: DropDownItem[];
@ -56,18 +58,20 @@ export class RawBootSequenceSelector extends React.Component<Props, {}> {
render() { render() {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.bootSequence}>
<label> <Col xs={ColWidth.label}>
{t("BOOT SEQUENCE")} <label>
</label> {t("BOOT SEQUENCE")}
</Col> </label>
<Col xs={7}> </Col>
<FBSelect <Col xs={7}>
allowEmpty={true} <FBSelect
list={this.props.list} allowEmpty={true}
selectedItem={this.props.selectedItem} list={this.props.list}
onChange={this.onChange} /> selectedItem={this.props.selectedItem}
</Col> onChange={this.onChange} />
</Col>
</Highlight>
</Row>; </Row>;
} }
} }

View File

@ -8,7 +8,8 @@ import { getDevice } from "../../../device";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { Feature, UserEnv } from "../../interfaces"; import { Feature, UserEnv } from "../../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Content, ToolTips } from "../../../constants"; import { Content, ToolTips, DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
/** Check if the camera has been disabled. */ /** Check if the camera has been disabled. */
export const cameraDisabled = (env: UserEnv): boolean => export const cameraDisabled = (env: UserEnv): boolean =>
@ -84,21 +85,23 @@ export class CameraSelection
render() { render() {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.camera}>
<label> <Col xs={ColWidth.label}>
{t("CAMERA")} <label>
</label> {t("CAMERA")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<div> <Col xs={ColWidth.description}>
<FBSelect <div>
allowEmpty={false} <FBSelect
list={CAMERA_CHOICES()} allowEmpty={false}
selectedItem={this.selectedCamera()} list={CAMERA_CHOICES()}
onChange={this.sendOffConfig} selectedItem={this.selectedCamera()}
extraClass={this.props.botOnline ? "" : "disabled"} /> onChange={this.sendOffConfig}
</div> extraClass={this.props.botOnline ? "" : "disabled"} />
</Col> </div>
</Col>
</Highlight>
</Row>; </Row>;
} }
} }

View File

@ -1,12 +1,13 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { factoryReset, updateConfig } from "../../actions"; import { factoryReset, updateConfig } from "../../actions";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { BotConfigInputBox } from "../bot_config_input_box"; import { BotConfigInputBox } from "../bot_config_input_box";
import { FactoryResetRowProps } from "./interfaces"; import { FactoryResetRowProps } from "./interfaces";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function FactoryResetRow(props: FactoryResetRowProps) { export function FactoryResetRow(props: FactoryResetRowProps) {
const { dispatch, sourceFbosConfig, botOnline } = props; const { dispatch, sourceFbosConfig, botOnline } = props;
@ -14,66 +15,72 @@ export function FactoryResetRow(props: FactoryResetRowProps) {
const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {}; const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {};
return <div> return <div>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.factoryReset}>
<label> <Col xs={ColWidth.label}>
{t("Factory Reset")} <label>
</label> {t(DeviceSetting.factoryReset)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.FACTORY_RESET_WARNING)} <p>
</p> {t(Content.FACTORY_RESET_WARNING)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<button <Col xs={ColWidth.button}>
className="fb-button red" <button
type="button" className="fb-button red"
onClick={factoryReset} type="button"
disabled={!botOnline}> onClick={factoryReset}
{t("FACTORY RESET")} disabled={!botOnline}>
</button> {t("FACTORY RESET")}
</Col> </button>
</Col>
</Highlight>
</Row> </Row>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.autoFactoryReset}>
<label> <Col xs={ColWidth.label}>
{t("Automatic Factory Reset")} <label>
</label> {t(DeviceSetting.autoFactoryReset)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.AUTO_FACTORY_RESET)} <p>
</p> {t(Content.AUTO_FACTORY_RESET)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<ToggleButton <Col xs={ColWidth.button}>
toggleValue={!disableFactoryReset.value} <ToggleButton
dim={!disableFactoryReset.consistent} toggleValue={!disableFactoryReset.value}
toggleAction={() => { dim={!disableFactoryReset.consistent}
dispatch(updateConfig({ toggleAction={() => {
disable_factory_reset: !disableFactoryReset.value dispatch(updateConfig({
})); disable_factory_reset: !disableFactoryReset.value
}} /> }));
</Col> }} />
</Col>
</Highlight>
</Row> </Row>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.connectionAttemptPeriod}>
<label style={maybeDisableTimer}> <Col xs={ColWidth.label}>
{t("Connection Attempt Period")} <label style={maybeDisableTimer}>
</label> {t(DeviceSetting.connectionAttemptPeriod)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p style={maybeDisableTimer}> <Col xs={ColWidth.description}>
{t(Content.AUTO_FACTORY_RESET_PERIOD)} <p style={maybeDisableTimer}>
</p> {t(Content.AUTO_FACTORY_RESET_PERIOD)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<BotConfigInputBox <Col xs={ColWidth.button}>
setting="network_not_found_timer" <BotConfigInputBox
dispatch={dispatch} setting="network_not_found_timer"
disabled={!!disableFactoryReset.value} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} /> disabled={!!disableFactoryReset.value}
</Col> sourceFbosConfig={sourceFbosConfig} />
</Col>
</Highlight>
</Row> </Row>
</div>; </div>;
} }

View File

@ -7,6 +7,8 @@ import { FarmbotOsRowProps } from "./interfaces";
import { FbosDetails } from "./fbos_details"; import { FbosDetails } from "./fbos_details";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { ErrorBoundary } from "../../../error_boundary"; import { ErrorBoundary } from "../../../error_boundary";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
const getVersionString = const getVersionString =
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => { (fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
@ -21,48 +23,50 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
} = bot.hardware.informational_settings; } = bot.hardware.informational_settings;
const version = getVersionString(controller_version, currently_on_beta); const version = getVersionString(controller_version, currently_on_beta);
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.farmbotOS}>
<label> <Col xs={ColWidth.label}>
{t("FARMBOT OS")} <label>
</label> {t(DeviceSetting.farmbotOS)}
</Col> </label>
<Col xs={3}> </Col>
<Popover position={Position.BOTTOM_LEFT}> <Col xs={3}>
<p> <Popover position={Position.BOTTOM_LEFT}>
{t("Version {{ version }}", { version })} <p>
</p> {t("Version {{ version }}", { version })}
<ErrorBoundary> </p>
<FbosDetails <ErrorBoundary>
botInfoSettings={bot.hardware.informational_settings} <FbosDetails
dispatch={dispatch} botInfoSettings={bot.hardware.informational_settings}
shouldDisplay={props.shouldDisplay} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} shouldDisplay={props.shouldDisplay}
botToMqttLastSeen={props.botToMqttLastSeen} sourceFbosConfig={sourceFbosConfig}
timeSettings={props.timeSettings} botToMqttLastSeen={props.botToMqttLastSeen}
deviceAccount={props.deviceAccount} /> timeSettings={props.timeSettings}
</ErrorBoundary> deviceAccount={props.deviceAccount} />
</Popover> </ErrorBoundary>
</Col> </Popover>
<Col xs={3}> </Col>
<Popover position={Position.BOTTOM}> <Col xs={3}>
<p className="release-notes-button"> <Popover position={Position.BOTTOM}>
{t("Release Notes")}&nbsp; <p className="release-notes-button">
{t("Release Notes")}&nbsp;
<i className="fa fa-caret-down" /> <i className="fa fa-caret-down" />
</p> </p>
<div className="release-notes"> <div className="release-notes">
<h1>{props.osReleaseNotesHeading}</h1> <h1>{props.osReleaseNotesHeading}</h1>
<Markdown> <Markdown>
{osReleaseNotes} {osReleaseNotes}
</Markdown> </Markdown>
</div> </div>
</Popover> </Popover>
</Col> </Col>
<Col xs={3}> <Col xs={3}>
<OsUpdateButton <OsUpdateButton
bot={bot} bot={bot}
sourceFbosConfig={sourceFbosConfig} sourceFbosConfig={sourceFbosConfig}
shouldDisplay={props.shouldDisplay} shouldDisplay={props.shouldDisplay}
botOnline={botOnline} /> botOnline={botOnline} />
</Col> </Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -2,10 +2,12 @@ import * as React from "react";
import { Row, Col } from "../../../ui"; import { Row, Col } from "../../../ui";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
export interface FbosButtonRowProps { export interface FbosButtonRowProps {
botOnline: boolean; botOnline: boolean;
label: string; label: DeviceSetting;
description: string; description: string;
buttonText: string; buttonText: string;
color: string; color: string;
@ -14,24 +16,26 @@ export interface FbosButtonRowProps {
export const FbosButtonRow = (props: FbosButtonRowProps) => { export const FbosButtonRow = (props: FbosButtonRowProps) => {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={props.label}>
<label> <Col xs={ColWidth.label}>
{t(props.label)} <label>
</label> {t(props.label)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(props.description)} <p>
</p> {t(props.description)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<button <Col xs={ColWidth.button}>
className={`fb-button ${props.color}`} <button
type="button" className={`fb-button ${props.color}`}
onClick={props.action} type="button"
disabled={!props.botOnline}> onClick={props.action}
{t(props.buttonText)} disabled={!props.botOnline}>
</button> {t(props.buttonText)}
</Col> </button>
</Col>
</Highlight>
</Row>; </Row>;
}; };

View File

@ -5,37 +5,39 @@ import { FactoryResetRow } from "./factory_reset_row";
import { PowerAndResetProps } from "./interfaces"; import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form"; import { ChangeOwnershipForm } from "./change_ownership_form";
import { FbosButtonRow } from "./fbos_button_row"; import { FbosButtonRow } from "./fbos_button_row";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions"; import { reboot, powerOff, restartFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function PowerAndReset(props: PowerAndResetProps) { export function PowerAndReset(props: PowerAndResetProps) {
const { dispatch, sourceFbosConfig, botOnline } = props; const { dispatch, sourceFbosConfig, botOnline } = props;
const { power_and_reset } = props.controlPanelState; const { power_and_reset } = props.controlPanelState;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.powerAndReset}>
<Header <Header
expanded={power_and_reset} expanded={power_and_reset}
title={t("Power and Reset")} title={DeviceSetting.powerAndReset}
name={"power_and_reset"} panel={"power_and_reset"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!power_and_reset}> <Collapse isOpen={!!power_and_reset}>
<FbosButtonRow <FbosButtonRow
botOnline={botOnline} botOnline={botOnline}
label={t("RESTART FARMBOT")} label={DeviceSetting.restartFarmbot}
description={Content.RESTART_FARMBOT} description={Content.RESTART_FARMBOT}
buttonText={t("RESTART")} buttonText={t("RESTART")}
color={"yellow"} color={"yellow"}
action={reboot} /> action={reboot} />
<FbosButtonRow <FbosButtonRow
botOnline={botOnline} botOnline={botOnline}
label={t("SHUTDOWN FARMBOT")} label={DeviceSetting.shutdownFarmbot}
description={Content.SHUTDOWN_FARMBOT} description={Content.SHUTDOWN_FARMBOT}
buttonText={t("SHUTDOWN")} buttonText={t("SHUTDOWN")}
color={"red"} color={"red"}
action={powerOff} /> action={powerOff} />
<FbosButtonRow <FbosButtonRow
botOnline={botOnline} botOnline={botOnline}
label={t("RESTART FIRMWARE")} label={DeviceSetting.restartFirmware}
description={Content.RESTART_FIRMWARE} description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")} buttonText={t("RESTART")}
color={"yellow"} color={"yellow"}
@ -45,14 +47,15 @@ export function PowerAndReset(props: PowerAndResetProps) {
sourceFbosConfig={sourceFbosConfig} sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} /> botOnline={botOnline} />
{botOnline && {botOnline &&
<Popover position={Position.BOTTOM_LEFT}> <Highlight settingName={DeviceSetting.changeOwnership}>
<p className={"release-notes-button"}> <Popover position={Position.BOTTOM_LEFT}>
{t("Change Ownership")}&nbsp; <p className={"release-notes-button"}>
<i className="fa fa-caret-down" /> {t(DeviceSetting.changeOwnership)}&nbsp;
</p> <i className="fa fa-caret-down" />
<ChangeOwnershipForm /> </p>
</Popover> <ChangeOwnershipForm />
} </Popover>
</Highlight>}
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,4 +1,4 @@
import { FirmwareHardware } from "farmbot"; import { FirmwareHardware, TaggedFbosConfig } from "farmbot";
export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
const values: FirmwareHardware[] = [ const values: FirmwareHardware[] = [
@ -10,6 +10,12 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
return !!values.includes(x as FirmwareHardware); return !!values.includes(x as FirmwareHardware);
}; };
export const getFwHardwareValue =
(fbosConfig: TaggedFbosConfig | undefined) => {
const value = fbosConfig?.body.firmware_hardware;
return isFwHardwareValue(value) ? value : undefined;
};
const TMC_BOARDS = ["express_k10", "farmduino_k15"]; const TMC_BOARDS = ["express_k10", "farmduino_k15"];
const EXPRESS_BOARDS = ["express_k10"]; const EXPRESS_BOARDS = ["express_k10"];

View File

@ -18,10 +18,14 @@ import { FwParamExportMenu } from "./hardware_settings/export_menu";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings"; import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling"; import { ErrorHandling } from "./hardware_settings/error_handling";
import { maybeOpenPanel } from "./maybe_highlight";
export class HardwareSettings extends export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> { React.Component<HardwareSettingsProps, {}> {
componentDidMount = () =>
this.props.dispatch(maybeOpenPanel(this.props.controlPanelState));
render() { render() {
const { const {
bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig, bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig,

View File

@ -3,6 +3,7 @@ import { mount } from "enzyme";
import { CalibrationRow } from "../calibration_row"; import { CalibrationRow } from "../calibration_row";
import { bot } from "../../../../__test_support__/fake_state/bot"; import { bot } from "../../../../__test_support__/fake_state/bot";
import { CalibrationRowProps } from "../../interfaces"; import { CalibrationRowProps } from "../../interfaces";
import { DeviceSetting } from "../../../../constants";
describe("<CalibrationRow />", () => { describe("<CalibrationRow />", () => {
const fakeProps = (): CalibrationRowProps => ({ const fakeProps = (): CalibrationRowProps => ({
@ -11,7 +12,7 @@ describe("<CalibrationRow />", () => {
botDisconnected: false, botDisconnected: false,
action: jest.fn(), action: jest.fn(),
toolTip: "calibrate", toolTip: "calibrate",
title: "calibrate", title: DeviceSetting.calibration,
axisTitle: "calibrate", axisTitle: "calibrate",
}); });

View File

@ -1,16 +1,17 @@
import * as React from "react"; import * as React from "react";
import { Header } from "../header"; import { Header } from "../header";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { DeviceSetting } from "../../../../constants";
describe("<Header/>", () => { describe("<Header/>", () => {
it("renders", () => { it("renders", () => {
const fn = jest.fn(); const fn = jest.fn();
const el = mount(<Header const el = mount(<Header
title="FOO" title={DeviceSetting.motors}
expanded={true} expanded={true}
name={"motors"} panel={"motors"}
dispatch={fn} />); dispatch={fn} />);
expect(el.text()).toContain("FOO"); expect(el.text().toLowerCase()).toContain("motors");
expect(el.find(".fa-minus").length).toBe(1); expect(el.find(".fa-minus").length).toBe(1);
}); });
}); });

View File

@ -5,30 +5,33 @@ import { Row, Col, Help } from "../../../ui/index";
import { CalibrationRowProps } from "../interfaces"; import { CalibrationRowProps } from "../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { Highlight } from "../maybe_highlight";
export function CalibrationRow(props: CalibrationRowProps) { export function CalibrationRow(props: CalibrationRowProps) {
const { hardware, botDisconnected } = props; const { hardware, botDisconnected } = props;
return <Row> return <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={props.title}>
<label> <Col xs={6} className={"widget-body-tooltips"}>
{t(props.title)} <label>
</label> {t(props.title)}
<Help text={t(props.toolTip)} </label>
requireClick={true} position={Position.RIGHT} /> <Help text={t(props.toolTip)}
</Col> requireClick={true} position={Position.RIGHT} />
{axisTrackingStatus(hardware) </Col>
.map(row => { {axisTrackingStatus(hardware)
const { axis } = row; .map(row => {
const hardwareDisabled = props.type == "zero" ? false : row.disabled; const { axis } = row;
return <Col xs={2} key={axis} className={"centered-button-div"}> const hardwareDisabled = props.type == "zero" ? false : row.disabled;
<LockableButton return <Col xs={2} key={axis} className={"centered-button-div"}>
disabled={hardwareDisabled || botDisconnected} <LockableButton
onClick={() => props.action(axis)}> disabled={hardwareDisabled || botDisconnected}
{`${t(props.axisTitle)} ${axis}`} onClick={() => props.action(axis)}>
</LockableButton> {`${t(props.axisTitle)} ${axis}`}
</Col>; </LockableButton>
})} </Col>;
})}
</Highlight>
</Row>; </Row>;
} }

View File

@ -3,41 +3,45 @@ import { DangerZoneProps } from "../interfaces";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function DangerZone(props: DangerZoneProps) { export function DangerZone(props: DangerZoneProps) {
const { dispatch, onReset, botDisconnected } = props; const { dispatch, onReset, botDisconnected } = props;
const { danger_zone } = props.controlPanelState; const { danger_zone } = props.controlPanelState;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.dangerZone}>
<Header <Header
expanded={danger_zone} expanded={danger_zone}
title={t("Danger Zone")} title={DeviceSetting.dangerZone}
name={"danger_zone"} panel={"danger_zone"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!danger_zone}> <Collapse isOpen={!!danger_zone}>
<Row> <Row>
<Col xs={4}> <Highlight settingName={DeviceSetting.resetHardwareParams}>
<label> <Col xs={4}>
{t("Reset hardware parameter defaults")} <label>
</label> {t(DeviceSetting.resetHardwareParams)}
</Col> </label>
<Col xs={6}> </Col>
<p> <Col xs={6}>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} <p>
</p> {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</Col> </p>
<Col xs={2} className={"centered-button-div"}> </Col>
<button <Col xs={2} className={"centered-button-div"}>
className="fb-button red" <button
disabled={botDisconnected} className="fb-button red"
onClick={onReset}> disabled={botDisconnected}
{t("RESET")} onClick={onReset}>
</button> {t("RESET")}
</Col> </button>
</Col>
</Highlight>
</Row> </Row>
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,12 +1,12 @@
import * as React from "react"; import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces"; import { EncodersProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { isExpressBoard } from "../firmware_hardware_support"; import { isExpressBoard } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
export function Encoders(props: EncodersProps) { export function Encoders(props: EncodersProps) {
@ -20,19 +20,20 @@ export function Encoders(props: EncodersProps) {
}; };
const isExpress = isExpressBoard(firmwareHardware); const isExpress = isExpressBoard(firmwareHardware);
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.encoders}>
<Header <Header
expanded={encoders} expanded={encoders}
title={isExpress title={isExpress
? t("Stall Detection") ? DeviceSetting.stallDetection
: t("Encoders")} : DeviceSetting.encoders}
name={"encoders"} panel={"encoders"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!encoders}> <Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={isExpress label={isExpress
? t("Enable Stall Detection") ? DeviceSetting.enableStallDetection
: t("Enable Encoders")} : DeviceSetting.enableEncoders}
tooltip={isExpress tooltip={isExpress
? ToolTips.ENABLE_STALL_DETECTION ? ToolTips.ENABLE_STALL_DETECTION
: ToolTips.ENABLE_ENCODERS} : ToolTips.ENABLE_ENCODERS}
@ -43,7 +44,7 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
{isExpress && {isExpress &&
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Stall Sensitivity")} label={DeviceSetting.stallSensitivity}
tooltip={ToolTips.STALL_SENSITIVITY} tooltip={ToolTips.STALL_SENSITIVITY}
x={"movement_stall_sensitivity_x"} x={"movement_stall_sensitivity_x"}
y={"movement_stall_sensitivity_y"} y={"movement_stall_sensitivity_y"}
@ -53,7 +54,7 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} />} sourceFwConfig={sourceFwConfig} />}
{!isExpress && {!isExpress &&
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Use Encoders for Positioning")} label={DeviceSetting.useEncodersForPositioning}
tooltip={ToolTips.ENCODER_POSITIONING} tooltip={ToolTips.ENCODER_POSITIONING}
x={"encoder_use_for_pos_x"} x={"encoder_use_for_pos_x"}
y={"encoder_use_for_pos_y"} y={"encoder_use_for_pos_y"}
@ -63,7 +64,7 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} />} sourceFwConfig={sourceFwConfig} />}
{!isExpress && {!isExpress &&
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Invert Encoders")} label={DeviceSetting.invertEncoders}
tooltip={ToolTips.INVERT_ENCODERS} tooltip={ToolTips.INVERT_ENCODERS}
x={"encoder_invert_x"} x={"encoder_invert_x"}
y={"encoder_invert_y"} y={"encoder_invert_y"}
@ -72,7 +73,7 @@ export function Encoders(props: EncodersProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />} sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Max Missed Steps")} label={DeviceSetting.maxMissedSteps}
tooltip={isExpress tooltip={isExpress
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION ? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
: ToolTips.MAX_MISSED_STEPS_ENCODERS} : ToolTips.MAX_MISSED_STEPS_ENCODERS}
@ -83,7 +84,7 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Missed Step Decay")} label={DeviceSetting.missedStepDecay}
tooltip={ToolTips.MISSED_STEP_DECAY} tooltip={ToolTips.MISSED_STEP_DECAY}
x={"encoder_missed_steps_decay_x"} x={"encoder_missed_steps_decay_x"}
y={"encoder_missed_steps_decay_y"} y={"encoder_missed_steps_decay_y"}
@ -93,7 +94,7 @@ export function Encoders(props: EncodersProps) {
dispatch={dispatch} /> dispatch={dispatch} />
{!isExpress && {!isExpress &&
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Encoder Scaling")} label={DeviceSetting.encoderScaling}
tooltip={ToolTips.ENCODER_SCALING} tooltip={ToolTips.ENCODER_SCALING}
x={"encoder_scaling_x"} x={"encoder_scaling_x"}
y={"encoder_scaling_y"} y={"encoder_scaling_y"}
@ -106,5 +107,5 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />} dispatch={dispatch} />}
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,25 +1,26 @@
import * as React from "react"; import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { EndStopsProps } from "../interfaces"; import { EndStopsProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper"; import { Highlight } from "../maybe_highlight";
export function EndStops(props: EndStopsProps) { export function EndStops(props: EndStopsProps) {
const { endstops } = props.controlPanelState; const { endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props; const { dispatch, sourceFwConfig } = props;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.endstops}>
<Header <Header
expanded={endstops} expanded={endstops}
title={"Endstops"} title={DeviceSetting.endstops}
name={"endstops"} panel={"endstops"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!endstops}> <Collapse isOpen={!!endstops}>
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Enable Endstops")} label={DeviceSetting.enableEndstops}
tooltip={ToolTips.ENABLE_ENDSTOPS} tooltip={ToolTips.ENABLE_ENDSTOPS}
x={"movement_enable_endpoints_x"} x={"movement_enable_endpoints_x"}
y={"movement_enable_endpoints_y"} y={"movement_enable_endpoints_y"}
@ -27,7 +28,7 @@ export function EndStops(props: EndStopsProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Swap Endstops")} label={DeviceSetting.swapEndstops}
tooltip={ToolTips.SWAP_ENDPOINTS} tooltip={ToolTips.SWAP_ENDPOINTS}
x={"movement_invert_endpoints_x"} x={"movement_invert_endpoints_x"}
y={"movement_invert_endpoints_y"} y={"movement_invert_endpoints_y"}
@ -40,7 +41,7 @@ export function EndStops(props: EndStopsProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Invert Endstops")} label={DeviceSetting.invertEndstops}
tooltip={ToolTips.INVERT_ENDPOINTS} tooltip={ToolTips.INVERT_ENDPOINTS}
x={"movement_invert_2_endpoints_x"} x={"movement_invert_2_endpoints_x"}
y={"movement_invert_2_endpoints_y"} y={"movement_invert_2_endpoints_y"}
@ -53,5 +54,5 @@ export function EndStops(props: EndStopsProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,14 +1,14 @@
import * as React from "react"; import * as React from "react";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { ErrorHandlingProps } from "../interfaces"; import { ErrorHandlingProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { McuInputBox } from "../mcu_input_box"; import { McuInputBox } from "../mcu_input_box";
import { settingToggle } from "../../actions"; import { settingToggle } from "../../actions";
import { SingleSettingRow } from "./single_setting_row"; import { SingleSettingRow } from "./single_setting_row";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { Highlight } from "../maybe_highlight";
export function ErrorHandling(props: ErrorHandlingProps) { export function ErrorHandling(props: ErrorHandlingProps) {
@ -16,15 +16,16 @@ export function ErrorHandling(props: ErrorHandlingProps) {
const { dispatch, sourceFwConfig } = props; const { dispatch, sourceFwConfig } = props;
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.errorHandling}>
<Header <Header
expanded={error_handling} expanded={error_handling}
title={"Error Handling"} title={DeviceSetting.errorHandling}
name={"error_handling"} panel={"error_handling"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!error_handling}> <Collapse isOpen={!!error_handling}>
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Timeout after (seconds)")} label={DeviceSetting.timeoutAfter}
tooltip={ToolTips.TIMEOUT_AFTER} tooltip={ToolTips.TIMEOUT_AFTER}
x={"movement_timeout_x"} x={"movement_timeout_x"}
y={"movement_timeout_y"} y={"movement_timeout_y"}
@ -32,7 +33,7 @@ export function ErrorHandling(props: ErrorHandlingProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<SingleSettingRow settingType="input" <SingleSettingRow settingType="input"
label={t("Max Retries")} label={DeviceSetting.maxRetries}
tooltip={ToolTips.MAX_MOVEMENT_RETRIES}> tooltip={ToolTips.MAX_MOVEMENT_RETRIES}>
<McuInputBox <McuInputBox
setting="param_mov_nr_retry" setting="param_mov_nr_retry"
@ -40,7 +41,7 @@ export function ErrorHandling(props: ErrorHandlingProps) {
dispatch={dispatch} /> dispatch={dispatch} />
</SingleSettingRow> </SingleSettingRow>
<SingleSettingRow settingType="button" <SingleSettingRow settingType="button"
label={t("E-Stop on Movement Error")} label={DeviceSetting.estopOnMovementError}
tooltip={ToolTips.E_STOP_ON_MOV_ERR}> tooltip={ToolTips.E_STOP_ON_MOV_ERR}>
<ToggleButton <ToggleButton
toggleValue={eStopOnMoveError.value} toggleValue={eStopOnMoveError.value}
@ -49,5 +50,5 @@ export function ErrorHandling(props: ErrorHandlingProps) {
settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} /> settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} />
</SingleSettingRow> </SingleSettingRow>
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -2,18 +2,20 @@ import * as React from "react";
import { ControlPanelState } from "../../interfaces"; import { ControlPanelState } from "../../interfaces";
import { toggleControlPanel } from "../../actions"; import { toggleControlPanel } from "../../actions";
import { ExpandableHeader } from "../../../ui/expandable_header"; import { ExpandableHeader } from "../../../ui/expandable_header";
import { t } from "../../../i18next_wrapper";
import { DeviceSetting } from "../../../constants";
interface Props { interface Props {
dispatch: Function; dispatch: Function;
name: keyof ControlPanelState; panel: keyof ControlPanelState;
title: string; title: DeviceSetting;
expanded: boolean; expanded: boolean;
} }
export const Header = (props: Props) => { export const Header = (props: Props) => {
const { dispatch, name, title, expanded } = props; const { dispatch, panel, title, expanded } = props;
return <ExpandableHeader return <ExpandableHeader
expanded={expanded} expanded={expanded}
title={title} title={t(title)}
onClick={() => dispatch(toggleControlPanel(name))} />; onClick={() => dispatch(toggleControlPanel(panel))} />;
}; };

View File

@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { CalibrationRow } from "./calibration_row"; import { CalibrationRow } from "./calibration_row";
import { disabledAxisMap } from "../axis_tracking_status"; import { disabledAxisMap } from "../axis_tracking_status";
@ -13,6 +13,7 @@ import { isExpressBoard } from "../firmware_hardware_support";
import { getDevice } from "../../../device"; import { getDevice } from "../../../device";
import { commandErr } from "../../actions"; import { commandErr } from "../../actions";
import { CONFIG_DEFAULTS } from "farmbot/dist/config"; import { CONFIG_DEFAULTS } from "farmbot/dist/config";
import { Highlight } from "../maybe_highlight";
export function HomingAndCalibration(props: HomingAndCalibrationProps) { export function HomingAndCalibration(props: HomingAndCalibrationProps) {
@ -31,16 +32,17 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const scale = calculateScale(sourceFwConfig); const scale = calculateScale(sourceFwConfig);
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.homingAndCalibration}>
<Header <Header
title={t("Homing and Calibration")} title={DeviceSetting.homingAndCalibration}
name={"homing_and_calibration"} panel={"homing_and_calibration"}
dispatch={dispatch} dispatch={dispatch}
expanded={homing_and_calibration} /> expanded={homing_and_calibration} />
<Collapse isOpen={!!homing_and_calibration}> <Collapse isOpen={!!homing_and_calibration}>
<CalibrationRow <CalibrationRow
type={"find_home"} type={"find_home"}
title={t("HOMING")} title={DeviceSetting.homing}
axisTitle={t("FIND HOME")} axisTitle={t("FIND HOME")}
toolTip={isExpressBoard(firmwareHardware) toolTip={isExpressBoard(firmwareHardware)
? ToolTips.HOMING_STALL_DETECTION ? ToolTips.HOMING_STALL_DETECTION
@ -52,7 +54,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
botDisconnected={botDisconnected} /> botDisconnected={botDisconnected} />
<CalibrationRow <CalibrationRow
type={"calibrate"} type={"calibrate"}
title={t("CALIBRATION")} title={DeviceSetting.calibration}
axisTitle={t("CALIBRATE")} axisTitle={t("CALIBRATE")}
toolTip={isExpressBoard(firmwareHardware) toolTip={isExpressBoard(firmwareHardware)
? ToolTips.CALIBRATION_STALL_DETECTION ? ToolTips.CALIBRATION_STALL_DETECTION
@ -63,7 +65,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
botDisconnected={botDisconnected} /> botDisconnected={botDisconnected} />
<CalibrationRow <CalibrationRow
type={"zero"} type={"zero"}
title={t("SET ZERO POSITION")} title={DeviceSetting.setZeroPosition}
axisTitle={t("ZERO")} axisTitle={t("ZERO")}
toolTip={ToolTips.SET_ZERO_POSITION} toolTip={ToolTips.SET_ZERO_POSITION}
action={axis => getDevice().setZero(axis) action={axis => getDevice().setZero(axis)
@ -71,7 +73,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
hardware={hardware} hardware={hardware}
botDisconnected={botDisconnected} /> botDisconnected={botDisconnected} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Find Home on Boot")} label={DeviceSetting.findHomeOnBoot}
tooltip={isExpressBoard(firmwareHardware) tooltip={isExpressBoard(firmwareHardware)
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION ? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS} : ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
@ -83,7 +85,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
caution={true} /> caution={true} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Stop at Home")} label={DeviceSetting.stopAtHome}
tooltip={ToolTips.STOP_AT_HOME} tooltip={ToolTips.STOP_AT_HOME}
x={"movement_stop_at_home_x"} x={"movement_stop_at_home_x"}
y={"movement_stop_at_home_y"} y={"movement_stop_at_home_y"}
@ -91,7 +93,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Stop at Max")} label={DeviceSetting.stopAtMax}
tooltip={ToolTips.STOP_AT_MAX} tooltip={ToolTips.STOP_AT_MAX}
x={"movement_stop_at_max_x"} x={"movement_stop_at_max_x"}
y={"movement_stop_at_max_y"} y={"movement_stop_at_max_y"}
@ -99,7 +101,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Negative Coordinates Only")} label={DeviceSetting.negativeCoordinatesOnly}
tooltip={ToolTips.NEGATIVE_COORDINATES_ONLY} tooltip={ToolTips.NEGATIVE_COORDINATES_ONLY}
x={"movement_home_up_x"} x={"movement_home_up_x"}
y={"movement_home_up_y"} y={"movement_home_up_y"}
@ -107,7 +109,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Axis Length (mm)")} label={DeviceSetting.axisLength}
tooltip={ToolTips.LENGTH} tooltip={ToolTips.LENGTH}
x={"movement_axis_nr_steps_x"} x={"movement_axis_nr_steps_x"}
y={"movement_axis_nr_steps_y"} y={"movement_axis_nr_steps_y"}
@ -124,5 +126,5 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
intSize={"long"} /> intSize={"long"} />
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,18 +1,18 @@
import * as React from "react"; import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions"; import { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { MotorsProps } from "../interfaces"; import { MotorsProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { Xyz, McuParamName } from "farmbot"; import { Xyz, McuParamName } from "farmbot";
import { SourceFwConfig } from "../../interfaces"; import { SourceFwConfig } from "../../interfaces";
import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props"; import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props";
import { isTMCBoard } from "../firmware_hardware_support"; import { isTMCBoard } from "../firmware_hardware_support";
import { SingleSettingRow } from "./single_setting_row"; import { SingleSettingRow } from "./single_setting_row";
import { Highlight } from "../maybe_highlight";
export const calculateScale = export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => { (sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
@ -35,15 +35,16 @@ export function Motors(props: MotorsProps) {
const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x");
const scale = calculateScale(sourceFwConfig); const scale = calculateScale(sourceFwConfig);
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.motors}>
<Header <Header
expanded={controlPanelState.motors} expanded={controlPanelState.motors}
title={t("Motors")} title={DeviceSetting.motors}
name={"motors"} panel={"motors"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!controlPanelState.motors}> <Collapse isOpen={!!controlPanelState.motors}>
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Max Speed (mm/s)")} label={DeviceSetting.maxSpeed}
tooltip={ToolTips.MAX_SPEED} tooltip={ToolTips.MAX_SPEED}
x={"movement_max_spd_x"} x={"movement_max_spd_x"}
y={"movement_max_spd_y"} y={"movement_max_spd_y"}
@ -54,7 +55,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Homing Speed (mm/s)")} label={DeviceSetting.homingSpeed}
tooltip={ToolTips.HOME_SPEED} tooltip={ToolTips.HOME_SPEED}
x={"movement_home_spd_x"} x={"movement_home_spd_x"}
y={"movement_home_spd_y"} y={"movement_home_spd_y"}
@ -65,7 +66,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Minimum Speed (mm/s)")} label={DeviceSetting.minimumSpeed}
tooltip={ToolTips.MIN_SPEED} tooltip={ToolTips.MIN_SPEED}
x={"movement_min_spd_x"} x={"movement_min_spd_x"}
y={"movement_min_spd_y"} y={"movement_min_spd_y"}
@ -76,7 +77,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Accelerate for (mm)")} label={DeviceSetting.accelerateFor}
tooltip={ToolTips.ACCELERATE_FOR} tooltip={ToolTips.ACCELERATE_FOR}
x={"movement_steps_acc_dec_x"} x={"movement_steps_acc_dec_x"}
y={"movement_steps_acc_dec_y"} y={"movement_steps_acc_dec_y"}
@ -87,7 +88,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Steps per MM")} label={DeviceSetting.stepsPerMm}
tooltip={ToolTips.STEPS_PER_MM} tooltip={ToolTips.STEPS_PER_MM}
x={"movement_step_per_mm_x"} x={"movement_step_per_mm_x"}
y={"movement_step_per_mm_y"} y={"movement_step_per_mm_y"}
@ -99,7 +100,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={props.sourceFwConfig} sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} /> dispatch={props.dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Microsteps per step")} label={DeviceSetting.microstepsPerStep}
tooltip={ToolTips.MICROSTEPS_PER_STEP} tooltip={ToolTips.MICROSTEPS_PER_STEP}
x={"movement_microsteps_x"} x={"movement_microsteps_x"}
y={"movement_microsteps_y"} y={"movement_microsteps_y"}
@ -107,7 +108,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={props.sourceFwConfig} sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} /> dispatch={props.dispatch} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Always Power Motors")} label={DeviceSetting.alwaysPowerMotors}
tooltip={ToolTips.ALWAYS_POWER_MOTORS} tooltip={ToolTips.ALWAYS_POWER_MOTORS}
x={"movement_keep_active_x"} x={"movement_keep_active_x"}
y={"movement_keep_active_y"} y={"movement_keep_active_y"}
@ -115,7 +116,7 @@ export function Motors(props: MotorsProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Invert Motors")} label={DeviceSetting.invertMotors}
tooltip={ToolTips.INVERT_MOTORS} tooltip={ToolTips.INVERT_MOTORS}
x={"movement_invert_motor_x"} x={"movement_invert_motor_x"}
y={"movement_invert_motor_y"} y={"movement_invert_motor_y"}
@ -124,7 +125,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
{isTMCBoard(firmwareHardware) && {isTMCBoard(firmwareHardware) &&
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Motor Current")} label={DeviceSetting.motorCurrent}
tooltip={ToolTips.MOTOR_CURRENT} tooltip={ToolTips.MOTOR_CURRENT}
x={"movement_motor_current_x"} x={"movement_motor_current_x"}
y={"movement_motor_current_y"} y={"movement_motor_current_y"}
@ -132,7 +133,7 @@ export function Motors(props: MotorsProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />} sourceFwConfig={sourceFwConfig} />}
<SingleSettingRow settingType="button" <SingleSettingRow settingType="button"
label={t("Enable 2nd X Motor")} label={DeviceSetting.enable2ndXMotor}
tooltip={ToolTips.ENABLE_X2_MOTOR}> tooltip={ToolTips.ENABLE_X2_MOTOR}>
<ToggleButton <ToggleButton
toggleValue={enable2ndXMotor.value} toggleValue={enable2ndXMotor.value}
@ -141,7 +142,7 @@ export function Motors(props: MotorsProps) {
settingToggle("movement_secondary_motor_x", sourceFwConfig))} /> settingToggle("movement_secondary_motor_x", sourceFwConfig))} />
</SingleSettingRow> </SingleSettingRow>
<SingleSettingRow settingType="button" <SingleSettingRow settingType="button"
label={t("Invert 2nd X Motor")} label={DeviceSetting.invert2ndXMotor}
tooltip={ToolTips.INVERT_MOTORS}> tooltip={ToolTips.INVERT_MOTORS}>
<ToggleButton <ToggleButton
grayscale={!enable2ndXMotor.value} grayscale={!enable2ndXMotor.value}
@ -151,5 +152,5 @@ export function Motors(props: MotorsProps) {
settingToggle("movement_secondary_motor_invert_x", sourceFwConfig))} /> settingToggle("movement_secondary_motor_invert_x", sourceFwConfig))} />
</SingleSettingRow> </SingleSettingRow>
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -3,20 +3,23 @@ import { PinBindingsProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { PinBindingsContent } from "../../pin_bindings/pin_bindings"; import { PinBindingsContent } from "../../pin_bindings/pin_bindings";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
export function PinBindings(props: PinBindingsProps) { export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState; const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props; const { dispatch, resources } = props;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.pinBindings}>
<Header <Header
expanded={pin_bindings} expanded={pin_bindings}
title={"Pin Bindings"} title={DeviceSetting.pinBindings}
name={"pin_bindings"} panel={"pin_bindings"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}> <Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} /> <PinBindingsContent dispatch={dispatch} resources={resources} />
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -4,19 +4,21 @@ import { PinGuardProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse, Position } from "@blueprintjs/core"; import { Collapse, Position } from "@blueprintjs/core";
import { Row, Col, Help } from "../../../ui/index"; import { Row, Col, Help } from "../../../ui/index";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function PinGuard(props: PinGuardProps) { export function PinGuard(props: PinGuardProps) {
const { pin_guard } = props.controlPanelState; const { pin_guard } = props.controlPanelState;
const { dispatch, sourceFwConfig, resources } = props; const { dispatch, sourceFwConfig, resources } = props;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.pinGuard}>
<Header <Header
expanded={pin_guard} expanded={pin_guard}
title={t("Pin Guard")} title={DeviceSetting.pinGuard}
name={"pin_guard"} panel={"pin_guard"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!pin_guard}> <Collapse isOpen={!!pin_guard}>
<Row> <Row>
@ -79,5 +81,5 @@ export function PinGuard(props: PinGuardProps) {
resources={resources} resources={resources}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,20 +1,27 @@
import * as React from "react"; import * as React from "react";
import { Row, Col, Help } from "../../../ui/index"; import { Row, Col, Help } from "../../../ui/index";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
import { t } from "../../../i18next_wrapper";
export interface SingleSettingRowProps {
label: DeviceSetting;
tooltip: string;
children: React.ReactChild;
settingType: "button" | "input";
}
export const SingleSettingRow = export const SingleSettingRow =
({ label, tooltip, settingType, children }: { ({ label, tooltip, settingType, children }: SingleSettingRowProps) =>
label: string,
tooltip: string,
children: React.ReactChild,
settingType: "button" | "input",
}) =>
<Row> <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={label}>
<label>{label}</label> <Col xs={6} className={"widget-body-tooltips"}>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} /> <label>{t(label)}</label>
</Col> <Help text={tooltip} requireClick={true} position={Position.RIGHT} />
{settingType === "button" </Col>
? <Col xs={2} className={"centered-button-div"}>{children}</Col> {settingType === "button"
: <Col xs={6}>{children}</Col>} ? <Col xs={2} className={"centered-button-div"}>{children}</Col>
: <Col xs={6}>{children}</Col>}
</Highlight>
</Row>; </Row>;

View File

@ -6,6 +6,7 @@ import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist";
import { IntegerSize } from "../../util"; import { IntegerSize } from "../../util";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { ResourceIndex } from "../../resources/interfaces"; import { ResourceIndex } from "../../resources/interfaces";
import { DeviceSetting } from "../../constants";
export interface ZeroRowProps { export interface ZeroRowProps {
botDisconnected: boolean; botDisconnected: boolean;
@ -25,7 +26,7 @@ export interface BooleanMCUInputGroupProps {
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
dispatch: Function; dispatch: Function;
tooltip: string; tooltip: string;
name: string; label: DeviceSetting;
x: McuParamName; x: McuParamName;
y: McuParamName; y: McuParamName;
z: McuParamName; z: McuParamName;
@ -41,7 +42,7 @@ export interface CalibrationRowProps {
botDisconnected: boolean; botDisconnected: boolean;
action(axis: Axis): void; action(axis: Axis): void;
toolTip: string; toolTip: string;
title: string; title: DeviceSetting;
axisTitle: string; axisTitle: string;
} }
@ -49,7 +50,7 @@ export interface NumericMCUInputGroupProps {
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
dispatch: Function; dispatch: Function;
tooltip: string; tooltip: string;
name: string; label: DeviceSetting;
x: McuParamName; x: McuParamName;
xScale?: number; xScale?: number;
y: McuParamName; y: McuParamName;

View File

@ -0,0 +1,162 @@
import * as React from "react";
import { ControlPanelState } from "../interfaces";
import { toggleControlPanel } from "../actions";
import { urlFriendly } from "../../util";
import { DeviceSetting } from "../../constants";
const HOMING_PANEL = [
DeviceSetting.homingAndCalibration,
DeviceSetting.homing,
DeviceSetting.calibration,
DeviceSetting.setZeroPosition,
DeviceSetting.findHomeOnBoot,
DeviceSetting.stopAtHome,
DeviceSetting.stopAtMax,
DeviceSetting.negativeCoordinatesOnly,
DeviceSetting.axisLength,
];
const MOTORS_PANEL = [
DeviceSetting.motors,
DeviceSetting.maxSpeed,
DeviceSetting.homingSpeed,
DeviceSetting.minimumSpeed,
DeviceSetting.accelerateFor,
DeviceSetting.stepsPerMm,
DeviceSetting.microstepsPerStep,
DeviceSetting.alwaysPowerMotors,
DeviceSetting.invertMotors,
DeviceSetting.motorCurrent,
DeviceSetting.enable2ndXMotor,
DeviceSetting.invert2ndXMotor,
];
const ENCODERS_PANEL = [
DeviceSetting.encoders,
DeviceSetting.stallDetection,
DeviceSetting.enableEncoders,
DeviceSetting.enableStallDetection,
DeviceSetting.stallSensitivity,
DeviceSetting.useEncodersForPositioning,
DeviceSetting.invertEncoders,
DeviceSetting.maxMissedSteps,
DeviceSetting.missedStepDecay,
DeviceSetting.encoderScaling,
];
const ENDSTOPS_PANEL = [
DeviceSetting.endstops,
DeviceSetting.enableEndstops,
DeviceSetting.swapEndstops,
DeviceSetting.invertEndstops,
];
const ERROR_HANDLING_PANEL = [
DeviceSetting.errorHandling,
DeviceSetting.timeoutAfter,
DeviceSetting.maxRetries,
DeviceSetting.estopOnMovementError,
];
const PIN_GUARD_PANEL = [
DeviceSetting.pinGuard,
];
const DANGER_ZONE_PANEL = [
DeviceSetting.dangerZone,
DeviceSetting.resetHardwareParams,
];
const PIN_BINDINGS_PANEL = [
DeviceSetting.pinBindings,
];
const POWER_AND_RESET_PANEL = [
DeviceSetting.powerAndReset,
DeviceSetting.restartFarmbot,
DeviceSetting.shutdownFarmbot,
DeviceSetting.restartFirmware,
DeviceSetting.factoryReset,
DeviceSetting.autoFactoryReset,
DeviceSetting.connectionAttemptPeriod,
DeviceSetting.changeOwnership,
];
/** Look up parent panels for settings. */
const SETTING_PANEL_LOOKUP = {} as Record<DeviceSetting, keyof ControlPanelState>;
HOMING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "homing_and_calibration");
MOTORS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "motors");
ENCODERS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "encoders");
ENDSTOPS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "endstops");
ERROR_HANDLING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "error_handling");
PIN_GUARD_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_guard");
DANGER_ZONE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "danger_zone");
PIN_BINDINGS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_bindings");
POWER_AND_RESET_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "power_and_reset");
/** Look up parent panels for settings using URL-friendly names. */
const URL_FRIENDLY_LOOKUP: Record<string, keyof ControlPanelState> = {};
Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) =>
URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel);
/** Look up all relevant names for the same setting. */
const ALTERNATE_NAMES =
Object.values(DeviceSetting).reduce((acc, s) => { acc[s] = [s]; return acc; },
{} as Record<DeviceSetting, DeviceSetting[]>);
ALTERNATE_NAMES[DeviceSetting.encoders].push(DeviceSetting.stallDetection);
ALTERNATE_NAMES[DeviceSetting.stallDetection].push(DeviceSetting.encoders);
/** Generate array of names for the same setting. Most only have one. */
const compareValues = (settingName: DeviceSetting) =>
(ALTERNATE_NAMES[settingName]).map(s => urlFriendly(s));
/** Retrieve a highlight search term. */
const getHighlightName = () => location.search.split("?highlight=").pop();
/** Only open panel and highlight once per app load. Exported for tests. */
export const highlight = { opened: false, highlighted: false };
/** Open a panel if a setting in that panel is highlighted. */
export const maybeOpenPanel = (panelState: ControlPanelState) =>
(dispatch: Function) => {
if (highlight.opened) { return; }
const urlFriendlySettingName = urlFriendly(getHighlightName() || "");
if (!urlFriendlySettingName) { return; }
const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName];
const panelIsOpen = panelState[panel];
if (panelIsOpen) { return; }
dispatch(toggleControlPanel(panel));
highlight.opened = true;
};
/** Highlight a setting if provided as a search term. */
export const maybeHighlight = (settingName: DeviceSetting) => {
const item = getHighlightName();
if (highlight.highlighted || !item) { return ""; }
const isCurrentSetting = compareValues(settingName).includes(item);
if (!isCurrentSetting) { return ""; }
highlight.highlighted = true;
return "highlight";
};
export interface HighlightProps {
settingName: DeviceSetting;
children: React.ReactChild
| React.ReactChild[]
| (React.ReactChild | React.ReactChild[])[];
className?: string;
}
interface HighlightState {
className: string;
}
/** Wrap highlight-able settings. */
export class Highlight extends React.Component<HighlightProps, HighlightState> {
state: HighlightState = { className: maybeHighlight(this.props.settingName) };
componentDidMount = () => {
if (this.state.className == "highlight") {
/** Slowly fades highlight. */
this.setState({ className: "unhighlight" });
}
}
render() {
return <div className={`${this.props.className} ${this.state.className}`}>
{this.props.children}
</div>;
}
}

View File

@ -3,48 +3,52 @@ import { McuInputBox } from "./mcu_input_box";
import { NumericMCUInputGroupProps } from "./interfaces"; import { NumericMCUInputGroupProps } from "./interfaces";
import { Row, Col, Help } from "../../ui/index"; import { Row, Col, Help } from "../../ui/index";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { Highlight } from "./maybe_highlight";
import { t } from "../../i18next_wrapper";
export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) { export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
const { const {
sourceFwConfig, dispatch, tooltip, name, x, y, z, intSize, gray, float, sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float,
} = props; } = props;
return <Row> return <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={label}>
<label> <Col xs={6} className={"widget-body-tooltips"}>
{name} <label>
</label> {t(label)}
<Help text={tooltip} requireClick={true} position={Position.RIGHT} /> </label>
</Col> <Help text={tooltip} requireClick={true} position={Position.RIGHT} />
<Col xs={2}> </Col>
<McuInputBox <Col xs={2}>
setting={x} <McuInputBox
sourceFwConfig={sourceFwConfig} setting={x}
dispatch={dispatch} sourceFwConfig={sourceFwConfig}
intSize={intSize} dispatch={dispatch}
float={float} intSize={intSize}
scale={props.xScale} float={float}
gray={gray?.x} /> scale={props.xScale}
</Col> gray={gray?.x} />
<Col xs={2}> </Col>
<McuInputBox <Col xs={2}>
setting={y} <McuInputBox
sourceFwConfig={sourceFwConfig} setting={y}
dispatch={dispatch} sourceFwConfig={sourceFwConfig}
intSize={intSize} dispatch={dispatch}
float={float} intSize={intSize}
scale={props.yScale} float={float}
gray={gray?.y} /> scale={props.yScale}
</Col> gray={gray?.y} />
<Col xs={2}> </Col>
<McuInputBox <Col xs={2}>
setting={z} <McuInputBox
sourceFwConfig={sourceFwConfig} setting={z}
dispatch={dispatch} sourceFwConfig={sourceFwConfig}
intSize={intSize} dispatch={dispatch}
float={float} intSize={intSize}
scale={props.zScale} float={float}
gray={gray?.z} /> scale={props.zScale}
</Col> gray={gray?.z} />
</Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -201,6 +201,7 @@ export interface PeripheralsProps {
peripherals: TaggedPeripheral[]; peripherals: TaggedPeripheral[];
dispatch: Function; dispatch: Function;
disabled: boolean | undefined; disabled: boolean | undefined;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface SensorsProps { export interface SensorsProps {
@ -208,6 +209,7 @@ export interface SensorsProps {
sensors: TaggedSensor[]; sensors: TaggedSensor[];
dispatch: Function; dispatch: Function;
disabled: boolean | undefined; disabled: boolean | undefined;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface FarmwareProps { export interface FarmwareProps {
@ -248,8 +250,8 @@ export interface ControlPanelState {
encoders: boolean; encoders: boolean;
endstops: boolean; endstops: boolean;
error_handling: boolean; error_handling: boolean;
pin_bindings: boolean;
danger_zone: boolean;
power_and_reset: boolean;
pin_guard: boolean; pin_guard: boolean;
danger_zone: boolean;
pin_bindings: boolean;
power_and_reset: boolean;
} }

View File

@ -89,17 +89,17 @@ export const piSpi1Pins = [16, 17, 18, 19, 20, 21];
/** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */ /** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */
export const reservedPiGPIO = piI2c0Pins; export const reservedPiGPIO = piI2c0Pins;
const LabeledGpioPins: { [x: number]: string } = { const GPIO_PIN_LABELS = (): { [x: number]: string } => ({
[ButtonPin.estop]: "Button 1: E-STOP", [ButtonPin.estop]: t("Button {{ num }}: E-STOP", { num: 1 }),
[ButtonPin.unlock]: "Button 2: UNLOCK", [ButtonPin.unlock]: t("Button {{ num }}: UNLOCK", { num: 2 }),
[ButtonPin.btn3]: "Button 3", [ButtonPin.btn3]: t("Button {{ num }})", { num: 3 }),
[ButtonPin.btn4]: "Button 4", [ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }),
[ButtonPin.btn5]: "Button 5", [ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }),
}; });
export const generatePinLabel = (pin: number) => export const generatePinLabel = (pin: number) =>
LabeledGpioPins[pin] GPIO_PIN_LABELS()[pin]
? `${LabeledGpioPins[pin]} (Pi ${pin})` ? `${t(GPIO_PIN_LABELS()[pin])} (Pi ${pin})`
: `Pi GPIO ${pin}`; : `Pi GPIO ${pin}`;
/** Raspberry Pi GPIO pin numbers. */ /** Raspberry Pi GPIO pin numbers. */

View File

@ -20,10 +20,11 @@ export namespace ExternalUrl {
const GITHUB_API = "https://api.github.com"; const GITHUB_API = "https://api.github.com";
const OPENFARM = "https://openfarm.cc"; const OPENFARM = "https://openfarm.cc";
const SOFTWARE_DOCS = "https://software.farm.bot"; const SOFTWARE_DOCS = "https://software.farm.bot";
const FORUM = "http://forum.farmbot.org"; const FORUM = "https://forum.farmbot.org";
const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files"; const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files";
const FBOS_RAW = `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}`; const FBOS_RAW =
`${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/staging`;
export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`; export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`;
export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`; export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`;
@ -31,8 +32,7 @@ export namespace ExternalUrl {
`${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`; `${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`;
export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`; export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`;
export const webAppRepo = export const webAppRepo = `${gitHubFarmBot}/${FarmBotRepo.FarmBotWebApp}`;
`${GITHUB}/${Org.FarmBot}/${FarmBotRepo.FarmBotWebApp}`;
export const softwareDocs = `${SOFTWARE_DOCS}/docs`; export const softwareDocs = `${SOFTWARE_DOCS}/docs`;
export const softwareForum = `${FORUM}/c/software`; export const softwareForum = `${FORUM}/c/software`;
@ -43,7 +43,7 @@ export namespace ExternalUrl {
export const newCrop = `${OPENFARM}/en/crops/new`; export const newCrop = `${OPENFARM}/en/crops/new`;
} }
export namespace Videos { export namespace Video {
export const desktop = export const desktop =
`${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`; `${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`;
export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`; export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`;

View File

@ -386,6 +386,7 @@ export class GardenMap extends
visible={!!this.props.showPlants} visible={!!this.props.showPlants}
plants={this.props.plants} plants={this.props.plants}
currentPlant={this.getPlant()} currentPlant={this.getPlant()}
hoveredPlant={this.props.hoveredPlant}
dragging={!!this.state.isDragging} dragging={!!this.state.isDragging}
editing={this.isEditing} editing={this.isEditing}
boxSelected={this.props.designer.selectedPlants} boxSelected={this.props.designer.selectedPlants}

View File

@ -14,6 +14,7 @@ export type TaggedPlant = TaggedPlantPointer | TaggedPlantTemplate;
export interface PlantLayerProps { export interface PlantLayerProps {
plants: TaggedPlant[]; plants: TaggedPlant[];
currentPlant: TaggedPlant | undefined; currentPlant: TaggedPlant | undefined;
hoveredPlant: TaggedPlant | undefined;
dragging: boolean; dragging: boolean;
editing: boolean; editing: boolean;
visible: boolean; visible: boolean;
@ -57,12 +58,14 @@ export interface GardenPlantProps {
dispatch: Function; dispatch: Function;
plant: Readonly<TaggedPlant>; plant: Readonly<TaggedPlant>;
selected: boolean; selected: boolean;
current: boolean;
editing: boolean; editing: boolean;
dragging: boolean; dragging: boolean;
zoomLvl: number; zoomLvl: number;
activeDragXY: BotPosition | undefined; activeDragXY: BotPosition | undefined;
uuid: string; uuid: string;
animate: boolean; animate: boolean;
hovered: boolean;
} }
export interface GardenPlantState { export interface GardenPlantState {

View File

@ -15,17 +15,19 @@ describe("<BotFigure/>", () => {
plantAreaOffset: { x: 100, y: 100 }, plantAreaOffset: { x: 100, y: 100 },
}); });
const EXPECTED_MOTORS_OPACITY = 0.5;
it.each<[ it.each<[
string, BotOriginQuadrant, Record<"x" | "y", number>, boolean, number string, BotOriginQuadrant, Record<"x" | "y", number>, boolean, number
]>([ ]>([
["motors", 1, { x: 3000, y: 0 }, false, 0.75], ["motors", 1, { x: 3000, y: 0 }, false, EXPECTED_MOTORS_OPACITY],
["motors", 2, { x: 0, y: 0 }, false, 0.75], ["motors", 2, { x: 0, y: 0 }, false, EXPECTED_MOTORS_OPACITY],
["motors", 3, { x: 0, y: 1500 }, false, 0.75], ["motors", 3, { x: 0, y: 1500 }, false, EXPECTED_MOTORS_OPACITY],
["motors", 4, { x: 3000, y: 1500 }, false, 0.75], ["motors", 4, { x: 3000, y: 1500 }, false, EXPECTED_MOTORS_OPACITY],
["motors", 1, { x: 0, y: 1500 }, true, 0.75], ["motors", 1, { x: 0, y: 1500 }, true, EXPECTED_MOTORS_OPACITY],
["motors", 2, { x: 0, y: 0 }, true, 0.75], ["motors", 2, { x: 0, y: 0 }, true, EXPECTED_MOTORS_OPACITY],
["motors", 3, { x: 3000, y: 0 }, true, 0.75], ["motors", 3, { x: 3000, y: 0 }, true, EXPECTED_MOTORS_OPACITY],
["motors", 4, { x: 3000, y: 1500 }, true, 0.75], ["motors", 4, { x: 3000, y: 1500 }, true, EXPECTED_MOTORS_OPACITY],
["encoders", 2, { x: 0, y: 0 }, false, 0.25], ["encoders", 2, { x: 0, y: 0 }, false, 0.25],
])("shows %s in correct location for quadrant %i", ])("shows %s in correct location for quadrant %i",
(name, quadrant, expected, xySwap, opacity) => { (name, quadrant, expected, xySwap, opacity) => {

View File

@ -31,7 +31,7 @@ export class BotFigure extends
const positionQ = transformXY( const positionQ = transformXY(
(position.x || 0), (position.y || 0), mapTransformProps); (position.x || 0), (position.y || 0), mapTransformProps);
const color = eStopStatus ? Color.virtualRed : Color.darkGray; const color = eStopStatus ? Color.virtualRed : Color.darkGray;
const opacity = name.includes("encoder") ? 0.25 : 0.75; const opacity = name.includes("encoder") ? 0.25 : 0.5;
return <g id={name}> return <g id={name}>
<rect id="gantry" <rect id="gantry"
x={xySwap ? -plantAreaOffset.x : positionQ.qx - 10} x={xySwap ? -plantAreaOffset.x : positionQ.qx - 10}

View File

@ -13,6 +13,7 @@ describe("<GardenPlant/>", () => {
return { return {
mapTransformProps: fakeMapTransformProps(), mapTransformProps: fakeMapTransformProps(),
plant: fakePlant(), plant: fakePlant(),
current: false,
selected: false, selected: false,
editing: false, editing: false,
dragging: false, dragging: false,
@ -21,6 +22,7 @@ describe("<GardenPlant/>", () => {
activeDragXY: { x: undefined, y: undefined, z: undefined }, activeDragXY: { x: undefined, y: undefined, z: undefined },
uuid: "plantUuid", uuid: "plantUuid",
animate: false, animate: false,
hovered: false,
}; };
} }
@ -31,6 +33,8 @@ describe("<GardenPlant/>", () => {
const wrapper = shallow(<GardenPlant {...p} />); const wrapper = shallow(<GardenPlant {...p} />);
expect(wrapper.find("image").length).toEqual(1); expect(wrapper.find("image").length).toEqual(1);
expect(wrapper.find("image").props().opacity).toEqual(1); expect(wrapper.find("image").props().opacity).toEqual(1);
expect(wrapper.find("image").props().visibility).toEqual("visible");
expect(wrapper.find("image").props().opacity).toEqual(1.0);
expect(wrapper.find("text").length).toEqual(0); expect(wrapper.find("text").length).toEqual(0);
expect(wrapper.find("rect").length).toBeLessThanOrEqual(1); expect(wrapper.find("rect").length).toBeLessThanOrEqual(1);
expect(wrapper.find("use").length).toEqual(0); expect(wrapper.find("use").length).toEqual(0);
@ -88,4 +92,21 @@ describe("<GardenPlant/>", () => {
expect(wrapper.find(".plant-indicator").length).toEqual(1); expect(wrapper.find(".plant-indicator").length).toEqual(1);
expect(wrapper.find("Circle").length).toEqual(1); expect(wrapper.find("Circle").length).toEqual(1);
}); });
it("doesn't render indicator circle twice", () => {
const p = fakeProps();
p.selected = true;
p.hovered = true;
const wrapper = shallow(<GardenPlant {...p} />);
expect(wrapper.find(".plant-indicator").length).toEqual(0);
expect(wrapper.find("Circle").length).toEqual(0);
});
it("renders while dragging", () => {
const p = fakeProps();
p.dragging = true;
const wrapper = shallow(<GardenPlant {...p} />);
expect(wrapper.find("image").props().visibility).toEqual("hidden");
expect(wrapper.find("image").props().opacity).toEqual(0.4);
});
}); });

View File

@ -8,11 +8,13 @@ import { PlantLayer } from "../plant_layer";
import { import {
fakePlant, fakePlantTemplate fakePlant, fakePlantTemplate
} from "../../../../../__test_support__/fake_state/resources"; } from "../../../../../__test_support__/fake_state/resources";
import { PlantLayerProps, GardenPlantProps } from "../../../interfaces"; import { PlantLayerProps } from "../../../interfaces";
import { import {
fakeMapTransformProps fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props"; } from "../../../../../__test_support__/map_transform_props";
import { svgMount } from "../../../../../__test_support__/svg_mount"; import { svgMount } from "../../../../../__test_support__/svg_mount";
import { shallow } from "enzyme";
import { GardenPlant } from "../garden_plant";
describe("<PlantLayer/>", () => { describe("<PlantLayer/>", () => {
const fakeProps = (): PlantLayerProps => ({ const fakeProps = (): PlantLayerProps => ({
@ -28,6 +30,7 @@ describe("<PlantLayer/>", () => {
zoomLvl: 1, zoomLvl: 1,
activeDragXY: { x: undefined, y: undefined, z: undefined }, activeDragXY: { x: undefined, y: undefined, z: undefined },
animate: true, animate: true,
hoveredPlant: undefined,
}); });
it("shows plants", () => { it("shows plants", () => {
@ -88,14 +91,14 @@ describe("<PlantLayer/>", () => {
.toEqual("/app/designer/gardens/templates/5"); .toEqual("/app/designer/gardens/templates/5");
}); });
it("has selected plant", () => { it("has hovered plant", () => {
mockPath = "/app/designer/plants"; mockPath = "/app/designer/plants";
const p = fakeProps(); const p = fakeProps();
const plant = fakePlant(); const plant = fakePlant();
p.plants = [plant]; p.plants = [plant];
p.currentPlant = plant; p.hoveredPlant = plant;
const wrapper = svgMount(<PlantLayer {...p} />); const wrapper = shallow(<PlantLayer {...p} />);
expect(wrapper.find("GardenPlant").props().selected).toEqual(true); expect(wrapper.find(GardenPlant).props().hovered).toEqual(true);
}); });
it("has plant selected by selection box", () => { it("has plant selected by selection box", () => {
@ -105,8 +108,7 @@ describe("<PlantLayer/>", () => {
p.plants = [plant]; p.plants = [plant];
p.boxSelected = [plant.uuid]; p.boxSelected = [plant.uuid];
const wrapper = svgMount(<PlantLayer {...p} />); const wrapper = svgMount(<PlantLayer {...p} />);
expect((wrapper.find("GardenPlant").props() as GardenPlantProps).selected) expect(wrapper.find("GardenPlant").props().selected).toEqual(true);
.toEqual(true);
}); });
it("allows clicking of unsaved plants", () => { it("allows clicking of unsaved plants", () => {

View File

@ -36,22 +36,22 @@ export class GardenPlant extends
}; };
get radius() { get radius() {
const { selected, plant } = this.props; const { plant } = this.props;
const { hover } = this.state; const { hover } = this.state;
const { radius } = plant.body; const { radius } = plant.body;
return (hover && !selected) ? radius * 1.1 : radius; return hover ? radius * 1.1 : radius;
} }
render() { render() {
const { selected, dragging, plant, mapTransformProps, const { current, selected, dragging, plant, mapTransformProps,
activeDragXY, zoomLvl, animate, editing } = this.props; activeDragXY, zoomLvl, animate, editing, hovered } = this.props;
const { id, radius, x, y } = plant.body; const { id, radius, x, y } = plant.body;
const { icon } = this.state; const { icon } = this.state;
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps); const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
const alpha = dragging ? 0.4 : 1.0; const alpha = dragging ? 0.4 : 1.0;
const className = [ const className = [
"plant-image", `is-chosen-${selected}`, animate ? "animate" : "" "plant-image", `is-chosen-${current || selected}`, animate ? "animate" : ""
].join(" "); ].join(" ");
return <g id={"plant-" + id}> return <g id={"plant-" + id}>
@ -65,7 +65,7 @@ export class GardenPlant extends
fill={Color.soilCloud} fill={Color.soilCloud}
fillOpacity={0} />} fillOpacity={0} />}
{selected && !editing && {(current || selected) && !editing && !hovered &&
<g id="selected-plant-indicator"> <g id="selected-plant-indicator">
<Circle <Circle
className={`plant-indicator ${animate ? "animate" : ""}`} className={`plant-indicator ${animate ? "animate" : ""}`}

View File

@ -19,11 +19,13 @@ export function PlantLayer(props: PlantLayerProps) {
boxSelected, boxSelected,
groupSelected, groupSelected,
animate, animate,
hoveredPlant,
} = props; } = props;
return <g id="plant-layer"> return <g id="plant-layer">
{visible && plants.map(p => { {visible && plants.map(p => {
const selected = !!(p.uuid === currentPlant?.uuid); const current = p.uuid === currentPlant?.uuid;
const hovered = p.uuid === hoveredPlant?.uuid;
const selectedByBox = !!boxSelected?.includes(p.uuid); const selectedByBox = !!boxSelected?.includes(p.uuid);
const selectedByGroup = groupSelected.includes(p.uuid); const selectedByGroup = groupSelected.includes(p.uuid);
const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate" const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate"
@ -33,12 +35,14 @@ export function PlantLayer(props: PlantLayerProps) {
uuid={p.uuid} uuid={p.uuid}
mapTransformProps={mapTransformProps} mapTransformProps={mapTransformProps}
plant={p} plant={p}
selected={selected || selectedByBox || selectedByGroup} selected={selectedByBox || selectedByGroup}
current={current}
editing={editing} editing={editing}
dragging={selected && dragging && editing} dragging={current && dragging && editing}
dispatch={dispatch} dispatch={dispatch}
zoomLvl={zoomLvl} zoomLvl={zoomLvl}
activeDragXY={activeDragXY} activeDragXY={activeDragXY}
hovered={hovered}
animate={animate} />; animate={animate} />;
const wrapperProps = { const wrapperProps = {
className: "plant-link-wrapper", className: "plant-link-wrapper",

View File

@ -66,12 +66,12 @@ describe("<ToolSlotPoint/>", () => {
expect(wrapper.find("text").props().dx).toEqual(-40); expect(wrapper.find("text").props().dx).toEqual(-40);
}); });
it("displays 'no tool'", () => { it("displays 'empty'", () => {
const p = fakeProps(); const p = fakeProps();
p.slot.tool = undefined; p.slot.tool = undefined;
p.hoveredToolSlot = p.slot.toolSlot.uuid; p.hoveredToolSlot = p.slot.toolSlot.uuid;
const wrapper = svgMount(<ToolSlotPoint {...p} />); const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("text").text()).toEqual("no tool"); expect(wrapper.find("text").text()).toEqual("empty");
expect(wrapper.find("text").props().dx).toEqual(40); expect(wrapper.find("text").props().dx).toEqual(40);
}); });

View File

@ -32,7 +32,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
const { quadrant, xySwap } = mapTransformProps; const { quadrant, xySwap } = mapTransformProps;
const xPosition = gantry_mounted ? (botPositionX || 0) : x; const xPosition = gantry_mounted ? (botPositionX || 0) : x;
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps); const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
const toolName = props.slot.tool ? props.slot.tool.body.name : "no tool"; const toolName = props.slot.tool ? props.slot.tool.body.name : "empty";
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot; const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
const toolProps = { const toolProps = {
x: qx, x: qx,

View File

@ -40,6 +40,7 @@ describe("<GroupDetailActive/>", () => {
allPoints: [], allPoints: [],
shouldDisplay: () => true, shouldDisplay: () => true,
slugs: [], slugs: [],
hovered: undefined,
}; };
}; };

View File

@ -10,6 +10,7 @@ import { GroupDetailActive } from "./group_detail_active";
import { ShouldDisplay } from "../../devices/interfaces"; import { ShouldDisplay } from "../../devices/interfaces";
import { getShouldDisplayFn } from "../../farmware/state_to_props"; import { getShouldDisplayFn } from "../../farmware/state_to_props";
import { uniq } from "lodash"; import { uniq } from "lodash";
import { UUID } from "../../resources/interfaces";
interface GroupDetailProps { interface GroupDetailProps {
dispatch: Function; dispatch: Function;
@ -17,6 +18,7 @@ interface GroupDetailProps {
allPoints: TaggedPoint[]; allPoints: TaggedPoint[];
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
slugs: string[]; slugs: string[];
hovered: UUID | undefined;
} }
/** Find a group from a URL-provided ID. */ /** Find a group from a URL-provided ID. */
@ -35,6 +37,7 @@ function mapStateToProps(props: Everything): GroupDetailProps {
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot), shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
slugs: uniq(selectAllPlantPointers(props.resources.index) slugs: uniq(selectAllPlantPointers(props.resources.index)
.map(p => p.body.openfarm_slug)), .map(p => p.body.openfarm_slug)),
hovered: props.resources.consumers.farm_designer.hoveredPlantListItem,
}; };
} }

View File

@ -18,6 +18,7 @@ import {
GroupCriteria, GroupPointCountBreakdown, pointsSelectedByGroup GroupCriteria, GroupPointCountBreakdown, pointsSelectedByGroup
} from "./criteria"; } from "./criteria";
import { Content } from "../../constants"; import { Content } from "../../constants";
import { UUID } from "../../resources/interfaces";
export interface GroupDetailActiveProps { export interface GroupDetailActiveProps {
dispatch: Function; dispatch: Function;
@ -25,6 +26,7 @@ export interface GroupDetailActiveProps {
allPoints: TaggedPoint[]; allPoints: TaggedPoint[];
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
slugs: string[]; slugs: string[];
hovered: UUID | undefined;
} }
type State = { timerId?: ReturnType<typeof setInterval> }; type State = { timerId?: ReturnType<typeof setInterval> };
@ -47,7 +49,7 @@ export class GroupDetailActive
return sortedPoints.map(point => { return sortedPoints.map(point => {
return <PointGroupItem return <PointGroupItem
key={point.uuid} key={point.uuid}
hovered={false} hovered={point.uuid === this.props.hovered}
group={this.props.group} group={this.props.group}
point={point} point={point}
dispatch={this.props.dispatch} />; dispatch={this.props.dispatch} />;

View File

@ -36,7 +36,7 @@ export interface PointsPathLineProps {
} }
export const PointsPathLine = (props: PointsPathLineProps) => export const PointsPathLine = (props: PointsPathLineProps) =>
<g id="group-order" <g id="group-order" style={{ pointerEvents: "none" }}
stroke={props.color || Color.mediumGray} stroke={props.color || Color.mediumGray}
strokeWidth={props.strokeWidth || 3} strokeWidth={props.strokeWidth || 3}
strokeDasharray={props.dash || 12}> strokeDasharray={props.dash || 12}>

View File

@ -93,6 +93,7 @@ export class PointGroupItem
style={{ style={{
border: this.criteriaIcon ? "1px solid gray" : "none", border: this.criteriaIcon ? "1px solid gray" : "none",
borderRadius: "5px", borderRadius: "5px",
background: this.props.hovered ? "lightgray" : "none",
}} }}
src={DEFAULT_ICON} src={DEFAULT_ICON}
onLoad={this.maybeGetCachedIcon} onLoad={this.maybeGetCachedIcon}

View File

@ -22,6 +22,7 @@ import {
import { init, save, edit, destroy } from "../../../api/crud"; import { init, save, edit, destroy } from "../../../api/crud";
import { history } from "../../../history"; import { history } from "../../../history";
import { SpecialStatus } from "farmbot"; import { SpecialStatus } from "farmbot";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
describe("<AddToolSlot />", () => { describe("<AddToolSlot />", () => {
const fakeProps = (): AddToolSlotProps => ({ const fakeProps = (): AddToolSlotProps => ({
@ -30,14 +31,20 @@ describe("<AddToolSlot />", () => {
botPosition: { x: undefined, y: undefined, z: undefined }, botPosition: { x: undefined, y: undefined, z: undefined },
dispatch: jest.fn(), dispatch: jest.fn(),
findToolSlot: fakeToolSlot, findToolSlot: fakeToolSlot,
firmwareHardware: undefined,
}); });
it("renders", () => { it("renders", () => {
const wrapper = mount(<AddToolSlot {...fakeProps()} />); const wrapper = mount(<AddToolSlot {...fakeProps()} />);
["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone", ["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
"change slot direction", "use current location", "gantry-mounted" "change slot direction", "use current location", "gantry-mounted"
].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); ].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
expect(init).toHaveBeenCalled(); expect(init).toHaveBeenCalledWith("Point", {
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
x: 0, y: 0, z: 0, tool_id: undefined,
pullout_direction: ToolPulloutDirection.NONE,
gantry_mounted: false,
});
}); });
it("renders while loading", () => { it("renders while loading", () => {
@ -102,6 +109,19 @@ describe("<AddToolSlot />", () => {
const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />); const wrapper = mount<AddToolSlot>(<AddToolSlot {...p} />);
expect(wrapper.instance().tool).toEqual(undefined); expect(wrapper.instance().tool).toEqual(undefined);
}); });
it("renders for express bots", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<AddToolSlot {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("tool");
expect(init).toHaveBeenCalledWith("Point", {
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
x: 0, y: 0, z: 0, tool_id: undefined,
pullout_direction: ToolPulloutDirection.NONE,
gantry_mounted: true,
});
});
}); });
describe("mapStateToProps()", () => { describe("mapStateToProps()", () => {

View File

@ -11,11 +11,13 @@ import { fakeState } from "../../../__test_support__/fake_state";
import { SaveBtn } from "../../../ui"; import { SaveBtn } from "../../../ui";
import { initSave } from "../../../api/crud"; import { initSave } from "../../../api/crud";
import { history } from "../../../history"; import { history } from "../../../history";
import { error } from "../../../toast/toast"; import { FirmwareHardware } from "farmbot";
describe("<AddTool />", () => { describe("<AddTool />", () => {
const fakeProps = (): AddToolProps => ({ const fakeProps = (): AddToolProps => ({
dispatch: jest.fn(), dispatch: jest.fn(),
existingToolNames: [],
firmwareHardware: undefined,
}); });
it("renders", () => { it("renders", () => {
@ -38,19 +40,29 @@ describe("<AddTool />", () => {
expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" }); expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" });
}); });
it("doesn't add stock tools", () => { it.each<[FirmwareHardware, number]>([
const wrapper = mount(<AddTool {...fakeProps()} />); ["arduino", 6],
["farmduino", 6],
["farmduino_k14", 6],
["farmduino_k15", 8],
["express_k10", 2],
])("adds peripherals: %s", (firmware, expectedAdds) => {
const p = fakeProps();
p.firmwareHardware = firmware;
const wrapper = mount(<AddTool {...p} />);
wrapper.find("button").last().simulate("click"); wrapper.find("button").last().simulate("click");
expect(error).toHaveBeenCalledWith("Please choose a FarmBot model."); expect(initSave).toHaveBeenCalledTimes(expectedAdds);
expect(initSave).not.toHaveBeenCalledTimes(6); expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools");
}); });
it("adds stock tools", () => { it("doesn't add stock tools twice", () => {
const wrapper = mount(<AddTool {...fakeProps()} />); const p = fakeProps();
p.firmwareHardware = "express_k10";
p.existingToolNames = ["Seed Trough 1"];
const wrapper = mount(<AddTool {...p} />);
wrapper.setState({ model: "express" }); wrapper.setState({ model: "express" });
wrapper.find("button").last().simulate("click"); wrapper.find("button").last().simulate("click");
expect(initSave).toHaveBeenCalledTimes(2); expect(initSave).toHaveBeenCalledTimes(1);
expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
}); });
}); });

View File

@ -28,6 +28,7 @@ describe("<EditToolSlot />", () => {
findTool: jest.fn(), findTool: jest.fn(),
botPosition: { x: undefined, y: undefined, z: undefined }, botPosition: { x: undefined, y: undefined, z: undefined },
dispatch: jest.fn(), dispatch: jest.fn(),
firmwareHardware: undefined,
}); });
it("redirects", () => { it("redirects", () => {
@ -39,7 +40,7 @@ describe("<EditToolSlot />", () => {
const p = fakeProps(); const p = fakeProps();
p.findToolSlot = () => fakeToolSlot(); p.findToolSlot = () => fakeToolSlot();
const wrapper = mount(<EditToolSlot {...p} />); const wrapper = mount(<EditToolSlot {...p} />);
["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone", ["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
"change slot direction", "use current location", "gantry-mounted" "change slot direction", "use current location", "gantry-mounted"
].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); ].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
}); });

View File

@ -39,6 +39,7 @@ describe("<Tools />", () => {
bot, bot,
botToMqttStatus: "down", botToMqttStatus: "down",
hoveredToolSlot: undefined, hoveredToolSlot: undefined,
firmwareHardware: undefined,
}); });
it("renders with no tools", () => { it("renders with no tools", () => {
@ -64,7 +65,7 @@ describe("<Tools />", () => {
p.toolSlots[1].body.y = 2; p.toolSlots[1].body.y = 2;
const wrapper = mount(<Tools {...p} />); const wrapper = mount(<Tools {...p} />);
[ [
"foo", "my tool", "unnamed tool", "(1, 0, 0)", "unknown", "(gantry, 2, 0)" "foo", "my tool", "unnamed", "(1, 0, 0)", "unknown", "(gantry, 2, 0)"
].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); ].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
}); });
@ -158,6 +159,7 @@ describe("<Tools />", () => {
p.bot.hardware.informational_settings.sync_status = "synced"; p.bot.hardware.informational_settings.sync_status = "synced";
p.botToMqttStatus = "up"; p.botToMqttStatus = "up";
const wrapper = mount(<Tools {...p} />); const wrapper = mount(<Tools {...p} />);
expect(wrapper.text().toLowerCase()).toContain("mounted tool");
wrapper.find(".yellow").first().simulate("click"); wrapper.find(".yellow").first().simulate("click");
expect(mockDevice.readPin).toHaveBeenCalledWith({ expect(mockDevice.readPin).toHaveBeenCalledWith({
label: "pin63", pin_mode: 0, pin_number: 63 label: "pin63", pin_mode: 0, pin_number: 63
@ -173,6 +175,13 @@ describe("<Tools />", () => {
expect(mockDevice.readPin).not.toHaveBeenCalled(); expect(mockDevice.readPin).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(Content.NOT_AVAILABLE_WHEN_OFFLINE); expect(error).toHaveBeenCalledWith(Content.NOT_AVAILABLE_WHEN_OFFLINE);
}); });
it("doesn't display mounted tool on express models", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Tools {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("mounted tool");
});
}); });
describe("mapStateToProps()", () => { describe("mapStateToProps()", () => {

View File

@ -136,6 +136,7 @@ describe("<ToolInputRow />", () => {
tools: [], tools: [],
selectedTool: undefined, selectedTool: undefined,
onChange: jest.fn(), onChange: jest.fn(),
isExpress: false,
}); });
it("renders", () => { it("renders", () => {
@ -149,6 +150,13 @@ describe("<ToolInputRow />", () => {
const wrapper = mount(<ToolInputRow {...p} />); const wrapper = mount(<ToolInputRow {...p} />);
expect(wrapper.text().toLowerCase()).toContain("foo"); expect(wrapper.text().toLowerCase()).toContain("foo");
}); });
it("renders for express bots", () => {
const p = fakeProps();
p.isExpress = true;
const wrapper = mount(<ToolInputRow {...p} />);
expect(wrapper.text().toLowerCase()).toContain("seed container");
});
}); });
describe("<SlotLocationInputRow />", () => { describe("<SlotLocationInputRow />", () => {

View File

@ -5,36 +5,37 @@ import {
} from "../designer_panel"; } from "../designer_panel";
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { SaveBtn, FBSelect, DropDownItem } from "../../ui"; import { SaveBtn } from "../../ui";
import { SpecialStatus } from "farmbot"; import { SpecialStatus, FirmwareHardware } from "farmbot";
import { initSave } from "../../api/crud"; import { initSave } from "../../api/crud";
import { Panel } from "../panel_header"; import { Panel } from "../panel_header";
import { history } from "../../history"; import { history } from "../../history";
import { error } from "../../toast/toast"; import { selectAllTools } from "../../resources/selectors";
import { betterCompact } from "../../util";
enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" } import {
isExpressBoard, getFwHardwareValue
const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ } from "../../devices/components/firmware_hardware_support";
[Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 }, import { getFbosConfig } from "../../resources/getters";
[Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 },
[Model.express]: { label: t("Express"), value: Model.express },
});
export interface AddToolProps { export interface AddToolProps {
dispatch: Function; dispatch: Function;
existingToolNames: string[];
firmwareHardware: FirmwareHardware | undefined;
} }
export interface AddToolState { export interface AddToolState {
toolName: string; toolName: string;
model: Model | undefined;
} }
export const mapStateToProps = (props: Everything): AddToolProps => ({ export const mapStateToProps = (props: Everything): AddToolProps => ({
dispatch: props.dispatch, dispatch: props.dispatch,
existingToolNames: betterCompact(selectAllTools(props.resources.index)
.map(tool => tool.body.name)),
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
}); });
export class RawAddTool extends React.Component<AddToolProps, AddToolState> { export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
state: AddToolState = { toolName: "", model: undefined }; state: AddToolState = { toolName: "" };
newTool = (name: string) => { newTool = (name: string) => {
this.props.dispatch(initSave("Tool", { name })); this.props.dispatch(initSave("Tool", { name }));
@ -45,9 +46,12 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
history.push("/app/designer/tools"); history.push("/app/designer/tools");
} }
stockToolNames = (model: Model) => { stockToolNames = () => {
switch (model) { switch (this.props.firmwareHardware) {
case Model.genesis14: case "arduino":
case "farmduino":
case "farmduino_k14":
default:
return [ return [
t("Seeder"), t("Seeder"),
t("Watering Nozzle"), t("Watering Nozzle"),
@ -56,7 +60,7 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
t("Seed Bin"), t("Seed Bin"),
t("Seed Tray"), t("Seed Tray"),
]; ];
case Model.genesis15: case "farmduino_k15":
return [ return [
t("Seeder"), t("Seeder"),
t("Watering Nozzle"), t("Watering Nozzle"),
@ -67,7 +71,7 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
t("Seed Trough 1"), t("Seed Trough 1"),
t("Seed Trough 2"), t("Seed Trough 2"),
]; ];
case Model.express: case "express_k10":
return [ return [
t("Seed Trough 1"), t("Seed Trough 1"),
t("Seed Trough 2"), t("Seed Trough 2"),
@ -77,31 +81,20 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
AddStockTools = () => AddStockTools = () =>
<div className="add-stock-tools"> <div className="add-stock-tools">
<label>{t("Add stock tools")}</label> <label>{t("Add stock names")}</label>
<FBSelect <ul>
customNullLabel={t("Choose model")} {this.stockToolNames().map(n => <li key={n}>{n}</li>)}
list={Object.values(MODEL_DDI_LOOKUP())} </ul>
selectedItem={this.state.model
? MODEL_DDI_LOOKUP()[this.state.model]
: undefined}
onChange={ddi => this.setState({ model: ddi.value as Model })}
/>
{this.state.model &&
<ul>
{this.stockToolNames(this.state.model).map(n => <li key={n}>{n}</li>)}
</ul>}
<button <button
className="fb-button green" className="fb-button green"
onClick={() => { onClick={() => {
if (this.state.model) { this.stockToolNames()
this.stockToolNames(this.state.model).map(n => this.newTool(n)); .filter(n => !this.props.existingToolNames.includes(n))
history.push("/app/designer/tools"); .map(n => this.newTool(n));
} else { history.push("/app/designer/tools");
error(t("Please choose a FarmBot model."));
}
}}> }}>
<i className="fa fa-plus" /> <i className="fa fa-plus" />
{t("Stock Tools")} {t("Stock names")}
</button> </button>
</div> </div>
@ -110,12 +103,14 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
return <DesignerPanel panelName={panelName} panel={Panel.Tools}> return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerPanelHeader <DesignerPanelHeader
panelName={panelName} panelName={panelName}
title={t("Add new tool")} title={isExpressBoard(this.props.firmwareHardware)
? t("Add new")
: t("Add new tool")}
backTo={"/app/designer/tools"} backTo={"/app/designer/tools"}
panel={Panel.Tools} /> panel={Panel.Tools} />
<DesignerPanelContent panelName={panelName}> <DesignerPanelContent panelName={panelName}>
<div className="add-new-tool"> <div className="add-new-tool">
<label>{t("Tool Name")}</label> <label>{t("Name")}</label>
<input onChange={e => <input onChange={e =>
this.setState({ toolName: e.currentTarget.value })} /> this.setState({ toolName: e.currentTarget.value })} />
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} /> <SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />

View File

@ -6,7 +6,9 @@ import {
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { SaveBtn } from "../../ui"; import { SaveBtn } from "../../ui";
import { SpecialStatus, TaggedTool, TaggedToolSlotPointer } from "farmbot"; import {
SpecialStatus, TaggedTool, TaggedToolSlotPointer, FirmwareHardware
} from "farmbot";
import { init, save, edit, destroy } from "../../api/crud"; import { init, save, edit, destroy } from "../../api/crud";
import { Panel } from "../panel_header"; import { Panel } from "../panel_header";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
@ -18,6 +20,10 @@ import { validBotLocationData } from "../../util";
import { history } from "../../history"; import { history } from "../../history";
import { SlotEditRows } from "./tool_slot_edit_components"; import { SlotEditRows } from "./tool_slot_edit_components";
import { UUID } from "../../resources/interfaces"; import { UUID } from "../../resources/interfaces";
import {
isExpressBoard, getFwHardwareValue
} from "../../devices/components/firmware_hardware_support";
import { getFbosConfig } from "../../resources/getters";
export interface AddToolSlotProps { export interface AddToolSlotProps {
tools: TaggedTool[]; tools: TaggedTool[];
@ -25,6 +31,7 @@ export interface AddToolSlotProps {
botPosition: BotPosition; botPosition: BotPosition;
findTool(id: number): TaggedTool | undefined; findTool(id: number): TaggedTool | undefined;
findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined; findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface AddToolSlotState { export interface AddToolSlotState {
@ -38,6 +45,7 @@ export const mapStateToProps = (props: Everything): AddToolSlotProps => ({
findTool: (id: number) => maybeFindToolById(props.resources.index, id), findTool: (id: number) => maybeFindToolById(props.resources.index, id),
findToolSlot: (uuid: UUID | undefined) => findToolSlot: (uuid: UUID | undefined) =>
maybeGetToolSlot(props.resources.index, uuid), maybeGetToolSlot(props.resources.index, uuid),
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
}); });
export class RawAddToolSlot export class RawAddToolSlot
@ -48,7 +56,8 @@ export class RawAddToolSlot
const action = init("Point", { const action = init("Point", {
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
x: 0, y: 0, z: 0, tool_id: undefined, x: 0, y: 0, z: 0, tool_id: undefined,
pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: false pullout_direction: ToolPulloutDirection.NONE,
gantry_mounted: isExpressBoard(this.props.firmwareHardware) ? true : false,
}); });
this.setState({ uuid: action.payload.uuid }); this.setState({ uuid: action.payload.uuid });
this.props.dispatch(action); this.props.dispatch(action);
@ -57,7 +66,7 @@ export class RawAddToolSlot
componentWillUnmount() { componentWillUnmount() {
if (this.state.uuid && this.toolSlot if (this.state.uuid && this.toolSlot
&& this.toolSlot.specialStatus == SpecialStatus.DIRTY) { && this.toolSlot.specialStatus == SpecialStatus.DIRTY) {
confirm(t("Save new tool?")) confirm(t("Save new slot?"))
? this.props.dispatch(save(this.state.uuid)) ? this.props.dispatch(save(this.state.uuid))
: this.props.dispatch(destroy(this.state.uuid, true)); : this.props.dispatch(destroy(this.state.uuid, true));
} }
@ -86,12 +95,15 @@ export class RawAddToolSlot
return <DesignerPanel panelName={panelName} panel={Panel.Tools}> return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerPanelHeader <DesignerPanelHeader
panelName={panelName} panelName={panelName}
title={t("Add new tool slot")} title={isExpressBoard(this.props.firmwareHardware)
? t("Add new slot")
: t("Add new tool slot")}
backTo={"/app/designer/tools"} backTo={"/app/designer/tools"}
panel={Panel.Tools} /> panel={Panel.Tools} />
<DesignerPanelContent panelName={panelName}> <DesignerPanelContent panelName={panelName}>
{this.toolSlot {this.toolSlot
? <SlotEditRows ? <SlotEditRows
isExpress={isExpressBoard(this.props.firmwareHardware)}
toolSlot={this.toolSlot} toolSlot={this.toolSlot}
tools={this.props.tools} tools={this.props.tools}
tool={this.tool} tool={this.tool}

View File

@ -51,7 +51,7 @@ export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
backTo={"/app/designer/tools"} backTo={"/app/designer/tools"}
panel={Panel.Tools} /> panel={Panel.Tools} />
<DesignerPanelContent panelName={panelName}> <DesignerPanelContent panelName={panelName}>
<label>{t("Tool Name")}</label> <label>{t("Name")}</label>
<input <input
value={toolName} value={toolName}
onChange={e => this.setState({ toolName: e.currentTarget.value })} /> onChange={e => this.setState({ toolName: e.currentTarget.value })} />

View File

@ -6,7 +6,7 @@ import {
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { getPathArray } from "../../history"; import { getPathArray } from "../../history";
import { TaggedToolSlotPointer, TaggedTool } from "farmbot"; import { TaggedToolSlotPointer, TaggedTool, FirmwareHardware } from "farmbot";
import { edit, save, destroy } from "../../api/crud"; import { edit, save, destroy } from "../../api/crud";
import { history } from "../../history"; import { history } from "../../history";
import { Panel } from "../panel_header"; import { Panel } from "../panel_header";
@ -17,6 +17,10 @@ import { BotPosition } from "../../devices/interfaces";
import { validBotLocationData } from "../../util"; import { validBotLocationData } from "../../util";
import { SlotEditRows } from "./tool_slot_edit_components"; import { SlotEditRows } from "./tool_slot_edit_components";
import { moveAbs } from "../../devices/actions"; import { moveAbs } from "../../devices/actions";
import {
getFwHardwareValue, isExpressBoard
} from "../../devices/components/firmware_hardware_support";
import { getFbosConfig } from "../../resources/getters";
export interface EditToolSlotProps { export interface EditToolSlotProps {
findToolSlot(id: string): TaggedToolSlotPointer | undefined; findToolSlot(id: string): TaggedToolSlotPointer | undefined;
@ -24,6 +28,7 @@ export interface EditToolSlotProps {
findTool(id: number): TaggedTool | undefined; findTool(id: number): TaggedTool | undefined;
dispatch: Function; dispatch: Function;
botPosition: BotPosition; botPosition: BotPosition;
firmwareHardware: FirmwareHardware | undefined;
} }
export const mapStateToProps = (props: Everything): EditToolSlotProps => ({ export const mapStateToProps = (props: Everything): EditToolSlotProps => ({
@ -33,6 +38,7 @@ export const mapStateToProps = (props: Everything): EditToolSlotProps => ({
findTool: (id: number) => maybeFindToolById(props.resources.index, id), findTool: (id: number) => maybeFindToolById(props.resources.index, id),
dispatch: props.dispatch, dispatch: props.dispatch,
botPosition: validBotLocationData(props.bot.hardware.location_data).position, botPosition: validBotLocationData(props.bot.hardware.location_data).position,
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
}); });
export class RawEditToolSlot extends React.Component<EditToolSlotProps> { export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
@ -64,6 +70,7 @@ export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
panel={Panel.Tools} /> panel={Panel.Tools} />
<DesignerPanelContent panelName={panelName}> <DesignerPanelContent panelName={panelName}>
<SlotEditRows <SlotEditRows
isExpress={isExpressBoard(this.props.firmwareHardware)}
toolSlot={toolSlot} toolSlot={toolSlot}
tools={this.props.tools} tools={this.props.tools}
tool={this.tool} tool={this.tool}

View File

@ -11,6 +11,7 @@ import {
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { import {
TaggedTool, TaggedToolSlotPointer, TaggedDevice, TaggedSensor, TaggedTool, TaggedToolSlotPointer, TaggedDevice, TaggedSensor,
FirmwareHardware,
} from "farmbot"; } from "farmbot";
import { import {
selectAllTools, selectAllToolSlotPointers, getDeviceAccountSettings, selectAllTools, selectAllToolSlotPointers, getDeviceAccountSettings,
@ -31,6 +32,10 @@ import { getStatus } from "../../connectivity/reducer_support";
import { setToolHover } from "../map/layers/tool_slots/tool_graphics"; import { setToolHover } from "../map/layers/tool_slots/tool_graphics";
import { ToolSelection } from "./tool_slot_edit_components"; import { ToolSelection } from "./tool_slot_edit_components";
import { error } from "../../toast/toast"; import { error } from "../../toast/toast";
import {
isExpressBoard, getFwHardwareValue
} from "../../devices/components/firmware_hardware_support";
import { getFbosConfig } from "../../resources/getters";
export interface ToolsProps { export interface ToolsProps {
tools: TaggedTool[]; tools: TaggedTool[];
@ -42,6 +47,7 @@ export interface ToolsProps {
bot: BotState; bot: BotState;
botToMqttStatus: NetworkState; botToMqttStatus: NetworkState;
hoveredToolSlot: string | undefined; hoveredToolSlot: string | undefined;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface ToolsState { export interface ToolsState {
@ -58,6 +64,7 @@ export const mapStateToProps = (props: Everything): ToolsProps => ({
bot: props.bot, bot: props.bot,
botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]), botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]),
hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot, hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot,
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
}); });
const toolStatus = (value: number | undefined): string => { const toolStatus = (value: number | undefined): string => {
@ -108,6 +115,8 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
this.props.botToMqttStatus); this.props.botToMqttStatus);
} }
get isExpress() { return isExpressBoard(this.props.firmwareHardware); }
MountedToolInfo = () => MountedToolInfo = () =>
<div className="mounted-tool"> <div className="mounted-tool">
<div className="mounted-tool-header"> <div className="mounted-tool-header">
@ -141,10 +150,10 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
ToolSlots = () => ToolSlots = () =>
<div className="tool-slots"> <div className="tool-slots">
<div className="tool-slots-header"> <div className="tool-slots-header">
<label>{t("tool slots")}</label> <label>{this.strings.toolSlots}</label>
<Link to={"/app/designer/tool-slots/add"}> <Link to={"/app/designer/tool-slots/add"}>
<div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}> <div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}>
<i className="fa fa-plus" title={t("Add tool slot")} /> <i className="fa fa-plus" title={this.strings.addSlot} />
</div> </div>
</Link> </Link>
</div> </div>
@ -162,10 +171,10 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
Tools = () => Tools = () =>
<div className="tools"> <div className="tools">
<div className="tools-header"> <div className="tools-header">
<label>{t("tools")}</label> <label>{this.strings.tools}</label>
<Link to={"/app/designer/tools/add"}> <Link to={"/app/designer/tools/add"}>
<div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}> <div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}>
<i className="fa fa-plus" title={t("Add tool")} /> <i className="fa fa-plus" title={this.strings.titleText} />
</div> </div>
</Link> </Link>
</div> </div>
@ -176,9 +185,32 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
.map(tool => .map(tool =>
<ToolInventoryItem key={tool.uuid} <ToolInventoryItem key={tool.uuid}
toolId={tool.body.id} toolId={tool.body.id}
toolName={tool.body.name || t("Unnamed tool")} />)} toolName={tool.body.name || t("Unnamed")} />)}
</div> </div>
get strings() {
return {
placeholder: this.isExpress
? t("Search your seed containers...")
: t("Search your tools..."),
titleText: this.isExpress
? t("Add a seed container")
: t("Add a tool or seed container"),
emptyStateText: this.isExpress
? Content.NO_SEED_CONTAINERS
: Content.NO_TOOLS,
tools: this.isExpress
? t("seed containers")
: t("tools and seed containers"),
toolSlots: this.isExpress
? t("seed container slots")
: t("tool slots"),
addSlot: this.isExpress
? t("Add slot")
: t("Add tool slot"),
};
}
render() { render() {
const panelName = "tools"; const panelName = "tools";
const hasTools = this.props.tools.length > 0; const hasTools = this.props.tools.length > 0;
@ -187,18 +219,19 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
<DesignerPanelTop <DesignerPanelTop
panel={Panel.Tools} panel={Panel.Tools}
linkTo={!hasTools ? "/app/designer/tools/add" : undefined} linkTo={!hasTools ? "/app/designer/tools/add" : undefined}
title={!hasTools ? t("Add tool") : undefined}> title={!hasTools ? this.strings.titleText : undefined}>
<input type="text" onChange={this.update} <input type="text" onChange={this.update}
placeholder={t("Search your tools...")} /> placeholder={this.strings.placeholder} />
</DesignerPanelTop> </DesignerPanelTop>
<DesignerPanelContent panelName={"tools"}> <DesignerPanelContent panelName={"tools"}>
<EmptyStateWrapper <EmptyStateWrapper
notEmpty={hasTools} notEmpty={hasTools}
graphic={EmptyStateGraphic.tools} graphic={EmptyStateGraphic.tools}
title={t("Add a tool")} title={this.strings.titleText}
text={Content.NO_TOOLS} text={this.strings.emptyStateText}
colorScheme={"tools"}> colorScheme={"tools"}>
<this.MountedToolInfo /> {!this.isExpress &&
<this.MountedToolInfo />}
<this.ToolSlots /> <this.ToolSlots />
<this.Tools /> <this.Tools />
</EmptyStateWrapper> </EmptyStateWrapper>
@ -223,7 +256,7 @@ const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
onMouseLeave={() => props.dispatch(setToolHover(undefined))}> onMouseLeave={() => props.dispatch(setToolHover(undefined))}>
<Row> <Row>
<Col xs={7}> <Col xs={7}>
<p>{props.getToolName(tool_id) || t("No tool")}</p> <p>{props.getToolName(tool_id) || t("Empty")}</p>
</Col> </Col>
<Col xs={5}> <Col xs={5}>
<p className="tool-slot-position"> <p className="tool-slot-position">

View File

@ -97,13 +97,18 @@ export interface ToolInputRowProps {
tools: TaggedTool[]; tools: TaggedTool[];
selectedTool: TaggedTool | undefined; selectedTool: TaggedTool | undefined;
onChange(update: { tool_id: number }): void; onChange(update: { tool_id: number }): void;
isExpress: boolean;
} }
export const ToolInputRow = (props: ToolInputRowProps) => export const ToolInputRow = (props: ToolInputRowProps) =>
<div className="tool-slot-tool-input"> <div className="tool-slot-tool-input">
<Row> <Row>
<Col xs={12}> <Col xs={12}>
<label>{t("Tool")}</label> <label>
{props.isExpress
? t("Seed Container")
: t("Tool or Seed Container")}
</label>
<ToolSelection <ToolSelection
tools={props.tools} tools={props.tools}
selectedTool={props.selectedTool} selectedTool={props.selectedTool}
@ -144,6 +149,7 @@ export interface SlotEditRowsProps {
tool: TaggedTool | undefined; tool: TaggedTool | undefined;
botPosition: BotPosition; botPosition: BotPosition;
updateToolSlot(update: Partial<TaggedToolSlotPointer["body"]>): void; updateToolSlot(update: Partial<TaggedToolSlotPointer["body"]>): void;
isExpress: boolean;
} }
export const SlotEditRows = (props: SlotEditRowsProps) => export const SlotEditRows = (props: SlotEditRowsProps) =>
@ -153,16 +159,19 @@ export const SlotEditRows = (props: SlotEditRowsProps) =>
gantryMounted={props.toolSlot.body.gantry_mounted} gantryMounted={props.toolSlot.body.gantry_mounted}
onChange={props.updateToolSlot} /> onChange={props.updateToolSlot} />
<ToolInputRow <ToolInputRow
isExpress={props.isExpress}
tools={props.tools} tools={props.tools}
selectedTool={props.tool} selectedTool={props.tool}
onChange={props.updateToolSlot} /> onChange={props.updateToolSlot} />
<SlotDirectionInputRow {!props.toolSlot.body.gantry_mounted &&
toolPulloutDirection={props.toolSlot.body.pullout_direction} <SlotDirectionInputRow
onChange={props.updateToolSlot} /> toolPulloutDirection={props.toolSlot.body.pullout_direction}
onChange={props.updateToolSlot} />}
<UseCurrentLocationInputRow <UseCurrentLocationInputRow
botPosition={props.botPosition} botPosition={props.botPosition}
onChange={props.updateToolSlot} /> onChange={props.updateToolSlot} />
<GantryMountedInput {!props.isExpress &&
gantryMounted={props.toolSlot.body.gantry_mounted} <GantryMountedInput
onChange={props.updateToolSlot} /> gantryMounted={props.toolSlot.body.gantry_mounted}
onChange={props.updateToolSlot} />}
</div>; </div>;

View File

@ -40,11 +40,11 @@ export const setFolderName = (id: number, name: string) => {
return d(save(folder.uuid)) as Promise<{}>; return d(save(folder.uuid)) as Promise<{}>;
}; };
const DEFAULTS: Folder = { const DEFAULTS = (): Folder => ({
name: "New Folder", name: t("New Folder"),
color: "gray", color: "gray",
parent_id: 0, parent_id: 0,
}; });
export const addNewSequenceToFolder = (folder_id?: number) => { export const addNewSequenceToFolder = (folder_id?: number) => {
const uuidMap = store.getState().resources.index.byKind["Sequence"]; const uuidMap = store.getState().resources.index.byKind["Sequence"];
@ -67,7 +67,7 @@ export const addNewSequenceToFolder = (folder_id?: number) => {
export const createFolder = (config: DeepPartial<Folder> = {}) => { export const createFolder = (config: DeepPartial<Folder> = {}) => {
const d: Function = store.dispatch; const d: Function = store.dispatch;
const folder: Folder = { ...DEFAULTS, ...config }; const folder: Folder = { ...DEFAULTS(), ...config };
const action = initSave("Folder", folder); const action = initSave("Folder", folder);
// tslint:disable-next-line:no-any // tslint:disable-next-line:no-any
const p: Promise<{}> = d(action); const p: Promise<{}> = d(action);

View File

@ -7,7 +7,7 @@ export const LaptopSplash = ({ className }: { className: string }) =>
<div className="laptop"> <div className="laptop">
<div className="laptop-screen"> <div className="laptop-screen">
<video muted autoPlay loop> <video muted autoPlay loop>
<source src={ExternalUrl.Videos.desktop} type="video/mp4" /> <source src={ExternalUrl.Video.desktop} type="video/mp4" />
</video> </video>
<span className="laptop-shine" /> <span className="laptop-shine" />
</div> </div>

View File

@ -56,7 +56,7 @@ describe("<Tour />", () => {
expect(wrapper.state()).toEqual({ expect(wrapper.state()).toEqual({
run: true, index: 1, returnPath: "/app/messages" run: true, index: 1, returnPath: "/app/messages"
}); });
expect(history.push).toHaveBeenCalledWith("/app/tools"); expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
}); });
it("navigates through tour: other", () => { it("navigates through tour: other", () => {

View File

@ -1,7 +1,22 @@
jest.mock("../../history", () => ({ history: { push: jest.fn() } })); jest.mock("../../history", () => ({ history: { push: jest.fn() } }));
import { tourPageNavigation } from "../tours"; let mockDev = false;
jest.mock("../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import { fakeState } from "../../__test_support__/fake_state";
const mockState = fakeState();
jest.mock("../../redux/store", () => ({
store: { getState: () => mockState },
}));
import { tourPageNavigation, TOUR_STEPS, Tours } from "../tours";
import { history } from "../../history"; import { history } from "../../history";
import { fakeTool, fakeFbosConfig } from "../../__test_support__/fake_state/resources";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
describe("tourPageNavigation()", () => { describe("tourPageNavigation()", () => {
const testCase = (el: string) => { const testCase = (el: string) => {
@ -20,8 +35,47 @@ describe("tourPageNavigation()", () => {
testCase(".regimen-list-panel"); testCase(".regimen-list-panel");
testCase(".tool-list"); testCase(".tool-list");
testCase(".toolbay-list"); testCase(".toolbay-list");
testCase(".tools");
testCase(".tool-slots");
testCase(".tools-panel");
testCase(".photos"); testCase(".photos");
testCase(".logs-table"); testCase(".logs-table");
testCase(".app-settings-widget"); testCase(".app-settings-widget");
}); });
it("includes steps based on tool count", () => {
const getTargets = () =>
Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target);
mockDev = false;
mockState.resources = buildResourceIndex([]);
expect(getTargets()).not.toContain(".tool-slots");
mockState.resources = buildResourceIndex([fakeTool()]);
expect(getTargets()).toContain(".tool-slots");
});
it("has correct content based on board version", () => {
const getTitles = () =>
Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.title);
mockDev = false;
mockState.resources = buildResourceIndex([]);
expect(getTitles()).toContain("Add tools and tool slots");
expect(getTitles()).not.toContain("Add seed containers");
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "express_k10";
mockState.resources = buildResourceIndex([fbosConfig]);
expect(getTitles()).toContain("Add seed containers and slots");
expect(getTitles()).not.toContain("Add seed containers");
mockState.resources = buildResourceIndex([fbosConfig, fakeTool()]);
expect(getTitles()).not.toContain("Add seed containers and slots");
expect(getTitles()).toContain("Add seed containers");
});
it("includes correct tour steps", () => {
mockDev = true;
const targets =
Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target);
expect(targets).not.toContain(".tools");
expect(targets).toContain(".tool-list");
expect(targets).toContain(".toolbay-list");
});
}); });

View File

@ -6,6 +6,7 @@ import { TOUR_STEPS, tourPageNavigation } from "./tours";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { Actions } from "../constants"; import { Actions } from "../constants";
import { store } from "../redux/store"; import { store } from "../redux/store";
import { ErrorBoundary } from "../error_boundary";
const strings = () => ({ const strings = () => ({
back: t("Back"), back: t("Back"),
@ -65,15 +66,17 @@ export class Tour extends React.Component<TourProps, TourState> {
return step; return step;
}); });
return <div className="tour"> return <div className="tour">
<Joyride <ErrorBoundary>
steps={steps} <Joyride
run={this.state.run} steps={steps}
callback={this.callback} run={this.state.run}
stepIndex={this.state.index} callback={this.callback}
showSkipButton={true} stepIndex={this.state.index}
continuous={true} showSkipButton={true}
styles={STYLES} continuous={true}
locale={strings()} /> styles={STYLES}
locale={strings()} />
</ErrorBoundary>
</div>; </div>;
} }
} }

View File

@ -2,6 +2,13 @@ import { history } from "../history";
import { Step as TourStep } from "react-joyride"; import { Step as TourStep } from "react-joyride";
import { TourContent } from "../constants"; import { TourContent } from "../constants";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { DevSettings } from "../account/dev/dev_support";
import { selectAllTools } from "../resources/selectors";
import { store } from "../redux/store";
import { getFbosConfig } from "../resources/getters";
import {
isExpressBoard, getFwHardwareValue
} from "../devices/components/firmware_hardware_support";
export enum Tours { export enum Tours {
gettingStarted = "gettingStarted", gettingStarted = "gettingStarted",
@ -15,70 +22,105 @@ export const tourNames = () => [
{ name: Tours.funStuff, description: t("find new features") }, { name: Tours.funStuff, description: t("find new features") },
]; ];
const hasTools = () =>
selectAllTools(store.getState().resources.index).length > 0;
const isExpress = () =>
isExpressBoard(getFwHardwareValue(
getFbosConfig(store.getState().resources.index)));
const toolsStep = () => hasTools()
? [{
target: ".tools",
content: isExpress()
? t(TourContent.ADD_SEED_CONTAINERS)
: t(TourContent.ADD_TOOLS),
title: isExpress()
? t("Add seed containers")
: t("Add tools and seed containers"),
}]
: [{
target: ".tools",
content: isExpress()
? t(TourContent.ADD_SEED_CONTAINERS_AND_SLOTS)
: t(TourContent.ADD_TOOLS_AND_SLOTS),
title: isExpress()
? t("Add seed containers and slots")
: t("Add tools and tool slots"),
}];
const toolSlotsStep = () => hasTools()
? [{
target: ".tool-slots",
content: t(TourContent.ADD_TOOLS_AND_SLOTS),
title: t("Add tool slots"),
}]
: [];
export const TOUR_STEPS = (): { [x: string]: TourStep[] } => ({ export const TOUR_STEPS = (): { [x: string]: TourStep[] } => ({
[Tours.gettingStarted]: [ [Tours.gettingStarted]: [
{ {
target: ".plant-inventory-panel", target: ".plant-inventory-panel",
content: TourContent.ADD_PLANTS, content: t(TourContent.ADD_PLANTS),
title: t("Add plants"), title: t("Add plants"),
}, },
{ ...(DevSettings.futureFeaturesEnabled() ? [{
target: ".tool-list", target: ".tool-list",
content: TourContent.ADD_TOOLS, content: t(TourContent.ADD_TOOLS),
title: t("Add tools"), title: t("Add tools and seed containers"),
}, }] : toolsStep()),
{ ...(DevSettings.futureFeaturesEnabled() ? [{
target: ".toolbay-list", target: ".toolbay-list",
content: TourContent.ADD_TOOLS_SLOTS, content: t(TourContent.ADD_TOOLS_SLOTS),
title: t("Add tools to tool bay"), title: t("Add tools to tool bay"),
}, }] : toolSlotsStep()),
{ {
target: ".peripherals-widget", target: ".peripherals-widget",
content: TourContent.ADD_PERIPHERALS, content: t(TourContent.ADD_PERIPHERALS),
title: t("Add peripherals"), title: t("Add peripherals"),
}, },
{ {
target: ".sequence-list-panel", target: ".sequence-list-panel",
content: TourContent.ADD_SEQUENCES, content: t(TourContent.ADD_SEQUENCES),
title: t("Create sequences"), title: t("Create sequences"),
}, },
{ {
target: ".regimen-list-panel", target: ".regimen-list-panel",
content: TourContent.ADD_REGIMENS, content: t(TourContent.ADD_REGIMENS),
title: t("Create regimens"), title: t("Create regimens"),
}, },
{ {
target: ".farm-event-panel", target: ".farm-event-panel",
content: TourContent.ADD_FARM_EVENTS, content: t(TourContent.ADD_FARM_EVENTS),
title: t("Create events"), title: t("Create events"),
}, },
], ],
[Tours.monitoring]: [ [Tours.monitoring]: [
{ {
target: ".move-widget", target: ".move-widget",
content: TourContent.LOCATION_GRID, content: t(TourContent.LOCATION_GRID),
title: t("View current location"), title: t("View current location"),
}, },
{ {
target: ".farm-designer", target: ".farm-designer",
content: TourContent.VIRTUAL_FARMBOT, content: t(TourContent.VIRTUAL_FARMBOT),
title: t("View current location"), title: t("View current location"),
}, },
{ {
target: ".logs-table", target: ".logs-table",
content: TourContent.LOGS_TABLE, content: t(TourContent.LOGS_TABLE),
title: t("View log messages"), title: t("View log messages"),
}, },
{ {
target: ".photos", target: ".photos",
content: TourContent.PHOTOS, content: t(TourContent.PHOTOS),
title: t("Take and view photos"), title: t("Take and view photos"),
}, },
], ],
[Tours.funStuff]: [ [Tours.funStuff]: [
{ {
target: ".app-settings-widget", target: ".app-settings-widget",
content: TourContent.APP_SETTINGS, content: t(TourContent.APP_SETTINGS),
title: t("Customize your web app experience"), title: t("Customize your web app experience"),
}, },
], ],
@ -112,6 +154,10 @@ export const tourPageNavigation = (nextStepTarget: string | HTMLElement) => {
case ".toolbay-list": case ".toolbay-list":
history.push("/app/tools"); history.push("/app/tools");
break; break;
case ".tools":
case ".tool-slots":
history.push("/app/designer/tools");
break;
case ".photos": case ".photos":
history.push("/app/farmware"); history.push("/app/farmware");
break; break;

View File

@ -52,8 +52,7 @@ describe("<Alerts />", () => {
const p = fakeProps(); const p = fakeProps();
p.alerts = [FIRMWARE_MISSING_ALERT, SEED_DATA_MISSING_ALERT]; p.alerts = [FIRMWARE_MISSING_ALERT, SEED_DATA_MISSING_ALERT];
const wrapper = mount(<Alerts {...p} />); const wrapper = mount(<Alerts {...p} />);
expect(wrapper.text()).toContain("2"); expect(wrapper.text()).not.toContain("Your device has no firmware");
expect(wrapper.text()).toContain("Your device has no firmware");
expect(wrapper.text()).toContain("Choose your FarmBot"); expect(wrapper.text()).toContain("Choose your FarmBot");
}); });
@ -61,7 +60,6 @@ describe("<Alerts />", () => {
const p = fakeProps(); const p = fakeProps();
p.alerts = [FIRMWARE_MISSING_ALERT, UNKNOWN_ALERT]; p.alerts = [FIRMWARE_MISSING_ALERT, UNKNOWN_ALERT];
const wrapper = mount(<Alerts {...p} />); const wrapper = mount(<Alerts {...p} />);
expect(wrapper.text()).toContain("1");
expect(wrapper.text()).toContain("firmware: alert"); expect(wrapper.text()).toContain("firmware: alert");
}); });
}); });

View File

@ -34,6 +34,7 @@ export const Alerts = (props: AlertsProps) =>
<div className="problem-alerts-content"> <div className="problem-alerts-content">
{sortAlerts(props.alerts) {sortAlerts(props.alerts)
.filter(filterIncompleteAlerts) .filter(filterIncompleteAlerts)
.filter(x => x.problem_tag != "farmbot_os.firmware.missing")
.map(x => .map(x =>
<AlertCard key={x.slug + x.created_at} <AlertCard key={x.slug + x.created_at}
alert={x} alert={x}

View File

@ -2,6 +2,7 @@ import { info } from "../toast/toast";
import { semverCompare, SemverResult, FbosVersionFallback } from "../util"; import { semverCompare, SemverResult, FbosVersionFallback } from "../util";
import { Content } from "../constants"; import { Content } from "../constants";
import { Dictionary } from "lodash"; import { Dictionary } from "lodash";
import { t } from "../i18next_wrapper";
const IDEAL_VERSION = const IDEAL_VERSION =
globalConfig.FBOS_END_OF_LIFE_VERSION || FbosVersionFallback.NULL; globalConfig.FBOS_END_OF_LIFE_VERSION || FbosVersionFallback.NULL;
@ -22,7 +23,7 @@ export function createReminderFn() {
!alreadyChecked[version] !alreadyChecked[version]
// Is it up to date? // Is it up to date?
&& semverCompare(version, IDEAL_VERSION) === SemverResult.RIGHT_IS_GREATER && semverCompare(version, IDEAL_VERSION) === SemverResult.RIGHT_IS_GREATER
&& info(Content.OLD_FBOS_REC_UPGRADE); && info(t(Content.OLD_FBOS_REC_UPGRADE));
alreadyChecked[version] = true; // Turn off checks for this version now. alreadyChecked[version] = true; // Turn off checks for this version now.
}; };

View File

@ -24,7 +24,7 @@ describe("unpackStep()", () => {
step: resourceUpdate({ label: "mounted_tool_id", value: 0 }), step: resourceUpdate({ label: "mounted_tool_id", value: 0 }),
resourceIndex: fakeResourceIndex() resourceIndex: fakeResourceIndex()
}); });
expect(result).toEqual(DISMOUNTED); expect(result).toEqual(DISMOUNTED());
}); });
it("unpacks valid tool_ids", () => { it("unpacks valid tool_ids", () => {
@ -37,7 +37,7 @@ describe("unpackStep()", () => {
resourceIndex resourceIndex
}); });
const actionLabel = "Mounted to: Generic Tool"; const actionLabel = "Mounted to: Generic Tool";
const { label, value } = TOOL_MOUNT; const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value); assertGoodness(result, actionLabel, "mounted", label, value);
}); });
@ -47,7 +47,7 @@ describe("unpackStep()", () => {
resourceIndex: fakeResourceIndex() resourceIndex: fakeResourceIndex()
}); });
const actionLabel = "Mounted to: an unknown tool"; const actionLabel = "Mounted to: an unknown tool";
const { label, value } = TOOL_MOUNT; const { label, value } = TOOL_MOUNT();
assertGoodness(result, actionLabel, "mounted", label, value); assertGoodness(result, actionLabel, "mounted", label, value);
}); });

View File

@ -9,15 +9,16 @@ import { GenericPointer } from "farmbot/dist/resources/api_resources";
import { MOUNTED_TO } from "./constants"; import { MOUNTED_TO } from "./constants";
import { DropDownPair, StepWithResourceIndex } from "./interfaces"; import { DropDownPair, StepWithResourceIndex } from "./interfaces";
import { TaggedPoint, TaggedPlantPointer } from "farmbot"; import { TaggedPoint, TaggedPlantPointer } from "farmbot";
import { t } from "../../../i18next_wrapper";
export const TOOL_MOUNT: DropDownItem = { export const TOOL_MOUNT = (): DropDownItem => ({
label: "Tool Mount", value: "tool_mount" label: t("Tool Mount"), value: "tool_mount"
}; });
const NOT_IN_USE: DropDownItem = { label: "Not Mounted", value: 0 }; const NOT_IN_USE = (): DropDownItem => ({ label: t("Not Mounted"), value: 0 });
export const DISMOUNTED: DropDownPair = { export const DISMOUNTED = (): DropDownPair => ({
leftSide: TOOL_MOUNT, leftSide: TOOL_MOUNT(),
rightSide: NOT_IN_USE rightSide: NOT_IN_USE()
}; });
const DEFAULT_TOOL_NAME = "Untitled Tool"; const DEFAULT_TOOL_NAME = "Untitled Tool";
const REMOVED_ACTION = { label: "Removed", value: "removed" }; const REMOVED_ACTION = { label: "Removed", value: "removed" };
@ -30,13 +31,13 @@ function mountTool(i: StepWithResourceIndex): DropDownPair {
if (typeof value === "number" && value > 0) { if (typeof value === "number" && value > 0) {
try { // Good tool id try { // Good tool id
const tool = findToolById(i.resourceIndex, value as number); const tool = findToolById(i.resourceIndex, value as number);
return { leftSide: TOOL_MOUNT, rightSide: mountedTo(tool.body.name) }; return { leftSide: TOOL_MOUNT(), rightSide: mountedTo(tool.body.name) };
} catch { // Bad tool ID or app still loading. } catch { // Bad tool ID or app still loading.
return { leftSide: TOOL_MOUNT, rightSide: mountedTo("an unknown tool") }; return { leftSide: TOOL_MOUNT(), rightSide: mountedTo("an unknown tool") };
} }
} else { } else {
// No tool id // No tool id
return DISMOUNTED; return DISMOUNTED();
} }
} }
@ -55,10 +56,10 @@ function unknownOption(i: StepWithResourceIndex): DropDownPair {
/** The user wants to mark a the `discarded_at` attribute of a Point. */ /** The user wants to mark a the `discarded_at` attribute of a Point. */
function discardPoint(i: StepWithResourceIndex): DropDownPair { function discardPoint(i: StepWithResourceIndex): DropDownPair {
const { resource_id } = i.step.args; const { resource_id } = i.step.args;
const t = const genericPointerBody =
findPointerByTypeAndId(i.resourceIndex, "GenericPointer", resource_id).body; findPointerByTypeAndId(i.resourceIndex, "GenericPointer", resource_id).body;
return { return {
leftSide: pointer2ddi(t as GenericPointer), leftSide: pointer2ddi(genericPointerBody as GenericPointer),
rightSide: REMOVED_ACTION rightSide: REMOVED_ACTION
}; };
} }

View File

@ -48,7 +48,7 @@ var HelperNamespace = (function () {
var T_REGEX = /[.{[(\s]t\(["`]([\w\s{}().,:'\-=\\?\/%!]*)["`],*\s*.*\)/g; var T_REGEX = /[.{[(\s]t\(["`]([\w\s{}().,:'\-=\\?\/%!]*)["`],*\s*.*\)/g;
// '``' // '``'
var C_REGEX = /[`]([\w\s{}().,:'\-=\\?"+!]*)[`].*/g; var C_REGEX = /[`]([\w\s{}().,:'\-=\/\\?"+!]*)[`].*/g;
/** Some additional phrases the regex can't find. */ /** Some additional phrases the regex can't find. */
var EXTRA_TAGS = [ var EXTRA_TAGS = [
@ -60,6 +60,7 @@ var HelperNamespace = (function () {
"Else Execute", "Connecting FarmBot to the Internet", "move to home", "Else Execute", "Connecting FarmBot to the Internet", "move to home",
"emergency stop", "SYNC ERROR", "inactive", "error", "No messages.", "emergency stop", "SYNC ERROR", "inactive", "error", "No messages.",
"back to regimens", "back to sequences", "back to farmware list", "back to regimens", "back to sequences", "back to farmware list",
"Verify Password",
]; ];
/** /**

View File

@ -21,20 +21,20 @@ For example, `sudo docker-compose run web npm run translation-check`._
See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app-into-your-language) for contribution instructions. See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app-into-your-language) for contribution instructions.
Total number of phrases identified by the language helper for translation: __1139__ Total number of phrases identified by the language helper for translation: __1238__
|Language|Percent translated|Translated|Untranslated|Other Translations| |Language|Percent translated|Translated|Untranslated|Other Translations|
|:---:|---:|---:|---:|---:| |:---:|---:|---:|---:|---:|
|da|10%|109|1030|44| |da|8%|105|1133|77|
|de|36%|413|726|141| |de|32%|397|841|168|
|es|88%|1002|137|173| |es|78%|965|273|210|
|fr|90%|1022|117|198| |fr|80%|985|253|242|
|it|8%|91|1048|189| |it|7%|87|1151|215|
|nl|7%|79|1060|161| |nl|6%|75|1163|187|
|pt|6%|71|1068|180| |pt|5%|66|1172|207|
|ru|52%|596|543|221| |ru|46%|575|663|246|
|th|0%|0|1139|0| |th|0%|0|1238|0|
|zh|8%|86|1053|161| |zh|7%|82|1156|187|
**Percent translated** refers to the percent of phrases identified by the **Percent translated** refers to the percent of phrases identified by the
language helper that have been translated. Additional phrases not identified language helper that have been translated. Additional phrases not identified