From 3ebf4349456fa89ae5ee7886154a36bc09b888ce Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 2 Apr 2020 09:53:55 -0700 Subject: [PATCH] use Weed instead of GenericPointer for weeds --- ...235926_add_show_weeds_to_web_app_config.rb | 8 + .../__test_support__/fake_designer_state.ts | 6 +- .../__test_support__/fake_state/resources.ts | 15 + .../resource_index_builder.ts | 22 ++ frontend/api/crud.ts | 1 + frontend/constants.ts | 8 +- frontend/css/farm_designer/farm_designer.scss | 82 ++++- .../farm_designer/farm_designer_panels.scss | 52 ++-- .../__tests__/farm_designer_test.tsx | 27 +- .../farm_designer/__tests__/reducer_test.ts | 68 ++++- .../__tests__/state_to_props_test.tsx | 2 +- frontend/farm_designer/index.tsx | 5 + frontend/farm_designer/interfaces.ts | 24 +- .../map/__tests__/actions_test.ts | 53 +++- .../map/__tests__/garden_map_test.tsx | 84 ++++- .../farm_designer/map/__tests__/util_test.ts | 78 +++-- frontend/farm_designer/map/actions.ts | 35 ++- .../__tests__/selection_box_actions_test.tsx | 11 +- .../map/background/selection_box_actions.tsx | 24 +- .../__tests__/drawn_point_actions_test.tsx | 40 ++- .../__tests__/drawn_point_test.tsx | 10 +- .../drawn_point/__tests__/drawn_weed_test.tsx | 36 +++ .../map/drawn_point/drawn_point.tsx | 4 +- .../map/drawn_point/drawn_point_actions.tsx | 42 ++- .../map/drawn_point/drawn_weed.tsx | 34 +++ frontend/farm_designer/map/garden_map.tsx | 99 ++++-- frontend/farm_designer/map/interfaces.ts | 18 +- frontend/farm_designer/map/layers/index.ts | 1 + .../plants/__tests__/plant_layer_test.tsx | 28 +- .../map/layers/plants/plant_layer.tsx | 7 +- .../points/__tests__/garden_point_test.tsx | 8 +- .../points/__tests__/point_layer_test.tsx | 3 + .../map/layers/points/garden_point.tsx | 9 +- .../map/layers/points/point_layer.tsx | 8 +- .../__tests__/tool_slot_layer_test.tsx | 11 + .../__tests__/tool_slot_point_test.tsx | 5 +- .../map/layers/tool_slots/tool_slot_layer.tsx | 6 +- .../map/layers/tool_slots/tool_slot_point.tsx | 14 +- .../weeds/__tests__/garden_weed_test.tsx | 89 ++++++ .../weeds/__tests__/weed_layer_test.tsx | 66 ++++ .../map/layers/weeds/garden_weed.tsx | 69 +++++ .../map/layers/weeds/weed_layer.tsx | 43 +++ .../zones/__tests__/zones_layer_test.tsx | 9 +- .../farm_designer/map/layers/zones/zones.tsx | 13 +- .../map/layers/zones/zones_layer.tsx | 6 +- .../__tests__/garden_map_legend_test.tsx | 1 + .../map/legend/garden_map_legend.tsx | 4 + frontend/farm_designer/map/util.ts | 40 ++- .../__tests__/plant_inventory_item_test.tsx | 67 ++-- .../plants/__tests__/select_plants_test.tsx | 140 +++++++-- .../plants/plant_inventory_item.tsx | 28 +- .../farm_designer/plants/select_plants.tsx | 233 ++++++++++++-- .../__tests__/group_inventory_item_test.tsx | 29 ++ .../__tests__/point_group_item_test.tsx | 43 ++- .../point_groups/criteria/interfaces.ts | 8 +- .../point_groups/group_inventory_item.tsx | 32 +- .../point_groups/point_group_item.tsx | 59 ++-- .../points/__tests__/create_points_test.tsx | 97 +++--- .../__tests__/point_inventory_item_test.tsx | 32 +- .../farm_designer/points/create_points.tsx | 58 ++-- .../points/point_edit_actions.tsx | 52 ++-- .../farm_designer/points/point_inventory.tsx | 5 +- .../points/point_inventory_item.tsx | 19 +- frontend/farm_designer/reducer.ts | 40 ++- .../__tests__/saved_gardens_test.tsx | 2 +- frontend/farm_designer/state_to_props.ts | 9 +- .../tools/__tests__/index_test.tsx | 41 ++- frontend/farm_designer/tools/index.tsx | 45 ++- .../__tests__/weed_inventory_item_test.tsx | 81 +++++ .../__tests__/weeds_edit_test.tsx | 22 +- .../__tests__/weeds_inventory_test.tsx | 10 +- .../weeds/weed_inventory_item.tsx | 69 +++++ .../{points => weeds}/weeds_edit.tsx | 10 +- .../{points => weeds}/weeds_inventory.tsx | 22 +- .../weed_detector/__tests__/actions_tests.ts | 9 +- .../__tests__/weed_detector_test.tsx | 2 +- frontend/farmware/weed_detector/actions.tsx | 7 +- frontend/farmware/weed_detector/index.tsx | 2 +- .../resources/__tests__/selectors_test.ts | 2 +- frontend/resources/selectors.ts | 26 +- frontend/resources/selectors_by_id.ts | 8 + frontend/resources/tagged_resources.ts | 3 + frontend/route_config.tsx | 4 +- .../__tests__/location_form_list_test.ts | 69 +++-- .../sequences/locals_list/handle_select.ts | 6 +- .../locals_list/location_form_list.ts | 16 +- .../sequences/locals_list/test_helpers.ts | 10 + frontend/sequences/step_tiles/mark_as.tsx | 4 +- .../mark_as/__tests__/action_list_test.ts | 12 +- .../mark_as/__tests__/resource_list_test.ts | 4 + .../mark_as/__tests__/unpack_step_test.ts | 21 ++ .../step_tiles/mark_as/action_list.ts | 7 +- .../step_tiles/mark_as/assertion_support.ts | 2 + .../sequences/step_tiles/mark_as/constants.ts | 20 +- .../step_tiles/mark_as/resource_list.ts | 36 +-- .../step_tiles/mark_as/unpack_step.ts | 38 ++- frontend/session_keys.ts | 1 + package.json | 2 +- public/app-resources/img/generic-weed.svg | 289 ++++++++++++++++++ 99 files changed, 2527 insertions(+), 654 deletions(-) create mode 100644 db/migrate/20200323235926_add_show_weeds_to_web_app_config.rb create mode 100644 frontend/farm_designer/map/drawn_point/__tests__/drawn_weed_test.tsx create mode 100644 frontend/farm_designer/map/drawn_point/drawn_weed.tsx create mode 100644 frontend/farm_designer/map/layers/weeds/__tests__/garden_weed_test.tsx create mode 100644 frontend/farm_designer/map/layers/weeds/__tests__/weed_layer_test.tsx create mode 100644 frontend/farm_designer/map/layers/weeds/garden_weed.tsx create mode 100644 frontend/farm_designer/map/layers/weeds/weed_layer.tsx create mode 100644 frontend/farm_designer/weeds/__tests__/weed_inventory_item_test.tsx rename frontend/farm_designer/{points => weeds}/__tests__/weeds_edit_test.tsx (80%) rename frontend/farm_designer/{points => weeds}/__tests__/weeds_inventory_test.tsx (83%) create mode 100644 frontend/farm_designer/weeds/weed_inventory_item.tsx rename frontend/farm_designer/{points => weeds}/weeds_edit.tsx (87%) rename frontend/farm_designer/{points => weeds}/weeds_inventory.tsx (75%) create mode 100644 public/app-resources/img/generic-weed.svg diff --git a/db/migrate/20200323235926_add_show_weeds_to_web_app_config.rb b/db/migrate/20200323235926_add_show_weeds_to_web_app_config.rb new file mode 100644 index 000000000..23fd018c1 --- /dev/null +++ b/db/migrate/20200323235926_add_show_weeds_to_web_app_config.rb @@ -0,0 +1,8 @@ +class AddShowWeedsToWebAppConfig < ActiveRecord::Migration[6.0] + def change + add_column :web_app_configs, + :show_weeds, + :boolean, + default: false + end +end diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index e122a9dfd..efcf2ba49 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -1,7 +1,8 @@ import { DesignerState } from "../farm_designer/interfaces"; export const fakeDesignerState = (): DesignerState => ({ - selectedPlants: undefined, + selectedPoints: undefined, + selectionPointType: undefined, hoveredPlant: { plantUUID: undefined, icon: "" @@ -13,7 +14,8 @@ export const fakeDesignerState = (): DesignerState => ({ cropSearchResults: [], cropSearchInProgress: false, chosenLocation: { x: undefined, y: undefined, z: undefined }, - currentPoint: undefined, + drawnPoint: undefined, + drawnWeed: undefined, openedSavedGarden: undefined, tryGroupSortType: undefined, editGroupAreaInMap: false, diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index 015b8432d..a98fb1b46 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -26,6 +26,7 @@ import { TaggedAlert, TaggedPointGroup, TaggedFolder, + TaggedWeedPointer, } from "farmbot"; import { fakeResource } from "../fake_resource"; import { @@ -171,6 +172,19 @@ export function fakePoint(): TaggedGenericPointer { }); } +export function fakeWeed(): TaggedWeedPointer { + return fakeResource("Point", { + id: idCounter++, + name: "Weed 1", + pointer_type: "Weed", + x: 200, + y: 400, + z: 0, + radius: 100, + meta: { created_by: "plant-detection", color: "red" } + }); +} + export function fakeSavedGarden(): TaggedSavedGarden { return fakeResource("SavedGarden", { id: idCounter++, @@ -289,6 +303,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig { show_sensor_readings: false, show_plants: true, show_points: true, + show_weeds: true, x_axis_inverted: false, y_axis_inverted: false, z_axis_inverted: true, diff --git a/frontend/__test_support__/resource_index_builder.ts b/frontend/__test_support__/resource_index_builder.ts index ff4ce3bb8..a7a81beb7 100644 --- a/frontend/__test_support__/resource_index_builder.ts +++ b/frontend/__test_support__/resource_index_builder.ts @@ -316,6 +316,27 @@ const tr15: TaggedResource = { "uuid": "Tool.15.50" }; +const tr16: TaggedPoint = { + specialStatus: SpecialStatus.SAVED, + kind: "Point", + body: { + id: 1395, + created_at: "2017-05-24T20:41:19.889Z", + updated_at: "2017-05-24T20:41:19.889Z", + meta: { + color: "gray", + created_by: "plant-detection" + }, + name: "untitled", + pointer_type: "Weed", + radius: 10, + x: 490, + y: 421, + z: 5 + }, + uuid: "Point.1397.11" +}; + const log: TaggedLog = { kind: "Log", specialStatus: SpecialStatus.SAVED, @@ -345,6 +366,7 @@ export const FAKE_RESOURCES: TaggedResource[] = [ tr0, tr14, tr15, + tr16, log, ]; const KIND: keyof TaggedResource = "kind"; // Safety first, kids. diff --git a/frontend/api/crud.ts b/frontend/api/crud.ts index 310c55a5a..ca823a66c 100644 --- a/frontend/api/crud.ts +++ b/frontend/api/crud.ts @@ -334,6 +334,7 @@ const MUST_CONFIRM_LIST: ResourceName[] = [ "Regimen", "Image", "SavedGarden", + "PointGroup", ]; const confirmationChecker = (resourceName: ResourceName, force = false) => diff --git a/frontend/constants.ts b/frontend/constants.ts index 4639d223e..a4f964298 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -773,7 +773,7 @@ export namespace Content { trim(`Click and drag or use the inputs to draw a weed.`); export const BOX_SELECT_DESCRIPTION = - trim(`Drag a box around the plants you would like to select. + trim(`Drag a box around the items you would like to select. Press the back arrow to exit.`); export const SAVED_GARDENS = @@ -1139,7 +1139,8 @@ export enum Actions { // Designer SEARCH_QUERY_CHANGE = "SEARCH_QUERY_CHANGE", - SELECT_PLANT = "SELECT_PLANT", + SELECT_POINT = "SELECT_POINT", + SET_SELECTION_POINT_TYPE = "SET_SELECTION_POINT_TYPE", TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT", TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT", HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM", @@ -1148,7 +1149,8 @@ export enum Actions { OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK", OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO", CHOOSE_LOCATION = "CHOOSE_LOCATION", - SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA", + SET_DRAWN_POINT_DATA = "SET_DRAWN_POINT_DATA", + SET_DRAWN_WEED_DATA = "SET_DRAWN_WEED_DATA", CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN", TRY_SORT_TYPE = "TRY_SORT_TYPE", EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP", diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index 44fe1f4e1..7dec70f81 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -185,23 +185,33 @@ } } + %panel-item-base { + text-align: right; + font-size: 1rem; + padding-right: 1rem; + line-height: 3rem; + float: right; + } .plant-search-item, .group-search-item { cursor: pointer; padding: 0.5rem 1rem; img { - margin: 0 1rem 0 0; - height: 4rem; - width: 4rem; + margin-right: 0.5rem; + height: 3rem; + width: 3rem; + } + &.quick-del { + &:hover { + background: lighten($red, 10%) !important; + &:after { + content: "x"; + margin-left: 1rem; + color: $darkest_red; + font-weight: bold; + } + } } - } - - %panel-item-base { - text-align: right; - font-size: 1rem; - padding-top: 1.4rem; - padding-right: 1rem; - float: right; } .plant-search-item-age { @extend %panel-item-base; @@ -209,6 +219,7 @@ .group-item-count { @extend %panel-item-base; padding-top: 0.6rem; + line-height: 1rem; } .plant-search-item-name { display: inline-block; @@ -219,24 +230,27 @@ text-overflow: ellipsis; margin-left: 1rem; } + .weed-search-item, .point-search-item { cursor: pointer; padding: 0.5rem 1rem; .saucer { display: inline-block; - margin: 0 1rem 0 0; - height: 2rem; - width: 2rem; + height: 3rem; + width: 3rem; vertical-align: middle; + margin-right: 0.25rem; } } + .weed-search-item-info, .point-search-item-info { text-align: right; font-size: 1rem; - padding-top: 0.6rem; padding-right: 1rem; + line-height: 3rem; float: right; } + .weed-search-item-name, .point-search-item-name { display: inline-block; vertical-align: middle; @@ -244,7 +258,34 @@ width: 40%; overflow: hidden; text-overflow: ellipsis; - margin-left: 1rem; + margin-left: 1.25rem; + } + .tool-search-item, + .tool-slot-search-item { + line-height: 4rem; + cursor: pointer; + .row { + margin-left: 0; + margin-right: 0; + } + .tool-slot-search-item-name { + margin-left: -1rem; + } + p { + font-size: 1rem; + line-height: 4rem; + &.tool-status, + &.tool-slot-position { + float: right; + } + } + svg { + vertical-align: middle; + } + .tool-slot-position-info { + padding: 0; + padding-right: 1.75rem; + } } } @@ -284,11 +325,17 @@ } .map-point { + cursor: pointer !important; stroke-width: 2; stroke-opacity: 0.3; fill-opacity: 0.1; } +.map-weed { + cursor: pointer !important; +} + +.weed-image, .plant-image { transform-origin: bottom; transform-box: fill-box; @@ -337,6 +384,9 @@ fill: $white; stroke: $white; } + &:hover { + opacity: 0.15; + } } } diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index a86266bde..8a58a0dec 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -291,6 +291,7 @@ .panel-action-buttons { position: absolute; z-index: 9; + height: 19rem; width: 100%; background: $panel_medium_light_gray; padding: 0.5rem; @@ -307,6 +308,9 @@ float: left; width: 100%; } + .filter-search { + padding-right: 1rem; + } .plant-status-bulk-update { display: inline-flex; width: 100%; @@ -321,15 +325,13 @@ } } .panel-content { - padding-top: 15rem; + padding-top: 19rem; padding-right: 0; padding-left: 0; padding-bottom: 5rem; max-height: calc(100vh - 13rem); overflow-y: auto; overflow-x: hidden; - .plant-search-item, - .group-search-item { pointer-events: none; } } } @@ -377,9 +379,13 @@ .weed-info-panel-content, .point-info-panel-content { - .saucer { - margin: 1rem; - margin-left: 2rem; + .point-color-input { + div[class*=col-] { + padding-left: 0.5rem; + } + .saucer { + margin-top: 4.5rem; + } } .fb-button & .red { display: block; @@ -557,22 +563,8 @@ overflow-x: hidden; .tool-search-item, .tool-slot-search-item { - line-height: 4rem; - cursor: pointer; margin-left: -15px; margin-right: -15px; - .row { - margin-left: 0; - margin-right: 0; - } - p { - font-size: 1.2rem; - line-height: 4rem; - &.tool-status, - &.tool-slot-position { - float: right; - } - } .filter-search { .bp3-button { min-height: 2.5rem; @@ -585,13 +577,6 @@ line-height: 2rem; } } - svg { - vertical-align: middle; - } - .tool-slot-position-info { - padding: 0; - padding-right: 1rem; - } } .mounted-tool-header { display: flex; @@ -821,6 +806,19 @@ } } +.weed-item-icon, +.group-item-icon { + display: inline-block; + position: relative; + .weed-icon { + position: absolute; + top: 13%; + left: 12%; + width: 70%; + height: 70%; + } +} + .weeds-inventory-panel, .zones-inventory-panel, .groups-panel { diff --git a/frontend/farm_designer/__tests__/farm_designer_test.tsx b/frontend/farm_designer/__tests__/farm_designer_test.tsx index 2190c3175..2d4c6f6d8 100644 --- a/frontend/farm_designer/__tests__/farm_designer_test.tsx +++ b/frontend/farm_designer/__tests__/farm_designer_test.tsx @@ -15,7 +15,6 @@ import * as React from "react"; import { RawFarmDesigner as FarmDesigner } from "../index"; import { mount } from "enzyme"; import { Props } from "../interfaces"; -import { GardenMapLegendProps } from "../map/interfaces"; import { bot } from "../../__test_support__/fake_state/bot"; import { fakeImage, fakeWebAppConfig, @@ -28,6 +27,8 @@ import { import { fakeState } from "../../__test_support__/fake_state"; import { edit } from "../../api/crud"; import { BooleanSetting } from "../../session_keys"; +import { GardenMapLegend } from "../map/legend/garden_map_legend"; +import { GardenMap } from "../map/garden_map"; describe("", () => { const fakeProps = (): Props => ({ @@ -36,6 +37,7 @@ describe("", () => { designer: fakeDesignerState(), hoveredPlant: undefined, genericPoints: [], + weeds: [], allPoints: [], plants: [], toolSlots: [], @@ -67,8 +69,7 @@ describe("", () => { it("loads default map settings", () => { const wrapper = mount(); - const legendProps = - wrapper.find("GardenMapLegend").props() as GardenMapLegendProps; + const legendProps = wrapper.find(GardenMapLegend).props(); expect(legendProps.legendMenuOpen).toBeFalsy(); expect(legendProps.showPlants).toBeTruthy(); expect(legendProps.showPoints).toBeTruthy(); @@ -76,8 +77,7 @@ describe("", () => { expect(legendProps.showFarmbot).toBeTruthy(); expect(legendProps.showImages).toBeFalsy(); expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 }); - // tslint:disable-next-line:no-any - const gardenMapProps = wrapper.find("GardenMap").props() as any; + const gardenMapProps = wrapper.find(GardenMap).props(); expect(gardenMapProps.gridSize.x).toEqual(2900); expect(gardenMapProps.gridSize.y).toEqual(1400); }); @@ -90,8 +90,7 @@ describe("", () => { image2.body.created_at = "2001-01-01T00:00:00.000Z"; p.latestImages = [image1, image2]; const wrapper = mount(); - const legendProps = - wrapper.find("GardenMapLegend").props() as GardenMapLegendProps; + const legendProps = wrapper.find(GardenMapLegend).props(); expect(legendProps.imageAgeInfo) .toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 }); }); @@ -137,4 +136,18 @@ describe("", () => { bot_origin_quadrant: 2 }); }); + + it("initializes setting", () => { + const p = fakeProps(); + p.getConfigValue = () => false; + const i = new FarmDesigner(p); + expect(i.initializeSetting(BooleanSetting.show_farmbot, true)).toBeFalsy(); + }); + + it("gets bot origin quadrant", () => { + const p = fakeProps(); + p.getConfigValue = () => 1; + const i = new FarmDesigner(p); + expect(i.getBotOriginQuadrant()).toEqual(1); + }); }); diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index 938f4fd9d..7b733efeb 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -2,7 +2,7 @@ import { designer } from "../reducer"; import { Actions } from "../../constants"; import { ReduxAction } from "../../redux/interfaces"; import { - HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult, + HoveredPlantPayl, DrawnPointPayl, CropLiveSearchResult, DrawnWeedPayl, } from "../interfaces"; import { BotPosition } from "../../devices/interfaces"; import { @@ -10,6 +10,7 @@ import { } from "../../__test_support__/fake_crop_search_result"; import { fakeDesignerState } from "../../__test_support__/fake_designer_state"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; +import { PointType } from "farmbot"; describe("designer reducer", () => { const oldState = fakeDesignerState; @@ -24,13 +25,22 @@ describe("designer reducer", () => { expect(newState.cropSearchInProgress).toEqual(true); }); - it("selects plants", () => { + it("selects points", () => { const action: ReduxAction = { - type: Actions.SELECT_PLANT, - payload: ["plantUuid"] + type: Actions.SELECT_POINT, + payload: ["pointUuid"] }; const newState = designer(oldState(), action); - expect(newState.selectedPlants).toEqual(["plantUuid"]); + expect(newState.selectedPoints).toEqual(["pointUuid"]); + }); + + it("sets selection point type", () => { + const action: ReduxAction = { + type: Actions.SET_SELECTION_POINT_TYPE, + payload: ["Plant"], + }; + const newState = designer(oldState(), action); + expect(newState.selectionPointType).toEqual(["Plant"]); }); it("sets hovered plant", () => { @@ -84,25 +94,49 @@ describe("designer reducer", () => { }); it("sets current point data", () => { - const action: ReduxAction = { - type: Actions.SET_CURRENT_POINT_DATA, + const action: ReduxAction = { + type: Actions.SET_DRAWN_POINT_DATA, payload: { cx: 10, cy: 20, r: 30, color: "red" } }; const newState = designer(oldState(), action); - expect(newState.currentPoint).toEqual({ + expect(newState.drawnPoint).toEqual({ cx: 10, cy: 20, r: 30, color: "red" }); }); it("uses current point color", () => { - const action: ReduxAction = { - type: Actions.SET_CURRENT_POINT_DATA, + const action: ReduxAction = { + type: Actions.SET_DRAWN_POINT_DATA, payload: { cx: 10, cy: 20, r: 30 } }; const state = oldState(); - state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" }; + state.drawnPoint = { cx: 0, cy: 0, r: 0, color: "red" }; const newState = designer(state, action); - expect(newState.currentPoint).toEqual({ + expect(newState.drawnPoint).toEqual({ + cx: 10, cy: 20, r: 30, color: "red" + }); + }); + + it("sets current weed data", () => { + const action: ReduxAction = { + type: Actions.SET_DRAWN_WEED_DATA, + payload: { cx: 10, cy: 20, r: 30, color: "red" } + }; + const newState = designer(oldState(), action); + expect(newState.drawnWeed).toEqual({ + cx: 10, cy: 20, r: 30, color: "red" + }); + }); + + it("uses current weed color", () => { + const action: ReduxAction = { + type: Actions.SET_DRAWN_WEED_DATA, + payload: { cx: 10, cy: 20, r: 30 } + }; + const state = oldState(); + state.drawnWeed = { cx: 0, cy: 0, r: 0, color: "red" }; + const newState = designer(state, action); + expect(newState.drawnWeed).toEqual({ cx: 10, cy: 20, r: 30, color: "red" }); }); @@ -156,4 +190,14 @@ describe("designer reducer", () => { const newState = designer(state, action); expect(newState.tryGroupSortType).toEqual("random"); }); + + it("enables edit group area in map mode", () => { + const state = oldState(); + state.editGroupAreaInMap = false; + const action: ReduxAction = { + type: Actions.EDIT_GROUP_AREA_IN_MAP, payload: true + }; + const newState = designer(state, action); + expect(newState.editGroupAreaInMap).toEqual(true); + }); }); diff --git a/frontend/farm_designer/__tests__/state_to_props_test.tsx b/frontend/farm_designer/__tests__/state_to_props_test.tsx index 3dfc9c517..f155e4cd6 100644 --- a/frontend/farm_designer/__tests__/state_to_props_test.tsx +++ b/frontend/farm_designer/__tests__/state_to_props_test.tsx @@ -51,7 +51,7 @@ describe("mapStateToProps()", () => { const state = fakeState(); state.resources = buildResourceIndex([fakePlant(), fakeDevice()]); const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0]; - state.resources.consumers.farm_designer.selectedPlants = [plantUuid]; + state.resources.consumers.farm_designer.selectedPoints = [plantUuid]; expect(mapStateToProps(state).selectedPlant).toEqual( expect.objectContaining({ uuid: plantUuid })); }); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index c73c6f234..0685d8218 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -70,6 +70,7 @@ export class RawFarmDesigner extends React.Component> { legend_menu_open: init(BooleanSetting.legend_menu_open, false), show_plants: init(BooleanSetting.show_plants, true), show_points: init(BooleanSetting.show_points, true), + show_weeds: init(BooleanSetting.show_weeds, true), show_spread: init(BooleanSetting.show_spread, false), show_farmbot: init(BooleanSetting.show_farmbot, true), show_images: init(BooleanSetting.show_images, false), @@ -116,6 +117,7 @@ export class RawFarmDesigner extends React.Component> { legend_menu_open, show_plants, show_points, + show_weeds, show_spread, show_farmbot, show_images, @@ -155,6 +157,7 @@ export class RawFarmDesigner extends React.Component> { legendMenuOpen={legend_menu_open} showPlants={show_plants} showPoints={show_points} + showWeeds={show_weeds} showSpread={show_spread} showFarmbot={show_farmbot} showImages={show_images} @@ -181,6 +184,7 @@ export class RawFarmDesigner extends React.Component> { > { designer={this.props.designer} plants={this.props.plants} genericPoints={this.props.genericPoints} + weeds={this.props.weeds} allPoints={this.props.allPoints} toolSlots={this.props.toolSlots} botLocationData={this.props.botLocationData} diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index 07c060fd7..beb9f55b1 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -11,8 +11,10 @@ import { TaggedSensor, TaggedPoint, TaggedPointGroup, + TaggedWeedPointer, + PointType, } from "farmbot"; -import { SlotWithTool, ResourceIndex } from "../resources/interfaces"; +import { SlotWithTool, ResourceIndex, UUID } from "../resources/interfaces"; import { BotPosition, StepsPerMmXY, BotLocationData, ShouldDisplay, } from "../devices/interfaces"; @@ -48,6 +50,7 @@ export interface State extends TypeCheckerHint { legend_menu_open: boolean; show_plants: boolean; show_points: boolean; + show_weeds: boolean; show_spread: boolean; show_farmbot: boolean; show_images: boolean; @@ -63,6 +66,7 @@ export interface Props { designer: DesignerState; hoveredPlant: TaggedPlant | undefined; genericPoints: TaggedGenericPointer[]; + weeds: TaggedWeedPointer[]; allPoints: TaggedPoint[]; plants: TaggedPlant[]; toolSlots: SlotWithTool[]; @@ -106,7 +110,8 @@ export interface Crop { } export interface DesignerState { - selectedPlants: string[] | undefined; + selectedPoints: UUID[] | undefined; + selectionPointType: PointType[] | undefined; hoveredPlant: HoveredPlantPayl; hoveredPoint: string | undefined; hoveredPlantListItem: string | undefined; @@ -115,7 +120,8 @@ export interface DesignerState { cropSearchResults: CropLiveSearchResult[]; cropSearchInProgress: boolean; chosenLocation: BotPosition; - currentPoint: CurrentPointPayl | undefined; + drawnPoint: DrawnPointPayl | undefined; + drawnWeed: DrawnWeedPayl | undefined; openedSavedGarden: string | undefined; tryGroupSortType: PointGroupSortType | "nn" | undefined; editGroupAreaInMap: boolean; @@ -181,6 +187,7 @@ export interface FarmEventState { export interface GardenMapProps { showPlants: boolean | undefined; showPoints: boolean | undefined; + showWeeds: boolean | undefined; showSpread: boolean | undefined; showFarmbot: boolean | undefined; showImages: boolean | undefined; @@ -189,6 +196,7 @@ export interface GardenMapProps { dispatch: Function; designer: DesignerState; genericPoints: TaggedGenericPointer[]; + weeds: TaggedWeedPointer[]; allPoints: TaggedPoint[]; plants: TaggedPlant[]; toolSlots: SlotWithTool[]; @@ -279,7 +287,15 @@ export interface CameraCalibrationData { calibrationZ: string | undefined; } -export interface CurrentPointPayl { +export interface DrawnPointPayl { + name?: string; + cx: number; + cy: number; + r: number; + color?: string; +} + +export interface DrawnWeedPayl { name?: string; cx: number; cy: number; diff --git a/frontend/farm_designer/map/__tests__/actions_test.ts b/frontend/farm_designer/map/__tests__/actions_test.ts index d97175eeb..744c533f9 100644 --- a/frontend/farm_designer/map/__tests__/actions_test.ts +++ b/frontend/farm_designer/map/__tests__/actions_test.ts @@ -16,8 +16,9 @@ jest.mock("../../point_groups/group_detail", () => ({ })); import { - movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPlant, + movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPoint, setHoveredPlant, + mapPointClickAction, } from "../actions"; import { MovePlantProps } from "../../interfaces"; import { fakePlant } from "../../../__test_support__/fake_state/resources"; @@ -74,7 +75,7 @@ describe("closePlantInfo()", () => { closePlantInfo(dispatch)(); expect(history.push).toHaveBeenCalledWith("/app/designer/plants"); expect(dispatch).toHaveBeenCalledWith({ - payload: undefined, type: Actions.SELECT_PLANT + payload: undefined, type: Actions.SELECT_POINT }); }); @@ -84,7 +85,7 @@ describe("closePlantInfo()", () => { closePlantInfo(dispatch)(); expect(history.push).toHaveBeenCalledWith("/app/designer/plants"); expect(dispatch).toHaveBeenCalledWith({ - payload: undefined, type: Actions.SELECT_PLANT + payload: undefined, type: Actions.SELECT_POINT }); }); }); @@ -115,7 +116,7 @@ describe("clickMapPlant", () => { const dispatch = jest.fn(); const getState: GetState = jest.fn(() => state); clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState); - expect(dispatch).toHaveBeenCalledWith(selectPlant(["fakeUuid"])); + expect(dispatch).toHaveBeenCalledWith(selectPoint(["fakeUuid"])); expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon")); expect(dispatch).toHaveBeenCalledTimes(2); }); @@ -136,6 +137,18 @@ describe("clickMapPlant", () => { expect(dispatch).toHaveBeenCalledTimes(1); }); + it("doesn't add a point to current group", () => { + mockPath = "/app/designer/groups/1"; + mockGroup.body.point_ids = [1]; + const state = fakeState(); + state.resources = buildResourceIndex([]); + const dispatch = jest.fn(); + const getState: GetState = jest.fn(() => state); + clickMapPlant("missing plant uuid", "fakeIcon")(dispatch, getState); + expect(overwrite).not.toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledTimes(1); + }); + it("removes a point from the current group if group editor is active", () => { mockPath = "/app/designer/groups/1"; mockGroup.body.point_ids = [1, 2]; @@ -162,7 +175,7 @@ describe("clickMapPlant", () => { const getState: GetState = jest.fn(() => state); clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState); expect(dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, payload: [plant.uuid] + type: Actions.SELECT_POINT, payload: [plant.uuid] }); expect(dispatch).toHaveBeenCalledTimes(1); }); @@ -173,13 +186,39 @@ describe("clickMapPlant", () => { const plant = fakePlant(); plant.uuid = "fakePlantUuid"; state.resources = buildResourceIndex([plant]); - state.resources.consumers.farm_designer.selectedPlants = [plant.uuid]; + state.resources.consumers.farm_designer.selectedPoints = [plant.uuid]; const dispatch = jest.fn(); const getState: GetState = jest.fn(() => state); clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState); expect(dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, payload: [] + type: Actions.SELECT_POINT, payload: [] }); expect(dispatch).toHaveBeenCalledTimes(1); }); }); + +describe("mapPointClickAction()", () => { + it("navigates", () => { + mockPath = "/app/designer/plants"; + const dispatch = jest.fn(); + mapPointClickAction(dispatch, "uuid", "fake path")(); + expect(history.push).toHaveBeenCalledWith("fake path"); + expect(dispatch).not.toHaveBeenCalled(); + }); + + it("doesn't navigate: box select", () => { + mockPath = "/app/designer/plants/select"; + const dispatch = jest.fn(); + mapPointClickAction(dispatch, "uuid", "fake path")(); + expect(history.push).not.toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalled(); + }); + + it("doesn't navigate: group edit", () => { + mockPath = "/app/designer/groups/edit/1"; + const dispatch = jest.fn(); + mapPointClickAction(dispatch, "uuid", "fake path")(); + expect(history.push).not.toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalled(); + }); +}); diff --git a/frontend/farm_designer/map/__tests__/garden_map_test.tsx b/frontend/farm_designer/map/__tests__/garden_map_test.tsx index a273c715b..91bfadd25 100644 --- a/frontend/farm_designer/map/__tests__/garden_map_test.tsx +++ b/frontend/farm_designer/map/__tests__/garden_map_test.tsx @@ -6,15 +6,17 @@ jest.mock("../actions", () => ({ import { Mode } from "../interfaces"; let mockMode = Mode.none; let mockAtPlant = true; +let mockInteractionAllow = true; jest.mock("../util", () => ({ getMode: () => mockMode, getMapSize: () => ({ h: 100, w: 100 }), getGardenCoordinates: jest.fn(), transformXY: jest.fn(() => ({ qx: 0, qy: 0 })), transformForQuadrant: jest.fn(), - maybeNoPointer: jest.fn(), round: jest.fn(), cursorAtPlant: () => mockAtPlant, + allowInteraction: () => mockInteractionAllow, + allowGroupAreaInteraction: jest.fn(), })); jest.mock("../layers/plants/plant_actions", () => ({ @@ -81,6 +83,7 @@ const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN }; const fakeProps = (): GardenMapProps => ({ showPoints: true, showPlants: true, + showWeeds: true, showSpread: false, showFarmbot: false, showImages: false, @@ -92,6 +95,7 @@ const fakeProps = (): GardenMapProps => ({ designer: fakeDesignerState(), plants: [], genericPoints: [], + weeds: [], allPoints: [], toolSlots: [], botLocationData: { @@ -286,7 +290,22 @@ describe("", () => { wrapper.find(".drop-area-svg").simulate("mouseDown", { pageX: 1, pageY: 2 }); - expect(startNewPoint).toHaveBeenCalled(); + expect(startNewPoint).toHaveBeenCalledWith(expect.objectContaining({ + type: "point" + })); + expect(getGardenCoordinates).toHaveBeenCalledWith( + expect.objectContaining({ pageX: 1, pageY: 2 })); + }); + + it("starts drawing weed", () => { + const wrapper = shallow(); + mockMode = Mode.createWeed; + wrapper.find(".drop-area-svg").simulate("mouseDown", { + pageX: 1, pageY: 2 + }); + expect(startNewPoint).toHaveBeenCalledWith(expect.objectContaining({ + type: "weed" + })); expect(getGardenCoordinates).toHaveBeenCalledWith( expect.objectContaining({ pageX: 1, pageY: 2 })); }); @@ -297,7 +316,20 @@ describe("", () => { wrapper.find(".drop-area-svg").simulate("mouseMove", { pageX: 10, pageY: 20 }); - expect(resizePoint).toHaveBeenCalled(); + expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({ + type: "point" + })); + }); + + it("sets drawn weed radius", () => { + const wrapper = shallow(); + mockMode = Mode.createWeed; + wrapper.find(".drop-area-svg").simulate("mouseMove", { + pageX: 10, pageY: 20 + }); + expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({ + type: "weed" + })); }); it("lays eggs", () => { @@ -350,7 +382,7 @@ describe("", () => { it("closes panel", () => { mockMode = Mode.boxSelect; const p = fakeProps(); - p.designer.selectedPlants = undefined; + p.designer.selectedPoints = undefined; const wrapper = mount(); wrapper.instance().closePanel()(); expect(closePlantInfo).toHaveBeenCalled(); @@ -366,7 +398,7 @@ describe("", () => { it("doesn't close panel: box select", () => { mockMode = Mode.boxSelect; const p = fakeProps(); - p.designer.selectedPlants = [fakePlant().uuid]; + p.designer.selectedPoints = [fakePlant().uuid]; const wrapper = mount(); wrapper.instance().closePanel()(); expect(closePlantInfo).not.toHaveBeenCalled(); @@ -375,7 +407,7 @@ describe("", () => { it("doesn't close panel: move mode", () => { mockMode = Mode.moveTo; const p = fakeProps(); - p.designer.selectedPlants = [fakePlant().uuid]; + p.designer.selectedPoints = [fakePlant().uuid]; const wrapper = mount(); wrapper.instance().closePanel()(); expect(closePlantInfo).not.toHaveBeenCalled(); @@ -404,6 +436,46 @@ describe("", () => { expect(wrapper.instance().state.isDragging).toBe(true); }); + it("allows interactions: default", () => { + mockMode = Mode.none; + mockInteractionAllow = true; + const p = fakeProps(); + p.designer.selectionPointType = undefined; + const wrapper = mount(); + const allowed = wrapper.instance().interactions("Plant"); + expect(allowed).toBeTruthy(); + }); + + it("allows interactions: box select", () => { + mockMode = Mode.boxSelect; + mockInteractionAllow = true; + const p = fakeProps(); + p.designer.selectionPointType = undefined; + const wrapper = mount(); + const allowed = wrapper.instance().interactions("Plant"); + expect(allowed).toBeTruthy(); + }); + + it("disallows interactions: default", () => { + mockMode = Mode.none; + mockInteractionAllow = false; + const p = fakeProps(); + p.designer.selectionPointType = undefined; + const wrapper = mount(); + const allowed = wrapper.instance().interactions("Plant"); + expect(allowed).toBeFalsy(); + }); + + it("disallows interactions: box select", () => { + mockMode = Mode.boxSelect; + mockInteractionAllow = true; + const p = fakeProps(); + p.designer.selectionPointType = ["Plant"]; + const wrapper = mount(); + const allowed = wrapper.instance().interactions("Weed"); + expect(allowed).toBeFalsy(); + }); + it("unswapped height and width", () => { const p = fakeProps(); p.getConfigValue = () => false; diff --git a/frontend/farm_designer/map/__tests__/util_test.ts b/frontend/farm_designer/map/__tests__/util_test.ts index 8bc4ab723..39b61cc05 100644 --- a/frontend/farm_designer/map/__tests__/util_test.ts +++ b/frontend/farm_designer/map/__tests__/util_test.ts @@ -21,6 +21,8 @@ import { mapPanelClassName, getMode, cursorAtPlant, + allowInteraction, + allowGroupAreaInteraction, } from "../util"; import { McuParams } from "farmbot"; import { @@ -32,13 +34,37 @@ import { } from "../../../__test_support__/map_transform_props"; import { fakePlant } from "../../../__test_support__/fake_state/resources"; -describe("Utils", () => { +describe("round()", () => { it("rounds a number", () => { expect(round(44)).toEqual(40); expect(round(98)).toEqual(100); }); }); +describe("mapPanelClassName()", () => { + it("returns correct panel status: short panel", () => { + Object.defineProperty(window, "innerWidth", { + value: 400, + configurable: true + }); + mockPath = "/app/designer/move_to"; + expect(mapPanelClassName()).toEqual("short-panel"); + mockPath = "/app/designer/plants/crop_search/mint/add"; + expect(mapPanelClassName()).toEqual("short-panel"); + }); + + it("returns correct panel status: panel open", () => { + Object.defineProperty(window, "innerWidth", { + value: 500, + configurable: true + }); + mockPath = "/app/designer/move_to"; + expect(mapPanelClassName()).toEqual("panel-open"); + mockPath = "/app/designer/plants/crop_search/mint/add"; + expect(mapPanelClassName()).toEqual("panel-open"); + }); +}); + describe("translateScreenToGarden()", () => { it("translates screen coords to garden coords: zoomLvl = 1", () => { const result = translateScreenToGarden({ @@ -344,6 +370,10 @@ describe("getMode()", () => { expect(getMode()).toEqual(Mode.points); mockPath = "/app/designer/points/add"; expect(getMode()).toEqual(Mode.createPoint); + mockPath = "/app/designer/weeds"; + expect(getMode()).toEqual(Mode.weeds); + mockPath = "/app/designer/weeds/add"; + expect(getMode()).toEqual(Mode.createWeed); mockPath = "/app/designer/gardens"; mockGardenOpen = true; expect(getMode()).toEqual(Mode.templateView); @@ -396,27 +426,37 @@ describe("getGardenCoordinates()", () => { }); }); -describe("mapPanelClassName()", () => { - it("returns correct panel status: short panel", () => { - Object.defineProperty(window, "innerWidth", { - value: 400, - configurable: true - }); - mockPath = "/app/designer/move_to"; - expect(mapPanelClassName()).toEqual("short-panel"); - mockPath = "/app/designer/plants/crop_search/mint/add"; - expect(mapPanelClassName()).toEqual("short-panel"); +describe("allowInteraction()", () => { + it("allows interaction", () => { + mockPath = "/app/designer/plants"; + expect(allowInteraction()).toBeTruthy(); }); - it("returns correct panel status: panel open", () => { - Object.defineProperty(window, "innerWidth", { - value: 500, - configurable: true - }); - mockPath = "/app/designer/move_to"; - expect(mapPanelClassName()).toEqual("panel-open"); + it("disallows interaction", () => { mockPath = "/app/designer/plants/crop_search/mint/add"; - expect(mapPanelClassName()).toEqual("panel-open"); + expect(allowInteraction()).toBeFalsy(); + mockPath = "/app/designer/move_to"; + expect(allowInteraction()).toBeFalsy(); + mockPath = "/app/designer/points/add"; + expect(allowInteraction()).toBeFalsy(); + mockPath = "/app/designer/weeds/add"; + expect(allowInteraction()).toBeFalsy(); + }); +}); + +describe("allowGroupAreaInteraction()", () => { + it("allows interaction", () => { + mockPath = "/app/designer/plants"; + expect(allowGroupAreaInteraction()).toBeTruthy(); + }); + + it("disallows interaction", () => { + mockPath = "/app/designer/plants/select"; + expect(allowGroupAreaInteraction()).toBeFalsy(); + mockPath = "/app/designer/move_to"; + expect(allowGroupAreaInteraction()).toBeFalsy(); + mockPath = "/app/designer/groups/1"; + expect(allowGroupAreaInteraction()).toBeFalsy(); }); }); diff --git a/frontend/farm_designer/map/actions.ts b/frontend/farm_designer/map/actions.ts index 9f1b44124..3be43c4f3 100644 --- a/frontend/farm_designer/map/actions.ts +++ b/frontend/farm_designer/map/actions.ts @@ -23,8 +23,8 @@ export function movePlant(payload: MovePlantProps) { return edit(tr, update); } -export const selectPlant = (payload: string[] | undefined) => { - return { type: Actions.SELECT_PLANT, payload }; +export const selectPoint = (payload: string[] | undefined) => { + return { type: Actions.SELECT_POINT, payload }; }; export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({ @@ -52,16 +52,16 @@ const addOrRemoveFromGroup = }; const addOrRemoveFromSelection = - (clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => { + (clickedPointUuid: UUID, selectedPoints: UUID[] | undefined) => { const nextSelected = - (selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid); - if (!(selectedPlants?.includes(clickedPlantUuid))) { - nextSelected.push(clickedPlantUuid); + (selectedPoints || []).filter(uuid => uuid !== clickedPointUuid); + if (!(selectedPoints?.includes(clickedPointUuid))) { + nextSelected.push(clickedPointUuid); } - return selectPlant(nextSelected); + return selectPoint(nextSelected); }; -export const clickMapPlant = (clickedPlantUuid: string, icon: string) => { +export const clickMapPlant = (clickedPlantUuid: UUID, icon: string) => { return (dispatch: Function, getState: GetState) => { switch (getMode()) { case Mode.editGroup: @@ -69,11 +69,11 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => { dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index)); break; case Mode.boxSelect: - const { selectedPlants } = getState().resources.consumers.farm_designer; - dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPlants)); + const { selectedPoints } = getState().resources.consumers.farm_designer; + dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPoints)); break; default: - dispatch(selectPlant([clickedPlantUuid])); + dispatch(selectPoint([clickedPlantUuid])); dispatch(setHoveredPlant(clickedPlantUuid, icon)); break; } @@ -81,7 +81,7 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => { }; export const unselectPlant = (dispatch: Function) => () => { - dispatch(selectPlant(undefined)); + dispatch(selectPoint(undefined)); dispatch(setHoveredPlant(undefined)); dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined }); }; @@ -104,3 +104,14 @@ export const setDragIcon = e.dataTransfer.setDragImage && e.dataTransfer.setDragImage(dragImg, width / 2, height / 2); }; + +export const mapPointClickAction = + (dispatch: Function, uuid: UUID, path?: string) => () => { + switch (getMode()) { + case Mode.editGroup: + case Mode.boxSelect: + return dispatch(clickMapPlant(uuid, "")); + default: + return path && history.push(path); + } + }; diff --git a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx index cae610a41..29382f009 100644 --- a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx +++ b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx @@ -55,6 +55,9 @@ describe("resizeBox", () => { const fakeProps = (): ResizeSelectionBoxProps => ({ selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined }, plants: [], + allPoints: [], + selectionPointType: undefined, + getConfigValue: () => true, gardenCoords: { x: 100, y: 200 }, setMapState: jest.fn(), dispatch: jest.fn(), @@ -68,7 +71,7 @@ describe("resizeBox", () => { selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 } }); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, + type: Actions.SELECT_POINT, payload: undefined }); }); @@ -113,7 +116,7 @@ describe("resizeBox", () => { selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 } }); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, + type: Actions.SELECT_POINT, payload: [plant.uuid] }); expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select"); @@ -135,7 +138,7 @@ describe("startNewSelectionBox", () => { selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined } }); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, + type: Actions.SELECT_POINT, payload: undefined }); }); @@ -157,7 +160,7 @@ describe("startNewSelectionBox", () => { startNewSelectionBox(p); expect(p.setMapState).not.toHaveBeenCalled(); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, + type: Actions.SELECT_POINT, payload: undefined }); }); diff --git a/frontend/farm_designer/map/background/selection_box_actions.tsx b/frontend/farm_designer/map/background/selection_box_actions.tsx index 9fda2f484..a9f819a10 100644 --- a/frontend/farm_designer/map/background/selection_box_actions.tsx +++ b/frontend/farm_designer/map/background/selection_box_actions.tsx @@ -3,18 +3,20 @@ import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces"; import { SelectionBoxData } from "./selection_box"; import { GardenMapState } from "../../interfaces"; import { history } from "../../../history"; -import { selectPlant } from "../actions"; +import { selectPoint } from "../actions"; import { getMode } from "../util"; import { editGtLtCriteria } from "../../point_groups/criteria"; -import { TaggedPointGroup } from "farmbot"; +import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot"; import { ShouldDisplay, Feature } from "../../../devices/interfaces"; import { overwrite } from "../../../api/crud"; import { unpackUUID } from "../../../util"; import { UUID } from "../../../resources/interfaces"; +import { getFilteredPoints } from "../../plants/select_plants"; +import { GetWebAppConfigValue } from "../../../config_storage/actions"; /** Return all plants within the selection box. */ export const getSelected = ( - plants: TaggedPlant[], + plants: (TaggedPlant | TaggedPoint)[], box: SelectionBoxData | undefined, ): string[] | undefined => { const arraySelected = plants.filter(p => { @@ -35,6 +37,9 @@ export const getSelected = ( export interface ResizeSelectionBoxProps { selectionBox: SelectionBoxData | undefined; plants: TaggedPlant[]; + allPoints: TaggedPoint[]; + selectionPointType: PointType[] | undefined; + getConfigValue: GetWebAppConfigValue; gardenCoords: AxisNumberProperty | undefined; setMapState: (x: Partial) => void; dispatch: Function; @@ -54,11 +59,16 @@ export const resizeBox = (props: ResizeSelectionBoxProps) => { props.setMapState({ selectionBox: newSelectionBox }); if (props.plantActions) { // Select all plants within the updated selection box - const payload = getSelected(props.plants, newSelectionBox); + const { plants, allPoints, selectionPointType, getConfigValue } = props; + const points = + getFilteredPoints({ + plants, allPoints, selectionPointType, getConfigValue + }); + const payload = getSelected(points, newSelectionBox); if (payload && getMode() === Mode.none) { history.push("/app/designer/plants/select"); } - props.dispatch(selectPlant(payload)); + props.dispatch(selectPoint(payload)); } } } @@ -84,7 +94,7 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => { } if (props.plantActions) { // Clear the previous plant selection when starting a new selection box - props.dispatch(selectPlant(undefined)); + props.dispatch(selectPoint(undefined)); } }; @@ -112,7 +122,7 @@ export const maybeUpdateGroup = nextGroupBody.point_ids = uniq(nextGroupBody.point_ids); if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) { props.dispatch(overwrite(props.group, nextGroupBody)); - props.dispatch(selectPlant(undefined)); + props.dispatch(selectPoint(undefined)); } } } diff --git a/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_actions_test.tsx b/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_actions_test.tsx index fef9bc3ba..ab1cfcfb2 100644 --- a/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_actions_test.tsx +++ b/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_actions_test.tsx @@ -1,11 +1,14 @@ -import { startNewPoint, resizePoint } from "../drawn_point_actions"; +import { + startNewPoint, resizePoint, StartNewPointProps, ResizePointProps, +} from "../drawn_point_actions"; import { Actions } from "../../../../constants"; describe("startNewPoint", () => { - const fakeProps = () => ({ + const fakeProps = (): StartNewPointProps => ({ gardenCoords: { x: 100, y: 200 }, dispatch: jest.fn(), setMapState: jest.fn(), + type: "point", }); it("starts point", () => { @@ -13,15 +16,25 @@ describe("startNewPoint", () => { startNewPoint(p); expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true }); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SET_CURRENT_POINT_DATA, + type: Actions.SET_DRAWN_POINT_DATA, + payload: { cx: 100, cy: 200, r: 0 } + }); + }); + + it("starts weed", () => { + const p = fakeProps(); + p.type = "weed"; + startNewPoint(p); + expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true }); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_DRAWN_WEED_DATA, payload: { cx: 100, cy: 200, r: 0 } }); }); it("doesn't start point", () => { const p = fakeProps(); - // tslint:disable-next-line:no-any - p.gardenCoords = undefined as any; + p.gardenCoords = undefined; startNewPoint(p); expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true }); expect(p.dispatch).not.toHaveBeenCalled(); @@ -29,18 +42,29 @@ describe("startNewPoint", () => { }); describe("resizePoint", () => { - const fakeProps = () => ({ + const fakeProps = (): ResizePointProps => ({ gardenCoords: { x: 100, y: 200 }, - currentPoint: { cx: 100, cy: 200, r: 0 }, + drawnPoint: { cx: 100, cy: 200, r: 0 }, dispatch: jest.fn(), isDragging: true, + type: "point", }); it("resizes point", () => { const p = fakeProps(); resizePoint(p); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SET_CURRENT_POINT_DATA, + type: Actions.SET_DRAWN_POINT_DATA, + payload: { cx: 100, cy: 200, r: 0 } + }); + }); + + it("resizes weed", () => { + const p = fakeProps(); + p.type = "weed"; + resizePoint(p); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_DRAWN_WEED_DATA, payload: { cx: 100, cy: 200, r: 0 } }); }); diff --git a/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_test.tsx b/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_test.tsx index a8305b477..40cdc33fd 100644 --- a/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_test.tsx +++ b/frontend/farm_designer/map/drawn_point/__tests__/drawn_point_test.tsx @@ -12,13 +12,12 @@ describe("", () => { cx: 10, cy: 20, r: 30, - color: "red" } }); it("renders point", () => { const wrapper = svgMount(); - expect(wrapper.find("g").props().stroke).toEqual("red"); + expect(wrapper.find("g").props().stroke).toEqual("green"); expect(wrapper.find("circle").first().props()).toEqual({ id: "point-radius", strokeDasharray: "4 5", cx: 10, cy: 20, r: 30, @@ -28,4 +27,11 @@ describe("", () => { cx: 10, cy: 20, r: 2, }); }); + + it("renders point with chosen color", () => { + const p = fakeProps(); + p.data = { cx: 0, cy: 0, r: 1, color: "red" }; + const wrapper = svgMount(); + expect(wrapper.find("g").props().stroke).toEqual("red"); + }); }); diff --git a/frontend/farm_designer/map/drawn_point/__tests__/drawn_weed_test.tsx b/frontend/farm_designer/map/drawn_point/__tests__/drawn_weed_test.tsx new file mode 100644 index 000000000..163035cf5 --- /dev/null +++ b/frontend/farm_designer/map/drawn_point/__tests__/drawn_weed_test.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { DrawnWeed, DrawnWeedProps } from "../drawn_weed"; +import { + fakeMapTransformProps, +} from "../../../../__test_support__/map_transform_props"; +import { svgMount } from "../../../../__test_support__/svg_mount"; + +describe("", () => { + const fakeProps = (): DrawnWeedProps => ({ + mapTransformProps: fakeMapTransformProps(), + data: { + cx: 10, + cy: 20, + r: 30, + } + }); + + it("renders weed", () => { + const wrapper = svgMount(); + const stop = wrapper.find("stop").first().props(); + expect(stop.stopColor).toEqual("red"); + expect(stop.stopOpacity).toEqual(0.25); + expect(wrapper.find("circle").first().props()).toEqual({ + id: "weed-radius", cx: 10, cy: 20, r: 30, fill: "url(#DrawnWeedGradient)", + }); + }); + + it("renders point with chosen color", () => { + const p = fakeProps(); + p.data = { cx: 0, cy: 0, r: 1, color: "orange" }; + const wrapper = svgMount(); + const stop = wrapper.find("stop").first().props(); + expect(stop.stopColor).toEqual("orange"); + expect(stop.stopOpacity).toEqual(0.5); + }); +}); diff --git a/frontend/farm_designer/map/drawn_point/drawn_point.tsx b/frontend/farm_designer/map/drawn_point/drawn_point.tsx index b5166d30e..88f55f525 100644 --- a/frontend/farm_designer/map/drawn_point/drawn_point.tsx +++ b/frontend/farm_designer/map/drawn_point/drawn_point.tsx @@ -1,11 +1,11 @@ import * as React from "react"; import { MapTransformProps } from "../interfaces"; import { transformXY } from "../util"; -import { CurrentPointPayl } from "../../interfaces"; +import { DrawnPointPayl } from "../../interfaces"; export interface DrawnPointProps { mapTransformProps: MapTransformProps; - data: CurrentPointPayl | undefined; + data: DrawnPointPayl | undefined; } export function DrawnPoint(props: DrawnPointProps) { diff --git a/frontend/farm_designer/map/drawn_point/drawn_point_actions.tsx b/frontend/farm_designer/map/drawn_point/drawn_point_actions.tsx index 32ffa24d4..16d4f6960 100644 --- a/frontend/farm_designer/map/drawn_point/drawn_point_actions.tsx +++ b/frontend/farm_designer/map/drawn_point/drawn_point_actions.tsx @@ -1,37 +1,47 @@ import { Actions } from "../../../constants"; import { AxisNumberProperty } from "../interfaces"; -import { CurrentPointPayl } from "../../interfaces"; +import { DrawnPointPayl } from "../../interfaces"; + +export interface StartNewPointProps { + gardenCoords: AxisNumberProperty | undefined; + dispatch: Function; + setMapState: Function; + type: "point" | "weed"; +} /** Create a new point. */ -export const startNewPoint = (props: { - gardenCoords: AxisNumberProperty | undefined, - dispatch: Function, - setMapState: Function, -}) => { +export const startNewPoint = (props: StartNewPointProps) => { props.setMapState({ isDragging: true }); const center = props.gardenCoords; if (center) { // Set the center of a new point props.dispatch({ - type: Actions.SET_CURRENT_POINT_DATA, + type: props.type == "weed" + ? Actions.SET_DRAWN_WEED_DATA + : Actions.SET_DRAWN_POINT_DATA, payload: { cx: center.x, cy: center.y, r: 0 } }); } }; +export interface ResizePointProps { + gardenCoords: AxisNumberProperty | undefined; + drawnPoint: DrawnPointPayl | undefined; + dispatch: Function; + isDragging: boolean | undefined; + type: "point" | "weed"; +} + /** Resize a point. */ -export const resizePoint = (props: { - gardenCoords: AxisNumberProperty | undefined, - currentPoint: CurrentPointPayl | undefined, - dispatch: Function, - isDragging: boolean | undefined, -}) => { +export const resizePoint = (props: ResizePointProps) => { const edge = props.gardenCoords; - if (edge && props.currentPoint && !!props.isDragging) { - const { cx, cy } = props.currentPoint; + if (edge && props.drawnPoint && !!props.isDragging) { + const { cx, cy } = props.drawnPoint; // Adjust the radius of the point being created props.dispatch({ - type: Actions.SET_CURRENT_POINT_DATA, + type: props.type == "weed" + ? Actions.SET_DRAWN_WEED_DATA + : Actions.SET_DRAWN_POINT_DATA, payload: { cx, cy, // Center was set by click, radius is adjusted by drag r: Math.round(Math.sqrt( diff --git a/frontend/farm_designer/map/drawn_point/drawn_weed.tsx b/frontend/farm_designer/map/drawn_point/drawn_weed.tsx new file mode 100644 index 000000000..cd09c83f8 --- /dev/null +++ b/frontend/farm_designer/map/drawn_point/drawn_weed.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { MapTransformProps } from "../interfaces"; +import { transformXY } from "../util"; +import { DrawnWeedPayl } from "../../interfaces"; + +export interface DrawnWeedProps { + mapTransformProps: MapTransformProps; + data: DrawnWeedPayl | undefined; +} + +export function DrawnWeed(props: DrawnWeedProps) { + const ID = "current-weed"; + const { data, mapTransformProps } = props; + if (!data) { return ; } + const { cx, cy, r } = data; + const color = data.color || "red"; + const { qx, qy } = transformXY(cx, cy, mapTransformProps); + const stopOpacity = ["gray", "pink", "orange"].includes(color) ? 0.5 : 0.25; + return + + + + + + + + + ; +} diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index cce11c0cf..ed89c0ce7 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -6,7 +6,7 @@ import { } from "./interfaces"; import { GardenMapProps, GardenMapState } from "../interfaces"; import { - getMapSize, getGardenCoordinates, getMode, cursorAtPlant, + getMapSize, getGardenCoordinates, getMode, cursorAtPlant, allowInteraction, } from "./util"; import { Grid, MapBackground, @@ -17,6 +17,7 @@ import { PlantLayer, SpreadLayer, PointLayer, + WeedLayer, ToolSlotLayer, FarmBotLayer, ImageLayer, @@ -34,9 +35,11 @@ import { NNPath } from "../point_groups/paths"; import { history } from "../../history"; import { ZonesLayer } from "./layers/zones/zones_layer"; import { ErrorBoundary } from "../../error_boundary"; -import { TaggedPoint, TaggedPointGroup } from "farmbot"; +import { TaggedPoint, TaggedPointGroup, PointType } from "farmbot"; import { findGroupFromUrl } from "../point_groups/group_detail"; import { pointsSelectedByGroup } from "../point_groups/criteria"; +import { DrawnWeed } from "./drawn_point/drawn_weed"; +import { UUID } from "../../resources/interfaces"; export class GardenMap extends React.Component> { @@ -81,6 +84,10 @@ export class GardenMap extends pointsSelectedByGroup(this.group, this.props.allPoints) : []; } + get groupSelected(): UUID[] { + return this.pointsSelectedByGroup.map(point => point.uuid); + } + /** Save the current plant (if needed) and reset drag state. */ endDrag = () => { maybeSavePlantLocation({ @@ -94,7 +101,7 @@ export class GardenMap extends dispatch: this.props.dispatch, shouldDisplay: this.props.shouldDisplay, editGroupAreaInMap: this.props.designer.editGroupAreaInMap, - boxSelected: this.props.designer.selectedPlants, + boxSelected: this.props.designer.selectedPoints, }); this.setState({ isDragging: false, qPageX: 0, qPageY: 0, @@ -152,6 +159,15 @@ export class GardenMap extends gardenCoords: this.getGardenCoordinates(e), dispatch: this.props.dispatch, setMapState: this.setMapState, + type: "point", + }); + break; + case Mode.createWeed: + startNewPoint({ + gardenCoords: this.getGardenCoordinates(e), + dispatch: this.props.dispatch, + setMapState: this.setMapState, + type: "weed", }); break; case Mode.clickToAdd: @@ -163,8 +179,8 @@ export class GardenMap extends startDragOnBackground = (e: React.MouseEvent): void => { switch (getMode()) { case Mode.moveTo: - break; case Mode.createPoint: + case Mode.createWeed: case Mode.clickToAdd: case Mode.editPlant: break; @@ -196,17 +212,26 @@ export class GardenMap extends } } + interactions = (pointerType: PointType): boolean => { + if (allowInteraction()) { + switch (getMode()) { + case Mode.boxSelect: + return (this.props.designer.selectionPointType || ["Plant"]) + .includes(pointerType); + } + } + return allowInteraction(); + }; + /** Return the selected plant, mode-allowing. */ getPlant = (): TaggedPlant | undefined => { - switch (getMode()) { - case Mode.boxSelect: - case Mode.moveTo: - case Mode.points: - case Mode.createPoint: - return undefined; // For modes without plant interaction - default: - return this.props.selectedPlant; - } + return allowInteraction() + ? this.props.selectedPlant + : undefined; + } + + get currentPoint(): UUID | undefined { + return this.props.designer.selectedPoints?.[0]; } handleDragOver = (e: React.DragEvent) => { @@ -273,15 +298,28 @@ export class GardenMap extends case Mode.createPoint: resizePoint({ gardenCoords: this.getGardenCoordinates(e), - currentPoint: this.props.designer.currentPoint, + drawnPoint: this.props.designer.drawnPoint, dispatch: this.props.dispatch, isDragging: this.state.isDragging, + type: "point", + }); + break; + case Mode.createWeed: + resizePoint({ + gardenCoords: this.getGardenCoordinates(e), + drawnPoint: this.props.designer.drawnWeed, + dispatch: this.props.dispatch, + isDragging: this.state.isDragging, + type: "weed", }); break; case Mode.editGroup: resizeBox({ selectionBox: this.state.selectionBox, plants: this.props.plants, + allPoints: this.props.allPoints, + selectionPointType: this.props.designer.selectionPointType, + getConfigValue: this.props.getConfigValue, gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, @@ -293,6 +331,9 @@ export class GardenMap extends resizeBox({ selectionBox: this.state.selectionBox, plants: this.props.plants, + allPoints: this.props.allPoints, + selectionPointType: this.props.designer.selectionPointType, + getConfigValue: this.props.getConfigValue, gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, @@ -308,7 +349,7 @@ export class GardenMap extends case Mode.moveTo: return () => { }; case Mode.boxSelect: - return this.props.designer.selectedPlants + return this.props.designer.selectedPoints ? () => { } : closePlantInfo(this.props.dispatch); default: @@ -362,6 +403,7 @@ export class GardenMap extends botSize={this.props.botSize} mapTransformProps={this.mapTransformProps} groups={this.props.groups} + startDrag={this.startDragOnBackground} currentGroup={this.group?.uuid} /> SensorReadingsLayer = () => + WeedLayer = () => PlantLayer = () => point.uuid)} + boxSelected={this.props.designer.selectedPoints} + groupSelected={this.groupSelected} zoomLvl={this.props.zoomLvl} activeDragXY={this.state.activeDragXY} + interactions={this.interactions("Plant")} animate={this.animate} /> ToolSlotLayer = () => FarmBotLayer = () => DrawnPoint = () => + DrawnWeed = () => GroupOrder = () => + @@ -476,6 +536,7 @@ export class GardenMap extends + diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts index 2cde4b601..46655ce5f 100644 --- a/frontend/farm_designer/map/interfaces.ts +++ b/frontend/farm_designer/map/interfaces.ts @@ -2,6 +2,7 @@ import { TaggedPlantPointer, TaggedGenericPointer, TaggedPlantTemplate, + TaggedWeedPointer, } from "farmbot"; import { State, BotOriginQuadrant } from "../interfaces"; import { BotPosition, BotLocationData } from "../../devices/interfaces"; @@ -22,9 +23,10 @@ export interface PlantLayerProps { mapTransformProps: MapTransformProps; zoomLvl: number; activeDragXY: BotPosition | undefined; - boxSelected: string[] | undefined; + boxSelected: UUID[] | undefined; groupSelected: UUID[]; animate: boolean; + interactions: boolean; } export interface GardenMapLegendProps { @@ -33,6 +35,7 @@ export interface GardenMapLegendProps { legendMenuOpen: boolean; showPlants: boolean; showPoints: boolean; + showWeeds: boolean; showSpread: boolean; showFarmbot: boolean; showImages: boolean; @@ -80,6 +83,17 @@ export interface GardenPointProps { dispatch: Function; } +export interface GardenWeedProps { + mapTransformProps: MapTransformProps; + weed: TaggedWeedPointer; + hovered: boolean; + current: boolean; + selected: boolean; + animate: boolean; + spreadVisible: boolean; + dispatch: Function; +} + interface DragHelpersBaseProps { dragging: boolean; mapTransformProps: MapTransformProps; @@ -152,7 +166,9 @@ export enum Mode { addPlant = "addPlant", moveTo = "moveTo", points = "points", + weeds = "weeds", createPoint = "createPoint", + createWeed = "createWeed", templateView = "templateView", editGroup = "editGroup", } diff --git a/frontend/farm_designer/map/layers/index.ts b/frontend/farm_designer/map/layers/index.ts index b456d34d5..66460e6f0 100644 --- a/frontend/farm_designer/map/layers/index.ts +++ b/frontend/farm_designer/map/layers/index.ts @@ -1,6 +1,7 @@ export * from "./farmbot/farmbot_layer"; export * from "./plants/plant_layer"; export * from "./points/point_layer"; +export * from "./weeds/weed_layer"; export * from "./spread/spread_layer"; export * from "./tool_slots/tool_slot_layer"; export * from "./images/image_layer"; diff --git a/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx b/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx index dda04f50f..fce6bf0cc 100644 --- a/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx +++ b/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx @@ -1,6 +1,6 @@ let mockPath = "/app/designer/plants"; jest.mock("../../../../../history", () => ({ - getPathArray: jest.fn(() => { return mockPath.split("/"); }) + getPathArray: jest.fn(() => mockPath.split("/")) })); import * as React from "react"; @@ -31,6 +31,7 @@ describe("", () => { activeDragXY: { x: undefined, y: undefined, z: undefined }, animate: true, hoveredPlant: undefined, + interactions: true, }); it("shows plants", () => { @@ -59,14 +60,19 @@ describe("", () => { it("is in clickable mode", () => { mockPath = "/app/designer/plants"; const p = fakeProps(); + p.interactions = true; + p.plants[0].body.id = 1; const wrapper = svgMount(); - expect(wrapper.find("Link").props().style).toEqual({}); + expect(wrapper.find("Link").props().style).toEqual({ + cursor: "pointer" + }); }); it("is in non-clickable mode", () => { mockPath = "/app/designer/plants/crop_search/mint/add"; const p = fakeProps(); - + p.interactions = false; + p.plants[0].body.id = 1; const wrapper = svgMount(); expect(wrapper.find("Link").props().style) .toEqual({ pointerEvents: "none" }); @@ -111,22 +117,12 @@ describe("", () => { expect(wrapper.find("GardenPlant").props().selected).toEqual(true); }); - it("allows clicking of unsaved plants", () => { - const p = fakeProps(); - const plant = fakePlant(); - plant.body.id = 1; - p.plants = [plant]; - const wrapper = svgMount(); - expect((wrapper.find("Link").props()).style).toEqual({}); - }); - it("doesn't allow clicking of unsaved plants", () => { const p = fakeProps(); - const plant = fakePlant(); - plant.body.id = 0; - p.plants = [plant]; + p.interactions = false; + p.plants[0].body.id = 0; const wrapper = svgMount(); - expect((wrapper.find("Link").props()).style) + expect(wrapper.find("Link").props().style) .toEqual({ pointerEvents: "none" }); }); diff --git a/frontend/farm_designer/map/layers/plants/plant_layer.tsx b/frontend/farm_designer/map/layers/plants/plant_layer.tsx index 77e84e68a..078fb1dad 100644 --- a/frontend/farm_designer/map/layers/plants/plant_layer.tsx +++ b/frontend/farm_designer/map/layers/plants/plant_layer.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { GardenPlant } from "./garden_plant"; import { PlantLayerProps, Mode } from "../../interfaces"; import { unpackUUID } from "../../../../util"; -import { maybeNoPointer, getMode } from "../../util"; +import { getMode } from "../../util"; import { Link } from "../../../../link"; export function PlantLayer(props: PlantLayerProps) { @@ -44,9 +44,12 @@ export function PlantLayer(props: PlantLayerProps) { activeDragXY={activeDragXY} hovered={hovered} animate={animate} />; + const style: React.SVGProps["style"] = + (props.interactions && p.body.id) + ? { cursor: "pointer" } : { pointerEvents: "none" }; const wrapperProps = { className: "plant-link-wrapper", - style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }), + style, key: p.uuid, }; return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect) diff --git a/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx b/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx index fc73f7fbf..578b14bc5 100644 --- a/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx +++ b/frontend/farm_designer/map/layers/points/__tests__/garden_point_test.tsx @@ -1,4 +1,7 @@ -jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } })); +jest.mock("../../../../../history", () => ({ + history: { push: jest.fn() }, + getPathArray: jest.fn(), +})); import * as React from "react"; import { GardenPoint } from "../garden_point"; @@ -55,10 +58,9 @@ describe("", () => { it("opens point info", () => { const p = fakeProps(); - p.point.body.name = "weed"; const wrapper = svgMount(); wrapper.find("g").simulate("click"); expect(history.push).toHaveBeenCalledWith( - `/app/designer/weeds/${p.point.body.id}`); + `/app/designer/points/${p.point.body.id}`); }); }); diff --git a/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx b/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx index 148c823d3..9db87cdb7 100644 --- a/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx +++ b/frontend/farm_designer/map/layers/points/__tests__/point_layer_test.tsx @@ -19,10 +19,12 @@ describe("", () => { mapTransformProps: fakeMapTransformProps(), hoveredPoint: undefined, dispatch: jest.fn(), + interactions: true, }); it("shows points", () => { const p = fakeProps(); + p.interactions = false; const wrapper = svgMount(); const layer = wrapper.find("#point-layer"); expect(layer.find(GardenPoint).html()).toContain("r=\"100\""); @@ -40,6 +42,7 @@ describe("", () => { it("allows point mode interaction", () => { mockPath = "/app/designer/points"; const p = fakeProps(); + p.interactions = true; const wrapper = svgMount(); const layer = wrapper.find("#point-layer"); expect(layer.props().style).toEqual({}); diff --git a/frontend/farm_designer/map/layers/points/garden_point.tsx b/frontend/farm_designer/map/layers/points/garden_point.tsx index bf0c158fe..fbc19c4dc 100644 --- a/frontend/farm_designer/map/layers/points/garden_point.tsx +++ b/frontend/farm_designer/map/layers/points/garden_point.tsx @@ -2,8 +2,7 @@ import * as React from "react"; import { GardenPointProps } from "../../interfaces"; import { transformXY } from "../../util"; import { Actions } from "../../../../constants"; -import { history } from "../../../../history"; -import { isAWeed } from "../../../points/weeds_inventory"; +import { mapPointClickAction } from "../../actions"; export const GardenPoint = (props: GardenPointProps) => { @@ -19,11 +18,11 @@ export const GardenPoint = (props: GardenPointProps) => { const { id, x, y, meta } = point.body; const { qx, qy } = transformXY(x, y, mapTransformProps); const color = meta.color || "green"; - const panel = isAWeed(point.body.name, meta.type) ? "weeds" : "points"; - return history.push(`/app/designer/${panel}/${id}`)}> + onClick={mapPointClickAction(props.dispatch, point.uuid, + `/app/designer/points/${id}`)}> diff --git a/frontend/farm_designer/map/layers/points/point_layer.tsx b/frontend/farm_designer/map/layers/points/point_layer.tsx index d0c348a0a..65a0e77e8 100644 --- a/frontend/farm_designer/map/layers/points/point_layer.tsx +++ b/frontend/farm_designer/map/layers/points/point_layer.tsx @@ -1,8 +1,7 @@ import * as React from "react"; import { TaggedGenericPointer } from "farmbot"; import { GardenPoint } from "./garden_point"; -import { MapTransformProps, Mode } from "../../interfaces"; -import { getMode } from "../../util"; +import { MapTransformProps } from "../../interfaces"; export interface PointLayerProps { visible: boolean; @@ -10,13 +9,14 @@ export interface PointLayerProps { mapTransformProps: MapTransformProps; hoveredPoint: string | undefined; dispatch: Function; + interactions: boolean; } export function PointLayer(props: PointLayerProps) { const { visible, genericPoints, mapTransformProps, hoveredPoint } = props; const style: React.CSSProperties = - getMode() === Mode.points ? {} : { pointerEvents: "none" }; - return + props.interactions ? {} : { pointerEvents: "none" }; + return {visible && genericPoints.map(p => ", () => { mapTransformProps: fakeMapTransformProps(), dispatch: jest.fn(), hoveredToolSlot: undefined, + interactions: true, }; } it("toggles visibility off", () => { @@ -61,9 +62,19 @@ describe("", () => { expect(history.push).not.toHaveBeenCalled(); }); + it("is in clickable mode", () => { + mockPath = "/app/designer/plants/crop_search/mint/add"; + const p = fakeProps(); + p.interactions = true; + const wrapper = shallow(); + expect(wrapper.find("g").props().style) + .toEqual({ cursor: "pointer" }); + }); + it("is in non-clickable mode", () => { mockPath = "/app/designer/plants/crop_search/mint/add"; const p = fakeProps(); + p.interactions = false; const wrapper = shallow(); expect(wrapper.find("g").props().style) .toEqual({ pointerEvents: "none" }); 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 8ef52b04b..2e2306a1c 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 @@ -1,4 +1,7 @@ -jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } })); +jest.mock("../../../../../history", () => ({ + history: { push: jest.fn() }, + getPathArray: jest.fn(), +})); import * as React from "react"; import { ToolSlotPoint, TSPProps } from "../tool_slot_point"; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx index cf13ff88a..c32549f8a 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { SlotWithTool, UUID } from "../../../../resources/interfaces"; import { ToolSlotPoint } from "./tool_slot_point"; import { MapTransformProps } from "../../interfaces"; -import { maybeNoPointer } from "../../util"; export interface ToolSlotLayerProps { visible: boolean; @@ -11,6 +10,7 @@ export interface ToolSlotLayerProps { mapTransformProps: MapTransformProps; dispatch: Function; hoveredToolSlot: UUID | undefined; + interactions: boolean; } export function ToolSlotLayer(props: ToolSlotLayerProps) { @@ -18,7 +18,9 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) { return + style={props.interactions + ? { cursor: "pointer" } + : { pointerEvents: "none" }}> {visible && slots.map(slot => { }; export const ToolSlotPoint = (props: TSPProps) => { + const { tool, toolSlot } = props.slot; const { id, x, y, pullout_direction, gantry_mounted - } = props.slot.toolSlot.body; + } = toolSlot.body; const { mapTransformProps, botPositionX } = props; 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 : t("Empty"); - const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot; + const toolName = tool ? tool.body.name : t("Empty"); + const hovered = toolSlot.uuid === props.hoveredToolSlot; const toolProps = { x: qx, y: qy, hovered, dispatch: props.dispatch, - uuid: props.slot.toolSlot.uuid, + uuid: toolSlot.uuid, xySwap, }; return history.push(`/app/designer/tool-slots/${id}`)}> + onClick={mapPointClickAction(props.dispatch, toolSlot.uuid, + `/app/designer/tool-slots/${id}`)}> {pullout_direction && !gantry_mounted && ({ + history: { push: jest.fn() }, + getPathArray: jest.fn(), +})); + +import * as React from "react"; +import { GardenWeed } from "../garden_weed"; +import { GardenWeedProps } from "../../../interfaces"; +import { fakeWeed } from "../../../../../__test_support__/fake_state/resources"; +import { + fakeMapTransformProps, +} from "../../../../../__test_support__/map_transform_props"; +import { Actions } from "../../../../../constants"; +import { history } from "../../../../../history"; +import { svgMount } from "../../../../../__test_support__/svg_mount"; + +describe("", () => { + const fakeProps = (): GardenWeedProps => ({ + mapTransformProps: fakeMapTransformProps(), + weed: fakeWeed(), + hovered: false, + dispatch: jest.fn(), + current: false, + selected: false, + animate: false, + spreadVisible: true, + }); + + it("renders weed", () => { + const p = fakeProps(); + p.weed.body.meta.color = undefined; + const wrapper = svgMount(); + expect(wrapper.find("#weed-radius").props().r).toEqual(100); + expect(wrapper.find("#weed-radius").props().opacity).toEqual(0.5); + expect(wrapper.find("stop").first().props().stopColor).toEqual("red"); + }); + + it("renders weed color", () => { + const p = fakeProps(); + p.weed.body.meta.color = "orange"; + const wrapper = svgMount(); + expect(wrapper.find("#weed-radius").props().r).toEqual(100); + expect(wrapper.find("#weed-radius").props().opacity).toEqual(0.5); + expect(wrapper.find("stop").first().props().stopColor).toEqual("orange"); + }); + + it("animates", () => { + const p = fakeProps(); + p.animate = true; + const wrapper = svgMount(); + expect(wrapper.find(".soil-cloud").length).toEqual(1); + expect(wrapper.find("image").hasClass("animate")).toBeTruthy(); + }); + + it("hovers weed", () => { + const p = fakeProps(); + const wrapper = svgMount(); + wrapper.find("g").first().simulate("mouseEnter"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: p.weed.uuid + }); + }); + + it("is hovered", () => { + const p = fakeProps(); + p.hovered = true; + const wrapper = svgMount(); + expect(wrapper.find("#weed-radius").props().opacity).toEqual(1); + }); + + it("un-hovers weed", () => { + const p = fakeProps(); + const wrapper = svgMount(); + wrapper.find("g").first().simulate("mouseLeave"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: undefined + }); + }); + + it("opens weed info", () => { + const p = fakeProps(); + const wrapper = svgMount(); + wrapper.find("g").first().simulate("click"); + expect(history.push).toHaveBeenCalledWith( + `/app/designer/weeds/${p.weed.body.id}`); + }); +}); diff --git a/frontend/farm_designer/map/layers/weeds/__tests__/weed_layer_test.tsx b/frontend/farm_designer/map/layers/weeds/__tests__/weed_layer_test.tsx new file mode 100644 index 000000000..9618e5393 --- /dev/null +++ b/frontend/farm_designer/map/layers/weeds/__tests__/weed_layer_test.tsx @@ -0,0 +1,66 @@ +let mockPath = "/app/designer/plants"; +jest.mock("../../../../../history", () => ({ + getPathArray: jest.fn(() => mockPath.split("/")), +})); + +import * as React from "react"; +import { WeedLayer, WeedLayerProps } from "../weed_layer"; +import { fakeWeed } from "../../../../../__test_support__/fake_state/resources"; +import { + fakeMapTransformProps, +} from "../../../../../__test_support__/map_transform_props"; +import { GardenWeed } from "../garden_weed"; +import { svgMount } from "../../../../../__test_support__/svg_mount"; + +describe("", () => { + const fakeProps = (): WeedLayerProps => ({ + visible: true, + spreadVisible: true, + weeds: [fakeWeed()], + mapTransformProps: fakeMapTransformProps(), + hoveredPoint: undefined, + dispatch: jest.fn(), + currentPoint: undefined, + boxSelected: undefined, + groupSelected: [], + animate: false, + interactions: true, + }); + + it("shows weeds", () => { + const p = fakeProps(); + p.interactions = false; + const wrapper = svgMount(); + const layer = wrapper.find("#weeds-layer"); + expect(layer.find(GardenWeed).html()).toContain("r=\"100\""); + expect(layer.props().style).toEqual({ pointerEvents: "none" }); + }); + + it("toggles visibility off", () => { + const p = fakeProps(); + p.visible = false; + const wrapper = svgMount(); + const layer = wrapper.find("#weeds-layer"); + expect(layer.find(GardenWeed).length).toEqual(0); + }); + + it("allows weed mode interaction", () => { + mockPath = "/app/designer/weeds"; + const p = fakeProps(); + p.interactions = true; + const wrapper = svgMount(); + const layer = wrapper.find("#weeds-layer"); + expect(layer.props().style).toEqual({ cursor: "pointer" }); + }); + + it("is selected", () => { + mockPath = "/app/designer/weeds"; + const p = fakeProps(); + const weed = fakeWeed(); + p.weeds = [weed]; + p.boxSelected = [weed.uuid]; + const wrapper = svgMount(); + const layer = wrapper.find("#weeds-layer"); + expect(layer.find(GardenWeed).props().selected).toBeTruthy(); + }); +}); diff --git a/frontend/farm_designer/map/layers/weeds/garden_weed.tsx b/frontend/farm_designer/map/layers/weeds/garden_weed.tsx new file mode 100644 index 000000000..137eb2915 --- /dev/null +++ b/frontend/farm_designer/map/layers/weeds/garden_weed.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import { GardenWeedProps } from "../../interfaces"; +import { transformXY } from "../../util"; +import { Actions } from "../../../../constants"; +import { Color } from "../../../../ui"; +import { mapPointClickAction } from "../../actions"; + +export const DEFAULT_WEED_ICON = "/app-resources/img/generic-weed.svg"; + +export const GardenWeed = (props: GardenWeedProps) => { + + const iconHover = (action: "start" | "end") => () => { + const hover = action === "start"; + props.dispatch({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: hover ? props.weed.uuid : undefined + }); + }; + + const { weed, mapTransformProps, hovered, current, selected, animate } = props; + const { id, x, y, meta, radius } = weed.body; + const { qx, qy } = transformXY(x, y, mapTransformProps); + const color = meta.color || "red"; + const stopOpacity = ["gray", "pink", "orange"].includes(color) ? 0.5 : 0.25; + const className = [ + "weed-image", `is-chosen-${current || selected}`, animate ? "animate" : "", + ].join(" "); + const iconRadius = hovered ? radius * 0.88 : radius * 0.8; + return + + + + + + + + {animate && + } + + {props.spreadVisible && + } + + + + + ; +}; diff --git a/frontend/farm_designer/map/layers/weeds/weed_layer.tsx b/frontend/farm_designer/map/layers/weeds/weed_layer.tsx new file mode 100644 index 000000000..66a826601 --- /dev/null +++ b/frontend/farm_designer/map/layers/weeds/weed_layer.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { TaggedWeedPointer } from "farmbot"; +import { GardenWeed } from "./garden_weed"; +import { MapTransformProps } from "../../interfaces"; +import { UUID } from "../../../../resources/interfaces"; + +export interface WeedLayerProps { + visible: boolean; + spreadVisible: boolean; + weeds: TaggedWeedPointer[]; + mapTransformProps: MapTransformProps; + hoveredPoint: UUID | undefined; + currentPoint: UUID | undefined; + boxSelected: UUID[] | undefined; + groupSelected: UUID[]; + dispatch: Function; + animate: boolean; + interactions: boolean; +} + +export function WeedLayer(props: WeedLayerProps) { + const { visible, weeds, mapTransformProps } = props; + return + {visible && + weeds.map(p => { + const current = p.uuid === props.currentPoint; + const hovered = p.uuid === props.hoveredPoint; + const selectedByBox = !!props.boxSelected?.includes(p.uuid); + const selectedByGroup = props.groupSelected.includes(p.uuid); + return ; + })} + ; +} diff --git a/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx b/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx index a812e4f53..e58d7ed08 100644 --- a/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx +++ b/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx @@ -18,6 +18,7 @@ describe("", () => { y: { value: 1500, isDefault: true } }, mapTransformProps: fakeMapTransformProps(), + startDrag: jest.fn(), }); it("renders", () => { @@ -70,15 +71,15 @@ describe("", () => { p.groups[0].body.id = 1; p.currentGroup = p.groups[0].uuid; const wrapper = svgMount(); - expect(wrapper.html()) - .toEqual(""); + expect(wrapper.html()).toEqual( + ""); }); it("doesn't render current group's zones", () => { const p = fakeProps(); p.visible = false; const wrapper = svgMount(); - expect(wrapper.html()) - .toEqual(""); + expect(wrapper.html()).toEqual( + ""); }); }); diff --git a/frontend/farm_designer/map/layers/zones/zones.tsx b/frontend/farm_designer/map/layers/zones/zones.tsx index 1d04f353c..5de295154 100644 --- a/frontend/farm_designer/map/layers/zones/zones.tsx +++ b/frontend/farm_designer/map/layers/zones/zones.tsx @@ -4,6 +4,7 @@ import { MapTransformProps, BotSize } from "../../interfaces"; import { transformXY } from "../../util"; import { isUndefined } from "lodash"; import { UUID } from "../../../../resources/interfaces"; +import { history } from "../../../../history"; export interface ZonesProps { currentGroup: UUID | undefined; @@ -43,6 +44,9 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => { return ZoneType.none; }; +const openGroup = (id: number | undefined) => + () => history.push(`/app/designer/groups/${id}`); + /** Bounds for area selected by criteria or bot extents. */ const getBoundary = (props: GetBoundaryProps): Boundary => { const { criteria } = props.group.body; @@ -85,7 +89,8 @@ const zone0D = (props: ZonesProps) => /** Coordinates selected by both x and y number equal values. */ export const Zones0D = (props: ZonesProps) => { const current = props.group.uuid == props.currentGroup; - return {zone0D(props).map((point, i) => )} @@ -126,7 +131,8 @@ const zone1D = (props: ZonesProps) => { /** Lines selected by an x or y number equal value. */ export const Zones1D = (props: ZonesProps) => { const current = props.group.uuid == props.currentGroup; - return {zone1D(props).map((line, i) => { export const Zones2D = (props: ZonesProps) => { const zone = zone2D(getBoundary(props), props.mapTransformProps); const current = props.group.uuid == props.currentGroup; - return {!zone.selectsAll && } diff --git a/frontend/farm_designer/map/layers/zones/zones_layer.tsx b/frontend/farm_designer/map/layers/zones/zones_layer.tsx index 21bd5bb02..020878126 100644 --- a/frontend/farm_designer/map/layers/zones/zones_layer.tsx +++ b/frontend/farm_designer/map/layers/zones/zones_layer.tsx @@ -3,6 +3,7 @@ import { TaggedPointGroup } from "farmbot"; import { MapTransformProps, BotSize } from "../../interfaces"; import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones"; import { UUID } from "../../../../resources/interfaces"; +import { allowGroupAreaInteraction } from "../../util"; export interface ZonesLayerProps { visible: boolean; @@ -10,6 +11,7 @@ export interface ZonesLayerProps { groups: TaggedPointGroup[]; botSize: BotSize; mapTransformProps: MapTransformProps; + startDrag(e: React.MouseEvent): void; } export function ZonesLayer(props: ZonesLayerProps) { @@ -17,7 +19,9 @@ export function ZonesLayer(props: ZonesLayerProps) { const commonProps = { botSize, mapTransformProps, currentGroup }; const visible = (group: TaggedPointGroup) => props.visible || (group.uuid == currentGroup); - return + return {groups.map(group => visible(group) && getZoneType(group) === ZoneType.area && )} diff --git a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx index c254da1e7..a35de71a2 100644 --- a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx +++ b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx @@ -35,6 +35,7 @@ describe("", () => { legendMenuOpen: true, showPlants: false, showPoints: false, + showWeeds: false, showSpread: false, showFarmbot: false, showImages: false, diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx index 4b8a2561d..d21f4d986 100644 --- a/frontend/farm_designer/map/legend/garden_map_legend.tsx +++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx @@ -59,6 +59,10 @@ const LayerToggles = (props: GardenMapLegendProps) => { popover={DevSettings.futureFeaturesEnabled() ? : undefined} /> + { if (pathArray[4] === "select") { return Mode.boxSelect; } if (pathArray[4] === "crop_search") { return Mode.addPlant; } if (pathArray[3] === "move_to") { return Mode.moveTo; } - if (pathArray[3] === "points" || pathArray[3] === "weeds") { + if (pathArray[3] === "points") { if (pathArray[4] === "add") { return Mode.createPoint; } return Mode.points; } + if (pathArray[3] === "weeds") { + if (pathArray[4] === "add") { return Mode.createWeed; } + return Mode.weeds; + } if (savedGardenOpen(pathArray)) { return Mode.templateView; } } return Mode.none; @@ -337,18 +341,28 @@ export const getGardenCoordinates = (props: { } }; -export const maybeNoPointer = - (defaultStyle: React.CSSProperties): React.SVGProps["style"] => { - switch (getMode()) { - case Mode.clickToAdd: - case Mode.moveTo: - case Mode.points: - case Mode.createPoint: - return { pointerEvents: "none" }; - default: - return defaultStyle; - } - }; +export const allowInteraction = () => { + switch (getMode()) { + case Mode.clickToAdd: + case Mode.moveTo: + case Mode.createPoint: + case Mode.createWeed: + return false; + default: + return true; + } +}; + +export const allowGroupAreaInteraction = () => { + if (!allowInteraction()) { return false; } + switch (getMode()) { + case Mode.boxSelect: + case Mode.editGroup: + return false; + default: + return true; + } +}; /** Check if the cursor is within the selected plant indicator area. */ export const cursorAtPlant = diff --git a/frontend/farm_designer/plants/__tests__/plant_inventory_item_test.tsx b/frontend/farm_designer/plants/__tests__/plant_inventory_item_test.tsx index 9bba0a612..31f1c7c33 100644 --- a/frontend/farm_designer/plants/__tests__/plant_inventory_item_test.tsx +++ b/frontend/farm_designer/plants/__tests__/plant_inventory_item_test.tsx @@ -2,7 +2,17 @@ jest.mock("../../../open_farm/cached_crop", () => ({ maybeGetCachedPlantIcon: jest.fn(), })); -jest.mock("../../../history", () => ({ push: jest.fn() })); +let mockPath = "/app/designer/plants"; +jest.mock("../../../history", () => ({ + push: jest.fn(), + getPathArray: () => mockPath.split("/"), +})); + +jest.mock("../../map/actions", () => ({ + mapPointClickAction: jest.fn(() => jest.fn()), + setHoveredPlant: jest.fn(), + selectPoint: jest.fn(), +})); import * as React from "react"; import { @@ -12,9 +22,11 @@ import { shallow, mount } from "enzyme"; import { fakePlant, fakePlantTemplate, } from "../../../__test_support__/fake_state/resources"; -import { Actions } from "../../../constants"; import { push } from "../../../history"; import { maybeGetCachedPlantIcon } from "../../../open_farm/cached_crop"; +import { + mapPointClickAction, setHoveredPlant, selectPoint, +} from "../../map/actions"; describe("", () => { const fakeProps = (): PlantInventoryItemProps => ({ @@ -40,48 +52,43 @@ describe("", () => { const p = fakeProps(); const wrapper = shallow(); wrapper.simulate("mouseEnter"); - expect(p.dispatch).toBeCalledWith({ - payload: { - icon: "", - plantUUID: p.plant.uuid - }, - type: Actions.TOGGLE_HOVERED_PLANT - }); + expect(setHoveredPlant).toBeCalledWith(p.plant.uuid, ""); }); it("hover end", () => { - const p = fakeProps(); - const wrapper = shallow(); + const wrapper = shallow(); wrapper.simulate("mouseLeave"); - expect(p.dispatch).toBeCalledWith({ - payload: { - icon: "", - plantUUID: undefined - }, - type: Actions.TOGGLE_HOVERED_PLANT - }); + expect(setHoveredPlant).toBeCalledWith(undefined, ""); }); it("selects plant", () => { + mockPath = "/app/designer/plants"; const p = fakeProps(); const wrapper = shallow(); wrapper.simulate("click"); - expect(p.dispatch).toBeCalledWith({ - payload: [p.plant.uuid], - type: Actions.SELECT_PLANT - }); + expect(mapPointClickAction).not.toHaveBeenCalled(); + expect(selectPoint).toBeCalledWith([p.plant.uuid]); expect(push).toHaveBeenCalledWith("/app/designer/plants/" + p.plant.body.id); }); + it("removes item in box select mode", () => { + mockPath = "/app/designer/plants/select"; + const p = fakeProps(); + const wrapper = shallow(); + wrapper.simulate("click"); + expect(mapPointClickAction).toHaveBeenCalledWith(expect.any(Function), + p.plant.uuid); + expect(push).not.toHaveBeenCalled(); + expect(setHoveredPlant).toHaveBeenCalledWith(undefined, ""); + }); + it("selects plant template", () => { + mockPath = "/app/designer/plants"; const p = fakeProps(); p.plant = fakePlantTemplate(); const wrapper = shallow(); wrapper.simulate("click"); - expect(p.dispatch).toBeCalledWith({ - payload: [p.plant.uuid], - type: Actions.SELECT_PLANT - }); + expect(selectPoint).toBeCalledWith([p.plant.uuid]); expect(push).toHaveBeenCalledWith( "/app/designer/gardens/templates/" + p.plant.body.id); }); @@ -94,4 +101,12 @@ describe("", () => { expect(maybeGetCachedPlantIcon).toHaveBeenCalledWith("strawberry", img.instance(), expect.any(Function)); }); + + it("sets icon", () => { + const wrapper = + mount(); + expect(wrapper.state().icon).toEqual(""); + wrapper.instance().updateStateIcon("fake icon"); + expect(wrapper.state().icon).toEqual("fake icon"); + }); }); diff --git a/frontend/farm_designer/plants/__tests__/select_plants_test.tsx b/frontend/farm_designer/plants/__tests__/select_plants_test.tsx index 25de6987a..d61e0c039 100644 --- a/frontend/farm_designer/plants/__tests__/select_plants_test.tsx +++ b/frontend/farm_designer/plants/__tests__/select_plants_test.tsx @@ -15,11 +15,15 @@ jest.mock("../../../account/dev/dev_support", () => ({ jest.mock("../../point_groups/actions", () => ({ createGroup: jest.fn() })); import * as React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps, + getFilteredPoints, GetFilteredPointsProps, } from "../select_plants"; -import { fakePlant } from "../../../__test_support__/fake_state/resources"; +import { + fakePlant, fakePoint, fakeWeed, fakeToolSlot, fakeTool, + fakePlantTemplate, +} from "../../../__test_support__/fake_state/resources"; import { Actions, Content } from "../../../constants"; import { clickButton } from "../../../__test_support__/helpers"; import { destroy } from "../../../api/crud"; @@ -41,9 +45,16 @@ describe("", () => { plant2.body.name = "Blueberry"; return { selected: ["plant.1"], + selectionPointType: undefined, + getConfigValue: () => true, plants: [plant1, plant2], dispatch: jest.fn(x => x), gardenOpen: undefined, + allPoints: [], + xySwap: false, + quadrant: 2, + isActive: () => false, + tools: [], }; } @@ -52,6 +63,53 @@ describe("", () => { expect(wrapper.text()).toContain("Strawberry"); }); + it("displays selected point", () => { + const p = fakeProps(); + const point = fakePoint(); + point.body.name = "fake point"; + p.allPoints = [point]; + p.selected = [point.uuid]; + p.selectionPointType = ["GenericPointer"]; + const wrapper = mount(); + expect(wrapper.text()).toContain(point.body.name); + }); + + it("displays selected weed", () => { + const p = fakeProps(); + const weed = fakeWeed(); + weed.body.name = "fake weed"; + p.allPoints = [weed]; + p.selected = [weed.uuid]; + p.selectionPointType = ["Weed"]; + const wrapper = mount(); + expect(wrapper.text()).toContain(weed.body.name); + }); + + it("displays selected slot", () => { + const p = fakeProps(); + const tool = fakeTool(); + tool.body.id = 1; + tool.body.name = "fake tool slot"; + p.tools = [tool]; + const slot = fakeToolSlot(); + slot.body.tool_id = 1; + p.allPoints = [slot]; + p.selected = [slot.uuid]; + p.selectionPointType = ["ToolSlot"]; + const wrapper = mount(); + expect(wrapper.text()).toContain(tool.body.name); + }); + + it("clears point section type", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.unmount(); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_SELECTION_POINT_TYPE, + payload: undefined, + }); + }); + it("displays multiple selected plants", () => { const p = fakeProps(); p.selected = ["plant.1", "plant.2"]; @@ -88,31 +146,42 @@ describe("", () => { expect(wrapper.text()).not.toContain("Strawberry Plant"); }); + it("changes selection type", () => { + const p = fakeProps(); + const wrapper = mount(); + const actionsWrapper = shallow(wrapper.instance().ActionButtons()); + actionsWrapper.find("FBSelect").first().simulate("change", + { label: "", value: "All" }); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_SELECTION_POINT_TYPE, + payload: ["Plant", "GenericPointer", "ToolSlot", "Weed"], + }); + }); + it("selects all", () => { const p = fakeProps(); - p.dispatch = jest.fn(); const wrapper = mount(); - clickButton(wrapper, 1, "select all"); + clickButton(wrapper, 2, "select all"); expect(p.dispatch).toHaveBeenCalledWith( - { payload: ["plant.1", "plant.2"], type: Actions.SELECT_PLANT }); + { payload: ["plant.1", "plant.2"], type: Actions.SELECT_POINT }); }); it("selects none", () => { const p = fakeProps(); - p.dispatch = jest.fn(); const wrapper = mount(); - clickButton(wrapper, 0, "select none"); + clickButton(wrapper, 1, "select none"); expect(p.dispatch).toHaveBeenCalledWith( - { payload: undefined, type: Actions.SELECT_PLANT }); + { payload: undefined, type: Actions.SELECT_POINT }); }); + const DELETE_BTN_INDEX = 3; + it("confirms deletion of selected plants", () => { const p = fakeProps(); p.selected = ["plant.1", "plant.2"]; const wrapper = mount(); - expect(wrapper.text()).toContain("Delete"); window.confirm = jest.fn(); - wrapper.find("button").at(2).simulate("click"); + clickButton(wrapper, DELETE_BTN_INDEX, "Delete"); expect(window.confirm).toHaveBeenCalledWith( "Are you sure you want to delete 2 plants?"); }); @@ -122,9 +191,8 @@ describe("", () => { mockDestroy = jest.fn(() => Promise.resolve()); p.selected = ["plant.1", "plant.2"]; const wrapper = mount(); - expect(wrapper.text()).toContain("Delete"); window.confirm = () => true; - wrapper.find("button").at(2).simulate("click"); + clickButton(wrapper, DELETE_BTN_INDEX, "Delete"); expect(destroy).toHaveBeenCalledWith("plant.1", true); expect(destroy).toHaveBeenCalledWith("plant.2", true); }); @@ -134,19 +202,17 @@ describe("", () => { mockDestroy = jest.fn(() => Promise.resolve()); p.selected = undefined; const wrapper = mount(); - expect(wrapper.text()).toContain("Delete"); - wrapper.find("button").at(2).simulate("click"); + clickButton(wrapper, DELETE_BTN_INDEX, "Delete"); expect(destroy).not.toHaveBeenCalled(); }); - it("errors when deleting selected plants", () => { + it("errors when deleting selected plants", async () => { const p = fakeProps(); mockDestroy = jest.fn(() => Promise.reject()); p.selected = ["plant.1", "plant.2"]; const wrapper = mount(); - expect(wrapper.text()).toContain("Delete"); window.confirm = () => true; - wrapper.find("button").at(2).simulate("click"); + await clickButton(wrapper, DELETE_BTN_INDEX, "Delete"); expect(destroy).toHaveBeenCalledWith("plant.1", true); expect(destroy).toHaveBeenCalledWith("plant.2", true); }); @@ -183,3 +249,43 @@ describe("mapStateToProps", () => { expect(result.dispatch).toBe(state.dispatch); }); }); + +describe("getFilteredPoints()", () => { + const plant = fakePlant(); + const point = fakePoint(); + const weed = fakeWeed(); + const slot = fakeToolSlot(); + + const fakeProps = (): GetFilteredPointsProps => ({ + selectionPointType: undefined, + getConfigValue: () => true, + plants: [plant], + allPoints: [plant, point, weed, slot], + }); + + it("returns filtered points: all", () => { + const p = fakeProps(); + p.selectionPointType = ["Plant", "GenericPointer", "Weed", "ToolSlot"]; + expect(getFilteredPoints(p)).toEqual([plant, point, weed, slot]); + }); + + it("returns filtered points: none", () => { + const p = fakeProps(); + p.selectionPointType = ["Plant", "GenericPointer", "Weed", "ToolSlot"]; + p.getConfigValue = () => false; + expect(getFilteredPoints(p)).toEqual([]); + }); + + it("returns filtered points: plants", () => { + const p = fakeProps(); + const plantTemplate = fakePlantTemplate(); + p.plants = [plantTemplate]; + expect(getFilteredPoints(p)).toEqual([plantTemplate]); + }); + + it("returns filtered points: tool slots", () => { + const p = fakeProps(); + p.selectionPointType = ["ToolSlot"]; + expect(getFilteredPoints(p)).toEqual([slot]); + }); +}); diff --git a/frontend/farm_designer/plants/plant_inventory_item.tsx b/frontend/farm_designer/plants/plant_inventory_item.tsx index c0392f278..f6b8dbe2a 100644 --- a/frontend/farm_designer/plants/plant_inventory_item.tsx +++ b/frontend/farm_designer/plants/plant_inventory_item.tsx @@ -1,12 +1,13 @@ import * as React from "react"; import { DEFAULT_ICON } from "../../open_farm/icons"; import { push } from "../../history"; -import { TaggedPlant } from "../map/interfaces"; +import { TaggedPlant, Mode } from "../map/interfaces"; import { unpackUUID } from "../../util"; import { t } from "../../i18next_wrapper"; import { maybeGetCachedPlantIcon } from "../../open_farm/cached_crop"; -import { selectPlant, setHoveredPlant } from "../map/actions"; +import { selectPoint, setHoveredPlant, mapPointClickAction } from "../map/actions"; import { plantAge } from "./map_state_to_props"; +import { getMode } from "../map/util"; export interface PlantInventoryItemProps { plant: TaggedPlant; @@ -21,9 +22,10 @@ interface PlantInventoryItemState { // The individual plants that show up in the farm designer sub nav. export class PlantInventoryItem extends React.Component { - state: PlantInventoryItemState = { icon: "" }; + updateStateIcon = (i: string) => this.setState({ icon: i }); + render() { const { plant, dispatch } = this.props; const plantId = (plant.body.id || "ERR_NO_PLANT_ID").toString(); @@ -36,17 +38,21 @@ export class PlantInventoryItem extends }; const click = () => { - const plantCategory = - unpackUUID(plant.uuid).kind === "PlantTemplate" - ? "gardens/templates" - : "plants"; - push(`/app/designer/${plantCategory}/${plantId}`); - dispatch(selectPlant([plant.uuid])); + if (getMode() == Mode.boxSelect) { + mapPointClickAction(dispatch, plant.uuid)(); + toggle("leave"); + } else { + const plantCategory = + unpackUUID(plant.uuid).kind === "PlantTemplate" + ? "gardens/templates" + : "plants"; + push(`/app/designer/${plantCategory}/${plantId}`); + dispatch(selectPoint([plant.uuid])); + } }; - const updateStateIcon = (i: string) => this.setState({ icon: i }); const onLoad = (e: React.SyntheticEvent) => - maybeGetCachedPlantIcon(slug, e.currentTarget, updateStateIcon); + maybeGetCachedPlantIcon(slug, e.currentTarget, this.updateStateIcon); // Name given from OpenFarm's API. const label = plant.body.name || "Unknown plant"; diff --git a/frontend/farm_designer/plants/select_plants.tsx b/frontend/farm_designer/plants/select_plants.tsx index 8060c42c7..5cebb3302 100644 --- a/frontend/farm_designer/plants/select_plants.tsx +++ b/frontend/farm_designer/plants/select_plants.tsx @@ -4,7 +4,7 @@ import { connect } from "react-redux"; import { Everything } from "../../interfaces"; import { PlantInventoryItem } from "./plant_inventory_item"; import { destroy } from "../../api/crud"; -import { unselectPlant, selectPlant, setHoveredPlant } from "../map/actions"; +import { unselectPlant, selectPoint, setHoveredPlant } from "../map/actions"; import { Actions, Content } from "../../constants"; import { TaggedPlant } from "../map/interfaces"; import { getPlants } from "../state_to_props"; @@ -16,19 +16,73 @@ import { createGroup } from "../point_groups/actions"; import { PanelColor } from "../panel_header"; import { error } from "../../toast/toast"; import { PlantStatusBulkUpdate } from "./edit_plant_status"; +import { FBSelect, DropDownItem } from "../../ui"; +import { + PointType, TaggedPoint, TaggedGenericPointer, TaggedToolSlotPointer, + TaggedTool, + TaggedWeedPointer, +} from "farmbot"; +import { UUID } from "../../resources/interfaces"; +import { + selectAllActivePoints, selectAllToolSlotPointers, selectAllTools, +} from "../../resources/selectors"; +import { PointInventoryItem } from "../points/point_inventory_item"; +import { ToolSlotInventoryItem } from "../tools"; +import { getWebAppConfigValue, GetWebAppConfigValue } from "../../config_storage/actions"; +import { BooleanSetting, NumericSetting } from "../../session_keys"; +import { isBotOriginQuadrant, BotOriginQuadrant } from "../interfaces"; +import { isActive } from "../tools/edit_tool"; +import { uniq } from "lodash"; +import { POINTER_TYPES } from "../point_groups/criteria/interfaces"; +import { WeedInventoryItem } from "../weeds/weed_inventory_item"; -export const mapStateToProps = (props: Everything): SelectPlantsProps => ({ - selected: props.resources.consumers.farm_designer.selectedPlants, - plants: getPlants(props.resources), - dispatch: props.dispatch, - gardenOpen: props.resources.consumers.farm_designer.openedSavedGarden, +export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ + Plant: { label: t("Plants"), value: "Plant" }, + GenericPointer: { label: t("Points"), value: "GenericPointer" }, + Weed: { label: t("Weeds"), value: "Weed" }, + ToolSlot: { label: t("Slots"), value: "ToolSlot" }, + All: { label: t("All"), value: "All" }, }); +export const POINTER_TYPE_LIST = () => [ + POINTER_TYPE_DDI_LOOKUP().Plant, + POINTER_TYPE_DDI_LOOKUP().GenericPointer, + POINTER_TYPE_DDI_LOOKUP().Weed, + POINTER_TYPE_DDI_LOOKUP().ToolSlot, + POINTER_TYPE_DDI_LOOKUP().All, +]; + +export const mapStateToProps = (props: Everything): SelectPlantsProps => { + const getWebAppConfig = getWebAppConfigValue(() => props); + const xySwap = !!getWebAppConfig(BooleanSetting.xy_swap); + const rawQuadrant = getWebAppConfig(NumericSetting.bot_origin_quadrant); + const quadrant = isBotOriginQuadrant(rawQuadrant) ? rawQuadrant : 2; + return { + selected: props.resources.consumers.farm_designer.selectedPoints, + selectionPointType: props.resources.consumers.farm_designer.selectionPointType, + getConfigValue: getWebAppConfig, + plants: getPlants(props.resources), + allPoints: selectAllActivePoints(props.resources.index), + dispatch: props.dispatch, + gardenOpen: props.resources.consumers.farm_designer.openedSavedGarden, + tools: selectAllTools(props.resources.index), + isActive: isActive(selectAllToolSlotPointers(props.resources.index)), + xySwap, + quadrant, + }; +}; export interface SelectPlantsProps { plants: TaggedPlant[]; + allPoints: TaggedPoint[]; dispatch: Function; - selected: string[] | undefined; + selected: UUID[] | undefined; + selectionPointType: PointType[] | undefined; + getConfigValue: GetWebAppConfigValue; gardenOpen: string | undefined; + xySwap: boolean; + quadrant: BotOriginQuadrant; + isActive(id: number | undefined): boolean; + tools: TaggedTool[]; } export class RawSelectPlants extends React.Component { @@ -42,6 +96,11 @@ export class RawSelectPlants extends React.Component { } } + componentWillUnmount = () => this.props.dispatch({ + type: Actions.SET_SELECTION_POINT_TYPE, + payload: undefined, + }); + get selected() { return this.props.selected || []; } destroySelected = (plantUUIDs: string[] | undefined) => { @@ -56,18 +115,32 @@ export class RawSelectPlants extends React.Component { } } + get selectionPointType() { + const selectionPointTypes = this.props.selectionPointType || ["Plant"]; + return selectionPointTypes.length > 1 ? "All" : selectionPointTypes[0]; + } + ActionButtons = () =>
+ { + this.props.dispatch(selectPoint(undefined)); + this.props.dispatch({ + type: Actions.SET_SELECTION_POINT_TYPE, + payload: ddi.value == "All" ? POINTER_TYPES : [ddi.value], + }); + }} />
@@ -85,42 +158,152 @@ export class RawSelectPlants extends React.Component { : error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}> {t("Create group")} - + {this.selectionPointType == "Plant" && + }
; - render() { - const { plants, dispatch } = this.props; - const selectedPlantData = - this.selected.map(uuid => plants.filter(p => p.uuid == uuid)[0]); + get filteredPoints() { + const { plants, allPoints, selectionPointType, getConfigValue } = this.props; + return getFilteredPoints({ + plants, allPoints, selectionPointType, getConfigValue + }); + } + get selectedPointData() { + const { plants, allPoints, selectionPointType } = this.props; + return getSelectedPoints({ + plants, allPoints, selectionPointType, + selected: this.selected, + }); + } + + get allPointUuids() { + return this.filteredPoints.map(p => p.uuid); + } + + get itemName() { + const { value } = POINTER_TYPE_DDI_LOOKUP()[this.selectionPointType]; + const ITEM_NAME_LOOKUP: + Record> = { + "Plant": { singular: t("plant"), plural: t("plants") }, + "GenericPointer": { singular: t("point"), plural: t("points") }, + "Weed": { singular: t("weed"), plural: t("weeds") }, + "ToolSlot": { singular: t("slot"), plural: t("slots") }, + "All": { singular: t("item"), plural: t("items") }, + }; + return ITEM_NAME_LOOKUP["" + value][ + this.selected.length == 1 ? "singular" : "plural"]; + } + + render() { + const { dispatch } = this.props; return - {selectedPlantData && selectedPlantData[0] && - selectedPlantData.map(p => - { + if (p.kind == "PlantTemplate" || p.body.pointer_type == "Plant") { + return )} + dispatch={dispatch} />; + } else { + switch (p.body.pointer_type) { + case "GenericPointer": + return ; + case "Weed": + return ; + case "ToolSlot": + return ; + } + } + })} ; } } export const SelectPlants = connect(mapStateToProps)(RawSelectPlants); + +export interface GetFilteredPointsProps { + selectionPointType: PointType[] | undefined; + plants: TaggedPlant[]; + allPoints: TaggedPoint[]; + getConfigValue?: GetWebAppConfigValue; +} + +export const getFilteredPoints = (props: GetFilteredPointsProps) => { + const selectionPointType = (props.selectionPointType || ["Plant"]) + .filter(x => !props.getConfigValue || + getVisibleLayers(props.getConfigValue).includes(x)); + const filterPoints = (p: TaggedPoint) => + selectionPointType.includes(p.body.pointer_type); + const plants = selectionPointType.includes("Plant") ? props.plants : []; + const otherPoints = + props.allPoints + .filter(p => p.body.pointer_type != "Plant") + .filter(filterPoints); + const plantsAndOtherPoints: (TaggedPlant | TaggedPoint)[] = []; + return uniq(plantsAndOtherPoints.concat(plants).concat(otherPoints)); +}; + +interface GetSelectedPointsProps extends GetFilteredPointsProps { + selected: UUID[]; +} + +export const getSelectedPoints = (props: GetSelectedPointsProps) => + props.selected + .map(uuid => getFilteredPoints(props).filter(p => p.uuid == uuid)[0]) + .filter(p => p); + +enum PointerType { + Plant = "Plant", + GenericPointer = "GenericPointer", + Weed = "Weed", + ToolSlot = "ToolSlot", +} + +const getVisibleLayers = (getConfigValue: GetWebAppConfigValue): PointType[] => { + const showPlants = getConfigValue(BooleanSetting.show_plants); + const showPoints = getConfigValue(BooleanSetting.show_points); + const showWeeds = getConfigValue(BooleanSetting.show_weeds); + const showFarmbot = getConfigValue(BooleanSetting.show_farmbot); + return [ + ...(showPlants ? [PointerType.Plant] : []), + ...(showPoints ? [PointerType.GenericPointer] : []), + ...(showWeeds ? [PointerType.Weed] : []), + ...(showFarmbot ? [PointerType.ToolSlot] : []), + ]; +}; diff --git a/frontend/farm_designer/point_groups/__tests__/group_inventory_item_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_inventory_item_test.tsx index ddb719e30..e6cf6194c 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_inventory_item_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_inventory_item_test.tsx @@ -1,3 +1,14 @@ +jest.mock("../../../api/crud", () => ({ + destroy: jest.fn(), +})); + +let mockDelMode = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + quickDeleteEnabled: () => mockDelMode, + } +})); + import React from "react"; import { GroupInventoryItem, GroupInventoryItemProps, @@ -6,6 +17,7 @@ import { fakePointGroup, fakePlant, } from "../../../__test_support__/fake_state/resources"; import { mount } from "enzyme"; +import { destroy } from "../../../api/crud"; describe("", () => { const fakeProps = (): GroupInventoryItemProps => ({ @@ -32,4 +44,21 @@ describe("", () => { expect(x.text()).toContain("woosh"); expect(x.find(".hovered").length).toBe(1); }); + + it("opens group", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.find("div").first().simulate("click"); + expect(p.onClick).toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalledWith(p.group.uuid); + }); + + it("deletes group", () => { + mockDelMode = true; + const p = fakeProps(); + const wrapper = mount(); + wrapper.find("div").first().simulate("click"); + expect(p.onClick).not.toHaveBeenCalled(); + expect(destroy).toHaveBeenCalledWith(p.group.uuid); + }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx index c6180656c..75a32c755 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx @@ -9,10 +9,11 @@ jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() })); import React from "react"; import { PointGroupItem, PointGroupItemProps, genericPointIcon, OTHER_POINT_ICON, + genericWeedIcon, } from "../point_group_item"; -import { shallow } from "enzyme"; +import { shallow, mount } from "enzyme"; import { - fakePlant, fakePointGroup, fakePoint, fakeToolSlot, + fakePlant, fakePointGroup, fakePoint, fakeToolSlot, fakeWeed, } from "../../../__test_support__/fake_state/resources"; import { maybeGetCachedPlantIcon, setImgSrc, @@ -22,7 +23,8 @@ import { overwrite } from "../../../api/crud"; import { cloneDeep } from "lodash"; import { imgEvent } from "../../../__test_support__/fake_html_events"; import { error } from "../../../toast/toast"; -import { svgToUrl } from "../../../open_farm/icons"; +import { svgToUrl, DEFAULT_ICON } from "../../../open_farm/icons"; +import { DEFAULT_WEED_ICON } from "../../map/layers/weeds/garden_weed"; describe("", () => { const fakeProps = (): PointGroupItemProps => ({ @@ -61,25 +63,36 @@ describe("", () => { expect(i.setState).toHaveBeenCalledWith({ icon: "fake icon" }); }); - it("fetches point icon", () => { + it("displays default plant icon", () => { + const p = fakeProps(); + p.point = fakePlant(); + const wrapper = mount(); + expect(wrapper.find("img").props().src).toEqual(DEFAULT_ICON); + }); + + it("displays point icon", () => { const p = fakeProps(); p.point = fakePoint(); - const i = new PointGroupItem(p); - const fakeImgEvent = imgEvent(); - i.maybeGetCachedIcon(fakeImgEvent); - expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled(); - expect(setImgSrc).toHaveBeenCalledWith(expect.any(Object), + const wrapper = mount(); + expect(wrapper.find("img").props().src).toEqual( svgToUrl(genericPointIcon(undefined))); }); - it("fetches other icon", () => { + it("displays weed icon", () => { + const p = fakeProps(); + p.point = fakeWeed(); + p.point.body.meta.color = undefined; + const wrapper = mount(); + expect(wrapper.find("img").first().props().src).toEqual(DEFAULT_WEED_ICON); + expect(wrapper.find("img").last().props().src).toEqual( + svgToUrl(genericWeedIcon(undefined))); + }); + + it("displays other icon", () => { const p = fakeProps(); p.point = fakeToolSlot(); - const i = new PointGroupItem(p); - const fakeImgEvent = imgEvent(); - i.maybeGetCachedIcon(fakeImgEvent); - expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled(); - expect(setImgSrc).toHaveBeenCalledWith(expect.any(Object), + const wrapper = mount(); + expect(wrapper.find("img").props().src).toEqual( svgToUrl(OTHER_POINT_ICON)); }); diff --git a/frontend/farm_designer/point_groups/criteria/interfaces.ts b/frontend/farm_designer/point_groups/criteria/interfaces.ts index b7bbde930..7b22f8b79 100644 --- a/frontend/farm_designer/point_groups/criteria/interfaces.ts +++ b/frontend/farm_designer/point_groups/criteria/interfaces.ts @@ -1,14 +1,14 @@ -import { TaggedPointGroup } from "farmbot"; -import { PointGroup, Point } from "farmbot/dist/resources/api_resources"; +import { TaggedPointGroup, PointType } from "farmbot"; +import { PointGroup } from "farmbot/dist/resources/api_resources"; export type PointGroupCriteria = PointGroup["criteria"]; export type StringEqCriteria = PointGroupCriteria["string_eq"]; -export type PointerType = Point["pointer_type"]; +export type PointerType = PointType; export type StrAndNumCriteriaKeys = (keyof Omit)[]; export type EqCriteria = Record; export const POINTER_TYPES: PointerType[] = - ["Plant", "GenericPointer", "ToolSlot"]; + ["Plant", "GenericPointer", "ToolSlot", "Weed"]; export const DEFAULT_CRITERIA: Readonly = { day: { op: "<", days_ago: 0 }, diff --git a/frontend/farm_designer/point_groups/group_inventory_item.tsx b/frontend/farm_designer/point_groups/group_inventory_item.tsx index 9b82370d9..caf647552 100644 --- a/frontend/farm_designer/point_groups/group_inventory_item.tsx +++ b/frontend/farm_designer/point_groups/group_inventory_item.tsx @@ -2,6 +2,9 @@ import React from "react"; import { TaggedPointGroup, TaggedPoint } from "farmbot"; import { t } from "../../i18next_wrapper"; import { pointsSelectedByGroup } from "./criteria"; +import { ErrorBoundary } from "../../error_boundary"; +import { DevSettings } from "../../account/dev/dev_support"; +import { destroy } from "../../api/crud"; export interface GroupInventoryItemProps { group: TaggedPointGroup; @@ -12,15 +15,30 @@ export interface GroupInventoryItemProps { } export function GroupInventoryItem(props: GroupInventoryItemProps) { - const count = pointsSelectedByGroup(props.group, props.allPoints).length; + const { group } = props; + const delMode = DevSettings.quickDeleteEnabled(); return
+ onClick={delMode ? () => props.dispatch(destroy(group.uuid)) : props.onClick} + className={["group-search-item", + props.hovered ? "hovered" : "", + delMode ? "quick-del" : ""].join(" ")}> - {props.group.body.name} + {group.body.name} - - {t("{{count}} items", { count })} - + {t("? items")}}> + +
; } + +interface GroupItemCountProps { + group: TaggedPointGroup; + allPoints: TaggedPoint[]; +} + +const GroupItemCount = (props: GroupItemCountProps) => { + const count = pointsSelectedByGroup(props.group, props.allPoints).length; + return + {t("{{count}} items", { count })} + ; +}; diff --git a/frontend/farm_designer/point_groups/point_group_item.tsx b/frontend/farm_designer/point_groups/point_group_item.tsx index 830c25bbf..f242a420e 100644 --- a/frontend/farm_designer/point_groups/point_group_item.tsx +++ b/frontend/farm_designer/point_groups/point_group_item.tsx @@ -1,11 +1,12 @@ import * as React from "react"; import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons"; -import { setImgSrc, maybeGetCachedPlantIcon } from "../../open_farm/cached_crop"; +import { maybeGetCachedPlantIcon } from "../../open_farm/cached_crop"; import { setHoveredPlant } from "../map/actions"; import { TaggedPointGroup, uuid, TaggedPoint } from "farmbot"; import { overwrite } from "../../api/crud"; import { error } from "../../toast/toast"; import { t } from "../../i18next_wrapper"; +import { DEFAULT_WEED_ICON } from "../map/layers/weeds/garden_weed"; export interface PointGroupItemProps { point: TaggedPoint; @@ -25,9 +26,23 @@ const removePoint = (group: TaggedPointGroup, pointId: number) => { export const genericPointIcon = (color: string | undefined) => ` + fill='none' stroke-width='1.5' stroke='${color || "green"}'> + `; + +export const genericWeedIcon = (color: string | undefined) => + ` + + + + + + + `; export const OTHER_POINT_ICON = @@ -66,36 +81,46 @@ export class PointGroupItem maybeGetCachedIcon = (e: React.SyntheticEvent) => { const img = e.currentTarget; - switch (this.props.point.body.pointer_type) { - case "Plant": - const slug = this.props.point.body.openfarm_slug; - maybeGetCachedPlantIcon(slug, img, this.setIconState); - break; - case "GenericPointer": - const { color } = this.props.point.body.meta; - const pointIcon = svgToUrl(genericPointIcon(color)); - setImgSrc(img, pointIcon); - break; - default: - const otherIcon = svgToUrl(OTHER_POINT_ICON); - setImgSrc(img, otherIcon); - break; + if (this.props.point.body.pointer_type == "Plant") { + const slug = this.props.point.body.openfarm_slug; + maybeGetCachedPlantIcon(slug, img, this.setIconState); } }; + get initIcon() { + switch (this.props.point.body.pointer_type) { + case "Plant": + return DEFAULT_ICON; + case "GenericPointer": + const { color } = this.props.point.body.meta; + return svgToUrl(genericPointIcon(color)); + case "Weed": + const weedColor = this.props.point.body.meta.color; + return svgToUrl(genericWeedIcon(weedColor)); + default: + return svgToUrl(OTHER_POINT_ICON); + } + } + render() { return + {this.props.point.body.pointer_type == "Weed" && + } diff --git a/frontend/farm_designer/points/__tests__/create_points_test.tsx b/frontend/farm_designer/points/__tests__/create_points_test.tsx index 37236388d..f0cb7801f 100644 --- a/frontend/farm_designer/points/__tests__/create_points_test.tsx +++ b/frontend/farm_designer/points/__tests__/create_points_test.tsx @@ -23,30 +23,29 @@ import { deletePoints } from "../../../farmware/weed_detector/actions"; import { Actions } from "../../../constants"; import { clickButton } from "../../../__test_support__/helpers"; import { fakeState } from "../../../__test_support__/fake_state"; -import { CurrentPointPayl } from "../../interfaces"; +import { DrawnPointPayl } from "../../interfaces"; import { inputEvent } from "../../../__test_support__/fake_html_events"; import { cloneDeep } from "lodash"; -const FAKE_POINT: CurrentPointPayl = +const FAKE_POINT: DrawnPointPayl = ({ name: "My Point", cx: 13, cy: 22, r: 345, color: "red" }); describe("mapStateToProps", () => { - it("maps state to props", () => { + it("maps state to props: drawn point", () => { const state = fakeState(); - state - .resources - .consumers - .farm_designer - .currentPoint = FAKE_POINT; - const result = mapStateToProps(state); - const { currentPoint } = result; - expect(currentPoint).toBeTruthy(); - if (currentPoint) { - expect(currentPoint.cx).toEqual(13); - expect(currentPoint.cy).toEqual(22); - } else { - fail("Nope"); - } + state.resources.consumers.farm_designer.drawnPoint = FAKE_POINT; + const props = mapStateToProps(state); + expect(props.drawnPoint?.cx).toEqual(13); + expect(props.drawnPoint?.cy).toEqual(22); + }); + + it("maps state to props: drawn weed", () => { + const state = fakeState(); + state.resources.consumers.farm_designer.drawnPoint = undefined; + state.resources.consumers.farm_designer.drawnWeed = FAKE_POINT; + const props = mapStateToProps(state); + expect(props.drawnPoint?.cx).toEqual(13); + expect(props.drawnPoint?.cy).toEqual(22); }); }); @@ -57,17 +56,11 @@ describe("", () => { const fakeProps = (): CreatePointsProps => ({ dispatch: jest.fn(), - currentPoint: undefined, + drawnPoint: undefined, deviceY: 1.23, deviceX: 3.21 }); - const fakeInstance = () => { - const props = fakeProps(); - props.currentPoint = FAKE_POINT; - return new CreatePoints(props); - }; - it("renders for points", () => { mockPath = "/app/designer"; const wrapper = mount(); @@ -83,13 +76,15 @@ describe("", () => { }); it("updates specific fields", () => { - const i = fakeInstance(); + const p = fakeProps(); + p.drawnPoint = FAKE_POINT; + const i = new CreatePoints(p); i.updateValue("color")(inputEvent("cheerful hue")); - expect(i.props.currentPoint).toBeTruthy(); + expect(i.props.drawnPoint).toBeTruthy(); const expected = cloneDeep(FAKE_POINT); expected.color = "cheerful hue"; expect(i.props.dispatch).toHaveBeenCalledWith({ - type: "SET_CURRENT_POINT_DATA", + type: "SET_DRAWN_POINT_DATA", payload: expected, }); }); @@ -103,25 +98,27 @@ describe("", () => { }); it("loads default point data", () => { - const i = fakeInstance(); + const p = fakeProps(); + p.drawnPoint = FAKE_POINT; + const i = new CreatePoints(p); i.loadDefaultPoint(); expect(i.props.dispatch).toHaveBeenCalledWith({ - type: "SET_CURRENT_POINT_DATA", + type: "SET_DRAWN_POINT_DATA", payload: { name: "Created Point", color: "green", cx: 1, cy: 1, r: 15 }, }); }); - it("updates point name", () => { + it("updates weed name", () => { mockPath = "/app/designer/weeds/add"; const p = fakeProps(); - p.currentPoint = { cx: 0, cy: 0, r: 100 }; + p.drawnPoint = { cx: 0, cy: 0, r: 100 }; const panel = mount(); const wrapper = shallow(panel.instance().PointProperties()); wrapper.find("BlurableInput").first().simulate("commit", { currentTarget: { value: "new name" } }); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SET_CURRENT_POINT_DATA, payload: { + type: Actions.SET_DRAWN_WEED_DATA, payload: { cx: 0, cy: 0, r: 100, name: "new name", color: "red", } }); @@ -148,7 +145,7 @@ describe("", () => { expect(initSave).toHaveBeenCalledWith("Point", { meta: { color: "red", created_by: "farm-designer", type: "weed" }, name: "Created Weed", - pointer_type: "GenericPointer", + pointer_type: "Weed", radius: 30, x: 10, y: 20, z: 0, }); }); @@ -167,7 +164,8 @@ describe("", () => { p.dispatch = jest.fn(x => x()); button.simulate("click"); expect(deletePoints).toHaveBeenCalledWith("points", { - created_by: "farm-designer", type: "point" + pointer_type: "GenericPointer", + meta: { created_by: "farm-designer" } }); }); @@ -186,26 +184,41 @@ describe("", () => { p.dispatch = jest.fn(x => x()); button.simulate("click"); expect(deletePoints).toHaveBeenCalledWith("points", { - created_by: "farm-designer", type: "weed" + pointer_type: "Weed", + meta: { created_by: "farm-designer" } }); }); - it("changes color", () => { + it("changes point color", () => { const p = fakeProps(); - p.currentPoint = { cx: 0, cy: 0, r: 0 }; + p.drawnPoint = { cx: 0, cy: 0, r: 0 }; const wrapper = mount(); const PP = wrapper.instance().PointProperties; const component = shallow(); component.find("ColorPicker").simulate("change", "red"); expect(p.dispatch).toHaveBeenCalledWith({ payload: { color: "red", cx: 0, cy: 0, r: 0 }, - type: Actions.SET_CURRENT_POINT_DATA + type: Actions.SET_DRAWN_POINT_DATA + }); + }); + + it("changes weed color", () => { + mockPath = "/app/designer/weeds/add"; + const p = fakeProps(); + p.drawnPoint = { cx: 0, cy: 0, r: 0 }; + const wrapper = mount(); + const PP = wrapper.instance().PointProperties; + const component = shallow(); + component.find("ColorPicker").simulate("change", "red"); + expect(p.dispatch).toHaveBeenCalledWith({ + payload: { color: "red", cx: 0, cy: 0, r: 0 }, + type: Actions.SET_DRAWN_WEED_DATA }); }); it("updates value", () => { const p = fakeProps(); - p.currentPoint = { cx: 0, cy: 0, r: 0 }; + p.drawnPoint = { cx: 0, cy: 0, r: 0 }; const wrapper = shallow(); const PP = wrapper.instance().PointProperties; const component = shallow(); @@ -214,13 +227,13 @@ describe("", () => { }); expect(p.dispatch).toHaveBeenCalledWith({ payload: { cx: 10, cy: 0, r: 0, color: "green" }, - type: Actions.SET_CURRENT_POINT_DATA + type: Actions.SET_DRAWN_POINT_DATA }); }); it("fills the state with point data", () => { const p = fakeProps(); - p.currentPoint = { cx: 1, cy: 2, r: 3, color: "blue" }; + p.drawnPoint = { cx: 1, cy: 2, r: 3, color: "blue" }; const wrapper = shallow(); const i = wrapper.instance(); expect(i.state).toEqual({}); @@ -239,7 +252,7 @@ describe("", () => { jest.clearAllMocks(); wrapper.unmount(); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SET_CURRENT_POINT_DATA, + type: Actions.SET_DRAWN_POINT_DATA, payload: undefined }); }); diff --git a/frontend/farm_designer/points/__tests__/point_inventory_item_test.tsx b/frontend/farm_designer/points/__tests__/point_inventory_item_test.tsx index f2f51292f..475e5c220 100644 --- a/frontend/farm_designer/points/__tests__/point_inventory_item_test.tsx +++ b/frontend/farm_designer/points/__tests__/point_inventory_item_test.tsx @@ -1,4 +1,12 @@ -jest.mock("../../../history", () => ({ push: jest.fn() })); +let mockPath = "/app/designer/points"; +jest.mock("../../../history", () => ({ + push: jest.fn(), + getPathArray: () => mockPath.split("/"), +})); + +jest.mock("../../map/actions", () => ({ + mapPointClickAction: jest.fn(() => jest.fn()), +})); import * as React from "react"; import { shallow } from "enzyme"; @@ -8,21 +16,41 @@ import { import { fakePoint } from "../../../__test_support__/fake_state/resources"; import { push } from "../../../history"; import { Actions } from "../../../constants"; +import { mapPointClickAction } from "../../map/actions"; describe(" />", () => { const fakeProps = (): PointInventoryItemProps => ({ tpp: fakePoint(), dispatch: jest.fn(), hovered: false, - navName: "points", }); it("navigates to point", () => { + mockPath = "/app/designer/points"; const p = fakeProps(); p.tpp.body.id = 1; const wrapper = shallow(); wrapper.simulate("click"); + expect(mapPointClickAction).not.toHaveBeenCalled(); expect(push).toHaveBeenCalledWith("/app/designer/points/1"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: [p.tpp.uuid], + }); + }); + + it("removes item in box select mode", () => { + mockPath = "/app/designer/plants/select"; + const p = fakeProps(); + const wrapper = shallow(); + wrapper.simulate("click"); + expect(mapPointClickAction).toHaveBeenCalledWith(expect.any(Function), + p.tpp.uuid); + expect(push).not.toHaveBeenCalled(); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: undefined, + }); }); it("hovers point", () => { diff --git a/frontend/farm_designer/points/create_points.tsx b/frontend/farm_designer/points/create_points.tsx index 611bbe7db..62d3a0083 100644 --- a/frontend/farm_designer/points/create_points.tsx +++ b/frontend/farm_designer/points/create_points.tsx @@ -11,10 +11,10 @@ import { BlurableInput, ColorPicker, } from "../../ui/index"; -import { CurrentPointPayl } from "../interfaces"; +import { DrawnPointPayl } from "../interfaces"; import { Actions, Content } from "../../constants"; import { deletePoints } from "../../farmware/weed_detector/actions"; -import { GenericPointer } from "farmbot/dist/resources/api_resources"; +import { GenericPointer, WeedPointer } from "farmbot/dist/resources/api_resources"; import { DesignerPanel, DesignerPanelHeader, @@ -29,9 +29,10 @@ import { success } from "../../toast/toast"; export function mapStateToProps(props: Everything): CreatePointsProps { const { position } = props.bot.hardware.location_data; + const { drawnPoint, drawnWeed } = props.resources.consumers.farm_designer; return { dispatch: props.dispatch, - currentPoint: props.resources.consumers.farm_designer.currentPoint, + drawnPoint: drawnPoint || drawnWeed, deviceX: position.x || 0, deviceY: position.y || 0, }; @@ -39,14 +40,14 @@ export function mapStateToProps(props: Everything): CreatePointsProps { export interface CreatePointsProps { dispatch: Function; - currentPoint: CurrentPointPayl | undefined; + drawnPoint: DrawnPointPayl | undefined; deviceX: number; deviceY: number; } -type CreatePointsState = Partial; +type CreatePointsState = Partial; -const DEFAULTS: CurrentPointPayl = { +const DEFAULTS: DrawnPointPayl = { name: undefined, cx: 1, cy: 1, @@ -61,10 +62,10 @@ export class RawCreatePoints this.state = {}; } - attr = (key: T, - fallback = DEFAULTS[key]): CurrentPointPayl[T] => { - const p = this.props.currentPoint; - const userValue = this.state[key] as CurrentPointPayl[T] | undefined; + attr = (key: T, + fallback = DEFAULTS[key]): DrawnPointPayl[T] => { + const p = this.props.drawnPoint; + const userValue = this.state[key] as DrawnPointPayl[T] | undefined; const propValue = p ? p[key] : fallback; if (typeof userValue === "undefined") { return propValue; @@ -81,7 +82,7 @@ export class RawCreatePoints get defaultColor() { return this.panel == "weeds" ? "red" : "green"; } - getPointData = (): CurrentPointPayl => { + getPointData = (): DrawnPointPayl => { return { name: this.attr("name"), cx: this.attr("cx"), @@ -93,7 +94,9 @@ export class RawCreatePoints cancel = () => { this.props.dispatch({ - type: Actions.SET_CURRENT_POINT_DATA, + type: this.panel == "weeds" + ? Actions.SET_DRAWN_WEED_DATA + : Actions.SET_DRAWN_POINT_DATA, payload: undefined }); this.setState({ @@ -106,14 +109,16 @@ export class RawCreatePoints loadDefaultPoint = () => { this.props.dispatch({ - type: Actions.SET_CURRENT_POINT_DATA, + type: this.panel == "weeds" + ? Actions.SET_DRAWN_WEED_DATA + : Actions.SET_DRAWN_POINT_DATA, payload: { name: this.defaultName, cx: DEFAULTS.cx, cy: DEFAULTS.cy, r: DEFAULTS.r, color: this.defaultColor, - } as CurrentPointPayl + } as DrawnPointPayl }); } @@ -129,7 +134,7 @@ export class RawCreatePoints updateValue = (key: keyof CreatePointsState) => { return (e: React.SyntheticEvent) => { const { value } = e.currentTarget; - if (this.props.currentPoint) { + if (this.props.drawnPoint) { const point = this.getPointData(); switch (key) { case "name": @@ -143,7 +148,9 @@ export class RawCreatePoints point[key] = intValue; } this.props.dispatch({ - type: Actions.SET_CURRENT_POINT_DATA, + type: this.panel == "weeds" + ? Actions.SET_DRAWN_WEED_DATA + : Actions.SET_DRAWN_POINT_DATA, payload: point }); } @@ -155,7 +162,9 @@ export class RawCreatePoints const point = this.getPointData(); point.color = color; this.props.dispatch({ - type: Actions.SET_CURRENT_POINT_DATA, + type: this.panel == "weeds" + ? Actions.SET_DRAWN_WEED_DATA + : Actions.SET_DRAWN_POINT_DATA, payload: point }); } @@ -163,8 +172,8 @@ export class RawCreatePoints get panel() { return getPathArray()[3] || "points"; } createPoint = () => { - const body: GenericPointer = { - pointer_type: "GenericPointer", + const body: GenericPointer | WeedPointer = { + pointer_type: this.panel == "weeds" ? "Weed" : "GenericPointer", name: this.attr("name") || this.defaultName, meta: { color: this.attr("color") || this.defaultColor, @@ -247,8 +256,9 @@ export class RawCreatePoints - DeleteAllPoints = (type: "point" | "weed") => - + DeleteAllPoints = (type: "point" | "weed") => { + const meta = { created_by: "farm-designer" }; + return

{type === "weed" @@ -261,7 +271,8 @@ export class RawCreatePoints ? t("Delete all the weeds you have created?") : t("Delete all the points you have created?"))) { this.props.dispatch(deletePoints("points", { - created_by: "farm-designer", type, + pointer_type: type === "weed" ? "Weed" : "GenericPointer", + meta, })); this.cancel(); } @@ -271,7 +282,8 @@ export class RawCreatePoints : t("Delete all created points")}

-
+
; + }; render() { const panelType = this.panel == "weeds" ? Panel.Weeds : Panel.Points; diff --git a/frontend/farm_designer/points/point_edit_actions.tsx b/frontend/farm_designer/points/point_edit_actions.tsx index 4a12f3fc6..a0cec53b3 100644 --- a/frontend/farm_designer/points/point_edit_actions.tsx +++ b/frontend/farm_designer/points/point_edit_actions.tsx @@ -3,16 +3,20 @@ import { t } from "../../i18next_wrapper"; import { getDevice } from "../../device"; import { destroy, edit, save } from "../../api/crud"; import { ResourceColor } from "../../interfaces"; -import { TaggedGenericPointer } from "farmbot"; +import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; import { ListItem } from "../plants/plant_panel"; import { round } from "lodash"; import { Row, Col, BlurableInput, ColorPicker } from "../../ui"; import { parseIntInput } from "../../util"; import { UUID } from "../../resources/interfaces"; +type PointUpdate = + Partial; + export const updatePoint = - (point: TaggedGenericPointer | undefined, dispatch: Function) => - (update: Partial) => { + (point: TaggedGenericPointer | TaggedWeedPointer | undefined, + dispatch: Function) => + (update: PointUpdate) => { if (point) { dispatch(edit(point, update)); dispatch(save(point.uuid)); @@ -20,18 +24,21 @@ export const updatePoint = }; export interface EditPointPropertiesProps { - point: TaggedGenericPointer; - updatePoint(update: Partial): void; + point: TaggedGenericPointer | TaggedWeedPointer; + updatePoint(update: PointUpdate): void; } export const EditPointProperties = (props: EditPointPropertiesProps) =>
  • -
    + -
    + +
  • radius={props.point.body.radius} updatePoint={props.updatePoint} /> - - -
; export interface PointActionsProps { @@ -76,13 +78,13 @@ export const PointActions = ({ x, y, z, uuid, dispatch }: PointActionsProps) => ; export interface EditPointNameProps { - updatePoint(update: Partial): void; + updatePoint(update: PointUpdate): void; name: string; } export const EditPointName = (props: EditPointNameProps) => - - +
+ value={props.name} onCommit={e => props.updatePoint({ name: e.currentTarget.value })} /> - ; +
; export interface EditPointLocationProps { - updatePoint(update: Partial): void; + updatePoint(update: PointUpdate): void; xyLocation: Record<"x" | "y", number>; } @@ -114,7 +116,7 @@ export const EditPointLocation = (props: EditPointLocationProps) =>
; export interface EditPointRadiusProps { - updatePoint(update: Partial): void; + updatePoint(update: PointUpdate): void; radius: number; } @@ -134,13 +136,15 @@ export const EditPointRadius = (props: EditPointRadiusProps) => ; export interface EditPointColorProps { - updatePoint(update: Partial): void; + updatePoint(update: PointUpdate): void; color: string | undefined; } export const EditPointColor = (props: EditPointColorProps) => - - props.updatePoint({ meta: { color } })} /> - ; +
+ + props.updatePoint({ meta: { color } })} /> + +
; diff --git a/frontend/farm_designer/points/point_inventory.tsx b/frontend/farm_designer/points/point_inventory.tsx index bfa4c1d0a..4412413a8 100644 --- a/frontend/farm_designer/points/point_inventory.tsx +++ b/frontend/farm_designer/points/point_inventory.tsx @@ -13,7 +13,6 @@ import { import { selectAllGenericPointers } from "../../resources/selectors"; import { TaggedGenericPointer } from "farmbot"; import { t } from "../../i18next_wrapper"; -import { isAWeed } from "./weeds_inventory"; export interface PointsProps { genericPoints: TaggedGenericPointer[]; @@ -29,8 +28,7 @@ export function mapStateToProps(props: Everything): PointsProps { const { hoveredPoint } = props.resources.consumers.farm_designer; return { genericPoints: selectAllGenericPointers(props.resources.index) - .filter(x => !x.body.discarded_at) - .filter(x => !isAWeed(x.body.name, x.body.meta.type)), + .filter(x => !x.body.discarded_at), dispatch: props.dispatch, hoveredPoint, }; @@ -65,7 +63,6 @@ export class RawPoints extends React.Component { .includes(this.state.searchTerm.toLowerCase())) .map(p => )} diff --git a/frontend/farm_designer/points/point_inventory_item.tsx b/frontend/farm_designer/points/point_inventory_item.tsx index 0bafcad8b..f5979c5b7 100644 --- a/frontend/farm_designer/points/point_inventory_item.tsx +++ b/frontend/farm_designer/points/point_inventory_item.tsx @@ -3,12 +3,15 @@ import { TaggedGenericPointer } from "farmbot"; import { Saucer } from "../../ui"; import { Actions } from "../../constants"; import { push } from "../../history"; +import { t } from "../../i18next_wrapper"; +import { getMode } from "../map/util"; +import { Mode } from "../map/interfaces"; +import { mapPointClickAction } from "../map/actions"; export interface PointInventoryItemProps { tpp: TaggedGenericPointer; dispatch: Function; hovered: boolean; - navName: "points" | "weeds"; } // The individual points that show up in the farm designer sub nav. @@ -29,13 +32,15 @@ export class PointInventoryItem extends }; const click = () => { - push(`/app/designer/${this.props.navName}/${pointId}`); - dispatch({ type: Actions.TOGGLE_HOVERED_POINT, payload: [tpp.uuid] }); + if (getMode() == Mode.boxSelect) { + mapPointClickAction(dispatch, tpp.uuid)(); + toggle("leave"); + } else { + push(`/app/designer/points/${pointId}`); + dispatch({ type: Actions.TOGGLE_HOVERED_POINT, payload: [tpp.uuid] }); + } }; - // Name given from OpenFarm's API. - const label = point.name || "Unknown plant"; - return
- {label} + {point.name || t("Untitled point")}

{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`} diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index 0c601afb1..81909dee2 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -1,14 +1,16 @@ -import { CropLiveSearchResult, CurrentPointPayl } from "./interfaces"; +import { CropLiveSearchResult, DrawnPointPayl, DrawnWeedPayl } from "./interfaces"; import { generateReducer } from "../redux/generate_reducer"; import { DesignerState, HoveredPlantPayl } from "./interfaces"; import { cloneDeep } from "lodash"; -import { TaggedResource } from "farmbot"; +import { TaggedResource, PointType } from "farmbot"; import { Actions } from "../constants"; import { BotPosition } from "../devices/interfaces"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; +import { UUID } from "../resources/interfaces"; export const initialState: DesignerState = { - selectedPlants: undefined, + selectedPoints: undefined, + selectionPointType: undefined, hoveredPlant: { plantUUID: undefined, icon: "" @@ -20,7 +22,8 @@ export const initialState: DesignerState = { cropSearchResults: [], cropSearchInProgress: false, chosenLocation: { x: undefined, y: undefined, z: undefined }, - currentPoint: undefined, + drawnPoint: undefined, + drawnWeed: undefined, openedSavedGarden: undefined, tryGroupSortType: undefined, editGroupAreaInMap: false, @@ -41,10 +44,15 @@ export const designer = generateReducer(initialState) s.cropSearchInProgress = false; return s; }) - .add(Actions.SELECT_PLANT, (s, { payload }) => { - s.selectedPlants = payload; + .add(Actions.SELECT_POINT, (s, { payload }) => { + s.selectedPoints = payload; return s; }) + .add( + Actions.SET_SELECTION_POINT_TYPE, (s, { payload }) => { + s.selectionPointType = payload; + return s; + }) .add(Actions.TOGGLE_HOVERED_PLANT, (s, { payload }) => { s.hoveredPlant = payload; return s; @@ -61,12 +69,20 @@ export const designer = generateReducer(initialState) s.hoveredToolSlot = payload; return s; }) - .add( - Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => { + .add( + Actions.SET_DRAWN_POINT_DATA, (s, { payload }) => { const { color } = (!payload || !payload.color) ? - (s.currentPoint || { color: "green" }) : payload; - s.currentPoint = payload; - s.currentPoint && (s.currentPoint.color = color); + (s.drawnPoint || { color: "green" }) : payload; + s.drawnPoint = payload; + s.drawnPoint && (s.drawnPoint.color = color); + return s; + }) + .add( + Actions.SET_DRAWN_WEED_DATA, (s, { payload }) => { + const { color } = (!payload || !payload.color) ? + (s.drawnWeed || { color: "red" }) : payload; + s.drawnWeed = payload; + s.drawnWeed && (s.drawnWeed.color = color); return s; }) .add(Actions.OF_SEARCH_RESULTS_OK, (s, a) => { @@ -75,7 +91,7 @@ export const designer = generateReducer(initialState) return s; }) .add(Actions.DESTROY_RESOURCE_OK, (s) => { - s.selectedPlants = undefined; + s.selectedPoints = undefined; s.hoveredPlant = { plantUUID: undefined, icon: "" }; return s; }) diff --git a/frontend/farm_designer/saved_gardens/__tests__/saved_gardens_test.tsx b/frontend/farm_designer/saved_gardens/__tests__/saved_gardens_test.tsx index 8ffe8ae67..18387a72c 100644 --- a/frontend/farm_designer/saved_gardens/__tests__/saved_gardens_test.tsx +++ b/frontend/farm_designer/saved_gardens/__tests__/saved_gardens_test.tsx @@ -125,7 +125,7 @@ describe("", () => { clickButton(wrapper, 1, "edit"); expect(history.push).toHaveBeenCalledWith("/app/designer/plants"); expect(dispatch).toHaveBeenCalledWith({ - type: Actions.SELECT_PLANT, + type: Actions.SELECT_POINT, payload: undefined }); }); diff --git a/frontend/farm_designer/state_to_props.ts b/frontend/farm_designer/state_to_props.ts index 09c29706b..3ca29cfb9 100644 --- a/frontend/farm_designer/state_to_props.ts +++ b/frontend/farm_designer/state_to_props.ts @@ -14,6 +14,7 @@ import { selectAllPointGroups, getDeviceAccountSettings, maybeFindToolById, + selectAllWeedPointers, } from "../resources/selectors"; import { validBotLocationData, validFwConfig, unpackUUID } from "../util"; import { getWebAppConfigValue } from "../config_storage/actions"; @@ -44,10 +45,10 @@ export function mapStateToProps(props: Everything): Props { const plants = getPlants(props.resources); const findPlant = plantFinder(plants); - const { selectedPlants } = props.resources.consumers.farm_designer; - const selectedPlant = selectedPlants ? findPlant(selectedPlants[0]) : undefined; - const { plantUUID } = props.resources.consumers.farm_designer.hoveredPlant; + const { selectedPoints } = props.resources.consumers.farm_designer; + const selectedPlant = selectedPoints ? findPlant(selectedPoints[0]) : undefined; + const { plantUUID } = props.resources.consumers.farm_designer.hoveredPlant; const hoveredPlant = findPlant(plantUUID); const getConfigValue = getWebAppConfigValue(() => props); @@ -55,6 +56,7 @@ export function mapStateToProps(props: Everything): Props { const genericPoints = getConfigValue(BooleanSetting.show_historic_points) ? allGenericPoints : allGenericPoints.filter(x => !x.body.discarded_at); + const weeds = selectAllWeedPointers(props.resources.index); const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index)); const { mcu_params } = props.bot.hardware; @@ -113,6 +115,7 @@ export function mapStateToProps(props: Everything): Props { selectedPlant, designer: props.resources.consumers.farm_designer, genericPoints, + weeds, allPoints: selectAllPoints(props.resources.index), toolSlots: joinToolsAndSlot(props.resources.index), hoveredPlant, diff --git a/frontend/farm_designer/tools/__tests__/index_test.tsx b/frontend/farm_designer/tools/__tests__/index_test.tsx index 13362abed..66a735318 100644 --- a/frontend/farm_designer/tools/__tests__/index_test.tsx +++ b/frontend/farm_designer/tools/__tests__/index_test.tsx @@ -1,6 +1,7 @@ +let mockPath = "/app/designer/tools"; jest.mock("../../../history", () => ({ history: { push: jest.fn() }, - getPathArray: () => "/app/designer/tools".split("/"), + getPathArray: () => mockPath.split("/"), })); jest.mock("../../../api/crud", () => ({ @@ -8,6 +9,10 @@ jest.mock("../../../api/crud", () => ({ save: jest.fn(), })); +jest.mock("../../map/actions", () => ({ + mapPointClickAction: jest.fn(() => jest.fn()), +})); + const mockDevice = { readPin: jest.fn(() => Promise.resolve()) }; jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); @@ -28,6 +33,7 @@ import { Content, Actions } from "../../../constants"; import { edit, save } from "../../../api/crud"; import { ToolSelection } from "../tool_slot_edit_components"; import { ToolsProps } from "../interfaces"; +import { mapPointClickAction } from "../../map/actions"; describe("", () => { const fakeProps = (): ToolsProps => ({ @@ -245,4 +251,37 @@ describe("", () => { wrapper.find(".tool-selection-wrapper").first().simulate("click", e); expect(e.stopPropagation).toHaveBeenCalled(); }); + + it("shows tool name", () => { + const p = fakeProps(); + p.hideDropdown = true; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("empty"); + }); + + it("opens tool slot", () => { + mockPath = "/app/designer/tool-slots"; + const p = fakeProps(); + p.toolSlot.body.id = 1; + const wrapper = shallow(); + wrapper.find("div").first().simulate("click"); + expect(mapPointClickAction).not.toHaveBeenCalled(); + expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1"); + expect(p.dispatch).not.toHaveBeenCalled(); + }); + + it("removes item in box select mode", () => { + mockPath = "/app/designer/plants/select"; + const p = fakeProps(); + p.toolSlot.body.id = 1; + const wrapper = shallow(); + wrapper.find("div").first().simulate("click"); + expect(mapPointClickAction).toHaveBeenCalledWith(expect.any(Function), + p.toolSlot.uuid); + expect(history.push).not.toHaveBeenCalled(); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.HOVER_TOOL_SLOT, + payload: undefined, + }); + }); }); diff --git a/frontend/farm_designer/tools/index.tsx b/frontend/farm_designer/tools/index.tsx index 8b48a8ac9..36648d70c 100644 --- a/frontend/farm_designer/tools/index.tsx +++ b/frontend/farm_designer/tools/index.tsx @@ -26,6 +26,9 @@ import { hasUTM } from "../../devices/components/firmware_hardware_support"; import { ToolsProps, ToolsState } from "./interfaces"; import { mapStateToProps } from "./state_to_props"; import { BotOriginQuadrant } from "../interfaces"; +import { mapPointClickAction } from "../map/actions"; +import { getMode } from "../map/util"; +import { Mode } from "../map/interfaces"; const toolStatus = (value: number | undefined): string => { switch (value) { @@ -207,6 +210,7 @@ export interface ToolSlotInventoryItemProps { isActive(id: number | undefined): boolean; xySwap: boolean; quadrant: BotOriginQuadrant; + hideDropdown?: boolean; } export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { @@ -215,7 +219,14 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { .filter(tool => tool.body.id == tool_id)[0]?.body.name; return

history.push(`/app/designer/tool-slots/${id}`)} + onClick={() => { + if (getMode() == Mode.boxSelect) { + mapPointClickAction(props.dispatch, props.toolSlot.uuid)(); + props.dispatch(setToolHover(undefined)); + } else { + history.push(`/app/designer/tool-slots/${id}`); + } + }} onMouseEnter={() => props.dispatch(setToolHover(props.toolSlot.uuid))} onMouseLeave={() => props.dispatch(setToolHover(undefined))}> @@ -226,20 +237,24 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { xySwap={props.xySwap} quadrant={props.quadrant} /> -
e.stopPropagation()}> - tool.body.id == tool_id)[0]} - onChange={update => { - props.dispatch(edit(props.toolSlot, update)); - props.dispatch(save(props.toolSlot.uuid)); - }} - isActive={props.isActive} - filterSelectedTool={false} - filterActiveTools={true} /> -
+ {props.hideDropdown + ? + {toolName || t("Empty")} + + :
e.stopPropagation()}> + tool.body.id == tool_id)[0]} + onChange={update => { + props.dispatch(edit(props.toolSlot, update)); + props.dispatch(save(props.toolSlot.uuid)); + }} + isActive={props.isActive} + filterSelectedTool={false} + filterActiveTools={true} /> +
}

diff --git a/frontend/farm_designer/weeds/__tests__/weed_inventory_item_test.tsx b/frontend/farm_designer/weeds/__tests__/weed_inventory_item_test.tsx new file mode 100644 index 000000000..125b0e697 --- /dev/null +++ b/frontend/farm_designer/weeds/__tests__/weed_inventory_item_test.tsx @@ -0,0 +1,81 @@ +let mockPath = "/app/designer/weeds"; +jest.mock("../../../history", () => ({ + push: jest.fn(), + getPathArray: () => mockPath.split("/"), +})); + +jest.mock("../../map/actions", () => ({ + mapPointClickAction: jest.fn(() => jest.fn()), +})); + +import * as React from "react"; +import { shallow } from "enzyme"; +import { + WeedInventoryItem, WeedInventoryItemProps, +} from "../weed_inventory_item"; +import { fakeWeed } from "../../../__test_support__/fake_state/resources"; +import { push } from "../../../history"; +import { Actions } from "../../../constants"; +import { mapPointClickAction } from "../../map/actions"; + +describe(" />", () => { + const fakeProps = (): WeedInventoryItemProps => ({ + tpp: fakeWeed(), + dispatch: jest.fn(), + hovered: false, + }); + + it("navigates to weed", () => { + const p = fakeProps(); + p.tpp.body.id = 1; + const wrapper = shallow(); + wrapper.simulate("click"); + expect(mapPointClickAction).not.toHaveBeenCalled(); + expect(push).toHaveBeenCalledWith("/app/designer/weeds/1"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: [p.tpp.uuid], + }); + }); + + it("removes item in box select mode", () => { + mockPath = "/app/designer/plants/select"; + const p = fakeProps(); + const wrapper = shallow(); + wrapper.simulate("click"); + expect(mapPointClickAction).toHaveBeenCalledWith(expect.any(Function), + p.tpp.uuid); + expect(push).not.toHaveBeenCalled(); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: undefined, + }); + }); + + it("hovers weed", () => { + const p = fakeProps(); + p.tpp.body.id = 1; + const wrapper = shallow(); + wrapper.simulate("mouseEnter"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, payload: p.tpp.uuid + }); + }); + + it("shows hovered", () => { + const p = fakeProps(); + p.hovered = true; + const wrapper = shallow(); + expect(wrapper.hasClass("hovered")).toBeTruthy(); + }); + + it("un-hovers weed", () => { + const p = fakeProps(); + p.tpp.body.id = 1; + const wrapper = shallow(); + wrapper.simulate("mouseLeave"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_HOVERED_POINT, payload: undefined + }); + }); +}); diff --git a/frontend/farm_designer/points/__tests__/weeds_edit_test.tsx b/frontend/farm_designer/weeds/__tests__/weeds_edit_test.tsx similarity index 80% rename from frontend/farm_designer/points/__tests__/weeds_edit_test.tsx rename to frontend/farm_designer/weeds/__tests__/weeds_edit_test.tsx index 9569ac587..fdfdc7516 100644 --- a/frontend/farm_designer/points/__tests__/weeds_edit_test.tsx +++ b/frontend/farm_designer/weeds/__tests__/weeds_edit_test.tsx @@ -9,7 +9,7 @@ import { mount, shallow } from "enzyme"; import { RawEditWeed as EditWeed, EditWeedProps, mapStateToProps, } from "../weeds_edit"; -import { fakePoint } from "../../../__test_support__/fake_state/resources"; +import { fakeWeed } from "../../../__test_support__/fake_state/resources"; import { fakeState } from "../../../__test_support__/fake_state"; import { buildResourceIndex, @@ -32,9 +32,9 @@ describe("", () => { it("renders", () => { mockPath = "/app/designer/weeds/1"; const p = fakeProps(); - const point = fakePoint(); - point.body.id = 1; - p.findPoint = () => point; + const weed = fakeWeed(); + weed.body.id = 1; + p.findPoint = () => weed; const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("edit"); }); @@ -42,9 +42,9 @@ describe("", () => { it("goes back", () => { mockPath = "/app/designer/weeds/1"; const p = fakeProps(); - const point = fakePoint(); - point.body.id = 1; - p.findPoint = () => point; + const weed = fakeWeed(); + weed.body.id = 1; + p.findPoint = () => weed; const wrapper = shallow(); wrapper.find(DesignerPanelHeader).simulate("back"); expect(p.dispatch).toHaveBeenCalledWith({ @@ -56,10 +56,10 @@ describe("", () => { describe("mapStateToProps()", () => { it("returns props", () => { const state = fakeState(); - const point = fakePoint(); - point.body.id = 1; - state.resources = buildResourceIndex([point]); + const weed = fakeWeed(); + weed.body.id = 1; + state.resources = buildResourceIndex([weed]); const props = mapStateToProps(state); - expect(props.findPoint(1)).toEqual(point); + expect(props.findPoint(1)).toEqual(weed); }); }); diff --git a/frontend/farm_designer/points/__tests__/weeds_inventory_test.tsx b/frontend/farm_designer/weeds/__tests__/weeds_inventory_test.tsx similarity index 83% rename from frontend/farm_designer/points/__tests__/weeds_inventory_test.tsx rename to frontend/farm_designer/weeds/__tests__/weeds_inventory_test.tsx index 638f79066..b76423aef 100644 --- a/frontend/farm_designer/points/__tests__/weeds_inventory_test.tsx +++ b/frontend/farm_designer/weeds/__tests__/weeds_inventory_test.tsx @@ -4,11 +4,11 @@ import { RawWeeds as Weeds, WeedsProps, mapStateToProps, } from "../weeds_inventory"; import { fakeState } from "../../../__test_support__/fake_state"; -import { fakePoint } from "../../../__test_support__/fake_state/resources"; +import { fakeWeed } from "../../../__test_support__/fake_state/resources"; describe(" />", () => { const fakeProps = (): WeedsProps => ({ - genericPoints: [], + weeds: [], dispatch: jest.fn(), hoveredPoint: undefined, }); @@ -27,9 +27,9 @@ describe(" />", () => { it("filters points", () => { const p = fakeProps(); - p.genericPoints = [fakePoint(), fakePoint()]; - p.genericPoints[0].body.name = "weed 0"; - p.genericPoints[1].body.name = "weed 1"; + p.weeds = [fakeWeed(), fakeWeed()]; + p.weeds[0].body.name = "weed 0"; + p.weeds[1].body.name = "weed 1"; const wrapper = mount(); wrapper.setState({ searchTerm: "0" }); expect(wrapper.text()).toContain("weed 0"); diff --git a/frontend/farm_designer/weeds/weed_inventory_item.tsx b/frontend/farm_designer/weeds/weed_inventory_item.tsx new file mode 100644 index 000000000..0eb19b476 --- /dev/null +++ b/frontend/farm_designer/weeds/weed_inventory_item.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import { TaggedWeedPointer } from "farmbot"; +import { Actions } from "../../constants"; +import { push } from "../../history"; +import { t } from "../../i18next_wrapper"; +import { DEFAULT_WEED_ICON } from "../map/layers/weeds/garden_weed"; +import { svgToUrl } from "../../open_farm/icons"; +import { genericWeedIcon } from "../point_groups/point_group_item"; +import { getMode } from "../map/util"; +import { Mode } from "../map/interfaces"; +import { mapPointClickAction } from "../map/actions"; + +export interface WeedInventoryItemProps { + tpp: TaggedWeedPointer; + dispatch: Function; + hovered: boolean; +} + +export class WeedInventoryItem extends + React.Component { + + render() { + const weed = this.props.tpp.body; + const { tpp, dispatch } = this.props; + const weedId = (weed.id || "ERR_NO_POINT_ID").toString(); + + const toggle = (action: "enter" | "leave") => { + const isEnter = action === "enter"; + dispatch({ + type: Actions.TOGGLE_HOVERED_POINT, + payload: isEnter ? tpp.uuid : undefined + }); + }; + + const click = () => { + if (getMode() == Mode.boxSelect) { + mapPointClickAction(dispatch, tpp.uuid)(); + toggle("leave"); + } else { + push(`/app/designer/weeds/${weedId}`); + dispatch({ type: Actions.TOGGLE_HOVERED_POINT, payload: [tpp.uuid] }); + } + }; + + return

toggle("enter")} + onMouseLeave={() => toggle("leave")} + onClick={click}> + + + + + + {weed.name || t("Untitled weed")} + +

+ {`(${weed.x}, ${weed.y}) ⌀${weed.radius * 2}`} +

+
; + } +} diff --git a/frontend/farm_designer/points/weeds_edit.tsx b/frontend/farm_designer/weeds/weeds_edit.tsx similarity index 87% rename from frontend/farm_designer/points/weeds_edit.tsx rename to frontend/farm_designer/weeds/weeds_edit.tsx index 04c4ac70f..e15cbf5b2 100644 --- a/frontend/farm_designer/points/weeds_edit.tsx +++ b/frontend/farm_designer/weeds/weeds_edit.tsx @@ -6,22 +6,22 @@ import { import { t } from "../../i18next_wrapper"; import { history, getPathArray } from "../../history"; import { Everything } from "../../interfaces"; -import { TaggedGenericPointer } from "farmbot"; -import { maybeFindGenericPointerById } from "../../resources/selectors"; +import { TaggedWeedPointer } from "farmbot"; +import { maybeFindWeedPointerById } from "../../resources/selectors"; import { Panel } from "../panel_header"; import { EditPointProperties, PointActions, updatePoint, -} from "./point_edit_actions"; +} from "../points/point_edit_actions"; import { Actions } from "../../constants"; export interface EditWeedProps { dispatch: Function; - findPoint(id: number): TaggedGenericPointer | undefined; + findPoint(id: number): TaggedWeedPointer | undefined; } export const mapStateToProps = (props: Everything): EditWeedProps => ({ dispatch: props.dispatch, - findPoint: id => maybeFindGenericPointerById(props.resources.index, id), + findPoint: id => maybeFindWeedPointerById(props.resources.index, id), }); export class RawEditWeed extends React.Component { diff --git a/frontend/farm_designer/points/weeds_inventory.tsx b/frontend/farm_designer/weeds/weeds_inventory.tsx similarity index 75% rename from frontend/farm_designer/points/weeds_inventory.tsx rename to frontend/farm_designer/weeds/weeds_inventory.tsx index f4517bac2..50b3d1c0d 100644 --- a/frontend/farm_designer/points/weeds_inventory.tsx +++ b/frontend/farm_designer/weeds/weeds_inventory.tsx @@ -10,12 +10,12 @@ import { DesignerPanel, DesignerPanelContent, DesignerPanelTop, } from "../designer_panel"; import { t } from "../../i18next_wrapper"; -import { TaggedGenericPointer } from "farmbot"; -import { selectAllGenericPointers } from "../../resources/selectors"; -import { PointInventoryItem } from "./point_inventory_item"; +import { TaggedWeedPointer } from "farmbot"; +import { selectAllWeedPointers } from "../../resources/selectors"; +import { WeedInventoryItem } from "./weed_inventory_item"; export interface WeedsProps { - genericPoints: TaggedGenericPointer[]; + weeds: TaggedWeedPointer[]; dispatch: Function; hoveredPoint: string | undefined; } @@ -24,13 +24,8 @@ interface WeedsState { searchTerm: string; } -export const isAWeed = (pointName: string, type?: string) => - type == "weed" || pointName.toLowerCase().includes("weed"); - export const mapStateToProps = (props: Everything): WeedsProps => ({ - genericPoints: selectAllGenericPointers(props.resources.index) - .filter(x => !x.body.discarded_at) - .filter(x => isAWeed(x.body.name, x.body.meta.type)), + weeds: selectAllWeedPointers(props.resources.index), dispatch: props.dispatch, hoveredPoint: props.resources.consumers.farm_designer.hoveredPoint, }); @@ -54,17 +49,16 @@ export class RawWeeds extends React.Component { 0} + notEmpty={this.props.weeds.length > 0} graphic={EmptyStateGraphic.weeds} title={t("No weeds yet.")} text={Content.NO_WEEDS} colorScheme={"weeds"}> - {this.props.genericPoints + {this.props.weeds .filter(p => p.body.name.toLowerCase() .includes(this.state.searchTerm.toLowerCase())) - .map(p => )} diff --git a/frontend/farmware/weed_detector/__tests__/actions_tests.ts b/frontend/farmware/weed_detector/__tests__/actions_tests.ts index e321cd544..ab8386018 100644 --- a/frontend/farmware/weed_detector/__tests__/actions_tests.ts +++ b/frontend/farmware/weed_detector/__tests__/actions_tests.ts @@ -62,7 +62,8 @@ describe("deletePoints()", () => { mockDelete = Promise.resolve(); mockData = [{ id: 1 }, { id: 2 }, { id: 3 }]; const dispatch = jest.fn(); - await deletePoints("weeds", { created_by: "plant-detection" })(dispatch, jest.fn()); + const query = { meta: { created_by: "plant-detection" } }; + await deletePoints("weeds", query)(dispatch, jest.fn()); expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search", { meta: { created_by: "plant-detection" } }); await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3"); @@ -80,7 +81,8 @@ describe("deletePoints()", () => { mockDelete = Promise.reject("error"); mockData = [{ id: 1 }, { id: 2 }, { id: 3 }]; const dispatch = jest.fn(); - await deletePoints("weeds", { created_by: "plant-detection" })(dispatch, jest.fn()); + const query = { meta: { created_by: "plant-detection" } }; + await deletePoints("weeds", query)(dispatch, jest.fn()); expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search", { meta: { created_by: "plant-detection" } }); await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3"); @@ -98,7 +100,8 @@ describe("deletePoints()", () => { mockDelete = Promise.resolve(); mockData = times(200, () => ({ id: 1 })); const dispatch = jest.fn(); - await deletePoints("weeds", { created_by: "plant-detection" })(dispatch, jest.fn()); + const query = { meta: { created_by: "plant-detection" } }; + await deletePoints("weeds", query)(dispatch, jest.fn()); expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search", { meta: { created_by: "plant-detection" } }); await expect(axios.delete).toHaveBeenCalledWith( diff --git a/frontend/farmware/weed_detector/__tests__/weed_detector_test.tsx b/frontend/farmware/weed_detector/__tests__/weed_detector_test.tsx index 2dd572f5a..799d561c3 100644 --- a/frontend/farmware/weed_detector/__tests__/weed_detector_test.tsx +++ b/frontend/farmware/weed_detector/__tests__/weed_detector_test.tsx @@ -85,7 +85,7 @@ describe("", () => { expect(wrapper.instance().state.deletionProgress).toBeUndefined(); clickButton(wrapper, 1, "clear weeds"); expect(deletePoints).toHaveBeenCalledWith( - "weeds", { created_by: "plant-detection" }, expect.any(Function)); + "weeds", { meta: { created_by: "plant-detection" } }, expect.any(Function)); expect(wrapper.instance().state.deletionProgress).toEqual("Deleting..."); const fakeProgress = { completed: 50, total: 100, isDone: false }; mockDeletePoints.mock.calls[0][2](fakeProgress); diff --git a/frontend/farmware/weed_detector/actions.tsx b/frontend/farmware/weed_detector/actions.tsx index 0c1b20626..10511bedd 100644 --- a/frontend/farmware/weed_detector/actions.tsx +++ b/frontend/farmware/weed_detector/actions.tsx @@ -5,20 +5,19 @@ import { API } from "../../api"; import { Progress, ProgressCallback, trim } from "../../util"; import { getDevice } from "../../device"; import { noop, chunk } from "lodash"; -import { GenericPointer } from "farmbot/dist/resources/api_resources"; +import { Point } from "farmbot/dist/resources/api_resources"; import { Actions } from "../../constants"; import { t } from "../../i18next_wrapper"; export function deletePoints( pointName: string, - metaQuery: { [key: string]: string }, + query: Partial, cb?: ProgressCallback): Thunk { // TODO: Generalize and add to api/crud.ts return async function (dispatch) { const URL = API.current.pointSearchPath; - const QUERY = { meta: metaQuery }; try { - const resp = await axios.post(URL, QUERY); + const resp = await axios.post(URL, query); const ids = resp.data.map(x => x.id); // If you delete too many points, you will violate the URL length // limitation of 2,083. Chunking helps fix that. diff --git a/frontend/farmware/weed_detector/index.tsx b/frontend/farmware/weed_detector/index.tsx index 766366ace..ced91eec0 100644 --- a/frontend/farmware/weed_detector/index.tsx +++ b/frontend/farmware/weed_detector/index.tsx @@ -39,7 +39,7 @@ export class WeedDetector this.setState({ deletionProgress: p.isDone ? "" : percentage }); }; this.props.dispatch(deletePoints(t("weeds"), - { created_by: "plant-detection" }, progress)); + { meta: { created_by: "plant-detection" } }, progress)); this.setState({ deletionProgress: t("Deleting...") }); } diff --git a/frontend/resources/__tests__/selectors_test.ts b/frontend/resources/__tests__/selectors_test.ts index f3041a578..9018d5529 100644 --- a/frontend/resources/__tests__/selectors_test.ts +++ b/frontend/resources/__tests__/selectors_test.ts @@ -111,7 +111,7 @@ describe("isKind()", () => { describe("groupPointsByType()", () => { it("returns points", () => { const points = Selector.groupPointsByType(fakeIndex); - const expectedKeys = ["Plant", "GenericPointer", "ToolSlot"]; + const expectedKeys = ["Plant", "GenericPointer", "ToolSlot", "Weed"]; expect(expectedKeys.every(key => key in points)).toBeTruthy(); }); }); diff --git a/frontend/resources/selectors.ts b/frontend/resources/selectors.ts index fbc60d8c3..0f3569422 100644 --- a/frontend/resources/selectors.ts +++ b/frontend/resources/selectors.ts @@ -10,15 +10,16 @@ import { TaggedToolSlotPointer, TaggedUser, TaggedDevice, + TaggedWeedPointer, } from "farmbot"; import { isTaggedPlantPointer, isTaggedGenericPointer, isTaggedRegimen, isTaggedSequence, - isTaggedTool, isTaggedToolSlotPointer, sanityCheck, + isTaggedWeedPointer, } from "./tagged_resources"; import { betterCompact, bail } from "../util"; import { findAllById } from "./selectors_by_id"; @@ -94,6 +95,13 @@ export function selectAllGenericPointers(index: ResourceIndex): return betterCompact(genericPointers); } +export function selectAllWeedPointers(index: ResourceIndex): + TaggedWeedPointer[] { + const weedPointers = selectAllPoints(index) + .map(p => isTaggedWeedPointer(p) ? p : undefined); + return betterCompact(weedPointers); +} + export function selectAllPlantPointers(index: ResourceIndex): TaggedPlantPointer[] { const plantPointers = selectAllActivePoints(index) .map(p => isTaggedPlantPointer(p) ? p : undefined); @@ -141,22 +149,6 @@ export function getSequenceByUUID(index: ResourceIndex, } } -/** GIVEN: a slot UUID. - * FINDS: Tool in that slot (if any) */ -export const currentToolInSlot = (index: ResourceIndex) => - (toolSlotUUID: string): TaggedTool | undefined => { - const currentSlot = selectCurrentToolSlot(index, toolSlotUUID); - if (currentSlot - && currentSlot.kind === "Point") { - const toolUUID = index - .byKindAndId[joinKindAndId("Tool", currentSlot.body.tool_id)]; - const tool = index.references[toolUUID || "NOPE!"]; - if (tool && isTaggedTool(tool)) { - return tool; - } - } - }; - /** FINDS: All tools that are in use. */ export function toolsInUse(index: ResourceIndex): TaggedTool[] { const ids = betterCompact(selectAllToolSlotPointers(index) diff --git a/frontend/resources/selectors_by_id.ts b/frontend/resources/selectors_by_id.ts index 2f42a1d49..361c71059 100644 --- a/frontend/resources/selectors_by_id.ts +++ b/frontend/resources/selectors_by_id.ts @@ -11,6 +11,7 @@ import { isTaggedGenericPointer, isTaggedSavedGarden, isTaggedFolder, + isTaggedWeedPointer, } from "./tagged_resources"; import { ResourceName, @@ -125,6 +126,13 @@ export function maybeFindGenericPointerById(index: ResourceIndex, id: number) { if (resource && isTaggedGenericPointer(resource)) { return resource; } } +/** Unlike other findById methods, this one allows undefined (missed) values */ +export function maybeFindWeedPointerById(index: ResourceIndex, id: number) { + const uuid = index.byKindAndId[joinKindAndId("Point", id)]; + const resource = index.references[uuid || "nope"]; + if (resource && isTaggedWeedPointer(resource)) { return resource; } +} + /** Unlike other findById methods, this one allows undefined (missed) values */ export function maybeFindSavedGardenById(index: ResourceIndex, id: number) { const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)]; diff --git a/frontend/resources/tagged_resources.ts b/frontend/resources/tagged_resources.ts index ab83ec9ab..67fc8c430 100644 --- a/frontend/resources/tagged_resources.ts +++ b/frontend/resources/tagged_resources.ts @@ -16,6 +16,7 @@ import { TaggedPlantTemplate, TaggedSavedGarden, TaggedPointGroup, + TaggedWeedPointer, } from "farmbot"; export interface TaggedResourceBase { @@ -102,6 +103,8 @@ export const isTaggedGenericPointer = (x: object): x is TaggedGenericPointer => { return isTaggedPoint(x) && (x.body.pointer_type === "GenericPointer"); }; +export const isTaggedWeedPointer = (x: object): x is TaggedWeedPointer => + isTaggedPoint(x) && (x.body.pointer_type === "Weed"); export const isTaggedSavedGarden = (x: object): x is TaggedSavedGarden => is("SavedGarden")(x); export const isTaggedPlantTemplate = diff --git a/frontend/route_config.tsx b/frontend/route_config.tsx index 77236b088..cd5539edf 100644 --- a/frontend/route_config.tsx +++ b/frontend/route_config.tsx @@ -376,7 +376,7 @@ export const UNBOUND_ROUTES = [ $: "/designer/weeds", getModule, key, - getChild: () => import("./farm_designer/points/weeds_inventory"), + getChild: () => import("./farm_designer/weeds/weeds_inventory"), childKey: "Weeds" }), route({ @@ -392,7 +392,7 @@ export const UNBOUND_ROUTES = [ $: "/designer/weeds/:point_id", getModule, key, - getChild: () => import("./farm_designer/points/weeds_edit"), + getChild: () => import("./farm_designer/weeds/weeds_edit"), childKey: "EditWeed" }), route({ 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 4606e3efa..4cd988f93 100644 --- a/frontend/sequences/locals_list/__tests__/location_form_list_test.ts +++ b/frontend/sequences/locals_list/__tests__/location_form_list_test.ts @@ -30,45 +30,58 @@ describe("locationFormList()", () => { label: "Generic tool (100, 200, 300)", value: "1", }); - const plantHeading = items[3]; - expect(plantHeading).toEqual({ - headingId: "Plant", - label: "Plants", - value: 0, - heading: true, - }); - const plant = items[4]; - expect(plant).toEqual({ - headingId: "Plant", - label: "Plant 1 (1, 2, 3)", - value: "1" - }); - const pointHeading = items[6]; - expect(pointHeading).toEqual({ - headingId: "GenericPointer", - label: "Map Points", - value: 0, - heading: true, - }); - const point = items[7]; - expect(point).toEqual({ - headingId: "GenericPointer", - label: "Point 1 (10, 20, 30)", - value: "2" - }); - const groupHeading = items[8]; + const groupHeading = items[3]; expect(groupHeading).toEqual({ headingId: "PointGroup", label: "Groups", value: 0, heading: true, }); - const group = items[9]; + const group = items[4]; expect(group).toEqual({ headingId: "PointGroup", label: "Fake", value: "1" }); + const plantHeading = items[5]; + expect(plantHeading).toEqual({ + headingId: "Plant", + label: "Plants", + value: 0, + heading: true, + }); + const plant = items[6]; + expect(plant).toEqual({ + headingId: "Plant", + label: "Plant 1 (1, 2, 3)", + value: "1" + }); + const pointHeading = items[8]; + expect(pointHeading).toEqual({ + headingId: "GenericPointer", + label: "Map Points", + value: 0, + heading: true, + }); + const point = items[9]; + expect(point).toEqual({ + headingId: "GenericPointer", + label: "Point 1 (10, 20, 30)", + value: "2" + }); + const weedHeading = items[10]; + expect(weedHeading).toEqual({ + headingId: "Weed", + label: "Weeds", + value: 0, + heading: true, + }); + const weed = items[11]; + expect(weed).toEqual({ + headingId: "Weed", + label: "Weed 1 (15, 25, 35)", + value: "5" + }); }); }); diff --git a/frontend/sequences/locals_list/handle_select.ts b/frontend/sequences/locals_list/handle_select.ts index af2773181..1f03ac1a6 100644 --- a/frontend/sequences/locals_list/handle_select.ts +++ b/frontend/sequences/locals_list/handle_select.ts @@ -78,7 +78,7 @@ const toolVar = (value: string | number) => }); const pointVar = ( - pointer_type: "Plant" | "GenericPointer", + pointer_type: "Plant" | "GenericPointer" | "Weed", value: string | number, ) => ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue => createVariableNode(allowedVariableNodes)(label, { @@ -123,7 +123,9 @@ const createNewVariable = (props: NewVarProps): VariableNode | undefined => { if (ddi.isNull) { return nothingVar(props); } // Empty form. Nothing selected yet. switch (ddi.headingId) { case "Plant": - case "GenericPointer": return pointVar(ddi.headingId, ddi.value)(props); + case "GenericPointer": + case "Weed": + return pointVar(ddi.headingId, ddi.value)(props); case "Tool": return toolVar(ddi.value)(props); case "parameter": return newParameter(props); case "Coordinate": return manualEntry(ddi.value)(props); diff --git a/frontend/sequences/locals_list/location_form_list.ts b/frontend/sequences/locals_list/location_form_list.ts index e778a8e72..39f90e8c5 100644 --- a/frontend/sequences/locals_list/location_form_list.ts +++ b/frontend/sequences/locals_list/location_form_list.ts @@ -72,17 +72,20 @@ export function locationFormList(resources: ResourceIndex, const allPoints = selectAllActivePoints(resources); const plantDDI = points2ddi(allPoints, "Plant"); const genericPointerDDI = points2ddi(allPoints, "GenericPointer"); + const weedDDI = points2ddi(allPoints, "Weed"); const toolDDI = activeToolDDIs(resources); return [COORDINATE_DDI()] .concat(additionalItems) .concat(heading("Tool")) .concat(toolDDI) + .concat(displayGroups ? heading("PointGroup") : []) + .concat(displayGroups ? groups2Ddi(selectAllPointGroups(resources)) : []) .concat(heading("Plant")) .concat(plantDDI) .concat(heading("GenericPointer")) .concat(genericPointerDDI) - .concat(displayGroups ? heading("PointGroup") : []) - .concat(displayGroups ? groups2Ddi(selectAllPointGroups(resources)) : []); + .concat(heading("Weed")) + .concat(weedDDI); } /** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */ @@ -126,15 +129,6 @@ export function dropDownName(name: string, v?: Record, return capitalize(label); } -export const ALL_POINT_LABELS = { - "Plant": "All plants", - "GenericPointer": "All map points", - "Tool": "All tools and seed containers", - "ToolSlot": "All slots", -}; - -export type EveryPointType = keyof typeof ALL_POINT_LABELS; - export const COORDINATE_DDI = (vector?: Vector3): DropDownItem => ({ label: vector ? `${t("Coordinate")} (${vector.x}, ${vector.y}, ${vector.z})` diff --git a/frontend/sequences/locals_list/test_helpers.ts b/frontend/sequences/locals_list/test_helpers.ts index 135ca28b1..add4ae492 100644 --- a/frontend/sequences/locals_list/test_helpers.ts +++ b/frontend/sequences/locals_list/test_helpers.ts @@ -52,6 +52,16 @@ export function fakeResourceIndex(extra: TaggedResource[] = []): ResourceIndex { "y": 200, "z": 300, }), + ...newTaggedResource("Point", { + id: 5, + meta: {}, + name: "Weed 1", + pointer_type: "Weed", + radius: 15, + x: 15, + y: 25, + z: 35, + }), ...newTaggedResource("Tool", { "id": 1, "name": "Generic Tool", diff --git a/frontend/sequences/step_tiles/mark_as.tsx b/frontend/sequences/step_tiles/mark_as.tsx index ccc1d507c..598efeb07 100644 --- a/frontend/sequences/step_tiles/mark_as.tsx +++ b/frontend/sequences/step_tiles/mark_as.tsx @@ -11,7 +11,7 @@ import { commitStepChanges } from "./mark_as/commit_step_changes"; import { t } from "../../i18next_wrapper"; interface MarkAsState { nextResource: DropDownItem | undefined } -const NONE: DropDownItem = { value: 0, label: "" }; +const NONE = (): DropDownItem => ({ label: t("Select one"), value: 0 }); export class MarkAs extends React.Component { state: MarkAsState = { nextResource: undefined }; @@ -57,7 +57,7 @@ export class MarkAs extends React.Component { list={actionList(this.state.nextResource, step, this.props.resources)} onChange={this.commitSelection} key={JSON.stringify(rightSide) + JSON.stringify(this.state)} - selectedItem={this.state.nextResource ? NONE : rightSide} /> + selectedItem={this.state.nextResource ? NONE() : rightSide} />
diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts index 15e0c97aa..04d6d33c8 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/action_list_test.ts @@ -10,7 +10,7 @@ describe("actionList()", () => { const step = resourceUpdate({ resource_type: "Plant" }); const { index } = markAsResourceFixture(); const result = actionList(undefined, step, index); - expect(result).toEqual(PLANT_OPTIONS); + expect(result).toEqual(PLANT_OPTIONS()); }); it("provides a list of tool mount actions", () => { @@ -35,6 +35,16 @@ describe("actionList()", () => { expect(labels).toContain("Removed"); }); + it("provides a list of weed pointer actions", () => { + const ddi = { label: "test case", value: 1, headingId: "Weed" }; + const step = resourceUpdate({}); + const { index } = markAsResourceFixture(); + const result = actionList(ddi, step, index); + expect(result.length).toBe(1); + const labels = result.map(x => x.label); + expect(labels).toContain("Removed"); + }); + it("returns an empty list for all other options", () => { const ddi = { label: "test case", value: 1, headingId: "USB Cables" }; const step = resourceUpdate({}); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts index df63cfd85..41e882a62 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/resource_list_test.ts @@ -10,5 +10,9 @@ describe("resourceList()", () => { expect(headings).toContain("Device"); expect(headings).toContain("Plants"); expect(headings).toContain("Points"); + expect(headings).toContain("Weeds"); + const weeds = result.filter(x => x.headingId == "Weed"); + expect(weeds.length).toEqual(2); + expect(weeds[1].label).toEqual("weed 1 (200, 400, 0)"); }); }); diff --git a/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts b/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts index 8c8f45072..cc2375c36 100644 --- a/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts +++ b/frontend/sequences/step_tiles/mark_as/__tests__/unpack_step_test.ts @@ -7,6 +7,10 @@ import { selectAllGenericPointers, } from "../../../../resources/selectors"; import { DropDownPair } from "../interfaces"; +import { fakeTool } from "../../../../__test_support__/fake_state/resources"; +import { + buildResourceIndex, +} from "../../../../__test_support__/resource_index_builder"; describe("unpackStep()", () => { function assertGoodness(result: DropDownPair, action_label: string, @@ -41,6 +45,23 @@ describe("unpackStep()", () => { assertGoodness(result, actionLabel, "mounted", label, value); }); + it("unpacks valid tool_ids with missing names", () => { + const tool = fakeTool(); + tool.body.id = 1; + tool.body.name = undefined; + const resourceIndex = buildResourceIndex([tool]).index; + const { body } = selectAllTools(resourceIndex)[0]; + expect(body).toBeTruthy(); + + const result = unpackStep({ + step: resourceUpdate({ label: "mounted_tool_id", value: body.id || NaN }), + resourceIndex + }); + const actionLabel = "Mounted to: Untitled Tool"; + const { label, value } = TOOL_MOUNT(); + assertGoodness(result, actionLabel, "mounted", label, value); + }); + it("unpacks invalid tool_ids (that may have been valid previously)", () => { const result = unpackStep({ step: resourceUpdate({ label: "mounted_tool_id", value: Infinity }), diff --git a/frontend/sequences/step_tiles/mark_as/action_list.ts b/frontend/sequences/step_tiles/mark_as/action_list.ts index a57281cc1..60225c797 100644 --- a/frontend/sequences/step_tiles/mark_as/action_list.ts +++ b/frontend/sequences/step_tiles/mark_as/action_list.ts @@ -16,7 +16,7 @@ const allToolsAsDDI = (i: ResourceIndex) => { .filter(x => !!x.body.id) .map(x => { return { - label: `${MOUNTED_TO} ${x.body.name}`, + label: `${MOUNTED_TO()} ${x.body.name}`, value: x.body.id || 0 }; }); @@ -25,9 +25,10 @@ const allToolsAsDDI = (i: ResourceIndex) => { const DEFAULT = "Default"; const ACTION_LIST: Dictionary = { - "Device": (i) => [DISMOUNT, ...allToolsAsDDI(i)], - "Plant": () => PLANT_OPTIONS, + "Device": (i) => [DISMOUNT(), ...allToolsAsDDI(i)], + "Plant": () => PLANT_OPTIONS(), "GenericPointer": () => POINT_OPTIONS, + "Weed": () => POINT_OPTIONS, [DEFAULT]: () => [] }; diff --git a/frontend/sequences/step_tiles/mark_as/assertion_support.ts b/frontend/sequences/step_tiles/mark_as/assertion_support.ts index bb5140a09..289b3be68 100644 --- a/frontend/sequences/step_tiles/mark_as/assertion_support.ts +++ b/frontend/sequences/step_tiles/mark_as/assertion_support.ts @@ -7,6 +7,7 @@ import { fakePlant, fakePoint, fakeSequence, + fakeWeed, } from "../../../__test_support__/fake_state/resources"; import { betterMerge } from "../../../util"; import { MarkAs } from "../mark_as"; @@ -30,6 +31,7 @@ export const markAsResourceFixture = () => buildResourceIndex([ fakePlant(), betterMerge(fakeTool(), { body: { name: "T2", id: 2 } }), betterMerge(fakePoint(), { body: { name: "my point", id: 7 } }), + betterMerge(fakeWeed(), { body: { name: "weed 1", id: 8 } }), betterMerge(fakeTool(), { body: { name: "T3", id: undefined } }), ]); diff --git a/frontend/sequences/step_tiles/mark_as/constants.ts b/frontend/sequences/step_tiles/mark_as/constants.ts index b488bd13c..256ca247d 100644 --- a/frontend/sequences/step_tiles/mark_as/constants.ts +++ b/frontend/sequences/step_tiles/mark_as/constants.ts @@ -1,9 +1,11 @@ import { DropDownItem } from "../../../ui"; import { t } from "../../../i18next_wrapper"; +import { PLANT_STAGE_LIST } from "../../../farm_designer/plants/edit_plant_status"; -export const MOUNTED_TO = t("Mounted to:"); +export const MOUNTED_TO = () => t("Mounted to:"); -export const DISMOUNT: DropDownItem = { label: t("Not Mounted"), value: 0 }; +export const DISMOUNT = (): DropDownItem => + ({ label: t("Not Mounted"), value: 0 }); /** Legal "actions" for "Mark As.." block when marking Point resources */ export const POINT_OPTIONS: DropDownItem[] = [ @@ -12,12 +14,7 @@ export const POINT_OPTIONS: DropDownItem[] = [ /** Legal "actions" in the "Mark As.." block when operating on * a Plant resource. */ -export const PLANT_OPTIONS: DropDownItem[] = [ - { label: t("Planned"), value: "planned" }, - { label: t("Planted"), value: "planted" }, - { label: t("Sprouted"), value: "sprouted" }, - { label: t("Harvested"), value: "harvested" }, -]; +export const PLANT_OPTIONS = PLANT_STAGE_LIST; const value = 0; // Not used in headings. @@ -35,6 +32,13 @@ export const POINT_HEADER: DropDownItem = { heading: true }; +export const WEED_HEADER: DropDownItem = { + headingId: "Weed", + label: t("Weeds"), + value, + heading: true +}; + export const TOP_HALF = [ { headingId: "Device", label: t("Device"), value, heading: true }, { headingId: "Device", label: t("Tool Mount"), value }, diff --git a/frontend/sequences/step_tiles/mark_as/resource_list.ts b/frontend/sequences/step_tiles/mark_as/resource_list.ts index 9657fcff8..ecfb606b2 100644 --- a/frontend/sequences/step_tiles/mark_as/resource_list.ts +++ b/frontend/sequences/step_tiles/mark_as/resource_list.ts @@ -1,9 +1,9 @@ import { ResourceIndex } from "../../../resources/interfaces"; import { DropDownItem } from "../../../ui/fb_select"; import { selectAllPoints } from "../../../resources/selectors"; -import { TaggedPoint, TaggedPlantPointer } from "farmbot"; -import { GenericPointer } from "farmbot/dist/resources/api_resources"; -import { POINT_HEADER, PLANT_HEADER, TOP_HALF } from "./constants"; +import { TaggedPoint } from "farmbot"; +import { Point } from "farmbot/dist/resources/api_resources"; +import { POINT_HEADER, PLANT_HEADER, TOP_HALF, WEED_HEADER } from "./constants"; /** Filter function to remove resources we don't care about, * such as ToolSlots and unsaved (Plant|Point)'s */ @@ -17,26 +17,14 @@ const isRelevant = (x: TaggedPoint) => { const labelStr = (n: string, x: number, y: number, z: number) => `${n} (${x}, ${y}, ${z})`; -/** Convert a GenericPointer to a DropDownItem that is formatted appropriately +/** Convert a Point to a DropDownItem that is formatted appropriately * for the "Mark As.." step. */ -export const pointer2ddi = (i: GenericPointer): DropDownItem => { - const { x, y, z, name } = i; +export const point2ddi = (i: Point): DropDownItem => { + const { x, y, z, name, id, pointer_type } = i; return { - value: i.id as number, + value: id || 0, label: labelStr(name, x, y, z), - headingId: "GenericPointer" - }; -}; - -/** Convert a PlantPointer to a DropDownItem appropriately formatted for the - * "Mark As.." step. */ -export const plant2ddi = (i: TaggedPlantPointer["body"]): DropDownItem => { - const { x, y, z, name, id } = i; - - return { - value: id as number, - label: labelStr(name, x, y, z), - headingId: "Plant" + headingId: pointer_type, }; }; @@ -45,16 +33,18 @@ export const plant2ddi = (i: TaggedPlantPointer["body"]): DropDownItem => { const pointList = (input: TaggedPoint[]): DropDownItem[] => { const genericPoints: DropDownItem[] = [POINT_HEADER]; + const weeds: DropDownItem[] = [WEED_HEADER]; const plants: DropDownItem[] = [PLANT_HEADER]; input .map(x => x.body) .forEach(body => { switch (body.pointer_type) { - case "GenericPointer": return genericPoints.push(pointer2ddi(body)); - case "Plant": return plants.push(plant2ddi(body)); + case "GenericPointer": return genericPoints.push(point2ddi(body)); + case "Weed": return weeds.push(point2ddi(body)); + case "Plant": return plants.push(point2ddi(body)); } }); - return [...plants, ...genericPoints]; + return [...plants, ...genericPoints, ...weeds]; }; /** Creates a formatted DropDownItem list for the "Resource" (left hand) side of diff --git a/frontend/sequences/step_tiles/mark_as/unpack_step.ts b/frontend/sequences/step_tiles/mark_as/unpack_step.ts index 7633c2cab..fe04769b7 100644 --- a/frontend/sequences/step_tiles/mark_as/unpack_step.ts +++ b/frontend/sequences/step_tiles/mark_as/unpack_step.ts @@ -1,15 +1,12 @@ import { DropDownItem } from "../../../ui"; import { - findToolById, - findByKindAndId, - findPointerByTypeAndId, + findToolById, findPointerByTypeAndId, } from "../../../resources/selectors"; -import { plant2ddi, pointer2ddi } from "./resource_list"; -import { GenericPointer } from "farmbot/dist/resources/api_resources"; +import { point2ddi } from "./resource_list"; import { MOUNTED_TO } from "./constants"; import { DropDownPair, StepWithResourceIndex } from "./interfaces"; -import { TaggedPoint, TaggedPlantPointer } from "farmbot"; import { t } from "../../../i18next_wrapper"; +import { PLANT_STAGE_DDI_LOOKUP } from "../../../farm_designer/plants/edit_plant_status"; export const TOOL_MOUNT = (): DropDownItem => ({ label: t("Tool Mount"), value: "tool_mount" @@ -19,11 +16,11 @@ export const DISMOUNTED = (): DropDownPair => ({ leftSide: TOOL_MOUNT(), rightSide: NOT_IN_USE() }); -const DEFAULT_TOOL_NAME = "Untitled Tool"; -const REMOVED_ACTION = { label: "Removed", value: "removed" }; +const DEFAULT_TOOL_NAME = () => t("Untitled Tool"); +const REMOVED_ACTION = () => ({ label: t("Removed"), value: "removed" }); -const mountedTo = (toolName = DEFAULT_TOOL_NAME): DropDownItem => - ({ label: `${MOUNTED_TO} ${toolName}`, value: "mounted" }); +const mountedTo = (toolName = DEFAULT_TOOL_NAME()): DropDownItem => + ({ label: `${MOUNTED_TO()} ${toolName}`, value: "mounted" }); /** The user wants to change the `mounted_tool_id` of their Device. */ function mountTool(i: StepWithResourceIndex): DropDownPair { @@ -55,23 +52,24 @@ function unknownOption(i: StepWithResourceIndex): DropDownPair { /** The user wants to mark a the `discarded_at` attribute of a Point. */ function discardPoint(i: StepWithResourceIndex): DropDownPair { - const { resource_id } = i.step.args; - const genericPointerBody = - findPointerByTypeAndId(i.resourceIndex, "GenericPointer", resource_id).body; + const { resource_id, resource_type } = i.step.args; + const pointerBody = + findPointerByTypeAndId(i.resourceIndex, resource_type, resource_id).body; return { - leftSide: pointer2ddi(genericPointerBody as GenericPointer), - rightSide: REMOVED_ACTION + leftSide: point2ddi(pointerBody), + rightSide: REMOVED_ACTION(), }; } /** The user wants to mark a the `plant_stage` attribute of a Plant resource. */ function plantStage(i: StepWithResourceIndex): DropDownPair { - const { resource_id, value } = i.step.args; - const r: TaggedPoint = findByKindAndId(i.resourceIndex, "Point", resource_id); - + const { resource_id, resource_type, value } = i.step.args; + const pointerBody = + findPointerByTypeAndId(i.resourceIndex, resource_type, resource_id).body; return { - leftSide: plant2ddi(r.body as TaggedPlantPointer["body"]), - rightSide: { label: ("" + value), value: ("" + value) } + leftSide: point2ddi(pointerBody), + rightSide: PLANT_STAGE_DDI_LOOKUP()["" + value] + || { label: "" + value, value: "" + value }, }; } diff --git a/frontend/session_keys.ts b/frontend/session_keys.ts index 8b6ffb66c..4337f7a21 100644 --- a/frontend/session_keys.ts +++ b/frontend/session_keys.ts @@ -17,6 +17,7 @@ export const BooleanSetting: Record = { legend_menu_open: "legend_menu_open", show_plants: "show_plants", show_points: "show_points", + show_weeds: "show_weeds", show_historic_points: "show_historic_points", show_spread: "show_spread", show_farmbot: "show_farmbot", diff --git a/package.json b/package.json index f3b378bc8..2905deeeb 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "coveralls": "3.0.11", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", - "farmbot": "9.2.0-rc3", + "farmbot": "9.2.3", "i18next": "19.3.3", "install": "0.13.0", "lodash": "4.17.15", diff --git a/public/app-resources/img/generic-weed.svg b/public/app-resources/img/generic-weed.svg new file mode 100644 index 000000000..d587a90df --- /dev/null +++ b/public/app-resources/img/generic-weed.svg @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +