model and version updates
parent
9dab0c4bc5
commit
cf0af59e42
|
@ -27,7 +27,7 @@ module Devices
|
|||
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
|
||||
x: 0,
|
||||
y: 25,
|
||||
z: -200,
|
||||
z: 0,
|
||||
tool: tools_seed_trough_1,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
|
@ -37,25 +37,18 @@ module Devices
|
|||
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
|
||||
x: 0,
|
||||
y: 50,
|
||||
z: -200,
|
||||
z: 0,
|
||||
tool: tools_seed_trough_2,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
end
|
||||
|
||||
def tool_slots_slot_3
|
||||
add_tool_slot(name: ToolNames::SEED_TROUGH_3,
|
||||
x: 0,
|
||||
y: 75,
|
||||
z: -200,
|
||||
tool: tools_seed_trough_3,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
end
|
||||
|
||||
def tool_slots_slot_3; end
|
||||
def tool_slots_slot_4; end
|
||||
def tool_slots_slot_5; end
|
||||
def tool_slots_slot_6; end
|
||||
def tool_slots_slot_7; end
|
||||
def tool_slots_slot_8; end
|
||||
def tools_seed_bin; end
|
||||
def tools_seed_tray; end
|
||||
|
||||
|
@ -69,11 +62,6 @@ module Devices
|
|||
add_tool(ToolNames::SEED_TROUGH_2)
|
||||
end
|
||||
|
||||
def tools_seed_trough_3
|
||||
@tools_seed_trough_3 ||=
|
||||
add_tool(ToolNames::SEED_TROUGH_3)
|
||||
end
|
||||
|
||||
def tools_seeder; end
|
||||
def tools_soil_sensor; end
|
||||
def tools_watering_nozzle; end
|
||||
|
|
|
@ -75,6 +75,9 @@ module Devices
|
|||
tool: tools_weeder)
|
||||
end
|
||||
|
||||
def tool_slots_slot_7; end
|
||||
def tool_slots_slot_8; end
|
||||
|
||||
def tools_seed_bin
|
||||
@tools_seed_bin ||=
|
||||
add_tool(ToolNames::SEED_BIN)
|
||||
|
@ -87,7 +90,6 @@ module Devices
|
|||
|
||||
def tools_seed_trough_1; end
|
||||
def tools_seed_trough_2; end
|
||||
def tools_seed_trough_3; end
|
||||
|
||||
def tools_seeder
|
||||
@tools_seeder ||=
|
||||
|
|
|
@ -37,7 +37,6 @@ module Devices
|
|||
:tools_seed_tray,
|
||||
:tools_seed_trough_1,
|
||||
:tools_seed_trough_2,
|
||||
:tools_seed_trough_3,
|
||||
:tools_seeder,
|
||||
:tools_soil_sensor,
|
||||
:tools_watering_nozzle,
|
||||
|
@ -50,6 +49,8 @@ module Devices
|
|||
:tool_slots_slot_4,
|
||||
:tool_slots_slot_5,
|
||||
:tool_slots_slot_6,
|
||||
:tool_slots_slot_7,
|
||||
:tool_slots_slot_8,
|
||||
|
||||
# WEBCAM FEEDS ===========================
|
||||
:webcam_feeds,
|
||||
|
@ -152,11 +153,12 @@ module Devices
|
|||
def tool_slots_slot_4; end
|
||||
def tool_slots_slot_5; end
|
||||
def tool_slots_slot_6; end
|
||||
def tool_slots_slot_7; end
|
||||
def tool_slots_slot_8; end
|
||||
def tools_seed_bin; end
|
||||
def tools_seed_tray; end
|
||||
def tools_seed_trough_1; end
|
||||
def tools_seed_trough_2; end
|
||||
def tools_seed_trough_3; end
|
||||
def tools_seeder; end
|
||||
def tools_soil_sensor; end
|
||||
def tools_watering_nozzle; end
|
||||
|
|
|
@ -31,7 +31,6 @@ module Devices
|
|||
LIGHTING = "Lighting"
|
||||
SEED_TROUGH_1 = "Seed Trough 1"
|
||||
SEED_TROUGH_2 = "Seed Trough 2"
|
||||
SEED_TROUGH_3 = "Seed Trough 3"
|
||||
end
|
||||
|
||||
# Stub plants ==============================
|
||||
|
|
|
@ -6,6 +6,36 @@ module Devices
|
|||
.fbos_config
|
||||
.update!(firmware_hardware: FbosConfig::FARMDUINO_K15)
|
||||
end
|
||||
|
||||
def tool_slots_slot_7
|
||||
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
|
||||
x: 0,
|
||||
y: 25,
|
||||
z: 0,
|
||||
tool: tools_seed_trough_1,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
end
|
||||
|
||||
def tool_slots_slot_8
|
||||
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
|
||||
x: 0,
|
||||
y: 50,
|
||||
z: 0,
|
||||
tool: tools_seed_trough_2,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
end
|
||||
|
||||
def tools_seed_trough_1
|
||||
@tools_seed_trough_1 ||=
|
||||
add_tool(ToolNames::SEED_TROUGH_1)
|
||||
end
|
||||
|
||||
def tools_seed_trough_2
|
||||
@tools_seed_trough_2 ||=
|
||||
add_tool(ToolNames::SEED_TROUGH_2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,36 @@ module Devices
|
|||
def settings_default_map_size_y
|
||||
device.web_app_config.update!(map_size_y: 2_900)
|
||||
end
|
||||
|
||||
def tool_slots_slot_7
|
||||
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
|
||||
x: 0,
|
||||
y: 25,
|
||||
z: 0,
|
||||
tool: tools_seed_trough_1,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
end
|
||||
|
||||
def tool_slots_slot_8
|
||||
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
|
||||
x: 0,
|
||||
y: 50,
|
||||
z: 0,
|
||||
tool: tools_seed_trough_2,
|
||||
pullout_direction: ToolSlot::NONE,
|
||||
gantry_mounted: true)
|
||||
end
|
||||
|
||||
def tools_seed_trough_1
|
||||
@tools_seed_trough_1 ||=
|
||||
add_tool(ToolNames::SEED_TROUGH_1)
|
||||
end
|
||||
|
||||
def tools_seed_trough_2
|
||||
@tools_seed_trough_2 ||=
|
||||
add_tool(ToolNames::SEED_TROUGH_2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,11 +28,12 @@ module Devices
|
|||
def tool_slots_slot_4; end
|
||||
def tool_slots_slot_5; end
|
||||
def tool_slots_slot_6; end
|
||||
def tool_slots_slot_7; end
|
||||
def tool_slots_slot_8; end
|
||||
def tools_seed_bin; end
|
||||
def tools_seed_tray; end
|
||||
def tools_seed_trough_1; end
|
||||
def tools_seed_trough_2; end
|
||||
def tools_seed_trough_3; end
|
||||
def tools_seeder; end
|
||||
def tools_soil_sensor; end
|
||||
def tools_watering_nozzle; end
|
||||
|
|
|
@ -157,7 +157,6 @@ describe("mapStateToProps()", () => {
|
|||
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" };
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
jest.mock("../../slow_down", () => {
|
||||
return {
|
||||
slowDown: jest.fn((fn: Function) => fn),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../../devices/actions", () => ({
|
||||
badVersion: jest.fn(),
|
||||
EXPECTED_MAJOR: 1,
|
||||
EXPECTED_MINOR: 0,
|
||||
jest.mock("../../slow_down", () => ({
|
||||
slowDown: jest.fn((fn: Function) => fn)
|
||||
}));
|
||||
|
||||
jest.mock("../../../devices/actions", () => ({ badVersion: jest.fn() }));
|
||||
|
||||
import {
|
||||
onStatus,
|
||||
incomingStatus,
|
||||
|
@ -49,8 +43,10 @@ describe("onStatus()", () => {
|
|||
});
|
||||
|
||||
it("version ok", () => {
|
||||
globalConfig.MINIMUM_FBOS_VERSION = "1.0.0";
|
||||
callOnStatus("1.0.0");
|
||||
expect(badVersion).not.toHaveBeenCalled();
|
||||
delete globalConfig.MINIMUM_FBOS_VERSION;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,13 +8,7 @@ import { success, error, info, warning, fun, busy } from "../toast/toast";
|
|||
import { HardwareState } from "../devices/interfaces";
|
||||
import { GetState, ReduxAction } from "../redux/interfaces";
|
||||
import { Content, Actions } from "../constants";
|
||||
import {
|
||||
EXPECTED_MAJOR,
|
||||
EXPECTED_MINOR,
|
||||
commandOK,
|
||||
badVersion,
|
||||
commandErr
|
||||
} from "../devices/actions";
|
||||
import { commandOK, badVersion, commandErr } from "../devices/actions";
|
||||
import { init } from "../api/crud";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
import { autoSync } from "./auto_sync";
|
||||
|
@ -123,7 +117,7 @@ const setBothUp = () => bothUp();
|
|||
const legacyChecks = (getState: GetState) => {
|
||||
const { controller_version } = getState().bot.hardware.informational_settings;
|
||||
if (HACKY_FLAGS.needVersionCheck && controller_version) {
|
||||
const IS_OK = versionOK(controller_version, EXPECTED_MAJOR, EXPECTED_MINOR);
|
||||
const IS_OK = versionOK(controller_version);
|
||||
if (!IS_OK) { badVersion(); }
|
||||
HACKY_FLAGS.needVersionCheck = false;
|
||||
}
|
||||
|
|
|
@ -580,6 +580,7 @@
|
|||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
.tools-header,
|
||||
.tool-slots-header {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
|
@ -624,6 +625,12 @@
|
|||
}
|
||||
}
|
||||
.add-stock-tools {
|
||||
.filter-search {
|
||||
margin-bottom: 1rem;
|
||||
button {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
font-size: 1.2rem;
|
||||
padding-left: 1rem;
|
||||
|
|
|
@ -1322,6 +1322,12 @@ ul {
|
|||
}
|
||||
}
|
||||
|
||||
.boolean-camera-calibration-config {
|
||||
input[type=checkbox] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.tour-list {
|
||||
margin: auto;
|
||||
max-width: 300px;
|
||||
|
@ -1533,16 +1539,24 @@ textarea:focus {
|
|||
box-shadow: 0 0 10px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.sort-path-info-bar {
|
||||
background: lightgray;
|
||||
.sort-option-bar {
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
white-space: nowrap;
|
||||
line-height: 1.75rem;
|
||||
&:hover {
|
||||
background: darken(lightgray, 10%);
|
||||
border: 2px solid $panel_light_blue;
|
||||
&:hover, &.selected {
|
||||
border: 2px solid $medium_gray;
|
||||
border-radius: 2px;
|
||||
.sort-path-info-bar {
|
||||
background: darken($light_gray, 10%);
|
||||
}
|
||||
}
|
||||
.sort-path-info-bar {
|
||||
background: $light_gray;
|
||||
font-size: 1.2rem;
|
||||
padding-left: 0.5rem;
|
||||
white-space: nowrap;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.camera-calibration,
|
||||
.weed-detector{
|
||||
.farmware-button{
|
||||
position: relative;
|
||||
|
|
|
@ -35,7 +35,6 @@ describe("mapStateToProps()", () => {
|
|||
it("uses the API as the source of FBOS settings", () => {
|
||||
const fakeApiConfig = fakeFbosConfig();
|
||||
fakeApiConfig.body.auto_sync = true;
|
||||
fakeApiConfig.body.api_migrated = true;
|
||||
mockFbosConfig = fakeApiConfig;
|
||||
const props = mapStateToProps(fakeState());
|
||||
expect(props.sourceFbosConfig("auto_sync")).toEqual({
|
||||
|
@ -53,19 +52,6 @@ describe("mapStateToProps()", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("uses the bot as the source of FBOS settings: ignore API defaults", () => {
|
||||
const state = fakeState();
|
||||
state.bot.hardware.configuration.auto_sync = false;
|
||||
const fakeApiConfig = fakeFbosConfig();
|
||||
fakeApiConfig.body.auto_sync = true;
|
||||
fakeApiConfig.body.api_migrated = false;
|
||||
mockFbosConfig = fakeApiConfig;
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.sourceFbosConfig("auto_sync")).toEqual({
|
||||
value: false, consistent: true
|
||||
});
|
||||
});
|
||||
|
||||
it("returns API Farmware env vars", () => {
|
||||
const state = fakeState();
|
||||
state.bot.hardware.user_env = {};
|
||||
|
|
|
@ -26,8 +26,6 @@ import { t } from "../i18next_wrapper";
|
|||
|
||||
const ON = 1, OFF = 0;
|
||||
export type ConfigKey = keyof McuParams;
|
||||
export const EXPECTED_MAJOR = 6;
|
||||
export const EXPECTED_MINOR = 0;
|
||||
export const FEATURE_MIN_VERSIONS_URL =
|
||||
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
|
||||
"FEATURE_MIN_VERSIONS.json";
|
||||
|
@ -132,7 +130,7 @@ export function sync(): Thunk {
|
|||
return function (_dispatch, getState) {
|
||||
const currentFBOSversion =
|
||||
getState().bot.hardware.informational_settings.controller_version;
|
||||
const IS_OK = versionOK(currentFBOSversion, EXPECTED_MAJOR, EXPECTED_MINOR);
|
||||
const IS_OK = versionOK(currentFBOSversion);
|
||||
if (IS_OK) {
|
||||
getDevice()
|
||||
.sync()
|
||||
|
|
|
@ -65,13 +65,7 @@ describe("<HardwareSettings />", () => {
|
|||
it("shows param export menu", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareConfig = fakeFirmwareConfig().body;
|
||||
p.firmwareConfig.api_migrated = true;
|
||||
const wrapper = shallow(<HardwareSettings {...p} />);
|
||||
expect(wrapper.html()).toContain("fa-download");
|
||||
});
|
||||
|
||||
it("doesn't show param export menu", () => {
|
||||
const wrapper = shallow(<HardwareSettings {...fakeProps()} />);
|
||||
expect(wrapper.html()).not.toContain("fa-download");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { t } from "../../i18next_wrapper";
|
|||
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
|
||||
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
|
||||
import { save, edit } from "../../api/crud";
|
||||
import { MustBeOnline, isBotOnline } from "../must_be_online";
|
||||
import { isBotOnline } from "../must_be_online";
|
||||
import { Content } from "../../constants";
|
||||
import { TimezoneSelector } from "../timezones/timezone_selector";
|
||||
import { timezoneMismatch } from "../timezones/guess_timezone";
|
||||
|
@ -116,53 +116,46 @@ export class FarmbotOsSettings
|
|||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<MustBeOnline
|
||||
syncStatus={sync_status}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"
|
||||
|| this.props.isValidFbosConfig}>
|
||||
<CameraSelection
|
||||
env={this.props.env}
|
||||
botOnline={botOnline}
|
||||
saveFarmwareEnv={this.props.saveFarmwareEnv}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
dispatch={this.props.dispatch} />
|
||||
<BoardType
|
||||
botOnline={botOnline}
|
||||
bot={bot}
|
||||
alerts={this.props.alerts}
|
||||
dispatch={this.props.dispatch}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
timeSettings={this.props.timeSettings}
|
||||
sourceFbosConfig={sourceFbosConfig} />
|
||||
<AutoUpdateRow
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
timeFormat={timeFormat}
|
||||
device={this.props.deviceAccount}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig} />
|
||||
<FarmbotOsRow
|
||||
bot={this.props.bot}
|
||||
osReleaseNotesHeading={this.osReleaseNotes.heading}
|
||||
osReleaseNotes={this.osReleaseNotes.notes}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
botOnline={botOnline}
|
||||
botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()}
|
||||
timeSettings={this.props.timeSettings}
|
||||
deviceAccount={this.props.deviceAccount} />
|
||||
<AutoSyncRow
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig} />
|
||||
{this.props.shouldDisplay(Feature.boot_sequence) &&
|
||||
<BootSequenceSelector />}
|
||||
<PowerAndReset
|
||||
controlPanelState={this.props.bot.controlPanelState}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
botOnline={botOnline} />
|
||||
</MustBeOnline>
|
||||
<CameraSelection
|
||||
env={this.props.env}
|
||||
botOnline={botOnline}
|
||||
saveFarmwareEnv={this.props.saveFarmwareEnv}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
dispatch={this.props.dispatch} />
|
||||
<BoardType
|
||||
botOnline={botOnline}
|
||||
bot={bot}
|
||||
alerts={this.props.alerts}
|
||||
dispatch={this.props.dispatch}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
timeSettings={this.props.timeSettings}
|
||||
sourceFbosConfig={sourceFbosConfig} />
|
||||
<AutoUpdateRow
|
||||
timeFormat={timeFormat}
|
||||
device={this.props.deviceAccount}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig} />
|
||||
<FarmbotOsRow
|
||||
bot={this.props.bot}
|
||||
osReleaseNotesHeading={this.osReleaseNotes.heading}
|
||||
osReleaseNotes={this.osReleaseNotes.notes}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
botOnline={botOnline}
|
||||
botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()}
|
||||
timeSettings={this.props.timeSettings}
|
||||
deviceAccount={this.props.deviceAccount} />
|
||||
<AutoSyncRow
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig} />
|
||||
{this.props.shouldDisplay(Feature.boot_sequence) &&
|
||||
<BootSequenceSelector />}
|
||||
<PowerAndReset
|
||||
controlPanelState={this.props.bot.controlPanelState}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
botOnline={botOnline} />
|
||||
</WidgetBody>
|
||||
</form>
|
||||
</Widget>;
|
||||
|
|
|
@ -21,7 +21,6 @@ describe("<AutoUpdateRow/>", () => {
|
|||
|
||||
const fakeProps = (): AutoUpdateRowProps => ({
|
||||
timeFormat: "12h",
|
||||
shouldDisplay: jest.fn(() => true),
|
||||
device: fakeDevice(),
|
||||
dispatch: jest.fn(x => x(jest.fn(), () => state)),
|
||||
sourceFbosConfig: () => ({ value: 1, consistent: true })
|
||||
|
|
|
@ -69,6 +69,9 @@ describe("<BoardType/>", () => {
|
|||
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
|
||||
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" },
|
||||
{ label: "Farmduino (Genesis v1.4)", value: "farmduino_k14" },
|
||||
{ label: "Farmduino (Genesis v1.5)", value: "farmduino_k15" },
|
||||
{ label: "Farmduino (Express v1.0)", value: "express_k10" },
|
||||
{ label: "None", value: "none" },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ describe("<FirmwareHardwareStatusDetails />", () => {
|
|||
apiFirmwareValue: undefined,
|
||||
botFirmwareValue: undefined,
|
||||
mcuFirmwareValue: undefined,
|
||||
shouldDisplay: () => true,
|
||||
timeSettings: fakeTimeSettings(),
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
@ -79,7 +78,6 @@ describe("<FirmwareHardwareStatus />", () => {
|
|||
alerts: [],
|
||||
botOnline: true,
|
||||
apiFirmwareValue: undefined,
|
||||
shouldDisplay: () => true,
|
||||
timeSettings: fakeTimeSettings(),
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
|
|
@ -7,17 +7,16 @@ import { Content } from "../../../constants";
|
|||
import { AutoUpdateRowProps } from "./interfaces";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector";
|
||||
import { Feature } from "../../interfaces";
|
||||
|
||||
export function AutoUpdateRow(props: AutoUpdateRowProps) {
|
||||
const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
|
||||
|
||||
return <div>
|
||||
{props.shouldDisplay(Feature.ota_update_hour) && <OtaTimeSelector
|
||||
<OtaTimeSelector
|
||||
timeFormat={props.timeFormat}
|
||||
disabled={!osAutoUpdate.value}
|
||||
value={props.device.body.ota_hour}
|
||||
onChange={changeOtaHour(props.dispatch, props.device)} />}
|
||||
onChange={changeOtaHour(props.dispatch, props.device)} />
|
||||
<Row>
|
||||
<Col xs={ColWidth.label}>
|
||||
<label>
|
||||
|
|
|
@ -57,7 +57,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
|||
<FBSelect
|
||||
key={this.apiValue}
|
||||
extraClass={this.state.sending ? "dim" : ""}
|
||||
list={getFirmwareChoices(this.props.shouldDisplay)}
|
||||
list={getFirmwareChoices()}
|
||||
selectedItem={this.selectedBoard}
|
||||
onChange={this.sendOffConfig} />
|
||||
</div>
|
||||
|
@ -69,8 +69,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
|||
alerts={this.props.alerts}
|
||||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
timeSettings={this.props.timeSettings}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
timeSettings={this.props.timeSettings} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Popover, Position } from "@blueprintjs/core";
|
|||
import { FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
|
||||
import { flashFirmware } from "../../actions";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { BotState, Feature, ShouldDisplay } from "../../interfaces";
|
||||
import { BotState } from "../../interfaces";
|
||||
import { FirmwareAlerts } from "../../../messages/alerts";
|
||||
import { TimeSettings } from "../../../interfaces";
|
||||
import { trim } from "../../../util";
|
||||
|
@ -36,7 +36,6 @@ export interface FirmwareHardwareStatusDetailsProps {
|
|||
apiFirmwareValue: string | undefined;
|
||||
botFirmwareValue: string | undefined;
|
||||
mcuFirmwareValue: string | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
timeSettings: TimeSettings;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
@ -81,13 +80,10 @@ export const FirmwareHardwareStatusDetails =
|
|||
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
|
||||
<label>{t("Arduino/Farmduino")}</label>
|
||||
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
|
||||
{props.shouldDisplay(Feature.flash_firmware) &&
|
||||
<div>
|
||||
<label>{t("Actions")}</label>
|
||||
<FirmwareActions
|
||||
apiFirmwareValue={props.apiFirmwareValue}
|
||||
botOnline={props.botOnline} />
|
||||
</div>}
|
||||
<label>{t("Actions")}</label>
|
||||
<FirmwareActions
|
||||
apiFirmwareValue={props.apiFirmwareValue}
|
||||
botOnline={props.botOnline} />
|
||||
<FirmwareAlerts
|
||||
alerts={props.alerts}
|
||||
dispatch={props.dispatch}
|
||||
|
@ -102,7 +98,6 @@ export interface FirmwareHardwareStatusProps {
|
|||
bot: BotState;
|
||||
botOnline: boolean;
|
||||
timeSettings: TimeSettings;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
|
@ -122,7 +117,6 @@ export const FirmwareHardwareStatus = (props: FirmwareHardwareStatusProps) => {
|
|||
botFirmwareValue={firmware_hardware}
|
||||
mcuFirmwareValue={boardType(firmware_version)}
|
||||
timeSettings={props.timeSettings}
|
||||
dispatch={props.dispatch}
|
||||
shouldDisplay={props.shouldDisplay} />
|
||||
dispatch={props.dispatch} />
|
||||
</Popover>;
|
||||
};
|
||||
|
|
|
@ -24,7 +24,6 @@ export interface AutoUpdateRowProps {
|
|||
timeFormat: PreferredHourFormat;
|
||||
sourceFbosConfig: SourceFbosConfig;
|
||||
device: TaggedDevice;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface CameraSelectionProps {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { FirmwareHardware } from "farmbot";
|
||||
import { ShouldDisplay, Feature } from "../interfaces";
|
||||
|
||||
export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
|
||||
const values: FirmwareHardware[] = [
|
||||
|
@ -77,12 +76,11 @@ export const FIRMWARE_CHOICES_DDI = {
|
|||
[NONE.value]: NONE
|
||||
};
|
||||
|
||||
export const getFirmwareChoices =
|
||||
(shouldDisplay: ShouldDisplay = () => true) => ([
|
||||
ARDUINO,
|
||||
FARMDUINO,
|
||||
FARMDUINO_K14,
|
||||
...(shouldDisplay(Feature.farmduino_k15) ? [FARMDUINO_K15] : []),
|
||||
...(shouldDisplay(Feature.express_k10) ? [EXPRESS_K10] : []),
|
||||
...(shouldDisplay(Feature.none_firmware) ? [NONE] : []),
|
||||
]);
|
||||
export const getFirmwareChoices = () => ([
|
||||
ARDUINO,
|
||||
FARMDUINO,
|
||||
FARMDUINO_K14,
|
||||
FARMDUINO_K15,
|
||||
EXPRESS_K10,
|
||||
NONE,
|
||||
]);
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
|
||||
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
|
||||
import { HardwareSettingsProps } from "../interfaces";
|
||||
import { MustBeOnline, isBotOnline } from "../must_be_online";
|
||||
import { isBotOnline } from "../must_be_online";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { DangerZone } from "./hardware_settings/danger_zone";
|
||||
import { PinGuard } from "./hardware_settings/pin_guard";
|
||||
|
@ -32,14 +32,7 @@ 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}>
|
||||
<MustBeOnline
|
||||
hideBanner={true}
|
||||
syncStatus={sync_status}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
</MustBeOnline>
|
||||
</WidgetHeader>
|
||||
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
|
||||
<WidgetBody>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
|
@ -51,43 +44,37 @@ export class HardwareSettings extends
|
|||
onClick={() => dispatch(bulkToggleControlPanel(false))}>
|
||||
{t("Collapse All")}
|
||||
</button>
|
||||
{firmwareConfig &&
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-download" />
|
||||
<FwParamExportMenu firmwareConfig={firmwareConfig} />
|
||||
</Popover>}
|
||||
<MustBeOnline
|
||||
networkState={this.props.botToMqttStatus}
|
||||
syncStatus={sync_status}
|
||||
lockOpen={process.env.NODE_ENV !== "production" || !!firmwareConfig}>
|
||||
<div className="label-headings">
|
||||
<SpacePanelHeader />
|
||||
</div>
|
||||
<HomingAndCalibration {...commonProps}
|
||||
bot={bot}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareConfig={firmwareConfig}
|
||||
firmwareHardware={firmwareHardware}
|
||||
botDisconnected={botDisconnected} />
|
||||
<Motors {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<Encoders {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<EndStops {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<ErrorHandling {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinGuard {...commonProps}
|
||||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<DangerZone {...commonProps}
|
||||
onReset={MCUFactoryReset}
|
||||
botDisconnected={botDisconnected} />
|
||||
<PinBindings {...commonProps}
|
||||
resources={resources} />
|
||||
</MustBeOnline>
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-download" />
|
||||
<FwParamExportMenu firmwareConfig={firmwareConfig} />
|
||||
</Popover>
|
||||
<div className="label-headings">
|
||||
<SpacePanelHeader />
|
||||
</div>
|
||||
<HomingAndCalibration {...commonProps}
|
||||
bot={bot}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareConfig={firmwareConfig}
|
||||
firmwareHardware={firmwareHardware}
|
||||
botDisconnected={botDisconnected} />
|
||||
<Motors {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<Encoders {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<EndStops {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<ErrorHandling {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinGuard {...commonProps}
|
||||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<DangerZone {...commonProps}
|
||||
onReset={MCUFactoryReset}
|
||||
botDisconnected={botDisconnected} />
|
||||
<PinBindings {...commonProps}
|
||||
resources={resources} />
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ const getSubKeyName = (key: string) => {
|
|||
};
|
||||
|
||||
export const FwParamExportMenu =
|
||||
({ firmwareConfig }: { firmwareConfig: FirmwareConfig }) => {
|
||||
({ firmwareConfig }: { firmwareConfig: FirmwareConfig | undefined }) => {
|
||||
/** Filter out unnecessary parameters. */
|
||||
const filteredConfig = pickBy(firmwareConfig, (_, key) =>
|
||||
!["id", "device_id", "api_migrated", "created_at", "updated_at",
|
||||
|
|
|
@ -11,7 +11,6 @@ import { maybeNegateStatus } from "../connectivity/maybe_negate_status";
|
|||
import { ReduxAction } from "../redux/interfaces";
|
||||
import { connectivityReducer, PingResultPayload } from "../connectivity/reducer";
|
||||
import { versionOK } from "../util";
|
||||
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
|
||||
import { DeepPartial } from "redux";
|
||||
import { incomingLegacyStatus } from "../connectivity/connect_device";
|
||||
import { merge } from "lodash";
|
||||
|
@ -205,8 +204,7 @@ function legacyStatusHandler(state: BotState,
|
|||
|
||||
const nextSyncStatus = maybeNegateStatus(info);
|
||||
|
||||
versionOK(informational_settings.controller_version,
|
||||
EXPECTED_MAJOR, EXPECTED_MINOR);
|
||||
versionOK(informational_settings.controller_version);
|
||||
state.hardware.informational_settings.sync_status = nextSyncStatus;
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export function FarmBotLayer(props: FarmBotLayerProps) {
|
|||
visible, stopAtHome, botSize, plantAreaOffset, mapTransformProps,
|
||||
peripherals, eStopStatus, botLocationData, getConfigValue
|
||||
} = props;
|
||||
return visible ? <g id="farmbot-layer">
|
||||
return visible ? <g id="farmbot-layer" style={{ pointerEvents: "none" }}>
|
||||
<VirtualFarmBot
|
||||
mapTransformProps={mapTransformProps}
|
||||
botLocationData={botLocationData}
|
||||
|
|
|
@ -50,6 +50,14 @@ describe("<ToolbaySlot />", () => {
|
|||
const wrapper = svgMount(<ToolbaySlot {...p} />);
|
||||
expect(wrapper.find("use").props().transform).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles bad data", () => {
|
||||
const p = fakeProps();
|
||||
p.pulloutDirection = 1.1;
|
||||
p.quadrant = 1.1;
|
||||
const wrapper = svgMount(<ToolbaySlot {...p} />);
|
||||
expect(wrapper.find("use").props().transform).toEqual("rotate(0, 10, 20)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Tool/>", () => {
|
||||
|
|
|
@ -49,4 +49,8 @@ describe("textAnchorPosition()", () => {
|
|||
expect(textAnchorPosition(4, 3, true)).toEqual(END);
|
||||
expect(textAnchorPosition(4, 4, true)).toEqual(START);
|
||||
});
|
||||
|
||||
it("handles bad data", () => {
|
||||
expect(textAnchorPosition(1.1, 1.1, false)).toEqual(START);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,11 @@ jest.mock("../../../../../history", () => ({
|
|||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../../../account/dev/dev_support", () => ({
|
||||
DevSettings: { futureFeaturesEnabled: () => mockDev }
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer";
|
||||
import {
|
||||
|
@ -53,6 +58,7 @@ describe("<ToolSlotLayer/>", () => {
|
|||
});
|
||||
|
||||
it("navigates to tools page", async () => {
|
||||
mockDev = true;
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||
|
|
|
@ -48,10 +48,10 @@ describe("<ToolSlotPoint/>", () => {
|
|||
const p = fakeProps();
|
||||
p.slot.toolSlot.body.id = 1;
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
mockDev = false;
|
||||
mockDev = true;
|
||||
wrapper.find("g").first().simulate("click");
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
mockDev = true;
|
||||
mockDev = false;
|
||||
wrapper.find("g").first().simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1");
|
||||
});
|
||||
|
|
|
@ -180,23 +180,30 @@ export interface GantryToolSlotGraphicProps {
|
|||
xySwap: boolean;
|
||||
}
|
||||
|
||||
/** dimensions */
|
||||
enum Trough {
|
||||
width = 20,
|
||||
length = 45,
|
||||
wall = 4,
|
||||
}
|
||||
|
||||
export const GantryToolSlot = (props: GantryToolSlotGraphicProps) => {
|
||||
const { x, y, xySwap } = props;
|
||||
const slotLengthX = xySwap ? 24 : 49;
|
||||
const slotLengthY = xySwap ? 49 : 24;
|
||||
const slotLengthX = Trough.wall + (xySwap ? Trough.width : Trough.length);
|
||||
const slotLengthY = Trough.wall + (xySwap ? Trough.length : Trough.width);
|
||||
return <g id={"gantry-toolbay-slot"}>
|
||||
<rect
|
||||
x={x - slotLengthX / 2} y={y - slotLengthY / 2}
|
||||
width={slotLengthX} height={slotLengthY}
|
||||
stroke={Color.mediumGray} strokeWidth={4} strokeOpacity={0.25}
|
||||
stroke={Color.mediumGray} strokeWidth={Trough.wall} strokeOpacity={0.25}
|
||||
fill="transparent" />
|
||||
</g>;
|
||||
};
|
||||
|
||||
const SeedTrough = (props: ToolGraphicProps) => {
|
||||
const { x, y, hovered, dispatch, uuid, xySwap } = props;
|
||||
const slotLengthX = xySwap ? 20 : 45;
|
||||
const slotLengthY = xySwap ? 45 : 20;
|
||||
const slotLengthX = xySwap ? Trough.width : Trough.length;
|
||||
const slotLengthY = xySwap ? Trough.length : Trough.width;
|
||||
return <g id={"seed-trough"}
|
||||
onMouseOver={() => dispatch(setToolHover(uuid))}
|
||||
onMouseLeave={() => dispatch(setToolHover(undefined))}>
|
||||
|
|
|
@ -39,7 +39,7 @@ export const textAnchorPosition = (
|
|||
case Anchor.end: return { anchor: "end", x: -40, y: 10 };
|
||||
case Anchor.middleTop: return { anchor: "middle", x: 0, y: 60 };
|
||||
case Anchor.middleBottom: return { anchor: "middle", x: 0, y: -40 };
|
||||
default: return { anchor: "start", x: 40, y: 10 };
|
||||
default: throw new Error("https://xkcd.com/2200");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
|||
const pathArray = getPathArray();
|
||||
const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4);
|
||||
const goToToolsPage = () => canClickTool &&
|
||||
!DevSettings.futureFeaturesEnabled() && history.push("/app/tools");
|
||||
DevSettings.futureFeaturesEnabled() && history.push("/app/tools");
|
||||
const { slots, visible, mapTransformProps } = props;
|
||||
const cursor = canClickTool ? "pointer" : "default";
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
|
|||
xySwap,
|
||||
};
|
||||
return <g id={"toolslot-" + id}
|
||||
onClick={() => DevSettings.futureFeaturesEnabled() &&
|
||||
onClick={() => !DevSettings.futureFeaturesEnabled() &&
|
||||
history.push(`/app/designer/tool-slots/${id}`)}>
|
||||
{pullout_direction &&
|
||||
<ToolbaySlot
|
||||
|
|
|
@ -139,7 +139,7 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
|
|||
panel={Panel.Weeds}
|
||||
linkTo={"/app/designer/weeds"}
|
||||
title={t("Weeds")} />
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
{!DevSettings.futureFeaturesEnabled() &&
|
||||
<NavTab
|
||||
panel={Panel.Tools}
|
||||
linkTo={"/app/designer/tools"}
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as React from "react";
|
|||
import { mount } from "enzyme";
|
||||
import { PlantGrid } from "../plant_grid";
|
||||
import { saveGrid, stashGrid } from "../thunks";
|
||||
import { error } from "../../../../toast/toast";
|
||||
import { error, success } from "../../../../toast/toast";
|
||||
|
||||
describe("PlantGrid", () => {
|
||||
function fakeProps() {
|
||||
|
@ -39,8 +39,11 @@ describe("PlantGrid", () => {
|
|||
it("saves a grid", async () => {
|
||||
const props = fakeProps();
|
||||
const pg = mount<PlantGrid>(<PlantGrid {...props} />).instance();
|
||||
const oldId = pg.state.gridId;
|
||||
await pg.saveGrid();
|
||||
expect(saveGrid).toHaveBeenCalledWith(pg.state.gridId);
|
||||
expect(saveGrid).toHaveBeenCalledWith(oldId);
|
||||
expect(success).toHaveBeenCalledWith("16 plants added.");
|
||||
expect(pg.state.gridId).not.toEqual(oldId);
|
||||
});
|
||||
|
||||
it("stashes a grid", async () => {
|
||||
|
|
|
@ -9,13 +9,18 @@ import { initPlantGrid } from "./generate_grid";
|
|||
import { init } from "../../../api/crud";
|
||||
import { uuid } from "farmbot";
|
||||
import { saveGrid, stashGrid } from "./thunks";
|
||||
import { error } from "../../../toast/toast";
|
||||
import { error, success } from "../../../toast/toast";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { GridInput } from "./grid_input";
|
||||
|
||||
export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
|
||||
state: PlantGridState = { ...EMPTY_PLANT_GRID, gridId: uuid() };
|
||||
|
||||
get plantCount() {
|
||||
const { numPlantsH, numPlantsV } = this.state.grid;
|
||||
return numPlantsH * numPlantsV;
|
||||
}
|
||||
|
||||
onchange = (key: PlantGridKey, val: number) => {
|
||||
const grid = { ...this.state.grid, [key]: val };
|
||||
this.setState({ grid });
|
||||
|
@ -33,9 +38,7 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
|
|||
}
|
||||
|
||||
performPreview = () => {
|
||||
const { numPlantsH, numPlantsV } = this.state.grid;
|
||||
const total = numPlantsH * numPlantsV;
|
||||
if (total > 100) {
|
||||
if (this.plantCount > 100) {
|
||||
error(t("Please make a grid with less than 100 plants"));
|
||||
return;
|
||||
}
|
||||
|
@ -57,7 +60,10 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
|
|||
|
||||
saveGrid = () => {
|
||||
const p: Promise<{}> = this.props.dispatch(saveGrid(this.state.gridId));
|
||||
return p.then(() => this.setState(EMPTY_PLANT_GRID));
|
||||
return p.then(() => {
|
||||
success(t("{{ count }} plants added.", { count: this.plantCount }));
|
||||
this.setState({ ...EMPTY_PLANT_GRID, gridId: uuid() });
|
||||
});
|
||||
}
|
||||
|
||||
inputs = () => {
|
||||
|
@ -73,16 +79,16 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
|
|||
case "clean":
|
||||
return <div>
|
||||
<a className={"clear-button"} onClick={this.performPreview}>
|
||||
Preview
|
||||
{t("Preview")}
|
||||
</a>
|
||||
</div>;
|
||||
case "dirty":
|
||||
return <div>
|
||||
<a className={"clear-button"} onClick={this.revertPreview}>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</a>
|
||||
<a className={"clear-button"} onClick={this.saveGrid}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { save, edit } from "../../../api/crud";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
|
||||
import { Content } from "../../../constants";
|
||||
|
||||
describe("<GroupDetailActive/>", () => {
|
||||
const fakeProps = (): GroupDetailActiveProps => {
|
||||
|
@ -105,16 +106,23 @@ describe("<GroupDetailActive/>", () => {
|
|||
});
|
||||
|
||||
it("shows paths", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("optimized");
|
||||
});
|
||||
|
||||
it("doesn't show paths", () => {
|
||||
mockDev = false;
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("optimized");
|
||||
expect(wrapper.text().toLowerCase()).toContain("0m");
|
||||
});
|
||||
|
||||
it("doesn't show paths", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("0m");
|
||||
});
|
||||
|
||||
it("shows random warning text", () => {
|
||||
const p = fakeProps();
|
||||
p.group.body.sort_type = "random";
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../account/dev/dev_support", () => ({
|
||||
DevSettings: {
|
||||
futureFeaturesEnabled: () => mockDev,
|
||||
}
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import {
|
||||
|
@ -141,6 +148,7 @@ describe("<Paths />", () => {
|
|||
p.pathPoints = cases.order.xy_ascending;
|
||||
const wrapper = mount<Paths>(<Paths {...p} />);
|
||||
expect(wrapper.state().pathData).toEqual(cases.distance);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("optimized");
|
||||
});
|
||||
|
||||
it.each<[PointGroupSortType]>([
|
||||
|
@ -154,4 +162,24 @@ describe("<Paths />", () => {
|
|||
expect(SORT_OPTIONS[sortType](cases.order.xy_ascending))
|
||||
.toEqual(cases.order[sortType]);
|
||||
});
|
||||
|
||||
it("renders new sort type", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
const cases = pathTestCases();
|
||||
p.pathPoints = cases.order.xy_ascending;
|
||||
const wrapper = mount<Paths>(<Paths {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("optimized");
|
||||
});
|
||||
|
||||
it("doesn't generate data twice", () => {
|
||||
const p = fakeProps();
|
||||
const cases = pathTestCases();
|
||||
p.pathPoints = cases.order.xy_ascending;
|
||||
const wrapper = mount<Paths>(<Paths {...p} />);
|
||||
expect(wrapper.state().pathData).toEqual(cases.distance);
|
||||
wrapper.setState({ pathData: { nn: 0 } });
|
||||
wrapper.update();
|
||||
expect(wrapper.state().pathData).toEqual({ nn: 0 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,7 +45,6 @@ describe("<PointGroupItem/>", () => {
|
|||
const p = fakeProps();
|
||||
p.point = fakePlant();
|
||||
const i = new PointGroupItem(p);
|
||||
i.setState = jest.fn();
|
||||
const fakeImgEvent = imgEvent();
|
||||
await i.maybeGetCachedIcon(fakeImgEvent);
|
||||
const slug = i.props.point.body.pointer_type === "Plant" ?
|
||||
|
@ -55,11 +54,17 @@ describe("<PointGroupItem/>", () => {
|
|||
expect(setImgSrc).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets icon in state", () => {
|
||||
const i = new PointGroupItem(fakeProps());
|
||||
i.setState = jest.fn();
|
||||
i.setIconState("fake icon");
|
||||
expect(i.setState).toHaveBeenCalledWith({ icon: "fake icon" });
|
||||
});
|
||||
|
||||
it("fetches point icon", () => {
|
||||
const p = fakeProps();
|
||||
p.point = fakePoint();
|
||||
const i = new PointGroupItem(p);
|
||||
i.setState = jest.fn();
|
||||
const fakeImgEvent = imgEvent();
|
||||
i.maybeGetCachedIcon(fakeImgEvent);
|
||||
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
|
||||
|
@ -71,7 +76,6 @@ describe("<PointGroupItem/>", () => {
|
|||
const p = fakeProps();
|
||||
p.point = fakeToolSlot();
|
||||
const i = new PointGroupItem(p);
|
||||
i.setState = jest.fn();
|
||||
const fakeImgEvent = imgEvent();
|
||||
i.maybeGetCachedIcon(fakeImgEvent);
|
||||
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
isSortType, sortTypeChange, SORT_OPTIONS, PointGroupSortSelector,
|
||||
PointGroupSortSelectorProps
|
||||
isSortType, sortTypeChange, SORT_OPTIONS
|
||||
} from "../point_group_sort_selector";
|
||||
import { DropDownItem } from "../../../ui";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { mount } from "enzyme";
|
||||
import { Content } from "../../../constants";
|
||||
|
||||
const tests: [string, boolean][] = [
|
||||
["", false],
|
||||
|
@ -89,15 +85,3 @@ describe("sort()", () => {
|
|||
expect(results).toEqual(["C", "D", "B", "A"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<PointGroupSortSelector />", () => {
|
||||
const fakeProps = (): PointGroupSortSelectorProps => ({
|
||||
onChange: jest.fn(),
|
||||
value: "random",
|
||||
});
|
||||
|
||||
it("shows random warning text", () => {
|
||||
const wrapper = mount(<PointGroupSortSelector {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -97,9 +97,24 @@ export class GroupDetailActive
|
|||
defaultValue={group.body.name}
|
||||
onChange={this.update}
|
||||
onBlur={this.saveGroup} />
|
||||
<PointGroupSortSelector
|
||||
value={group.body.sort_type}
|
||||
onChange={this.changeSortType} />
|
||||
<div>
|
||||
<label>
|
||||
{t("SORT BY")}
|
||||
</label>
|
||||
{!DevSettings.futureFeaturesEnabled()
|
||||
? <Paths
|
||||
key={JSON.stringify(this.pointsSelectedByGroup
|
||||
.map(p => p.body.id))}
|
||||
pathPoints={this.pointsSelectedByGroup}
|
||||
dispatch={dispatch}
|
||||
group={group} />
|
||||
: <PointGroupSortSelector
|
||||
value={group.body.sort_type}
|
||||
onChange={this.changeSortType} />}
|
||||
<p>
|
||||
{group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)}
|
||||
</p>
|
||||
</div>
|
||||
<label>
|
||||
{t("GROUP MEMBERS ({{count}})", { count: this.icons.length })}
|
||||
</label>
|
||||
|
@ -117,11 +132,6 @@ export class GroupDetailActive
|
|||
{this.props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupCriteria dispatch={dispatch}
|
||||
group={group} slugs={this.props.slugs} />}
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<Paths
|
||||
pathPoints={this.pointsSelectedByGroup}
|
||||
dispatch={dispatch}
|
||||
group={group} />}
|
||||
<DeleteButton
|
||||
className="group-delete-btn"
|
||||
dispatch={dispatch}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { MapTransformProps } from "../map/interfaces";
|
||||
import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector";
|
||||
import { sortBy } from "lodash";
|
||||
import { sortBy, isNumber } from "lodash";
|
||||
import { PointsPathLine } from "./group_order_visual";
|
||||
import { Color } from "../../ui";
|
||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
|
@ -10,6 +10,7 @@ import { Actions } from "../../constants";
|
|||
import { edit } from "../../api/crud";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
import { error } from "../../toast/toast";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
|
||||
const xy = (point: TaggedPoint) => ({ x: point.body.x, y: point.body.y });
|
||||
|
||||
|
@ -66,7 +67,8 @@ export const PathInfoBar = (props: PathInfoBarProps) => {
|
|||
const normalizedLength = pathLength / maxLength * 100;
|
||||
const sortLabel =
|
||||
sortTypeKey == "nn" ? "Optimized" : sortOptionsTable()[sortTypeKey];
|
||||
return <div className={"sort-path-info-bar"}
|
||||
const selected = group.body.sort_type == sortTypeKey;
|
||||
return <div className={`sort-option-bar ${selected ? "selected" : ""}`}
|
||||
onMouseEnter={() =>
|
||||
dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })}
|
||||
onMouseLeave={() =>
|
||||
|
@ -74,9 +76,11 @@ export const PathInfoBar = (props: PathInfoBarProps) => {
|
|||
onClick={() =>
|
||||
sortTypeKey == "nn"
|
||||
? error(t("Not supported yet."))
|
||||
: dispatch(edit(group, { sort_type: sortTypeKey }))}
|
||||
style={{ width: `${normalizedLength}%` }}>
|
||||
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
|
||||
: dispatch(edit(group, { sort_type: sortTypeKey }))}>
|
||||
<div className={"sort-path-info-bar"}
|
||||
style={{ width: `${normalizedLength}%` }}>
|
||||
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -101,15 +105,17 @@ export class Paths extends React.Component<PathsProps, PathsState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.pathData.nn) { this.generatePathData(this.props.pathPoints); }
|
||||
return <div>
|
||||
<label>{t("Path lengths by sort type")}</label>
|
||||
{SORT_TYPES.concat("nn").map(st =>
|
||||
<PathInfoBar key={st}
|
||||
sortTypeKey={st}
|
||||
dispatch={this.props.dispatch}
|
||||
group={this.props.group}
|
||||
pathData={this.state.pathData} />)}
|
||||
if (!isNumber(this.state.pathData.nn)) {
|
||||
this.generatePathData(this.props.pathPoints);
|
||||
}
|
||||
return <div className={"group-sort-types"}>
|
||||
{SORT_TYPES.concat(DevSettings.futureFeaturesEnabled() ? "nn" : [])
|
||||
.map(sortType =>
|
||||
<PathInfoBar key={sortType}
|
||||
sortTypeKey={sortType}
|
||||
dispatch={this.props.dispatch}
|
||||
group={this.props.group}
|
||||
pathData={this.state.pathData} />)}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ export class PointGroupItem
|
|||
this.leave();
|
||||
}
|
||||
|
||||
setIconState = (icon: string) => this.setState({ icon });
|
||||
|
||||
get criteriaIcon() {
|
||||
return !this.props.group.body.point_ids
|
||||
.includes(this.props.point.body.id || 0);
|
||||
|
@ -67,7 +69,7 @@ export class PointGroupItem
|
|||
switch (this.props.point.body.pointer_type) {
|
||||
case "Plant":
|
||||
const slug = this.props.point.body.openfarm_slug;
|
||||
maybeGetCachedPlantIcon(slug, img, icon => this.setState({ icon }));
|
||||
maybeGetCachedPlantIcon(slug, img, this.setIconState);
|
||||
break;
|
||||
case "GenericPointer":
|
||||
const { color } = this.props.point.body.meta;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
|||
import { FBSelect, DropDownItem } from "../../ui";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { shuffle, sortBy } from "lodash";
|
||||
import { Content } from "../../constants";
|
||||
import { TaggedPoint } from "farmbot";
|
||||
|
||||
export interface PointGroupSortSelectorProps {
|
||||
|
@ -42,22 +41,11 @@ export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => {
|
|||
};
|
||||
|
||||
export function PointGroupSortSelector(p: PointGroupSortSelectorProps) {
|
||||
|
||||
return <div>
|
||||
<div className="default-value-tooltip">
|
||||
<label>
|
||||
{t("SORT BY")}
|
||||
</label>
|
||||
</div>
|
||||
<FBSelect
|
||||
key={p.value}
|
||||
list={optionPlusDescriptions()}
|
||||
selectedItem={selected(p.value as PointGroupSortType)}
|
||||
onChange={sortTypeChange(p.onChange)} />
|
||||
<p>
|
||||
{(p.value == "random") ? t(Content.SORT_DESCRIPTION) : ""}
|
||||
</p>
|
||||
</div>;
|
||||
return <FBSelect
|
||||
key={p.value}
|
||||
list={optionPlusDescriptions()}
|
||||
selectedItem={selected(p.value as PointGroupSortType)}
|
||||
onChange={sortTypeChange(p.onChange)} />;
|
||||
}
|
||||
|
||||
type Sorter = (p: TaggedPoint[]) => TaggedPoint[];
|
||||
|
|
|
@ -11,6 +11,7 @@ import { fakeState } from "../../../__test_support__/fake_state";
|
|||
import { SaveBtn } from "../../../ui";
|
||||
import { initSave } from "../../../api/crud";
|
||||
import { history } from "../../../history";
|
||||
import { error } from "../../../toast/toast";
|
||||
|
||||
describe("<AddTool />", () => {
|
||||
const fakeProps = (): AddToolProps => ({
|
||||
|
@ -37,10 +38,19 @@ describe("<AddTool />", () => {
|
|||
expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" });
|
||||
});
|
||||
|
||||
it("adds stock tools", () => {
|
||||
it("doesn't add stock tools", () => {
|
||||
const wrapper = mount(<AddTool {...fakeProps()} />);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(initSave).toHaveBeenCalledTimes(6);
|
||||
expect(error).toHaveBeenCalledWith("Please choose a FarmBot model.");
|
||||
expect(initSave).not.toHaveBeenCalledTimes(6);
|
||||
expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools");
|
||||
});
|
||||
|
||||
it("adds stock tools", () => {
|
||||
const wrapper = mount(<AddTool {...fakeProps()} />);
|
||||
wrapper.setState({ model: "express" });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(initSave).toHaveBeenCalledTimes(2);
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,20 @@ import {
|
|||
} from "../designer_panel";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { SaveBtn } from "../../ui";
|
||||
import { SaveBtn, FBSelect, DropDownItem } from "../../ui";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { initSave } from "../../api/crud";
|
||||
import { Panel } from "../panel_header";
|
||||
import { history } from "../../history";
|
||||
import { error } from "../../toast/toast";
|
||||
|
||||
enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" }
|
||||
|
||||
const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
||||
[Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 },
|
||||
[Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 },
|
||||
[Model.express]: { label: t("Express"), value: Model.express },
|
||||
});
|
||||
|
||||
export interface AddToolProps {
|
||||
dispatch: Function;
|
||||
|
@ -17,6 +26,7 @@ export interface AddToolProps {
|
|||
|
||||
export interface AddToolState {
|
||||
toolName: string;
|
||||
model: Model | undefined;
|
||||
}
|
||||
|
||||
export const mapStateToProps = (props: Everything): AddToolProps => ({
|
||||
|
@ -24,7 +34,7 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({
|
|||
});
|
||||
|
||||
export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
|
||||
state: AddToolState = { toolName: "" };
|
||||
state: AddToolState = { toolName: "", model: undefined };
|
||||
|
||||
newTool = (name: string) => {
|
||||
this.props.dispatch(initSave("Tool", { name }));
|
||||
|
@ -35,28 +45,60 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
|
|||
history.push("/app/designer/tools");
|
||||
}
|
||||
|
||||
get stockToolNames() {
|
||||
return [
|
||||
t("Seeder"),
|
||||
t("Watering Nozzle"),
|
||||
t("Weeder"),
|
||||
t("Soil Sensor"),
|
||||
t("Seed Bin"),
|
||||
t("Seed Tray"),
|
||||
];
|
||||
stockToolNames = (model: Model) => {
|
||||
switch (model) {
|
||||
case Model.genesis14:
|
||||
return [
|
||||
t("Seeder"),
|
||||
t("Watering Nozzle"),
|
||||
t("Weeder"),
|
||||
t("Soil Sensor"),
|
||||
t("Seed Bin"),
|
||||
t("Seed Tray"),
|
||||
];
|
||||
case Model.genesis15:
|
||||
return [
|
||||
t("Seeder"),
|
||||
t("Watering Nozzle"),
|
||||
t("Weeder"),
|
||||
t("Soil Sensor"),
|
||||
t("Seed Bin"),
|
||||
t("Seed Tray"),
|
||||
t("Seed Trough 1"),
|
||||
t("Seed Trough 2"),
|
||||
];
|
||||
case Model.express:
|
||||
return [
|
||||
t("Seed Trough 1"),
|
||||
t("Seed Trough 2"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
AddStockTools = () =>
|
||||
<div className="add-stock-tools">
|
||||
<label>{t("Add stock tools")}</label>
|
||||
<ul>
|
||||
{this.stockToolNames.map(n => <li key={n}>{n}</li>)}
|
||||
</ul>
|
||||
<FBSelect
|
||||
customNullLabel={t("Choose model")}
|
||||
list={Object.values(MODEL_DDI_LOOKUP())}
|
||||
selectedItem={this.state.model
|
||||
? MODEL_DDI_LOOKUP()[this.state.model]
|
||||
: undefined}
|
||||
onChange={ddi => this.setState({ model: ddi.value as Model })}
|
||||
/>
|
||||
{this.state.model &&
|
||||
<ul>
|
||||
{this.stockToolNames(this.state.model).map(n => <li key={n}>{n}</li>)}
|
||||
</ul>}
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={() => {
|
||||
this.stockToolNames.map(n => this.newTool(n));
|
||||
history.push("/app/designer/tools");
|
||||
if (this.state.model) {
|
||||
this.stockToolNames(this.state.model).map(n => this.newTool(n));
|
||||
history.push("/app/designer/tools");
|
||||
} else {
|
||||
error(t("Please choose a FarmBot model."));
|
||||
}
|
||||
}}>
|
||||
<i className="fa fa-plus" />
|
||||
{t("Stock Tools")}
|
||||
|
|
|
@ -159,14 +159,20 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
getToolName={this.getToolName} />)}
|
||||
</div>
|
||||
|
||||
InactiveTools = () =>
|
||||
<div className="inactive-tools">
|
||||
<label>{t("inactive tools")}</label>
|
||||
Tools = () =>
|
||||
<div className="tools">
|
||||
<div className="tools-header">
|
||||
<label>{t("tools")}</label>
|
||||
<Link to={"/app/designer/tools/add"}>
|
||||
<div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}>
|
||||
<i className="fa fa-plus" title={t("Add tool")} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{this.props.tools
|
||||
.filter(tool => !tool.body.name ||
|
||||
tool.body.name && tool.body.name.toLowerCase()
|
||||
.includes(this.state.searchTerm.toLowerCase()))
|
||||
.filter(tool => tool.body.status === "inactive")
|
||||
.map(tool =>
|
||||
<ToolInventoryItem key={tool.uuid}
|
||||
toolId={tool.body.id}
|
||||
|
@ -175,25 +181,26 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
|
||||
render() {
|
||||
const panelName = "tools";
|
||||
const hasTools = this.props.tools.length > 0;
|
||||
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
|
||||
<DesignerNavTabs />
|
||||
<DesignerPanelTop
|
||||
panel={Panel.Tools}
|
||||
linkTo={"/app/designer/tools/add"}
|
||||
title={t("Add tool")}>
|
||||
linkTo={!hasTools ? "/app/designer/tools/add" : undefined}
|
||||
title={!hasTools ? t("Add tool") : undefined}>
|
||||
<input type="text" onChange={this.update}
|
||||
placeholder={t("Search your tools...")} />
|
||||
</DesignerPanelTop>
|
||||
<DesignerPanelContent panelName={"tools"}>
|
||||
<EmptyStateWrapper
|
||||
notEmpty={this.props.tools.length > 0}
|
||||
notEmpty={hasTools}
|
||||
graphic={EmptyStateGraphic.tools}
|
||||
title={t("Add a tool")}
|
||||
text={Content.NO_TOOLS}
|
||||
colorScheme={"tools"}>
|
||||
<this.MountedToolInfo />
|
||||
<this.ToolSlots />
|
||||
<this.InactiveTools />
|
||||
<this.Tools />
|
||||
</EmptyStateWrapper>
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
|
|
|
@ -3,6 +3,13 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
|||
jest.mock("../actions", () => ({ scanImage: jest.fn() }));
|
||||
jest.mock("../../images/actions", () => ({ selectImage: jest.fn() }));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../account/dev/dev_support", () => ({
|
||||
DevSettings: {
|
||||
futureFeaturesEnabled: () => mockDev,
|
||||
}
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { CameraCalibration } from "../camera_calibration";
|
||||
|
@ -12,6 +19,7 @@ import { selectImage } from "../../images/actions";
|
|||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { error } from "../../../toast/toast";
|
||||
import { Content, ToolTips } from "../../../constants";
|
||||
import { SPECIAL_VALUES } from "../../weed_detector/remote_env/constants";
|
||||
|
||||
describe("<CameraCalibration/>", () => {
|
||||
const fakeProps = (): CameraCalibrationProps => ({
|
||||
|
@ -116,4 +124,21 @@ describe("<CameraCalibration/>", () => {
|
|||
expect(error).toHaveBeenCalledWith(
|
||||
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
|
||||
});
|
||||
|
||||
it("toggles simple version", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<CameraCalibration {...p} />);
|
||||
wrapper.find("input").first().simulate("change");
|
||||
expect(mockDevice.setUserEnv).toHaveBeenCalledWith({
|
||||
CAMERA_CALIBRATION_easy_calibration: "\"FALSE\""
|
||||
});
|
||||
});
|
||||
|
||||
it("renders simple version", () => {
|
||||
const p = fakeProps();
|
||||
p.wDEnv = { CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.TRUE };
|
||||
const wrapper = mount(<CameraCalibration {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("blur");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import { selectImage } from "../images/actions";
|
|||
import { calibrate, scanImage } from "./actions";
|
||||
import { envGet } from "../weed_detector/remote_env/selectors";
|
||||
import { MustBeOnline, isBotOnline } from "../../devices/must_be_online";
|
||||
import { WeedDetectorConfig } from "../weed_detector/config";
|
||||
import { WeedDetectorConfig, BoolConfig } from "../weed_detector/config";
|
||||
import { Feature } from "../../devices/interfaces";
|
||||
import { namespace } from "../weed_detector";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
@ -16,6 +16,10 @@ import { formatEnvKey } from "../weed_detector/remote_env/translators";
|
|||
import {
|
||||
cameraBtnProps
|
||||
} from "../../devices/components/fbos_settings/camera_selection";
|
||||
import { ImageFlipper } from "../images/image_flipper";
|
||||
import { PhotoFooter } from "../images/photos";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
|
||||
export class CameraCalibration extends
|
||||
React.Component<CameraCalibrationProps, {}> {
|
||||
|
@ -31,9 +35,11 @@ export class CameraCalibration extends
|
|||
key, JSON.stringify(formatEnvKey(key, value))))
|
||||
: envSave(key, value)
|
||||
|
||||
onFlip = (uuid: UUID) => this.props.dispatch(selectImage(uuid));
|
||||
|
||||
render() {
|
||||
const camDisabled = cameraBtnProps(this.props.env);
|
||||
return <div className="weed-detector">
|
||||
return <div className="camera-calibration">
|
||||
<div className="farmware-button">
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
|
@ -50,15 +56,27 @@ export class CameraCalibration extends
|
|||
</div>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<ImageWorkspace
|
||||
{DevSettings.futureFeaturesEnabled() &&
|
||||
<BoolConfig
|
||||
wDEnv={this.props.wDEnv}
|
||||
configKey={this.namespace("easy_calibration")}
|
||||
label={t("Simpler")}
|
||||
onChange={this.saveEnvVar} />}
|
||||
{!!envGet(this.namespace("easy_calibration"), this.props.wDEnv)
|
||||
? <div className={"flipper-section"}>
|
||||
<ImageFlipper
|
||||
onFlip={this.onFlip}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage} />
|
||||
<PhotoFooter
|
||||
image={this.props.currentImage}
|
||||
timeSettings={this.props.timeSettings} />
|
||||
</div>
|
||||
: <ImageWorkspace
|
||||
botOnline={
|
||||
isBotOnline(this.props.syncStatus, this.props.botToMqttStatus)}
|
||||
onProcessPhoto={id => this.props.dispatch(scanImage(id))}
|
||||
onFlip={uuid => this.props.dispatch(selectImage(uuid))}
|
||||
onFlip={this.onFlip}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage}
|
||||
onChange={this.change}
|
||||
|
@ -73,11 +91,10 @@ export class CameraCalibration extends
|
|||
S_HI={this.props.S_HI}
|
||||
V_HI={this.props.V_HI}
|
||||
invertHue={!!envGet(this.namespace("invert_hue_selection"),
|
||||
this.props.wDEnv)} />
|
||||
<WeedDetectorConfig
|
||||
values={this.props.wDEnv}
|
||||
onChange={this.saveEnvVar} />
|
||||
</MustBeOnline>
|
||||
this.props.wDEnv)} />}
|
||||
<WeedDetectorConfig
|
||||
values={this.props.wDEnv}
|
||||
onChange={this.saveEnvVar} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
|
|
@ -31,18 +31,6 @@ describe("<WeedDetectorConfig />", () => {
|
|||
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} />);
|
||||
|
|
|
@ -15,6 +15,9 @@ import { isNumber } from "lodash";
|
|||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
|
||||
getValue(conf: keyof WD_ENV) { return envGet(conf, this.props.values); }
|
||||
get simple() { return !!this.getValue("CAMERA_CALIBRATION_easy_calibration"); }
|
||||
|
||||
NumberBox = ({ conf, label }: {
|
||||
conf: keyof WD_ENV;
|
||||
label: string;
|
||||
|
@ -25,7 +28,7 @@ export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
|
|||
</label>
|
||||
<BlurableInput type="number"
|
||||
id={conf}
|
||||
value={"" + envGet(conf, this.props.values)}
|
||||
value={"" + this.getValue(conf)}
|
||||
onCommit={e =>
|
||||
this.props.onChange(conf, parseFloat(e.currentTarget.value))}
|
||||
placeholder={label} />
|
||||
|
@ -40,57 +43,48 @@ export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
|
|||
}
|
||||
};
|
||||
|
||||
find = (needle: keyof WD_ENV): DropDownItem => {
|
||||
const wow = envGet(needle, this.props.values);
|
||||
const ok = SPECIAL_VALUE_DDI[wow];
|
||||
return ok || NULL_CHOICE;
|
||||
};
|
||||
find = (conf: keyof WD_ENV): DropDownItem =>
|
||||
SPECIAL_VALUE_DDI[this.getValue(conf)] || NULL_CHOICE
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<label htmlFor="invert_hue_selection">
|
||||
{t("Invert Hue Range Selection")}
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="invert_hue_selection"
|
||||
checked={!!envGet("CAMERA_CALIBRATION_invert_hue_selection",
|
||||
this.props.values)}
|
||||
onChange={e =>
|
||||
this.props.onChange("CAMERA_CALIBRATION_invert_hue_selection",
|
||||
e.currentTarget.checked ?
|
||||
SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} />
|
||||
</div>
|
||||
<this.NumberBox
|
||||
conf={"CAMERA_CALIBRATION_calibration_object_separation"}
|
||||
label={t(`Calibration Object Separation`)} />
|
||||
<label>
|
||||
{t(`Calibration Object Separation along axis`)}
|
||||
</label>
|
||||
<FBSelect
|
||||
onChange={this.setDDI("CAMERA_CALIBRATION_calibration_along_axis")}
|
||||
selectedItem={this.find("CAMERA_CALIBRATION_calibration_along_axis")}
|
||||
list={CALIBRATION_DROPDOWNS} />
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
return <div className={"camera-calibration-config"}>
|
||||
{!this.simple &&
|
||||
<div className={"camera-calibration-configs"}>
|
||||
<BoolConfig
|
||||
wDEnv={this.props.values}
|
||||
configKey={"CAMERA_CALIBRATION_invert_hue_selection"}
|
||||
label={t("Invert Hue Range Selection")}
|
||||
onChange={this.props.onChange} />
|
||||
<this.NumberBox
|
||||
conf={"CAMERA_CALIBRATION_camera_offset_x"}
|
||||
label={t(`Camera Offset X`)} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<this.NumberBox
|
||||
conf={"CAMERA_CALIBRATION_camera_offset_y"}
|
||||
label={t(`Camera Offset Y`)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<label htmlFor="image_bot_origin_location">
|
||||
{t(`Origin Location in Image`)}
|
||||
</label>
|
||||
<FBSelect
|
||||
list={ORIGIN_DROPDOWNS}
|
||||
onChange={this.setDDI("CAMERA_CALIBRATION_image_bot_origin_location")}
|
||||
selectedItem={this.find("CAMERA_CALIBRATION_image_bot_origin_location")} />
|
||||
conf={"CAMERA_CALIBRATION_calibration_object_separation"}
|
||||
label={t(`Calibration Object Separation`)} />
|
||||
<label>
|
||||
{t(`Calibration Object Separation along axis`)}
|
||||
</label>
|
||||
<FBSelect
|
||||
onChange={this.setDDI("CAMERA_CALIBRATION_calibration_along_axis")}
|
||||
selectedItem={this.find("CAMERA_CALIBRATION_calibration_along_axis")}
|
||||
list={CALIBRATION_DROPDOWNS} />
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<this.NumberBox
|
||||
conf={"CAMERA_CALIBRATION_camera_offset_x"}
|
||||
label={t(`Camera Offset X`)} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<this.NumberBox
|
||||
conf={"CAMERA_CALIBRATION_camera_offset_y"}
|
||||
label={t(`Camera Offset Y`)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<label htmlFor="image_bot_origin_location">
|
||||
{t(`Origin Location in Image`)}
|
||||
</label>
|
||||
<FBSelect
|
||||
list={ORIGIN_DROPDOWNS}
|
||||
onChange={this.setDDI("CAMERA_CALIBRATION_image_bot_origin_location")}
|
||||
selectedItem={this.find("CAMERA_CALIBRATION_image_bot_origin_location")} />
|
||||
</div>}
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<this.NumberBox
|
||||
|
@ -106,3 +100,25 @@ export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoolConfigProps {
|
||||
configKey: keyof WD_ENV;
|
||||
label: string;
|
||||
wDEnv: Partial<WD_ENV>;
|
||||
onChange(key: keyof WD_ENV, value: number): void;
|
||||
}
|
||||
|
||||
export const BoolConfig = (props: BoolConfigProps) =>
|
||||
<div className="boolean-camera-calibration-config">
|
||||
<label htmlFor={props.configKey}>
|
||||
{t(props.label)}
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={props.configKey}
|
||||
checked={!!envGet(props.configKey, props.wDEnv)}
|
||||
onChange={e =>
|
||||
props.onChange(props.configKey,
|
||||
e.currentTarget.checked ?
|
||||
SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} />
|
||||
</div>;
|
||||
|
|
|
@ -79,29 +79,24 @@ export class WeedDetector
|
|||
</div>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<ImageWorkspace
|
||||
botOnline={
|
||||
isBotOnline(this.props.syncStatus, this.props.botToMqttStatus)}
|
||||
onProcessPhoto={id => this.props.dispatch(scanImage(id))}
|
||||
onFlip={uuid => this.props.dispatch(selectImage(uuid))}
|
||||
currentImage={this.props.currentImage}
|
||||
images={this.props.images}
|
||||
onChange={this.change}
|
||||
timeSettings={this.props.timeSettings}
|
||||
iteration={wDEnvGet(this.namespace("iteration"))}
|
||||
morph={wDEnvGet(this.namespace("morph"))}
|
||||
blur={wDEnvGet(this.namespace("blur"))}
|
||||
H_LO={wDEnvGet(this.namespace("H_LO"))}
|
||||
H_HI={wDEnvGet(this.namespace("H_HI"))}
|
||||
S_LO={wDEnvGet(this.namespace("S_LO"))}
|
||||
S_HI={wDEnvGet(this.namespace("S_HI"))}
|
||||
V_LO={wDEnvGet(this.namespace("V_LO"))}
|
||||
V_HI={wDEnvGet(this.namespace("V_HI"))} />
|
||||
</MustBeOnline>
|
||||
<ImageWorkspace
|
||||
botOnline={
|
||||
isBotOnline(this.props.syncStatus, this.props.botToMqttStatus)}
|
||||
onProcessPhoto={id => this.props.dispatch(scanImage(id))}
|
||||
onFlip={uuid => this.props.dispatch(selectImage(uuid))}
|
||||
currentImage={this.props.currentImage}
|
||||
images={this.props.images}
|
||||
onChange={this.change}
|
||||
timeSettings={this.props.timeSettings}
|
||||
iteration={wDEnvGet(this.namespace("iteration"))}
|
||||
morph={wDEnvGet(this.namespace("morph"))}
|
||||
blur={wDEnvGet(this.namespace("blur"))}
|
||||
H_LO={wDEnvGet(this.namespace("H_LO"))}
|
||||
H_HI={wDEnvGet(this.namespace("H_HI"))}
|
||||
S_LO={wDEnvGet(this.namespace("S_LO"))}
|
||||
S_HI={wDEnvGet(this.namespace("S_HI"))}
|
||||
V_LO={wDEnvGet(this.namespace("V_LO"))}
|
||||
V_HI={wDEnvGet(this.namespace("V_HI"))} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
|
|
@ -40,6 +40,11 @@ describe("formatEnvKey()", () => {
|
|||
v: SPECIAL_VALUES.FALSE,
|
||||
r: "FALSE"
|
||||
},
|
||||
{
|
||||
k: "CAMERA_CALIBRATION_easy_calibration",
|
||||
v: SPECIAL_VALUES.FALSE,
|
||||
r: "FALSE"
|
||||
},
|
||||
{
|
||||
k: "CAMERA_CALIBRATION_calibration_along_axis",
|
||||
v: SPECIAL_VALUES.X,
|
||||
|
|
|
@ -25,6 +25,7 @@ export const WD_KEY_DEFAULTS = {
|
|||
CAMERA_CALIBRATION_calibration_along_axis: SPECIAL_VALUES.X,
|
||||
CAMERA_CALIBRATION_image_bot_origin_location: SPECIAL_VALUES.BOTTOM_LEFT,
|
||||
CAMERA_CALIBRATION_invert_hue_selection: SPECIAL_VALUES.TRUE,
|
||||
CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.FALSE,
|
||||
CAMERA_CALIBRATION_blur: 5,
|
||||
CAMERA_CALIBRATION_calibration_object_separation: 100,
|
||||
CAMERA_CALIBRATION_camera_offset_x: 50,
|
||||
|
@ -61,6 +62,7 @@ export const DEFAULT_FORMATTER: Translation = {
|
|||
case "CAMERA_CALIBRATION_calibration_along_axis":
|
||||
case "CAMERA_CALIBRATION_image_bot_origin_location":
|
||||
case "CAMERA_CALIBRATION_invert_hue_selection":
|
||||
case "CAMERA_CALIBRATION_easy_calibration":
|
||||
return ("" + (SPECIAL_VALUES[val] || val));
|
||||
default:
|
||||
return val;
|
||||
|
|
|
@ -24,24 +24,10 @@ describe("mapStateToProps()", () => {
|
|||
state.bot.hardware.configuration.sequence_init_log = false;
|
||||
const fakeApiConfig = fakeFbosConfig();
|
||||
fakeApiConfig.body.sequence_init_log = true;
|
||||
fakeApiConfig.body.api_migrated = true;
|
||||
state.resources = buildResourceIndex([fakeApiConfig]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.sourceFbosConfig("sequence_init_log")).toEqual({
|
||||
value: true, consistent: false
|
||||
});
|
||||
});
|
||||
|
||||
it("bot source of FBOS settings", () => {
|
||||
const state = fakeState();
|
||||
state.bot.hardware.configuration.sequence_init_log = false;
|
||||
const fakeApiConfig = fakeFbosConfig();
|
||||
fakeApiConfig.body.sequence_init_log = true;
|
||||
fakeApiConfig.body.api_migrated = false;
|
||||
state.resources = buildResourceIndex([fakeApiConfig]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.sourceFbosConfig("sequence_init_log")).toEqual({
|
||||
value: false, consistent: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@ describe("mapStateToProps()", () => {
|
|||
it("returns firmware value", () => {
|
||||
const state = fakeState();
|
||||
const fbosConfig = fakeFbosConfig();
|
||||
fbosConfig.body.api_migrated = true;
|
||||
fbosConfig.body.firmware_hardware = "arduino";
|
||||
state.resources = buildResourceIndex([fbosConfig]);
|
||||
const props = mapStateToProps(state);
|
||||
|
|
|
@ -28,7 +28,7 @@ describe("<NavLinks />", () => {
|
|||
});
|
||||
|
||||
it("shows links", () => {
|
||||
mockDev = true;
|
||||
mockDev = false;
|
||||
const wrapper = mount(<NavLinks close={jest.fn()} alertCount={1} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("tools");
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ export const getLinks = (): NavLinkParams[] => betterCompact([
|
|||
name: "Regimens", icon: "calendar-check-o", slug: "regimens",
|
||||
computeHref: computeEditorUrlFromState("Regimen")
|
||||
},
|
||||
DevSettings.futureFeaturesEnabled() ? undefined :
|
||||
!DevSettings.futureFeaturesEnabled() ? undefined :
|
||||
{ name: "Tools", icon: "wrench", slug: "tools" },
|
||||
{
|
||||
name: "Farmware", icon: "crosshairs", slug: "farmware",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { info } from "../toast/toast";
|
||||
import { semverCompare, SemverResult, MinVersionOverride } from "../util";
|
||||
import { semverCompare, SemverResult, FbosVersionFallback } from "../util";
|
||||
import { Content } from "../constants";
|
||||
import { Dictionary } from "lodash";
|
||||
|
||||
const IDEAL_VERSION =
|
||||
globalConfig.FBOS_END_OF_LIFE_VERSION || MinVersionOverride.ALWAYS;
|
||||
globalConfig.FBOS_END_OF_LIFE_VERSION || FbosVersionFallback.NULL;
|
||||
|
||||
/** Returns a function that, when given a version string, (possibly) warns the
|
||||
* user to upgrade FBOS versions before it hits end of life. */
|
||||
|
@ -12,8 +12,8 @@ export function createReminderFn() {
|
|||
/** FBOS Version can change during the app lifecycle. We only want one
|
||||
* reminder per FBOS version change. */
|
||||
const alreadyChecked: Dictionary<boolean | undefined> = {
|
||||
// Dont bother when the user is offline.
|
||||
[MinVersionOverride.ALWAYS]: true
|
||||
// Don't bother when the user is offline.
|
||||
[FbosVersionFallback.NULL]: true
|
||||
};
|
||||
|
||||
return function reminder(version: string) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { EnvName } from "./interfaces";
|
||||
import { determineInstalledOsVersion, MinVersionOverride } from "../util/index";
|
||||
import { determineInstalledOsVersion, FbosVersionFallback } from "../util/index";
|
||||
import { maybeGetDevice } from "../resources/selectors";
|
||||
import { MW } from "./middlewares";
|
||||
import { Everything } from "../interfaces";
|
||||
|
@ -11,10 +11,10 @@ const maybeRemindUserToUpdate = createReminderFn();
|
|||
|
||||
function getVersionFromState(state: Everything) {
|
||||
const device = maybeGetDevice(state.resources.index);
|
||||
const v =
|
||||
determineInstalledOsVersion(state.bot, device) || MinVersionOverride.ALWAYS;
|
||||
maybeRemindUserToUpdate(v);
|
||||
return v;
|
||||
const version = determineInstalledOsVersion(state.bot, device)
|
||||
|| FbosVersionFallback.NULL;
|
||||
maybeRemindUserToUpdate(version);
|
||||
return version;
|
||||
}
|
||||
|
||||
const fn: MW =
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
createShouldDisplayFn,
|
||||
determineInstalledOsVersion,
|
||||
versionOK,
|
||||
MinVersionOverride,
|
||||
} from "../version";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { fakeDevice } from "../../__test_support__/resource_index_builder";
|
||||
|
@ -128,6 +129,10 @@ describe("shouldDisplay()", () => {
|
|||
expect(createShouldDisplayFn("10.0.0",
|
||||
{ jest_feature: "1.0.0" }, undefined)(
|
||||
Feature.jest_feature)).toBeTruthy();
|
||||
globalConfig.FBOS_END_OF_LIFE_VERSION = MinVersionOverride.NEVER;
|
||||
expect(createShouldDisplayFn(undefined, fakeMinOsData, undefined)(
|
||||
Feature.jest_feature)).toBeTruthy();
|
||||
delete globalConfig.FBOS_END_OF_LIFE_VERSION;
|
||||
});
|
||||
|
||||
it("shouldn't display", () => {
|
||||
|
@ -179,14 +184,20 @@ describe("determineInstalledOsVersion()", () => {
|
|||
|
||||
describe("versionOK()", () => {
|
||||
it("checks if major/minor version meets min requirement", () => {
|
||||
expect(versionOK("9.1.9-rc99", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("3.0.9-rc99", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("4.0.0", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("4.0.0", 3, 1)).toBeTruthy();
|
||||
expect(versionOK("3.1.0", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("2.0.-", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("2.9.4", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("1.9.6", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("3.1.6", 4, 0)).toBeFalsy();
|
||||
globalConfig.MINIMUM_FBOS_VERSION = "3.0.0";
|
||||
expect(versionOK("9.1.9-rc99")).toBeTruthy();
|
||||
expect(versionOK("3.0.9-rc99")).toBeTruthy();
|
||||
expect(versionOK("4.0.0")).toBeTruthy();
|
||||
expect(versionOK("3.1.0")).toBeTruthy();
|
||||
expect(versionOK("2.0.-")).toBeFalsy();
|
||||
expect(versionOK("2.9.4")).toBeFalsy();
|
||||
expect(versionOK("1.9.6")).toBeFalsy();
|
||||
globalConfig.MINIMUM_FBOS_VERSION = "3.1.0";
|
||||
expect(versionOK("4.0.0")).toBeTruthy();
|
||||
globalConfig.MINIMUM_FBOS_VERSION = "4.0.0";
|
||||
expect(versionOK("3.1.6")).toBeFalsy();
|
||||
delete globalConfig.MINIMUM_FBOS_VERSION;
|
||||
expect(versionOK("5.0.0")).toBeFalsy();
|
||||
expect(versionOK("7.0.0")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -183,9 +183,7 @@ export function validBotLocationData(
|
|||
*/
|
||||
export function validFwConfig(config: TaggedFirmwareConfig | undefined):
|
||||
TaggedFirmwareConfig["body"] | undefined {
|
||||
return (config?.body.api_migrated)
|
||||
? config.body
|
||||
: undefined;
|
||||
return config ? config.body : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -193,9 +191,7 @@ export function validFwConfig(config: TaggedFirmwareConfig | undefined):
|
|||
*/
|
||||
export function validFbosConfig(
|
||||
config: TaggedFbosConfig | undefined): TaggedFbosConfig["body"] | undefined {
|
||||
return (config?.body.api_migrated)
|
||||
? config.body
|
||||
: undefined;
|
||||
return config ? config.body : undefined;
|
||||
}
|
||||
|
||||
interface BetterUUID {
|
||||
|
|
|
@ -97,10 +97,13 @@ export function minFwVersionCheck(current: string | undefined, min: string) {
|
|||
* for shouldDisplay()
|
||||
*/
|
||||
export enum MinVersionOverride {
|
||||
ALWAYS = "0.0.0",
|
||||
NEVER = "999.999.999",
|
||||
}
|
||||
|
||||
export enum FbosVersionFallback {
|
||||
NULL = "0.0.0",
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a feature should be displayed based on
|
||||
* the user's current FBOS version. Min FBOS version feature data is pulled
|
||||
|
@ -114,19 +117,18 @@ export function createShouldDisplayFn(
|
|||
lookupData: MinOsFeatureLookup | undefined,
|
||||
override: string | undefined) {
|
||||
return function (feature: Feature): boolean {
|
||||
const target = override || current;
|
||||
if (isString(target)) {
|
||||
const table = lookupData || {};
|
||||
const min = table[feature] || MinVersionOverride.NEVER;
|
||||
switch (semverCompare(target, min)) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
const fallback = globalConfig.FBOS_END_OF_LIFE_VERSION ||
|
||||
FbosVersionFallback.NULL;
|
||||
const target = override || current || fallback;
|
||||
const table = lookupData || {};
|
||||
const min = table[feature] || MinVersionOverride.NEVER;
|
||||
switch (semverCompare(target, min)) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -147,6 +149,9 @@ export function determineInstalledOsVersion(
|
|||
}
|
||||
}
|
||||
|
||||
const parseVersion = (version: string) =>
|
||||
version.split(".").map(x => parseInt(x, 10));
|
||||
|
||||
/**
|
||||
* Compare installed FBOS version against the lowest version compatible
|
||||
* with the web app to lock out incompatible FBOS versions from the App.
|
||||
|
@ -155,20 +160,16 @@ export function determineInstalledOsVersion(
|
|||
* identifiers.
|
||||
*
|
||||
* @param stringyVersion version string to check ("0.0.0")
|
||||
* @param _EXPECTED_MAJOR minimum required major version number
|
||||
* @param _EXPECTED_MINOR minimum required minor version number
|
||||
*/
|
||||
export function versionOK(stringyVersion = "0.0.0",
|
||||
_EXPECTED_MAJOR: number,
|
||||
_EXPECTED_MINOR: number) {
|
||||
const [actual_major, actual_minor] = stringyVersion
|
||||
.split(".")
|
||||
.map(x => parseInt(x, 10));
|
||||
if (actual_major > _EXPECTED_MAJOR) {
|
||||
export function versionOK(stringyVersion = "0.0.0") {
|
||||
const [actual_major, actual_minor] = parseVersion(stringyVersion);
|
||||
const [EXPECTED_MAJOR, EXPECTED_MINOR] =
|
||||
parseVersion(globalConfig.MINIMUM_FBOS_VERSION || "6.0.0");
|
||||
if (actual_major > EXPECTED_MAJOR) {
|
||||
return true;
|
||||
} else {
|
||||
const majorOK = (actual_major == _EXPECTED_MAJOR);
|
||||
const minorOK = (actual_minor >= _EXPECTED_MINOR);
|
||||
const majorOK = (actual_major == EXPECTED_MAJOR);
|
||||
const minorOK = (actual_minor >= EXPECTED_MINOR);
|
||||
return (majorOK && minorOK);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,6 +101,14 @@ describe Api::DevicesController do
|
|||
device.tool_slots.order(id: :asc)[5]
|
||||
end
|
||||
|
||||
def tool_slots_slot_7?(device)
|
||||
device.tool_slots.order(id: :asc)[6]
|
||||
end
|
||||
|
||||
def tool_slots_slot_8?(device)
|
||||
device.tool_slots.order(id: :asc)[7]
|
||||
end
|
||||
|
||||
def tools_seed_bin?(device)
|
||||
device.tools.find_by(name: "Seed Bin")
|
||||
end
|
||||
|
@ -117,10 +125,6 @@ describe Api::DevicesController do
|
|||
device.tools.find_by(name: "Seed Trough 2")
|
||||
end
|
||||
|
||||
def tools_seed_trough_3?(device)
|
||||
device.tools.find_by(name: "Seed Trough 3")
|
||||
end
|
||||
|
||||
def tools_seeder?(device)
|
||||
device.tools.find_by(name: "Seeder")
|
||||
end
|
||||
|
@ -230,17 +234,20 @@ describe Api::DevicesController do
|
|||
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
|
||||
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
|
||||
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
|
||||
expect(tool_slots_slot_7?(device)).to_not be
|
||||
expect(tool_slots_slot_8?(device)).to_not be
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
|
||||
check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray")
|
||||
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
|
||||
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
|
||||
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
|
||||
|
||||
expect(tools_seed_bin?(device)).to be
|
||||
expect(tools_seed_tray?(device)).to be
|
||||
expect(tools_seed_trough_1?(device)).to_not be
|
||||
expect(tools_seed_trough_2?(device)).to_not be
|
||||
expect(tools_seed_trough_3?(device)).to_not be
|
||||
expect(tools_seeder?(device)).to be_kind_of(Tool)
|
||||
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
|
||||
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
|
||||
|
@ -280,6 +287,8 @@ describe Api::DevicesController do
|
|||
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
|
||||
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
|
||||
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
|
||||
expect(tool_slots_slot_7?(device)).to_not be
|
||||
expect(tool_slots_slot_8?(device)).to_not be
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
|
||||
|
@ -287,11 +296,11 @@ describe Api::DevicesController do
|
|||
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
|
||||
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
|
||||
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
|
||||
|
||||
expect(tools_seed_bin?(device)).to be
|
||||
expect(tools_seed_tray?(device)).to be
|
||||
expect(tools_seed_trough_1?(device)).to_not be
|
||||
expect(tools_seed_trough_2?(device)).to_not be
|
||||
expect(tools_seed_trough_3?(device)).to_not be
|
||||
expect(tools_seeder?(device)).to be_kind_of(Tool)
|
||||
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
|
||||
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
|
||||
|
@ -331,11 +340,20 @@ describe Api::DevicesController do
|
|||
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
|
||||
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
|
||||
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
|
||||
expect(tool_slots_slot_7?(device)).to_not be
|
||||
expect(tool_slots_slot_8?(device)).to_not be
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
|
||||
check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray")
|
||||
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
|
||||
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
|
||||
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
|
||||
|
||||
expect(tools_seed_bin?(device)).to be
|
||||
expect(tools_seed_tray?(device)).to be
|
||||
expect(tools_seed_trough_1?(device)).to_not be
|
||||
expect(tools_seed_trough_2?(device)).to_not be
|
||||
expect(tools_seed_trough_3?(device)).to_not be
|
||||
expect(tools_seeder?(device)).to be_kind_of(Tool)
|
||||
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
|
||||
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
|
||||
|
@ -375,11 +393,22 @@ describe Api::DevicesController do
|
|||
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
|
||||
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
|
||||
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
|
||||
expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1")
|
||||
expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2")
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
|
||||
check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray")
|
||||
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
|
||||
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
|
||||
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
|
||||
check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1")
|
||||
check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2")
|
||||
|
||||
expect(tools_seed_bin?(device)).to be
|
||||
expect(tools_seed_tray?(device)).to be
|
||||
expect(tools_seed_trough_1?(device)).to_not be
|
||||
expect(tools_seed_trough_2?(device)).to_not be
|
||||
expect(tools_seed_trough_3?(device)).to_not be
|
||||
expect(tools_seed_trough_1?(device)).to be
|
||||
expect(tools_seed_trough_2?(device)).to be
|
||||
expect(tools_seeder?(device)).to be_kind_of(Tool)
|
||||
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
|
||||
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
|
||||
|
@ -419,6 +448,8 @@ describe Api::DevicesController do
|
|||
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
|
||||
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
|
||||
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
|
||||
expect(tool_slots_slot_7?(device)).to_not be
|
||||
expect(tool_slots_slot_8?(device)).to_not be
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
|
||||
|
@ -431,7 +462,6 @@ describe Api::DevicesController do
|
|||
expect(tools_seed_tray?(device)).to be
|
||||
expect(tools_seed_trough_1?(device)).to_not be
|
||||
expect(tools_seed_trough_2?(device)).to_not be
|
||||
expect(tools_seed_trough_3?(device)).to_not be
|
||||
expect(tools_seeder?(device)).to be_kind_of(Tool)
|
||||
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
|
||||
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
|
||||
|
@ -471,6 +501,8 @@ describe Api::DevicesController do
|
|||
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
|
||||
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
|
||||
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
|
||||
expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1")
|
||||
expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2")
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
|
||||
|
@ -478,12 +510,13 @@ describe Api::DevicesController do
|
|||
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
|
||||
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
|
||||
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
|
||||
check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1")
|
||||
check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2")
|
||||
|
||||
expect(tools_seed_bin?(device)).to be
|
||||
expect(tools_seed_tray?(device)).to be
|
||||
expect(tools_seed_trough_1?(device)).to_not be
|
||||
expect(tools_seed_trough_2?(device)).to_not be
|
||||
expect(tools_seed_trough_3?(device)).to_not be
|
||||
expect(tools_seed_trough_1?(device)).to be
|
||||
expect(tools_seed_trough_2?(device)).to be
|
||||
expect(tools_seeder?(device)).to be_kind_of(Tool)
|
||||
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
|
||||
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
|
||||
|
@ -519,18 +552,20 @@ describe Api::DevicesController do
|
|||
expect(settings_hide_sensors?(device)).to be(true)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2")
|
||||
expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3")
|
||||
expect(tool_slots_slot_3?(device)).to_not be
|
||||
expect(tool_slots_slot_4?(device)).to_not be
|
||||
expect(tool_slots_slot_5?(device)).to_not be
|
||||
expect(tool_slots_slot_6?(device)).to_not be
|
||||
expect(tool_slots_slot_7?(device)).to_not be
|
||||
expect(tool_slots_slot_8?(device)).to_not be
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2")
|
||||
check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3")
|
||||
|
||||
expect(tools_seed_bin?(device)).to_not be
|
||||
expect(tools_seed_tray?(device)).to_not be
|
||||
expect(tools_seed_trough_1?(device)).to be
|
||||
expect(tools_seed_trough_2?(device)).to be
|
||||
expect(tools_seed_trough_3?(device)).to be
|
||||
expect(tools_seeder?(device)).to_not be
|
||||
expect(tools_soil_sensor?(device)).to_not be
|
||||
expect(tools_watering_nozzle?(device)).to_not be
|
||||
|
@ -566,18 +601,20 @@ describe Api::DevicesController do
|
|||
expect(settings_hide_sensors?(device)).to be(true)
|
||||
expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1")
|
||||
expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2")
|
||||
expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3")
|
||||
expect(tool_slots_slot_3?(device)).to_not be
|
||||
expect(tool_slots_slot_4?(device)).to_not be
|
||||
expect(tool_slots_slot_5?(device)).to_not be
|
||||
expect(tool_slots_slot_6?(device)).to_not be
|
||||
expect(tool_slots_slot_7?(device)).to_not be
|
||||
expect(tool_slots_slot_8?(device)).to_not be
|
||||
|
||||
check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1")
|
||||
check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2")
|
||||
check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3")
|
||||
|
||||
expect(tools_seed_bin?(device)).to_not be
|
||||
expect(tools_seed_tray?(device)).to_not be
|
||||
expect(tools_seed_trough_1?(device)).to be
|
||||
expect(tools_seed_trough_2?(device)).to be
|
||||
expect(tools_seed_trough_3?(device)).to be
|
||||
expect(tools_seeder?(device)).to_not be
|
||||
expect(tools_soil_sensor?(device)).to_not be
|
||||
expect(tools_watering_nozzle?(device)).to_not be
|
||||
|
|
Loading…
Reference in New Issue