diff --git a/app/models/in_use_point.rb b/app/models/in_use_point.rb index 820684516..4c29f3894 100644 --- a/app/models/in_use_point.rb +++ b/app/models/in_use_point.rb @@ -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", } diff --git a/app/models/tool_slot.rb b/app/models/tool_slot.rb index 22a0abd90..e416dadbd 100644 --- a/app/models/tool_slot.rb +++ b/app/models/tool_slot.rb @@ -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." diff --git a/app/mutations/tools/destroy.rb b/app/mutations/tools/destroy.rb index f60fb3c29..3618a5d98 100644 --- a/app/mutations/tools/destroy.rb +++ b/app/mutations/tools/destroy.rb @@ -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 diff --git a/frontend/constants.ts b/frontend/constants.ts index 7adb0a80f..2c9f6449d 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -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`, diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index 74d11b8af..bb34b097a 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -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 { diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index fea4106db..a915f1f5e 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -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 { diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 02ff86b62..e8530d2c6 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -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; diff --git a/frontend/css/inputs.scss b/frontend/css/inputs.scss index e31b27443..9326c1a44 100644 --- a/frontend/css/inputs.scss +++ b/frontend/css/inputs.scss @@ -154,4 +154,12 @@ select { } } } + &.disabled { + input[type="checkbox"] { + cursor: not-allowed; + &:checked:after { + border-color: $gray; + } + } + } } diff --git a/frontend/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts index 2d0e5a1cd..2f69f5bc3 100644 --- a/frontend/devices/__tests__/actions_test.ts +++ b/frontend/devices/__tests__/actions_test.ts @@ -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", diff --git a/frontend/devices/actions.ts b/frontend/devices/actions.ts index edea6ce0f..c1a9565ea 100644 --- a/frontend/devices/actions.ts +++ b/frontend/devices/actions.ts @@ -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" diff --git a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx index 0c4cdb602..55042b2b4 100644 --- a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx @@ -89,6 +89,7 @@ describe("", () => { const p = fakeProps(); const commit = "abcdefgh"; p.botInfoSettings.firmware_commit = commit; + p.botInfoSettings.firmware_version = "1.0.0"; const wrapper = mount(); expect(wrapper.find("a").last().text()).toEqual(commit); expect(wrapper.find("a").last().props().href?.split("/").slice(-1)[0]) @@ -115,6 +116,7 @@ describe("", () => { 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(); diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index c06a4b627..c4b138cbd 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -69,7 +69,7 @@ export function WiFiStrengthDisplay( return

{t("WiFi strength")}: - {wifiStrength ? dbString : "N/A"} + {wifiStrength ? `${dbString} (${percentString})` : "N/A"}

{wifiStrength &&
@@ -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
; type EveryTimeTable = Record; +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 - - - - - - + + + + + + + + ; }; diff --git a/frontend/devices/components/maybe_highlight.tsx b/frontend/devices/components/maybe_highlight.tsx index eab806b0f..7381534fa 100644 --- a/frontend/devices/components/maybe_highlight.tsx +++ b/frontend/devices/components/maybe_highlight.tsx @@ -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 = {}; -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(); diff --git a/frontend/farm_designer/__tests__/farm_designer_test.tsx b/frontend/farm_designer/__tests__/farm_designer_test.tsx index 82249de16..4b7251dcc 100644 --- a/frontend/farm_designer/__tests__/farm_designer_test.tsx +++ b/frontend/farm_designer/__tests__/farm_designer_test.tsx @@ -62,6 +62,7 @@ describe("", () => { sensors: [], groups: [], shouldDisplay: () => false, + mountedToolName: undefined, }); it("loads default map settings", () => { diff --git a/frontend/farm_designer/__tests__/state_to_props_test.tsx b/frontend/farm_designer/__tests__/state_to_props_test.tsx index 68dc494d8..5e48b7ef3 100644 --- a/frontend/farm_designer/__tests__/state_to_props_test.tsx +++ b/frontend/farm_designer/__tests__/state_to_props_test.tsx @@ -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" })); diff --git a/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx b/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx index 04dfb8a2d..85d74e786 100644 --- a/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx @@ -55,7 +55,7 @@ describe("", () => { const wrapper = mount(); 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(); diff --git a/frontend/farm_designer/farm_events/add_farm_event.tsx b/frontend/farm_designer/farm_events/add_farm_event.tsx index 95e11f9af..878f6dabb 100644 --- a/frontend/farm_designer/farm_events/add_farm_event.tsx +++ b/frontend/farm_designer/farm_events/add_farm_event.tsx @@ -102,7 +102,7 @@ export class RawAddFarmEvent 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} diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index 746bb48cb..ab6721152 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -211,6 +211,7 @@ export class RawFarmDesigner extends React.Component> { timeSettings={this.props.timeSettings} sensors={this.props.sensors} groups={this.props.groups} + mountedToolName={this.props.mountedToolName} shouldDisplay={this.props.shouldDisplay} />
diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index d265e5fd9..2f243ae78 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -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 { diff --git a/frontend/farm_designer/map/__tests__/garden_map_test.tsx b/frontend/farm_designer/map/__tests__/garden_map_test.tsx index f2d6b6ac7..61b48f64f 100644 --- a/frontend/farm_designer/map/__tests__/garden_map_test.tsx +++ b/frontend/farm_designer/map/__tests__/garden_map_test.tsx @@ -124,6 +124,7 @@ const fakeProps = (): GardenMapProps => ({ timeSettings: fakeTimeSettings(), groups: [], shouldDisplay: () => false, + mountedToolName: undefined, }); describe("", () => { @@ -200,6 +201,16 @@ describe("", () => { expect(getGardenCoordinates).not.toHaveBeenCalled(); }); + it("starts drag on background: does nothing when in move mode", () => { + const wrapper = mount(); + 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(); mockMode = Mode.createPoint; @@ -348,7 +359,7 @@ describe("", () => { 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("", () => { 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(); + wrapper.instance().closePanel()(); + expect(closePlantInfo).not.toHaveBeenCalled(); + }); + it("calls unselectPlant on unmount", () => { const wrapper = shallow(); wrapper.unmount(); @@ -405,7 +425,7 @@ describe("", () => { const point = fakePoint(); point.body.id = 1; p.allPoints = [point]; - const wrapper = shallow(); + const wrapper = mount(); expect(wrapper.instance().pointsSelectedByGroup).toEqual([point]); }); }); diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 063c5ccbf..fa2694827 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -160,6 +160,8 @@ export class GardenMap extends /** Map background drag start actions. */ startDragOnBackground = (e: React.MouseEvent): 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 = () => ", () => { p.position.x = 100; const wrapper = shallow(); 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("", () => { p.position.x = 100; p.mapTransformProps.xySwap = true; const wrapper = shallow(); - 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("", () => { })); expect(wrapper.text()).toEqual("(100, 0, 0)"); }); + + it("shows mounted tool", () => { + const p = fakeProps(); + p.mountedToolName = "Seeder"; + const wrapper = shallow(); + expect(wrapper.find("#UTM-wrapper").find("#mounted-tool").length) + .toEqual(1); + }); }); diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx index 8649870e5..c61de5f08 100644 --- a/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx +++ b/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx @@ -25,6 +25,7 @@ describe("", () => { peripherals: [], eStopStatus: false, getConfigValue: jest.fn(), + mountedToolName: undefined, }; } diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx index 305d29d8c..3ea77f5b4 100644 --- a/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx +++ b/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx @@ -19,6 +19,7 @@ describe("", () => { peripherals: [], eStopStatus: false, getConfigValue: () => true, + mountedToolName: undefined, }; } diff --git a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx index 59d699041..9ceef1efa 100644 --- a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx +++ b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx @@ -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 - this.setHover(true)} onMouseLeave={() => this.setHover(false)} - cx={positionQ.qx} - cy={positionQ.qy} - r={35} fillOpacity={opacity} - fill={color} /> + fill={color}> + {this.props.mountedToolName + ? + + + + : } + {encoderFigure && ", () => { const fakeProps = (): ToolSlotGraphicProps => ({ @@ -15,6 +19,7 @@ describe("", () => { pulloutDirection: 0, quadrant: 2, xySwap: false, + occupied: true, }); it.each<[number, BotOriginQuadrant, boolean, string]>([ @@ -89,6 +94,29 @@ describe("", () => { }); }; + it("renders empty tool slot styling", () => { + const p = fakeProps(); + p.tool = ToolNames.emptyToolSlot; + const wrapper = svgMount(); + 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(); + 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(); const props = wrapper.find("circle").last().props(); @@ -107,12 +135,96 @@ describe("", () => { }); 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); const elements = wrapper.find("#seed-bin").find("circle"); expect(elements.length).toEqual(2); @@ -121,20 +233,19 @@ describe("", () => { it("renders bin hover styling", () => { const p = fakeProps(); - p.tool = "seedBin"; + p.tool = ToolNames.seedBin; p.toolProps.hovered = true; const wrapper = svgMount(); - 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(); const elements = wrapper.find("#seed-tray"); expect(elements.find("circle").length).toEqual(2); @@ -144,20 +255,19 @@ describe("", () => { it("renders tray hover styling", () => { const p = fakeProps(); - p.tool = "seedTray"; + p.tool = ToolNames.seedTray; p.toolProps.hovered = true; const wrapper = svgMount(); - 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(); const elements = wrapper.find("#seed-trough"); expect(elements.find("circle").length).toEqual(0); @@ -166,15 +276,49 @@ describe("", () => { it("renders trough hover styling", () => { const p = fakeProps(); - p.tool = "seedTrough"; + p.tool = ToolNames.seedTrough; p.toolProps.hovered = true; const wrapper = svgMount(); - 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("", () => { + const fakeProps = (): ToolSVGProps => ({ + toolName: "seed trough", + }); + + it("renders trough", () => { + const wrapper = shallow(); + expect(wrapper.find("svg").props().viewBox).toEqual("-25 0 50 1"); + }); +}); + +describe("", () => { + 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(); + 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(); + expect(wrapper.find(ToolbaySlot).length).toEqual(0); }); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts index 41323925d..7b72f45ae 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts @@ -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); }); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index f47a485eb..a53c1508f 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -71,7 +71,7 @@ describe("", () => { p.slot.tool = undefined; p.hoveredToolSlot = p.slot.toolSlot.uuid; const wrapper = svgMount(); - 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("", () => { 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(); + 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(); + 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(); + 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(); + 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"; } diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx index b87288589..51dc35f5d 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx @@ -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) => { - @@ -91,9 +100,14 @@ export const ToolbaySlot = (props: ToolSlotGraphicProps) => { export const Tool = (props: ToolProps) => { switch (props.tool) { + case ToolNames.weeder: return ; + case ToolNames.wateringNozzle: return ; + case ToolNames.seeder: return ; + case ToolNames.soilSensor: return ; case ToolNames.seedBin: return ; case ToolNames.seedTray: return ; case ToolNames.seedTrough: return ; + case ToolNames.emptyToolSlot: return ; default: return ; } }; @@ -115,6 +129,115 @@ const StandardTool = (props: ToolGraphicProps) => { ; }; +const EmptySlot = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + ; +}; + +const Weeder = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + const size = 10; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + + ; +}; + +const WateringNozzle = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + + + + + + + + ; +}; + +const Seeder = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + const size = 10; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + ; +}; + +const SoilSensor = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + const size = 20; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + + ; +}; + const seedBinGradient = @@ -214,3 +337,62 @@ const SeedTrough = (props: ToolGraphicProps) => { fill={hovered ? Color.darkGray : Color.mediumGray} /> ; }; + +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 + ? + + {props.toolSlot.body.tool_id && + } + + : + {props.toolSlot.body.pullout_direction && + } + {(props.toolSlot.body.tool_id || + !props.toolSlot.body.pullout_direction) && + } + ; +}; + +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 + } +; +}; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx index 18d9ff713..93bfcf04f 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx @@ -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 { +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 !DevSettings.futureFeaturesEnabled() && history.push(`/app/designer/tool-slots/${id}`)}> - {pullout_direction && + {pullout_direction && !gantry_mounted && } {gantry_mounted && } @@ -67,6 +74,7 @@ export const ToolSlotPoint = (props: TSPProps) => { x={qx} y={qy} pulloutDirection={pullout_direction} + gantryMounted={gantry_mounted} quadrant={quadrant} xySwap={xySwap} /> ; diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx index 1054d9806..d987e4db7 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx @@ -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("", () => { }); it("shows paths", () => { - mockDev = false; const p = fakeProps(); const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("0m"); }); - it("doesn't show paths", () => { - mockDev = true; - const p = fakeProps(); - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("0m"); - }); - it("shows random warning text", () => { const p = fakeProps(); p.group.body.sort_type = "random"; diff --git a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx index 31cecd0ba..4ad44cabf 100644 --- a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx @@ -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"; /** diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_sort_test.ts similarity index 59% rename from frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx rename to frontend/farm_designer/point_groups/__tests__/point_group_sort_test.ts index bfd1f1d60..096e36b7d 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_sort_test.ts @@ -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(); diff --git a/frontend/farm_designer/point_groups/criteria/add.tsx b/frontend/farm_designer/point_groups/criteria/add.tsx index d4f9275b2..a24826756 100644 --- a/frontend/farm_designer/point_groups/criteria/add.tsx +++ b/frontend/farm_designer/point_groups/criteria/add.tsx @@ -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, diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index 5c50e92a3..66ae340e7 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -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 - {!DevSettings.futureFeaturesEnabled() - ? p.body.id))} - pathPoints={this.pointsSelectedByGroup} - dispatch={dispatch} - group={group} /> - : } + p.body.id))} + pathPoints={this.pointsSelectedByGroup} + dispatch={dispatch} + group={group} />

{group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)}

diff --git a/frontend/farm_designer/point_groups/group_list_panel.tsx b/frontend/farm_designer/point_groups/group_list_panel.tsx index 6f7949970..93f7884d8 100644 --- a/frontend/farm_designer/point_groups/group_list_panel.tsx +++ b/frontend/farm_designer/point_groups/group_list_panel.tsx @@ -48,7 +48,7 @@ export class RawGroupListPanel extends React.Component + title={t("Add group")}> diff --git a/frontend/farm_designer/point_groups/group_order_visual.tsx b/frontend/farm_designer/point_groups/group_order_visual.tsx index cdf9f21cc..800df2c13 100644 --- a/frontend/farm_designer/point_groups/group_order_visual.tsx +++ b/frontend/farm_designer/point_groups/group_order_visual.tsx @@ -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"; diff --git a/frontend/farm_designer/point_groups/paths.tsx b/frontend/farm_designer/point_groups/paths.tsx index e2368e967..9c9b5b134 100644 --- a/frontend/farm_designer/point_groups/paths.tsx +++ b/frontend/farm_designer/point_groups/paths.tsx @@ -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"; diff --git a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx b/frontend/farm_designer/point_groups/point_group_sort.ts similarity index 58% rename from frontend/farm_designer/point_groups/point_group_sort_selector.tsx rename to frontend/farm_designer/point_groups/point_group_sort.ts index e1c335dd9..36803a2d2 100644 --- a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx +++ b/frontend/farm_designer/point_groups/point_group_sort.ts @@ -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 => ({ "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 ; -} - type Sorter = (p: TaggedPoint[]) => TaggedPoint[]; type SortDictionary = Record; diff --git a/frontend/farm_designer/points/__tests__/create_points_test.tsx b/frontend/farm_designer/points/__tests__/create_points_test.tsx index 669958c6e..1d66b188c 100644 --- a/frontend/farm_designer/points/__tests__/create_points_test.tsx +++ b/frontend/farm_designer/points/__tests__/create_points_test.tsx @@ -70,14 +70,14 @@ describe("", () => { it("renders for points", () => { mockPath = "/app/designer"; const wrapper = mount(); - ["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(); - ["create weed", "delete", "x", "y", "radius", "color"] + ["add weed", "delete", "x", "y", "radius", "color"] .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); diff --git a/frontend/farm_designer/points/create_points.tsx b/frontend/farm_designer/points/create_points.tsx index 227ab3051..a0ebd2143 100644 --- a/frontend/farm_designer/points/create_points.tsx +++ b/frontend/farm_designer/points/create_points.tsx @@ -274,7 +274,7 @@ export class RawCreatePoints diff --git a/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx b/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx index 883600f9c..aeaa2a0b1 100644 --- a/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx +++ b/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx @@ -15,7 +15,7 @@ describe("", () => { it("renders add garden panel", () => { const wrapper = mount(); - expect(wrapper.text()).toContain("create new garden"); + expect(wrapper.text().toLowerCase()).toContain("add garden"); }); }); diff --git a/frontend/farm_designer/saved_gardens/garden_add.tsx b/frontend/farm_designer/saved_gardens/garden_add.tsx index b0a1b3daa..255f43dde 100644 --- a/frontend/farm_designer/saved_gardens/garden_add.tsx +++ b/frontend/farm_designer/saved_gardens/garden_add.tsx @@ -29,7 +29,7 @@ export class RawAddGarden extends React.Component { diff --git a/frontend/farm_designer/saved_gardens/garden_snapshot.tsx b/frontend/farm_designer/saved_gardens/garden_snapshot.tsx index 6687f10e0..2c906ae38 100644 --- a/frontend/farm_designer/saved_gardens/garden_snapshot.tsx +++ b/frontend/farm_designer/saved_gardens/garden_snapshot.tsx @@ -49,7 +49,7 @@ export class GardenSnapshot
; } diff --git a/frontend/farm_designer/state_to_props.ts b/frontend/farm_designer/state_to_props.ts index b5f76e15b..c1e32879b 100644 --- a/frontend/farm_designer/state_to_props.ts +++ b/frontend/farm_designer/state_to_props.ts @@ -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, }; } diff --git a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx index 76a5c991b..3c631136f 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx @@ -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("", () => { const fakeProps = (): AddToolSlotProps => ({ @@ -32,15 +31,18 @@ describe("", () => { dispatch: jest.fn(), findToolSlot: fakeToolSlot, firmwareHardware: undefined, + xySwap: false, + quadrant: 2, + isActive: jest.fn(), }); it("renders", () => { const wrapper = mount(); - ["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("", () => { const wrapper = mount(); 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("", () => { }); }); -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); }); diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx index c8d06b478..91b6f2d1f 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx @@ -22,7 +22,7 @@ describe("", () => { it("renders", () => { const wrapper = mount(); - expect(wrapper.text()).toContain("Add new tool"); + expect(wrapper.text()).toContain("Add new"); }); it("edits tool name", () => { @@ -60,11 +60,36 @@ describe("", () => { p.firmwareHardware = "express_k10"; p.existingToolNames = ["Seed Trough 1"]; const wrapper = mount(); - 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(); + 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(); + 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(); + wrapper.setState({ toAdd: [] }); + wrapper.find("input").last().simulate("change"); + expect(wrapper.state().toAdd).toEqual(["Seed Trough 2"]); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx index 7057f20dd..d1938eed9 100644 --- a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx @@ -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("", () => { const fakeProps = (): EditToolSlotProps => ({ @@ -29,6 +31,9 @@ describe("", () => { 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("", () => { const p = fakeProps(); p.findToolSlot = () => fakeToolSlot(); const wrapper = mount(); - ["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("", () => { 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(); + 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(); + 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("", () => { 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(); + 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); }); }); diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx index 88193b14c..2bef0eae0 100644 --- a/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx @@ -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("", () => { beforeEach(() => { @@ -32,6 +33,8 @@ describe("", () => { const fakeProps = (): EditToolProps => ({ findTool: jest.fn(() => fakeTool()), dispatch: jest.fn(), + mountedToolId: undefined, + isActive: jest.fn(), }); it("renders", () => { @@ -75,11 +78,38 @@ describe("", () => { 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(); - 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(); + 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(); + 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); + }); +}); diff --git a/frontend/farm_designer/tools/__tests__/index_test.tsx b/frontend/farm_designer/tools/__tests__/index_test.tsx index c4584f781..22f074c0e 100644 --- a/frontend/farm_designer/tools/__tests__/index_test.tsx +++ b/frontend/farm_designer/tools/__tests__/index_test.tsx @@ -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("", () => { botToMqttStatus: "down", hoveredToolSlot: undefined, firmwareHardware: undefined, + isActive: jest.fn(), }); it("renders with no tools", () => { @@ -182,6 +186,62 @@ describe("", () => { const wrapper = mount(); 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(); + 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(); + 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(); + expect(wrapper.find("p").last().text().toLowerCase()).not.toContain("mounted"); + }); +}); + +describe("", () => { + const fakeProps = (): ToolSlotInventoryItemProps => ({ + toolSlot: fakeToolSlot(), + tools: [], + hovered: false, + dispatch: jest.fn(), + isActive: jest.fn(), + }); + + it("changes tool", () => { + const p = fakeProps(); + const wrapper = shallow(); + 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(); + const e = { stopPropagation: jest.fn() }; + wrapper.find(".tool-selection-wrapper").first().simulate("click", e); + expect(e.stopPropagation).toHaveBeenCalled(); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx index 861c26b3f..e75dbf479 100644 --- a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx +++ b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx @@ -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("", () => { const fakeProps = (): GantryMountedInputProps => ({ @@ -30,33 +29,6 @@ describe("", () => { }); }); -describe("", () => { - const fakeProps = (): UseCurrentLocationInputRowProps => ({ - botPosition: { x: undefined, y: undefined, z: undefined }, - onChange: jest.fn(), - }); - - it("renders", () => { - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("use current location"); - }); - - it("doesn't change value", () => { - const p = fakeProps(); - const wrapper = shallow(); - 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(); - wrapper.find("button").simulate("click"); - expect(p.onChange).toHaveBeenCalledWith(p.botPosition); - }); -}); - describe("", () => { const fakeProps = (): SlotDirectionInputRowProps => ({ toolPulloutDirection: 0, @@ -65,7 +37,7 @@ describe("", () => { it("renders", () => { const wrapper = mount(); - 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("", () => { selectedTool: undefined, onChange: jest.fn(), filterSelectedTool: false, + isActive: jest.fn(), }); it("renders", () => { @@ -98,12 +71,13 @@ describe("", () => { 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(); - 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("", () => { selectedTool: undefined, onChange: jest.fn(), isExpress: false, + isActive: jest.fn(), }); it("renders", () => { @@ -164,6 +139,7 @@ describe("", () => { 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("", () => { expect(p.onChange).toHaveBeenCalledWith({ y: 2 }); expect(p.onChange).toHaveBeenCalledWith({ z: 3 }); }); + + it("doesn't use current coordinates", () => { + const p = fakeProps(); + const wrapper = shallow(); + 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(); + wrapper.find("button").simulate("click"); + expect(p.onChange).toHaveBeenCalledWith(p.botPosition); + }); +}); + +describe("", () => { + 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(); + expect(wrapper.text()).toContain("None"); + }); }); diff --git a/frontend/farm_designer/tools/add_tool.tsx b/frontend/farm_designer/tools/add_tool.tsx index 0eb08d86d..4078e7957 100644 --- a/frontend/farm_designer/tools/add_tool.tsx +++ b/frontend/farm_designer/tools/add_tool.tsx @@ -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 { - 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 { } } + StockToolCheckbox = ({ toolName }: { toolName: string }) => { + const alreadyAdded = !this.filterExisting(toolName); + const checked = this.state.toAdd.includes(toolName) || alreadyAdded; + return
+ checked + ? this.remove(toolName) + : this.add(toolName)} /> +
; + } + AddStockTools = () =>
- +
    - {this.stockToolNames().map(n =>
  • {n}
  • )} + {this.stockToolNames().map(n => +
  • + +

    this.setState({ toolName: n })}>{n}

    +
  • )}
@@ -103,16 +133,16 @@ export class RawAddTool extends React.Component { return
+ - - this.setState({ toolName: e.currentTarget.value })} /> + + this.setState({ toolName: e.currentTarget.value })} />
diff --git a/frontend/farm_designer/tools/add_tool_slot.tsx b/frontend/farm_designer/tools/add_tool_slot.tsx index ffd4062d8..381abcfdc 100644 --- a/frontend/farm_designer/tools/add_tool_slot.tsx +++ b/frontend/farm_designer/tools/add_tool_slot.tsx @@ -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 { 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 @@ -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"} @@ -116,4 +90,4 @@ export class RawAddToolSlot } } -export const AddToolSlot = connect(mapStateToProps)(RawAddToolSlot); +export const AddToolSlot = connect(mapStateToPropsAdd)(RawAddToolSlot); diff --git a/frontend/farm_designer/tools/edit_tool.tsx b/frontend/farm_designer/tools/edit_tool.tsx index 41b0fe387..27cfbbca7 100644 --- a/frontend/farm_designer/tools/edit_tool.tsx +++ b/frontend/farm_designer/tools/edit_tool.tsx @@ -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 { @@ -44,6 +57,11 @@ export class RawEditTool extends React.Component { 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 { backTo={"/app/designer/tools"} panel={Panel.Tools} /> + { }} status={SpecialStatus.DIRTY} /> diff --git a/frontend/farm_designer/tools/edit_tool_slot.tsx b/frontend/farm_designer/tools/edit_tool_slot.tsx index 210b283d3..7bf2bc05e 100644 --- a/frontend/farm_designer/tools/edit_tool_slot.tsx +++ b/frontend/farm_designer/tools/edit_tool_slot.tsx @@ -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 { @@ -65,7 +40,7 @@ export class RawEditToolSlot extends React.Component { return @@ -75,14 +50,20 @@ export class RawEditToolSlot extends React.Component { 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)} /> -

{positionButtonTitle(props.botPosition)}

- ; - export interface SlotDirectionInputRowProps { toolPulloutDirection: ToolPulloutDirection; onChange(update: { pullout_direction: ToolPulloutDirection }): void; @@ -51,7 +35,7 @@ export interface SlotDirectionInputRowProps { export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
(!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} /> @@ -122,24 +109,43 @@ export interface SlotLocationInputRowProps { slotLocation: Record; gantryMounted: boolean; onChange(update: Partial>): void; + botPosition: BotPosition; } export const SlotLocationInputRow = (props: SlotLocationInputRowProps) =>
- {["x", "y", "z"].map((axis: Xyz) => - - - {axis == "x" && props.gantryMounted - ? - : props.onChange({ - [axis]: parseFloat(e.currentTarget.value) - })} />} - )} + + {["x", "y", "z"].map((axis: Xyz) => + + + {axis == "x" && props.gantryMounted + ? + : props.onChange({ + [axis]: parseFloat(e.currentTarget.value) + })} />} + )} + + + + +
+ +

{positionButtonTitle(props.botPosition)}

+
+
+ +
; @@ -150,26 +156,31 @@ export interface SlotEditRowsProps { botPosition: BotPosition; updateToolSlot(update: Partial): void; isExpress: boolean; + xySwap: boolean; + quadrant: BotOriginQuadrant; + isActive(id: number | undefined): boolean; } export const SlotEditRows = (props: SlotEditRowsProps) =>
+ {!props.toolSlot.body.gantry_mounted && } - {!props.isExpress && { 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"; diff --git a/frontend/help/tours.ts b/frontend/help/tours.ts index fc4339da5..26f78eda6 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -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"), }] : []; diff --git a/frontend/resources/sequence_meta.ts b/frontend/resources/sequence_meta.ts index be03fff20..b137f2032 100644 --- a/frontend/resources/sequence_meta.ts +++ b/frontend/resources/sequence_meta.ts @@ -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; } diff --git a/frontend/sequences/locals_list/__tests__/location_form_list_test.ts b/frontend/sequences/locals_list/__tests__/location_form_list_test.ts index c857e97c6..2d431305e 100644 --- a/frontend/sequences/locals_list/__tests__/location_form_list_test.ts +++ b/frontend/sequences/locals_list/__tests__/location_form_list_test.ts @@ -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)"); }); }); diff --git a/frontend/sequences/locals_list/location_form_list.ts b/frontend/sequences/locals_list/location_form_list.ts index 2faa683ab..4d93c95e8 100644 --- a/frontend/sequences/locals_list/location_form_list.ts +++ b/frontend/sequences/locals_list/location_form_list.ts @@ -38,7 +38,7 @@ type DropdownHeadingId = export const NAME_MAP: Record = { "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) { +export function dropDownName(name: string, v?: Record, + 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) 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; diff --git a/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx index b34d3d098..d398b1b0f 100644 --- a/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx +++ b/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx @@ -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("", () => { const fakeProps = (): StepParams => { @@ -75,6 +76,25 @@ describe("", () => { 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(); + 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(); diff --git a/frontend/sequences/step_tiles/tile_move_absolute.tsx b/frontend/sequences/step_tiles/tile_move_absolute.tsx index 5a87fe7b9..a909cfe47 100644 --- a/frontend/sequences/step_tiles/tile_move_absolute.tsx +++ b/frontend/sequences/step_tiles/tile_move_absolute.tsx @@ -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 }; } - 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 = () => {t("{{axis}}-Offset", { axis })} diff --git a/package.json b/package.json index d6fa95495..512d04ad1 100644 --- a/package.json +++ b/package.json @@ -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"