Merge pull request #1723 from FarmBot/staging

v9.2.0 - Jolly Juniper
hmm v9.2.0
Rick Carlino 2020-02-27 15:37:03 -06:00 committed by GitHub
commit bce0700cd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
176 changed files with 1906 additions and 2023 deletions

View File

@ -72,7 +72,7 @@ GEM
amq-protocol (2.3.0)
bcrypt (3.1.13)
builder (3.2.4)
bunny (2.14.3)
bunny (2.14.4)
amq-protocol (~> 2.3, >= 2.3.0)
case_transform (0.2)
activesupport
@ -82,9 +82,9 @@ GEM
simplecov
url
coderay (1.1.2)
concurrent-ruby (1.1.5)
concurrent-ruby (1.1.6)
crass (1.0.6)
database_cleaner (1.7.0)
database_cleaner (1.8.3)
declarative (0.0.10)
declarative-option (0.1.0)
delayed_job (4.1.8)
@ -100,7 +100,7 @@ GEM
warden (~> 1.2.3)
diff-lcs (1.3)
digest-crc (0.4.1)
discard (1.1.0)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
erubi (1.9.0)
@ -109,7 +109,7 @@ GEM
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.10.1)
faker (2.10.2)
i18n (>= 1.6, < 2)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
@ -119,7 +119,7 @@ GEM
railties (>= 3.2, < 6.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.36.4)
google-api-client (0.37.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@ -127,10 +127,12 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.4.1)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
@ -174,7 +176,7 @@ GEM
mimemagic (~> 0.3.2)
memoist (0.16.2)
method_source (0.9.2)
mimemagic (0.3.3)
mimemagic (0.3.4)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
@ -183,7 +185,7 @@ GEM
mutations (0.9.0)
activesupport
nio4r (2.5.2)
nokogiri (1.10.7)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0)
os (1.0.1)
@ -202,7 +204,7 @@ GEM
faraday_middleware (~> 0.13.0)
hashie (~> 3.6)
multi_json (~> 1.13.1)
rack (2.1.1)
rack (2.2.2)
rack-attack (6.2.2)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
@ -252,7 +254,7 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
retriable (3.1.2)
rollbar (2.23.2)
rollbar (2.24.0)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
@ -276,7 +278,7 @@ GEM
rspec-support (3.9.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
scenic (1.5.1)
scenic (1.5.2)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
secure_headers (6.3.0)
@ -285,11 +287,10 @@ GEM
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.17.1)
simplecov (0.18.5)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
simplecov-html (~> 0.11)
simplecov-html (0.12.1)
sprockets (4.0.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)

View File

@ -6,7 +6,7 @@ class InUsePoint < ApplicationRecord
DEFAULT_NAME = "point"
FANCY_NAMES = {
GenericPointer.name => DEFAULT_NAME,
ToolSlot.name => "tool slot",
ToolSlot.name => "slot",
Plant.name => "plant",
}

View File

