add none camera type

pull/1642/head
gabrielburnworth 2019-12-27 10:37:54 -08:00
parent 4fa48cb74b
commit 22465a5558
68 changed files with 655 additions and 436 deletions

View File

@ -0,0 +1,17 @@
import { HardwareFlags, FarmwareData } from "../sequences/interfaces";
export const fakeHardwareFlags = (): HardwareFlags => ({
findHomeEnabled: { x: false, y: false, z: false },
stopAtHome: { x: false, y: false, z: false },
stopAtMax: { x: false, y: false, z: false },
negativeOnly: { x: false, y: false, z: false },
axisLength: { x: 0, y: 0, z: 0 },
});
export const fakeFarmwareData = (): FarmwareData => ({
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
cameraDisabled: false,
});

View File

@ -1,11 +0,0 @@
import { HardwareFlags } from "../sequences/interfaces";
export const fakeHardwareFlags = (): HardwareFlags => {
return {
findHomeEnabled: { x: false, y: false, z: false },
stopAtHome: { x: false, y: false, z: false },
stopAtMax: { x: false, y: false, z: false },
negativeOnly: { x: false, y: false, z: false },
axisLength: { x: 0, y: 0, z: 0 },
};
};

View File

@ -9,7 +9,7 @@ import { RawApp as App, AppProps, mapStateToProps } from "../app";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { bot } from "../__test_support__/fake_state/bot"; import { bot } from "../__test_support__/fake_state/bot";
import { import {
fakeUser, fakeWebAppConfig fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv
} from "../__test_support__/fake_state/resources"; } from "../__test_support__/fake_state/resources";
import { fakeState } from "../__test_support__/fake_state"; import { fakeState } from "../__test_support__/fake_state";
import { import {
@ -40,7 +40,8 @@ const fakeProps = (): AppProps => ({
resources: buildResourceIndex().index, resources: buildResourceIndex().index,
autoSync: false, autoSync: false,
alertCount: 0, alertCount: 0,
pings: fakePings() pings: fakePings(),
env: {},
}); });
describe("<App />: Controls Pop-Up", () => { describe("<App />: Controls Pop-Up", () => {
@ -145,7 +146,24 @@ describe("mapStateToProps()", () => {
const config = fakeWebAppConfig(); const config = fakeWebAppConfig();
config.body.x_axis_inverted = true; config.body.x_axis_inverted = true;
state.resources = buildResourceIndex([config]); state.resources = buildResourceIndex([config]);
state.bot.hardware.user_env = { fake: "value" };
const result = mapStateToProps(state); const result = mapStateToProps(state);
expect(result.axisInversion.x).toEqual(true); expect(result.axisInversion.x).toEqual(true);
expect(result.autoSync).toEqual(false);
expect(result.env).toEqual({ fake: "value" });
});
it("returns api props", () => {
const state = fakeState();
const config = fakeFbosConfig();
config.body.auto_sync = true;
config.body.api_migrated = true;
const fakeEnv = fakeFarmwareEnv();
state.resources = buildResourceIndex([config, fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
state.bot.hardware.informational_settings.controller_version = "8.0.0";
const result = mapStateToProps(state);
expect(result.autoSync).toEqual(true);
expect(result.env).toEqual({ [fakeEnv.body.key]: fakeEnv.body.value });
}); });
}); });

View File

@ -9,38 +9,37 @@ import { ControlsPopup } from "../controls_popup";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { bot } from "../__test_support__/fake_state/bot"; import { bot } from "../__test_support__/fake_state/bot";
import { ControlsPopupProps } from "../controls/move/interfaces"; import { ControlsPopupProps } from "../controls/move/interfaces";
import { error } from "../toast/toast";
import { Content, ToolTips } from "../constants";
describe("<ControlsPopup />", () => { describe("<ControlsPopup />", () => {
const fakeProps = (): ControlsPopupProps => { const fakeProps = (): ControlsPopupProps => ({
return { dispatch: jest.fn(),
dispatch: jest.fn(), axisInversion: { x: true, y: false, z: false },
axisInversion: { x: false, y: false, z: false }, botPosition: { x: undefined, y: undefined, z: undefined },
botPosition: { x: undefined, y: undefined, z: undefined }, firmwareSettings: bot.hardware.mcu_params,
firmwareSettings: bot.hardware.mcu_params, xySwap: false,
xySwap: false, arduinoBusy: false,
arduinoBusy: false, stepSize: 100,
stepSize: 100, botOnline: true,
botOnline: true, env: {},
}; });
};
const p = fakeProps();
p.axisInversion.x = true;
const wrapper = mount(<ControlsPopup {...p} />);
afterAll(wrapper.unmount);
it("Has a false initial state", () => { it("Has a false initial state", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
expect(wrapper.state("isOpen")).toBeFalsy(); expect(wrapper.state("isOpen")).toBeFalsy();
}); });
it("Toggles state", () => { it("Toggles state", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
const parent = wrapper.find("i").first(); const parent = wrapper.find("i").first();
parent.simulate("click"); parent.simulate("click");
expect(wrapper.state("isOpen")).toBeTruthy(); expect(wrapper.state("isOpen")).toBeTruthy();
}); });
it("x axis is inverted", () => { it("x axis is inverted", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: true });
const button = wrapper.find("button").at(3); const button = wrapper.find("button").at(3);
expect(button.props().title).toBe("move x axis (100)"); expect(button.props().title).toBe("move x axis (100)");
button.simulate("click"); button.simulate("click");
@ -49,6 +48,8 @@ describe("<ControlsPopup />", () => {
}); });
it("y axis is not inverted", () => { it("y axis is not inverted", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: true });
const button = wrapper.find("button").at(1); const button = wrapper.find("button").at(1);
expect(button.props().title).toBe("move y axis (100)"); expect(button.props().title).toBe("move y axis (100)");
button.simulate("click"); button.simulate("click");
@ -57,6 +58,7 @@ describe("<ControlsPopup />", () => {
}); });
it("disabled when closed", () => { it("disabled when closed", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: false }); wrapper.setState({ isOpen: false });
[0, 1, 2, 3].map((i) => wrapper.find("button").at(i).simulate("click")); [0, 1, 2, 3].map((i) => wrapper.find("button").at(i).simulate("click"));
expect(mockDevice.moveRelative).not.toHaveBeenCalled(); expect(mockDevice.moveRelative).not.toHaveBeenCalled();
@ -65,6 +67,7 @@ describe("<ControlsPopup />", () => {
it("swaps axes", () => { it("swaps axes", () => {
const swappedProps = fakeProps(); const swappedProps = fakeProps();
swappedProps.xySwap = true; swappedProps.xySwap = true;
swappedProps.axisInversion.x = false;
const swapped = mount(<ControlsPopup {...swappedProps} />); const swapped = mount(<ControlsPopup {...swappedProps} />);
swapped.setState({ isOpen: true }); swapped.setState({ isOpen: true });
expect(swapped.state("isOpen")).toBeTruthy(); expect(swapped.state("isOpen")).toBeTruthy();
@ -76,7 +79,25 @@ describe("<ControlsPopup />", () => {
}); });
it("takes photo", () => { it("takes photo", () => {
wrapper.find("button").at(4).simulate("click"); const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: true });
const btn = wrapper.find("button").at(4);
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(mockDevice.takePhoto).toHaveBeenCalled(); expect(mockDevice.takePhoto).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("shows camera as disabled", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = mount(<ControlsPopup {...p} />);
wrapper.setState({ isOpen: true });
const btn = wrapper.find("button").at(4);
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
}); });
}); });

View File

@ -4,7 +4,7 @@ import { init, error } from "./toast/toast";
import { NavBar } from "./nav"; import { NavBar } from "./nav";
import { Everything, TimeSettings } from "./interfaces"; import { Everything, TimeSettings } from "./interfaces";
import { LoadingPlant } from "./loading_plant"; import { LoadingPlant } from "./loading_plant";
import { BotState, Xyz } from "./devices/interfaces"; import { BotState, Xyz, UserEnv } from "./devices/interfaces";
import { ResourceName, TaggedUser, TaggedLog } from "farmbot"; import { ResourceName, TaggedUser, TaggedLog } from "farmbot";
import { import {
maybeFetchUser, maybeFetchUser,
@ -30,6 +30,7 @@ import { isBotOnline } from "./devices/must_be_online";
import { getStatus } from "./connectivity/reducer_support"; import { getStatus } from "./connectivity/reducer_support";
import { getAllAlerts } from "./messages/state_to_props"; import { getAllAlerts } from "./messages/state_to_props";
import { PingDictionary } from "./devices/connectivity/qos"; import { PingDictionary } from "./devices/connectivity/qos";
import { getEnv, getShouldDisplayFn } from "./farmware/state_to_props";
/** For the logger module */ /** For the logger module */
init(); init();
@ -52,11 +53,14 @@ export interface AppProps {
autoSync: boolean; autoSync: boolean;
alertCount: number; alertCount: number;
pings: PingDictionary; pings: PingDictionary;
env: UserEnv;
} }
export function mapStateToProps(props: Everything): AppProps { export function mapStateToProps(props: Everything): AppProps {
const webAppConfigValue = getWebAppConfigValue(() => props); const webAppConfigValue = getWebAppConfigValue(() => props);
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index)); const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
return { return {
timeSettings: maybeGetTimeSettings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index),
dispatch: props.dispatch, dispatch: props.dispatch,
@ -78,7 +82,8 @@ export function mapStateToProps(props: Everything): AppProps {
resources: props.resources.index, resources: props.resources.index,
autoSync: !!(fbosConfig && fbosConfig.auto_sync), autoSync: !!(fbosConfig && fbosConfig.auto_sync),
alertCount: getAllAlerts(props.resources).length, alertCount: getAllAlerts(props.resources).length,
pings: props.bot.connectivity.pings pings: props.bot.connectivity.pings,
env,
}; };
} }
/** Time at which the app gives up and asks the user to refresh */ /** Time at which the app gives up and asks the user to refresh */
@ -147,6 +152,7 @@ export class RawApp extends React.Component<AppProps, {}> {
xySwap={this.props.xySwap} xySwap={this.props.xySwap}
arduinoBusy={!!this.props.bot.hardware.informational_settings.busy} arduinoBusy={!!this.props.bot.hardware.informational_settings.busy}
botOnline={isBotOnline(sync_status, getStatus(bot2mqtt))} botOnline={isBotOnline(sync_status, getStatus(bot2mqtt))}
env={this.props.env}
stepSize={this.props.bot.stepSize} />} stepSize={this.props.bot.stepSize} />}
</div>; </div>;
} }

View File

