commit
bce0700cd9
33
Gemfile.lock
33
Gemfile.lock
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Move } from "./move/move";
|
|||
import { BooleanSetting } from "../session_keys";
|
||||
import { SensorReadings } from "./sensor_readings/sensor_readings";
|
||||
import { isBotOnline } from "../devices/must_be_online";
|
||||
import { hasSensors } from "../devices/components/firmware_hardware_support";
|
||||
|
||||
/** Controls page. */
|
||||
export class RawControls extends React.Component<Props, {}> {
|
||||
|
@ -24,7 +25,8 @@ export class RawControls extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
get hideSensors() {
|
||||
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors);
|
||||
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors)
|
||||
|| !hasSensors(this.props.firmwareHardware);
|
||||
}
|
||||
|
||||
move = () => <Move
|
||||
|
|
|
@ -7,7 +7,7 @@ import { AxisInputBoxGroup } from "../axis_input_box_group";
|
|||
import { GetWebAppBool } from "./interfaces";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
|
||||
export interface BotPositionRowsProps {
|
||||
|
@ -34,12 +34,12 @@ export const BotPositionRows = (props: BotPositionRowsProps) => {
|
|||
<AxisDisplayGroup
|
||||
position={locationData.position}
|
||||
label={t("Motor Coordinates (mm)")} />
|
||||
{!isExpressBoard(props.firmwareHardware) &&
|
||||
{hasEncoders(props.firmwareHardware) &&
|
||||
getValue(BooleanSetting.scaled_encoders) &&
|
||||
<AxisDisplayGroup
|
||||
position={locationData.scaled_encoders}
|
||||
label={t("Scaled Encoder (mm)")} />}
|
||||
{!isExpressBoard(props.firmwareHardware) &&
|
||||
{hasEncoders(props.firmwareHardware) &&
|
||||
getValue(BooleanSetting.raw_encoders) &&
|
||||
<AxisDisplayGroup
|
||||
position={locationData.raw_encoders}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
|||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
|
||||
|
||||
export const moveWidgetSetting =
|
||||
(toggle: ToggleWebAppBool, getValue: GetWebAppBool) =>
|
||||
|
@ -36,7 +36,7 @@ export const MoveWidgetSettingsMenu = (
|
|||
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
|
||||
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
|
||||
|
||||
{!isExpressBoard(firmwareHardware) &&
|
||||
{hasEncoders(firmwareHardware) &&
|
||||
<div className="display-encoder-data">
|
||||
<p>{t("Display Encoder Data")}</p>
|
||||
<Setting
|
||||
|
|
|
@ -88,4 +88,14 @@ describe("<Peripherals />", () => {
|
|||
clickButton(wrapper, 3, "stock");
|
||||
expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
|
||||
});
|
||||
|
||||
it("hides stock button", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "none";
|
||||
const wrapper = mount(<Peripherals {...p} />);
|
||||
wrapper.setState({ isEditing: true });
|
||||
const btn = wrapper.find("button").at(3);
|
||||
expect(btn.text().toLowerCase()).toContain("stock");
|
||||
expect(btn.props().hidden).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -108,7 +108,7 @@ export class Peripherals
|
|||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
hidden={!isEditing}
|
||||
hidden={!isEditing || this.props.firmwareHardware == "none"}
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={() => this.stockPeripherals.map(p =>
|
||||
|
|
|
@ -72,6 +72,7 @@ describe("<Sensors />", () => {
|
|||
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
|
||||
wrapper.setState({ isEditing: true });
|
||||
clickButton(wrapper, 3, "stock sensors");
|
||||
expect(wrapper.find("button").at(3).props().hidden).toBeFalsy();
|
||||
expect(p.dispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
@ -79,6 +80,18 @@ describe("<Sensors />", () => {
|
|||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount(<Sensors {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
|
||||
const btn = wrapper.find("button").at(3);
|
||||
expect(btn.text().toLowerCase()).toContain("stock");
|
||||
expect(btn.props().hidden).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides stock button", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "none";
|
||||
const wrapper = mount(<Sensors {...p} />);
|
||||
wrapper.setState({ isEditing: true });
|
||||
const btn = wrapper.find("button").at(3);
|
||||
expect(btn.text().toLowerCase()).toContain("stock");
|
||||
expect(btn.props().hidden).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@ import { saveAll, init } from "../../api/crud";
|
|||
import { ToolTips } from "../../constants";
|
||||
import { uniq } from "lodash";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
|
||||
|
||||
export class Sensors extends React.Component<SensorsProps, SensorState> {
|
||||
constructor(props: SensorsProps) {
|
||||
|
@ -80,15 +79,14 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
|
|||
onClick={() => this.newSensor()}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
{!isExpressBoard(this.props.firmwareHardware) &&
|
||||
<button
|
||||
hidden={!isEditing}
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={this.stockSensors}>
|
||||
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
|
||||
{t("Stock sensors")}
|
||||
</button>}
|
||||
<button
|
||||
hidden={!isEditing || this.props.firmwareHardware == "none"}
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={this.stockSensors}>
|
||||
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
|
||||
{t("Stock sensors")}
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
{this.showPins()}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -154,4 +154,12 @@ select {
|
|||
}
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
input[type="checkbox"] {
|
||||
cursor: not-allowed;
|
||||
&:checked:after {
|
||||
border-color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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} />);
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)} />;
|
||||
}
|
||||
|
|
|
@ -16,15 +16,31 @@ export const getFwHardwareValue =
|
|||
return isFwHardwareValue(value) ? value : undefined;
|
||||
};
|
||||
|
||||
const TMC_BOARDS = ["express_k10", "farmduino_k15"];
|
||||
const NO_BUTTONS = ["arduino", "farmduino", "none"];
|
||||
const EXPRESS_BOARDS = ["express_k10"];
|
||||
const NO_SENSORS = [...EXPRESS_BOARDS];
|
||||
const NO_ENCODERS = [...EXPRESS_BOARDS];
|
||||
const NO_TOOLS = [...EXPRESS_BOARDS];
|
||||
const NO_TMC = ["arduino", "farmduino", "farmduino_k14"];
|
||||
|
||||
export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware));
|
||||
!firmwareHardware || !NO_TMC.includes(firmwareHardware);
|
||||
|
||||
export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware));
|
||||
|
||||
export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_BUTTONS.includes(firmwareHardware);
|
||||
|
||||
export const hasEncoders = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_ENCODERS.includes(firmwareHardware);
|
||||
|
||||
export const hasSensors = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_SENSORS.includes(firmwareHardware);
|
||||
|
||||
export const hasUTM = (firmwareHardware: FirmwareHardware | undefined) =>
|
||||
!firmwareHardware || !NO_TOOLS.includes(firmwareHardware);
|
||||
|
||||
export const getBoardIdentifier =
|
||||
(firmwareVersion: string | undefined): string =>
|
||||
firmwareVersion ? firmwareVersion.split(".")[3] : "undefined";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
|
||||
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
|
||||
import { HardwareSettingsProps } from "../interfaces";
|
||||
import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index";
|
||||
import { HardwareSettingsProps, SourceFwConfig } from "../interfaces";
|
||||
import { isBotOnline } from "../must_be_online";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { DangerZone } from "./hardware_settings/danger_zone";
|
||||
|
@ -19,6 +19,8 @@ import { t } from "../../i18next_wrapper";
|
|||
import { PinBindings } from "./hardware_settings/pin_bindings";
|
||||
import { ErrorHandling } from "./hardware_settings/error_handling";
|
||||
import { maybeOpenPanel } from "./maybe_highlight";
|
||||
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import type { McuParamName } from "farmbot";
|
||||
|
||||
export class HardwareSettings extends
|
||||
React.Component<HardwareSettingsProps, {}> {
|
||||
|
@ -36,7 +38,10 @@ export class HardwareSettings extends
|
|||
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
|
||||
const commonProps = { dispatch, controlPanelState };
|
||||
return <Widget className="hardware-widget">
|
||||
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
|
||||
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
|
||||
<SettingLoadProgress firmwareConfig={firmwareConfig}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
|
@ -78,8 +83,33 @@ export class HardwareSettings extends
|
|||
onReset={MCUFactoryReset}
|
||||
botDisconnected={botDisconnected} />
|
||||
<PinBindings {...commonProps}
|
||||
resources={resources} />
|
||||
resources={resources}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
||||
interface SettingLoadProgressProps {
|
||||
sourceFwConfig: SourceFwConfig;
|
||||
firmwareConfig: FirmwareConfig | undefined;
|
||||
}
|
||||
|
||||
const UNTRACKED_KEYS: (keyof FirmwareConfig)[] = [
|
||||
"id", "created_at", "updated_at", "device_id", "api_migrated",
|
||||
"param_config_ok", "param_test", "param_use_eeprom", "param_version",
|
||||
];
|
||||
|
||||
/** Track firmware configuration adoption by FarmBot OS. */
|
||||
const SettingLoadProgress = (props: SettingLoadProgressProps) => {
|
||||
const keys = Object.keys(props.firmwareConfig || {})
|
||||
.filter((k: keyof FirmwareConfig) => !UNTRACKED_KEYS.includes(k));
|
||||
const loadedKeys = keys.filter((key: McuParamName) =>
|
||||
props.sourceFwConfig(key).consistent);
|
||||
const progress = loadedKeys.length / keys.length * 100;
|
||||
const color = [0, 100].includes(progress) ? Color.darkGray : Color.white;
|
||||
return <div className={"load-progress-bar-wrapper"}>
|
||||
<div className={"load-progress-bar"}
|
||||
style={{ width: `${progress}%`, background: color }} />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ describe("<PinBindings />", () => {
|
|||
dispatch: jest.fn(),
|
||||
controlPanelState: panelState(),
|
||||
resources: buildResourceIndex([]).index,
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("shows pin binding labels", () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
|
|||
import { EncodersProps } from "../interfaces";
|
||||
import { Header } from "./header";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import { isExpressBoard } from "../firmware_hardware_support";
|
||||
import { hasEncoders } from "../firmware_hardware_support";
|
||||
import { Highlight } from "../maybe_highlight";
|
||||
|
||||
export function Encoders(props: EncodersProps) {
|
||||
|
@ -18,23 +18,23 @@ export function Encoders(props: EncodersProps) {
|
|||
y: !sourceFwConfig("encoder_enabled_y").value,
|
||||
z: !sourceFwConfig("encoder_enabled_z").value
|
||||
};
|
||||
const isExpress = isExpressBoard(firmwareHardware);
|
||||
const showEncoders = hasEncoders(firmwareHardware);
|
||||
|
||||
return <Highlight className={"section"}
|
||||
settingName={DeviceSetting.encoders}>
|
||||
<Header
|
||||
expanded={encoders}
|
||||
title={isExpress
|
||||
title={!showEncoders
|
||||
? DeviceSetting.stallDetection
|
||||
: DeviceSetting.encoders}
|
||||
panel={"encoders"}
|
||||
dispatch={dispatch} />
|
||||
<Collapse isOpen={!!encoders}>
|
||||
<BooleanMCUInputGroup
|
||||
label={isExpress
|
||||
label={!showEncoders
|
||||
? DeviceSetting.enableStallDetection
|
||||
: DeviceSetting.enableEncoders}
|
||||
tooltip={isExpress
|
||||
tooltip={!showEncoders
|
||||
? ToolTips.ENABLE_STALL_DETECTION
|
||||
: ToolTips.ENABLE_ENCODERS}
|
||||
x={"encoder_enabled_x"}
|
||||
|
@ -42,7 +42,7 @@ export function Encoders(props: EncodersProps) {
|
|||
z={"encoder_enabled_z"}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
{isExpress &&
|
||||
{!showEncoders &&
|
||||
<NumericMCUInputGroup
|
||||
label={DeviceSetting.stallSensitivity}
|
||||
tooltip={ToolTips.STALL_SENSITIVITY}
|
||||
|
@ -52,7 +52,7 @@ export function Encoders(props: EncodersProps) {
|
|||
gray={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
{!isExpress &&
|
||||
{showEncoders &&
|
||||
<BooleanMCUInputGroup
|
||||
label={DeviceSetting.useEncodersForPositioning}
|
||||
tooltip={ToolTips.ENCODER_POSITIONING}
|
||||
|
@ -62,7 +62,7 @@ export function Encoders(props: EncodersProps) {
|
|||
grayscale={encodersDisabled}
|
||||
dispatch={dispatch}
|
||||
sourceFwConfig={sourceFwConfig} />}
|
||||
{!isExpress &&
|
||||
{showEncoders &&
|
||||
<BooleanMCUInputGroup
|
||||
label={DeviceSetting.invertEncoders}
|
||||
tooltip={ToolTips.INVERT_ENCODERS}
|
||||
|
@ -74,7 +74,7 @@ export function Encoders(props: EncodersProps) {
|
|||
sourceFwConfig={sourceFwConfig} />}
|
||||
<NumericMCUInputGroup
|
||||
label={DeviceSetting.maxMissedSteps}
|
||||
tooltip={isExpress
|
||||
tooltip={!showEncoders
|
||||
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
|
||||
: ToolTips.MAX_MISSED_STEPS_ENCODERS}
|
||||
x={"encoder_missed_steps_max_x"}
|
||||
|
@ -92,7 +92,7 @@ export function Encoders(props: EncodersProps) {
|
|||
gray={encodersDisabled}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
dispatch={dispatch} />
|
||||
{!isExpress &&
|
||||
{showEncoders &&
|
||||
<NumericMCUInputGroup
|
||||
label={DeviceSetting.encoderScaling}
|
||||
tooltip={ToolTips.ENCODER_SCALING}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Header } from "./header";
|
|||
import { Collapse } from "@blueprintjs/core";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { calculateScale } from "./motors";
|
||||
import { isExpressBoard } from "../firmware_hardware_support";
|
||||
import { hasEncoders } from "../firmware_hardware_support";
|
||||
import { getDevice } from "../../../device";
|
||||
import { commandErr } from "../../actions";
|
||||
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
|
||||
|
@ -44,7 +44,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
type={"find_home"}
|
||||
title={DeviceSetting.homing}
|
||||
axisTitle={t("FIND HOME")}
|
||||
toolTip={isExpressBoard(firmwareHardware)
|
||||
toolTip={!hasEncoders(firmwareHardware)
|
||||
? ToolTips.HOMING_STALL_DETECTION
|
||||
: ToolTips.HOMING_ENCODERS}
|
||||
action={axis => getDevice()
|
||||
|
@ -56,7 +56,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
type={"calibrate"}
|
||||
title={DeviceSetting.calibration}
|
||||
axisTitle={t("CALIBRATE")}
|
||||
toolTip={isExpressBoard(firmwareHardware)
|
||||
toolTip={!hasEncoders(firmwareHardware)
|
||||
? ToolTips.CALIBRATION_STALL_DETECTION
|
||||
: ToolTips.CALIBRATION_ENCODERS}
|
||||
action={axis => getDevice().calibrate({ axis })
|
||||
|
@ -74,7 +74,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
|
|||
botDisconnected={botDisconnected} />
|
||||
<BooleanMCUInputGroup
|
||||
label={DeviceSetting.findHomeOnBoot}
|
||||
tooltip={isExpressBoard(firmwareHardware)
|
||||
tooltip={!hasEncoders(firmwareHardware)
|
||||
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
|
||||
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
|
||||
disable={disabled}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Highlight } from "../maybe_highlight";
|
|||
export function PinBindings(props: PinBindingsProps) {
|
||||
|
||||
const { pin_bindings } = props.controlPanelState;
|
||||
const { dispatch, resources } = props;
|
||||
const { dispatch, resources, firmwareHardware } = props;
|
||||
|
||||
return <Highlight className={"section"}
|
||||
settingName={DeviceSetting.pinBindings}>
|
||||
|
@ -19,7 +19,8 @@ export function PinBindings(props: PinBindingsProps) {
|
|||
panel={"pin_bindings"}
|
||||
dispatch={dispatch} />
|
||||
<Collapse isOpen={!!pin_bindings}>
|
||||
<PinBindingsContent dispatch={dispatch} resources={resources} />
|
||||
<PinBindingsContent dispatch={dispatch} resources={resources}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
</Collapse>
|
||||
</Highlight>;
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ export interface PinBindingsProps {
|
|||
dispatch: Function;
|
||||
controlPanelState: ControlPanelState;
|
||||
resources: ResourceIndex;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface DangerZoneProps {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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()", () => {
|
||||
|
|
|
@ -46,6 +46,7 @@ describe("<PinBindingsContent/>", () => {
|
|||
return {
|
||||
dispatch: jest.fn(),
|
||||
resources: resources,
|
||||
firmwareHardware: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,37 @@
|
|||
jest.mock("../../../api/crud", () => ({
|
||||
initSave: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { StockPinBindingsButton } from "../tagged_pin_binding_init";
|
||||
import {
|
||||
StockPinBindingsButton, StockPinBindingsButtonProps
|
||||
} from "../tagged_pin_binding_init";
|
||||
import { initSave } from "../../../api/crud";
|
||||
import { stockPinBindings } from "../list_and_label_support";
|
||||
|
||||
describe("<StockPinBindingsButton />", () => {
|
||||
const fakeProps = () => ({
|
||||
shouldDisplay: () => false,
|
||||
const fakeProps = (): StockPinBindingsButtonProps => ({
|
||||
dispatch: jest.fn(),
|
||||
firmwareHardware: undefined,
|
||||
});
|
||||
|
||||
it("adds bindings", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<StockPinBindingsButton {...p} />);
|
||||
const wrapper = mount(<StockPinBindingsButton {...fakeProps()} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
stockPinBindings.map(body =>
|
||||
expect(initSave).toHaveBeenCalledWith("PinBinding", body));
|
||||
});
|
||||
|
||||
it("is hidden", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "arduino";
|
||||
const wrapper = mount(<StockPinBindingsButton {...p} />);
|
||||
expect(wrapper.find("button").props().hidden).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is not hidden", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "farmduino_k14";
|
||||
const wrapper = mount(<StockPinBindingsButton {...p} />);
|
||||
expect(wrapper.find("button").props().hidden).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,10 +3,12 @@ import {
|
|||
PinBindingType,
|
||||
PinBindingSpecialAction
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
|
||||
export interface PinBindingsContentProps {
|
||||
dispatch: Function;
|
||||
resources: ResourceIndex;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export interface PinBindingListItems {
|
||||
|
|
|
@ -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 }),
|
||||
});
|
||||
|
|
|
@ -68,12 +68,13 @@ const PinBindingsListHeader = () =>
|
|||
</Row>;
|
||||
|
||||
export const PinBindingsContent = (props: PinBindingsContentProps) => {
|
||||
const { dispatch, resources } = props;
|
||||
const { dispatch, resources, firmwareHardware } = props;
|
||||
const pinBindings = apiPinBindings(resources);
|
||||
|
||||
return <div className="pin-bindings">
|
||||
<Row>
|
||||
<StockPinBindingsButton dispatch={dispatch} />
|
||||
<StockPinBindingsButton
|
||||
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
||||
<Popover
|
||||
position={Position.RIGHT_TOP}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
|
|
|
@ -8,6 +8,8 @@ import { PinBindingListItems } from "./interfaces";
|
|||
import { stockPinBindings } from "./list_and_label_support";
|
||||
import { initSave } from "../../api/crud";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
import { hasButtons } from "../components/firmware_hardware_support";
|
||||
|
||||
/** Return the correct Pin Binding resource according to binding type. */
|
||||
export const pinBindingBody =
|
||||
|
@ -34,15 +36,21 @@ export const pinBindingBody =
|
|||
return body;
|
||||
};
|
||||
|
||||
export interface StockPinBindingsButtonProps {
|
||||
dispatch: Function;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
/** Add default pin bindings. */
|
||||
export const StockPinBindingsButton = ({ dispatch }: { dispatch: Function }) =>
|
||||
export const StockPinBindingsButton = (props: StockPinBindingsButtonProps) =>
|
||||
<div className="stock-pin-bindings-button">
|
||||
<button
|
||||
className="fb-button green"
|
||||
hidden={!hasButtons(props.firmwareHardware)}
|
||||
onClick={() => stockPinBindings.map(binding =>
|
||||
dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
|
||||
props.dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
|
||||
<i className="fa fa-plus" />
|
||||
{t("v1.4 Stock Bindings")}
|
||||
{t("Stock Bindings")}
|
||||
</button>
|
||||
</div>;
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ describe("<FarmDesigner/>", () => {
|
|||
sensors: [],
|
||||
groups: [],
|
||||
shouldDisplay: () => false,
|
||||
mountedToolName: undefined,
|
||||
});
|
||||
|
||||
it("loads default map settings", () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { Plant } from "../plant";
|
||||
|
||||
describe("Plant()", () => {
|
||||
it("returns defaults", () => {
|
||||
expect(Plant({})).toEqual({
|
||||
created_at: "",
|
||||
id: undefined,
|
||||
meta: {},
|
||||
name: "Untitled Plant",
|
||||
openfarm_slug: "not-set",
|
||||
plant_stage: "planned",
|
||||
pointer_type: "Plant",
|
||||
radius: 25,
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -94,6 +94,19 @@ describe("designer reducer", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("uses current point color", () => {
|
||||
const action: ReduxAction<CurrentPointPayl> = {
|
||||
type: Actions.SET_CURRENT_POINT_DATA,
|
||||
payload: { cx: 10, cy: 20, r: 30 }
|
||||
};
|
||||
const state = oldState();
|
||||
state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" };
|
||||
const newState = designer(state, action);
|
||||
expect(newState.currentPoint).toEqual({
|
||||
cx: 10, cy: 20, r: 30, color: "red"
|
||||
});
|
||||
});
|
||||
|
||||
it("sets opened saved garden", () => {
|
||||
const payload = "savedGardenUuid";
|
||||
const action: ReduxAction<string | undefined> = {
|
||||
|
|
|
@ -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" }));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -23,7 +23,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
|
|||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
panel={Panel.FarmEvents}
|
||||
title={t("Edit Event")} />
|
||||
title={t("Edit event")} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
<EditFEForm farmEvent={fe}
|
||||
deviceTimezone={this.props.deviceTimezone}
|
||||
|
@ -31,7 +31,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
|
|||
executableOptions={this.props.executableOptions}
|
||||
dispatch={this.props.dispatch}
|
||||
findExecutable={this.props.findExecutable}
|
||||
title={t("Edit Event")}
|
||||
title={t("Edit event")}
|
||||
deleteBtn={true}
|
||||
timeSettings={this.props.timeSettings}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
} from "../../sequences/locals_list/locals_list_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { ErrorBoundary } from "../../error_boundary";
|
||||
|
||||
export const NEVER: TimeUnit = "never";
|
||||
/** Separate each of the form fields into their own interface. Recombined later
|
||||
|
@ -360,19 +361,24 @@ export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
|
|||
render() {
|
||||
const { farmEvent } = this.props;
|
||||
return <div className="edit-farm-event-form">
|
||||
<FarmEventForm
|
||||
isRegimen={this.isReg}
|
||||
fieldGet={this.fieldGet}
|
||||
fieldSet={this.fieldSet}
|
||||
timeSettings={this.props.timeSettings}
|
||||
executableOptions={this.props.executableOptions}
|
||||
executableSet={this.executableSet}
|
||||
executableGet={this.executableGet}
|
||||
dispatch={this.props.dispatch}
|
||||
specialStatus={farmEvent.specialStatus || this.state.specialStatusLocal}
|
||||
onSave={() => this.commitViewModel()}>
|
||||
<this.LocalsList />
|
||||
</FarmEventForm>
|
||||
<ErrorBoundary>
|
||||
<FarmEventForm
|
||||
isRegimen={this.isReg}
|
||||
fieldGet={this.fieldGet}
|
||||
fieldSet={this.fieldSet}
|
||||
timeSettings={this.props.timeSettings}
|
||||
executableOptions={this.props.executableOptions}
|
||||
executableSet={this.executableSet}
|
||||
executableGet={this.executableGet}
|
||||
dispatch={this.props.dispatch}
|
||||
specialStatus={farmEvent.specialStatus
|
||||
|| this.state.specialStatusLocal}
|
||||
onSave={() => this.commitViewModel()}>
|
||||
<ErrorBoundary>
|
||||
<this.LocalsList />
|
||||
</ErrorBoundary>
|
||||
</FarmEventForm>
|
||||
</ErrorBoundary>
|
||||
<FarmEventDeleteButton
|
||||
hidden={!this.props.deleteBtn}
|
||||
farmEvent={this.props.farmEvent}
|
||||
|
|
|
@ -111,7 +111,7 @@ export class PureFarmEvents
|
|||
<input
|
||||
value={this.state.searchTerm}
|
||||
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
|
||||
placeholder={t("Search events...")} />
|
||||
placeholder={t("Search your events...")} />
|
||||
</DesignerPanelTop>
|
||||
<DesignerPanelContent panelName={"farm-event"}>
|
||||
<div className="farm-events">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ describe("<FarmBotLayer/>", () => {
|
|||
peripherals: [],
|
||||
eStopStatus: false,
|
||||
getConfigValue: jest.fn(),
|
||||
mountedToolName: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
peripherals: [],
|
||||
eStopStatus: false,
|
||||
getConfigValue: () => true,
|
||||
mountedToolName: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"; }
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
|
@ -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> => ({
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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: "" };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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...")} />
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>;
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
@ -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"}>
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue