misc updates
parent
49fdced812
commit
9bd98aca1e
|
@ -10,6 +10,7 @@ import { Move } from "./move/move";
|
|||
import { BooleanSetting } from "../session_keys";
|
||||
import { SensorReadings } from "./sensor_readings/sensor_readings";
|
||||
import { isBotOnline } from "../devices/must_be_online";
|
||||
import { hasSensors } from "../devices/components/firmware_hardware_support";
|
||||
|
||||
/** Controls page. */
|
||||
export class RawControls extends React.Component<Props, {}> {
|
||||
|
@ -24,7 +25,8 @@ export class RawControls extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
get hideSensors() {
|
||||
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors);
|
||||
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors)
|
||||
|| !hasSensors(this.props.firmwareHardware);
|
||||
}
|
||||
|
||||
move = () => <Move
|
||||
|
|
|
@ -7,7 +7,7 @@ import { AxisInputBoxGroup } from "../axis_input_box_group";
|
|||
import { GetWebAppBool } from "./interfaces";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
|
||||
export interface BotPositionRowsProps {
|
||||
|
@ -34,12 +34,12 @@ export const BotPositionRows = (props: BotPositionRowsProps) => {
|
|||
<AxisDisplayGroup
|
||||
position={locationData.position}
|
||||
label={t("Motor Coordinates (mm)")} />
|
||||
{!isExpressBoard(props.firmwareHardware) &&
|
||||
{hasEncoders(props.firmwareHardware) &&
|
||||
getValue(BooleanSetting.scaled_encoders) &&
|
||||
<AxisDisplayGroup
|
||||
position={locationData.scaled_encoders}
|
||||
label={t("Scaled Encoder (mm)")} />}
|
||||
{!isExpressBoard(props.firmwareHardware) &&
|
||||
{hasEncoders(props.firmwareHardware) &&
|
||||
getValue(BooleanSetting.raw_encoders) &&
|
||||
<AxisDisplayGroup
|
||||
position={locationData.raw_encoders}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
|||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
|
||||
|
||||
export const moveWidgetSetting =
|
||||
(toggle: ToggleWebAppBool, getValue: GetWebAppBool) =>
|
||||
|
@ -36,7 +36,7 @@ export const MoveWidgetSettingsMenu = (
|
|||
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
|
||||
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
|
||||
|
||||
{!isExpressBoard(firmwareHardware) &&
|
||||
{hasEncoders(firmwareHardware) &&
|
||||
<div className="display-encoder-data">
|
||||
<p>{t("Display Encoder Data")}</p>
|
||||
<Setting
|
||||
|
|
|
@ -88,4 +88,14 @@ describe("<Peripherals />", () => {
|
|||
clickButton(wrapper, 3, "stock");
|
||||
expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
|
||||
});
|
||||
|
||||
it("hides stock button", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "none";
|
||||
const wrapper = mount(<Peripherals {...p} />);
|
||||
wrapper.setState({ isEditing: true });
|
||||
const btn = wrapper.find("button").at(3);
|
||||
expect(btn.text().toLowerCase()).toContain("stock");
|
||||
expect(btn.props().hidden).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -108,7 +108,7 @@ export class Peripherals
|
|||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
hidden={!isEditing}
|
||||
hidden={!isEditing || this.props.firmwareHardware == "none"}
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={() => this.stockPeripherals.map(p =>
|
||||
|
|
|
@ -72,6 +72,7 @@ describe("<Sensors />", () => {
|
|||
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
|
||||
wrapper.setState({ isEditing: true });
|
||||
clickButton(wrapper, 3, "stock sensors");
|
||||
expect(wrapper.find("button").at(3).props().hidden).toBeFalsy();
|
||||
expect(p.dispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
@ -79,6 +80,18 @@ describe("<Sensors />", () => {
|
|||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount(<Sensors {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
|
||||
const btn = wrapper.find("button").at(3);
|
||||
expect(btn.text().toLowerCase()).toContain("stock");
|
||||
expect(btn.props().hidden).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides stock button", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "none";
|
||||
const wrapper = mount(<Sensors {...p} />);
|
||||
wrapper.setState({ isEditing: true });
|
||||
const btn = wrapper.find("button").at(3);
|
||||
expect(btn.text().toLowerCase()).toContain("stock");
|
||||
expect(btn.props().hidden).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@ import { saveAll, init } from "../../api/crud";
|
|||
import { ToolTips } from "../../constants";
|
||||
import { uniq } from "lodash";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
|
||||
export class Sensors extends React.Component<SensorsProps, SensorState> {
|
||||
constructor(props: SensorsProps) {
|
||||
|
@ -80,15 +79,14 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
|
|||
onClick={() => this.newSensor()}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
{!isExpressBoard(this.props.firmwareHardware) &&
|
||||
<button
|
||||
hidden={!isEditing}
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={this.stockSensors}>
|
||||
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
|
||||
{t("Stock sensors")}
|
||||
</button>}
|
||||
<button
|
||||
hidden={!isEditing || this.props.firmwareHardware == "none"}
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={this.stockSensors}>
|
||||
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
|
||||
{t("Stock sensors")}
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
{this.showPins()}
|
||||
|
|
|
@ -958,3 +958,10 @@
|
|||
margin-right: 1.5rem;
|
||||
&:hover { color: $white; }
|
||||
}
|
||||
|
||||
.desktop-hide {
|
||||
display: none !important;
|
||||
@media screen and (max-width: 1075px) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -407,6 +407,18 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
.load-progress-bar-wrapper {
|
||||
position: absolute;
|
||||
top: 3.2rem;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
.load-progress-bar {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.firmware-setting-export-menu {
|
||||
button {
|
||||
margin-bottom: 1rem;
|
||||
|
@ -1654,3 +1666,9 @@ textarea:focus {
|
|||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.read-only-icon {
|
||||
margin: 9px 0px 0px 9px;
|
||||
float: right;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
|
|
@ -322,6 +322,9 @@
|
|||
border-left: 4px solid transparent;
|
||||
&.active {
|
||||
border-left: 4px solid $dark_gray;
|
||||
p {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.fa-chevron-down, .fa-chevron-right {
|
||||
position: absolute;
|
||||
|
@ -330,11 +333,11 @@
|
|||
font-size: 1.1rem;
|
||||
}
|
||||
.folder-settings-icon,
|
||||
.fa-bars {
|
||||
.fa-arrows-v {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.fa-bars, .fa-ellipsis-v {
|
||||
.fa-arrows-v, .fa-ellipsis-v {
|
||||
display: none;
|
||||
}
|
||||
.fa-ellipsis-v {
|
||||
|
@ -342,8 +345,14 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 450px) {
|
||||
.fa-arrows-v, .fa-ellipsis-v {
|
||||
display: block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.fa-bars, .fa-ellipsis-v {
|
||||
.fa-arrows-v, .fa-ellipsis-v {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@ -367,7 +376,7 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
width: 75%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 0;
|
||||
|
|
|
@ -12,6 +12,8 @@ import { clickButton } from "../../../__test_support__/helpers";
|
|||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import { Color } from "../../../ui";
|
||||
|
||||
describe("<HardwareSettings />", () => {
|
||||
const fakeProps = (): HardwareSettingsProps => ({
|
||||
|
@ -68,4 +70,41 @@ describe("<HardwareSettings />", () => {
|
|||
const wrapper = shallow(<HardwareSettings {...p} />);
|
||||
expect(wrapper.html()).toContain("fa-download");
|
||||
});
|
||||
|
||||
it("shows setting load progress", () => {
|
||||
type ConsistencyLookup = Record<keyof FirmwareConfig, boolean>;
|
||||
const consistent: Partial<ConsistencyLookup> =
|
||||
({ id: false, encoder_invert_x: true, encoder_enabled_y: false });
|
||||
const consistencyLookup = consistent as ConsistencyLookup;
|
||||
const p = fakeProps();
|
||||
const fakeConfig: Partial<FirmwareConfig> =
|
||||
({ id: 0, encoder_invert_x: 1, encoder_enabled_y: 0 });
|
||||
p.firmwareConfig = fakeConfig as FirmwareConfig;
|
||||
p.sourceFwConfig = x =>
|
||||
({ value: p.firmwareConfig?.[x], consistent: consistencyLookup[x] });
|
||||
const wrapper = mount(<HardwareSettings {...p} />);
|
||||
const barStyle = wrapper.find(".load-progress-bar").props().style;
|
||||
expect(barStyle?.background).toEqual(Color.white);
|
||||
expect(barStyle?.width).toEqual("50%");
|
||||
});
|
||||
|
||||
it("shows setting load progress: 0%", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareConfig = fakeFirmwareConfig().body;
|
||||
p.sourceFwConfig = () => ({ value: 0, consistent: false });
|
||||
const wrapper = mount(<HardwareSettings {...p} />);
|
||||
const barStyle = wrapper.find(".load-progress-bar").props().style;
|
||||
expect(barStyle?.width).toEqual("0%");
|
||||
expect(barStyle?.background).toEqual(Color.darkGray);
|
||||
});
|
||||
|
||||
it("shows setting load progress: 100%", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareConfig = fakeFirmwareConfig().body;
|
||||
p.sourceFwConfig = () => ({ value: 0, consistent: true });
|
||||
const wrapper = mount(<HardwareSettings {...p} />);
|
||||
const barStyle = wrapper.find(".load-progress-bar").props().style;
|
||||
expect(barStyle?.width).toEqual("100%");
|
||||
expect(barStyle?.background).toEqual(Color.darkGray);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,15 +16,31 @@ export const getFwHardwareValue =
|
|||
return isFwHardwareValue(value) ? value : undefined;
|
||||
};
|
||||
|
||||
const TMC_BOARDS = ["express_k10", "farmduino_k15"];
|
||||
const NO_BUTTONS = ["arduino", "farmduino", "none"];
|
||||
const EXPRESS_BOARDS = ["express_k10"];
|
||||
const NO_SENSORS = [...EXPRESS_BOARDS];
|
||||
const NO_ENCODERS = [...EXPRESS_BOARDS];
|
||||
const NO_TOOLS = [...EXPRESS_BOARDS];
|
||||
const NO_TMC = ["arduino", "farmduino", "farmduino_k14"];
|
||||
|
||||
export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware));
|
||||
!firmwareHardware || !NO_TMC.includes(firmwareHardware);
|
||||
|
||||
export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware));
|
||||
|
||||
export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_BUTTONS.includes(firmwareHardware);
|
||||
|
||||
export const hasEncoders = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_ENCODERS.includes(firmwareHardware);
|
||||
|
||||
export const hasSensors = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_SENSORS.includes(firmwareHardware);
|
||||
|
||||
export const hasUTM = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_TOOLS.includes(firmwareHardware);
|
||||
|
||||
export const getBoardIdentifier =
|
||||
(firmwareVersion: string | undefined): string =>
|
||||
firmwareVersion ? firmwareVersion.split(".")[3] : "undefined";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
|
||||
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
|
||||
import { HardwareSettingsProps } from "../interfaces";
|
||||
import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index";
|
||||
import { HardwareSettingsProps, SourceFwConfig } from "../interfaces";
|
||||
import { isBotOnline } from "../must_be_online";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { DangerZone } from "./hardware_settings/danger_zone";
|
||||
|
@ -19,6 +19,8 @@ import { t } from "../../i18next_wrapper";
|
|||
import { PinBindings } from "./hardware_settings/pin_bindings";
|
||||
import { ErrorHandling } from "./hardware_settings/error_handling";
|
||||
import { maybeOpenPanel } from "./maybe_highlight";
|
||||
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import type { McuParamName } from "farmbot";
|
||||
|
||||
export class HardwareSettings extends
|
||||
React.Component<HardwareSettingsProps, {}> {
|
||||
|
@ -36,7 +38,10 @@ export class HardwareSettings extends
|
|||
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
|
||||
const commonProps = { dispatch, controlPanelState };
|
||||
return <Widget className="hardware-widget">
|
||||
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
|
||||
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
|
||||
<SettingLoadProgress firmwareConfig={firmwareConfig}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
|
@ -78,8 +83,33 @@ export class HardwareSettings extends
|
|||
onReset={MCUFactoryReset}
|
||||
botDisconnected={botDisconnected} />
|
||||
<PinBindings {...commonProps}
|
||||
resources={resources} />
|
||||
resources={resources}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
||||
interface SettingLoadProgressProps {
|
||||
sourceFwConfig: SourceFwConfig;
|
||||
firmwareConfig: FirmwareConfig | undefined;
|
||||
}
|
||||
|
||||
const UNTRACKED_KEYS: (keyof FirmwareConfig)[] = [
|
||||
"id", "created_at", "updated_at", "device_id", "api_migrated",
|
||||
"param_config_ok", "param_test", "param_use_eeprom", "param_version",
|
||||
];
|
||||
|
||||
/** Track firmware configuration adoption by FarmBot OS. */
|
||||
const SettingLoadProgress = (props: SettingLoadProgressProps) => {
|
||||
const keys = Object.keys(props.firmwareConfig || {})
|
||||
.filter((k: keyof FirmwareConfig) => !UNTRACKED_KEYS.includes(k));
|
||||
const loadedKeys = keys.filter((key: McuParamName) =>
|
||||
props.sourceFwConfig(key).consistent);
|
||||
const progress = loadedKeys.length / keys.length * 100;
|
||||
const color = [0, 100].includes(progress) ? Color.darkGray : Color.white;
|
||||
return <div className={"load-progress-bar-wrapper"}>
|
||||
<div className={"load-progress-bar"}
|
||||
style={{ width: `${progress}%`, background: color }} />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ describe("<PinBindings />", () => {
|
|||
dispatch: jest.fn(),
|
||||
controlPanelState: panelState(),
|
||||
resources: buildResourceIndex([]).index,
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("shows pin binding labels", () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
|
|||
import { EncodersProps } from "../interfaces";
|
||||
import { Header } from "./header";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import { isExpressBoard } from "../firmware_hardware_support";
|
||||
import { hasEncoders } from "../firmware_hardware_support";
|
||||
import { Highlight } from "../maybe_highlight";
|
||||
|
||||
export function Encoders(props: EncodersProps) {
|
||||
|
@ -18,23 +18,23 @@ export function Encoders(props: EncodersProps) {
|
|||
y: !sourceFwConfig("encoder_enabled_y").value,
|
||||
z: !sourceFwConfig("encoder_enabled_z").value
|
||||
};
|
||||
const isExpress = isExpressBoard(firmwareHardware);
|
||||
const showEncoders = hasEncoders(firmwareHardware);
|
||||
|
||||
return <Highlight className={"section"}
|
||||
settingName={DeviceSetting.encoders}>
|
||||
<Header
|
||||
expanded={encoders}
|
||||
title={isExpress
|
||||
title={!showEncoders
|
||||
? DeviceSetting.stallDetection
|
||||
: DeviceSetting.encoders}
|
||||
panel={"encoders"}
|
||||
dispatch={dispatch} />
|
||||
<Collapse isOpen={!!encoders}>
|
||||
<BooleanMCUInputGroup
|
||||
label={isExpress
|
||||
label={!showEncoders
|
||||
? DeviceSetting.enableStallDetection
|
||||
: DeviceSetting.enableEncoders}
|
||||
tooltip={isExpress
|
||||
tooltip={!showEncoders
|
||||
? ToolTips.ENABLE_STALL_DETECTION
|
||||
: ToolTips.ENABLE_ENCODERS}
|
||||
x={"encoder_enabled_x"}
|
||||
|
@ -42,7 +42,7 @@ export function Encoders(props: EncodersProps) {
|
|||
z={"encoder_enabled_z"}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
{isExpress &&
|
||||
{!showEncoders &&
|
||||
<NumericMCUInputGroup
|
||||
label={DeviceSetting.stallSensitivity}
|
||||
tooltip={ToolTips.STALL_SENSITIVITY}
|
||||
|
@ -52,7 +52,7 @@ export function Encoders(props: EncodersProps) {
|
|||
gray={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
{!isExpress &&
|
||||
{showEncoders &&
|
||||
<BooleanMCUInputGroup
|
||||
label={DeviceSetting.useEncodersForPositioning}
|
||||
tooltip={ToolTips.ENCODER_POSITIONING}
|
||||
|
@ -62,7 +62,7 @@ export function Encoders(props: EncodersProps) {
|
|||
grayscale={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
{!isExpress &&
|
||||
{showEncoders &&
|
||||
<BooleanMCUInputGroup
|
||||
label={DeviceSetting.invertEncoders}
|
||||
tooltip={ToolTips.INVERT_ENCODERS}
|
||||
|
@ -74,7 +74,7 @@ export function Encoders(props: EncodersProps) {
|
|||
sourceFwConfig={sourceFwConfig} />}
|
||||
<NumericMCUInputGroup
|
||||
label={DeviceSetting.maxMissedSteps}
|
||||
tooltip={isExpress
|
||||
tooltip={!showEncoders
|
||||
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
|
||||
: ToolTips.MAX_MISSED_STEPS_ENCODERS}
|
||||
x={"encoder_missed_steps_max_x"}
|
||||
|
@ -92,7 +92,7 @@ export function Encoders(props: EncodersProps) {
|
|||
gray={encodersDisabled}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
{!isExpress &&
|
||||
{showEncoders &&
|
||||
<NumericMCUInputGroup
|
||||
label={DeviceSetting.encoderScaling}
|
||||
tooltip={ToolTips.ENCODER_SCALING}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Header } from "./header";
|
|||
import { Collapse } from "@blueprintjs/core";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { calculateScale } from "./motors";
|
||||
import { isExpressBoard } from "../firmware_hardware_support";
|
||||
import { hasEncoders } from "../firmware_hardware_support";
|
||||
import { getDevice } from "../../../device";
|
||||
import { commandErr } from "../../actions";
|
||||
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
|
||||
|
@ -44,7 +44,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
type={"find_home"}
|
||||
title={DeviceSetting.homing}
|
||||
axisTitle={t("FIND HOME")}
|
||||
toolTip={isExpressBoard(firmwareHardware)
|
||||
toolTip={!hasEncoders(firmwareHardware)
|
||||
? ToolTips.HOMING_STALL_DETECTION
|
||||
: ToolTips.HOMING_ENCODERS}
|
||||
action={axis => getDevice()
|
||||
|
@ -56,7 +56,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
type={"calibrate"}
|
||||
title={DeviceSetting.calibration}
|
||||
axisTitle={t("CALIBRATE")}
|
||||
toolTip={isExpressBoard(firmwareHardware)
|
||||
toolTip={!hasEncoders(firmwareHardware)
|
||||
? ToolTips.CALIBRATION_STALL_DETECTION
|
||||
: ToolTips.CALIBRATION_ENCODERS}
|
||||
action={axis => getDevice().calibrate({ axis })
|
||||
|
@ -74,7 +74,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
botDisconnected={botDisconnected} />
|
||||
<BooleanMCUInputGroup
|
||||
label={DeviceSetting.findHomeOnBoot}
|
||||
tooltip={isExpressBoard(firmwareHardware)
|
||||
tooltip={!hasEncoders(firmwareHardware)
|
||||
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
|
||||
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
|
||||
disable={disabled}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Highlight } from "../maybe_highlight";
|
|||
export function PinBindings(props: PinBindingsProps) {
|
||||
|
||||
const { pin_bindings } = props.controlPanelState;
|
||||
const { dispatch, resources } = props;
|
||||
const { dispatch, resources, firmwareHardware } = props;
|
||||
|
||||
return <Highlight className={"section"}
|
||||
settingName={DeviceSetting.pinBindings}>
|
||||
|
@ -19,7 +19,8 @@ export function PinBindings(props: PinBindingsProps) {
|
|||
panel={"pin_bindings"}
|
||||
dispatch={dispatch} />
|
||||
<Collapse isOpen={!!pin_bindings}>
|
||||
<PinBindingsContent dispatch={dispatch} resources={resources} />
|
||||
<PinBindingsContent dispatch={dispatch} resources={resources}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
</Collapse>
|
||||
</Highlight>;
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ export interface PinBindingsProps {
|
|||
dispatch: Function;
|
||||
controlPanelState: ControlPanelState;
|
||||
resources: ResourceIndex;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface DangerZoneProps {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import {
|
||||
ConnectivityDiagram,
|
||||
ConnectivityDiagramProps,
|
||||
|
@ -83,9 +82,9 @@ describe("getTextPosition()", () => {
|
|||
|
||||
describe("nodeLabel()", () => {
|
||||
it("renders", () => {
|
||||
const label = mount(nodeLabel("Top Node", "top" as DiagramNodes));
|
||||
expect(label.text()).toEqual("Top Node");
|
||||
expect(label.props())
|
||||
const label = svgMount(nodeLabel("Top Node", "top" as DiagramNodes));
|
||||
expect(label.find("text").text()).toEqual("Top Node");
|
||||
expect(label.find("text").props())
|
||||
.toEqual({ children: "Top Node", textAnchor: "middle", x: 0, y: -75 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ describe("<PinBindingsContent/>", () => {
|
|||
return {
|
||||
dispatch: jest.fn(),
|
||||
resources: resources,
|
||||
firmwareHardware: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,37 @@
|
|||
jest.mock("../../../api/crud", () => ({
|
||||
initSave: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { StockPinBindingsButton } from "../tagged_pin_binding_init";
|
||||
import {
|
||||
StockPinBindingsButton, StockPinBindingsButtonProps
|
||||
} from "../tagged_pin_binding_init";
|
||||
import { initSave } from "../../../api/crud";
|
||||
import { stockPinBindings } from "../list_and_label_support";
|
||||
|
||||
describe("<StockPinBindingsButton />", () => {
|
||||
const fakeProps = () => ({
|
||||
shouldDisplay: () => false,
|
||||
const fakeProps = (): StockPinBindingsButtonProps => ({
|
||||
dispatch: jest.fn(),
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("adds bindings", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<StockPinBindingsButton {...p} />);
|
||||
const wrapper = mount(<StockPinBindingsButton {...fakeProps()} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
stockPinBindings.map(body =>
|
||||
expect(initSave).toHaveBeenCalledWith("PinBinding", body));
|
||||
});
|
||||
|
||||
it("is hidden", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "arduino";
|
||||
const wrapper = mount(<StockPinBindingsButton {...p} />);
|
||||
expect(wrapper.find("button").props().hidden).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is not hidden", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "farmduino_k14";
|
||||
const wrapper = mount(<StockPinBindingsButton {...p} />);
|
||||
expect(wrapper.find("button").props().hidden).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,10 +3,12 @@ import {
|
|||
PinBindingType,
|
||||
PinBindingSpecialAction
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
|
||||
export interface PinBindingsContentProps {
|
||||
dispatch: Function;
|
||||
resources: ResourceIndex;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface PinBindingListItems {
|
||||
|
|
|
@ -68,12 +68,13 @@ const PinBindingsListHeader = () =>
|
|||
</Row>;
|
||||
|
||||
export const PinBindingsContent = (props: PinBindingsContentProps) => {
|
||||
const { dispatch, resources } = props;
|
||||
const { dispatch, resources, firmwareHardware } = props;
|
||||
const pinBindings = apiPinBindings(resources);
|
||||
|
||||
return <div className="pin-bindings">
|
||||
<Row>
|
||||
<StockPinBindingsButton dispatch={dispatch} />
|
||||
<StockPinBindingsButton
|
||||
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
||||
<Popover
|
||||
position={Position.RIGHT_TOP}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
|
|
|
@ -8,6 +8,8 @@ import { PinBindingListItems } from "./interfaces";
|
|||
import { stockPinBindings } from "./list_and_label_support";
|
||||
import { initSave } from "../../api/crud";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
import { hasButtons } from "../components/firmware_hardware_support";
|
||||
|
||||
/** Return the correct Pin Binding resource according to binding type. */
|
||||
export const pinBindingBody =
|
||||
|
@ -34,15 +36,21 @@ export const pinBindingBody =
|
|||
return body;
|
||||
};
|
||||
|
||||
export interface StockPinBindingsButtonProps {
|
||||
dispatch: Function;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
/** Add default pin bindings. */
|
||||
export const StockPinBindingsButton = ({ dispatch }: { dispatch: Function }) =>
|
||||
export const StockPinBindingsButton = (props: StockPinBindingsButtonProps) =>
|
||||
<div className="stock-pin-bindings-button">
|
||||
<button
|
||||
className="fb-button green"
|
||||
hidden={!hasButtons(props.firmwareHardware)}
|
||||
onClick={() => stockPinBindings.map(binding =>
|
||||
dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
|
||||
props.dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
|
||||
<i className="fa fa-plus" />
|
||||
{t("v1.4 Stock Bindings")}
|
||||
{t("Stock Bindings")}
|
||||
</button>
|
||||
</div>;
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { Plant } from "../plant";
|
||||
|
||||
describe("Plant()", () => {
|
||||
it("returns defaults", () => {
|
||||
expect(Plant({})).toEqual({
|
||||
created_at: "",
|
||||
id: undefined,
|
||||
meta: {},
|
||||
name: "Untitled Plant",
|
||||
openfarm_slug: "not-set",
|
||||
plant_stage: "planned",
|
||||
pointer_type: "Plant",
|
||||
radius: 25,
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -94,6 +94,19 @@ describe("designer reducer", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("uses current point color", () => {
|
||||
const action: ReduxAction<CurrentPointPayl> = {
|
||||
type: Actions.SET_CURRENT_POINT_DATA,
|
||||
payload: { cx: 10, cy: 20, r: 30 }
|
||||
};
|
||||
const state = oldState();
|
||||
state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" };
|
||||
const newState = designer(state, action);
|
||||
expect(newState.currentPoint).toEqual({
|
||||
cx: 10, cy: 20, r: 30, color: "red"
|
||||
});
|
||||
});
|
||||
|
||||
it("sets opened saved garden", () => {
|
||||
const payload = "savedGardenUuid";
|
||||
const action: ReduxAction<string | undefined> = {
|
||||
|
|
|
@ -90,7 +90,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
|||
<div className="thin-search-wrapper">
|
||||
<div className="text-input-wrapper">
|
||||
{!props.noIcon &&
|
||||
<i className="fa fa-search"></i>}
|
||||
<i className="fa fa-search" />}
|
||||
<ErrorBoundary>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -23,7 +23,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
|
|||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
panel={Panel.FarmEvents}
|
||||
title={t("Edit Event")} />
|
||||
title={t("Edit event")} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
<EditFEForm farmEvent={fe}
|
||||
deviceTimezone={this.props.deviceTimezone}
|
||||
|
@ -31,7 +31,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
|
|||
executableOptions={this.props.executableOptions}
|
||||
dispatch={this.props.dispatch}
|
||||
findExecutable={this.props.findExecutable}
|
||||
title={t("Edit Event")}
|
||||
title={t("Edit event")}
|
||||
deleteBtn={true}
|
||||
timeSettings={this.props.timeSettings}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
} from "../../sequences/locals_list/locals_list_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { ErrorBoundary } from "../../error_boundary";
|
||||
|
||||
export const NEVER: TimeUnit = "never";
|
||||
/** Separate each of the form fields into their own interface. Recombined later
|
||||
|
@ -360,19 +361,24 @@ export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
|
|||
render() {
|
||||
const { farmEvent } = this.props;
|
||||
return <div className="edit-farm-event-form">
|
||||
<FarmEventForm
|
||||
isRegimen={this.isReg}
|
||||
fieldGet={this.fieldGet}
|
||||
fieldSet={this.fieldSet}
|
||||
timeSettings={this.props.timeSettings}
|
||||
executableOptions={this.props.executableOptions}
|
||||
executableSet={this.executableSet}
|
||||
executableGet={this.executableGet}
|
||||
dispatch={this.props.dispatch}
|
||||
specialStatus={farmEvent.specialStatus || this.state.specialStatusLocal}
|
||||
onSave={() => this.commitViewModel()}>
|
||||
<this.LocalsList />
|
||||
</FarmEventForm>
|
||||
<ErrorBoundary>
|
||||
<FarmEventForm
|
||||
isRegimen={this.isReg}
|
||||
fieldGet={this.fieldGet}
|
||||
fieldSet={this.fieldSet}
|
||||
timeSettings={this.props.timeSettings}
|
||||
executableOptions={this.props.executableOptions}
|
||||
executableSet={this.executableSet}
|
||||
executableGet={this.executableGet}
|
||||
dispatch={this.props.dispatch}
|
||||
specialStatus={farmEvent.specialStatus
|
||||
|| this.state.specialStatusLocal}
|
||||
onSave={() => this.commitViewModel()}>
|
||||
<ErrorBoundary>
|
||||
<this.LocalsList />
|
||||
</ErrorBoundary>
|
||||
</FarmEventForm>
|
||||
</ErrorBoundary>
|
||||
<FarmEventDeleteButton
|
||||
hidden={!this.props.deleteBtn}
|
||||
farmEvent={this.props.farmEvent}
|
||||
|
|
|
@ -111,7 +111,7 @@ export class PureFarmEvents
|
|||
<input
|
||||
value={this.state.searchTerm}
|
||||
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
|
||||
placeholder={t("Search events...")} />
|
||||
placeholder={t("Search your events...")} />
|
||||
</DesignerPanelTop>
|
||||
<DesignerPanelContent panelName={"farm-event"}>
|
||||
<div className="farm-events">
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { MapImage, MapImageProps } from "../map_image";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
@ -7,6 +6,9 @@ import { trim } from "../../../../../util";
|
|||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||
|
||||
const NOT_DISPLAYED = "<svg><image></image></svg>";
|
||||
|
||||
describe("<MapImage />", () => {
|
||||
const fakeProps = (): MapImageProps => {
|
||||
|
@ -37,28 +39,28 @@ describe("<MapImage />", () => {
|
|||
};
|
||||
|
||||
it("doesn't render image", () => {
|
||||
const wrapper = mount(<MapImage {...fakeProps()} />);
|
||||
expect(wrapper.html()).toEqual("<image></image>");
|
||||
const wrapper = svgMount(<MapImage {...fakeProps()} />);
|
||||
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
|
||||
});
|
||||
|
||||
it("renders pre-calibration preview", () => {
|
||||
const p = fakeProps();
|
||||
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
|
||||
const wrapper = mount(<MapImage {...p} />);
|
||||
wrapper.setState({ width: 100, height: 100 });
|
||||
const wrapper = svgMount(<MapImage {...p} />);
|
||||
wrapper.find(MapImage).setState({ width: 100, height: 100 });
|
||||
expect(wrapper.html()).toContain("image_url");
|
||||
});
|
||||
|
||||
it("gets image size", () => {
|
||||
const p = fakeProps();
|
||||
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
|
||||
const wrapper = mount<MapImage>(<MapImage {...p} />);
|
||||
expect(wrapper.state()).toEqual({ width: 0, height: 0 });
|
||||
const wrapper = svgMount(<MapImage {...p} />);
|
||||
expect(wrapper.find(MapImage).state()).toEqual({ width: 0, height: 0 });
|
||||
const img = new Image();
|
||||
img.width = 100;
|
||||
img.height = 200;
|
||||
wrapper.instance().imageCallback(img)();
|
||||
expect(wrapper.state()).toEqual({ width: 100, height: 200 });
|
||||
wrapper.find<MapImage>(MapImage).instance().imageCallback(img)();
|
||||
expect(wrapper.find(MapImage).state()).toEqual({ width: 100, height: 200 });
|
||||
});
|
||||
|
||||
interface ExpectedData {
|
||||
|
@ -83,8 +85,8 @@ describe("<MapImage />", () => {
|
|||
expectedData: ExpectedData,
|
||||
extra?: ExtraTranslationData) => {
|
||||
it(`renders image: INPUT_SET_${num}`, () => {
|
||||
const wrapper = mount(<MapImage {...inputData[num]} />);
|
||||
wrapper.setState({ width: 480, height: 640 });
|
||||
const wrapper = svgMount(<MapImage {...inputData[num]} />);
|
||||
wrapper.find(MapImage).setState({ width: 480, height: 640 });
|
||||
expect(wrapper.find("image").props()).toEqual({
|
||||
xlinkHref: "image_url",
|
||||
x: 0,
|
||||
|
@ -183,21 +185,21 @@ describe("<MapImage />", () => {
|
|||
it("doesn't render placeholder image", () => {
|
||||
const p = INPUT_SET_1;
|
||||
p.image && (p.image.body.attachment_url = "/placehold.");
|
||||
const wrapper = mount(<MapImage {...p} />);
|
||||
expect(wrapper.html()).toEqual("<image></image>");
|
||||
const wrapper = svgMount(<MapImage {...p} />);
|
||||
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
|
||||
});
|
||||
|
||||
it("doesn't render image taken at different height than calibration", () => {
|
||||
const p = INPUT_SET_1;
|
||||
p.image && (p.image.body.meta.z = 100);
|
||||
const wrapper = mount(<MapImage {...p} />);
|
||||
expect(wrapper.html()).toEqual("<image></image>");
|
||||
const wrapper = svgMount(<MapImage {...p} />);
|
||||
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
|
||||
});
|
||||
|
||||
it("doesn't render images that are not adjusted for camera rotation", () => {
|
||||
const p = INPUT_SET_1;
|
||||
p.image && (p.image.body.meta.name = "na");
|
||||
const wrapper = mount(<MapImage {...p} />);
|
||||
expect(wrapper.html()).toEqual("<image></image>");
|
||||
const wrapper = svgMount(<MapImage {...p} />);
|
||||
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,11 +93,15 @@ interface NavTabProps {
|
|||
linkTo: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
desktopHide?: boolean;
|
||||
}
|
||||
|
||||
const NavTab = (props: NavTabProps) =>
|
||||
<Link to={props.linkTo} style={{ flex: 0.3 }}
|
||||
className={getCurrentTab() === props.panel ? "active" : ""}>
|
||||
className={[
|
||||
getCurrentTab() === props.panel ? "active" : "",
|
||||
props.desktopHide ? "desktop-hide" : "",
|
||||
].join(" ")}>
|
||||
<img {...common}
|
||||
src={TAB_ICON[props.panel]} title={props.title} />
|
||||
</Link>;
|
||||
|
@ -109,7 +113,7 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
|
|||
<div className="panel-tabs">
|
||||
<NavTab panel={Panel.Map}
|
||||
linkTo={"/app/designer"}
|
||||
title={t("Map")} />
|
||||
title={t("Map")} desktopHide={true} />
|
||||
<NavTab
|
||||
panel={Panel.Plants}
|
||||
linkTo={"/app/designer/plants"}
|
||||
|
|
|
@ -87,7 +87,7 @@ export class GroupDetailActive
|
|||
onBack={this.saveGroup}
|
||||
panelName={Panel.Groups}
|
||||
panel={Panel.Groups}
|
||||
title={t("Edit Group")}
|
||||
title={t("Edit group")}
|
||||
backTo={"/app/designer/groups"} />
|
||||
<DesignerPanelContent
|
||||
panelName={"groups"}>
|
||||
|
|
|
@ -6,6 +6,7 @@ jest.mock("../../../farmware/weed_detector/actions", () => ({
|
|||
|
||||
let mockPath = "/app/designer/points/add";
|
||||
jest.mock("../../../history", () => ({
|
||||
history: { push: jest.fn() },
|
||||
push: jest.fn(),
|
||||
getPathArray: () => mockPath.split("/"),
|
||||
}));
|
||||
|
|
|
@ -23,8 +23,9 @@ import {
|
|||
import { parseIntInput } from "../../util";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Panel } from "../panel_header";
|
||||
import { getPathArray } from "../../history";
|
||||
import { history, getPathArray } from "../../history";
|
||||
import { ListItem } from "../plants/plant_panel";
|
||||
import { success } from "../../toast/toast";
|
||||
|
||||
export function mapStateToProps(props: Everything): CreatePointsProps {
|
||||
const { position } = props.bot.hardware.location_data;
|
||||
|
@ -176,9 +177,13 @@ export class RawCreatePoints
|
|||
radius: this.attr("r"),
|
||||
};
|
||||
this.props.dispatch(initSave("Point", body));
|
||||
success(this.panel == "weeds"
|
||||
? t("Weed created.")
|
||||
: t("Point created."));
|
||||
this.cancel();
|
||||
this.loadDefaultPoint();
|
||||
history.push(`/app/designer/${this.panel}`);
|
||||
}
|
||||
|
||||
PointProperties = () =>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
|
@ -47,7 +47,7 @@ export class PointInventoryItem extends
|
|||
{label}
|
||||
</span>
|
||||
<p className="point-search-item-info">
|
||||
{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}
|
||||
<i>{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}</i>
|
||||
</p>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ export class RawEditGarden extends React.Component<EditGardenProps, {}> {
|
|||
<DesignerPanelHeader
|
||||
panelName={"saved-garden"}
|
||||
panel={Panel.SavedGardens}
|
||||
title={t("Edit Garden")}
|
||||
title={t("Edit garden")}
|
||||
backTo={"/app/designer/gardens"} />
|
||||
<DesignerPanelContent panelName={"saved-garden-edit"}>
|
||||
{savedGarden
|
||||
|
|
|
@ -14,7 +14,7 @@ export const GardenInfo = (props: SavedGardenInfoProps) => {
|
|||
onClick={() => dispatch(openSavedGarden(savedGarden.uuid))}>
|
||||
<Col>
|
||||
<span>{savedGarden.body.name}</span>
|
||||
<p>{props.plantTemplateCount} {t("plants")}</p>
|
||||
<p><i>{props.plantTemplateCount} {t("plants")}</i></p>
|
||||
</Col>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -193,7 +193,7 @@ describe("<Tools />", () => {
|
|||
p.isActive = () => true;
|
||||
p.device.body.mounted_tool_id = undefined;
|
||||
const wrapper = mount(<Tools {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("active");
|
||||
expect(wrapper.text().toLowerCase()).toContain("in slot");
|
||||
});
|
||||
|
||||
it("displays tool as mounted", () => {
|
||||
|
|
|
@ -288,7 +288,7 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
|||
</Col>
|
||||
<Col xs={4} className={"tool-slot-position-info"}>
|
||||
<p className="tool-slot-position">
|
||||
{botPositionLabel({ x, y, z }, gantry_mounted)}
|
||||
<i>{botPositionLabel({ x, y, z }, gantry_mounted)}</i>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -303,7 +303,7 @@ interface ToolInventoryItemProps {
|
|||
}
|
||||
|
||||
const ToolInventoryItem = (props: ToolInventoryItemProps) => {
|
||||
const activeText = props.active ? t("active") : t("inactive");
|
||||
const activeText = props.active ? t("in slot") : t("inactive");
|
||||
return <div className={"tool-search-item"}
|
||||
onClick={() => history.push(`/app/designer/tools/${props.toolId}`)}>
|
||||
<Row>
|
||||
|
@ -315,7 +315,7 @@ const ToolInventoryItem = (props: ToolInventoryItemProps) => {
|
|||
</Col>
|
||||
<Col xs={3}>
|
||||
<p className="tool-status">
|
||||
{props.mounted ? t("mounted") : activeText}
|
||||
<i>{props.mounted ? t("mounted") : activeText}</i>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -294,14 +294,14 @@ describe("<FolderListItem />", () => {
|
|||
it("starts sequence move", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<FolderListItem {...p} />);
|
||||
wrapper.find(".fa-bars").simulate("mouseDown");
|
||||
wrapper.find(".fa-arrows-v").simulate("mouseDown");
|
||||
expect(p.startSequenceMove).toHaveBeenCalledWith(p.sequence.uuid);
|
||||
});
|
||||
|
||||
it("toggles sequence move", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<FolderListItem {...p} />);
|
||||
wrapper.find(".fa-bars").simulate("mouseUp");
|
||||
wrapper.find(".fa-arrows-v").simulate("mouseUp");
|
||||
expect(p.toggleSequenceMove).toHaveBeenCalledWith(p.sequence.uuid);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -75,7 +75,7 @@ export const FolderListItem = (props: FolderItemProps) => {
|
|||
<div className="sequence-list-item-icons">
|
||||
{props.inUse &&
|
||||
<i className="in-use fa fa-hdd-o" title={t(Content.IN_USE)} />}
|
||||
<i className="fa fa-bars"
|
||||
<i className="fa fa-arrows-v"
|
||||
onMouseDown={() => props.startSequenceMove(sequence.uuid)}
|
||||
onMouseUp={() => props.toggleSequenceMove(sequence.uuid)} />
|
||||
</div>
|
||||
|
@ -328,7 +328,7 @@ export const FolderPanelTop = (props: FolderPanelTopProps) =>
|
|||
value={props.searchTerm || ""}
|
||||
onChange={e => updateSearchTerm(e.currentTarget.value)}
|
||||
type="text"
|
||||
placeholder={t("Search sequences")} />
|
||||
placeholder={t("Search sequences...")} />
|
||||
</div>
|
||||
</div>
|
||||
<ToggleFolderBtn
|
||||
|
|
|
@ -40,13 +40,14 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
|
|||
noFolder: (localMetaAttributes[PARENTLESS] || {}).sequences || []
|
||||
};
|
||||
const index = folders.map(setDefaultParentId).reduce(addToIndex, emptyIndex);
|
||||
const childrenOf = (i: number) => sortBy(index[i] || [], (x) => x.name.toLowerCase());
|
||||
const childrenOf = (i: number) =>
|
||||
sortBy(index[i] || [], (x) => x.name.toLowerCase());
|
||||
|
||||
const terminal = (x: FolderNode): FolderNodeTerminal => ({
|
||||
...x,
|
||||
kind: "terminal",
|
||||
content: (localMetaAttributes[x.id] || {}).sequences || [],
|
||||
open: true,
|
||||
open: false,
|
||||
editing: false,
|
||||
// children: [],
|
||||
...(localMetaAttributes[x.id] || {})
|
||||
|
@ -55,7 +56,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
|
|||
const medial = (x: FolderNode): FolderNodeMedial => ({
|
||||
...x,
|
||||
kind: "medial",
|
||||
open: true,
|
||||
open: false,
|
||||
editing: false,
|
||||
children: childrenOf(x.id).map(terminal),
|
||||
content: (localMetaAttributes[x.id] || {}).sequences || [],
|
||||
|
@ -67,7 +68,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
|
|||
return output.folders.push({
|
||||
...root,
|
||||
kind: "initial",
|
||||
open: true,
|
||||
open: false,
|
||||
editing: false,
|
||||
children,
|
||||
content: (localMetaAttributes[root.id] || {}).sequences || [],
|
||||
|
|
|
@ -7,7 +7,7 @@ import { selectAllTools } from "../resources/selectors";
|
|||
import { store } from "../redux/store";
|
||||
import { getFbosConfig } from "../resources/getters";
|
||||
import {
|
||||
isExpressBoard, getFwHardwareValue
|
||||
getFwHardwareValue, hasUTM
|
||||
} from "../devices/components/firmware_hardware_support";
|
||||
|
||||
export enum Tours {
|
||||
|
@ -25,26 +25,26 @@ export const tourNames = () => [
|
|||
const hasTools = () =>
|
||||
selectAllTools(store.getState().resources.index).length > 0;
|
||||
|
||||
const isExpress = () =>
|
||||
isExpressBoard(getFwHardwareValue(
|
||||
const noUTM = () =>
|
||||
!hasUTM(getFwHardwareValue(
|
||||
getFbosConfig(store.getState().resources.index)));
|
||||
|
||||
const toolsStep = () => hasTools()
|
||||
? [{
|
||||
target: ".tools",
|
||||
content: isExpress()
|
||||
content: noUTM()
|
||||
? t(TourContent.ADD_SEED_CONTAINERS)
|
||||
: t(TourContent.ADD_TOOLS),
|
||||
title: isExpress()
|
||||
title: noUTM()
|
||||
? t("Add seed containers")
|
||||
: t("Add tools and seed containers"),
|
||||
}]
|
||||
: [{
|
||||
target: ".tools",
|
||||
content: isExpress()
|
||||
content: noUTM()
|
||||
? t(TourContent.ADD_SEED_CONTAINERS_AND_SLOTS)
|
||||
: t(TourContent.ADD_TOOLS_AND_SLOTS),
|
||||
title: isExpress()
|
||||
title: noUTM()
|
||||
? t("Add seed containers and slots")
|
||||
: t("Add tools and slots"),
|
||||
}];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const mockStorj: Dictionary<number | boolean> = {};
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { RawLogs as Logs } from "../index";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { TaggedLog, Dictionary } from "farmbot";
|
||||
|
@ -172,4 +172,12 @@ describe("<Logs />", () => {
|
|||
wrapper.setState({ markdown: false });
|
||||
expect(wrapper.html()).not.toContain("<code>message</code>");
|
||||
});
|
||||
|
||||
it("changes search term", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<Logs>(<Logs {...p} />);
|
||||
wrapper.find("input").first().simulate("change",
|
||||
{ currentTarget: { value: "one" } });
|
||||
expect(wrapper.state().searchTerm).toEqual("one");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,8 @@ const logTypes = MESSAGE_TYPES;
|
|||
|
||||
describe("<LogsFilterMenu />", () => {
|
||||
const fakeState: LogsState = {
|
||||
autoscroll: true, markdown: false, success: 1, busy: 1, warn: 1,
|
||||
autoscroll: true, markdown: false, searchTerm: "",
|
||||
success: 1, busy: 1, warn: 1,
|
||||
error: 1, info: 1, fun: 1, debug: 1, assertion: 1,
|
||||
};
|
||||
|
||||
|
@ -24,7 +25,7 @@ describe("<LogsFilterMenu />", () => {
|
|||
const wrapper = mount(<LogsFilterMenu {...fakeProps()} />);
|
||||
logTypes.filter(x => x !== "assertion").map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
|
||||
["autoscroll", "markdown"].map(string =>
|
||||
["autoscroll", "markdown", "searchTerm"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).not.toContain(string));
|
||||
});
|
||||
|
||||
|
@ -34,7 +35,7 @@ describe("<LogsFilterMenu />", () => {
|
|||
const wrapper = mount(<LogsFilterMenu {...p} />);
|
||||
logTypes.map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
|
||||
["autoscroll", "markdown"].map(string =>
|
||||
["autoscroll", "markdown", "searchTerm"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).not.toContain(string));
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { bySearchTerm } from "../logs_table";
|
||||
import { fakeLog } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("bySearchTerm()", () => {
|
||||
it("includes log", () => {
|
||||
const log = fakeLog();
|
||||
log.body.message = "include this log";
|
||||
const result = bySearchTerm("include", fakeTimeSettings())(log);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("excludes log", () => {
|
||||
const log = fakeLog();
|
||||
log.body.created_at = undefined;
|
||||
log.body.message = "exclude this log";
|
||||
const result = bySearchTerm("include", fakeTimeSettings())(log);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -28,7 +28,7 @@ const menuSort = (a: string, b: string) =>
|
|||
export const filterStateKeys =
|
||||
(state: LogsState, shouldDisplay: ShouldDisplay) =>
|
||||
Object.keys(state)
|
||||
.filter(key => !["autoscroll", "markdown"].includes(key))
|
||||
.filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key))
|
||||
.filter(key => shouldDisplay(Feature.assertion_block)
|
||||
|| key !== "assertion");
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TaggedLog, ALLOWED_MESSAGE_TYPES } from "farmbot";
|
|||
import { LogsState, LogsTableProps, Filters } from "../interfaces";
|
||||
import { formatLogTime } from "../index";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import { isNumber, startCase } from "lodash";
|
||||
import { isNumber, startCase, some } from "lodash";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
|
@ -81,6 +81,7 @@ export const LogsTable = (props: LogsTableProps) => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{filterByVerbosity(getFilterLevel(props.state), props.logs)
|
||||
.filter(bySearchTerm(props.state.searchTerm, props.timeSettings))
|
||||
.map((log: TaggedLog) =>
|
||||
<LogsRow
|
||||
key={log.uuid}
|
||||
|
@ -114,3 +115,18 @@ export const filterByVerbosity =
|
|||
return displayLog;
|
||||
});
|
||||
};
|
||||
|
||||
export const bySearchTerm =
|
||||
(searchTerm: string, timeSettings: TimeSettings) =>
|
||||
(log: TaggedLog) => {
|
||||
const { x, y, z, created_at, message, type } = log.body;
|
||||
const displayedTime = formatLogTime(created_at || NaN, timeSettings);
|
||||
const displayedPosition = xyzTableEntry(x, y, z);
|
||||
const lowerSearchTerm = searchTerm.toLowerCase();
|
||||
return some([message, type]
|
||||
.map(string => string.toLowerCase().includes(lowerSearchTerm))
|
||||
.concat([
|
||||
displayedTime.toLowerCase().includes(lowerSearchTerm),
|
||||
displayedPosition.includes(lowerSearchTerm),
|
||||
]));
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
|
|||
fun: this.initialize(NumericSetting.fun_log, 1),
|
||||
debug: this.initialize(NumericSetting.debug_log, 1),
|
||||
assertion: this.initialize(NumericSetting.assertion_log, 1),
|
||||
searchTerm: "",
|
||||
markdown: true,
|
||||
};
|
||||
|
||||
|
@ -85,13 +86,13 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
|
|||
const filterBtnColor = this.filterActive ? "green" : "gray";
|
||||
return <Page className="logs-page">
|
||||
<Row>
|
||||
<Col xs={7}>
|
||||
<Col xs={6}>
|
||||
<h3>
|
||||
<i>{t("Logs")}</i>
|
||||
</h3>
|
||||
<ToolTip helpText={ToolTips.LOGS} />
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
<Col xs={6}>
|
||||
<div className={"settings-menu-button"}>
|
||||
<Popover position={Position.TOP_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
|
@ -121,6 +122,19 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
|
|||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12} md={5} lg={4}>
|
||||
<div className="thin-search-wrapper">
|
||||
<div className="text-input-wrapper">
|
||||
<i className="fa fa-search" />
|
||||
<input
|
||||
onChange={e =>
|
||||
this.setState({ searchTerm: e.currentTarget.value })}
|
||||
placeholder={t("Search logs...")} />
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<LogsTable logs={this.props.logs}
|
||||
dispatch={this.props.dispatch}
|
||||
|
|
|
@ -16,6 +16,7 @@ export type Filters = Record<ALLOWED_MESSAGE_TYPES, number>;
|
|||
|
||||
export interface LogsState extends Filters {
|
||||
autoscroll: boolean;
|
||||
searchTerm: string;
|
||||
markdown: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,23 +9,23 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
|
|||
return <div className="nav-additional-menu">
|
||||
<div>
|
||||
<Link to="/app/account" onClick={props.close("accountMenuOpen")}>
|
||||
<i className="fa fa-cog"></i>
|
||||
<i className="fa fa-cog" />
|
||||
{t("Account Settings")}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link to="/app/logs" onClick={props.close("accountMenuOpen")}>
|
||||
<i className="fa fa-list"></i>
|
||||
<i className="fa fa-list" />
|
||||
{t("Logs")}
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/app/help" onClick={props.close("accountMenuOpen")}>
|
||||
<i className="fa fa-question-circle"></i>
|
||||
<i className="fa fa-question-circle" />
|
||||
{t("Help")}
|
||||
</Link>
|
||||
<div>
|
||||
<a onClick={props.logout}>
|
||||
<i className="fa fa-sign-out"></i>
|
||||
<i className="fa fa-sign-out" />
|
||||
{t("Logout")}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { store } from "../redux/store";
|
|||
import { warning } from "../toast/toast";
|
||||
import React from "react";
|
||||
import { appIsReadonly } from "./app_is_read_only";
|
||||
import { t } from "../i18next_wrapper";
|
||||
|
||||
export const readOnlyInterceptor = (config: AxiosRequestConfig) => {
|
||||
const method = (config.method || "get").toLowerCase();
|
||||
|
@ -10,7 +11,7 @@ export const readOnlyInterceptor = (config: AxiosRequestConfig) => {
|
|||
|
||||
if (relevant && appIsReadonly(store.getState().resources.index)) {
|
||||
if (!(config.url || "").includes("web_app_config")) {
|
||||
warning("Refusing to modify data in read-only mode");
|
||||
warning(t("Refusing to modify data in read-only mode"));
|
||||
return Promise.reject(config);
|
||||
}
|
||||
}
|
||||
|
@ -18,19 +19,12 @@ export const readOnlyInterceptor = (config: AxiosRequestConfig) => {
|
|||
return Promise.resolve(config);
|
||||
};
|
||||
|
||||
const MOVE_ME_ELSEWHERE: React.CSSProperties = {
|
||||
float: "right",
|
||||
boxSizing: "inherit",
|
||||
margin: "9px 0px 0px 9px"
|
||||
};
|
||||
|
||||
export const ReadOnlyIcon = (p: { locked: boolean }) => {
|
||||
if (p.locked) {
|
||||
return <div className="fa-stack fa-lg" style={MOVE_ME_ELSEWHERE}>
|
||||
<i className="fa fa-pencil fa-stack-1x"></i>
|
||||
<i className="fa fa-ban fa-stack-2x fa-rotate-90 text-danger"></i>
|
||||
return <div className=" read-only-icon fa-stack fa-lg">
|
||||
<i className="fa fa-pencil fa-stack-1x" />
|
||||
<i className="fa fa-ban fa-stack-2x fa-rotate-90 text-danger" />
|
||||
</div>;
|
||||
|
||||
} else {
|
||||
return <div />;
|
||||
}
|
||||
|
|
|
@ -42,8 +42,7 @@ export class RawRegimens extends React.Component<Props, {}> {
|
|||
<Row>
|
||||
<LeftPanel
|
||||
className={`regimen-list-panel ${activeClasses}`}
|
||||
title={t("Regimens")}
|
||||
helpText={t(ToolTips.REGIMEN_LIST)}>
|
||||
title={t("Regimens")}>
|
||||
<RegimensList
|
||||
usageStats={this.props.regimenUsageStats}
|
||||
dispatch={this.props.dispatch}
|
||||
|
|
|
@ -18,10 +18,10 @@ const RegimenListHeader = (props: RegimenListHeaderProps) =>
|
|||
<div className={"panel-top with-button"}>
|
||||
<div className="thin-search-wrapper">
|
||||
<div className="text-input-wrapper">
|
||||
<i className="fa fa-search"></i>
|
||||
<i className="fa fa-search" />
|
||||
<input
|
||||
onChange={props.onChange}
|
||||
placeholder={t("Search Regimens...")} />
|
||||
placeholder={t("Search regimens...")} />
|
||||
</div>
|
||||
</div>
|
||||
<AddRegimen dispatch={props.dispatch} length={props.regimenCount} />
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
isTaggedGenericPointer,
|
||||
isTaggedSavedGarden,
|
||||
isTaggedFolder,
|
||||
isTaggedPoint,
|
||||
isTaggedPointGroup,
|
||||
} from "./tagged_resources";
|
||||
import {
|
||||
ResourceName,
|
||||
|
@ -127,20 +125,6 @@ export function maybeFindGenericPointerById(index: ResourceIndex, id: number) {
|
|||
if (resource && isTaggedGenericPointer(resource)) { return resource; }
|
||||
}
|
||||
|
||||
/** Unlike other findById methods, this one allows undefined (missed) values */
|
||||
export function maybeFindPointById(index: ResourceIndex, id: number) {
|
||||
const uuid = index.byKindAndId[joinKindAndId("Point", id)];
|
||||
const resource = index.references[uuid || "nope"];
|
||||
if (resource && isTaggedPoint(resource)) { return resource; }
|
||||
}
|
||||
|
||||
/** Unlike other findById methods, this one allows undefined (missed) values */
|
||||
export function maybeFindGroupById(index: ResourceIndex, id: number) {
|
||||
const uuid = index.byKindAndId[joinKindAndId("PointGroup", id)];
|
||||
const resource = index.references[uuid || "nope"];
|
||||
if (resource && isTaggedPointGroup(resource)) { return resource; }
|
||||
}
|
||||
|
||||
/** Unlike other findById methods, this one allows undefined (missed) values */
|
||||
export function maybeFindSavedGardenById(index: ResourceIndex, id: number) {
|
||||
const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)];
|
||||
|
|
|
@ -100,7 +100,7 @@ export function InnerIf(props: IfParams) {
|
|||
confirmStepDeletion={confirmStepDeletion}>
|
||||
{recursive &&
|
||||
<span>
|
||||
<i className="fa fa-exclamation-triangle"></i>
|
||||
<i className="fa fa-exclamation-triangle" />
|
||||
{t("Recursive condition.")}
|
||||
</span>
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@ export function BackArrow(props: BackArrowProps) {
|
|||
};
|
||||
|
||||
return <a onClick={onClick} className="back-arrow">
|
||||
<i className="fa fa-arrow-left"></i>
|
||||
<i className="fa fa-arrow-left" />
|
||||
</a>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue