tool updates
parent
26a4f66a75
commit
40150a307c
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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 - 15rem);
|
||||
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: 2.5rem;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
|
|
@ -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%);
|
||||
|
@ -1543,16 +1543,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;
|
||||
|
|
|
@ -154,4 +154,12 @@ select {
|
|||
}
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
input[type="checkbox"] {
|
||||
cursor: not-allowed;
|
||||
&:checked:after {
|
||||
border-color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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} />);
|
||||
|
|
|
@ -69,7 +69,7 @@ export function WiFiStrengthDisplay(
|
|||
return <div className="wifi-strength-display">
|
||||
<p>
|
||||
<b>{t("WiFi strength")}: </b>
|
||||
{wifiStrength ? dbString : "N/A"}
|
||||
{wifiStrength ? `${dbString} (${percentString})` : "N/A"}
|
||||
</p>
|
||||
{wifiStrength &&
|
||||
<div className="percent-bar">
|
||||
|
@ -261,8 +261,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
|
||||
|
|
|
@ -4,6 +4,8 @@ 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";
|
||||
|
||||
// tslint:disable-next-line:no-null-keyword
|
||||
const UNDEFINED = null as unknown as undefined;
|
||||
|
@ -37,6 +39,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 +65,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 +92,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 +147,19 @@ 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>;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -62,6 +62,7 @@ describe("<FarmDesigner/>", () => {
|
|||
sensors: [],
|
||||
groups: [],
|
||||
shouldDisplay: () => false,
|
||||
mountedToolName: undefined,
|
||||
});
|
||||
|
||||
it("loads default map settings", () => {
|
||||
|
|
|
@ -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" }));
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -71,7 +71,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 +80,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}
|
||||
|
|
|
@ -7,6 +7,7 @@ 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 +17,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 +38,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,
|
||||
|
@ -45,13 +51,14 @@ export const ToolSlotPoint = (props: TSPProps) => {
|
|||
return <g id={"toolslot-" + id}
|
||||
onClick={() => !DevSettings.futureFeaturesEnabled() &&
|
||||
history.push(`/app/designer/tool-slots/${id}`)}>
|
||||
{pullout_direction &&
|
||||
{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 +74,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
|
|||
x={qx}
|
||||
y={qy}
|
||||
pulloutDirection={pullout_direction}
|
||||
gantryMounted={gantry_mounted}
|
||||
quadrant={quadrant}
|
||||
xySwap={xySwap} />
|
||||
</g>;
|
||||
|
|
|
@ -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();
|
|
@ -70,7 +70,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,
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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>;
|
||||
|
|
@ -70,14 +70,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));
|
||||
});
|
||||
|
||||
|
|
|
@ -274,7 +274,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"}>
|
||||
|
|
|
@ -15,7 +15,7 @@ describe("<AddGarden />", () => {
|
|||
|
||||
it("renders add garden panel", () => {
|
||||
const wrapper = mount(<AddGarden {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("create new garden");
|
||||
expect(wrapper.text().toLowerCase()).toContain("add garden");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ export class RawAddGarden extends React.Component<GardenSnapshotProps, {}> {
|
|||
<DesignerPanelHeader
|
||||
panelName={"saved-garden"}
|
||||
panel={Panel.SavedGardens}
|
||||
title={t("Add Garden")}
|
||||
title={t("Add garden")}
|
||||
description={Content.SAVED_GARDENS}
|
||||
backTo={"/app/designer/gardens"} />
|
||||
<DesignerPanelContent panelName={"saved-garden"}>
|
||||
|
|
|
@ -49,7 +49,7 @@ export class GardenSnapshot
|
|||
<button
|
||||
className="fb-button green wide"
|
||||
onClick={this.new}>
|
||||
{t("create new garden")}
|
||||
{t("Add new garden")}
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
selectAllSensors,
|
||||
maybeGetTimeSettings,
|
||||
selectAllPoints,
|
||||
selectAllPointGroups
|
||||
selectAllPointGroups,
|
||||
getDeviceAccountSettings,
|
||||
maybeFindToolById
|
||||
} from "../resources/selectors";
|
||||
import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
|
@ -64,6 +66,11 @@ export function mapStateToProps(props: Everything): Props {
|
|||
y: calcMicrostepsPerMm(fw.movement_step_per_mm_y, fw.movement_microsteps_y),
|
||||
};
|
||||
|
||||
const mountedToolId =
|
||||
getDeviceAccountSettings(props.resources.index).body.mounted_tool_id;
|
||||
const mountedToolName =
|
||||
maybeFindToolById(props.resources.index, mountedToolId)?.body.name;
|
||||
|
||||
const peripherals = uniq(selectAllPeripherals(props.resources.index))
|
||||
.map(x => {
|
||||
const label = x.body.label;
|
||||
|
@ -123,5 +130,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
sensors: selectAllSensors(props.resources.index),
|
||||
groups: selectAllPointGroups(props.resources.index),
|
||||
shouldDisplay,
|
||||
mountedToolName,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,12 +9,10 @@ jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
|
|||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
RawAddToolSlot as AddToolSlot, AddToolSlotProps, mapStateToProps
|
||||
} from "../add_tool_slot";
|
||||
import { RawAddToolSlot as AddToolSlot } from "../add_tool_slot";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
fakeTool, fakeToolSlot
|
||||
fakeTool, fakeToolSlot, fakeWebAppConfig
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
buildResourceIndex
|
||||
|
@ -23,6 +21,7 @@ import { init, save, edit, destroy } from "../../../api/crud";
|
|||
import { history } from "../../../history";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
|
||||
import { AddToolSlotProps, mapStateToPropsAdd } from "../map_to_props_add_edit";
|
||||
|
||||
describe("<AddToolSlot />", () => {
|
||||
const fakeProps = (): AddToolSlotProps => ({
|
||||
|
@ -32,15 +31,18 @@ describe("<AddToolSlot />", () => {
|
|||
dispatch: jest.fn(),
|
||||
findToolSlot: fakeToolSlot,
|
||||
firmwareHardware: undefined,
|
||||
xySwap: false,
|
||||
quadrant: 2,
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<AddToolSlot {...fakeProps()} />);
|
||||
["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
|
||||
"change slot direction", "use current location", "gantry-mounted"
|
||||
["add new slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
|
||||
"change direction", "gantry-mounted"
|
||||
].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
expect(init).toHaveBeenCalledWith("Point", {
|
||||
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
|
||||
pointer_type: "ToolSlot", name: "Slot", radius: 0, meta: {},
|
||||
x: 0, y: 0, z: 0, tool_id: undefined,
|
||||
pullout_direction: ToolPulloutDirection.NONE,
|
||||
gantry_mounted: false,
|
||||
|
@ -116,7 +118,7 @@ describe("<AddToolSlot />", () => {
|
|||
const wrapper = mount(<AddToolSlot {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("tool");
|
||||
expect(init).toHaveBeenCalledWith("Point", {
|
||||
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
|
||||
pointer_type: "ToolSlot", name: "Slot", radius: 0, meta: {},
|
||||
x: 0, y: 0, z: 0, tool_id: undefined,
|
||||
pullout_direction: ToolPulloutDirection.NONE,
|
||||
gantry_mounted: true,
|
||||
|
@ -124,14 +126,17 @@ describe("<AddToolSlot />", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
describe("mapStateToPropsAdd()", () => {
|
||||
it("returns props", () => {
|
||||
const webAppConfig = fakeWebAppConfig();
|
||||
webAppConfig.body.bot_origin_quadrant = 1;
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
const toolSlot = fakeToolSlot();
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([tool, toolSlot]);
|
||||
const props = mapStateToProps(state);
|
||||
state.resources = buildResourceIndex([tool, toolSlot, webAppConfig]);
|
||||
const props = mapStateToPropsAdd(state);
|
||||
expect(props.quadrant).toEqual(1);
|
||||
expect(props.findTool(1)).toEqual(tool);
|
||||
expect(props.findToolSlot(toolSlot.uuid)).toEqual(toolSlot);
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ describe("<AddTool />", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<AddTool {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("Add new tool");
|
||||
expect(wrapper.text()).toContain("Add new");
|
||||
});
|
||||
|
||||
it("edits tool name", () => {
|
||||
|
@ -60,11 +60,36 @@ describe("<AddTool />", () => {
|
|||
p.firmwareHardware = "express_k10";
|
||||
p.existingToolNames = ["Seed Trough 1"];
|
||||
const wrapper = mount(<AddTool {...p} />);
|
||||
wrapper.setState({ model: "express" });
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(initSave).toHaveBeenCalledTimes(1);
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
|
||||
});
|
||||
|
||||
it("copies a tool name", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount<AddTool>(<AddTool {...p} />);
|
||||
wrapper.find("p").last().simulate("click");
|
||||
expect(wrapper.state().toolName).toEqual("Seed Trough 2");
|
||||
});
|
||||
|
||||
it("deselects a tool", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount<AddTool>(<AddTool {...p} />);
|
||||
expect(wrapper.state().toAdd).toEqual(["Seed Trough 1", "Seed Trough 2"]);
|
||||
wrapper.find("input").last().simulate("change");
|
||||
expect(wrapper.state().toAdd).toEqual(["Seed Trough 1"]);
|
||||
});
|
||||
|
||||
it("selects a tool", () => {
|
||||
const p = fakeProps();
|
||||
p.firmwareHardware = "express_k10";
|
||||
const wrapper = mount<AddTool>(<AddTool {...p} />);
|
||||
wrapper.setState({ toAdd: [] });
|
||||
wrapper.find("input").last().simulate("change");
|
||||
expect(wrapper.state().toAdd).toEqual(["Seed Trough 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
|
|
@ -9,9 +9,7 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
|||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
RawEditToolSlot as EditToolSlot, EditToolSlotProps, mapStateToProps
|
||||
} from "../edit_tool_slot";
|
||||
import { RawEditToolSlot as EditToolSlot } from "../edit_tool_slot";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
fakeToolSlot, fakeTool
|
||||
|
@ -20,6 +18,10 @@ import {
|
|||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { destroy, edit, save } from "../../../api/crud";
|
||||
import {
|
||||
EditToolSlotProps, mapStateToPropsEdit
|
||||
} from "../map_to_props_add_edit";
|
||||
import { SlotEditRows } from "../tool_slot_edit_components";
|
||||
|
||||
describe("<EditToolSlot />", () => {
|
||||
const fakeProps = (): EditToolSlotProps => ({
|
||||
|
@ -29,6 +31,9 @@ describe("<EditToolSlot />", () => {
|
|||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
dispatch: jest.fn(),
|
||||
firmwareHardware: undefined,
|
||||
xySwap: false,
|
||||
quadrant: 2,
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("redirects", () => {
|
||||
|
@ -40,8 +45,8 @@ describe("<EditToolSlot />", () => {
|
|||
const p = fakeProps();
|
||||
p.findToolSlot = () => fakeToolSlot();
|
||||
const wrapper = mount(<EditToolSlot {...p} />);
|
||||
["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
|
||||
"change slot direction", "use current location", "gantry-mounted"
|
||||
["edit slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
|
||||
"change direction", "gantry-mounted"
|
||||
].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
|
||||
|
@ -65,6 +70,34 @@ describe("<EditToolSlot />", () => {
|
|||
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
|
||||
});
|
||||
|
||||
it("moves to gantry-mounted tool slot", () => {
|
||||
const p = fakeProps();
|
||||
p.botPosition = { x: 10, y: 20, z: 30 };
|
||||
const toolSlot = fakeToolSlot();
|
||||
toolSlot.body.gantry_mounted = true;
|
||||
toolSlot.body.x = 1;
|
||||
toolSlot.body.y = 2;
|
||||
toolSlot.body.z = 3;
|
||||
p.findToolSlot = () => toolSlot;
|
||||
const wrapper = shallow(<EditToolSlot {...p} />);
|
||||
wrapper.find(".gray").last().simulate("click");
|
||||
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 10, y: 2, z: 3 });
|
||||
});
|
||||
|
||||
it("falls back to tool slot when moving to gantry-mounted tool slot", () => {
|
||||
const p = fakeProps();
|
||||
p.botPosition = { x: undefined, y: undefined, z: undefined };
|
||||
const toolSlot = fakeToolSlot();
|
||||
toolSlot.body.gantry_mounted = true;
|
||||
toolSlot.body.x = 1;
|
||||
toolSlot.body.y = 2;
|
||||
toolSlot.body.z = 3;
|
||||
p.findToolSlot = () => toolSlot;
|
||||
const wrapper = shallow(<EditToolSlot {...p} />);
|
||||
wrapper.find(".gray").last().simulate("click");
|
||||
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
|
||||
});
|
||||
|
||||
it("removes tool slot", () => {
|
||||
const p = fakeProps();
|
||||
const toolSlot = fakeToolSlot();
|
||||
|
@ -73,9 +106,19 @@ describe("<EditToolSlot />", () => {
|
|||
wrapper.find("button").last().simulate("click");
|
||||
expect(destroy).toHaveBeenCalledWith(toolSlot.uuid);
|
||||
});
|
||||
|
||||
it("finds tool", () => {
|
||||
const p = fakeProps();
|
||||
const toolSlot = fakeToolSlot();
|
||||
p.findToolSlot = () => toolSlot;
|
||||
const tool = fakeTool();
|
||||
p.findTool = () => tool;
|
||||
const wrapper = mount(<EditToolSlot {...p} />);
|
||||
expect(wrapper.find(SlotEditRows).props().tool).toEqual(tool);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
describe("mapStateToPropsEdit()", () => {
|
||||
it("returns props", () => {
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
|
@ -83,7 +126,7 @@ describe("mapStateToProps()", () => {
|
|||
toolSlot.body.id = 1;
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([tool, toolSlot]);
|
||||
const props = mapStateToProps(state);
|
||||
const props = mapStateToPropsEdit(state);
|
||||
expect(props.findTool(1)).toEqual(tool);
|
||||
expect(props.findToolSlot("1")).toEqual(toolSlot);
|
||||
});
|
||||
|
@ -91,7 +134,7 @@ describe("mapStateToProps()", () => {
|
|||
it("doesn't find tool slot", () => {
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([]);
|
||||
const props = mapStateToProps(state);
|
||||
const props = mapStateToPropsEdit(state);
|
||||
expect(props.findToolSlot("1")).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,16 +13,17 @@ jest.mock("../../../history", () => ({
|
|||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
RawEditTool as EditTool, EditToolProps, mapStateToProps
|
||||
RawEditTool as EditTool, EditToolProps, mapStateToProps, isActive
|
||||
} from "../edit_tool";
|
||||
import { fakeTool } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeTool, fakeToolSlot } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
buildResourceIndex
|
||||
buildResourceIndex, fakeDevice
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { SaveBtn } from "../../../ui";
|
||||
import { history } from "../../../history";
|
||||
import { edit, destroy } from "../../../api/crud";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
|
||||
describe("<EditTool />", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -32,6 +33,8 @@ describe("<EditTool />", () => {
|
|||
const fakeProps = (): EditToolProps => ({
|
||||
findTool: jest.fn(() => fakeTool()),
|
||||
dispatch: jest.fn(),
|
||||
mountedToolId: undefined,
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
@ -75,11 +78,38 @@ describe("<EditTool />", () => {
|
|||
it("removes tool", () => {
|
||||
const p = fakeProps();
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
p.findTool = () => tool;
|
||||
p.isActive = () => false;
|
||||
p.mountedToolId = undefined;
|
||||
const wrapper = shallow(<EditTool {...p} />);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
clickButton(wrapper, 0, "delete");
|
||||
expect(destroy).toHaveBeenCalledWith(tool.uuid);
|
||||
});
|
||||
|
||||
it("doesn't remove tool: active", () => {
|
||||
const p = fakeProps();
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
p.findTool = () => tool;
|
||||
p.isActive = () => true;
|
||||
p.mountedToolId = undefined;
|
||||
const wrapper = shallow(<EditTool {...p} />);
|
||||
clickButton(wrapper, 0, "delete");
|
||||
expect(destroy).not.toHaveBeenCalledWith(tool.uuid);
|
||||
});
|
||||
|
||||
it("doesn't remove tool: mounted", () => {
|
||||
const p = fakeProps();
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
p.findTool = () => tool;
|
||||
p.isActive = () => false;
|
||||
p.mountedToolId = tool.body.id;
|
||||
const wrapper = shallow(<EditTool {...p} />);
|
||||
clickButton(wrapper, 0, "delete");
|
||||
expect(destroy).not.toHaveBeenCalledWith(tool.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
@ -87,8 +117,19 @@ describe("mapStateToProps()", () => {
|
|||
const state = fakeState();
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 123;
|
||||
state.resources = buildResourceIndex([tool]);
|
||||
state.resources = buildResourceIndex([tool, fakeDevice()]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.findTool("" + tool.body.id)).toEqual(tool);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isActive()", () => {
|
||||
it("returns tool state", () => {
|
||||
const toolSlot = fakeToolSlot();
|
||||
toolSlot.body.tool_id = 1;
|
||||
const active = isActive([toolSlot]);
|
||||
expect(active(1)).toEqual(true);
|
||||
expect(active(2)).toEqual(false);
|
||||
expect(active(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,10 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
|||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { RawTools as Tools, ToolsProps, mapStateToProps } from "../index";
|
||||
import {
|
||||
RawTools as Tools, ToolsProps, mapStateToProps,
|
||||
ToolSlotInventoryItem, ToolSlotInventoryItemProps,
|
||||
} from "../index";
|
||||
import {
|
||||
fakeTool, fakeToolSlot, fakeSensor
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
|
@ -40,6 +43,7 @@ describe("<Tools />", () => {
|
|||
botToMqttStatus: "down",
|
||||
hoveredToolSlot: undefined,
|
||||
firmwareHardware: undefined,
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders with no tools", () => {
|
||||
|
@ -182,6 +186,62 @@ describe("<Tools />", () => {
|
|||
const wrapper = mount(<Tools {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("mounted tool");
|
||||
});
|
||||
|
||||
it("displays tool as active", () => {
|
||||
const p = fakeProps();
|
||||
p.tools = [fakeTool()];
|
||||
p.isActive = () => true;
|
||||
p.device.body.mounted_tool_id = undefined;
|
||||
const wrapper = mount(<Tools {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("active");
|
||||
});
|
||||
|
||||
it("displays tool as mounted", () => {
|
||||
const p = fakeProps();
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
p.findTool = () => tool;
|
||||
p.tools = [tool];
|
||||
p.device.body.mounted_tool_id = 1;
|
||||
const wrapper = mount(<Tools {...p} />);
|
||||
expect(wrapper.find("p").last().text().toLowerCase()).toContain("mounted");
|
||||
});
|
||||
|
||||
it("handles missing tools", () => {
|
||||
const p = fakeProps();
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
p.findTool = () => undefined;
|
||||
p.tools = [tool];
|
||||
p.device.body.mounted_tool_id = 1;
|
||||
const wrapper = mount(<Tools {...p} />);
|
||||
expect(wrapper.find("p").last().text().toLowerCase()).not.toContain("mounted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<ToolSlotInventoryItem />", () => {
|
||||
const fakeProps = (): ToolSlotInventoryItemProps => ({
|
||||
toolSlot: fakeToolSlot(),
|
||||
tools: [],
|
||||
hovered: false,
|
||||
dispatch: jest.fn(),
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("changes tool", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotInventoryItem {...p} />);
|
||||
wrapper.find(ToolSelection).simulate("change", { tool_id: 1 });
|
||||
expect(edit).toHaveBeenCalledWith(p.toolSlot, { tool_id: 1 });
|
||||
expect(save).toHaveBeenCalledWith(p.toolSlot.uuid);
|
||||
});
|
||||
|
||||
it("doesn't open tool slot", () => {
|
||||
const wrapper = shallow(<ToolSlotInventoryItem {...fakeProps()} />);
|
||||
const e = { stopPropagation: jest.fn() };
|
||||
wrapper.find(".tool-selection-wrapper").first().simulate("click", e);
|
||||
expect(e.stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
|
|
@ -2,14 +2,13 @@ import * as React from "react";
|
|||
import { shallow, mount } from "enzyme";
|
||||
import {
|
||||
GantryMountedInput, GantryMountedInputProps,
|
||||
UseCurrentLocationInputRow, UseCurrentLocationInputRowProps,
|
||||
SlotDirectionInputRow, SlotDirectionInputRowProps,
|
||||
ToolInputRow, ToolInputRowProps,
|
||||
SlotLocationInputRow, SlotLocationInputRowProps,
|
||||
ToolSelection, ToolSelectionProps,
|
||||
ToolSelection, ToolSelectionProps, SlotEditRows, SlotEditRowsProps,
|
||||
} from "../tool_slot_edit_components";
|
||||
import { fakeTool } from "../../../__test_support__/fake_state/resources";
|
||||
import { FBSelect } from "../../../ui";
|
||||
import { fakeTool, fakeToolSlot } from "../../../__test_support__/fake_state/resources";
|
||||
import { FBSelect, NULL_CHOICE } from "../../../ui";
|
||||
|
||||
describe("<GantryMountedInput />", () => {
|
||||
const fakeProps = (): GantryMountedInputProps => ({
|
||||
|
@ -30,33 +29,6 @@ describe("<GantryMountedInput />", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("<UseCurrentLocationInputRow />", () => {
|
||||
const fakeProps = (): UseCurrentLocationInputRowProps => ({
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
onChange: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<UseCurrentLocationInputRow {...fakeProps()} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("use current location");
|
||||
});
|
||||
|
||||
it("doesn't change value", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<UseCurrentLocationInputRow {...p} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(p.onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("changes value", () => {
|
||||
const p = fakeProps();
|
||||
p.botPosition = { x: 0, y: 1, z: 2 };
|
||||
const wrapper = shallow(<UseCurrentLocationInputRow {...p} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(p.onChange).toHaveBeenCalledWith(p.botPosition);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SlotDirectionInputRow />", () => {
|
||||
const fakeProps = (): SlotDirectionInputRowProps => ({
|
||||
toolPulloutDirection: 0,
|
||||
|
@ -65,7 +37,7 @@ describe("<SlotDirectionInputRow />", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<SlotDirectionInputRow {...fakeProps()} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("change slot direction");
|
||||
expect(wrapper.text().toLowerCase()).toContain("change direction");
|
||||
});
|
||||
|
||||
it("changes value by click", () => {
|
||||
|
@ -89,6 +61,7 @@ describe("<ToolSelection />", () => {
|
|||
selectedTool: undefined,
|
||||
onChange: jest.fn(),
|
||||
filterSelectedTool: false,
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
@ -98,12 +71,13 @@ describe("<ToolSelection />", () => {
|
|||
|
||||
it("handles missing tool data", () => {
|
||||
const p = fakeProps();
|
||||
p.filterSelectedTool = true;
|
||||
const tool = fakeTool();
|
||||
tool.body.name = undefined;
|
||||
tool.body.id = undefined;
|
||||
p.tools = [tool];
|
||||
const wrapper = shallow(<ToolSelection {...p} />);
|
||||
expect(wrapper.find("FBSelect").props().list).toEqual([]);
|
||||
expect(wrapper.find("FBSelect").props().list).toEqual([NULL_CHOICE]);
|
||||
});
|
||||
|
||||
it("handles missing selected tool data", () => {
|
||||
|
@ -137,6 +111,7 @@ describe("<ToolInputRow />", () => {
|
|||
selectedTool: undefined,
|
||||
onChange: jest.fn(),
|
||||
isExpress: false,
|
||||
isActive: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
@ -164,6 +139,7 @@ describe("<SlotLocationInputRow />", () => {
|
|||
slotLocation: { x: 0, y: 0, z: 0 },
|
||||
gantryMounted: false,
|
||||
onChange: jest.fn(),
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
@ -195,4 +171,40 @@ describe("<SlotLocationInputRow />", () => {
|
|||
expect(p.onChange).toHaveBeenCalledWith({ y: 2 });
|
||||
expect(p.onChange).toHaveBeenCalledWith({ z: 3 });
|
||||
});
|
||||
|
||||
it("doesn't use current coordinates", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<SlotLocationInputRow {...p} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(p.onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses current coordinates", () => {
|
||||
const p = fakeProps();
|
||||
p.botPosition = { x: 0, y: 1, z: 2 };
|
||||
const wrapper = shallow(<SlotLocationInputRow {...p} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(p.onChange).toHaveBeenCalledWith(p.botPosition);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SlotEditRows />", () => {
|
||||
const fakeProps = (): SlotEditRowsProps => ({
|
||||
toolSlot: fakeToolSlot(),
|
||||
tools: [],
|
||||
tool: undefined,
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
updateToolSlot: jest.fn(),
|
||||
isExpress: false,
|
||||
xySwap: false,
|
||||
quadrant: 2,
|
||||
isActive: () => false,
|
||||
});
|
||||
|
||||
it("handles missing tool", () => {
|
||||
const p = fakeProps();
|
||||
p.tool = undefined;
|
||||
const wrapper = mount(<SlotEditRows {...p} />);
|
||||
expect(wrapper.text()).toContain("None");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,9 +13,10 @@ import { history } from "../../history";
|
|||
import { selectAllTools } from "../../resources/selectors";
|
||||
import { betterCompact } from "../../util";
|
||||
import {
|
||||
isExpressBoard, getFwHardwareValue
|
||||
getFwHardwareValue
|
||||
} from "../../devices/components/firmware_hardware_support";
|
||||
import { getFbosConfig } from "../../resources/getters";
|
||||
import { ToolSVG } from "../map/layers/tool_slots/tool_graphics";
|
||||
|
||||
export interface AddToolProps {
|
||||
dispatch: Function;
|
||||
|
@ -25,6 +26,7 @@ export interface AddToolProps {
|
|||
|
||||
export interface AddToolState {
|
||||
toolName: string;
|
||||
toAdd: string[];
|
||||
}
|
||||
|
||||
export const mapStateToProps = (props: Everything): AddToolProps => ({
|
||||
|
@ -35,7 +37,19 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({
|
|||
});
|
||||
|
||||
export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
|
||||
state: AddToolState = { toolName: "" };
|
||||
state: AddToolState = { toolName: "", toAdd: [] };
|
||||
|
||||
filterExisting = (n: string) => !this.props.existingToolNames.includes(n);
|
||||
|
||||
add = (n: string) => this.filterExisting(n) && !this.state.toAdd.includes(n) &&
|
||||
this.setState({ toAdd: this.state.toAdd.concat([n]) });
|
||||
|
||||
remove = (n: string) =>
|
||||
this.setState({ toAdd: this.state.toAdd.filter(name => name != n) });
|
||||
|
||||
componentDidMount = () => this.setState({
|
||||
toAdd: this.stockToolNames().filter(this.filterExisting)
|
||||
});
|
||||
|
||||
newTool = (name: string) => {
|
||||
this.props.dispatch(initSave("Tool", { name }));
|
||||
|
@ -79,22 +93,38 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
|
|||
}
|
||||
}
|
||||
|
||||
StockToolCheckbox = ({ toolName }: { toolName: string }) => {
|
||||
const alreadyAdded = !this.filterExisting(toolName);
|
||||
const checked = this.state.toAdd.includes(toolName) || alreadyAdded;
|
||||
return <div className={`fb-checkbox ${alreadyAdded ? "disabled" : ""}`}>
|
||||
<input type="checkbox" key={JSON.stringify(this.state.toAdd)}
|
||||
title={alreadyAdded ? t("Already added.") : ""}
|
||||
checked={checked}
|
||||
onChange={() => checked
|
||||
? this.remove(toolName)
|
||||
: this.add(toolName)} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
AddStockTools = () =>
|
||||
<div className="add-stock-tools">
|
||||
<label>{t("Add stock names")}</label>
|
||||
<label>{t("stock names")}</label>
|
||||
<ul>
|
||||
{this.stockToolNames().map(n => <li key={n}>{n}</li>)}
|
||||
{this.stockToolNames().map(n =>
|
||||
<li key={n}>
|
||||
<this.StockToolCheckbox toolName={n} />
|
||||
<p onClick={() => this.setState({ toolName: n })}>{n}</p>
|
||||
</li>)}
|
||||
</ul>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={() => {
|
||||
this.stockToolNames()
|
||||
.filter(n => !this.props.existingToolNames.includes(n))
|
||||
this.state.toAdd.filter(this.filterExisting)
|
||||
.map(n => this.newTool(n));
|
||||
history.push("/app/designer/tools");
|
||||
}}>
|
||||
<i className="fa fa-plus" />
|
||||
{t("Stock names")}
|
||||
{t("selected")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -103,16 +133,16 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
|
|||
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
|
||||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
title={isExpressBoard(this.props.firmwareHardware)
|
||||
? t("Add new")
|
||||
: t("Add new tool")}
|
||||
title={t("Add new")}
|
||||
backTo={"/app/designer/tools"}
|
||||
panel={Panel.Tools} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
<div className="add-new-tool">
|
||||
<ToolSVG toolName={this.state.toolName} />
|
||||
<label>{t("Name")}</label>
|
||||
<input onChange={e =>
|
||||
this.setState({ toolName: e.currentTarget.value })} />
|
||||
<input defaultValue={this.state.toolName}
|
||||
onChange={e =>
|
||||
this.setState({ toolName: e.currentTarget.value })} />
|
||||
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />
|
||||
</div>
|
||||
<this.AddStockTools />
|
||||
|
|
|
@ -3,58 +3,31 @@ import { connect } from "react-redux";
|
|||
import {
|
||||
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
|
||||
} from "../designer_panel";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { SaveBtn } from "../../ui";
|
||||
import {
|
||||
SpecialStatus, TaggedTool, TaggedToolSlotPointer, FirmwareHardware
|
||||
} from "farmbot";
|
||||
import { SpecialStatus, TaggedToolSlotPointer } from "farmbot";
|
||||
import { init, save, edit, destroy } from "../../api/crud";
|
||||
import { Panel } from "../panel_header";
|
||||
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
|
||||
import {
|
||||
selectAllTools, maybeFindToolById, maybeGetToolSlot
|
||||
} from "../../resources/selectors";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { validBotLocationData } from "../../util";
|
||||
import { history } from "../../history";
|
||||
import { SlotEditRows } from "./tool_slot_edit_components";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import {
|
||||
isExpressBoard, getFwHardwareValue
|
||||
isExpressBoard
|
||||
} from "../../devices/components/firmware_hardware_support";
|
||||
import { getFbosConfig } from "../../resources/getters";
|
||||
|
||||
export interface AddToolSlotProps {
|
||||
tools: TaggedTool[];
|
||||
dispatch: Function;
|
||||
botPosition: BotPosition;
|
||||
findTool(id: number): TaggedTool | undefined;
|
||||
findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
import { AddToolSlotProps, mapStateToPropsAdd } from "./map_to_props_add_edit";
|
||||
|
||||
export interface AddToolSlotState {
|
||||
uuid: UUID | undefined;
|
||||
}
|
||||
|
||||
export const mapStateToProps = (props: Everything): AddToolSlotProps => ({
|
||||
tools: selectAllTools(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
botPosition: validBotLocationData(props.bot.hardware.location_data).position,
|
||||
findTool: (id: number) => maybeFindToolById(props.resources.index, id),
|
||||
findToolSlot: (uuid: UUID | undefined) =>
|
||||
maybeGetToolSlot(props.resources.index, uuid),
|
||||
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
|
||||
});
|
||||
|
||||
export class RawAddToolSlot
|
||||
extends React.Component<AddToolSlotProps, AddToolSlotState> {
|
||||
state: AddToolSlotState = { uuid: undefined };
|
||||
|
||||
componentDidMount() {
|
||||
const action = init("Point", {
|
||||
pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {},
|
||||
pointer_type: "ToolSlot", name: t("Slot"), radius: 0, meta: {},
|
||||
x: 0, y: 0, z: 0, tool_id: undefined,
|
||||
pullout_direction: ToolPulloutDirection.NONE,
|
||||
gantry_mounted: isExpressBoard(this.props.firmwareHardware) ? true : false,
|
||||
|
@ -95,9 +68,7 @@ export class RawAddToolSlot
|
|||
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
|
||||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
title={isExpressBoard(this.props.firmwareHardware)
|
||||
? t("Add new slot")
|
||||
: t("Add new tool slot")}
|
||||
title={t("Add new slot")}
|
||||
backTo={"/app/designer/tools"}
|
||||
panel={Panel.Tools} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
|
@ -108,6 +79,9 @@ export class RawAddToolSlot
|
|||
tools={this.props.tools}
|
||||
tool={this.tool}
|
||||
botPosition={this.props.botPosition}
|
||||
xySwap={this.props.xySwap}
|
||||
quadrant={this.props.quadrant}
|
||||
isActive={this.props.isActive}
|
||||
updateToolSlot={this.updateSlot(this.toolSlot)} />
|
||||
: "initializing"}
|
||||
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />
|
||||
|
@ -116,4 +90,4 @@ export class RawAddToolSlot
|
|||
}
|
||||
}
|
||||
|
||||
export const AddToolSlot = connect(mapStateToProps)(RawAddToolSlot);
|
||||
export const AddToolSlot = connect(mapStateToPropsAdd)(RawAddToolSlot);
|
||||
|
|
|
@ -6,16 +6,26 @@ import {
|
|||
import { Everything } from "../../interfaces";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { getPathArray } from "../../history";
|
||||
import { TaggedTool, SpecialStatus } from "farmbot";
|
||||
import { maybeFindToolById } from "../../resources/selectors";
|
||||
import { TaggedTool, SpecialStatus, TaggedToolSlotPointer } from "farmbot";
|
||||
import {
|
||||
maybeFindToolById, getDeviceAccountSettings, selectAllToolSlotPointers
|
||||
} from "../../resources/selectors";
|
||||
import { SaveBtn } from "../../ui";
|
||||
import { edit, destroy } from "../../api/crud";
|
||||
import { history } from "../../history";
|
||||
import { Panel } from "../panel_header";
|
||||
import { ToolSVG } from "../map/layers/tool_slots/tool_graphics";
|
||||
import { error } from "../../toast/toast";
|
||||
|
||||
export const isActive = (toolSlots: TaggedToolSlotPointer[]) =>
|
||||
(toolId: number | undefined) =>
|
||||
!!(toolId && toolSlots.map(x => x.body.tool_id).includes(toolId));
|
||||
|
||||
export interface EditToolProps {
|
||||
findTool(id: string): TaggedTool | undefined;
|
||||
dispatch: Function;
|
||||
mountedToolId: number | undefined;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
export interface EditToolState {
|
||||
|
@ -26,6 +36,9 @@ export const mapStateToProps = (props: Everything): EditToolProps => ({
|
|||
findTool: (id: string) =>
|
||||
maybeFindToolById(props.resources.index, parseInt(id)),
|
||||
dispatch: props.dispatch,
|
||||
mountedToolId: getDeviceAccountSettings(props.resources.index)
|
||||
.body.mounted_tool_id,
|
||||
isActive: isActive(selectAllToolSlotPointers(props.resources.index)),
|
||||
});
|
||||
|
||||
export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
|
||||
|
@ -44,6 +57,11 @@ export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
|
|||
const { dispatch } = this.props;
|
||||
const { toolName } = this.state;
|
||||
const panelName = "edit-tool";
|
||||
const isMounted = this.props.mountedToolId == tool.body.id;
|
||||
const message = isMounted
|
||||
? t("Cannot delete while mounted.")
|
||||
: t("Cannot delete while in a slot.");
|
||||
const activeOrMounted = this.props.isActive(tool.body.id) || isMounted;
|
||||
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
|
||||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
|
@ -51,6 +69,7 @@ export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
|
|||
backTo={"/app/designer/tools"}
|
||||
panel={Panel.Tools} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
<ToolSVG toolName={this.state.toolName} />
|
||||
<label>{t("Name")}</label>
|
||||
<input
|
||||
value={toolName}
|
||||
|
@ -62,8 +81,12 @@ export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
|
|||
}}
|
||||
status={SpecialStatus.DIRTY} />
|
||||
<button
|
||||
className="fb-button red no-float"
|
||||
onClick={() => dispatch(destroy(tool.uuid))}>
|
||||
className={`fb-button red no-float ${activeOrMounted
|
||||
? "pseudo-disabled" : ""}`}
|
||||
title={activeOrMounted ? message : t("delete")}
|
||||
onClick={() => activeOrMounted
|
||||
? error(t(message))
|
||||
: dispatch(destroy(tool.uuid))}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
</DesignerPanelContent>
|
||||
|
|
|
@ -3,43 +3,18 @@ import { connect } from "react-redux";
|
|||
import {
|
||||
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
|
||||
} from "../designer_panel";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { getPathArray } from "../../history";
|
||||
import { TaggedToolSlotPointer, TaggedTool, FirmwareHardware } from "farmbot";
|
||||
import { TaggedToolSlotPointer } from "farmbot";
|
||||
import { edit, save, destroy } from "../../api/crud";
|
||||
import { history } from "../../history";
|
||||
import { Panel } from "../panel_header";
|
||||
import {
|
||||
maybeFindToolSlotById, selectAllTools, maybeFindToolById
|
||||
} from "../../resources/selectors";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { validBotLocationData } from "../../util";
|
||||
import { SlotEditRows } from "./tool_slot_edit_components";
|
||||
import { moveAbs } from "../../devices/actions";
|
||||
import {
|
||||
getFwHardwareValue, isExpressBoard
|
||||
isExpressBoard
|
||||
} from "../../devices/components/firmware_hardware_support";
|
||||
import { getFbosConfig } from "../../resources/getters";
|
||||
|
||||
export interface EditToolSlotProps {
|
||||
findToolSlot(id: string): TaggedToolSlotPointer | undefined;
|
||||
tools: TaggedTool[];
|
||||
findTool(id: number): TaggedTool | undefined;
|
||||
dispatch: Function;
|
||||
botPosition: BotPosition;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
}
|
||||
|
||||
export const mapStateToProps = (props: Everything): EditToolSlotProps => ({
|
||||
findToolSlot: (id: string) =>
|
||||
maybeFindToolSlotById(props.resources.index, parseInt(id)),
|
||||
tools: selectAllTools(props.resources.index),
|
||||
findTool: (id: number) => maybeFindToolById(props.resources.index, id),
|
||||
dispatch: props.dispatch,
|
||||
botPosition: validBotLocationData(props.bot.hardware.location_data).position,
|
||||
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
|
||||
});
|
||||
import { EditToolSlotProps, mapStateToPropsEdit } from "./map_to_props_add_edit";
|
||||
|
||||
export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
|
||||
|
||||
|
@ -65,7 +40,7 @@ export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
|
|||
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
|
||||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
title={t("Edit tool slot")}
|
||||
title={t("Edit slot")}
|
||||
backTo={"/app/designer/tools"}
|
||||
panel={Panel.Tools} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
|
@ -75,14 +50,20 @@ export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
|
|||
tools={this.props.tools}
|
||||
tool={this.tool}
|
||||
botPosition={this.props.botPosition}
|
||||
xySwap={this.props.xySwap}
|
||||
quadrant={this.props.quadrant}
|
||||
isActive={this.props.isActive}
|
||||
updateToolSlot={this.updateSlot(toolSlot)} />
|
||||
<button
|
||||
className="fb-button gray no-float"
|
||||
onClick={() => {
|
||||
const { x, y, z } = toolSlot.body;
|
||||
const x = toolSlot.body.gantry_mounted
|
||||
? this.props.botPosition.x ?? toolSlot.body.x
|
||||
: toolSlot.body.x;
|
||||
const { y, z } = toolSlot.body;
|
||||
moveAbs({ x, y, z });
|
||||
}}>
|
||||
{t("Move FarmBot to tool slot location")}
|
||||
{t("Move FarmBot to slot location")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button red no-float"
|
||||
|
@ -98,4 +79,4 @@ export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
|
|||
}
|
||||
}
|
||||
|
||||
export const EditToolSlot = connect(mapStateToProps)(RawEditToolSlot);
|
||||
export const EditToolSlot = connect(mapStateToPropsEdit)(RawEditToolSlot);
|
||||
|
|
|
@ -29,13 +29,16 @@ import { isBotOnline } from "../../devices/must_be_online";
|
|||
import { BotState } from "../../devices/interfaces";
|
||||
import { NetworkState } from "../../connectivity/interfaces";
|
||||
import { getStatus } from "../../connectivity/reducer_support";
|
||||
import { setToolHover } from "../map/layers/tool_slots/tool_graphics";
|
||||
import {
|
||||
setToolHover, ToolSlotSVG, ToolSVG
|
||||
} from "../map/layers/tool_slots/tool_graphics";
|
||||
import { ToolSelection } from "./tool_slot_edit_components";
|
||||
import { error } from "../../toast/toast";
|
||||
import {
|
||||
isExpressBoard, getFwHardwareValue
|
||||
} from "../../devices/components/firmware_hardware_support";
|
||||
import { getFbosConfig } from "../../resources/getters";
|
||||
import { isActive } from "./edit_tool";
|
||||
|
||||
export interface ToolsProps {
|
||||
tools: TaggedTool[];
|
||||
|
@ -48,6 +51,7 @@ export interface ToolsProps {
|
|||
botToMqttStatus: NetworkState;
|
||||
hoveredToolSlot: string | undefined;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
export interface ToolsState {
|
||||
|
@ -65,6 +69,7 @@ export const mapStateToProps = (props: Everything): ToolsProps => ({
|
|||
botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]),
|
||||
hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot,
|
||||
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
|
||||
isActive: isActive(selectAllToolSlotPointers(props.resources.index)),
|
||||
});
|
||||
|
||||
const toolStatus = (value: number | undefined): string => {
|
||||
|
@ -121,7 +126,7 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
<div className="mounted-tool">
|
||||
<div className="mounted-tool-header">
|
||||
<label>{t("mounted tool")}</label>
|
||||
<Help text={Content.MOUNTED_TOOL} />
|
||||
<Help text={Content.MOUNTED_TOOL} requireClick={true} />
|
||||
</div>
|
||||
<ToolSelection
|
||||
tools={this.props.tools}
|
||||
|
@ -131,6 +136,7 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
{ mounted_tool_id: tool_id }));
|
||||
this.props.dispatch(save(this.props.device.uuid));
|
||||
}}
|
||||
isActive={this.props.isActive}
|
||||
filterSelectedTool={true} />
|
||||
<div className="tool-verification-status">
|
||||
<p>{t("status")}: {toolStatus(this.toolVerificationValue)}</p>
|
||||
|
@ -165,7 +171,8 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
hovered={toolSlot.uuid === this.props.hoveredToolSlot}
|
||||
dispatch={this.props.dispatch}
|
||||
toolSlot={toolSlot}
|
||||
getToolName={this.getToolName} />)}
|
||||
isActive={this.props.isActive}
|
||||
tools={this.props.tools} />)}
|
||||
</div>
|
||||
|
||||
Tools = () =>
|
||||
|
@ -185,6 +192,8 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
.map(tool =>
|
||||
<ToolInventoryItem key={tool.uuid}
|
||||
toolId={tool.body.id}
|
||||
active={this.props.isActive(tool.body.id)}
|
||||
mounted={this.mountedTool?.uuid == tool.uuid}
|
||||
toolName={tool.body.name || t("Unnamed")} />)}
|
||||
</div>
|
||||
|
||||
|
@ -202,12 +211,8 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
tools: this.isExpress
|
||||
? t("seed containers")
|
||||
: t("tools and seed containers"),
|
||||
toolSlots: this.isExpress
|
||||
? t("seed container slots")
|
||||
: t("tool slots"),
|
||||
addSlot: this.isExpress
|
||||
? t("Add slot")
|
||||
: t("Add tool slot"),
|
||||
toolSlots: t("slots"),
|
||||
addSlot: t("Add slot"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -240,25 +245,46 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
|
|||
}
|
||||
}
|
||||
|
||||
interface ToolSlotInventoryItemProps {
|
||||
export interface ToolSlotInventoryItemProps {
|
||||
toolSlot: TaggedToolSlotPointer;
|
||||
getToolName(toolId: number | undefined): string | undefined;
|
||||
tools: TaggedTool[];
|
||||
hovered: boolean;
|
||||
dispatch: Function;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
||||
export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
||||
const { x, y, z, id, tool_id, gantry_mounted } = props.toolSlot.body;
|
||||
const toolName = props.tools
|
||||
.filter(tool => tool.body.id == tool_id)[0]?.body.name;
|
||||
return <div
|
||||
className={`tool-slot-search-item ${props.hovered ? "hovered" : ""}`}
|
||||
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}
|
||||
onMouseEnter={() => props.dispatch(setToolHover(props.toolSlot.uuid))}
|
||||
onMouseLeave={() => props.dispatch(setToolHover(undefined))}>
|
||||
<Row>
|
||||
<Col xs={7}>
|
||||
<p>{props.getToolName(tool_id) || t("Empty")}</p>
|
||||
<Col xs={2}>
|
||||
<ToolSlotSVG
|
||||
toolSlot={props.toolSlot}
|
||||
toolName={tool_id ? toolName : "Empty"}
|
||||
renderRotation={false} />
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
<Col xs={6}>
|
||||
<div className={"tool-selection-wrapper"}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<ToolSelection
|
||||
tools={props.tools}
|
||||
selectedTool={props.tools
|
||||
.filter(tool => tool.body.id == tool_id)[0]}
|
||||
onChange={update => {
|
||||
props.dispatch(edit(props.toolSlot, update));
|
||||
props.dispatch(save(props.toolSlot.uuid));
|
||||
}}
|
||||
isActive={props.isActive}
|
||||
filterSelectedTool={false} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={4} className={"tool-slot-position-info"}>
|
||||
<p className="tool-slot-position">
|
||||
{botPositionLabel({ x, y, z }, gantry_mounted)}
|
||||
</p>
|
||||
|
@ -270,16 +296,28 @@ const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
|||
interface ToolInventoryItemProps {
|
||||
toolName: string;
|
||||
toolId: number | undefined;
|
||||
mounted: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const ToolInventoryItem = (props: ToolInventoryItemProps) =>
|
||||
<div className={"tool-search-item"}
|
||||
const ToolInventoryItem = (props: ToolInventoryItemProps) => {
|
||||
const activeText = props.active ? t("active") : t("inactive");
|
||||
return <div className={"tool-search-item"}
|
||||
onClick={() => history.push(`/app/designer/tools/${props.toolId}`)}>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<Col xs={2}>
|
||||
<ToolSVG toolName={props.toolName} />
|
||||
</Col>
|
||||
<Col xs={7}>
|
||||
<p>{t(props.toolName)}</p>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<p className="tool-status">
|
||||
{props.mounted ? t("mounted") : activeText}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const Tools = connect(mapStateToProps)(RawTools);
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
import { TaggedTool, TaggedToolSlotPointer, FirmwareHardware } from "farmbot";
|
||||
import {
|
||||
selectAllTools, maybeFindToolById, maybeGetToolSlot, maybeFindToolSlotById,
|
||||
selectAllToolSlotPointers,
|
||||
} from "../../resources/selectors";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { validBotLocationData } from "../../util";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import {
|
||||
getFwHardwareValue
|
||||
} from "../../devices/components/firmware_hardware_support";
|
||||
import { getFbosConfig } from "../../resources/getters";
|
||||
import { getWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { BooleanSetting, NumericSetting } from "../../session_keys";
|
||||
import { BotOriginQuadrant, isBotOriginQuadrant } from "../interfaces";
|
||||
import { isActive } from "./edit_tool";
|
||||
|
||||
export interface AddEditToolSlotPropsBase {
|
||||
tools: TaggedTool[];
|
||||
dispatch: Function;
|
||||
botPosition: BotPosition;
|
||||
findTool(id: number): TaggedTool | undefined;
|
||||
firmwareHardware: FirmwareHardware | undefined;
|
||||
xySwap: boolean;
|
||||
quadrant: BotOriginQuadrant;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
export const mapStateToPropsBase = (props: Everything): AddEditToolSlotPropsBase => {
|
||||
const getWebAppConfig = getWebAppConfigValue(() => props);
|
||||
const xySwap = !!getWebAppConfig(BooleanSetting.xy_swap);
|
||||
const rawQuadrant = getWebAppConfig(NumericSetting.bot_origin_quadrant);
|
||||
const quadrant = isBotOriginQuadrant(rawQuadrant) ? rawQuadrant : 2;
|
||||
return {
|
||||
tools: selectAllTools(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
botPosition: validBotLocationData(props.bot.hardware.location_data).position,
|
||||
findTool: (id: number) => maybeFindToolById(props.resources.index, id),
|
||||
firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)),
|
||||
xySwap,
|
||||
quadrant,
|
||||
isActive: isActive(selectAllToolSlotPointers(props.resources.index)),
|
||||
};
|
||||
};
|
||||
|
||||
export interface AddToolSlotProps extends AddEditToolSlotPropsBase {
|
||||
findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined;
|
||||
}
|
||||
|
||||
export const mapStateToPropsAdd = (props: Everything): AddToolSlotProps => {
|
||||
const mapStateToProps = mapStateToPropsBase(props) as AddToolSlotProps;
|
||||
mapStateToProps.findToolSlot = (uuid: UUID | undefined) =>
|
||||
maybeGetToolSlot(props.resources.index, uuid);
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export interface EditToolSlotProps extends AddEditToolSlotPropsBase {
|
||||
findToolSlot(id: string): TaggedToolSlotPointer | undefined;
|
||||
}
|
||||
|
||||
export const mapStateToPropsEdit = (props: Everything): EditToolSlotProps => {
|
||||
const mapStateToProps = mapStateToPropsBase(props) as EditToolSlotProps;
|
||||
mapStateToProps.findToolSlot = (id: string) =>
|
||||
maybeFindToolSlotById(props.resources.index, parseInt(id));
|
||||
return mapStateToProps;
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Xyz, TaggedTool, TaggedToolSlotPointer } from "farmbot";
|
||||
import { Row, Col, BlurableInput, FBSelect, NULL_CHOICE } from "../../ui";
|
||||
import { Row, Col, BlurableInput, FBSelect, NULL_CHOICE, DropDownItem } from "../../ui";
|
||||
import {
|
||||
directionIconClass, positionButtonTitle, newSlotDirection, positionIsDefined
|
||||
} from "../../tools/components/toolbay_slot_menu";
|
||||
|
@ -10,6 +10,9 @@ import {
|
|||
} from "../../tools/components/toolbay_slot_direction_selection";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
|
||||
import { Popover } from "@blueprintjs/core";
|
||||
import { ToolSlotSVG } from "../map/layers/tool_slots/tool_graphics";
|
||||
import { BotOriginQuadrant } from "../interfaces";
|
||||
|
||||
export interface GantryMountedInputProps {
|
||||
gantryMounted: boolean;
|
||||
|
@ -24,25 +27,6 @@ export const GantryMountedInput = (props: GantryMountedInputProps) =>
|
|||
checked={props.gantryMounted} />
|
||||
</fieldset>;
|
||||
|
||||
export interface UseCurrentLocationInputRowProps {
|
||||
botPosition: BotPosition;
|
||||
onChange(botPosition: BotPosition): void;
|
||||
}
|
||||
|
||||
export const UseCurrentLocationInputRow =
|
||||
(props: UseCurrentLocationInputRowProps) =>
|
||||
<fieldset className="use-current-location-input">
|
||||
<label>{t("Use current location")}</label>
|
||||
<button
|
||||
className="blue fb-button"
|
||||
title={positionButtonTitle(props.botPosition)}
|
||||
onClick={() => positionIsDefined(props.botPosition) &&
|
||||
props.onChange(props.botPosition)}>
|
||||
<i className="fa fa-crosshairs" />
|
||||
</button>
|
||||
<p>{positionButtonTitle(props.botPosition)}</p>
|
||||
</fieldset>;
|
||||
|
||||
export interface SlotDirectionInputRowProps {
|
||||
toolPulloutDirection: ToolPulloutDirection;
|
||||
onChange(update: { pullout_direction: ToolPulloutDirection }): void;
|
||||
|
@ -51,7 +35,7 @@ export interface SlotDirectionInputRowProps {
|
|||
export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
|
||||
<fieldset className="tool-slot-direction-input">
|
||||
<label>
|
||||
{t("Change slot direction")}
|
||||
{t("Change direction")}
|
||||
</label>
|
||||
<i className={"direction-icon "
|
||||
+ directionIconClass(props.toolPulloutDirection)}
|
||||
|
@ -72,24 +56,25 @@ export interface ToolSelectionProps {
|
|||
selectedTool: TaggedTool | undefined;
|
||||
onChange(update: { tool_id: number }): void;
|
||||
filterSelectedTool: boolean;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
export const ToolSelection = (props: ToolSelectionProps) =>
|
||||
<FBSelect
|
||||
list={props.tools
|
||||
.filter(tool => (!props.filterSelectedTool || !props.selectedTool)
|
||||
|| tool.body.id != props.selectedTool.body.id)
|
||||
list={([NULL_CHOICE] as DropDownItem[]).concat(props.tools
|
||||
.filter(tool => !props.filterSelectedTool
|
||||
|| tool.body.id != props.selectedTool?.body.id)
|
||||
.filter(tool => !props.isActive(tool.body.id))
|
||||
.map(tool => ({
|
||||
label: tool.body.name || "untitled",
|
||||
value: tool.body.id || 0,
|
||||
}))
|
||||
.filter(ddi => ddi.value > 0)}
|
||||
.filter(ddi => ddi.value > 0))}
|
||||
selectedItem={props.selectedTool
|
||||
? {
|
||||
label: props.selectedTool.body.name || "untitled",
|
||||
value: "" + props.selectedTool.body.id
|
||||
} : NULL_CHOICE}
|
||||
allowEmpty={true}
|
||||
onChange={ddi =>
|
||||
props.onChange({ tool_id: parseInt("" + ddi.value) })} />;
|
||||
|
||||
|
@ -98,6 +83,7 @@ export interface ToolInputRowProps {
|
|||
selectedTool: TaggedTool | undefined;
|
||||
onChange(update: { tool_id: number }): void;
|
||||
isExpress: boolean;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
export const ToolInputRow = (props: ToolInputRowProps) =>
|
||||
|
@ -113,6 +99,7 @@ export const ToolInputRow = (props: ToolInputRowProps) =>
|
|||
tools={props.tools}
|
||||
selectedTool={props.selectedTool}
|
||||
onChange={props.onChange}
|
||||
isActive={props.isActive}
|
||||
filterSelectedTool={false} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -122,24 +109,43 @@ export interface SlotLocationInputRowProps {
|
|||
slotLocation: Record<Xyz, number>;
|
||||
gantryMounted: boolean;
|
||||
onChange(update: Partial<Record<Xyz, number>>): void;
|
||||
botPosition: BotPosition;
|
||||
}
|
||||
|
||||
export const SlotLocationInputRow = (props: SlotLocationInputRowProps) =>
|
||||
<div className="tool-slot-location-input">
|
||||
<Row>
|
||||
{["x", "y", "z"].map((axis: Xyz) =>
|
||||
<Col xs={4} key={axis}>
|
||||
<label>{t("{{axis}} (mm)", { axis })}</label>
|
||||
{axis == "x" && props.gantryMounted
|
||||
? <input disabled value={t("Gantry")} />
|
||||
: <BlurableInput
|
||||
type="number"
|
||||
value={props.slotLocation[axis]}
|
||||
min={axis == "z" ? undefined : 0}
|
||||
onCommit={e => props.onChange({
|
||||
[axis]: parseFloat(e.currentTarget.value)
|
||||
})} />}
|
||||
</Col>)}
|
||||
<Col xs={11} className="axis-inputs">
|
||||
{["x", "y", "z"].map((axis: Xyz) =>
|
||||
<Col xs={4} key={axis}>
|
||||
<label>{t("{{axis}} (mm)", { axis })}</label>
|
||||
{axis == "x" && props.gantryMounted
|
||||
? <input disabled value={t("Gantry")} />
|
||||
: <BlurableInput
|
||||
type="number"
|
||||
value={props.slotLocation[axis]}
|
||||
min={axis == "z" ? undefined : 0}
|
||||
onCommit={e => props.onChange({
|
||||
[axis]: parseFloat(e.currentTarget.value)
|
||||
})} />}
|
||||
</Col>)}
|
||||
</Col>
|
||||
<Col xs={1} className="use-current-location">
|
||||
<Popover>
|
||||
<i className="fa fa-question-circle help-icon" />
|
||||
<div className="current-location-info">
|
||||
<label>{t("Use current location")}</label>
|
||||
<p>{positionButtonTitle(props.botPosition)}</p>
|
||||
</div>
|
||||
</Popover>
|
||||
<button
|
||||
className="blue fb-button"
|
||||
title={positionButtonTitle(props.botPosition)}
|
||||
onClick={() => positionIsDefined(props.botPosition) &&
|
||||
props.onChange(props.botPosition)}>
|
||||
<i className="fa fa-crosshairs" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
||||
|
@ -150,26 +156,31 @@ export interface SlotEditRowsProps {
|
|||
botPosition: BotPosition;
|
||||
updateToolSlot(update: Partial<TaggedToolSlotPointer["body"]>): void;
|
||||
isExpress: boolean;
|
||||
xySwap: boolean;
|
||||
quadrant: BotOriginQuadrant;
|
||||
isActive(id: number | undefined): boolean;
|
||||
}
|
||||
|
||||
export const SlotEditRows = (props: SlotEditRowsProps) =>
|
||||
<div className="tool-slot-edit-rows">
|
||||
<ToolSlotSVG toolSlot={props.toolSlot}
|
||||
toolName={props.tool ? props.tool.body.name : "Empty"}
|
||||
renderRotation={true} xySwap={props.xySwap} quadrant={props.quadrant} />
|
||||
<SlotLocationInputRow
|
||||
slotLocation={props.toolSlot.body}
|
||||
gantryMounted={props.toolSlot.body.gantry_mounted}
|
||||
botPosition={props.botPosition}
|
||||
onChange={props.updateToolSlot} />
|
||||
<ToolInputRow
|
||||
isExpress={props.isExpress}
|
||||
tools={props.tools}
|
||||
selectedTool={props.tool}
|
||||
isActive={props.isActive}
|
||||
onChange={props.updateToolSlot} />
|
||||
{!props.toolSlot.body.gantry_mounted &&
|
||||
<SlotDirectionInputRow
|
||||
toolPulloutDirection={props.toolSlot.body.pullout_direction}
|
||||
onChange={props.updateToolSlot} />}
|
||||
<UseCurrentLocationInputRow
|
||||
botPosition={props.botPosition}
|
||||
onChange={props.updateToolSlot} />
|
||||
{!props.isExpress &&
|
||||
<GantryMountedInput
|
||||
gantryMounted={props.toolSlot.body.gantry_mounted}
|
||||
|
|
|
@ -58,7 +58,7 @@ describe("tourPageNavigation()", () => {
|
|||
Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.title);
|
||||
mockDev = false;
|
||||
mockState.resources = buildResourceIndex([]);
|
||||
expect(getTitles()).toContain("Add tools and tool slots");
|
||||
expect(getTitles()).toContain("Add tools and slots");
|
||||
expect(getTitles()).not.toContain("Add seed containers");
|
||||
const fbosConfig = fakeFbosConfig();
|
||||
fbosConfig.body.firmware_hardware = "express_k10";
|
||||
|
|
|
@ -46,14 +46,14 @@ const toolsStep = () => hasTools()
|
|||
: t(TourContent.ADD_TOOLS_AND_SLOTS),
|
||||
title: isExpress()
|
||||
? t("Add seed containers and slots")
|
||||
: t("Add tools and tool slots"),
|
||||
: t("Add tools and slots"),
|
||||
}];
|
||||
|
||||
const toolSlotsStep = () => hasTools()
|
||||
? [{
|
||||
target: ".tool-slots",
|
||||
content: t(TourContent.ADD_TOOLS_AND_SLOTS),
|
||||
title: t("Add tool slots"),
|
||||
title: t("Add slots"),
|
||||
}]
|
||||
: [];
|
||||
|
||||
|
|
|
@ -20,10 +20,14 @@ import {
|
|||
import { VariableNode } from "../sequences/locals_list/locals_list_support";
|
||||
import { t } from "../i18next_wrapper";
|
||||
|
||||
export interface Vector3Plus extends Vector3 {
|
||||
gantry_mounted: boolean;
|
||||
}
|
||||
|
||||
export interface SequenceMeta {
|
||||
celeryNode: VariableNode;
|
||||
dropdown: DropDownItem;
|
||||
vector: Vector3 | undefined;
|
||||
vector: Vector3 | Vector3Plus | undefined;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ describe("formatTool()", () => {
|
|||
const toolSlot = fakeToolSlot();
|
||||
toolSlot.body.gantry_mounted = true;
|
||||
const ddi = formatTool(fakeTool(), toolSlot);
|
||||
expect(ddi.label).toEqual("Foo (---, 0, 0)");
|
||||
expect(ddi.label).toEqual("Foo (gantry, 0, 0)");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ type DropdownHeadingId =
|
|||
export const NAME_MAP: Record<DropdownHeadingId, string> = {
|
||||
"GenericPointer": "Map Points",
|
||||
"Plant": "Plants",
|
||||
"ToolSlot": "Tool Slots",
|
||||
"ToolSlot": "Slots",
|
||||
"Tool": "Tools and Seed Containers",
|
||||
"PointGroup": "Groups",
|
||||
"Other": "Other",
|
||||
|
@ -100,24 +100,27 @@ export const formatTool =
|
|||
const { id, name } = tool.body;
|
||||
const coordinate = slot
|
||||
? {
|
||||
x: slot.body.gantry_mounted ? undefined : slot.body.x,
|
||||
x: slot.body.x,
|
||||
y: slot.body.y,
|
||||
z: slot.body.z
|
||||
}
|
||||
: undefined;
|
||||
const gantryMounted = !!slot?.body.gantry_mounted;
|
||||
return {
|
||||
label: dropDownName((name || "Untitled tool"), coordinate),
|
||||
label: dropDownName((name || "Untitled tool"), coordinate, gantryMounted),
|
||||
value: "" + id,
|
||||
headingId: TOOL
|
||||
};
|
||||
};
|
||||
|
||||
/** Uniformly generate a label for things that have an X/Y/Z value. */
|
||||
export function dropDownName(name: string, v?: Record<Xyz, number | undefined>) {
|
||||
export function dropDownName(name: string, v?: Record<Xyz, number | undefined>,
|
||||
gantryMounted = false) {
|
||||
let label = name || "untitled";
|
||||
if (v) {
|
||||
const labelFor = (axis: number | undefined) => isNumber(axis) ? axis : "---";
|
||||
label += ` (${labelFor(v.x)}, ${labelFor(v.y)}, ${labelFor(v.z)})`;
|
||||
const xLabel = gantryMounted ? t("Gantry") : labelFor(v.x);
|
||||
label += ` (${xLabel}, ${labelFor(v.y)}, ${labelFor(v.z)})`;
|
||||
}
|
||||
return capitalize(label);
|
||||
}
|
||||
|
@ -125,8 +128,8 @@ export function dropDownName(name: string, v?: Record<Xyz, number | undefined>)
|
|||
export const ALL_POINT_LABELS = {
|
||||
"Plant": "All plants",
|
||||
"GenericPointer": "All map points",
|
||||
"Tool": "All tools",
|
||||
"ToolSlot": "All tool slots",
|
||||
"Tool": "All tools and seed containers",
|
||||
"ToolSlot": "All slots",
|
||||
};
|
||||
|
||||
export type EveryPointType = keyof typeof ALL_POINT_LABELS;
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { TileMoveAbsolute } from "../tile_move_absolute";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import {
|
||||
fakeSequence, fakePoint, fakeTool
|
||||
fakeSequence, fakePoint, fakeTool, fakeToolSlot
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
Coordinate,
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
import { emptyState } from "../../../resources/reducer";
|
||||
import { inputEvent } from "../../../__test_support__/fake_html_events";
|
||||
import { StepParams } from "../../interfaces";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("<TileMoveAbsolute/>", () => {
|
||||
const fakeProps = (): StepParams => {
|
||||
|
@ -75,6 +76,25 @@ describe("<TileMoveAbsolute/>", () => {
|
|||
checkField(block, 5, "z-offset", "6");
|
||||
});
|
||||
|
||||
it("disables x-offset", () => {
|
||||
const p = fakeProps();
|
||||
const toolSlot = fakeToolSlot();
|
||||
toolSlot.body.gantry_mounted = true;
|
||||
toolSlot.body.tool_id = 1;
|
||||
const tool = fakeTool();
|
||||
tool.body.id = 1;
|
||||
p.resources = buildResourceIndex([toolSlot, tool]).index;
|
||||
const toolKind: Tool = { kind: "tool", args: { tool_id: 1 } };
|
||||
(p.currentStep as MoveAbsolute).args.location = toolKind;
|
||||
const block = mount(<TileMoveAbsolute {...p} />);
|
||||
const xOffsetInput = block.find("input").at(1);
|
||||
expect(xOffsetInput.props().name).toEqual("offset-x");
|
||||
expect(xOffsetInput.props().disabled).toBeTruthy();
|
||||
const yOffsetInput = block.find("input").at(2);
|
||||
expect(yOffsetInput.props().name).toEqual("offset-y");
|
||||
expect(yOffsetInput.props().disabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it("updates input value", () => {
|
||||
const tma = ordinaryMoveAbs();
|
||||
const mock = jest.fn();
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ToolTips } from "../../constants";
|
|||
import { StepWrapper, StepHeader, StepContent } from "../step_ui";
|
||||
import { StepInputBox } from "../inputs/step_input_box";
|
||||
import {
|
||||
determineDropdown, determineVector
|
||||
determineDropdown, determineVector, Vector3Plus
|
||||
} from "../../resources/sequence_meta";
|
||||
import { LocationForm } from "../locals_list/location_form";
|
||||
import {
|
||||
|
@ -75,11 +75,16 @@ export class TileMoveAbsolute extends React.Component<StepParams, MoveAbsState>
|
|||
};
|
||||
}
|
||||
|
||||
get vector(): Vector3 | undefined {
|
||||
get vector(): Vector3 | Vector3Plus | undefined {
|
||||
const sequenceUuid = this.props.currentSequence.uuid;
|
||||
return determineVector(this.celeryNode, this.props.resources, sequenceUuid);
|
||||
}
|
||||
|
||||
get gantryMounted() {
|
||||
return this.vector && ("gantry_mounted" in this.vector)
|
||||
&& this.vector.gantry_mounted;
|
||||
}
|
||||
|
||||
LocationForm = () =>
|
||||
<LocationForm
|
||||
variable={{
|
||||
|
@ -120,6 +125,7 @@ export class TileMoveAbsolute extends React.Component<StepParams, MoveAbsState>
|
|||
{t("{{axis}}-Offset", { axis })}
|
||||
</label>
|
||||
<BlurableInput type="number"
|
||||
disabled={axis == "x" && this.gantryMounted}
|
||||
onCommit={this.updateInputValue(axis, "offset")}
|
||||
name={`offset-${axis}`}
|
||||
value={(this.args.offset.args[axis] || 0).toString()} />
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"sass-lint": "./node_modules/sass-lint/bin/sass-lint.js -c .sass-lint.yml -v -q",
|
||||
"sass-check": "./node_modules/sass/sass.js --no-source-map frontend/css/_index.scss sass.log",
|
||||
"translation-check": " ./node_modules/jshint/bin/jshint --config public/app-resources/languages/.config public/app-resources/languages/*.js*",
|
||||
"linters": "npm run typecheck && npm run tslint && npm run sass-lint && npm run sass-check && npm run translation-check"
|
||||
"linters": "npm run typecheck; npm run tslint; npm run sass-lint; npm run sass-check; npm run translation-check"
|
||||
},
|
||||
"keywords": [
|
||||
"farmbot"
|
||||
|
|
Loading…
Reference in New Issue