@ -285,6 +285,9 @@ export namespace ToolTips {
trim(`Snaps a photo using the device camera. Select the camera type trim(`Snaps a photo using the device camera. Select the camera type
on the Device page.`); on the Device page.`);
export const SELECT_A_CAMERA =
trim(`Select a camera on the Device page to take photos.`);
export const MARK_AS = export const MARK_AS =
trim(`The Mark As step allows FarmBot to programmatically edit the trim(`The Mark As step allows FarmBot to programmatically edit the
properties of the UTM, plants, and weeds from within a sequence. properties of the UTM, plants, and weeds from within a sequence.
@ -822,6 +825,9 @@ export namespace Content {
export const NOT_AVAILABLE_WHEN_OFFLINE = export const NOT_AVAILABLE_WHEN_OFFLINE =
trim(`Not available when device is offline.`); trim(`Not available when device is offline.`);
export const NO_CAMERA_SELECTED =
trim(`No camera selected`);
} }
export namespace TourContent { export namespace TourContent {

View File

@ -24,6 +24,7 @@ describe("<Controls />", () => {
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
sensorReadings: [], sensorReadings: [],
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
env: {},
}); });
it("shows webcam widget", () => { it("shows webcam widget", () => {

View File

@ -1,6 +1,6 @@
import { mapStateToProps } from "../state_to_props"; import { mapStateToProps } from "../state_to_props";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { fakeUser } from "../../__test_support__/fake_state/resources"; import { fakeUser, fakeFarmwareEnv } from "../../__test_support__/fake_state/resources";
import { fakeState } from "../../__test_support__/fake_state"; import { fakeState } from "../../__test_support__/fake_state";
describe("mapStateToProps()", () => { describe("mapStateToProps()", () => {
@ -10,4 +10,14 @@ describe("mapStateToProps()", () => {
const result = mapStateToProps(state); const result = mapStateToProps(state);
expect(result.timeSettings).toEqual({ utcOffset: 0, hour24: false }); expect(result.timeSettings).toEqual({ utcOffset: 0, hour24: false });
}); });
it("returns api props", () => {
const state = fakeState();
const fakeEnv = fakeFarmwareEnv();
state.resources = buildResourceIndex([fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
state.bot.hardware.informational_settings.controller_version = "8.0.0";
const result = mapStateToProps(state);
expect(result.env).toEqual({ [fakeEnv.body.key]: fakeEnv.body.value });
});
}); });

View File

@ -29,6 +29,7 @@ export class RawControls extends React.Component<Props, {}> {
move = () => <Move move = () => <Move
bot={this.props.bot} bot={this.props.bot}
env={this.props.env}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
arduinoBusy={this.arduinoBusy} arduinoBusy={this.arduinoBusy}
botToMqttStatus={this.props.botToMqttStatus} botToMqttStatus={this.props.botToMqttStatus}

View File

@ -1,4 +1,6 @@
import { BotState, Xyz, BotPosition, ShouldDisplay } from "../devices/interfaces"; import {
BotState, Xyz, BotPosition, ShouldDisplay, UserEnv
} from "../devices/interfaces";
import { Vector3, McuParams } from "farmbot/dist"; import { Vector3, McuParams } from "farmbot/dist";
import { import {
TaggedWebcamFeed, TaggedWebcamFeed,
@ -22,6 +24,7 @@ export interface Props {
getWebAppConfigVal: GetWebAppConfigValue; getWebAppConfigVal: GetWebAppConfigValue;
sensorReadings: TaggedSensorReading[]; sensorReadings: TaggedSensorReading[];
timeSettings: TimeSettings; timeSettings: TimeSettings;
env: UserEnv;
} }
export interface AxisDisplayGroupProps { export interface AxisDisplayGroupProps {

View File

@ -15,6 +15,8 @@ import { mount } from "enzyme";
import { JogButtons } from "../jog_buttons"; import { JogButtons } from "../jog_buttons";
import { JogMovementControlsProps } from "../interfaces"; import { JogMovementControlsProps } from "../interfaces";
import { bot } from "../../../__test_support__/fake_state/bot"; import { bot } from "../../../__test_support__/fake_state/bot";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<JogButtons/>", function () { describe("<JogButtons/>", function () {
const jogButtonProps = (): JogMovementControlsProps => { const jogButtonProps = (): JogMovementControlsProps => {
@ -26,6 +28,7 @@ describe("<JogButtons/>", function () {
firmwareSettings: bot.hardware.mcu_params, firmwareSettings: bot.hardware.mcu_params,
xySwap: false, xySwap: false,
doFindHome: false, doFindHome: false,
env: {},
}; };
}; };
@ -60,8 +63,12 @@ describe("<JogButtons/>", function () {
it("takes photo", () => { it("takes photo", () => {
const jogButtons = mount(<JogButtons {...jogButtonProps()} />); const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
jogButtons.find("button").at(0).simulate("click"); const cameraBtn = jogButtons.find("button").at(0);
expect(cameraBtn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
cameraBtn.simulate("click");
expect(mockDevice.takePhoto).toHaveBeenCalled(); expect(mockDevice.takePhoto).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
}); });
it("error taking photo", () => { it("error taking photo", () => {
@ -71,6 +78,18 @@ describe("<JogButtons/>", function () {
expect(mockDevice.takePhoto).toHaveBeenCalled(); expect(mockDevice.takePhoto).toHaveBeenCalled();
}); });
it("shows camera as disabled", () => {
const p = jogButtonProps();
p.env = { camera: "NONE" };
const jogButtons = mount(<JogButtons {...p} />);
const cameraBtn = jogButtons.find("button").at(0);
expect(cameraBtn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
cameraBtn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
});
it("has unswapped xy jog buttons", () => { it("has unswapped xy jog buttons", () => {
const jogButtons = mount(<JogButtons {...jogButtonProps()} />); const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
const button = jogButtons.find("button").at(6); const button = jogButtons.find("button").at(6);

View File

@ -1,16 +1,15 @@
const mockDevice = { const mockDevice = { moveAbsolute: jest.fn(() => Promise.resolve()) };
moveAbsolute: jest.fn(() => { return Promise.resolve(); }), jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
};
jest.mock("../../../device", () => ({ jest.mock("../../../config_storage/actions", () => ({
getDevice: () => (mockDevice) toggleWebAppBool: jest.fn(),
})); }));
jest.mock("../../../config_storage/actions", () => { jest.mock("../../../account/dev/dev_support", () => ({
return { DevSettings: {
toggleWebAppBool: jest.fn() futureFeaturesEnabled: () => false,
}; }
}); }));
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
@ -26,16 +25,15 @@ import { clickButton } from "../../../__test_support__/helpers";
describe("<Move />", () => { describe("<Move />", () => {
const mockConfig: Dictionary<boolean> = {}; const mockConfig: Dictionary<boolean> = {};
function fakeProps(): MoveProps { const fakeProps = (): MoveProps => ({
return { dispatch: jest.fn(),
dispatch: jest.fn(), bot: bot,
bot: bot, arduinoBusy: false,
arduinoBusy: false, botToMqttStatus: "up",
botToMqttStatus: "up", firmwareSettings: bot.hardware.mcu_params,
firmwareSettings: bot.hardware.mcu_params, getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), env: {},
}; });
}
it("has default elements", () => { it("has default elements", () => {
const wrapper = mount(<Move {...fakeProps()} />); const wrapper = mount(<Move {...fakeProps()} />);

View File

@ -1,4 +1,4 @@
import { BotPosition, BotState } from "../../devices/interfaces"; import { BotPosition, BotState, UserEnv } from "../../devices/interfaces";
import { McuParams, Xyz } from "farmbot"; import { McuParams, Xyz } from "farmbot";
import { NetworkState } from "../../connectivity/interfaces"; import { NetworkState } from "../../connectivity/interfaces";
import { GetWebAppConfigValue } from "../../config_storage/actions"; import { GetWebAppConfigValue } from "../../config_storage/actions";
@ -14,6 +14,7 @@ export interface MoveProps {
botToMqttStatus: NetworkState; botToMqttStatus: NetworkState;
firmwareSettings: McuParams; firmwareSettings: McuParams;
getWebAppConfigVal: GetWebAppConfigValue; getWebAppConfigVal: GetWebAppConfigValue;
env: UserEnv;
} }
export interface DirectionButtonProps { export interface DirectionButtonProps {
@ -47,6 +48,7 @@ interface JogMovementControlsBaseProps extends DirectionAxesProps {
stepSize: number; stepSize: number;
arduinoBusy: boolean; arduinoBusy: boolean;
xySwap: boolean; xySwap: boolean;
env: UserEnv;
} }
export interface JogMovementControlsProps extends JogMovementControlsBaseProps { export interface JogMovementControlsProps extends JogMovementControlsBaseProps {

View File

@ -5,6 +5,9 @@ import { JogMovementControlsProps } from "./interfaces";
import { getDevice } from "../../device"; import { getDevice } from "../../device";
import { buildDirectionProps } from "./direction_axes_props"; import { buildDirectionProps } from "./direction_axes_props";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
const DEFAULT_STEP_SIZE = 100; const DEFAULT_STEP_SIZE = 100;
/* /*
@ -20,35 +23,37 @@ export function JogButtons(props: JogMovementControlsProps) {
const directionAxesProps = buildDirectionProps(props); const directionAxesProps = buildDirectionProps(props);
const rightLeft = xySwap ? "y" : "x"; const rightLeft = xySwap ? "y" : "x";
const upDown = xySwap ? "x" : "y"; const upDown = xySwap ? "x" : "y";
const commonProps = {
steps: stepSize || DEFAULT_STEP_SIZE,
disabled: arduinoBusy
};
const camDisabled = cameraBtnProps(props.env);
return <table className="jog-table"> return <table className="jog-table">
<tbody> <tbody>
<tr> <tr>
<td> <td>
<button <button
className="i fa fa-camera arrow-button fb-button" className={`fa fa-camera arrow-button fb-button ${
title={t("Take a photo")} camDisabled.class}`}
onClick={() => getDevice().takePhoto().catch(() => { })} /> title={camDisabled.title || t("Take a photo")}
onClick={camDisabled.click ||
(() => getDevice().takePhoto().catch(() => { }))} />
</td> </td>
<td /> <td />
<td /> <td />
<td> <td>
<DirectionButton <DirectionButton {...commonProps}
axis={upDown} axis={upDown}
direction="up" direction="up"
directionAxisProps={directionAxesProps[upDown]} directionAxisProps={directionAxesProps[upDown]} />
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
</td> </td>
<td /> <td />
<td /> <td />
<td> <td>
<DirectionButton <DirectionButton {...commonProps}
axis="z" axis="z"
direction="up" direction="up"
directionAxisProps={directionAxesProps.z} directionAxisProps={directionAxesProps.z} />
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -61,37 +66,29 @@ export function JogButtons(props: JogMovementControlsProps) {
</td> </td>
<td /> <td />
<td> <td>
<DirectionButton <DirectionButton {...commonProps}
axis={rightLeft} axis={rightLeft}
direction="left" direction="left"
directionAxisProps={directionAxesProps[rightLeft]} directionAxisProps={directionAxesProps[rightLeft]} />
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
</td> </td>
<td> <td>
<DirectionButton <DirectionButton {...commonProps}
axis={upDown} axis={upDown}
direction="down" direction="down"
directionAxisProps={directionAxesProps[upDown]} directionAxisProps={directionAxesProps[upDown]} />
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
</td> </td>
<td> <td>
<DirectionButton <DirectionButton {...commonProps}
axis={rightLeft} axis={rightLeft}
direction="right" direction="right"
directionAxisProps={directionAxesProps[rightLeft]} directionAxisProps={directionAxesProps[rightLeft]} />
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
</td> </td>
<td /> <td />
<td> <td>
<DirectionButton <DirectionButton {...commonProps}
axis="z" axis="z"
direction="down" direction="down"
directionAxisProps={directionAxesProps.z} directionAxisProps={directionAxesProps.z} />
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { McuParams } from "farmbot"; import { McuParams } from "farmbot";
import { BotPosition } from "../../devices/interfaces"; import { BotPosition, UserEnv } from "../../devices/interfaces";
import { changeStepSize } from "../../devices/actions"; import { changeStepSize } from "../../devices/actions";
import { StepSizeSelector } from "./step_size_selector"; import { StepSizeSelector } from "./step_size_selector";
import { GetWebAppBool } from "./interfaces"; import { GetWebAppBool } from "./interfaces";
@ -15,6 +15,7 @@ interface JogControlsGroupProps {
getValue: GetWebAppBool; getValue: GetWebAppBool;
arduinoBusy: boolean; arduinoBusy: boolean;
firmwareSettings: McuParams; firmwareSettings: McuParams;
env: UserEnv;
} }
export const JogControlsGroup = (props: JogControlsGroupProps) => { export const JogControlsGroup = (props: JogControlsGroupProps) => {
@ -38,6 +39,7 @@ export const JogControlsGroup = (props: JogControlsGroupProps) => {
z: getValue(BooleanSetting.z_axis_inverted) z: getValue(BooleanSetting.z_axis_inverted)
}} }}
arduinoBusy={arduinoBusy} arduinoBusy={arduinoBusy}
env={props.env}
firmwareSettings={firmwareSettings} firmwareSettings={firmwareSettings}
xySwap={getValue(BooleanSetting.xy_swap)} xySwap={getValue(BooleanSetting.xy_swap)}
doFindHome={getValue(BooleanSetting.home_button_homing)} /> doFindHome={getValue(BooleanSetting.home_button_homing)} />

View File

@ -47,6 +47,7 @@ export class Move extends React.Component<MoveProps, {}> {
botPosition={locationData.position} botPosition={locationData.position}
getValue={this.getValue} getValue={this.getValue}
arduinoBusy={this.props.arduinoBusy} arduinoBusy={this.props.arduinoBusy}
env={this.props.env}
firmwareSettings={this.props.firmwareSettings} /> firmwareSettings={this.props.firmwareSettings} />
<BotPositionRows <BotPositionRows
locationData={locationData} locationData={locationData}

View File

@ -3,31 +3,23 @@ import {
selectAllPeripherals, selectAllPeripherals,
selectAllWebcamFeeds, selectAllWebcamFeeds,
selectAllSensors, selectAllSensors,
maybeGetDevice,
selectAllSensorReadings, selectAllSensorReadings,
maybeGetTimeSettings maybeGetTimeSettings
} from "../resources/selectors"; } from "../resources/selectors";
import { Props } from "./interfaces"; import { Props } from "./interfaces";
import { import { validFwConfig } from "../util";
validFwConfig,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../util";
import { getWebAppConfigValue } from "../config_storage/actions"; import { getWebAppConfigValue } from "../config_storage/actions";
import { getFirmwareConfig } from "../resources/getters"; import { getFirmwareConfig } from "../resources/getters";
import { uniq } from "lodash"; import { uniq } from "lodash";
import { getStatus } from "../connectivity/reducer_support"; import { getStatus } from "../connectivity/reducer_support";
import { DevSettings } from "../account/dev/dev_support"; import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
export function mapStateToProps(props: Everything): Props { export function mapStateToProps(props: Everything): Props {
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index)); const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
const { mcu_params } = props.bot.hardware; const { mcu_params } = props.bot.hardware;
const device = maybeGetDevice(props.resources.index); const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const installedOsVersion = determineInstalledOsVersion(props.bot, device); const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
return { return {
feeds: selectAllWebcamFeeds(props.resources.index), feeds: selectAllWebcamFeeds(props.resources.index),
@ -41,5 +33,6 @@ export function mapStateToProps(props: Everything): Props {
shouldDisplay, shouldDisplay,
sensorReadings: selectAllSensorReadings(props.resources.index), sensorReadings: selectAllSensorReadings(props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index),
env,
}; };
} }

View File

@ -5,6 +5,10 @@ import { buildDirectionProps } from "./controls/move/direction_axes_props";
import { ControlsPopupProps } from "./controls/move/interfaces"; import { ControlsPopupProps } from "./controls/move/interfaces";
import { commandErr } from "./devices/actions"; import { commandErr } from "./devices/actions";
import { mapPanelClassName } from "./farm_designer/map/util"; import { mapPanelClassName } from "./farm_designer/map/util";
import {
cameraBtnProps
} from "./devices/components/fbos_settings/camera_selection";
import { t } from "./i18next_wrapper";
interface State { interface State {
isOpen: boolean; isOpen: boolean;
@ -23,40 +27,38 @@ export class ControlsPopup
const directionAxesProps = buildDirectionProps(this.props); const directionAxesProps = buildDirectionProps(this.props);
const rightLeft = xySwap ? "y" : "x"; const rightLeft = xySwap ? "y" : "x";
const upDown = xySwap ? "x" : "y"; const upDown = xySwap ? "x" : "y";
const movementDisabled = !isOpen || arduinoBusy || !botOnline;
const commonProps = { steps: stepSize, disabled: movementDisabled };
const camDisabled = cameraBtnProps(this.props.env);
return <div return <div
className={`controls-popup ${isOpen} ${mapPanelClassName()}`}> className={`controls-popup ${isOpen} ${mapPanelClassName()}`}>
<i className="fa fa-crosshairs" <i className="fa fa-crosshairs"
onClick={this.toggle("isOpen")} /> onClick={this.toggle("isOpen")} />
<div className="controls-popup-menu-outer"> <div className="controls-popup-menu-outer">
<div className="controls-popup-menu-inner"> <div className="controls-popup-menu-inner">
<DirectionButton <DirectionButton {...commonProps}
axis={rightLeft} axis={rightLeft}
direction="right" direction="right"
directionAxisProps={directionAxesProps[rightLeft]} directionAxisProps={directionAxesProps[rightLeft]} />
steps={stepSize} <DirectionButton {...commonProps}
disabled={!isOpen || arduinoBusy || !botOnline} />
<DirectionButton
axis={upDown} axis={upDown}
direction="up" direction="up"
directionAxisProps={directionAxesProps[upDown]} directionAxisProps={directionAxesProps[upDown]} />
steps={stepSize} <DirectionButton {...commonProps}
disabled={!isOpen || arduinoBusy || !botOnline} />
<DirectionButton
axis={upDown} axis={upDown}
direction="down" direction="down"
directionAxisProps={directionAxesProps[upDown]} directionAxisProps={directionAxesProps[upDown]} />
steps={stepSize} <DirectionButton {...commonProps}
disabled={!isOpen || arduinoBusy || !botOnline} />
<DirectionButton
axis={rightLeft} axis={rightLeft}
direction="left" direction="left"
directionAxisProps={directionAxesProps[rightLeft]} directionAxisProps={directionAxesProps[rightLeft]} />
steps={stepSize}
disabled={!isOpen || arduinoBusy || !botOnline} />
<button <button
className="i fa fa-camera arrow-button fb-button brown" className={`fa fa-camera arrow-button fb-button brown ${
disabled={!botOnline} camDisabled.class}`}
onClick={() => getDevice().takePhoto().catch(commandErr("Photo"))} /> disabled={!isOpen || !botOnline}
title={camDisabled.title || t("Take a photo")}
onClick={camDisabled.click ||
(() => getDevice().takePhoto().catch(commandErr("Photo")))} />
</div> </div>
</div> </div>
</div>; </div>;

View File

@ -279,6 +279,7 @@
&.pseudo-disabled { &.pseudo-disabled {
background: $medium_light_gray !important; background: $medium_light_gray !important;
box-shadow: 0 2px 0px 0px lighten($medium_light_gray, 5%) !important; box-shadow: 0 2px 0px 0px lighten($medium_light_gray, 5%) !important;
border-bottom: none !important;
&:focus, &:focus,
&:hover, &:hover,
&.active { &.active {

View File

@ -1,26 +1,20 @@
const mockDevice = { const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve()) };
setUserEnv: jest.fn(() => { return Promise.resolve(); }), jest.mock("../../../../device", () => ({ getDevice: () => mockDevice }));
};
jest.mock("../../../../device", () => ({
getDevice: () => mockDevice
}));
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import { CameraSelection } from "../camera_selection"; import { CameraSelection, cameraDisabled } from "../camera_selection";
import { CameraSelectionProps } from "../interfaces"; import { CameraSelectionProps } from "../interfaces";
import { info, error } from "../../../../toast/toast"; import { info, error } from "../../../../toast/toast";
describe("<CameraSelection/>", () => { describe("<CameraSelection/>", () => {
const fakeProps = (): CameraSelectionProps => { const fakeProps = (): CameraSelectionProps => ({
return { env: {},
env: {}, botOnline: true,
botOnline: true, shouldDisplay: () => false,
shouldDisplay: () => false, saveFarmwareEnv: jest.fn(),
saveFarmwareEnv: jest.fn(), dispatch: jest.fn(),
dispatch: jest.fn(), });
};
};
it("doesn't render camera", () => { it("doesn't render camera", () => {
const cameraSelection = mount(<CameraSelection {...fakeProps()} />); const cameraSelection = mount(<CameraSelection {...fakeProps()} />);
@ -66,3 +60,15 @@ describe("<CameraSelection/>", () => {
expect(p.saveFarmwareEnv).toHaveBeenCalledWith("camera", "\"mycamera\""); expect(p.saveFarmwareEnv).toHaveBeenCalledWith("camera", "\"mycamera\"");
}); });
}); });
describe("cameraDisabled()", () => {
it("returns enabled", () => {
expect(cameraDisabled({ camera: "USB" })).toEqual(false);
expect(cameraDisabled({ camera: "" })).toEqual(false);
});
it("returns disabled", () => {
expect(cameraDisabled({ camera: "none" })).toEqual(true);
expect(cameraDisabled({ camera: "\"NONE\"" })).toEqual(true);
});
});

View File

@ -6,25 +6,56 @@ import {
import { info, success, error } from "../../../toast/toast"; import { info, success, error } from "../../../toast/toast";
import { getDevice } from "../../../device"; import { getDevice } from "../../../device";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { Feature } from "../../interfaces"; import { Feature, UserEnv } from "../../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Content, ToolTips } from "../../../constants";
/** Check if the camera has been disabled. */
export const cameraDisabled = (env: UserEnv): boolean =>
parseCameraSelection(env) === Camera.NONE;
/** `disabled` and `title` props for buttons with actions that use the camera. */
export const cameraBtnProps = (env: UserEnv) => {
const disabled = cameraDisabled(env);
return disabled
? {
class: "pseudo-disabled",
click: () =>
error(t(ToolTips.SELECT_A_CAMERA), t(Content.NO_CAMERA_SELECTED)),
title: t(Content.NO_CAMERA_SELECTED)
}
: { class: "", click: undefined, title: "" };
};
enum Camera {
USB = "USB",
RPI = "RPI",
NONE = "NONE",
}
const parseCameraSelection = (env: UserEnv): Camera => {
const camera = env["camera"]?.toUpperCase();
if (camera?.includes(Camera.NONE)) {
return Camera.NONE;
} else if (camera?.includes(Camera.RPI)) {
return Camera.RPI;
} else {
return Camera.USB;
}
};
const CAMERA_CHOICES = () => ([ const CAMERA_CHOICES = () => ([
{ label: t("USB Camera"), value: "USB" }, { label: t("USB Camera"), value: Camera.USB },
{ label: t("Raspberry Pi Camera"), value: "RPI" } { label: t("Raspberry Pi Camera"), value: Camera.RPI },
{ label: t("None"), value: Camera.NONE },
]); ]);
const CAMERA_CHOICES_DDI = () => { const CAMERA_CHOICES_DDI = () => {
const CHOICES = CAMERA_CHOICES(); const CHOICES = CAMERA_CHOICES();
return { return {
[CHOICES[0].value]: { [CHOICES[0].value]: { label: CHOICES[0].label, value: CHOICES[0].value },
label: CHOICES[0].label, [CHOICES[1].value]: { label: CHOICES[1].label, value: CHOICES[1].value },
value: CHOICES[0].value [CHOICES[2].value]: { label: CHOICES[2].label, value: CHOICES[2].value },
},
[CHOICES[1].value]: {
label: CHOICES[1].label,
value: CHOICES[1].value
}
}; };
}; };
@ -35,12 +66,8 @@ export class CameraSelection
cameraStatus: "" cameraStatus: ""
}; };
selectedCamera(): DropDownItem { selectedCamera = (): DropDownItem =>
const camera = this.props.env["camera"]; CAMERA_CHOICES_DDI()[parseCameraSelection(this.props.env)]
return camera
? CAMERA_CHOICES_DDI()[JSON.parse(camera)]
: CAMERA_CHOICES_DDI()["USB"];
}
sendOffConfig = (selectedCamera: DropDownItem) => { sendOffConfig = (selectedCamera: DropDownItem) => {
const { props } = this; const { props } = this;

View File

@ -209,8 +209,8 @@ export interface SensorsProps {
export interface FarmwareProps { export interface FarmwareProps {
dispatch: Function; dispatch: Function;
env: Partial<WD_ENV>; wDEnv: Partial<WD_ENV>;
user_env: UserEnv; env: UserEnv;
images: TaggedImage[]; images: TaggedImage[];
currentImage: TaggedImage | undefined; currentImage: TaggedImage | undefined;
botToMqttStatus: NetworkState; botToMqttStatus: NetworkState;

View File

@ -1,38 +1,28 @@
import { Everything } from "../interfaces"; import { Everything } from "../interfaces";
import { Props, Feature } from "./interfaces"; import { Props } from "./interfaces";
import { import {
selectAllImages, selectAllImages,
getDeviceAccountSettings, getDeviceAccountSettings,
maybeGetDevice,
maybeGetTimeSettings, maybeGetTimeSettings,
} from "../resources/selectors"; } from "../resources/selectors";
import { import {
sourceFbosConfigValue, sourceFwConfigValue sourceFbosConfigValue, sourceFwConfigValue
} from "./components/source_config_value"; } from "./components/source_config_value";
import { validFwConfig, validFbosConfig } from "../util";
import { import {
determineInstalledOsVersion, validFwConfig, validFbosConfig, saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn
createShouldDisplayFn as shouldDisplayFunc
} from "../util";
import {
saveOrEditFarmwareEnv, reduceFarmwareEnv
} from "../farmware/state_to_props"; } from "../farmware/state_to_props";
import { getFbosConfig, getFirmwareConfig, getWebAppConfig } from "../resources/getters"; import {
import { DevSettings } from "../account/dev/dev_support"; getFbosConfig, getFirmwareConfig, getWebAppConfig
} from "../resources/getters";
import { getAllAlerts } from "../messages/state_to_props"; import { getAllAlerts } from "../messages/state_to_props";
export function mapStateToProps(props: Everything): Props { export function mapStateToProps(props: Everything): Props {
const { hardware } = props.bot; const { hardware } = props.bot;
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index)); const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const firmwareConfig = validFwConfig(getFirmwareConfig(props.resources.index)); const firmwareConfig = validFwConfig(getFirmwareConfig(props.resources.index));
const installedOsVersion = determineInstalledOsVersion( const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
props.bot, maybeGetDevice(props.resources.index)); const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(installedOsVersion,
props.bot.minOsFeatureData,
fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;
const webAppConfig = getWebAppConfig(props.resources.index); const webAppConfig = getWebAppConfig(props.resources.index);
if (!webAppConfig) { if (!webAppConfig) {
throw new Error("Missing web app config"); throw new Error("Missing web app config");

View File

@ -13,7 +13,6 @@ import {
findSequenceById, findSequenceById,
findRegimenById, findRegimenById,
getDeviceAccountSettings, getDeviceAccountSettings,
maybeGetDevice,
maybeGetTimeSettings maybeGetTimeSettings
} from "../../resources/selectors"; } from "../../resources/selectors";
import { import {
@ -22,11 +21,7 @@ import {
TaggedRegimen TaggedRegimen
} from "farmbot"; } from "farmbot";
import { DropDownItem } from "../../ui/index"; import { DropDownItem } from "../../ui/index";
import { import { validFbosConfig } from "../../util";
validFbosConfig,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../../util";
import { import {
sourceFbosConfigValue sourceFbosConfigValue
} from "../../devices/components/source_config_value"; } from "../../devices/components/source_config_value";
@ -34,7 +29,7 @@ import { hasId } from "../../resources/util";
import { ExecutableType } from "farmbot/dist/resources/api_resources"; import { ExecutableType } from "farmbot/dist/resources/api_resources";
import { getFbosConfig } from "../../resources/getters"; import { getFbosConfig } from "../../resources/getters";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support"; import { getShouldDisplayFn } from "../../farmware/state_to_props";
export const formatTime = (input: string, timeSettings: TimeSettings) => { export const formatTime = (input: string, timeSettings: TimeSettings) => {
const iso = new Date(input).toISOString(); const iso = new Date(input).toISOString();
@ -143,12 +138,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
const autoSyncEnabled = const autoSyncEnabled =
!!sourceFbosConfigValue(fbosConfig, configuration)("auto_sync").value; !!sourceFbosConfigValue(fbosConfig, configuration)("auto_sync").value;
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
return { return {
deviceTimezone: dev deviceTimezone: dev
.body .body
@ -167,6 +156,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
timeSettings: maybeGetTimeSettings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index),
autoSyncEnabled, autoSyncEnabled,
resources: props.resources.index, resources: props.resources.index,
shouldDisplay, shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
}; };
} }

View File

@ -9,24 +9,17 @@ import {
selectAllPlantTemplates, selectAllPlantTemplates,
selectAllSensorReadings, selectAllSensorReadings,
selectAllSensors, selectAllSensors,
maybeGetDevice,
maybeGetTimeSettings maybeGetTimeSettings
} from "../resources/selectors"; } from "../resources/selectors";
import { import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
validBotLocationData, validFwConfig, unpackUUID,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../util";
import { getWebAppConfigValue } from "../config_storage/actions"; import { getWebAppConfigValue } from "../config_storage/actions";
import { Props } from "./interfaces"; import { Props } from "./interfaces";
import { TaggedPlant } from "./map/interfaces"; import { TaggedPlant } from "./map/interfaces";
import { RestResources } from "../resources/interfaces"; import { RestResources } from "../resources/interfaces";
import { isString, uniq, chain } from "lodash"; import { isString, uniq, chain } from "lodash";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { Feature } from "../devices/interfaces"; import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
import { reduceFarmwareEnv } from "../farmware/state_to_props";
import { getFirmwareConfig } from "../resources/getters"; import { getFirmwareConfig } from "../resources/getters";
import { DevSettings } from "../account/dev/dev_support";
import { calcMicrostepsPerMm } from "../controls/move/direction_axes_props"; import { calcMicrostepsPerMm } from "../controls/move/direction_axes_props";
const plantFinder = (plants: TaggedPlant[]) => const plantFinder = (plants: TaggedPlant[]) =>
@ -84,14 +77,8 @@ export function mapStateToProps(props: Everything): Props {
.reverse() .reverse()
.value(); .value();
const installedOsVersion = determineInstalledOsVersion( const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
props.bot, maybeGetDevice(props.resources.index)); const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;
const cameraCalibrationData = { const cameraCalibrationData = {
scale: env["CAMERA_CALIBRATION_coord_scale"], scale: env["CAMERA_CALIBRATION_coord_scale"],

View File

@ -93,7 +93,7 @@ describe("<FarmwareForm />", () => {
const fakeProps = (): FarmwareFormProps => { const fakeProps = (): FarmwareFormProps => {
return { return {
farmware: fakeFarmware(), farmware: fakeFarmware(),
user_env: {}, env: {},
dispatch: jest.fn(), dispatch: jest.fn(),
shouldDisplay: () => false, shouldDisplay: () => false,
saveFarmwareEnv: jest.fn(), saveFarmwareEnv: jest.fn(),

View File

@ -16,8 +16,8 @@ describe("<FarmwarePage />", () => {
const fakeProps = (): FarmwareProps => ({ const fakeProps = (): FarmwareProps => ({
farmwares: fakeFarmwares(), farmwares: fakeFarmwares(),
botToMqttStatus: "up", botToMqttStatus: "up",
wDEnv: {},
env: {}, env: {},
user_env: {},
dispatch: jest.fn(), dispatch: jest.fn(),
currentImage: undefined, currentImage: undefined,
images: [], images: [],

View File

@ -1,14 +1,15 @@
let mockLastUrlChunk = "farmware"; let mockLastUrlChunk = "farmware";
jest.mock("../../util/urls", () => ({
urlFriendly: jest.fn(x => x),
lastUrlChunk: jest.fn(() => mockLastUrlChunk)
}));
jest.mock("../../util/urls", () => { jest.mock("../../redux/store", () => ({ store: { dispatch: jest.fn() } }));
return {
urlFriendly: jest.fn(x => x),
lastUrlChunk: jest.fn(() => mockLastUrlChunk)
};
});
jest.mock("../../redux/store", () => ({ jest.mock("../../account/dev/dev_support", () => ({
store: { dispatch: jest.fn() } DevSettings: {
futureFeaturesEnabled: () => false,
}
})); }));
import { setActiveFarmwareByName } from "../set_active_farmware_by_name"; import { setActiveFarmwareByName } from "../set_active_farmware_by_name";

View File

@ -43,7 +43,7 @@ describe("mapStateToProps()", () => {
const state = fakeState(); const state = fakeState();
state.bot.hardware.user_env = env; state.bot.hardware.user_env = env;
const props = mapStateToProps(state); const props = mapStateToProps(state);
expect(props.user_env).toEqual(env); expect(props.env).toEqual(env);
}); });
it("returns API farmware env", () => { it("returns API farmware env", () => {
@ -53,7 +53,7 @@ describe("mapStateToProps()", () => {
DevSettings.MAX_FBOS_VERSION_OVERRIDE; DevSettings.MAX_FBOS_VERSION_OVERRIDE;
state.resources = buildResourceIndex([fakeFarmwareEnv()]); state.resources = buildResourceIndex([fakeFarmwareEnv()]);
const props = mapStateToProps(state); const props = mapStateToProps(state);
expect(props.user_env).toEqual({ expect(props.env).toEqual({
fake_FarmwareEnv_key: "fake_FarmwareEnv_value" fake_FarmwareEnv_key: "fake_FarmwareEnv_value"
}); });
}); });

View File

@ -1,6 +1,5 @@
const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve({})) }; const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve({})) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../actions", () => ({ scanImage: jest.fn() })); jest.mock("../actions", () => ({ scanImage: jest.fn() }));
jest.mock("../../images/actions", () => ({ selectImage: jest.fn() })); jest.mock("../../images/actions", () => ({ selectImage: jest.fn() }));
@ -11,12 +10,15 @@ import { CameraCalibrationProps } from "../interfaces";
import { scanImage } from "../actions"; import { scanImage } from "../actions";
import { selectImage } from "../../images/actions"; import { selectImage } from "../../images/actions";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<CameraCalibration/>", () => { describe("<CameraCalibration/>", () => {
const fakeProps = (): CameraCalibrationProps => ({ const fakeProps = (): CameraCalibrationProps => ({
dispatch: jest.fn(), dispatch: jest.fn(),
currentImage: undefined, currentImage: undefined,
images: [], images: [],
wDEnv: {},
env: {}, env: {},
iteration: 1, iteration: 1,
morph: 2, morph: 2,
@ -95,4 +97,23 @@ describe("<CameraCalibration/>", () => {
expect(p.saveFarmwareEnv).toHaveBeenCalledWith( expect(p.saveFarmwareEnv).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_image_bot_origin_location", "\"BOTTOM_LEFT\""); "CAMERA_CALIBRATION_image_bot_origin_location", "\"BOTTOM_LEFT\"");
}); });
it("shows calibrate as enabled", () => {
const wrapper = shallow(<CameraCalibration {...fakeProps()} />);
const btn = wrapper.find("button").first();
expect(btn.text()).toEqual("Calibrate");
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
expect(error).not.toHaveBeenCalled();
});
it("shows calibrate as disabled when camera is disabled", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = shallow(<CameraCalibration {...p} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
});
}); });

View File

@ -13,6 +13,9 @@ import { Feature } from "../../devices/interfaces";
import { namespace } from "../weed_detector"; import { namespace } from "../weed_detector";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { formatEnvKey } from "../weed_detector/remote_env/translators"; import { formatEnvKey } from "../weed_detector/remote_env/translators";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
export class CameraCalibration extends export class CameraCalibration extends
React.Component<CameraCalibrationProps, {}> { React.Component<CameraCalibrationProps, {}> {
@ -29,6 +32,7 @@ export class CameraCalibration extends
: envSave(key, value) : envSave(key, value)
render() { render() {
const camDisabled = cameraBtnProps(this.props.env);
return <div className="weed-detector"> return <div className="weed-detector">
<div className="farmware-button"> <div className="farmware-button">
<MustBeOnline <MustBeOnline
@ -37,8 +41,9 @@ export class CameraCalibration extends
hideBanner={true} hideBanner={true}
lockOpen={process.env.NODE_ENV !== "production"}> lockOpen={process.env.NODE_ENV !== "production"}>
<button <button
onClick={this.props.dispatch(calibrate)} className={`fb-button green ${camDisabled.class}`}
className="fb-button green"> title={camDisabled.title}
onClick={camDisabled.click || this.props.dispatch(calibrate)}>
{t("Calibrate")} {t("Calibrate")}
</button> </button>
</MustBeOnline> </MustBeOnline>
@ -68,9 +73,9 @@ export class CameraCalibration extends
S_HI={this.props.S_HI} S_HI={this.props.S_HI}
V_HI={this.props.V_HI} V_HI={this.props.V_HI}
invertHue={!!envGet(this.namespace("invert_hue_selection"), invertHue={!!envGet(this.namespace("invert_hue_selection"),
this.props.env)} /> this.props.wDEnv)} />
<WeedDetectorConfig <WeedDetectorConfig
values={this.props.env} values={this.props.wDEnv}
onChange={this.saveEnvVar} /> onChange={this.saveEnvVar} />
</MustBeOnline> </MustBeOnline>
</Col> </Col>

View File

@ -1,14 +1,17 @@
import { TaggedImage, SyncStatus } from "farmbot"; import { TaggedImage, SyncStatus } from "farmbot";
import { WD_ENV } from "../weed_detector/remote_env/interfaces"; import { WD_ENV } from "../weed_detector/remote_env/interfaces";
import { NetworkState } from "../../connectivity/interfaces"; import { NetworkState } from "../../connectivity/interfaces";
import { ShouldDisplay, SaveFarmwareEnv } from "../../devices/interfaces"; import {
ShouldDisplay, SaveFarmwareEnv, UserEnv
} from "../../devices/interfaces";
import { TimeSettings } from "../../interfaces"; import { TimeSettings } from "../../interfaces";
export interface CameraCalibrationProps { export interface CameraCalibrationProps {
dispatch: Function; dispatch: Function;
images: TaggedImage[]; images: TaggedImage[];
currentImage: TaggedImage | undefined; currentImage: TaggedImage | undefined;
env: Partial<WD_ENV>; wDEnv: Partial<WD_ENV>;
env: UserEnv;
iteration: number; iteration: number;
morph: number; morph: number;
blur: number; blur: number;

View File

@ -11,7 +11,7 @@ import { t } from "../i18next_wrapper";
export interface FarmwareFormProps { export interface FarmwareFormProps {
farmware: FarmwareManifestInfo; farmware: FarmwareManifestInfo;
user_env: UserEnv; env: UserEnv;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
saveFarmwareEnv: SaveFarmwareEnv; saveFarmwareEnv: SaveFarmwareEnv;
dispatch: Function; dispatch: Function;
@ -73,7 +73,7 @@ export function FarmwareForm(props: FarmwareFormProps): JSX.Element {
/** Get a Farmware input value from FBOS. */ /** Get a Farmware input value from FBOS. */
function getValue(farmwareName: string, currentConfig: FarmwareConfig) { function getValue(farmwareName: string, currentConfig: FarmwareConfig) {
return (user_env[getConfigEnvName(farmwareName, currentConfig.name)] return (env[getConfigEnvName(farmwareName, currentConfig.name)]
|| toString(currentConfig.value)); || toString(currentConfig.value));
} }
@ -87,7 +87,7 @@ export function FarmwareForm(props: FarmwareFormProps): JSX.Element {
getDevice().execScript(farmwareName, pairs).catch(() => { }); getDevice().execScript(farmwareName, pairs).catch(() => { });
} }
const { farmware, user_env } = props; const { farmware, env } = props;
return <Col key={farmware.name}> return <Col key={farmware.name}>
<div className={kebabCase(farmware.name)}> <div className={kebabCase(farmware.name)}>
<button <button

View File

@ -26,8 +26,8 @@ describe("<ImageFlipper/>", () => {
it("defaults to index 0 and flips up", () => { it("defaults to index 0 and flips up", () => {
const p = fakeProps(); const p = fakeProps();
const x = shallow(<ImageFlipper {...p} />); const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const up = (x.instance() as ImageFlipper).go(1); const up = flipper.instance().go(1);
up(); up();
expect(p.onFlip).toHaveBeenCalledWith(p.images[1].uuid); expect(p.onFlip).toHaveBeenCalledWith(p.images[1].uuid);
}); });
@ -35,8 +35,8 @@ describe("<ImageFlipper/>", () => {
it("flips down", () => { it("flips down", () => {
const p = fakeProps(); const p = fakeProps();
p.currentImage = p.images[1]; p.currentImage = p.images[1];
const x = shallow(<ImageFlipper {...p} />); const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const down = (x.instance() as ImageFlipper).go(-1); const down = flipper.instance().go(-1);
down(); down();
expect(p.onFlip).toHaveBeenCalledWith(p.images[0].uuid); expect(p.onFlip).toHaveBeenCalledWith(p.images[0].uuid);
}); });
@ -44,8 +44,8 @@ describe("<ImageFlipper/>", () => {
it("stops at upper end", () => { it("stops at upper end", () => {
const p = fakeProps(); const p = fakeProps();
p.currentImage = p.images[2]; p.currentImage = p.images[2];
const x = shallow(<ImageFlipper {...p} />); const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const up = (x.instance() as ImageFlipper).go(1); const up = flipper.instance().go(1);
up(); up();
expect(p.onFlip).not.toHaveBeenCalled(); expect(p.onFlip).not.toHaveBeenCalled();
}); });
@ -53,8 +53,8 @@ describe("<ImageFlipper/>", () => {
it("stops at lower end", () => { it("stops at lower end", () => {
const p = fakeProps(); const p = fakeProps();
p.currentImage = p.images[0]; p.currentImage = p.images[0];
const x = shallow(<ImageFlipper {...p} />); const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const down = (x.instance() as ImageFlipper).go(-1); const down = flipper.instance().go(-1);
down(); down();
expect(p.onFlip).not.toHaveBeenCalled(); expect(p.onFlip).not.toHaveBeenCalled();
}); });
@ -118,4 +118,12 @@ describe("<ImageFlipper/>", () => {
const wrapper = mount(<ImageFlipper {...p} />); const wrapper = mount(<ImageFlipper {...p} />);
expect(wrapper.find("img").last().props().src).toEqual(PLACEHOLDER_FARMBOT); expect(wrapper.find("img").last().props().src).toEqual(PLACEHOLDER_FARMBOT);
}); });
it("knows when image is loaded", () => {
const wrapper = mount<ImageFlipper>(<ImageFlipper {...fakeProps()} />);
const image = shallow(wrapper.instance().imageJSX());
expect(wrapper.state().isLoaded).toEqual(false);
image.find("img").simulate("load");
expect(wrapper.state().isLoaded).toEqual(true);
});
}); });

View File

@ -16,6 +16,7 @@ import { PhotosProps } from "../interfaces";
import { selectImage } from "../actions"; import { selectImage } from "../actions";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { success, error } from "../../../toast/toast"; import { success, error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<Photos/>", () => { describe("<Photos/>", () => {
const fakeProps = (): PhotosProps => ({ const fakeProps = (): PhotosProps => ({
@ -26,6 +27,7 @@ describe("<Photos/>", () => {
imageJobs: [], imageJobs: [],
botToMqttStatus: "up", botToMqttStatus: "up",
syncStatus: "synced", syncStatus: "synced",
env: {},
}); });
it("shows photo", () => { it("shows photo", () => {
@ -44,9 +46,25 @@ describe("<Photos/>", () => {
it("takes photo", async () => { it("takes photo", async () => {
const wrapper = mount(<Photos {...fakeProps()} />); const wrapper = mount(<Photos {...fakeProps()} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
await clickButton(wrapper, 0, "take photo"); await clickButton(wrapper, 0, "take photo");
expect(mockDevice.takePhoto).toHaveBeenCalled(); expect(mockDevice.takePhoto).toHaveBeenCalled();
await expect(success).toHaveBeenCalled(); await expect(success).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("shows disabled take photo button", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = mount(<Photos {...p} />);
const btn = wrapper.find("button").first();
expect(btn.text()).toEqual("Take Photo");
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
}); });
it("fails to take photo", async () => { it("fails to take photo", async () => {

View File

@ -1,6 +1,7 @@
import { TaggedImage, JobProgress, SyncStatus } from "farmbot"; import { TaggedImage, JobProgress, SyncStatus } from "farmbot";
import { NetworkState } from "../../connectivity/interfaces"; import { NetworkState } from "../../connectivity/interfaces";
import { TimeSettings } from "../../interfaces"; import { TimeSettings } from "../../interfaces";
import { UserEnv } from "../../devices/interfaces";
export interface ImageFlipperProps { export interface ImageFlipperProps {
onFlip(uuid: string | undefined): void; onFlip(uuid: string | undefined): void;
@ -22,6 +23,7 @@ export interface PhotosProps {
imageJobs: JobProgress[]; imageJobs: JobProgress[];
botToMqttStatus: NetworkState; botToMqttStatus: NetworkState;
syncStatus: SyncStatus | undefined; syncStatus: SyncStatus | undefined;
env: UserEnv;
} }
export interface PhotoButtonsProps { export interface PhotoButtonsProps {
@ -30,4 +32,5 @@ export interface PhotoButtonsProps {
imageJobs: JobProgress[], imageJobs: JobProgress[],
botToMqttStatus: NetworkState; botToMqttStatus: NetworkState;
syncStatus: SyncStatus | undefined; syncStatus: SyncStatus | undefined;
env: UserEnv;
} }

View File

@ -16,6 +16,9 @@ import { startCase } from "lodash";
import { MustBeOnline } from "../../devices/must_be_online"; import { MustBeOnline } from "../../devices/must_be_online";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { TimeSettings } from "../../interfaces"; import { TimeSettings } from "../../interfaces";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
interface MetaInfoProps { interface MetaInfoProps {
/** Default conversion is `attr_name ==> Attr Name`. /** Default conversion is `attr_name ==> Attr Name`.
@ -52,6 +55,7 @@ const PhotoMetaData = ({ image }: { image: TaggedImage | undefined }) =>
const PhotoButtons = (props: PhotoButtonsProps) => { const PhotoButtons = (props: PhotoButtonsProps) => {
const imageUploadJobProgress = downloadProgress(props.imageJobs[0]); const imageUploadJobProgress = downloadProgress(props.imageJobs[0]);
const camDisabled = cameraBtnProps(props.env);
return <div className="farmware-button"> return <div className="farmware-button">
<MustBeOnline <MustBeOnline
syncStatus={props.syncStatus} syncStatus={props.syncStatus}
@ -59,8 +63,9 @@ const PhotoButtons = (props: PhotoButtonsProps) => {
hideBanner={true} hideBanner={true}
lockOpen={process.env.NODE_ENV !== "production"}> lockOpen={process.env.NODE_ENV !== "production"}>
<button <button
className="fb-button green" className={`fb-button green ${camDisabled.class}`}
onClick={props.takePhoto}> title={camDisabled.title}
onClick={camDisabled.click || props.takePhoto}>
{t("Take Photo")} {t("Take Photo")}
</button> </button>
</MustBeOnline> </MustBeOnline>
@ -125,6 +130,7 @@ export class Photos extends React.Component<PhotosProps, {}> {
botToMqttStatus={this.props.botToMqttStatus} botToMqttStatus={this.props.botToMqttStatus}
takePhoto={this.takePhoto} takePhoto={this.takePhoto}
deletePhoto={this.deletePhoto} deletePhoto={this.deletePhoto}
env={this.props.env}
imageJobs={this.props.imageJobs} /> imageJobs={this.props.imageJobs} />
<ImageFlipper <ImageFlipper
onFlip={id => this.props.dispatch(selectImage(id))} onFlip={id => this.props.dispatch(selectImage(id))}

View File

@ -24,6 +24,7 @@ import { t } from "../i18next_wrapper";
import { isBotOnline } from "../devices/must_be_online"; import { isBotOnline } from "../devices/must_be_online";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { Dictionary } from "farmbot"; import { Dictionary } from "farmbot";
import { WDENVKey } from "./weed_detector/remote_env/interfaces";
/** Get the correct help text for the provided Farmware. */ /** Get the correct help text for the provided Farmware. */
const getToolTipByFarmware = const getToolTipByFarmware =
@ -136,6 +137,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
/** Load Farmware input panel contents for 1st & 3rd party Farmware. */ /** Load Farmware input panel contents for 1st & 3rd party Farmware. */
getPanelByFarmware(farmwareName: string) { getPanelByFarmware(farmwareName: string) {
const wDEnvGet = (key: WDENVKey) => envGet(key, this.props.wDEnv);
switch (farmwareUrlFriendly(farmwareName)) { switch (farmwareUrlFriendly(farmwareName)) {
case "take_photo": case "take_photo":
case "photos": case "photos":
@ -145,6 +147,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
timeSettings={this.props.timeSettings} timeSettings={this.props.timeSettings}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
images={this.props.images} images={this.props.images}
env={this.props.env}
currentImage={this.props.currentImage} currentImage={this.props.currentImage}
imageJobs={this.props.imageJobs} />; imageJobs={this.props.imageJobs} />;
case "camera_calibration": case "camera_calibration":
@ -153,17 +156,18 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
currentImage={this.props.currentImage} currentImage={this.props.currentImage}
images={this.props.images} images={this.props.images}
wDEnv={this.props.wDEnv}
env={this.props.env} env={this.props.env}
saveFarmwareEnv={this.props.saveFarmwareEnv} saveFarmwareEnv={this.props.saveFarmwareEnv}
iteration={envGet("CAMERA_CALIBRATION_iteration", this.props.env)} iteration={wDEnvGet("CAMERA_CALIBRATION_iteration")}
morph={envGet("CAMERA_CALIBRATION_morph", this.props.env)} morph={wDEnvGet("CAMERA_CALIBRATION_morph")}
blur={envGet("CAMERA_CALIBRATION_blur", this.props.env)} blur={wDEnvGet("CAMERA_CALIBRATION_blur")}
H_LO={envGet("CAMERA_CALIBRATION_H_LO", this.props.env)} H_LO={wDEnvGet("CAMERA_CALIBRATION_H_LO")}
S_LO={envGet("CAMERA_CALIBRATION_S_LO", this.props.env)} S_LO={wDEnvGet("CAMERA_CALIBRATION_S_LO")}
V_LO={envGet("CAMERA_CALIBRATION_V_LO", this.props.env)} V_LO={wDEnvGet("CAMERA_CALIBRATION_V_LO")}
H_HI={envGet("CAMERA_CALIBRATION_H_HI", this.props.env)} H_HI={wDEnvGet("CAMERA_CALIBRATION_H_HI")}
S_HI={envGet("CAMERA_CALIBRATION_S_HI", this.props.env)} S_HI={wDEnvGet("CAMERA_CALIBRATION_S_HI")}
V_HI={envGet("CAMERA_CALIBRATION_V_HI", this.props.env)} V_HI={wDEnvGet("CAMERA_CALIBRATION_V_HI")}
timeSettings={this.props.timeSettings} timeSettings={this.props.timeSettings}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
botToMqttStatus={this.props.botToMqttStatus} />; botToMqttStatus={this.props.botToMqttStatus} />;
@ -174,7 +178,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
const farmware = getFarmwareByName(this.props.farmwares, farmwareName); const farmware = getFarmwareByName(this.props.farmwares, farmwareName);
return farmware && needsFarmwareForm(farmware) return farmware && needsFarmwareForm(farmware)
? <FarmwareForm farmware={farmware} ? <FarmwareForm farmware={farmware}
user_env={this.props.user_env} env={this.props.env}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
saveFarmwareEnv={this.props.saveFarmwareEnv} saveFarmwareEnv={this.props.saveFarmwareEnv}
botOnline={this.botOnline} botOnline={this.botOnline}

View File

@ -3,7 +3,7 @@ import {
selectAllImages, maybeGetDevice, maybeGetTimeSettings selectAllImages, maybeGetDevice, maybeGetTimeSettings
} from "../resources/selectors"; } from "../resources/selectors";
import { import {
FarmwareProps, Feature, SaveFarmwareEnv, UserEnv FarmwareProps, Feature, SaveFarmwareEnv, UserEnv, ShouldDisplay, BotState
} from "../devices/interfaces"; } from "../devices/interfaces";
import { prepopulateEnv } from "./weed_detector/remote_env/selectors"; import { prepopulateEnv } from "./weed_detector/remote_env/selectors";
import { import {
@ -11,7 +11,7 @@ import {
} from "../resources/selectors_by_kind"; } from "../resources/selectors_by_kind";
import { import {
determineInstalledOsVersion, determineInstalledOsVersion,
createShouldDisplayFn as shouldDisplayFunc, createShouldDisplayFn,
betterCompact betterCompact
} from "../util"; } from "../util";
import { ResourceIndex } from "../resources/interfaces"; import { ResourceIndex } from "../resources/interfaces";
@ -51,6 +51,20 @@ export const reduceFarmwareEnv =
return farmwareEnv; return farmwareEnv;
}; };
export const getEnv =
(ri: ResourceIndex, shouldDisplay: ShouldDisplay, bot: BotState) =>
shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(ri)
: bot.hardware.user_env;
export const getShouldDisplayFn = (ri: ResourceIndex, bot: BotState) => {
const lookupData = bot.minOsFeatureData;
const installed = determineInstalledOsVersion(bot, maybeGetDevice(ri));
const override = DevSettings.overriddenFbosVersion();
const shouldDisplay = createShouldDisplayFn(installed, lookupData, override);
return shouldDisplay;
};
export function mapStateToProps(props: Everything): FarmwareProps { export function mapStateToProps(props: Everything): FarmwareProps {
const images = chain(selectAllImages(props.resources.index)) const images = chain(selectAllImages(props.resources.index))
.sortBy(x => x.body.id) .sortBy(x => x.body.id)
@ -64,14 +78,8 @@ export function mapStateToProps(props: Everything): FarmwareProps {
const { currentFarmware, firstPartyFarmwareNames, infoOpen } = const { currentFarmware, firstPartyFarmwareNames, infoOpen } =
props.resources.consumers.farmware; props.resources.consumers.farmware;
const installedOsVersion = determineInstalledOsVersion( const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
props.bot, maybeGetDevice(props.resources.index)); const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;
const taggedFarmwareInstallations = const taggedFarmwareInstallations =
selectAllFarmwareInstallations(props.resources.index); selectAllFarmwareInstallations(props.resources.index);
@ -115,8 +123,8 @@ export function mapStateToProps(props: Everything): FarmwareProps {
currentFarmware, currentFarmware,
farmwares, farmwares,
botToMqttStatus, botToMqttStatus,
env: prepopulateEnv(env), wDEnv: prepopulateEnv(env),
user_env: env, env,
dispatch: props.dispatch, dispatch: props.dispatch,
currentImage, currentImage,
images, images,

View File

@ -23,7 +23,7 @@ jest.mock("../../../util", () => ({
})); }));
import { deletePoints } from "../actions"; import { deletePoints } from "../actions";
import { scanImage, test } from "../actions"; import { scanImage, detectPlants } from "../actions";
import axios from "axios"; import axios from "axios";
import { API } from "../../../api"; import { API } from "../../../api";
import { times } from "lodash"; import { times } from "lodash";
@ -44,10 +44,10 @@ describe("scanImage()", () => {
}); });
}); });
describe("test()", () => { describe("detectPlants()", () => {
it("calls out to the device", () => { it("calls out to the device", () => {
// Run function to invoke side effects // Run function to invoke side effects
const thunk = test(); const thunk = detectPlants();
thunk(); thunk();
// Ensure the side effects were the ones we expected. // Ensure the side effects were the ones we expected.
expect(mockDevice.execScript) expect(mockDevice.execScript)

View File

@ -1,11 +1,16 @@
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import { WeedDetectorConfig } from "../config"; import { WeedDetectorConfig } from "../config";
import { SettingsMenuProps } from "../interfaces";
describe("<WeedDetectorConfig />", () => { describe("<WeedDetectorConfig />", () => {
const fakeProps = (): SettingsMenuProps => ({
values: {},
onChange: jest.fn(),
});
it("renders", () => { it("renders", () => {
const wrapper = mount(<WeedDetectorConfig const wrapper = mount(<WeedDetectorConfig {...fakeProps()} />);
values={{}} onChange={jest.fn()} />);
["Invert Hue Range Selection", ["Invert Hue Range Selection",
"Calibration Object Separation", "Calibration Object Separation",
"Calibration Object Separation along axis", "Calibration Object Separation along axis",
@ -15,15 +20,39 @@ describe("<WeedDetectorConfig />", () => {
.map(string => expect(wrapper.text()).toContain(string)); .map(string => expect(wrapper.text()).toContain(string));
}); });
it("changes value", () => { it("changes axis value", () => {
const onChange = jest.fn(); const p = fakeProps();
const wrapper = shallow(<WeedDetectorConfig const wrapper = shallow(<WeedDetectorConfig {...p} />);
values={{}} onChange={onChange} />);
const input = wrapper.find("FBSelect").first(); const input = wrapper.find("FBSelect").first();
input.simulate("change", { label: "", value: 4 }); input.simulate("change", { label: "", value: 4 });
expect(onChange).toHaveBeenCalledWith( expect(p.onChange).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_calibration_along_axis", 4); "CAMERA_CALIBRATION_calibration_along_axis", 4);
const badChange = () => input.simulate("change", { label: "", value: "4" }); const badChange = () => input.simulate("change", { label: "", value: "4" });
expect(badChange).toThrow("Weed detector got a non-numeric value"); expect(badChange).toThrow("Weed detector got a non-numeric value");
}); });
it("changes hue invert value", () => {
const p = fakeProps();
const wrapper = shallow(<WeedDetectorConfig {...p} />);
const input = wrapper.find("input").first();
input.simulate("change", { currentTarget: { checked: true } });
expect(p.onChange).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_invert_hue_selection", 1);
input.simulate("change", { currentTarget: { checked: false } });
expect(p.onChange).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_invert_hue_selection", 0);
});
it("changes number value", () => {
const p = fakeProps();
const wrapper = shallow<WeedDetectorConfig>(<WeedDetectorConfig {...p} />);
const numBox = wrapper.instance().NumberBox({
conf: "CAMERA_CALIBRATION_blur", label: "label"
});
const NumBox = shallow(numBox);
NumBox.find("BlurableInput").first().simulate("commit", {
currentTarget: { value: "1.23" }
});
expect(p.onChange).toHaveBeenCalledWith("CAMERA_CALIBRATION_blur", 1.23);
});
}); });

View File

@ -1,11 +1,15 @@
const mockDevice = { const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve()) };
execScript: jest.fn(() => Promise.resolve()),
setUserEnv: jest.fn(() => Promise.resolve()),
};
jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../../images/actions", () => ({ selectImage: jest.fn() })); jest.mock("../../images/actions", () => ({ selectImage: jest.fn() }));
const mockDeletePoints = jest.fn();
jest.mock("../actions", () => ({
deletePoints: mockDeletePoints,
scanImage: jest.fn(),
detectPlants: jest.fn(),
}));
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import { WeedDetector, namespace } from "../index"; import { WeedDetector, namespace } from "../index";
@ -14,6 +18,9 @@ import { API } from "../../../api";
import { selectImage } from "../../images/actions"; import { selectImage } from "../../images/actions";
import { clickButton } from "../../../__test_support__/helpers"; import { clickButton } from "../../../__test_support__/helpers";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { deletePoints, detectPlants, scanImage } from "../actions";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<WeedDetector />", () => { describe("<WeedDetector />", () => {
API.setBaseUrl("http://localhost:3000"); API.setBaseUrl("http://localhost:3000");
@ -22,8 +29,8 @@ describe("<WeedDetector />", () => {
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
farmwares: {}, farmwares: {},
botToMqttStatus: "up", botToMqttStatus: "up",
wDEnv: {},
env: {}, env: {},
user_env: {},
dispatch: jest.fn(), dispatch: jest.fn(),
currentImage: undefined, currentImage: undefined,
images: [], images: [],
@ -54,16 +61,38 @@ describe("<WeedDetector />", () => {
const p = fakeProps(); const p = fakeProps();
p.dispatch = jest.fn(x => x()); p.dispatch = jest.fn(x => x());
const wrapper = shallow(<WeedDetector {...p} />); const wrapper = shallow(<WeedDetector {...p} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
clickButton(wrapper, 0, "detect weeds"); clickButton(wrapper, 0, "detect weeds");
expect(mockDevice.execScript).toHaveBeenCalledWith("plant-detection"); expect(detectPlants).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("shows detection button as disabled when camera is disabled", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = shallow(<WeedDetector {...p} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(detectPlants).not.toHaveBeenCalled();
}); });
it("executes clear weeds", () => { it("executes clear weeds", () => {
const wrapper = const wrapper = shallow<WeedDetector>(<WeedDetector {...fakeProps()} />);
shallow<WeedDetector>(<WeedDetector {...fakeProps()} />);
expect(wrapper.instance().state.deletionProgress).toBeUndefined(); expect(wrapper.instance().state.deletionProgress).toBeUndefined();
clickButton(wrapper, 1, "clear weeds"); clickButton(wrapper, 1, "clear weeds");
expect(deletePoints).toHaveBeenCalledWith(
"weeds", { created_by: "plant-detection" }, expect.any(Function));
expect(wrapper.instance().state.deletionProgress).toEqual("Deleting..."); expect(wrapper.instance().state.deletionProgress).toEqual("Deleting...");
const fakeProgress = { completed: 50, total: 100, isDone: false };
mockDeletePoints.mock.calls[0][2](fakeProgress);
expect(wrapper.instance().state.deletionProgress).toEqual("50 %");
fakeProgress.isDone = true;
mockDeletePoints.mock.calls[0][2](fakeProgress);
expect(wrapper.instance().state.deletionProgress).toEqual("");
}); });
it("saves changes", () => { it("saves changes", () => {
@ -85,16 +114,9 @@ describe("<WeedDetector />", () => {
}); });
it("calls scanImage", () => { it("calls scanImage", () => {
const p = fakeProps(); const wrapper = shallow(<WeedDetector {...fakeProps()} />);
p.dispatch = jest.fn(x => x());
const wrapper = shallow(<WeedDetector {...p} />);
wrapper.find("ImageWorkspace").simulate("processPhoto", 1); wrapper.find("ImageWorkspace").simulate("processPhoto", 1);
expect(mockDevice.execScript).toHaveBeenCalledWith( expect(scanImage).toHaveBeenCalledWith(1);
"historical-plant-detection",
[expect.objectContaining({
kind: "pair",
args: expect.objectContaining({ value: "1" })
})]);
}); });
it("calls selectImage", () => { it("calls selectImage", () => {

View File

@ -69,7 +69,7 @@ export function scanImage(imageId: number) {
}; };
} }
export function test() { export function detectPlants() {
return function () { return function () {
getDevice().execScript("plant-detection").catch(() => { }); getDevice().execScript("plant-detection").catch(() => { });
}; };

View File

@ -154,10 +154,9 @@ export class ImageWorkspace extends React.Component<ImageWorkspaceProps, {}> {
<Col xs={12}> <Col xs={12}>
<button <button
className="green fb-button" className="green fb-button"
title="Scan this image" title={t("Scan this image")}
onClick={this.maybeProcessPhoto} onClick={this.maybeProcessPhoto}
disabled={!this.props.botOnline} disabled={!this.props.botOnline || !this.props.images.length}>
hidden={!this.props.images.length}>
{t("Scan image")} {t("Scan image")}
</button> </button>
</Col> </Col>

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { DetectorState } from "./interfaces"; import { DetectorState } from "./interfaces";
import { Row, Col } from "../../ui/index"; import { Row, Col } from "../../ui/index";
import { deletePoints, scanImage, test } from "./actions"; import { deletePoints, scanImage, detectPlants } from "./actions";
import { selectImage } from "../images/actions"; import { selectImage } from "../images/actions";
import { Progress } from "../../util"; import { Progress } from "../../util";
import { FarmwareProps, Feature } from "../../devices/interfaces"; import { FarmwareProps, Feature } from "../../devices/interfaces";
@ -11,6 +11,9 @@ import { envGet } from "./remote_env/selectors";
import { MustBeOnline, isBotOnline } from "../../devices/must_be_online"; import { MustBeOnline, isBotOnline } from "../../devices/must_be_online";
import { envSave } from "./remote_env/actions"; import { envSave } from "./remote_env/actions";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
export const namespace = (prefix: string) => (key: string): WDENVKey => { export const namespace = (prefix: string) => (key: string): WDENVKey => {
const namespacedKey = prefix + key; const namespacedKey = prefix + key;
@ -52,6 +55,8 @@ export class WeedDetector
: envSave(key, value) : envSave(key, value)
render() { render() {
const wDEnvGet = (key: WDENVKey) => envGet(key, this.props.wDEnv);
const camDisabled = cameraBtnProps(this.props.env);
return <div className="weed-detector"> return <div className="weed-detector">
<div className="farmware-button"> <div className="farmware-button">
<MustBeOnline <MustBeOnline
@ -60,8 +65,9 @@ export class WeedDetector
hideBanner={true} hideBanner={true}
lockOpen={process.env.NODE_ENV !== "production"}> lockOpen={process.env.NODE_ENV !== "production"}>
<button <button
onClick={this.props.dispatch(test)} className={`fb-button green ${camDisabled.class}`}
className="fb-button green"> title={camDisabled.title}
onClick={camDisabled.click || this.props.dispatch(detectPlants)}>
{t("detect weeds")} {t("detect weeds")}
</button> </button>
</MustBeOnline> </MustBeOnline>
@ -86,15 +92,15 @@ export class WeedDetector
images={this.props.images} images={this.props.images}
onChange={this.change} onChange={this.change}
timeSettings={this.props.timeSettings} timeSettings={this.props.timeSettings}
iteration={envGet(this.namespace("iteration"), this.props.env)} iteration={wDEnvGet(this.namespace("iteration"))}
morph={envGet(this.namespace("morph"), this.props.env)} morph={wDEnvGet(this.namespace("morph"))}
blur={envGet(this.namespace("blur"), this.props.env)} blur={wDEnvGet(this.namespace("blur"))}
H_LO={envGet(this.namespace("H_LO"), this.props.env)} H_LO={wDEnvGet(this.namespace("H_LO"))}
H_HI={envGet(this.namespace("H_HI"), this.props.env)} H_HI={wDEnvGet(this.namespace("H_HI"))}
S_LO={envGet(this.namespace("S_LO"), this.props.env)} S_LO={wDEnvGet(this.namespace("S_LO"))}
S_HI={envGet(this.namespace("S_HI"), this.props.env)} S_HI={wDEnvGet(this.namespace("S_HI"))}
V_LO={envGet(this.namespace("V_LO"), this.props.env)} V_LO={wDEnvGet(this.namespace("V_LO"))}
V_HI={envGet(this.namespace("V_HI"), this.props.env)} /> V_HI={wDEnvGet(this.namespace("V_HI"))} />
</MustBeOnline> </MustBeOnline>
</Col> </Col>
</Row> </Row>

View File

@ -1,21 +1,16 @@
import { Everything } from "../interfaces"; import { Everything } from "../interfaces";
import { import { selectAllLogs, maybeGetTimeSettings } from "../resources/selectors";
selectAllLogs, maybeGetTimeSettings, maybeGetDevice
} from "../resources/selectors";
import { LogsProps } from "./interfaces"; import { LogsProps } from "./interfaces";
import { import {
sourceFbosConfigValue sourceFbosConfigValue
} from "../devices/components/source_config_value"; } from "../devices/components/source_config_value";
import { import { validFbosConfig } from "../util";
validFbosConfig, determineInstalledOsVersion,
createShouldDisplayFn as shouldDisplayFunc
} from "../util";
import { ResourceIndex } from "../resources/interfaces"; import { ResourceIndex } from "../resources/interfaces";
import { TaggedLog } from "farmbot"; import { TaggedLog } from "farmbot";
import { getWebAppConfigValue } from "../config_storage/actions"; import { getWebAppConfigValue } from "../config_storage/actions";
import { getFbosConfig } from "../resources/getters"; import { getFbosConfig } from "../resources/getters";
import { chain } from "lodash"; import { chain } from "lodash";
import { DevSettings } from "../account/dev/dev_support"; import { getShouldDisplayFn } from "../farmware/state_to_props";
/** Take the specified number of logs after sorting by time created. */ /** Take the specified number of logs after sorting by time created. */
export function takeSortedLogs( export function takeSortedLogs(
@ -30,19 +25,12 @@ export function takeSortedLogs(
export function mapStateToProps(props: Everything): LogsProps { export function mapStateToProps(props: Everything): LogsProps {
const { hardware } = props.bot; const { hardware } = props.bot;
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index)); const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const sourceFbosConfig =
sourceFbosConfigValue(fbosConfig, hardware.configuration);
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
return { return {
dispatch: props.dispatch, dispatch: props.dispatch,
sourceFbosConfig, sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration),
logs: takeSortedLogs(250, props.resources.index), logs: takeSortedLogs(250, props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index),
getConfigValue: getWebAppConfigValue(() => props), getConfigValue: getWebAppConfigValue(() => props),
shouldDisplay, shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
}; };
} }

View File

@ -9,21 +9,16 @@ import {
maybeGetRegimen, maybeGetRegimen,
findId, findId,
findSequence, findSequence,
maybeGetDevice,
findSequenceById, findSequenceById,
maybeGetTimeSettings maybeGetTimeSettings
} from "../resources/selectors"; } from "../resources/selectors";
import { TaggedRegimen, TaggedSequence } from "farmbot"; import { TaggedRegimen, TaggedSequence } from "farmbot";
import moment from "moment"; import moment from "moment";
import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces"; import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces";
import { import { randomColor, timeFormatString } from "../util";
randomColor, determineInstalledOsVersion,
createShouldDisplayFn as shouldDisplayFunc,
timeFormatString
} from "../util";
import { resourceUsageList } from "../resources/in_use"; import { resourceUsageList } from "../resources/in_use";
import { groupBy, chain, sortBy } from "lodash"; import { groupBy, chain, sortBy } from "lodash";
import { DevSettings } from "../account/dev/dev_support"; import { getShouldDisplayFn } from "../farmware/state_to_props";
export function mapStateToProps(props: Everything): Props { export function mapStateToProps(props: Everything): Props {
const { resources, dispatch, bot } = props; const { resources, dispatch, bot } = props;
@ -36,12 +31,6 @@ export function mapStateToProps(props: Everything): Props {
const calendar = current ? const calendar = current ?
generateCalendar(current, index, dispatch, timeSettings) : []; generateCalendar(current, index, dispatch, timeSettings) : [];
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const calledSequences = (): UUID[] => { const calledSequences = (): UUID[] => {
if (current) { if (current) {
const sequenceIds = current.body.regimen_items.map(x => x.sequence_id); const sequenceIds = current.body.regimen_items.map(x => x.sequence_id);
@ -70,7 +59,7 @@ export function mapStateToProps(props: Everything): Props {
bot, bot,
calendar, calendar,
regimenUsageStats: resourceUsageList(props.resources.index.inUse), regimenUsageStats: resourceUsageList(props.resources.index.inUse),
shouldDisplay, shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
schedulerOpen, schedulerOpen,
}; };
} }

View File

@ -43,8 +43,8 @@ import {
import { fakeSequence } from "../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { destroy, save, edit } from "../../api/crud"; import { destroy, save, edit } from "../../api/crud";
import { import {
fakeHardwareFlags fakeHardwareFlags, fakeFarmwareData as fakeFarmwareData
} from "../../__test_support__/sequence_hardware_settings"; } from "../../__test_support__/fake_sequence_step_data";
import { SpecialStatus } from "farmbot"; import { SpecialStatus } from "farmbot";
import { move, splice } from "../step_tiles"; import { move, splice } from "../step_tiles";
import { copySequence, editCurrentSequence } from "../actions"; import { copySequence, editCurrentSequence } from "../actions";
@ -66,12 +66,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index, resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced", syncStatus: "synced",
hardwareFlags: fakeHardwareFlags(), hardwareFlags: fakeHardwareFlags(),
farmwareInfo: { farmwareData: fakeFarmwareData(),
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
},
shouldDisplay: jest.fn(), shouldDisplay: jest.fn(),
getWebAppConfigValue: jest.fn(), getWebAppConfigValue: jest.fn(),
menuOpen: false, menuOpen: false,

View File

@ -7,8 +7,8 @@ import {
} from "../../__test_support__/resource_index_builder"; } from "../../__test_support__/resource_index_builder";
import { fakeSequence } from "../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { import {
fakeHardwareFlags fakeHardwareFlags, fakeFarmwareData
} from "../../__test_support__/sequence_hardware_settings"; } from "../../__test_support__/fake_sequence_step_data";
describe("<SequenceEditorMiddle/>", () => { describe("<SequenceEditorMiddle/>", () => {
function fakeProps(): SequenceEditorMiddleProps { function fakeProps(): SequenceEditorMiddleProps {
@ -18,12 +18,7 @@ describe("<SequenceEditorMiddle/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index, resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced", syncStatus: "synced",
hardwareFlags: fakeHardwareFlags(), hardwareFlags: fakeHardwareFlags(),
farmwareInfo: { farmwareData: fakeFarmwareData(),
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
},
shouldDisplay: jest.fn(), shouldDisplay: jest.fn(),
getWebAppConfigValue: jest.fn(), getWebAppConfigValue: jest.fn(),
menuOpen: false, menuOpen: false,

View File

@ -15,8 +15,8 @@ import {
import { fakeSequence } from "../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { ToolTips, Actions } from "../../constants"; import { ToolTips, Actions } from "../../constants";
import { import {
fakeHardwareFlags fakeHardwareFlags, fakeFarmwareData
} from "../../__test_support__/sequence_hardware_settings"; } from "../../__test_support__/fake_sequence_step_data";
import { push } from "../../history"; import { push } from "../../history";
import { mapStateToFolderProps } from "../../folders/map_state_to_props"; import { mapStateToFolderProps } from "../../folders/map_state_to_props";
import { fakeState } from "../../__test_support__/fake_state"; import { fakeState } from "../../__test_support__/fake_state";
@ -29,12 +29,7 @@ describe("<Sequences/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index, resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced", syncStatus: "synced",
hardwareFlags: fakeHardwareFlags(), hardwareFlags: fakeHardwareFlags(),
farmwareInfo: { farmwareData: fakeFarmwareData(),
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
},
shouldDisplay: jest.fn(), shouldDisplay: jest.fn(),
getWebAppConfigValue: jest.fn(), getWebAppConfigValue: jest.fn(),
menuOpen: false, menuOpen: false,

View File

@ -4,31 +4,25 @@ const mockData = {
fakeSequences: [fakeSequence()] fakeSequences: [fakeSequence()]
}; };
jest.mock("../../util/urls", () => { jest.mock("../../util/urls", () => ({
return { urlFriendly: jest.fn(x => x),
urlFriendly: jest.fn(x => x), lastUrlChunk: jest.fn(() => mockData.lastUrlChunk)
lastUrlChunk: jest.fn(() => mockData.lastUrlChunk) }));
};
});
jest.mock("../actions", () => ({ selectSequence: jest.fn() })); jest.mock("../actions", () => ({ selectSequence: jest.fn() }));
jest.mock("../../resources/selectors", () => { jest.mock("../../resources/selectors", () => ({
return { selectAllSequences: jest.fn(() => mockData.fakeSequences || []),
selectAllSequences: jest.fn(() => { }));
return mockData.fakeSequences || [];
})
};
});
jest.mock("../../redux/store", () => { jest.mock("../../redux/store", () => ({
return { store: {
store: { dispatch: jest.fn(),
dispatch: jest.fn(), getState: jest.fn(() => ({ resources: { index: {} } }))
getState: jest.fn(() => ({ resources: { index: {} } })) }
} }));
};
}); jest.mock("../../account/dev/dev_support", () => ({}));
import { setActiveSequenceByName } from "../set_active_sequence_by_name"; import { setActiveSequenceByName } from "../set_active_sequence_by_name";
import { selectSequence } from "../actions"; import { selectSequence } from "../actions";

View File

@ -3,7 +3,7 @@ import { fakeState } from "../../__test_support__/fake_state";
import { Feature } from "../../devices/interfaces"; import { Feature } from "../../devices/interfaces";
import { fakeFarmwareManifestV1 } from "../../__test_support__/fake_farmwares"; import { fakeFarmwareManifestV1 } from "../../__test_support__/fake_farmwares";
import { import {
fakeSequence, fakeWebAppConfig fakeSequence, fakeWebAppConfig, fakeFarmwareEnv
} from "../../__test_support__/fake_state/resources"; } from "../../__test_support__/fake_state/resources";
import { import {
buildResourceIndex buildResourceIndex
@ -67,12 +67,24 @@ describe("mapStateToProps()", () => {
"My Fake Farmware": fakeFarmwareManifestV1() "My Fake Farmware": fakeFarmwareManifestV1()
}; };
const props = mapStateToProps(state); const props = mapStateToProps(state);
expect(props.farmwareInfo.farmwareNames).toEqual(["My Fake Farmware"]); expect(props.farmwareData.farmwareNames).toEqual(["My Fake Farmware"]);
expect(props.farmwareInfo.showFirstPartyFarmware).toEqual(true); expect(props.farmwareData.showFirstPartyFarmware).toEqual(true);
expect(props.farmwareInfo.farmwareConfigs).toEqual({ expect(props.farmwareData.farmwareConfigs).toEqual({
"My Fake Farmware": [{ "My Fake Farmware": [{
name: "config_1", label: "Config 1", value: "4" name: "config_1", label: "Config 1", value: "4"
}] }]
}); });
}); });
it("returns api props", () => {
const state = fakeState();
const fakeEnv = fakeFarmwareEnv();
fakeEnv.body.key = "camera";
fakeEnv.body.value = "NONE";
state.resources = buildResourceIndex([fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
state.bot.hardware.informational_settings.controller_version = "8.0.0";
const props = mapStateToProps(state);
expect(props.farmwareData.cameraDisabled).toEqual(true);
});
}); });

View File

@ -6,7 +6,7 @@ import { StepDragger } from "../draggable/step_dragger";
import { renderCeleryNode } from "./step_tiles/index"; import { renderCeleryNode } from "./step_tiles/index";
import { ResourceIndex } from "../resources/interfaces"; import { ResourceIndex } from "../resources/interfaces";
import { getStepTag } from "../resources/sequence_tagging"; import { getStepTag } from "../resources/sequence_tagging";
import { HardwareFlags, FarmwareInfo } from "./interfaces"; import { HardwareFlags, FarmwareData } from "./interfaces";
import { ShouldDisplay } from "../devices/interfaces"; import { ShouldDisplay } from "../devices/interfaces";
import { AddCommandButton } from "./sequence_editor_middle_active"; import { AddCommandButton } from "./sequence_editor_middle_active";
import { ErrorBoundary } from "../error_boundary"; import { ErrorBoundary } from "../error_boundary";
@ -18,7 +18,7 @@ export interface AllStepsProps {
dispatch: Function; dispatch: Function;
resources: ResourceIndex; resources: ResourceIndex;
hardwareFlags?: HardwareFlags; hardwareFlags?: HardwareFlags;
farmwareInfo?: FarmwareInfo; farmwareData?: FarmwareData;
shouldDisplay?: ShouldDisplay; shouldDisplay?: ShouldDisplay;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins?: boolean; showPins?: boolean;
@ -43,7 +43,7 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
currentSequence: sequence, currentSequence: sequence,
resources: this.props.resources, resources: this.props.resources,
hardwareFlags: this.props.hardwareFlags, hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo, farmwareData: this.props.farmwareData,
shouldDisplay: this.props.shouldDisplay, shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: this.props.confirmStepDeletion, confirmStepDeletion: this.props.confirmStepDeletion,
showPins: this.props.showPins, showPins: this.props.showPins,

View File

@ -44,7 +44,7 @@ export interface Props {
resources: ResourceIndex; resources: ResourceIndex;
syncStatus: SyncStatus; syncStatus: SyncStatus;
hardwareFlags: HardwareFlags; hardwareFlags: HardwareFlags;
farmwareInfo: FarmwareInfo; farmwareData: FarmwareData;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
getWebAppConfigValue: GetWebAppConfigValue; getWebAppConfigValue: GetWebAppConfigValue;
menuOpen: boolean; menuOpen: boolean;
@ -58,7 +58,7 @@ export interface SequenceEditorMiddleProps {
resources: ResourceIndex; resources: ResourceIndex;
syncStatus: SyncStatus; syncStatus: SyncStatus;
hardwareFlags: HardwareFlags; hardwareFlags: HardwareFlags;
farmwareInfo: FarmwareInfo; farmwareData: FarmwareData;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
getWebAppConfigValue: GetWebAppConfigValue; getWebAppConfigValue: GetWebAppConfigValue;
menuOpen: boolean; menuOpen: boolean;
@ -189,11 +189,12 @@ export type dispatcher = (a: Function | { type: string }) => DataXferObj;
export type FarmwareConfigs = { [x: string]: FarmwareConfig[] }; export type FarmwareConfigs = { [x: string]: FarmwareConfig[] };
export interface FarmwareInfo { export interface FarmwareData {
farmwareNames: string[]; farmwareNames: string[];
firstPartyFarmwareNames: string[]; firstPartyFarmwareNames: string[];
showFirstPartyFarmware: boolean; showFirstPartyFarmware: boolean;
farmwareConfigs: FarmwareConfigs; farmwareConfigs: FarmwareConfigs;
cameraDisabled: boolean;
} }
export interface StepParams { export interface StepParams {
@ -203,7 +204,7 @@ export interface StepParams {
index: number; index: number;
resources: ResourceIndex; resources: ResourceIndex;
hardwareFlags?: HardwareFlags; hardwareFlags?: HardwareFlags;
farmwareInfo?: FarmwareInfo; farmwareData?: FarmwareData;
shouldDisplay?: ShouldDisplay; shouldDisplay?: ShouldDisplay;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins?: boolean; showPins?: boolean;

View File

@ -23,7 +23,7 @@ export class SequenceEditorMiddle
resources={this.props.resources} resources={this.props.resources}
syncStatus={this.props.syncStatus} syncStatus={this.props.syncStatus}
hardwareFlags={this.props.hardwareFlags} hardwareFlags={this.props.hardwareFlags}
farmwareInfo={this.props.farmwareInfo} farmwareData={this.props.farmwareData}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
getWebAppConfigValue={this.props.getWebAppConfigValue} getWebAppConfigValue={this.props.getWebAppConfigValue}
menuOpen={this.props.menuOpen} />} menuOpen={this.props.menuOpen} />}

View File

@ -249,7 +249,7 @@ export class SequenceEditorMiddleActive extends
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
resources: this.props.resources, resources: this.props.resources,
hardwareFlags: this.props.hardwareFlags, hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo, farmwareData: this.props.farmwareData,
shouldDisplay: this.props.shouldDisplay, shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: !!getConfig(BooleanSetting.confirm_step_deletion), confirmStepDeletion: !!getConfig(BooleanSetting.confirm_step_deletion),
showPins: !!getConfig(BooleanSetting.show_pins), showPins: !!getConfig(BooleanSetting.show_pins),

View File

@ -58,7 +58,7 @@ export class RawSequences extends React.Component<Props, {}> {
sequence={this.props.sequence} sequence={this.props.sequence}
resources={this.props.resources} resources={this.props.resources}
hardwareFlags={this.props.hardwareFlags} hardwareFlags={this.props.hardwareFlags}
farmwareInfo={this.props.farmwareInfo} farmwareData={this.props.farmwareData}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
getWebAppConfigValue={this.props.getWebAppConfigValue} getWebAppConfigValue={this.props.getWebAppConfigValue}
menuOpen={this.props.menuOpen} /> menuOpen={this.props.menuOpen} />

View File

@ -1,22 +1,20 @@
import { Everything } from "../interfaces"; import { Everything } from "../interfaces";
import { Props, HardwareFlags, FarmwareConfigs } from "./interfaces"; import { Props, HardwareFlags, FarmwareConfigs } from "./interfaces";
import { import { selectAllSequences, findSequence } from "../resources/selectors";
selectAllSequences, findSequence, maybeGetDevice
} from "../resources/selectors";
import { getStepTag } from "../resources/sequence_tagging"; import { getStepTag } from "../resources/sequence_tagging";
import { enabledAxisMap } from "../devices/components/axis_tracking_status"; import { enabledAxisMap } from "../devices/components/axis_tracking_status";
import { import { validFwConfig } from "../util";
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion, validFwConfig
} from "../util";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { getWebAppConfigValue } from "../config_storage/actions"; import { getWebAppConfigValue } from "../config_storage/actions";
import { getFirmwareConfig } from "../resources/getters"; import { getFirmwareConfig } from "../resources/getters";
import { Farmwares } from "../farmware/interfaces"; import { Farmwares } from "../farmware/interfaces";
import { manifestInfo } from "../farmware/generate_manifest_info"; import { manifestInfo } from "../farmware/generate_manifest_info";
import { DevSettings } from "../account/dev/dev_support";
import { calculateAxialLengths } from "../controls/move/direction_axes_props"; import { calculateAxialLengths } from "../controls/move/direction_axes_props";
import { mapStateToFolderProps } from "../folders/map_state_to_props"; import { mapStateToFolderProps } from "../folders/map_state_to_props";
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
import {
cameraDisabled
} from "../devices/components/fbos_settings/camera_selection";
export function mapStateToProps(props: Everything): Props { export function mapStateToProps(props: Everything): Props {
const uuid = props.resources.consumers.sequences.current; const uuid = props.resources.consumers.sequences.current;
@ -62,11 +60,8 @@ export function mapStateToProps(props: Everything): Props {
const farmwareConfigs: FarmwareConfigs = {}; const farmwareConfigs: FarmwareConfigs = {};
Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config); Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config);
const installedOsVersion = determineInstalledOsVersion( const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
props.bot, maybeGetDevice(props.resources.index)); const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
return { return {
dispatch: props.dispatch, dispatch: props.dispatch,
@ -79,11 +74,12 @@ export function mapStateToProps(props: Everything): Props {
.informational_settings .informational_settings
.sync_status || "unknown"), .sync_status || "unknown"),
hardwareFlags: hardwareFlags(), hardwareFlags: hardwareFlags(),
farmwareInfo: { farmwareData: {
farmwareNames, farmwareNames,
firstPartyFarmwareNames, firstPartyFarmwareNames,
showFirstPartyFarmware, showFirstPartyFarmware,
farmwareConfigs, farmwareConfigs,
cameraDisabled: cameraDisabled(env),
}, },
shouldDisplay, shouldDisplay,
getWebAppConfigValue: getConfig, getWebAppConfigValue: getConfig,

View File

@ -6,7 +6,7 @@ import { emptyState } from "../../../resources/reducer";
import { HardwareFlags } from "../../interfaces"; import { HardwareFlags } from "../../interfaces";
import { import {
fakeHardwareFlags fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings"; } from "../../../__test_support__/fake_sequence_step_data";
describe("<TileCalibrate/>", () => { describe("<TileCalibrate/>", () => {
const fakeProps = (): CalibrateParams => ({ const fakeProps = (): CalibrateParams => ({

View File

@ -4,8 +4,11 @@ import { mount, shallow } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { ExecuteScript } from "farmbot/dist"; import { ExecuteScript } from "farmbot/dist";
import { StepParams } from "../../interfaces"; import { StepParams } from "../../interfaces";
import { Actions } from "../../../constants"; import { Actions, Content } from "../../../constants";
import { emptyState } from "../../../resources/reducer"; import { emptyState } from "../../../resources/reducer";
import {
fakeFarmwareData
} from "../../../__test_support__/fake_sequence_step_data";
describe("<TileExecuteScript/>", () => { describe("<TileExecuteScript/>", () => {
const fakeProps = (): StepParams => { const fakeProps = (): StepParams => {
@ -15,18 +18,17 @@ describe("<TileExecuteScript/>", () => {
label: "farmware-to-execute" label: "farmware-to-execute"
} }
}; };
const farmwareData = fakeFarmwareData();
farmwareData.farmwareNames = ["one", "two", "three"];
farmwareData.firstPartyFarmwareNames = ["one"];
farmwareData.farmwareConfigs = { "farmware-to-execute": [] };
return { return {
currentSequence: fakeSequence(), currentSequence: fakeSequence(),
currentStep, currentStep,
dispatch: jest.fn(), dispatch: jest.fn(),
index: 0, index: 0,
resources: emptyState().index, resources: emptyState().index,
farmwareInfo: { farmwareData,
farmwareNames: ["one", "two", "three"],
firstPartyFarmwareNames: ["one"],
showFirstPartyFarmware: false,
farmwareConfigs: { "farmware-to-execute": [] },
},
confirmStepDeletion: false, confirmStepDeletion: false,
}; };
}; };
@ -61,7 +63,7 @@ describe("<TileExecuteScript/>", () => {
it("shows 1st party in list", () => { it("shows 1st party in list", () => {
const p = fakeProps(); const p = fakeProps();
p.farmwareInfo && (p.farmwareInfo.showFirstPartyFarmware = true); p.farmwareData && (p.farmwareData.showFirstPartyFarmware = true);
const wrapper = shallow(<TileExecuteScript {...p} />); const wrapper = shallow(<TileExecuteScript {...p} />);
expect(wrapper.find("FBSelect").props().list).toEqual([ expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "one", value: "one" }, { label: "one", value: "one" },
@ -81,7 +83,7 @@ describe("<TileExecuteScript/>", () => {
it("shows special 1st-party Farmware name", () => { it("shows special 1st-party Farmware name", () => {
const p = fakeProps(); const p = fakeProps();
(p.currentStep as ExecuteScript).args.label = "plant-detection"; (p.currentStep as ExecuteScript).args.label = "plant-detection";
p.farmwareInfo && p.farmwareInfo.farmwareNames.push("plant-detection"); p.farmwareData && p.farmwareData.farmwareNames.push("plant-detection");
const wrapper = mount(<TileExecuteScript {...p} />); const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.find("label").length).toEqual(1); expect(wrapper.find("label").length).toEqual(1);
expect(wrapper.text()).toContain("Weed Detector"); expect(wrapper.text()).toContain("Weed Detector");
@ -89,7 +91,7 @@ describe("<TileExecuteScript/>", () => {
it("renders manual input", () => { it("renders manual input", () => {
const p = fakeProps(); const p = fakeProps();
p.farmwareInfo = undefined; p.farmwareData = undefined;
const wrapper = mount(<TileExecuteScript {...p} />); const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.find("button").text()).toEqual("Manual Input"); expect(wrapper.find("button").text()).toEqual("Manual Input");
expect(wrapper.find("label").at(1).text()).toEqual("Manual input"); expect(wrapper.find("label").at(1).text()).toEqual("Manual input");
@ -131,4 +133,12 @@ describe("<TileExecuteScript/>", () => {
type: Actions.OVERWRITE_RESOURCE type: Actions.OVERWRITE_RESOURCE
}); });
}); });
it("displays warning when camera is disabled", () => {
const p = fakeProps();
(p.currentStep as ExecuteScript).args.label = "plant-detection";
p.farmwareData && (p.farmwareData.cameraDisabled = true);
const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.text()).toContain(Content.NO_CAMERA_SELECTED);
});
}); });

View File

@ -4,7 +4,7 @@ import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources"; import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { import {
fakeHardwareFlags fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings"; } from "../../../__test_support__/fake_sequence_step_data";
import { HardwareFlags } from "../../interfaces"; import { HardwareFlags } from "../../interfaces";
import { emptyState } from "../../../resources/reducer"; import { emptyState } from "../../../resources/reducer";

View File

@ -3,7 +3,7 @@ import { mount } from "enzyme";
import { MoveAbsoluteWarningProps } from "../../interfaces"; import { MoveAbsoluteWarningProps } from "../../interfaces";
import { import {
fakeHardwareFlags fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings"; } from "../../../__test_support__/fake_sequence_step_data";
import { MoveAbsoluteWarning } from "../tile_move_absolute_conflict_check"; import { MoveAbsoluteWarning } from "../tile_move_absolute_conflict_check";
describe("<MoveAbsoluteWarning/>", () => { describe("<MoveAbsoluteWarning/>", () => {

View File

@ -13,7 +13,7 @@ import {
} from "farmbot"; } from "farmbot";
import { import {
fakeHardwareFlags fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings"; } from "../../../__test_support__/fake_sequence_step_data";
import { emptyState } from "../../../resources/reducer"; import { emptyState } from "../../../resources/reducer";
import { inputEvent } from "../../../__test_support__/fake_input_event"; import { inputEvent } from "../../../__test_support__/fake_input_event";
import { StepParams } from "../../interfaces"; import { StepParams } from "../../interfaces";

View File

@ -5,6 +5,10 @@ import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { TakePhoto } from "farmbot/dist"; import { TakePhoto } from "farmbot/dist";
import { StepParams } from "../../interfaces"; import { StepParams } from "../../interfaces";
import { emptyState } from "../../../resources/reducer"; import { emptyState } from "../../../resources/reducer";
import {
fakeFarmwareData
} from "../../../__test_support__/fake_sequence_step_data";
import { Content } from "../../../constants";
describe("<TileTakePhoto/>", () => { describe("<TileTakePhoto/>", () => {
const currentStep: TakePhoto = { const currentStep: TakePhoto = {
@ -19,6 +23,7 @@ describe("<TileTakePhoto/>", () => {
index: 0, index: 0,
resources: emptyState().index, resources: emptyState().index,
confirmStepDeletion: false, confirmStepDeletion: false,
farmwareData: fakeFarmwareData(),
}); });
it("renders step", () => { it("renders step", () => {
@ -34,4 +39,11 @@ describe("<TileTakePhoto/>", () => {
expect(inputs.first().props().placeholder).toEqual("Take a Photo"); expect(inputs.first().props().placeholder).toEqual("Take a Photo");
expect(wrapper.text()).toContain("farmware page"); expect(wrapper.text()).toContain("farmware page");
}); });
it("displays warning when camera is disabled", () => {
const p = fakeProps();
p.farmwareData && (p.farmwareData.cameraDisabled = true);
const wrapper = mount(<TileTakePhoto {...p} />);
expect(wrapper.text()).toContain(Content.NO_CAMERA_SELECTED);
});
}); });

View File

@ -1,8 +1,10 @@
import * as React from "react"; import * as React from "react";
import { StepParams } from "../interfaces"; import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips, Content } from "../../constants";
import { StepInputBox } from "../inputs/step_input_box"; import { StepInputBox } from "../inputs/step_input_box";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index"; import {
StepWrapper, StepHeader, StepContent, StepWarning
} from "../step_ui/index";
import { Row, Col, FBSelect, DropDownItem } from "../../ui/index"; import { Row, Col, FBSelect, DropDownItem } from "../../ui/index";
import { editStep } from "../../api/crud"; import { editStep } from "../../api/crud";
import { ExecuteScript, FarmwareConfig } from "farmbot"; import { ExecuteScript, FarmwareConfig } from "farmbot";
@ -10,14 +12,14 @@ import { FarmwareInputs, farmwareList } from "./tile_execute_script_support";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
export function TileExecuteScript(props: StepParams) { export function TileExecuteScript(props: StepParams) {
const { dispatch, currentStep, index, currentSequence, farmwareInfo } = props; const { dispatch, currentStep, index, currentSequence, farmwareData } = props;
if (currentStep.kind === "execute_script") { if (currentStep.kind === "execute_script") {
const farmwareName = currentStep.args.label; const farmwareName = currentStep.args.label;
/** Selected Farmware is installed on connected bot. */ /** Selected Farmware is installed on connected bot. */
const isInstalled = (name: string): boolean => { const isInstalled = (name: string): boolean => {
return !!(farmwareInfo && farmwareInfo.farmwareNames.includes(name)); return !!(farmwareData && farmwareData.farmwareNames.includes(name));
}; };
const selectedFarmwareDDI = (name: string): DropDownItem => { const selectedFarmwareDDI = (name: string): DropDownItem => {
@ -52,8 +54,8 @@ export function TileExecuteScript(props: StepParams) {
/** Configs (inputs) from Farmware manifest for <FarmwareInputs />. */ /** Configs (inputs) from Farmware manifest for <FarmwareInputs />. */
const currentFarmwareConfigDefaults = (fwName: string): FarmwareConfig[] => { const currentFarmwareConfigDefaults = (fwName: string): FarmwareConfig[] => {
return farmwareInfo && farmwareInfo.farmwareConfigs[fwName] return farmwareData && farmwareData.farmwareConfigs[fwName]
? farmwareInfo.farmwareConfigs[fwName] ? farmwareData.farmwareConfigs[fwName]
: []; : [];
}; };
@ -66,14 +68,20 @@ export function TileExecuteScript(props: StepParams) {
currentStep={currentStep} currentStep={currentStep}
dispatch={dispatch} dispatch={dispatch}
index={index} index={index}
confirmStepDeletion={props.confirmStepDeletion} /> confirmStepDeletion={props.confirmStepDeletion}>
{props.farmwareData && props.farmwareData.cameraDisabled &&
(farmwareName === "plant-detection") &&
<StepWarning
titleBase={t(Content.NO_CAMERA_SELECTED)}
warning={t(ToolTips.SELECT_A_CAMERA)} />}
</StepHeader>
<StepContent className={className}> <StepContent className={className}>
<Row> <Row>
<Col xs={12}> <Col xs={12}>
<label>{t("Package Name")}</label> <label>{t("Package Name")}</label>
<FBSelect <FBSelect
key={JSON.stringify(props.currentSequence)} key={JSON.stringify(props.currentSequence)}
list={farmwareList(farmwareInfo)} list={farmwareList(farmwareData)}
selectedItem={selectedFarmwareDDI(farmwareName)} selectedItem={selectedFarmwareDDI(farmwareName)}
onChange={updateStepFarmwareSelection} onChange={updateStepFarmwareSelection}
allowEmpty={true} allowEmpty={true}

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { FarmwareInfo } from "../interfaces"; import { FarmwareData } from "../interfaces";
import { DropDownItem, BlurableInput, Help } from "../../ui/index"; import { DropDownItem, BlurableInput, Help } from "../../ui/index";
import { without, isNumber } from "lodash"; import { without, isNumber } from "lodash";
import { ExecuteScript, Pair, FarmwareConfig } from "farmbot"; import { ExecuteScript, Pair, FarmwareConfig } from "farmbot";
@ -113,11 +113,11 @@ const farmwareInputs =
/** List of installed Farmware, if bot is connected (for DropDown). */ /** List of installed Farmware, if bot is connected (for DropDown). */
export const farmwareList = export const farmwareList =
(farmwareInfo: FarmwareInfo | undefined): DropDownItem[] => { (farmwareData: FarmwareData | undefined): DropDownItem[] => {
if (farmwareInfo) { if (farmwareData) {
const { const {
farmwareNames, showFirstPartyFarmware, firstPartyFarmwareNames farmwareNames, showFirstPartyFarmware, firstPartyFarmwareNames
} = farmwareInfo; } = farmwareData;
return farmwareNames return farmwareNames
.filter(x => (firstPartyFarmwareNames && !showFirstPartyFarmware) .filter(x => (firstPartyFarmwareNames && !showFirstPartyFarmware)
? !firstPartyFarmwareNames.includes(x) : x) ? !firstPartyFarmwareNames.includes(x) : x)

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { StepParams } from "../interfaces"; import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips, Content } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui"; import { StepWrapper, StepHeader, StepContent, StepWarning } from "../step_ui";
import { Col, Row } from "../../ui/index"; import { Col, Row } from "../../ui/index";
import { Link } from "../../link"; import { Link } from "../../link";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
@ -17,7 +17,12 @@ export function TileTakePhoto(props: StepParams) {
currentStep={currentStep} currentStep={currentStep}
dispatch={dispatch} dispatch={dispatch}
index={index} index={index}
confirmStepDeletion={props.confirmStepDeletion} /> confirmStepDeletion={props.confirmStepDeletion}>
{props.farmwareData && props.farmwareData.cameraDisabled &&
<StepWarning
titleBase={t(Content.NO_CAMERA_SELECTED)}
warning={t(ToolTips.SELECT_A_CAMERA)} />}
</StepHeader>
<StepContent className={className}> <StepContent className={className}>
<Row> <Row>
<Col xs={12}> <Col xs={12}>

View File

@ -6,6 +6,7 @@ import { t } from "../../i18next_wrapper";
interface StepWarningProps { interface StepWarningProps {
warning: string; warning: string;
conflicts?: Record<Xyz, boolean>; conflicts?: Record<Xyz, boolean>;
titleBase?: string;
} }
const TITLE_BASE = t("Hardware setting conflict"); const TITLE_BASE = t("Hardware setting conflict");
@ -20,11 +21,10 @@ export const conflictsString = (conflicts: Record<Xyz, boolean>) => {
}; };
export function StepWarning(props: StepWarningProps) { export function StepWarning(props: StepWarningProps) {
const { conflicts, warning } = props; const { conflicts, warning, titleBase } = props;
const warningTitle = () => { const warningTitle = () => {
return conflicts return (titleBase || TITLE_BASE) +
? TITLE_BASE + ": " + conflictsString(conflicts) (conflicts ? ": " + conflictsString(conflicts) : "");
: TITLE_BASE;
}; };
return <div className="step-warning"> return <div className="step-warning">
<Popover <Popover