@ -4,7 +4,7 @@ class PointGroup < ApplicationRecord
BAD_SORT = "%{value} is not valid. Valid options are: " +
SORT_TYPES.map(&:inspect).join(", ")
DEFAULT_CRITERIA = {
day: { op: "<", days: 0 },
day: { op: "<", days_ago: 0 },
string_eq: {},
number_eq: {},
number_lt: {},

View File

@ -11,7 +11,7 @@ class ToolSlot < Point
MIN_PULLOUT = PULLOUT_DIRECTIONS.min
PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\
"%{value} is not valid."
IN_USE = "already in use by another tool slot. "\
IN_USE = "already in use by another slot. "\
"Please un-assign the tool from its current slot"\
" before reassigning."

View File

@ -5,7 +5,7 @@ module PointGroups
hash :criteria do
hash(:day) do
string :op, in: [">", "<"]
integer :days
integer :days_ago
end
hash(:string_eq) { array :*, class: String }
hash(:number_eq) { array :*, class: Integer }

View File

@ -1,9 +1,9 @@
module Tools
class Destroy < Mutations::Command
STILL_IN_USE = "Can't delete tool because the following sequences are " \
"still using it: %s"
STILL_IN_SLOT = "Can't delete tool because it is still in a tool slot. " \
"Please remove it from the tool slot first."
STILL_IN_USE = "Can't delete tool or seed container because the " \
"following sequences are still using it: %s"
STILL_IN_SLOT = "Can't delete tool or seed container because it is " \
"still in a slot. Please remove it from the slot first."
required do
model :tool, class: Tool

View File

@ -460,7 +460,7 @@ export function fakePointGroup(): TaggedPointGroup {
sort_type: "xy_ascending",
point_ids: [],
criteria: {
day: { op: "<", days: 0 },
day: { op: "<", days_ago: 0 },
number_eq: {},
number_gt: {},
number_lt: {},

View File

@ -30,7 +30,6 @@ import "../regimens/editor/interfaces";
import "../regimens/interfaces";
import "../resources/interfaces";
import "../sequences/interfaces";
import "../tools/interfaces";
describe("interfaces", () => {
it("cant explain why coverage is 0 for interface files", () => {

View File

@ -648,8 +648,8 @@ export namespace Content {
trim(`Restart the Farmduino or Arduino firmware.`);
export const OS_AUTO_UPDATE =
trim(`When enabled, FarmBot OS will periodically check for, download,
and install updates automatically.`);
trim(`When enabled, FarmBot OS will automatically download and install
software updates at the chosen time.`);
export const AUTO_SYNC =
trim(`When enabled, device resources such as sequences and regimens
@ -663,7 +663,7 @@ export namespace Content {
back on, unplug FarmBot and plug it back in.`);
export const OS_BETA_RELEASES =
trim(`Warning! Opting in to FarmBot OS beta releases may reduce
trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
FarmBot system stability. Are you sure?`);
export const DIAGNOSTIC_CHECK =
@ -897,16 +897,16 @@ export namespace TourContent {
export const ADD_TOOLS_AND_SLOTS =
trim(`Press the + button to add tools and seed containers. Then create
tool slots for them to by pressing the tool slot + button.`);
slots for them to by pressing the slot + button.`);
export const ADD_SEED_CONTAINERS_AND_SLOTS =
trim(`Press the + button to add seed containers. Then create
slots for them to by pressing the seed container slot + button.`);
slots for them to by pressing the slot + button.`);
export const ADD_TOOLS_SLOTS =
trim(`Add the newly created tools and seed containers to the
corresponding tool slots on FarmBot:
press the + button to create a tool slot.`);
corresponding slots on FarmBot:
press the + button to create a slot.`);
export const ADD_PERIPHERALS =
trim(`Press edit and then the + button to add peripherals.`);
@ -998,7 +998,7 @@ export enum DeviceSetting {
pinGuard = `Pin Guard`,
// Danger Zone
dangerZone = `dangerZone`,
dangerZone = `Danger Zone`,
resetHardwareParams = `Reset hardware parameter defaults`,
// Pin Bindings
@ -1009,7 +1009,8 @@ export enum DeviceSetting {
timezone = `timezone`,
camera = `camera`,
firmware = `firmware`,
farmbotOSAutoUpdate = `Farmbot OS Auto Update`,
applySoftwareUpdates = `update time`,
farmbotOSAutoUpdate = `auto update`,
farmbotOS = `Farmbot OS`,
autoSync = `Auto Sync`,
bootSequence = `Boot Sequence`,

View File

@ -10,6 +10,7 @@ import { Move } from "./move/move";
import { BooleanSetting } from "../session_keys";
import { SensorReadings } from "./sensor_readings/sensor_readings";
import { isBotOnline } from "../devices/must_be_online";
import { hasSensors } from "../devices/components/firmware_hardware_support";
/** Controls page. */
export class RawControls extends React.Component<Props, {}> {
@ -24,7 +25,8 @@ export class RawControls extends React.Component<Props, {}> {
}
get hideSensors() {
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors);
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors)
|| !hasSensors(this.props.firmwareHardware);
}
move = () => <Move

View File

@ -7,7 +7,7 @@ import { AxisInputBoxGroup } from "../axis_input_box_group";
import { GetWebAppBool } from "./interfaces";
import { BooleanSetting } from "../../session_keys";
import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
import { FirmwareHardware } from "farmbot";
export interface BotPositionRowsProps {
@ -34,12 +34,12 @@ export const BotPositionRows = (props: BotPositionRowsProps) => {
<AxisDisplayGroup
position={locationData.position}
label={t("Motor Coordinates (mm)")} />
{!isExpressBoard(props.firmwareHardware) &&
{hasEncoders(props.firmwareHardware) &&
getValue(BooleanSetting.scaled_encoders) &&
<AxisDisplayGroup
position={locationData.scaled_encoders}
label={t("Scaled Encoder (mm)")} />}
{!isExpressBoard(props.firmwareHardware) &&
{hasEncoders(props.firmwareHardware) &&
getValue(BooleanSetting.raw_encoders) &&
<AxisDisplayGroup
position={locationData.raw_encoders}

View File

@ -6,7 +6,7 @@ import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { DevSettings } from "../../account/dev/dev_support";
import { t } from "../../i18next_wrapper";
import { FirmwareHardware } from "farmbot";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
export const moveWidgetSetting =
(toggle: ToggleWebAppBool, getValue: GetWebAppBool) =>
@ -36,7 +36,7 @@ export const MoveWidgetSettingsMenu = (
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
{!isExpressBoard(firmwareHardware) &&
{hasEncoders(firmwareHardware) &&
<div className="display-encoder-data">
<p>{t("Display Encoder Data")}</p>
<Setting

View File

@ -88,4 +88,14 @@ describe("<Peripherals />", () => {
clickButton(wrapper, 3, "stock");
expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
});
it("hides stock button", () => {
const p = fakeProps();
p.firmwareHardware = "none";
const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true });
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
});
});

View File

@ -108,7 +108,7 @@ export class Peripherals
<i className="fa fa-plus" />
</button>
<button
hidden={!isEditing}
hidden={!isEditing || this.props.firmwareHardware == "none"}
className="fb-button green"
type="button"
onClick={() => this.stockPeripherals.map(p =>

View File

@ -72,6 +72,7 @@ describe("<Sensors />", () => {
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "stock sensors");
expect(wrapper.find("button").at(3).props().hidden).toBeFalsy();
expect(p.dispatch).toHaveBeenCalledTimes(2);
});
@ -79,6 +80,18 @@ describe("<Sensors />", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
});
it("hides stock button", () => {
const p = fakeProps();
p.firmwareHardware = "none";
const wrapper = mount(<Sensors {...p} />);
wrapper.setState({ isEditing: true });
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
});
});

View File

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

View File

@ -30,6 +30,9 @@
padding: 35rem 2rem 2rem 2rem; // at zoom = 1.0: 350px 20px 20px 20px
}
transition: 0.2s ease;
&::-webkit-scrollbar {
display: none;
}
}
.drop-area {

View File

@ -552,8 +552,12 @@
}
.tool-slots-panel-content,
.tools-panel-content {
max-height: calc(100vh - 19rem);
overflow-y: auto;
overflow-x: hidden;
.tool-search-item,
.tool-slot-search-item {
line-height: 4rem;
cursor: pointer;
margin-left: -15px;
margin-right: -15px;
@ -562,11 +566,32 @@
margin-right: 0;
}
p {
line-height: 3rem;
font-size: 1.2rem;
line-height: 4rem;
&.tool-status,
&.tool-slot-position {
float: right;
}
}
.filter-search {
.bp3-button {
min-height: 2.5rem;
max-height: 2.5rem;
span {
line-height: 1.5rem;
}
}
i {
line-height: 2rem;
}
}
svg {
vertical-align: middle;
}
.tool-slot-position-info {
padding: 0;
padding-right: 1rem;
}
}
.mounted-tool-header {
display: flex;
@ -624,6 +649,13 @@
float: left;
}
}
svg {
display: block;
margin: auto;
width: 10rem;
height: 10rem;
margin-top: 2rem;
}
.add-stock-tools {
.filter-search {
margin-bottom: 1rem;
@ -634,6 +666,25 @@
ul {
font-size: 1.2rem;
padding-left: 1rem;
li {
margin-top: 0.5rem;
line-height: 2rem;
cursor: pointer;
width: 50%;
&:hover {
font-weight: bold;
}
.fb-checkbox {
display: inline;
}
p {
display: inline;
line-height: 2.25rem;
font-size: 1.2rem;
vertical-align: top;
margin-left: 1rem;
}
}
}
button {
.fa-plus {
@ -645,6 +696,13 @@
.add-tool-slot-panel-content,
.edit-tool-slot-panel-content {
svg {
display: block;
margin: auto;
width: 10rem;
height: 10rem;
margin-top: 2rem;
}
label {
margin-top: 0 !important;
}
@ -657,12 +715,24 @@
.direction-icon {
margin-left: 1rem;
}
.use-current-location-input {
.help-icon {
color: $dark_gray;
}
.tool-slot-location-input {
.axis-inputs {
padding-left: 0;
}
.use-current-location {
padding: 0;
margin-left: -1rem;
}
button {
margin: 0;
float: none;
margin-left: 1rem;
vertical-align: middle;
margin-top: 0.5rem;
margin-right: 0.5rem;
height: 2.5rem;
.fa {
font-size: 1.5rem;
}
}
}
.gantry-mounted-input {
@ -888,3 +958,10 @@
margin-right: 1.5rem;
&:hover { color: $white; }
}
.desktop-hide {
display: none !important;
@media screen and (max-width: 1075px) {
display: block !important;
}
}

View File

@ -226,7 +226,7 @@ fieldset {
.percent-bar {
position: absolute;
top: 2px;
left: 12rem;
right: 0;
height: 1rem;
width: 25%;
clip-path: polygon(0 85%, 100% 0, 100% 100%, 0% 100%);
@ -407,6 +407,18 @@ a {
}
}
.load-progress-bar-wrapper {
position: absolute;
top: 3.2rem;
bottom: 0;
right: 0;
width: 100%;
height: 1px;
.load-progress-bar {
height: 100%;
}
}
.firmware-setting-export-menu {
button {
margin-bottom: 1rem;
@ -1543,16 +1555,21 @@ textarea:focus {
cursor: pointer;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
border: 2px solid $panel_light_blue;
border: 2px solid darken($panel_light_blue, 30%);
border-radius: 5px;
&:hover, &.selected {
border: 2px solid $medium_gray;
border-radius: 2px;
.sort-path-info-bar {
background: darken($light_gray, 10%);
background: darken($panel_light_blue, 40%);
}
}
&:hover {
border: 2px solid darken($panel_light_blue, 40%);
}
&.selected {
border: 2px solid $medium_gray;
}
.sort-path-info-bar {
background: $light_gray;
background: darken($panel_light_blue, 30%);
font-size: 1.2rem;
padding-left: 0.5rem;
white-space: nowrap;
@ -1649,3 +1666,9 @@ textarea:focus {
background-color: transparent;
box-shadow: none;
}
.read-only-icon {
margin: 9px 0px 0px 9px;
float: right;
box-sizing: inherit;
}

View File

@ -154,4 +154,12 @@ select {
}
}
}
&.disabled {
input[type="checkbox"] {
cursor: not-allowed;
&:checked:after {
border-color: $gray;
}
}
}
}

View File

@ -322,6 +322,9 @@
border-left: 4px solid transparent;
&.active {
border-left: 4px solid $dark_gray;
p {
font-weight: bold;
}
}
.fa-chevron-down, .fa-chevron-right {
position: absolute;
@ -330,11 +333,11 @@
font-size: 1.1rem;
}
.folder-settings-icon,
.fa-bars {
.fa-arrows-v {
position: absolute;
right: 0;
}
.fa-bars, .fa-ellipsis-v {
.fa-arrows-v, .fa-ellipsis-v {
display: none;
}
.fa-ellipsis-v {
@ -342,8 +345,14 @@
display: block;
}
}
@media screen and (max-width: 450px) {
.fa-arrows-v, .fa-ellipsis-v {
display: block;
margin-right: 0.5rem;
}
}
&:hover {
.fa-bars, .fa-ellipsis-v {
.fa-arrows-v, .fa-ellipsis-v {
display: block;
}
}
@ -367,7 +376,7 @@
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.2rem;
font-weight: bold;
font-weight: normal;
width: 75%;
padding: 0.5rem;
padding-left: 0;

View File

@ -353,9 +353,10 @@ describe("fetchReleases()", () => {
it("fails to fetches latest OS release version", async () => {
mockGetRelease = Promise.reject("error");
const dispatch = jest.fn();
console.error = jest.fn();
await actions.fetchReleases("url")(dispatch);
await expect(axios.get).toHaveBeenCalledWith("url");
expect(error).toHaveBeenCalledWith(
expect(console.error).toHaveBeenCalledWith(
"Could not download FarmBot OS update information.");
expect(dispatch).toHaveBeenCalledWith({
payload: "error",

View File

@ -212,7 +212,7 @@ export const fetchReleases =
})
.catch((ferror) => {
!options.beta &&
error(t("Could not download FarmBot OS update information."));
console.error(t("Could not download FarmBot OS update information."));
dispatch({
type: options.beta
? "FETCH_BETA_OS_UPDATE_INFO_ERROR"

View File

@ -12,6 +12,8 @@ import { clickButton } from "../../../__test_support__/helpers";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { Color } from "../../../ui";
describe("<HardwareSettings />", () => {
const fakeProps = (): HardwareSettingsProps => ({
@ -68,4 +70,41 @@ describe("<HardwareSettings />", () => {
const wrapper = shallow(<HardwareSettings {...p} />);
expect(wrapper.html()).toContain("fa-download");
});
it("shows setting load progress", () => {
type ConsistencyLookup = Record<keyof FirmwareConfig, boolean>;
const consistent: Partial<ConsistencyLookup> =
({ id: false, encoder_invert_x: true, encoder_enabled_y: false });
const consistencyLookup = consistent as ConsistencyLookup;
const p = fakeProps();
const fakeConfig: Partial<FirmwareConfig> =
({ id: 0, encoder_invert_x: 1, encoder_enabled_y: 0 });
p.firmwareConfig = fakeConfig as FirmwareConfig;
p.sourceFwConfig = x =>
({ value: p.firmwareConfig?.[x], consistent: consistencyLookup[x] });
const wrapper = mount(<HardwareSettings {...p} />);
const barStyle = wrapper.find(".load-progress-bar").props().style;
expect(barStyle?.background).toEqual(Color.white);
expect(barStyle?.width).toEqual("50%");
});
it("shows setting load progress: 0%", () => {
const p = fakeProps();
p.firmwareConfig = fakeFirmwareConfig().body;
p.sourceFwConfig = () => ({ value: 0, consistent: false });
const wrapper = mount(<HardwareSettings {...p} />);
const barStyle = wrapper.find(".load-progress-bar").props().style;
expect(barStyle?.width).toEqual("0%");
expect(barStyle?.background).toEqual(Color.darkGray);
});
it("shows setting load progress: 100%", () => {
const p = fakeProps();
p.firmwareConfig = fakeFirmwareConfig().body;
p.sourceFwConfig = () => ({ value: 0, consistent: true });
const wrapper = mount(<HardwareSettings {...p} />);
const barStyle = wrapper.find(".load-progress-bar").props().style;
expect(barStyle?.width).toEqual("100%");
expect(barStyle?.background).toEqual(Color.darkGray);
});
});

View File

@ -17,6 +17,7 @@ import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector";
import { ExternalUrl } from "../../external_urls";
import { Highlight } from "./maybe_highlight";
import { OtaTimeSelectorRow } from "./fbos_settings/ota_time_selector";
export enum ColWidth {
label = 3,
@ -78,8 +79,6 @@ export class FarmbotOsSettings
const { bot, sourceFbosConfig, botToMqttStatus } = this.props;
const { sync_status } = bot.hardware.informational_settings;
const botOnline = isBotOnline(sync_status, botToMqttStatus);
const timeFormat = this.props.webAppConfig.body.time_format_24_hour ?
"24h" : "12h";
return <Widget className="device-widget">
<form onSubmit={(e) => e.preventDefault()}>
<WidgetHeader title="Device">
@ -133,11 +132,14 @@ export class FarmbotOsSettings
shouldDisplay={this.props.shouldDisplay}
timeSettings={this.props.timeSettings}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow
timeFormat={timeFormat}
<OtaTimeSelectorRow
timeSettings={this.props.timeSettings}
device={this.props.deviceAccount}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<FarmbotOsRow
bot={this.props.bot}
osReleaseNotesHeading={this.osReleaseNotes.heading}

View File

@ -11,7 +11,7 @@ import { fakeState } from "../../../../__test_support__/fake_state";
import { edit, save } from "../../../../api/crud";
import { fakeFbosConfig } from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex, fakeDevice
buildResourceIndex
} from "../../../../__test_support__/resource_index_builder";
describe("<AutoUpdateRow/>", () => {
@ -20,10 +20,8 @@ describe("<AutoUpdateRow/>", () => {
state.resources = buildResourceIndex([fakeConfig]);
const fakeProps = (): AutoUpdateRowProps => ({
timeFormat: "12h",
device: fakeDevice(),
dispatch: jest.fn(x => x(jest.fn(), () => state)),
sourceFbosConfig: () => ({ value: 1, consistent: true })
sourceFbosConfig: () => ({ value: 1, consistent: true }),
});
it("renders", () => {
@ -35,7 +33,7 @@ describe("<AutoUpdateRow/>", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: 0, consistent: true });
const wrapper = mount(<AutoUpdateRow {...p} />);
wrapper.find("button").at(1).simulate("click");
wrapper.find("button").first().simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: true });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
@ -44,7 +42,7 @@ describe("<AutoUpdateRow/>", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: 1, consistent: true });
const wrapper = mount(<AutoUpdateRow {...p} />);
wrapper.find("button").at(1).simulate("click");
wrapper.find("button").first().simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: false });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});

View File

@ -89,6 +89,7 @@ describe("<FbosDetails/>", () => {
const p = fakeProps();
const commit = "abcdefgh";
p.botInfoSettings.firmware_commit = commit;
p.botInfoSettings.firmware_version = "1.0.0";
const wrapper = mount(<FbosDetails {...p} />);
expect(wrapper.find("a").last().text()).toEqual(commit);
expect(wrapper.find("a").last().props().href?.split("/").slice(-1)[0])
@ -115,6 +116,7 @@ describe("<FbosDetails/>", () => {
it("doesn't display link without commit", () => {
const p = fakeProps();
p.botInfoSettings.firmware_version = undefined;
p.botInfoSettings.commit = "---";
p.botInfoSettings.firmware_commit = "---";
const wrapper = mount(<FbosDetails {...p} />);

View File

@ -6,38 +6,30 @@ import { updateConfig } from "../../actions";
import { Content, DeviceSetting } from "../../../constants";
import { AutoUpdateRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector";
import { Highlight } from "../maybe_highlight";
export function AutoUpdateRow(props: AutoUpdateRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
return <div>
<OtaTimeSelector
timeFormat={props.timeFormat}
disabled={!osAutoUpdate.value}
value={props.device.body.ota_hour}
onChange={changeOtaHour(props.dispatch, props.device)} />
<Row>
<Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.farmbotOSAutoUpdate)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.OS_AUTO_UPDATE)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value
}))} />
</Col>
</Highlight>
</Row>
</div>;
return <Row>
<Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.farmbotOSAutoUpdate)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.OS_AUTO_UPDATE)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value
}))} />
</Col>
</Highlight>
</Row>;
}

View File

@ -55,21 +55,24 @@ export function ChipTemperatureDisplay(
interface WiFiStrengthDisplayProps {
wifiStrength: number | undefined;
wifiStrengthPercent?: number | undefined;
extraInfo?: boolean;
}
/** WiFi signal strength display row: label, strength, indicator. */
export function WiFiStrengthDisplay(
{ wifiStrength, wifiStrengthPercent }: WiFiStrengthDisplayProps
{ wifiStrength, wifiStrengthPercent, extraInfo }: WiFiStrengthDisplayProps
): JSX.Element {
const percent = wifiStrength
? Math.round(-0.0154 * wifiStrength ** 2 - 0.4 * wifiStrength + 98)
: 0;
const dbString = `${wifiStrength || 0}dBm`;
const percentString = `${wifiStrengthPercent || percent}%`;
const numberDisplay =
extraInfo ? `${percentString} (${dbString})` : percentString;
return <div className="wifi-strength-display">
<p>
<b>{t("WiFi strength")}: </b>
{wifiStrength ? dbString : "N/A"}
{wifiStrength ? numberDisplay : "N/A"}
</p>
{wifiStrength &&
<div className="percent-bar">
@ -261,8 +264,8 @@ export function FbosDetails(props: FbosDetailsProps) {
wifi_level_percent, cpu_usage, private_ip,
} = props.botInfoSettings;
const { last_ota, last_ota_checkup } = props.deviceAccount.body;
const firmwareCommit = [firmware_commit, firmware_version].includes("---")
? firmware_commit : firmware_version?.split("-")[1] || firmware_commit;
const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---";
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
return <div>
<LastSeen
@ -287,7 +290,7 @@ export function FbosDetails(props: FbosDetailsProps) {
{isNumber(disk_usage) && <p><b>{t("Disk usage")}: </b>{disk_usage}%</p>}
{isNumber(cpu_usage) && <p><b>{t("CPU usage")}: </b>{cpu_usage}%</p>}
<ChipTemperatureDisplay chip={target} temperature={soc_temp} />
<WiFiStrengthDisplay
<WiFiStrengthDisplay extraInfo={true}
wifiStrength={wifi_level} wifiStrengthPercent={wifi_level_percent} />
<VoltageDisplay chip={target} throttled={throttled} />
<BetaReleaseOptIn

View File

@ -12,7 +12,6 @@ import {
TaggedDevice,
} from "farmbot";
import { TimeSettings } from "../../../interfaces";
import { PreferredHourFormat } from "./ota_time_selector";
export interface AutoSyncRowProps {
dispatch: Function;
@ -21,9 +20,14 @@ export interface AutoSyncRowProps {
export interface AutoUpdateRowProps {
dispatch: Function;
timeFormat: PreferredHourFormat;
sourceFbosConfig: SourceFbosConfig;
}
export interface OtaTimeSelectorRowProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
device: TaggedDevice;
timeSettings: TimeSettings;
}
export interface CameraSelectionProps {

View File

@ -4,11 +4,14 @@ import { t } from "../../../i18next_wrapper";
import { TaggedDevice } from "farmbot";
import { edit, save } from "../../../api/crud";
import { ColWidth } from "../farmbot_os_settings";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
import { OtaTimeSelectorRowProps } from "./interfaces";
// tslint:disable-next-line:no-null-keyword
const UNDEFINED = null as unknown as undefined;
const IMMEDIATELY = -1;
export type PreferredHourFormat = "12h" | "24h";
type PreferredHourFormat = "12h" | "24h";
type HOUR =
| typeof IMMEDIATELY
| 0
@ -37,6 +40,7 @@ type HOUR =
| 23;
type TimeTable = Record<HOUR, DropDownItem>;
type EveryTimeTable = Record<PreferredHourFormat, TimeTable>;
const ASAP = () => t("As soon as possible");
const TIME_TABLE_12H = (): TimeTable => ({
0: { label: t("Midnight"), value: 0 },
1: { label: "1:00 AM", value: 1 },
@ -62,7 +66,7 @@ const TIME_TABLE_12H = (): TimeTable => ({
21: { label: "9:00 PM", value: 21 },
22: { label: "10:00 PM", value: 22 },
23: { label: "11:00 PM", value: 23 },
[IMMEDIATELY]: { label: t("as soon as possible"), value: IMMEDIATELY },
[IMMEDIATELY]: { label: ASAP(), value: IMMEDIATELY },
});
const TIME_TABLE_24H = (): TimeTable => ({
0: { label: "00:00", value: 0 },
@ -89,7 +93,7 @@ const TIME_TABLE_24H = (): TimeTable => ({
21: { label: "21:00", value: 21 },
22: { label: "22:00", value: 22 },
23: { label: "23:00", value: 23 },
[IMMEDIATELY]: { label: t("as soon as possible"), value: IMMEDIATELY },
[IMMEDIATELY]: { label: ASAP(), value: IMMEDIATELY },
});
const DEFAULT_HOUR: keyof TimeTable = IMMEDIATELY;
@ -144,17 +148,29 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => {
const selectedItem = (typeof value == "number") ?
theTimeTable[value as HOUR] : theTimeTable[DEFAULT_HOUR];
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("Apply Software Updates ")}
</label>
</Col>
<Col xs={ColWidth.description}>
<FBSelect
selectedItem={selectedItem}
onChange={cb}
list={list}
extraClass={disabled ? "disabled" : ""} />
</Col>
<Highlight settingName={DeviceSetting.applySoftwareUpdates}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.applySoftwareUpdates)}
</label>
</Col>
<Col xs={ColWidth.description}>
<FBSelect
selectedItem={selectedItem}
onChange={cb}
list={list}
extraClass={disabled ? "disabled" : ""} />
</Col>
</Highlight>
</Row>;
};
export function OtaTimeSelectorRow(props: OtaTimeSelectorRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
const timeFormat = props.timeSettings.hour24 ? "24h" : "12h";
return <OtaTimeSelector
timeFormat={timeFormat}
disabled={!osAutoUpdate.value}
value={props.device.body.ota_hour}
onChange={changeOtaHour(props.dispatch, props.device)} />;
}

View File

@ -16,15 +16,31 @@ export const getFwHardwareValue =
return isFwHardwareValue(value) ? value : undefined;
};
const TMC_BOARDS = ["express_k10", "farmduino_k15"];
const NO_BUTTONS = ["arduino", "farmduino", "none"];
const EXPRESS_BOARDS = ["express_k10"];
const NO_SENSORS = [...EXPRESS_BOARDS];
const NO_ENCODERS = [...EXPRESS_BOARDS];
const NO_TOOLS = [...EXPRESS_BOARDS];
const NO_TMC = ["arduino", "farmduino", "farmduino_k14"];
export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) =>
!!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware));
!firmwareHardware || !NO_TMC.includes(firmwareHardware);
export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) =>
!!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware));
export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_BUTTONS.includes(firmwareHardware);
export const hasEncoders = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_ENCODERS.includes(firmwareHardware);
export const hasSensors = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_SENSORS.includes(firmwareHardware);
export const hasUTM = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_TOOLS.includes(firmwareHardware);
export const getBoardIdentifier =
(firmwareVersion: string | undefined): string =>
firmwareVersion ? firmwareVersion.split(".")[3] : "undefined";

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
import { HardwareSettingsProps } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index";
import { HardwareSettingsProps, SourceFwConfig } from "../interfaces";
import { isBotOnline } from "../must_be_online";
import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone";
@ -19,6 +19,8 @@ import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling";
import { maybeOpenPanel } from "./maybe_highlight";
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import type { McuParamName } from "farmbot";
export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> {
@ -36,7 +38,10 @@ export class HardwareSettings extends
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
<SettingLoadProgress firmwareConfig={firmwareConfig}
sourceFwConfig={sourceFwConfig} />
</WidgetHeader>
<WidgetBody>
<button
className={"fb-button gray no-float"}
@ -78,8 +83,33 @@ export class HardwareSettings extends
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
<PinBindings {...commonProps}
resources={resources} />
resources={resources}
firmwareHardware={firmwareHardware} />
</WidgetBody>
</Widget>;
}
}
interface SettingLoadProgressProps {
sourceFwConfig: SourceFwConfig;
firmwareConfig: FirmwareConfig | undefined;
}
const UNTRACKED_KEYS: (keyof FirmwareConfig)[] = [
"id", "created_at", "updated_at", "device_id", "api_migrated",
"param_config_ok", "param_test", "param_use_eeprom", "param_version",
];
/** Track firmware configuration adoption by FarmBot OS. */
const SettingLoadProgress = (props: SettingLoadProgressProps) => {
const keys = Object.keys(props.firmwareConfig || {})
.filter((k: keyof FirmwareConfig) => !UNTRACKED_KEYS.includes(k));
const loadedKeys = keys.filter((key: McuParamName) =>
props.sourceFwConfig(key).consistent);
const progress = loadedKeys.length / keys.length * 100;
const color = [0, 100].includes(progress) ? Color.darkGray : Color.white;
return <div className={"load-progress-bar-wrapper"}>
<div className={"load-progress-bar"}
style={{ width: `${progress}%`, background: color }} />
</div>;
};

View File

@ -12,6 +12,7 @@ describe("<PinBindings />", () => {
dispatch: jest.fn(),
controlPanelState: panelState(),
resources: buildResourceIndex([]).index,
firmwareHardware: undefined,
});
it("shows pin binding labels", () => {

View File

@ -5,7 +5,7 @@ import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { isExpressBoard } from "../firmware_hardware_support";
import { hasEncoders } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
export function Encoders(props: EncodersProps) {
@ -18,23 +18,23 @@ export function Encoders(props: EncodersProps) {
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value
};
const isExpress = isExpressBoard(firmwareHardware);
const showEncoders = hasEncoders(firmwareHardware);
return <Highlight className={"section"}
settingName={DeviceSetting.encoders}>
<Header
expanded={encoders}
title={isExpress
title={!showEncoders
? DeviceSetting.stallDetection
: DeviceSetting.encoders}
panel={"encoders"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup
label={isExpress
label={!showEncoders
? DeviceSetting.enableStallDetection
: DeviceSetting.enableEncoders}
tooltip={isExpress
tooltip={!showEncoders
? ToolTips.ENABLE_STALL_DETECTION
: ToolTips.ENABLE_ENCODERS}
x={"encoder_enabled_x"}
@ -42,7 +42,7 @@ export function Encoders(props: EncodersProps) {
z={"encoder_enabled_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
{isExpress &&
{!showEncoders &&
<NumericMCUInputGroup
label={DeviceSetting.stallSensitivity}
tooltip={ToolTips.STALL_SENSITIVITY}
@ -52,7 +52,7 @@ export function Encoders(props: EncodersProps) {
gray={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
{showEncoders &&
<BooleanMCUInputGroup
label={DeviceSetting.useEncodersForPositioning}
tooltip={ToolTips.ENCODER_POSITIONING}
@ -62,7 +62,7 @@ export function Encoders(props: EncodersProps) {
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
{showEncoders &&
<BooleanMCUInputGroup
label={DeviceSetting.invertEncoders}
tooltip={ToolTips.INVERT_ENCODERS}
@ -74,7 +74,7 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
label={DeviceSetting.maxMissedSteps}
tooltip={isExpress
tooltip={!showEncoders
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
: ToolTips.MAX_MISSED_STEPS_ENCODERS}
x={"encoder_missed_steps_max_x"}
@ -92,7 +92,7 @@ export function Encoders(props: EncodersProps) {
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
{!isExpress &&
{showEncoders &&
<NumericMCUInputGroup
label={DeviceSetting.encoderScaling}
tooltip={ToolTips.ENCODER_SCALING}

View File

@ -9,7 +9,7 @@ import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { calculateScale } from "./motors";
import { isExpressBoard } from "../firmware_hardware_support";
import { hasEncoders } from "../firmware_hardware_support";
import { getDevice } from "../../../device";
import { commandErr } from "../../actions";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
@ -44,7 +44,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
type={"find_home"}
title={DeviceSetting.homing}
axisTitle={t("FIND HOME")}
toolTip={isExpressBoard(firmwareHardware)
toolTip={!hasEncoders(firmwareHardware)
? ToolTips.HOMING_STALL_DETECTION
: ToolTips.HOMING_ENCODERS}
action={axis => getDevice()
@ -56,7 +56,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
type={"calibrate"}
title={DeviceSetting.calibration}
axisTitle={t("CALIBRATE")}
toolTip={isExpressBoard(firmwareHardware)
toolTip={!hasEncoders(firmwareHardware)
? ToolTips.CALIBRATION_STALL_DETECTION
: ToolTips.CALIBRATION_ENCODERS}
action={axis => getDevice().calibrate({ axis })
@ -74,7 +74,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
botDisconnected={botDisconnected} />
<BooleanMCUInputGroup
label={DeviceSetting.findHomeOnBoot}
tooltip={isExpressBoard(firmwareHardware)
tooltip={!hasEncoders(firmwareHardware)
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
disable={disabled}

View File

@ -9,7 +9,7 @@ import { Highlight } from "../maybe_highlight";
export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props;
const { dispatch, resources, firmwareHardware } = props;
return <Highlight className={"section"}
settingName={DeviceSetting.pinBindings}>
@ -19,7 +19,8 @@ export function PinBindings(props: PinBindingsProps) {
panel={"pin_bindings"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} />
<PinBindingsContent dispatch={dispatch} resources={resources}
firmwareHardware={firmwareHardware} />
</Collapse>
</Highlight>;
}

View File

@ -109,6 +109,7 @@ export interface PinBindingsProps {
dispatch: Function;
controlPanelState: ControlPanelState;
resources: ResourceIndex;
firmwareHardware: FirmwareHardware | undefined;
}
export interface DangerZoneProps {

View File

@ -3,6 +3,7 @@ import { ControlPanelState } from "../interfaces";
import { toggleControlPanel } from "../actions";
import { urlFriendly } from "../../util";
import { DeviceSetting } from "../../constants";
import { trim } from "lodash";
const HOMING_PANEL = [
DeviceSetting.homingAndCalibration,
@ -86,10 +87,15 @@ DANGER_ZONE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "danger_zone");
PIN_BINDINGS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_bindings");
POWER_AND_RESET_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "power_and_reset");
/** Keep string up until first `(` character (trailing whitespace removed). */
const stripUnits = (settingName: string) => trim(settingName.split("(")[0]);
/** Look up parent panels for settings using URL-friendly names. */
const URL_FRIENDLY_LOOKUP: Record<string, keyof ControlPanelState> = {};
Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) =>
URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel);
Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) => {
URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel;
URL_FRIENDLY_LOOKUP[urlFriendly(stripUnits(setting))] = panel;
});
/** Look up all relevant names for the same setting. */
const ALTERNATE_NAMES =
@ -100,7 +106,9 @@ ALTERNATE_NAMES[DeviceSetting.stallDetection].push(DeviceSetting.encoders);
/** Generate array of names for the same setting. Most only have one. */
const compareValues = (settingName: DeviceSetting) =>
(ALTERNATE_NAMES[settingName]).map(s => urlFriendly(s));
(ALTERNATE_NAMES[settingName] as string[])
.concat(stripUnits(settingName))
.map(s => urlFriendly(s));
/** Retrieve a highlight search term. */
const getHighlightName = () => location.search.split("?highlight=").pop();

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { mount } from "enzyme";
import {
ConnectivityDiagram,
ConnectivityDiagramProps,
@ -83,9 +82,9 @@ describe("getTextPosition()", () => {
describe("nodeLabel()", () => {
it("renders", () => {
const label = mount(nodeLabel("Top Node", "top" as DiagramNodes));
expect(label.text()).toEqual("Top Node");
expect(label.props())
const label = svgMount(nodeLabel("Top Node", "top" as DiagramNodes));
expect(label.find("text").text()).toEqual("Top Node");
expect(label.find("text").props())
.toEqual({ children: "Top Node", textAnchor: "middle", x: 0, y: -75 });
});
});

View File

@ -31,7 +31,9 @@ export class Connectivity
render() {
const { informational_settings } = this.props.bot.hardware;
const { soc_temp, wifi_level, throttled } = informational_settings;
const {
soc_temp, wifi_level, throttled, wifi_level_percent
} = informational_settings;
return <div className="connectivity">
<Row>
<Col md={12} lg={4}>
@ -42,7 +44,8 @@ export class Connectivity
<div className="fbos-info">
<label>{t("Raspberry Pi Info")}</label>
<ChipTemperatureDisplay temperature={soc_temp} />
<WiFiStrengthDisplay wifiStrength={wifi_level} />
<WiFiStrengthDisplay wifiStrength={wifi_level}
wifiStrengthPercent={wifi_level_percent} />
<VoltageDisplay throttled={throttled} />
</div>
<QosPanel pings={this.props.pings} />

View File

@ -1,4 +1,6 @@
import { sortByNameAndPin, ButtonPin, getSpecialActionLabel } from "../list_and_label_support";
import {
sortByNameAndPin, ButtonPin, getSpecialActionLabel
} from "../list_and_label_support";
import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
describe("sortByNameAndPin()", () => {

View File

@ -46,6 +46,7 @@ describe("<PinBindingsContent/>", () => {
return {
dispatch: jest.fn(),
resources: resources,
firmwareHardware: undefined,
};
}

View File

@ -1,25 +1,37 @@
jest.mock("../../../api/crud", () => ({
initSave: jest.fn(),
}));
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
import * as React from "react";
import { mount } from "enzyme";
import { StockPinBindingsButton } from "../tagged_pin_binding_init";
import {
StockPinBindingsButton, StockPinBindingsButtonProps
} from "../tagged_pin_binding_init";
import { initSave } from "../../../api/crud";
import { stockPinBindings } from "../list_and_label_support";
describe("<StockPinBindingsButton />", () => {
const fakeProps = () => ({
shouldDisplay: () => false,
const fakeProps = (): StockPinBindingsButtonProps => ({
dispatch: jest.fn(),
firmwareHardware: undefined,
});
it("adds bindings", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = mount(<StockPinBindingsButton {...p} />);
const wrapper = mount(<StockPinBindingsButton {...fakeProps()} />);
wrapper.find("button").simulate("click");
stockPinBindings.map(body =>
expect(initSave).toHaveBeenCalledWith("PinBinding", body));
});
it("is hidden", () => {
const p = fakeProps();
p.firmwareHardware = "arduino";
const wrapper = mount(<StockPinBindingsButton {...p} />);
expect(wrapper.find("button").props().hidden).toBeTruthy();
});
it("is not hidden", () => {
const p = fakeProps();
p.firmwareHardware = "farmduino_k14";
const wrapper = mount(<StockPinBindingsButton {...p} />);
expect(wrapper.find("button").props().hidden).toBeFalsy();
});
});

View File

@ -3,10 +3,12 @@ import {
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
import { FirmwareHardware } from "farmbot";
export interface PinBindingsContentProps {
dispatch: Function;
resources: ResourceIndex;
firmwareHardware: FirmwareHardware | undefined;
}
export interface PinBindingListItems {

View File

@ -92,7 +92,7 @@ export const reservedPiGPIO = piI2c0Pins;
const GPIO_PIN_LABELS = (): { [x: number]: string } => ({
[ButtonPin.estop]: t("Button {{ num }}: E-STOP", { num: 1 }),
[ButtonPin.unlock]: t("Button {{ num }}: UNLOCK", { num: 2 }),
[ButtonPin.btn3]: t("Button {{ num }})", { num: 3 }),
[ButtonPin.btn3]: t("Button {{ num }}", { num: 3 }),
[ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }),
[ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }),
});

View File

@ -68,12 +68,13 @@ const PinBindingsListHeader = () =>
</Row>;
export const PinBindingsContent = (props: PinBindingsContentProps) => {
const { dispatch, resources } = props;
const { dispatch, resources, firmwareHardware } = props;
const pinBindings = apiPinBindings(resources);
return <div className="pin-bindings">
<Row>
<StockPinBindingsButton dispatch={dispatch} />
<StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover
position={Position.RIGHT_TOP}
interactionKind={PopoverInteractionKind.HOVER}

View File

@ -8,6 +8,8 @@ import { PinBindingListItems } from "./interfaces";
import { stockPinBindings } from "./list_and_label_support";
import { initSave } from "../../api/crud";
import { t } from "../../i18next_wrapper";
import { FirmwareHardware } from "farmbot";
import { hasButtons } from "../components/firmware_hardware_support";
/** Return the correct Pin Binding resource according to binding type. */
export const pinBindingBody =
@ -34,15 +36,21 @@ export const pinBindingBody =
return body;
};
export interface StockPinBindingsButtonProps {
dispatch: Function;
firmwareHardware: FirmwareHardware | undefined;
}
/** Add default pin bindings. */
export const StockPinBindingsButton = ({ dispatch }: { dispatch: Function }) =>
export const StockPinBindingsButton = (props: StockPinBindingsButtonProps) =>
<div className="stock-pin-bindings-button">
<button
className="fb-button green"
hidden={!hasButtons(props.firmwareHardware)}
onClick={() => stockPinBindings.map(binding =>
dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
props.dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
<i className="fa fa-plus" />
{t("v1.4 Stock Bindings")}
{t("Stock Bindings")}
</button>
</div>;

View File

@ -62,6 +62,7 @@ describe("<FarmDesigner/>", () => {
sensors: [],
groups: [],
shouldDisplay: () => false,
mountedToolName: undefined,
});
it("loads default map settings", () => {

View File

@ -71,6 +71,14 @@ describe("<DesignerNavTabs />", () => {
expect(wrapper.html()).toContain("active");
});
it("renders for tools", () => {
mockPath = "/app/designer/tools";
mockDev = false;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("gray-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});
it("renders for zones", () => {
mockPath = "/app/designer/zones";
mockDev = true;

View File

@ -0,0 +1,19 @@
import { Plant } from "../plant";
describe("Plant()", () => {
it("returns defaults", () => {
expect(Plant({})).toEqual({
created_at: "",
id: undefined,
meta: {},
name: "Untitled Plant",
openfarm_slug: "not-set",
plant_stage: "planned",
pointer_type: "Plant",
radius: 25,
x: 0,
y: 0,
z: 0,
});
});
});

View File

@ -94,6 +94,19 @@ describe("designer reducer", () => {
});
});
it("uses current point color", () => {
const action: ReduxAction<CurrentPointPayl> = {
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx: 10, cy: 20, r: 30 }
};
const state = oldState();
state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" };
const newState = designer(state, action);
expect(newState.currentPoint).toEqual({
cx: 10, cy: 20, r: 30, color: "red"
});
});
it("sets opened saved garden", () => {
const payload = "savedGardenUuid";
const action: ReduxAction<string | undefined> = {

View File

@ -1,7 +1,7 @@
import { mapStateToProps, getPlants } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import {
buildResourceIndex
buildResourceIndex, fakeDevice
} from "../../__test_support__/resource_index_builder";
import {
fakePlant,
@ -49,7 +49,7 @@ describe("mapStateToProps()", () => {
it("returns selected plant", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakePlant()]);
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0];
state.resources.consumers.farm_designer.selectedPlants = [plantUuid];
expect(mapStateToProps(state).selectedPlant).toEqual(
@ -66,7 +66,9 @@ describe("mapStateToProps()", () => {
point2.body.discarded_at = DISCARDED_AT;
const point3 = fakePoint();
point3.body.discarded_at = DISCARDED_AT;
state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]);
state.resources = buildResourceIndex([
webAppConfig, point1, point2, point3, fakeDevice()
]);
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
});
@ -80,7 +82,9 @@ describe("mapStateToProps()", () => {
point2.body.discarded_at = DISCARDED_AT;
const point3 = fakePoint();
point3.body.discarded_at = DISCARDED_AT;
state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]);
state.resources = buildResourceIndex([
webAppConfig, point1, point2, point3, fakeDevice()
]);
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
});
@ -90,7 +94,7 @@ describe("mapStateToProps()", () => {
sr1.body.created_at = "2018-01-14T20:20:38.362Z";
const sr2 = fakeSensorReading();
sr2.body.created_at = "2018-01-11T20:20:38.362Z";
state.resources = buildResourceIndex([sr1, sr2]);
state.resources = buildResourceIndex([sr1, sr2, fakeDevice()]);
const uuid1 = Object.keys(state.resources.index.byKind["SensorReading"])[0];
const uuid2 = Object.keys(state.resources.index.byKind["SensorReading"])[1];
expect(mapStateToProps(state).sensorReadings).toEqual([
@ -112,7 +116,8 @@ describe("getPlants()", () => {
const template2 = fakePlantTemplate();
template2.body.saved_garden_id = 2;
return buildResourceIndex([
savedGarden, plant1, plant2, template1, template2]);
savedGarden, plant1, plant2, template1, template2, fakeDevice()
]);
};
it("returns plants", () => {
expect(getPlants(fakeResources()).length).toEqual(2);
@ -133,7 +138,7 @@ describe("getPlants()", () => {
const fwEnv = fakeFarmwareEnv();
fwEnv.body.key = "CAMERA_CALIBRATION_total_rotation_angle";
fwEnv.body.value = 15;
state.resources = buildResourceIndex([fwEnv]);
state.resources = buildResourceIndex([fwEnv, fakeDevice()]);
const props = mapStateToProps(state);
expect(props.cameraCalibrationData).toEqual(
expect.objectContaining({ rotation: "15" }));

View File

@ -90,7 +90,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
{!props.noIcon &&
<i className="fa fa-search"></i>}
<i className="fa fa-search" />}
<ErrorBoundary>
{props.children}
</ErrorBoundary>

View File

@ -55,7 +55,7 @@ describe("<AddFarmEvent />", () => {
const wrapper = mount(<AddFarmEvent {...fakeProps()} />);
wrapper.setState({ uuid: "FarmEvent" });
["Add Event", "Sequence or Regimen", "fake", "Save"].map(string =>
expect(wrapper.text()).toContain(string));
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
const deleteBtn = wrapper.find("button").last();
expect(deleteBtn.text()).toEqual("Delete");
expect(deleteBtn.props().hidden).toBeTruthy();

View File

@ -102,7 +102,7 @@ export class RawAddFarmEvent
<DesignerPanelHeader
panelName={panelName}
panel={Panel.FarmEvents}
title={t("Add Event")}
title={t("Add event")}
onBack={(farmEvent && !farmEvent.body.id)
? () => this.props.dispatch(destroyOK(farmEvent))
: undefined} />
@ -115,7 +115,7 @@ export class RawAddFarmEvent
executableOptions={this.props.executableOptions}
dispatch={this.props.dispatch}
findExecutable={this.props.findExecutable}
title={t("Add Event")}
title={t("Add event")}
timeSettings={this.props.timeSettings}
autoSyncEnabled={this.props.autoSyncEnabled}
resources={this.props.resources}

View File

@ -23,7 +23,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
<DesignerPanelHeader
panelName={panelName}
panel={Panel.FarmEvents}
title={t("Edit Event")} />
title={t("Edit event")} />
<DesignerPanelContent panelName={panelName}>
<EditFEForm farmEvent={fe}
deviceTimezone={this.props.deviceTimezone}
@ -31,7 +31,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
executableOptions={this.props.executableOptions}
dispatch={this.props.dispatch}
findExecutable={this.props.findExecutable}
title={t("Edit Event")}
title={t("Edit event")}
deleteBtn={true}
timeSettings={this.props.timeSettings}
autoSyncEnabled={this.props.autoSyncEnabled}

View File

@ -41,6 +41,7 @@ import {
} from "../../sequences/locals_list/locals_list_support";
import { t } from "../../i18next_wrapper";
import { TimeSettings } from "../../interfaces";
import { ErrorBoundary } from "../../error_boundary";
export const NEVER: TimeUnit = "never";
/** Separate each of the form fields into their own interface. Recombined later
@ -360,19 +361,24 @@ export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
render() {
const { farmEvent } = this.props;
return <div className="edit-farm-event-form">
<FarmEventForm
isRegimen={this.isReg}
fieldGet={this.fieldGet}
fieldSet={this.fieldSet}
timeSettings={this.props.timeSettings}
executableOptions={this.props.executableOptions}
executableSet={this.executableSet}
executableGet={this.executableGet}
dispatch={this.props.dispatch}
specialStatus={farmEvent.specialStatus || this.state.specialStatusLocal}
onSave={() => this.commitViewModel()}>
<this.LocalsList />
</FarmEventForm>
<ErrorBoundary>
<FarmEventForm
isRegimen={this.isReg}
fieldGet={this.fieldGet}
fieldSet={this.fieldSet}
timeSettings={this.props.timeSettings}
executableOptions={this.props.executableOptions}
executableSet={this.executableSet}
executableGet={this.executableGet}
dispatch={this.props.dispatch}
specialStatus={farmEvent.specialStatus
|| this.state.specialStatusLocal}
onSave={() => this.commitViewModel()}>
<ErrorBoundary>
<this.LocalsList />
</ErrorBoundary>
</FarmEventForm>
</ErrorBoundary>
<FarmEventDeleteButton
hidden={!this.props.deleteBtn}
farmEvent={this.props.farmEvent}

View File

@ -111,7 +111,7 @@ export class PureFarmEvents
<input
value={this.state.searchTerm}
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search events...")} />
placeholder={t("Search your events...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"farm-event"}>
<div className="farm-events">

View File

@ -211,6 +211,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
timeSettings={this.props.timeSettings}
sensors={this.props.sensors}
groups={this.props.groups}
mountedToolName={this.props.mountedToolName}
shouldDisplay={this.props.shouldDisplay} />
</div>

View File

@ -80,6 +80,7 @@ export interface Props {
sensors: TaggedSensor[];
groups: TaggedPointGroup[];
shouldDisplay: ShouldDisplay;
mountedToolName: string | undefined;
}
export interface MovePlantProps {
@ -210,6 +211,7 @@ export interface GardenMapProps {
timeSettings: TimeSettings;
groups: TaggedPointGroup[];
shouldDisplay: ShouldDisplay;
mountedToolName: string | undefined;
}
export interface GardenMapState {

View File

@ -124,6 +124,7 @@ const fakeProps = (): GardenMapProps => ({
timeSettings: fakeTimeSettings(),
groups: [],
shouldDisplay: () => false,
mountedToolName: undefined,
});
describe("<GardenMap/>", () => {
@ -200,6 +201,16 @@ describe("<GardenMap/>", () => {
expect(getGardenCoordinates).not.toHaveBeenCalled();
});
it("starts drag on background: does nothing when in move mode", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />);
mockMode = Mode.moveTo;
const e = { pageX: 1000, pageY: 2000 };
wrapper.find(".drop-area-background").simulate("mouseDown", e);
expect(startNewSelectionBox).not.toHaveBeenCalled();
expect(history.push).not.toHaveBeenCalled();
expect(getGardenCoordinates).not.toHaveBeenCalled();
});
it("starts drag on background: creating points", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />);
mockMode = Mode.createPoint;
@ -348,7 +359,7 @@ describe("<GardenMap/>", () => {
expect(closePlantInfo).toHaveBeenCalled();
});
it("doesn't close panel", () => {
it("doesn't close panel: box select", () => {
mockMode = Mode.boxSelect;
const p = fakeProps();
p.designer.selectedPlants = [fakePlant().uuid];
@ -357,6 +368,15 @@ describe("<GardenMap/>", () => {
expect(closePlantInfo).not.toHaveBeenCalled();
});
it("doesn't close panel: move mode", () => {
mockMode = Mode.moveTo;
const p = fakeProps();
p.designer.selectedPlants = [fakePlant().uuid];
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).not.toHaveBeenCalled();
});
it("calls unselectPlant on unmount", () => {
const wrapper = shallow(<GardenMap {...fakeProps()} />);
wrapper.unmount();
@ -405,7 +425,7 @@ describe("<GardenMap/>", () => {
const point = fakePoint();
point.body.id = 1;
p.allPoints = [point];
const wrapper = shallow<GardenMap>(<GardenMap {...p} />);
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
expect(wrapper.instance().pointsSelectedByGroup).toEqual([point]);
});
});

View File

@ -160,6 +160,8 @@ export class GardenMap extends
/** Map background drag start actions. */
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
switch (getMode()) {
case Mode.moveTo:
break;
case Mode.createPoint:
case Mode.clickToAdd:
case Mode.editPlant:
@ -301,6 +303,8 @@ export class GardenMap extends
/** Return to garden (unless selecting more plants). */
closePanel = () => {
switch (getMode()) {
case Mode.moveTo:
return () => { };
case Mode.boxSelect:
return this.props.designer.selectedPlants
? () => { }
@ -410,6 +414,7 @@ export class GardenMap extends
plantAreaOffset={this.props.gridOffset}
peripherals={this.props.peripherals}
eStopStatus={this.props.eStopStatus}
mountedToolName={this.props.mountedToolName}
getConfigValue={this.props.getConfigValue} />
HoveredPlant = () => <HoveredPlant
visible={!!this.props.showPlants}

View File

@ -127,6 +127,7 @@ export interface VirtualFarmBotProps {
peripherals: { label: string, value: boolean }[];
eStopStatus: boolean;
getConfigValue: GetWebAppConfigValue;
mountedToolName: string | undefined;
}
export interface FarmBotLayerProps extends VirtualFarmBotProps, BotExtentsProps {

View File

@ -86,7 +86,7 @@ describe("<BotFigure/>", () => {
p.position.x = 100;
const wrapper = shallow<BotFigure>(<BotFigure {...p} />);
expect(wrapper.instance().state.hovered).toBeFalsy();
const utm = wrapper.find("#UTM");
const utm = wrapper.find("#UTM-wrapper");
utm.simulate("mouseOver");
expect(wrapper.instance().state.hovered).toBeTruthy();
expect(wrapper.find("text").props()).toEqual(expect.objectContaining({
@ -105,7 +105,7 @@ describe("<BotFigure/>", () => {
p.position.x = 100;
p.mapTransformProps.xySwap = true;
const wrapper = shallow<BotFigure>(<BotFigure {...p} />);
const utm = wrapper.find("#UTM");
const utm = wrapper.find("#UTM-wrapper");
utm.simulate("mouseOver");
expect(wrapper.instance().state.hovered).toBeTruthy();
expect(wrapper.find("text").props()).toEqual(expect.objectContaining({
@ -114,4 +114,12 @@ describe("<BotFigure/>", () => {
}));
expect(wrapper.text()).toEqual("(100, 0, 0)");
});
it("shows mounted tool", () => {
const p = fakeProps();
p.mountedToolName = "Seeder";
const wrapper = shallow<BotFigure>(<BotFigure {...p} />);
expect(wrapper.find("#UTM-wrapper").find("#mounted-tool").length)
.toEqual(1);
});
});

View File

@ -25,6 +25,7 @@ describe("<FarmBotLayer/>", () => {
peripherals: [],
eStopStatus: false,
getConfigValue: jest.fn(),
mountedToolName: undefined,
};
}

View File

@ -19,6 +19,7 @@ describe("<VirtualFarmBot/>", () => {
peripherals: [],
eStopStatus: false,
getConfigValue: () => true,
mountedToolName: undefined,
};
}

View File

@ -4,6 +4,9 @@ import { getMapSize, transformXY } from "../../util";
import { BotPosition } from "../../../../devices/interfaces";
import { Color } from "../../../../ui/index";
import { botPositionLabel } from "./bot_position_label";
import { Tool } from "../tool_slots/tool_graphics";
import { reduceToolName } from "../tool_slots/tool_slot_point";
import { noop } from "lodash";
export interface BotFigureProps {
name: string;
@ -11,6 +14,7 @@ export interface BotFigureProps {
mapTransformProps: MapTransformProps;
plantAreaOffset: AxisNumberProperty;
eStopStatus?: boolean;
mountedToolName?: string | undefined;
}
interface BotFigureState {
@ -24,7 +28,8 @@ export class BotFigure extends
setHover = (state: boolean) => { this.setState({ hovered: state }); };
render() {
const { name, position, plantAreaOffset, eStopStatus, mapTransformProps
const {
name, position, plantAreaOffset, eStopStatus, mapTransformProps,
} = this.props;
const { xySwap } = mapTransformProps;
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
@ -32,6 +37,14 @@ export class BotFigure extends
(position.x || 0), (position.y || 0), mapTransformProps);
const color = eStopStatus ? Color.virtualRed : Color.darkGray;
const opacity = name.includes("encoder") ? 0.25 : 0.5;
const toolProps = {
x: positionQ.qx,
y: positionQ.qy,
hovered: this.state.hovered,
dispatch: noop,
uuid: "utm",
xySwap,
};
return <g id={name}>
<rect id="gantry"
x={xySwap ? -plantAreaOffset.x : positionQ.qx - 10}
@ -40,14 +53,32 @@ export class BotFigure extends
height={xySwap ? 20 : mapSize.h}
fillOpacity={opacity}
fill={color} />
<circle id="UTM"
<g id="UTM-wrapper" style={{ pointerEvents: "all" }}
onMouseOver={() => this.setHover(true)}
onMouseLeave={() => this.setHover(false)}
cx={positionQ.qx}
cy={positionQ.qy}
r={35}
fillOpacity={opacity}
fill={color} />
fill={color}>
{this.props.mountedToolName
? <g id="mounted-tool">
<circle
cx={positionQ.qx}
cy={positionQ.qy}
r={32}
stroke={Color.darkGray}
strokeWidth={6}
opacity={0.25}
fill={"none"} />
<Tool
tool={reduceToolName(this.props.mountedToolName)}
toolProps={toolProps} />
</g>
: <circle id="UTM"
cx={positionQ.qx}
cy={positionQ.qy}
r={35}
fillOpacity={opacity}
fill={color} />}
</g>
<text
visibility={this.state.hovered ? "visible" : "hidden"}
x={positionQ.qx}

View File

@ -6,7 +6,7 @@ import { FarmBotLayerProps } from "../../interfaces";
export function FarmBotLayer(props: FarmBotLayerProps) {
const {
visible, stopAtHome, botSize, plantAreaOffset, mapTransformProps,
peripherals, eStopStatus, botLocationData, getConfigValue
peripherals, eStopStatus, botLocationData, getConfigValue, mountedToolName,
} = props;
return visible ? <g id="farmbot-layer" style={{ pointerEvents: "none" }}>
<VirtualFarmBot
@ -15,6 +15,7 @@ export function FarmBotLayer(props: FarmBotLayerProps) {
plantAreaOffset={plantAreaOffset}
peripherals={peripherals}
eStopStatus={eStopStatus}
mountedToolName={mountedToolName}
getConfigValue={getConfigValue} />
<BotExtents
mapTransformProps={mapTransformProps}

View File

@ -28,6 +28,7 @@ export function VirtualFarmBot(props: VirtualFarmBotProps) {
position={props.botLocationData.position}
mapTransformProps={mapTransformProps}
plantAreaOffset={plantAreaOffset}
mountedToolName={props.mountedToolName}
eStopStatus={eStopStatus} />
{encoderFigure &&
<BotFigure name={"encoder-position"}

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { mount } from "enzyme";
import { MapImage, MapImageProps } from "../map_image";
import { SpecialStatus } from "farmbot";
import { cloneDeep } from "lodash";
@ -7,6 +6,9 @@ import { trim } from "../../../../../util";
import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";
import { svgMount } from "../../../../../__test_support__/svg_mount";
const NOT_DISPLAYED = "<svg><image></image></svg>";
describe("<MapImage />", () => {
const fakeProps = (): MapImageProps => {
@ -37,28 +39,28 @@ describe("<MapImage />", () => {
};
it("doesn't render image", () => {
const wrapper = mount(<MapImage {...fakeProps()} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...fakeProps()} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
it("renders pre-calibration preview", () => {
const p = fakeProps();
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
const wrapper = mount(<MapImage {...p} />);
wrapper.setState({ width: 100, height: 100 });
const wrapper = svgMount(<MapImage {...p} />);
wrapper.find(MapImage).setState({ width: 100, height: 100 });
expect(wrapper.html()).toContain("image_url");
});
it("gets image size", () => {
const p = fakeProps();
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
const wrapper = mount<MapImage>(<MapImage {...p} />);
expect(wrapper.state()).toEqual({ width: 0, height: 0 });
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.find(MapImage).state()).toEqual({ width: 0, height: 0 });
const img = new Image();
img.width = 100;
img.height = 200;
wrapper.instance().imageCallback(img)();
expect(wrapper.state()).toEqual({ width: 100, height: 200 });
wrapper.find<MapImage>(MapImage).instance().imageCallback(img)();
expect(wrapper.find(MapImage).state()).toEqual({ width: 100, height: 200 });
});
interface ExpectedData {
@ -83,8 +85,8 @@ describe("<MapImage />", () => {
expectedData: ExpectedData,
extra?: ExtraTranslationData) => {
it(`renders image: INPUT_SET_${num}`, () => {
const wrapper = mount(<MapImage {...inputData[num]} />);
wrapper.setState({ width: 480, height: 640 });
const wrapper = svgMount(<MapImage {...inputData[num]} />);
wrapper.find(MapImage).setState({ width: 480, height: 640 });
expect(wrapper.find("image").props()).toEqual({
xlinkHref: "image_url",
x: 0,
@ -183,21 +185,21 @@ describe("<MapImage />", () => {
it("doesn't render placeholder image", () => {
const p = INPUT_SET_1;
p.image && (p.image.body.attachment_url = "/placehold.");
const wrapper = mount(<MapImage {...p} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
it("doesn't render image taken at different height than calibration", () => {
const p = INPUT_SET_1;
p.image && (p.image.body.meta.z = 100);
const wrapper = mount(<MapImage {...p} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
it("doesn't render images that are not adjusted for camera rotation", () => {
const p = INPUT_SET_1;
p.image && (p.image.body.meta.name = "na");
const wrapper = mount(<MapImage {...p} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
});

View File

@ -1,11 +1,15 @@
import * as React from "react";
import {
ToolbaySlot, Tool, ToolProps, ToolGraphicProps, ToolSlotGraphicProps
ToolbaySlot, Tool, ToolProps, ToolGraphicProps, ToolSlotGraphicProps,
ToolNames, ToolSVG, ToolSVGProps, ToolSlotSVG, ToolSlotSVGProps,
} from "../tool_graphics";
import { BotOriginQuadrant } from "../../../../interfaces";
import { Color } from "../../../../../ui";
import { svgMount } from "../../../../../__test_support__/svg_mount";
import { Actions } from "../../../../../constants";
import { shallow } from "enzyme";
import { fakeToolSlot } from "../../../../../__test_support__/fake_state/resources";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
describe("<ToolbaySlot />", () => {
const fakeProps = (): ToolSlotGraphicProps => ({
@ -15,6 +19,7 @@ describe("<ToolbaySlot />", () => {
pulloutDirection: 0,
quadrant: 2,
xySwap: false,
occupied: true,
});
it.each<[number, BotOriginQuadrant, boolean, string]>([
@ -89,6 +94,29 @@ describe("<Tool/>", () => {
});
};
it("renders empty tool slot styling", () => {
const p = fakeProps();
p.tool = ToolNames.emptyToolSlot;
const wrapper = svgMount(<Tool {...p} />);
const props = wrapper.find("circle").last().props();
expect(props.r).toEqual(34);
expect(props.fill).toEqual("none");
expect(props.strokeDasharray).toEqual("10 5");
});
it("renders empty tool slot hover styling", () => {
const p = fakeProps();
p.tool = ToolNames.emptyToolSlot;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
const props = wrapper.find("circle").first().props();
expect(props.fill).toEqual(Color.darkGray);
});
it("sets hover state for empty tool slot", () => {
testHoverActions(ToolNames.emptyToolSlot);
});
it("renders standard tool styling", () => {
const wrapper = svgMount(<Tool {...fakeProps()} />);
const props = wrapper.find("circle").last().props();
@ -107,12 +135,96 @@ describe("<Tool/>", () => {
});
it("sets hover state for tool", () => {
testHoverActions("tool");
testHoverActions(ToolNames.tool);
});
it("renders special tool styling: weeder", () => {
const p = fakeProps();
p.tool = ToolNames.weeder;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#weeder").find("line");
expect(elements.length).toEqual(2);
});
it("renders weeder hover styling", () => {
const p = fakeProps();
p.tool = ToolNames.weeder;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
expect(wrapper.find("#weeder").find("circle").props().fill)
.toEqual(Color.darkGray);
});
it("sets hover state for weeder", () => {
testHoverActions(ToolNames.weeder);
});
it("renders special tool styling: watering nozzle", () => {
const p = fakeProps();
p.tool = ToolNames.wateringNozzle;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#watering-nozzle").find("circle");
expect(elements.length).toEqual(3);
});
it("renders watering nozzle hover styling", () => {
const p = fakeProps();
p.tool = ToolNames.wateringNozzle;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
expect(wrapper.find("#watering-nozzle").find("circle").at(1).props().fill)
.toEqual(Color.darkGray);
});
it("sets hover state for watering nozzle", () => {
testHoverActions(ToolNames.wateringNozzle);
});
it("renders special tool styling: seeder", () => {
const p = fakeProps();
p.tool = ToolNames.seeder;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#seeder").find("circle");
expect(elements.length).toEqual(2);
});
it("renders seeder hover styling", () => {
const p = fakeProps();
p.tool = ToolNames.seeder;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
expect(wrapper.find("#seeder").find("circle").first().props().fill)
.toEqual(Color.darkGray);
});
it("sets hover state for seeder", () => {
testHoverActions(ToolNames.seeder);
});
it("renders special tool styling: soil sensor", () => {
const p = fakeProps();
p.tool = ToolNames.soilSensor;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#soil-sensor").find("line");
expect(elements.length).toEqual(2);
});
it("renders soil sensor hover styling", () => {
const p = fakeProps();
p.tool = ToolNames.soilSensor;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
expect(wrapper.find("#soil-sensor").find("circle").props().fill)
.toEqual(Color.darkGray);
});
it("sets hover state for soil sensor", () => {
testHoverActions(ToolNames.soilSensor);
});
it("renders special tool styling: bin", () => {
const p = fakeProps();
p.tool = "seedBin";
p.tool = ToolNames.seedBin;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#seed-bin").find("circle");
expect(elements.length).toEqual(2);
@ -121,20 +233,19 @@ describe("<Tool/>", () => {
it("renders bin hover styling", () => {
const p = fakeProps();
p.tool = "seedBin";
p.tool = ToolNames.seedBin;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
p.toolProps.hovered = true;
expect(wrapper.find("#seed-bin").find("circle").length).toEqual(3);
});
it("sets hover state for bin", () => {
testHoverActions("seedBin");
testHoverActions(ToolNames.seedBin);
});
it("renders special tool styling: tray", () => {
const p = fakeProps();
p.tool = "seedTray";
p.tool = ToolNames.seedTray;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#seed-tray");
expect(elements.find("circle").length).toEqual(2);
@ -144,20 +255,19 @@ describe("<Tool/>", () => {
it("renders tray hover styling", () => {
const p = fakeProps();
p.tool = "seedTray";
p.tool = ToolNames.seedTray;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
p.toolProps.hovered = true;
expect(wrapper.find("#seed-tray").find("circle").length).toEqual(3);
});
it("sets hover state for tray", () => {
testHoverActions("seedTray");
testHoverActions(ToolNames.seedTray);
});
it("renders special tool styling: trough", () => {
const p = fakeProps();
p.tool = "seedTrough";
p.tool = ToolNames.seedTrough;
const wrapper = svgMount(<Tool {...p} />);
const elements = wrapper.find("#seed-trough");
expect(elements.find("circle").length).toEqual(0);
@ -166,15 +276,49 @@ describe("<Tool/>", () => {
it("renders trough hover styling", () => {
const p = fakeProps();
p.tool = "seedTrough";
p.tool = ToolNames.seedTrough;
p.toolProps.hovered = true;
const wrapper = svgMount(<Tool {...p} />);
p.toolProps.hovered = true;
expect(wrapper.find("#seed-trough").find("circle").length).toEqual(0);
expect(wrapper.find("#seed-trough").find("rect").length).toEqual(1);
});
it("sets hover state for trough", () => {
testHoverActions("seedTrough");
testHoverActions(ToolNames.seedTrough);
});
});
describe("<ToolSVG />", () => {
const fakeProps = (): ToolSVGProps => ({
toolName: "seed trough",
});
it("renders trough", () => {
const wrapper = shallow(<ToolSVG {...fakeProps()} />);
expect(wrapper.find("svg").props().viewBox).toEqual("-25 0 50 1");
});
});
describe("<ToolSlotSVG />", () => {
const fakeProps = (): ToolSlotSVGProps => ({
toolSlot: fakeToolSlot(),
toolName: "seeder",
renderRotation: false,
xySwap: false,
quadrant: 2,
});
it("renders slot", () => {
const p = fakeProps();
p.toolSlot.body.pullout_direction = ToolPulloutDirection.POSITIVE_X;
const wrapper = shallow(<ToolSlotSVG {...p} />);
expect(wrapper.find(ToolbaySlot).length).toEqual(1);
});
it("doesn't render slot", () => {
const p = fakeProps();
p.toolSlot.body.pullout_direction = ToolPulloutDirection.NONE;
const wrapper = shallow(<ToolSlotSVG {...p} />);
expect(wrapper.find(ToolbaySlot).length).toEqual(0);
});
});

View File

@ -7,50 +7,61 @@ describe("textAnchorPosition()", () => {
const MIDDLE_BOTTOM = { anchor: "middle", x: 0, y: -40 };
it("returns correct label position: positive x", () => {
expect(textAnchorPosition(1, 1, false)).toEqual(END);
expect(textAnchorPosition(1, 2, false)).toEqual(START);
expect(textAnchorPosition(1, 3, false)).toEqual(START);
expect(textAnchorPosition(1, 4, false)).toEqual(END);
expect(textAnchorPosition(1, 1, true)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(1, 2, true)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(1, 3, true)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(1, 4, true)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(1, 1, false, false)).toEqual(END);
expect(textAnchorPosition(1, 2, false, false)).toEqual(START);
expect(textAnchorPosition(1, 3, false, false)).toEqual(START);
expect(textAnchorPosition(1, 4, false, false)).toEqual(END);
expect(textAnchorPosition(1, 1, true, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(1, 2, true, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(1, 3, true, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(1, 4, true, false)).toEqual(MIDDLE_BOTTOM);
});
it("returns correct label position: negative x", () => {
expect(textAnchorPosition(2, 1, false)).toEqual(START);
expect(textAnchorPosition(2, 2, false)).toEqual(END);
expect(textAnchorPosition(2, 3, false)).toEqual(END);
expect(textAnchorPosition(2, 4, false)).toEqual(START);
expect(textAnchorPosition(2, 1, true)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(2, 2, true)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(2, 3, true)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(2, 4, true)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(2, 1, false, false)).toEqual(START);
expect(textAnchorPosition(2, 2, false, false)).toEqual(END);
expect(textAnchorPosition(2, 3, false, false)).toEqual(END);
expect(textAnchorPosition(2, 4, false, false)).toEqual(START);
expect(textAnchorPosition(2, 1, true, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(2, 2, true, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(2, 3, true, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(2, 4, true, false)).toEqual(MIDDLE_TOP);
});
it("returns correct label position: positive y", () => {
expect(textAnchorPosition(3, 1, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(3, 2, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(3, 3, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(3, 4, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(3, 1, true)).toEqual(END);
expect(textAnchorPosition(3, 2, true)).toEqual(START);
expect(textAnchorPosition(3, 3, true)).toEqual(START);
expect(textAnchorPosition(3, 4, true)).toEqual(END);
expect(textAnchorPosition(3, 1, false, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(3, 2, false, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(3, 3, false, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(3, 4, false, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(3, 1, true, false)).toEqual(END);
expect(textAnchorPosition(3, 2, true, false)).toEqual(START);
expect(textAnchorPosition(3, 3, true, false)).toEqual(START);
expect(textAnchorPosition(3, 4, true, false)).toEqual(END);
});
it("returns correct label position: negative y", () => {
expect(textAnchorPosition(4, 1, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(4, 2, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(4, 3, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(4, 4, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(4, 1, true)).toEqual(START);
expect(textAnchorPosition(4, 2, true)).toEqual(END);
expect(textAnchorPosition(4, 3, true)).toEqual(END);
expect(textAnchorPosition(4, 4, true)).toEqual(START);
expect(textAnchorPosition(4, 1, false, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(4, 2, false, false)).toEqual(MIDDLE_BOTTOM);
expect(textAnchorPosition(4, 3, false, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(4, 4, false, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(4, 1, true, false)).toEqual(START);
expect(textAnchorPosition(4, 2, true, false)).toEqual(END);
expect(textAnchorPosition(4, 3, true, false)).toEqual(END);
expect(textAnchorPosition(4, 4, true, false)).toEqual(START);
});
it("returns correct label position: no pullout direction", () => {
expect(textAnchorPosition(0, 1, false, false)).toEqual(END);
expect(textAnchorPosition(1, 1, false, true)).toEqual(END);
expect(textAnchorPosition(0, 1, true, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(1, 1, true, true)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(0, 2, false, false)).toEqual(START);
expect(textAnchorPosition(1, 2, false, true)).toEqual(START);
expect(textAnchorPosition(0, 2, true, false)).toEqual(MIDDLE_TOP);
expect(textAnchorPosition(1, 2, true, true)).toEqual(MIDDLE_TOP);
});
it("handles bad data", () => {
expect(textAnchorPosition(1.1, 1.1, false)).toEqual(START);
expect(textAnchorPosition(1.1, 1.1, false, false)).toEqual(START);
});
});

View File

@ -4,11 +4,6 @@ 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 {
@ -57,16 +52,6 @@ describe("<ToolSlotLayer/>", () => {
expect(result.find(ToolSlotPoint).length).toEqual(1);
});
it("navigates to tools page", async () => {
mockDev = true;
mockPath = "/app/designer/plants";
const p = fakeProps();
const wrapper = shallow(<ToolSlotLayer {...p} />);
const tools = wrapper.find("g").first();
await tools.simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/tools");
});
it("doesn't navigate to tools page", async () => {
mockPath = "/app/designer/plants/1";
const p = fakeProps();

View File

@ -1,8 +1,3 @@
let mockDev = false;
jest.mock("../../../../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
@ -17,10 +12,6 @@ import { svgMount } from "../../../../../__test_support__/svg_mount";
import { history } from "../../../../../history";
describe("<ToolSlotPoint/>", () => {
beforeEach(() => {
mockDev = false;
});
const fakeProps = (): TSPProps => ({
mapTransformProps: fakeMapTransformProps(),
botPositionX: undefined,
@ -48,10 +39,6 @@ describe("<ToolSlotPoint/>", () => {
const p = fakeProps();
p.slot.toolSlot.body.id = 1;
const wrapper = svgMount(<ToolSlotPoint {...p} />);
mockDev = true;
wrapper.find("g").first().simulate("click");
expect(history.push).not.toHaveBeenCalled();
mockDev = false;
wrapper.find("g").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1");
});
@ -71,7 +58,7 @@ describe("<ToolSlotPoint/>", () => {
p.slot.tool = undefined;
p.hoveredToolSlot = p.slot.toolSlot.uuid;
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("text").text()).toEqual("empty");
expect(wrapper.find("text").text()).toEqual("Empty");
expect(wrapper.find("text").props().dx).toEqual(40);
});
@ -80,6 +67,34 @@ describe("<ToolSlotPoint/>", () => {
expect(wrapper.find("text").props().visibility).toEqual("hidden");
});
it("renders weeder", () => {
const p = fakeProps();
if (p.slot.tool) { p.slot.tool.body.name = "weeder"; }
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("#weeder").length).toEqual(1);
});
it("renders watering nozzle", () => {
const p = fakeProps();
if (p.slot.tool) { p.slot.tool.body.name = "watering nozzle"; }
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("#watering-nozzle").length).toEqual(1);
});
it("renders seeder", () => {
const p = fakeProps();
if (p.slot.tool) { p.slot.tool.body.name = "seeder"; }
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("#seeder").length).toEqual(1);
});
it("renders soil sensor", () => {
const p = fakeProps();
if (p.slot.tool) { p.slot.tool.body.name = "soil sensor"; }
const wrapper = svgMount(<ToolSlotPoint {...p} />);
expect(wrapper.find("#soil-sensor").length).toEqual(1);
});
it("renders bin", () => {
const p = fakeProps();
if (p.slot.tool) { p.slot.tool.body.name = "seed bin"; }

View File

@ -5,6 +5,9 @@ import { BotOriginQuadrant } from "../../../interfaces";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import { Actions } from "../../../../constants";
import { UUID } from "../../../../resources/interfaces";
import { TaggedToolSlotPointer } from "farmbot";
import { reduceToolName } from "./tool_slot_point";
import { noop } from "lodash";
export interface ToolGraphicProps {
x: number;
@ -27,6 +30,7 @@ export interface ToolSlotGraphicProps {
pulloutDirection: ToolPulloutDirection;
quadrant: BotOriginQuadrant;
xySwap: boolean;
occupied: boolean;
}
const toolbaySlotAngle = (
@ -57,10 +61,15 @@ const toolbaySlotAngle = (
};
export enum ToolNames {
weeder = "weeder",
wateringNozzle = "wateringNozzle",
seeder = "seeder",
soilSensor = "soilSensor",
seedBin = "seedBin",
seedTray = "seedTray",
seedTrough = "seedTrough",
tool = "tool",
emptyToolSlot = "emptyToolSlot",
}
export const ToolbaySlot = (props: ToolSlotGraphicProps) => {
@ -82,7 +91,7 @@ export const ToolbaySlot = (props: ToolSlotGraphicProps) => {
</g>
</defs>
<use style={{ pointerEvents: "none" }}
<use style={props.occupied ? { pointerEvents: "none" } : {}}
xlinkHref={"#toolbay-slot-" + id}
transform={
`rotate(${angle}, ${x}, ${y})`} />
@ -91,9 +100,14 @@ export const ToolbaySlot = (props: ToolSlotGraphicProps) => {
export const Tool = (props: ToolProps) => {
switch (props.tool) {
case ToolNames.weeder: return <Weeder {...props.toolProps} />;
case ToolNames.wateringNozzle: return <WateringNozzle {...props.toolProps} />;
case ToolNames.seeder: return <Seeder {...props.toolProps} />;
case ToolNames.soilSensor: return <SoilSensor {...props.toolProps} />;
case ToolNames.seedBin: return <SeedBin {...props.toolProps} />;
case ToolNames.seedTray: return <SeedTray {...props.toolProps} />;
case ToolNames.seedTrough: return <SeedTrough {...props.toolProps} />;
case ToolNames.emptyToolSlot: return <EmptySlot {...props.toolProps} />;
default: return <StandardTool {...props.toolProps} />;
}
};
@ -115,6 +129,115 @@ const StandardTool = (props: ToolGraphicProps) => {
</g>;
};
const EmptySlot = (props: ToolGraphicProps) => {
const { x, y, hovered, dispatch, uuid } = props;
return <g id={"empty-tool-slot"}
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<circle
cx={x}
cy={y}
r={35}
fillOpacity={0.2}
fill={hovered ? Color.darkGray : Color.mediumGray} />
<circle
cx={x}
cy={y}
r={34}
fill={"none"}
stroke={Color.mediumGray}
opacity={0.5}
strokeWidth={2}
strokeDasharray={"10 5"} />
</g>;
};
const Weeder = (props: ToolGraphicProps) => {
const { x, y, hovered, dispatch, uuid } = props;
const size = 10;
return <g id={"weeder"}
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<circle
cx={x}
cy={y}
r={35}
fillOpacity={0.5}
fill={hovered ? Color.darkGray : Color.mediumGray} />
<line
x1={x - size} y1={y - size} x2={x + size} y2={y + size}
stroke={Color.darkGray} opacity={0.8} strokeWidth={5} />
<line
x1={x - size} y1={y + size} x2={x + size} y2={y - size}
stroke={Color.darkGray} opacity={0.8} strokeWidth={5} />
</g>;
};
const WateringNozzle = (props: ToolGraphicProps) => {
const { x, y, hovered, dispatch, uuid } = props;
return <g id={"watering-nozzle"}
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<defs>
<pattern id="WateringNozzlePattern"
x={0} y={0} width={0.2} height={0.2}>
<circle cx={5} cy={5} r={2} fill={Color.darkGray} fillOpacity={0.8} />
</pattern>
</defs>
<circle
cx={x}
cy={y}
r={35}
fillOpacity={0.5}
fill={hovered ? Color.darkGray : Color.mediumGray} />
<circle
cx={x} cy={y} r={25}
fill="url(#WateringNozzlePattern)" />
</g>;
};
const Seeder = (props: ToolGraphicProps) => {
const { x, y, hovered, dispatch, uuid } = props;
const size = 10;
return <g id={"seeder"}
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<circle
cx={x}
cy={y}
r={35}
fillOpacity={0.5}
fill={hovered ? Color.darkGray : Color.mediumGray} />
<circle
cx={x} cy={y} r={size}
fillOpacity={0.8}
fill={Color.darkGray} />
</g>;
};
const SoilSensor = (props: ToolGraphicProps) => {
const { x, y, hovered, dispatch, uuid } = props;
const size = 20;
return <g id={"soil-sensor"}
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>
<circle
cx={x}
cy={y}
r={35}
fillOpacity={0.5}
fill={hovered ? Color.darkGray : Color.mediumGray} />
<line
x1={x - size} y1={y} x2={x - size / 2} y2={y}
stroke={Color.darkGray} opacity={0.8} strokeWidth={5} />
<line
x1={x + size} y1={y} x2={x + size / 2} y2={y}
stroke={Color.darkGray} opacity={0.8} strokeWidth={5} />
</g>;
};
const seedBinGradient =
<radialGradient id="SeedBinGradient">
<stop offset="5%" stopColor="rgb(0, 0, 0)" stopOpacity={0.3} />
@ -214,3 +337,62 @@ const SeedTrough = (props: ToolGraphicProps) => {
fill={hovered ? Color.darkGray : Color.mediumGray} />
</g>;
};
export interface ToolSlotSVGProps {
toolSlot: TaggedToolSlotPointer;
toolName: string | undefined;
renderRotation: boolean;
xySwap?: boolean;
quadrant?: BotOriginQuadrant;
}
export const ToolSlotSVG = (props: ToolSlotSVGProps) => {
const xySwap = props.renderRotation ? !!props.xySwap : false;
const toolProps = {
x: 0, y: 0,
hovered: false,
dispatch: noop,
uuid: props.toolSlot.uuid,
xySwap,
};
const pulloutDirection = props.renderRotation
? props.toolSlot.body.pullout_direction
: ToolPulloutDirection.POSITIVE_X;
const quadrant = props.renderRotation && props.quadrant ? props.quadrant : 2;
const viewBox = props.renderRotation ? "-25 0 50 1" : "-25 0 50 1";
return props.toolSlot.body.gantry_mounted
? <svg width="3rem" height="3rem" viewBox={viewBox}>
<GantryToolSlot x={0} y={0} xySwap={xySwap} />
{props.toolSlot.body.tool_id &&
<Tool tool={reduceToolName(props.toolName)} toolProps={toolProps} />}
</svg>
: <svg width="3rem" height="3rem" viewBox={`-50 0 100 1`}>
{props.toolSlot.body.pullout_direction &&
<ToolbaySlot
id={props.toolSlot.body.id}
x={0}
y={0}
pulloutDirection={pulloutDirection}
quadrant={quadrant}
occupied={false}
xySwap={xySwap} />}
{(props.toolSlot.body.tool_id ||
!props.toolSlot.body.pullout_direction) &&
<Tool tool={reduceToolName(props.toolName)} toolProps={toolProps} />}
</svg>;
};
export interface ToolSVGProps {
toolName: string | undefined;
}
export const ToolSVG = (props: ToolSVGProps) => {
const toolProps = {
x: 0, y: 0, hovered: false, dispatch: noop, uuid: "", xySwap: false,
};
const viewBox = reduceToolName(props.toolName) === ToolNames.seedTrough
? "-25 0 50 1" : "-40 0 80 1";
return <svg width="3rem" height="3rem" viewBox={viewBox}>
<Tool tool={reduceToolName(props.toolName)} toolProps={toolProps} />}
</svg>;
};

View File

@ -13,9 +13,17 @@ enum Anchor {
export const textAnchorPosition = (
pulloutDirection: ToolPulloutDirection,
quadrant: BotOriginQuadrant,
xySwap: boolean): { x: number, y: number, anchor: string } => {
xySwap: boolean,
gantryMounted: boolean,
): { x: number, y: number, anchor: string } => {
const rawAnchor = () => {
const direction = pulloutDirection + (xySwap ? 2 : 0);
const noDirection = !pulloutDirection || gantryMounted;
const noDirectionXY = xySwap
? ToolPulloutDirection.POSITIVE_Y
: ToolPulloutDirection.POSITIVE_X;
const direction = noDirection
? noDirectionXY
: pulloutDirection + (xySwap ? 2 : 0);
switch (direction > 4 ? direction % 4 : direction) {
case ToolPulloutDirection.POSITIVE_X: return Anchor.start;
case ToolPulloutDirection.NEGATIVE_X: return Anchor.end;
@ -51,12 +59,15 @@ interface ToolLabelProps {
pulloutDirection: ToolPulloutDirection;
quadrant: BotOriginQuadrant;
xySwap: boolean;
gantryMounted: boolean;
}
export const ToolLabel = (props: ToolLabelProps) => {
const { toolName, hovered, x, y, pulloutDirection, quadrant, xySwap } = props;
const labelAnchor = textAnchorPosition(pulloutDirection, quadrant, xySwap);
const {
toolName, hovered, x, y, pulloutDirection, quadrant, xySwap, gantryMounted,
} = props;
const labelAnchor = textAnchorPosition
(pulloutDirection, quadrant, xySwap, gantryMounted);
return <text textAnchor={labelAnchor.anchor}
visibility={hovered ? "visible" : "hidden"}
x={x}

View File

@ -2,9 +2,7 @@ import * as React from "react";
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
import { ToolSlotPoint } from "./tool_slot_point";
import { MapTransformProps } from "../../interfaces";
import { history, getPathArray } from "../../../../history";
import { maybeNoPointer } from "../../util";
import { DevSettings } from "../../../../account/dev/dev_support";
export interface ToolSlotLayerProps {
visible: boolean;
@ -16,17 +14,11 @@ export interface ToolSlotLayerProps {
}
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");
const { slots, visible, mapTransformProps } = props;
const cursor = canClickTool ? "pointer" : "default";
return <g
id="toolslot-layer"
onClick={goToToolsPage}
style={maybeNoPointer({ cursor: cursor })}>
style={maybeNoPointer({ cursor: "pointer" })}>
{visible &&
slots.map(slot =>
<ToolSlotPoint

View File

@ -5,8 +5,8 @@ import { MapTransformProps } from "../../interfaces";
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
import { ToolLabel } from "./tool_label";
import { includes } from "lodash";
import { DevSettings } from "../../../../account/dev/dev_support";
import { history } from "../../../../history";
import { t } from "../../../../i18next_wrapper";
export interface TSPProps {
slot: SlotWithTool;
@ -16,8 +16,13 @@ export interface TSPProps {
hoveredToolSlot: UUID | undefined;
}
const reduceToolName = (raw: string | undefined) => {
export const reduceToolName = (raw: string | undefined) => {
const lower = (raw || "").toLowerCase();
if (raw == "Empty") { return ToolNames.emptyToolSlot; }
if (includes(lower, "weeder")) { return ToolNames.weeder; }
if (includes(lower, "watering nozzle")) { return ToolNames.wateringNozzle; }
if (includes(lower, "seeder")) { return ToolNames.seeder; }
if (includes(lower, "soil sensor")) { return ToolNames.soilSensor; }
if (includes(lower, "seed bin")) { return ToolNames.seedBin; }
if (includes(lower, "seed tray")) { return ToolNames.seedTray; }
if (includes(lower, "seed trough")) { return ToolNames.seedTrough; }
@ -32,7 +37,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
const { quadrant, xySwap } = mapTransformProps;
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
const toolName = props.slot.tool ? props.slot.tool.body.name : "empty";
const toolName = props.slot.tool ? props.slot.tool.body.name : t("Empty");
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
const toolProps = {
x: qx,
@ -43,15 +48,15 @@ export const ToolSlotPoint = (props: TSPProps) => {
xySwap,
};
return <g id={"toolslot-" + id}
onClick={() => !DevSettings.futureFeaturesEnabled() &&
history.push(`/app/designer/tool-slots/${id}`)}>
{pullout_direction &&
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}>
{pullout_direction && !gantry_mounted &&
<ToolbaySlot
id={id}
id={-(id || 1)}
x={qx}
y={qy}
pulloutDirection={pullout_direction}
quadrant={quadrant}
occupied={!!props.slot.tool}
xySwap={xySwap} />}
{gantry_mounted && <GantryToolSlot x={qx} y={qy} xySwap={xySwap} />}
@ -67,6 +72,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
x={qx}
y={qy}
pulloutDirection={pullout_direction}
gantryMounted={gantry_mounted}
quadrant={quadrant}
xySwap={xySwap} />
</g>;

View File

@ -93,11 +93,15 @@ interface NavTabProps {
linkTo: string;
title: string;
icon?: string;
desktopHide?: boolean;
}
const NavTab = (props: NavTabProps) =>
<Link to={props.linkTo} style={{ flex: 0.3 }}
className={getCurrentTab() === props.panel ? "active" : ""}>
className={[
getCurrentTab() === props.panel ? "active" : "",
props.desktopHide ? "desktop-hide" : "",
].join(" ")}>
<img {...common}
src={TAB_ICON[props.panel]} title={props.title} />
</Link>;
@ -109,7 +113,7 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
<div className="panel-tabs">
<NavTab panel={Panel.Map}
linkTo={"/app/designer"}
title={t("Map")} />
title={t("Map")} desktopHide={true} />
<NavTab
panel={Panel.Plants}
linkTo={"/app/designer/plants"}
@ -139,11 +143,10 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
panel={Panel.Weeds}
linkTo={"/app/designer/weeds"}
title={t("Weeds")} />
{!DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.Tools}
linkTo={"/app/designer/tools"}
title={t("Tools")} />}
<NavTab
panel={Panel.Tools}
linkTo={"/app/designer/tools"}
title={t("Tools")} />
<NavTab
panel={Panel.Settings}
icon={"fa fa-gear"}

View File

@ -9,32 +9,19 @@ import { UUID } from "../../resources/interfaces";
import { edit, save } from "../../api/crud";
import { EditPlantStatusProps } from "./plant_panel";
const PLANT_STAGES: DropDownItem[] = [
{ value: "planned", label: t("Planned") },
{ value: "planted", label: t("Planted") },
{ value: "sprouted", label: t("Sprouted") },
{ value: "harvested", label: t("Harvested") },
export const PLANT_STAGE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
planned: { label: t("Planned"), value: "planned" },
planted: { label: t("Planted"), value: "planted" },
sprouted: { label: t("Sprouted"), value: "sprouted" },
harvested: { label: t("Harvested"), value: "harvested" },
});
export const PLANT_STAGE_LIST = () => [
PLANT_STAGE_DDI_LOOKUP().planned,
PLANT_STAGE_DDI_LOOKUP().planted,
PLANT_STAGE_DDI_LOOKUP().sprouted,
PLANT_STAGE_DDI_LOOKUP().harvested,
];
const PLANT_STAGES_DDI = {
[PLANT_STAGES[0].value]: {
label: PLANT_STAGES[0].label,
value: PLANT_STAGES[0].value
},
[PLANT_STAGES[1].value]: {
label: PLANT_STAGES[1].label,
value: PLANT_STAGES[1].value
},
[PLANT_STAGES[2].value]: {
label: PLANT_STAGES[2].label,
value: PLANT_STAGES[2].value
},
[PLANT_STAGES[3].value]: {
label: PLANT_STAGES[3].label,
value: PLANT_STAGES[3].value
},
};
/** Change `planted_at` value based on `plant_stage` update. */
const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => {
const update: PlantOptions = { plant_stage };
@ -52,8 +39,8 @@ const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => {
export function EditPlantStatus(props: EditPlantStatusProps) {
const { plantStatus, updatePlant, uuid } = props;
return <FBSelect
list={PLANT_STAGES}
selectedItem={PLANT_STAGES_DDI[plantStatus]}
list={PLANT_STAGE_LIST()}
selectedItem={PLANT_STAGE_DDI_LOOKUP()[plantStatus]}
onChange={ddi =>
updatePlant(uuid, getUpdateByPlantStage(ddi.value as PlantStage))} />;
}
@ -70,7 +57,7 @@ export const PlantStatusBulkUpdate = (props: PlantStatusBulkUpdateProps) =>
<p>{t("update plant status to")}</p>
<FBSelect
key={JSON.stringify(props.selected)}
list={PLANT_STAGES}
list={PLANT_STAGE_LIST()}
selectedItem={undefined}
customNullLabel={t("Select a status")}
onChange={ddi => {

View File

@ -6,13 +6,6 @@ jest.mock("../../../api/crud", () => ({
jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import React from "react";
import {
GroupDetailActive, GroupDetailActiveProps
@ -107,19 +100,11 @@ describe("<GroupDetailActive/>", () => {
});
it("shows paths", () => {
mockDev = false;
const p = fakeProps();
const wrapper = mount(<GroupDetailActive {...p} />);
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";

View File

@ -22,7 +22,7 @@ import { Actions } from "../../../constants";
import { edit } from "../../../api/crud";
import { error } from "../../../toast/toast";
import { svgMount } from "../../../__test_support__/svg_mount";
import { SORT_OPTIONS } from "../point_group_sort_selector";
import { SORT_OPTIONS } from "../point_group_sort";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
/**

View File

@ -1,45 +1,8 @@
import {
isSortType, sortTypeChange, SORT_OPTIONS
} from "../point_group_sort_selector";
import { DropDownItem } from "../../../ui";
import { SORT_OPTIONS } from "../point_group_sort";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { TaggedPoint } from "farmbot";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
const tests: [string, boolean][] = [
["", false],
["nope", false],
["random", true],
["xy_ascending", true],
["xy_descending", true],
["yx_ascending", true],
["yx_descending", true]
];
describe("isSortType", () => {
it("identifies malformed sort types", () => {
tests.map(([sortType, valid]) => {
expect(isSortType(sortType)).toBe(valid);
});
});
});
describe("sortTypeChange", () => {
it("selectively triggers the callback", () => {
tests.map(([value, valid]) => {
const cb = jest.fn();
const ddi: DropDownItem = { value, label: "TEST" };
if (valid) {
sortTypeChange(cb)(ddi);
expect(cb).toHaveBeenCalledWith(value);
} else {
sortTypeChange(cb)(ddi);
expect(cb).not.toHaveBeenCalled();
}
});
});
});
describe("sort()", () => {
const phony = (name: string, x: number, y: number): TaggedPoint => {
const plant = fakePlant();

View File

@ -9,7 +9,6 @@ import {
AddEqCriteria, AddNumberCriteria, editCriteria, AddStringCriteria,
toggleStringCriteria,
POINTER_TYPE_LIST,
PLANT_STAGE_LIST
} from "..";
import {
AddEqCriteriaProps, NumberCriteriaProps, DEFAULT_CRITERIA,
@ -19,6 +18,7 @@ import {
fakePointGroup
} from "../../../../__test_support__/fake_state/resources";
import { PointGroup } from "farmbot/dist/resources/api_resources";
import { PLANT_STAGE_LIST } from "../../../plants/edit_plant_status";
describe("<AddEqCriteria<string> />", () => {
const fakeProps = (): AddEqCriteriaProps<string> => ({

View File

@ -57,7 +57,7 @@ describe("selectPointsByCriteria()", () => {
it("matches age greater than 1 day old", () => {
const criteria = fakeCriteria();
criteria.day = { days: 1, op: ">" };
criteria.day = { days_ago: 1, op: ">" };
const matchingPoint = fakePoint();
matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z";
const otherPoint = fakePoint();
@ -70,7 +70,7 @@ describe("selectPointsByCriteria()", () => {
it("matches age less than 1 day old", () => {
const criteria = fakeCriteria();
criteria.day = { days: 1, op: "<" };
criteria.day = { days_ago: 1, op: "<" };
const matchingPoint = fakePoint();
matchingPoint.body.created_at = "2020-02-20T20:00:00.000Z";
const otherPoint = fakePoint();
@ -83,7 +83,7 @@ describe("selectPointsByCriteria()", () => {
it("matches planted date less than 1 day old", () => {
const criteria = fakeCriteria();
criteria.day = { days: 1, op: "<" };
criteria.day = { days_ago: 1, op: "<" };
const matchingPoint = fakePlant();
matchingPoint.body.planted_at = "2020-02-20T20:00:00.000Z";
matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z";

View File

@ -36,7 +36,7 @@ describe("editCriteria()", () => {
it("edits criteria: full update", () => {
const group = fakePointGroup();
const criteria: PointGroup["criteria"] = {
day: { days: 1, op: "<" },
day: { days_ago: 1, op: "<" },
string_eq: { openfarm_slug: ["slug"] },
number_eq: { x: [0] },
number_gt: { x: 0 },

View File

@ -100,7 +100,7 @@ describe("<DaySelection />", () => {
currentTarget: { value: "1" }
});
const expectedBody = cloneDeep(p.group.body);
expectedBody.criteria.day.days = 1;
expectedBody.criteria.day.days_ago = 1;
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
});

View File

@ -10,6 +10,9 @@ import {
AddNumberCriteriaState,
AddStringCriteriaProps,
} from "./interfaces";
import {
PLANT_STAGE_DDI_LOOKUP, PLANT_STAGE_LIST
} from "../../plants/edit_plant_status";
export class AddEqCriteria<T extends string | number>
extends React.Component<AddEqCriteriaProps<T>, AddEqCriteriaState> {
@ -70,7 +73,7 @@ export const CRITERIA_TYPE_LIST = () => [
export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
Plant: { label: t("Plants"), value: "Plant" },
GenericPointer: { label: t("Points"), value: "GenericPointer" },
ToolSlot: { label: t("Tool Slots"), value: "ToolSlot" },
ToolSlot: { label: t("Slots"), value: "ToolSlot" },
});
export const POINTER_TYPE_LIST = () => [
POINTER_TYPE_DDI_LOOKUP().Plant,
@ -78,19 +81,6 @@ export const POINTER_TYPE_LIST = () => [
POINTER_TYPE_DDI_LOOKUP().ToolSlot,
];
export const PLANT_STAGE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
planned: { label: t("Planned"), value: "planned" },
planted: { label: t("Planted"), value: "planted" },
sprouted: { label: t("Sprouted"), value: "sprouted" },
harvested: { label: t("Harvested"), value: "harvested" },
});
export const PLANT_STAGE_LIST = () => [
PLANT_STAGE_DDI_LOOKUP().planned,
PLANT_STAGE_DDI_LOOKUP().planted,
PLANT_STAGE_DDI_LOOKUP().sprouted,
PLANT_STAGE_DDI_LOOKUP().harvested,
];
export class AddStringCriteria
extends React.Component<AddStringCriteriaProps, AddEqCriteriaState> {
state: AddEqCriteriaState = { key: "", value: "" };

View File

@ -30,11 +30,11 @@ const checkCriteria =
? point.body.planted_at
: point.body.created_at);
const compareDate = moment(now)
.subtract(criteria[criteriaKey].days, "days");
.subtract(criteria[criteriaKey].days_ago, "days");
const matchesDays = criteria[criteriaKey].op == "<"
? pointDate.isAfter(compareDate)
: pointDate.isBefore(compareDate);
return matchesDays || !criteria[criteriaKey].days;
return matchesDays || !criteria[criteriaKey].days_ago;
}
};

View File

@ -2,7 +2,7 @@ import { TaggedPointGroup } from "farmbot";
import { PointGroup } from "farmbot/dist/resources/api_resources";
export const DEFAULT_CRITERIA: Readonly<PointGroup["criteria"]> = {
day: { op: "<", days: 0 },
day: { op: "<", days_ago: 0 },
number_eq: {},
number_gt: {},
number_lt: {},

View File

@ -3,7 +3,7 @@ import { cloneDeep, capitalize } from "lodash";
import { Row, Col, FBSelect, DropDownItem } from "../../../ui";
import {
AddEqCriteria, toggleEqCriteria, editCriteria, AddNumberCriteria,
POINTER_TYPE_DDI_LOOKUP, PLANT_STAGE_DDI_LOOKUP, AddStringCriteria,
POINTER_TYPE_DDI_LOOKUP, AddStringCriteria,
CRITERIA_TYPE_DDI_LOOKUP, toggleStringCriteria
} from ".";
import {
@ -14,6 +14,7 @@ import {
} from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { PointGroup } from "farmbot/dist/resources/api_resources";
import { PLANT_STAGE_DDI_LOOKUP } from "../../plants/edit_plant_status";
export class EqCriteriaSelection<T extends string | number>
extends React.Component<EqCriteriaSelectionProps<T>> {
@ -105,16 +106,16 @@ export const DaySelection = (props: CriteriaSelectionProps) => {
selectedItem={DAY_OPERATOR_DDI_LOOKUP()[dayCriteria.op]}
onChange={ddi => dispatch(editCriteria(group, {
day: {
days: dayCriteria.days,
days_ago: dayCriteria.days_ago,
op: ddi.value as PointGroup["criteria"]["day"]["op"]
}
}))} />
</Col>
<Col xs={3}>
<input type="number" value={dayCriteria.days} onChange={e => {
<input type="number" value={dayCriteria.days_ago} onChange={e => {
const { op } = dayCriteria;
const days = parseInt(e.currentTarget.value);
dispatch(editCriteria(group, { day: { days, op } }));
const days_ago = parseInt(e.currentTarget.value);
dispatch(editCriteria(group, { day: { days_ago, op } }));
}} />
</Col>
<Col xs={4}>

View File

@ -7,11 +7,10 @@ import {
import { TaggedPointGroup, TaggedPoint } from "farmbot";
import { DeleteButton } from "../../ui/delete_button";
import { save, edit } from "../../api/crud";
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
import { sortGroupBy } from "./point_group_sort";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { PointGroupItem } from "./point_group_item";
import { Paths } from "./paths";
import { DevSettings } from "../../account/dev/dev_support";
import { Feature, ShouldDisplay } from "../../devices/interfaces";
import { ErrorBoundary } from "../../error_boundary";
import {
@ -88,7 +87,7 @@ export class GroupDetailActive
onBack={this.saveGroup}
panelName={Panel.Groups}
panel={Panel.Groups}
title={t("Edit Group")}
title={t("Edit group")}
backTo={"/app/designer/groups"} />
<DesignerPanelContent
panelName={"groups"}>
@ -103,16 +102,12 @@ export class GroupDetailActive
<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} />}
<Paths
key={JSON.stringify(this.pointsSelectedByGroup
.map(p => p.body.id))}
pathPoints={this.pointsSelectedByGroup}
dispatch={dispatch}
group={group} />
<p>
{group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)}
</p>

View File

@ -48,7 +48,7 @@ export class RawGroupListPanel extends React.Component<GroupListPanelProps, Stat
<DesignerPanelTop
panel={Panel.Groups}
linkTo={"/app/designer/plants/select"}
title={t("Add Group")}>
title={t("Add group")}>
<input type="text"
onChange={this.update}
placeholder={t("Search your groups...")} />

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { store } from "../../redux/store";
import { MapTransformProps } from "../map/interfaces";
import { isUndefined } from "lodash";
import { sortGroupBy } from "./point_group_sort_selector";
import { sortGroupBy } from "./point_group_sort";
import { Color } from "../../ui";
import { transformXY } from "../map/util";
import { nn } from "./paths";

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { MapTransformProps } from "../map/interfaces";
import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector";
import { sortGroupBy, sortOptionsTable } from "./point_group_sort";
import { sortBy, isNumber } from "lodash";
import { PointsPathLine } from "./group_order_visual";
import { Color } from "../../ui";

View File

@ -1,6 +1,4 @@
import * as React from "react";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { FBSelect, DropDownItem } from "../../ui";
import { t } from "../../i18next_wrapper";
import { shuffle, sortBy } from "lodash";
import { TaggedPoint } from "farmbot";
@ -18,36 +16,6 @@ export const sortOptionsTable = (): Record<PointGroupSortType, string> => ({
"yx_descending": t("Y/X, Descending"),
}); // Typechecker will remind us when this needs an update. Don't simplify - RC
const optionPlusDescriptions = () =>
(Object
.entries(sortOptionsTable()) as [PointGroupSortType, string][])
.map(x => ({ label: x[1], value: x[0] }));
const optionList =
optionPlusDescriptions().map(x => x.value);
export const isSortType = (x: unknown): x is PointGroupSortType => {
return optionList.includes(x as PointGroupSortType);
};
const selected = (value: PointGroupSortType) => ({
label: t(sortOptionsTable()[value] || value),
value: value
});
export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => {
const { value } = ddi;
isSortType(value) && cb(value);
};
export function PointGroupSortSelector(p: PointGroupSortSelectorProps) {
return <FBSelect
key={p.value}
list={optionPlusDescriptions()}
selectedItem={selected(p.value as PointGroupSortType)}
onChange={sortTypeChange(p.onChange)} />;
}
type Sorter = (p: TaggedPoint[]) => TaggedPoint[];
type SortDictionary = Record<PointGroupSortType, Sorter>;

View File

@ -6,6 +6,7 @@ jest.mock("../../../farmware/weed_detector/actions", () => ({
let mockPath = "/app/designer/points/add";
jest.mock("../../../history", () => ({
history: { push: jest.fn() },
push: jest.fn(),
getPathArray: () => mockPath.split("/"),
}));
@ -70,14 +71,14 @@ describe("<CreatePoints />", () => {
it("renders for points", () => {
mockPath = "/app/designer";
const wrapper = mount(<CreatePoints {...fakeProps()} />);
["create point", "delete", "x", "y", "radius", "color"]
["add point", "delete", "x", "y", "radius", "color"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});
it("renders for weeds", () => {
mockPath = "/app/designer/weeds/add";
const wrapper = mount(<CreatePoints {...fakeProps()} />);
["create weed", "delete", "x", "y", "radius", "color"]
["add weed", "delete", "x", "y", "radius", "color"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});

View File

@ -23,8 +23,9 @@ import {
import { parseIntInput } from "../../util";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
import { getPathArray } from "../../history";
import { history, getPathArray } from "../../history";
import { ListItem } from "../plants/plant_panel";
import { success } from "../../toast/toast";
export function mapStateToProps(props: Everything): CreatePointsProps {
const { position } = props.bot.hardware.location_data;
@ -176,9 +177,13 @@ export class RawCreatePoints
radius: this.attr("r"),
};
this.props.dispatch(initSave("Point", body));
success(this.panel == "weeds"
? t("Weed created.")
: t("Point created."));
this.cancel();
this.loadDefaultPoint();
history.push(`/app/designer/${this.panel}`);
}
PointProperties = () =>
<ul>
<li>
@ -274,7 +279,7 @@ export class RawCreatePoints
<DesignerPanelHeader
panelName={"point-creation"}
panel={panelType}
title={this.panel == "weeds" ? t("Create weed") : t("Create point")}
title={this.panel == "weeds" ? t("Add weed") : t("Add point")}
backTo={`/app/designer/${this.panel}`}
description={panelDescription} />
<DesignerPanelContent panelName={"point-creation"}>

View File

@ -47,7 +47,7 @@ export class PointInventoryItem extends
{label}
</span>
<p className="point-search-item-info">
{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}
<i>{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}</i>
</p>
</div>;
}

Some files were not shown because too many files have changed in this diff Show More