use Weed instead of GenericPointer for weeds
parent
92a7194c6e
commit
3ebf434945
|
@ -0,0 +1,8 @@
|
||||||
|
class AddShowWeedsToWebAppConfig < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :web_app_configs,
|
||||||
|
:show_weeds,
|
||||||
|
:boolean,
|
||||||
|
default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,8 @@
|
||||||
import { DesignerState } from "../farm_designer/interfaces";
|
import { DesignerState } from "../farm_designer/interfaces";
|
||||||
|
|
||||||
export const fakeDesignerState = (): DesignerState => ({
|
export const fakeDesignerState = (): DesignerState => ({
|
||||||
selectedPlants: undefined,
|
selectedPoints: undefined,
|
||||||
|
selectionPointType: undefined,
|
||||||
hoveredPlant: {
|
hoveredPlant: {
|
||||||
plantUUID: undefined,
|
plantUUID: undefined,
|
||||||
icon: ""
|
icon: ""
|
||||||
|
@ -13,7 +14,8 @@ export const fakeDesignerState = (): DesignerState => ({
|
||||||
cropSearchResults: [],
|
cropSearchResults: [],
|
||||||
cropSearchInProgress: false,
|
cropSearchInProgress: false,
|
||||||
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
||||||
currentPoint: undefined,
|
drawnPoint: undefined,
|
||||||
|
drawnWeed: undefined,
|
||||||
openedSavedGarden: undefined,
|
openedSavedGarden: undefined,
|
||||||
tryGroupSortType: undefined,
|
tryGroupSortType: undefined,
|
||||||
editGroupAreaInMap: false,
|
editGroupAreaInMap: false,
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
TaggedAlert,
|
TaggedAlert,
|
||||||
TaggedPointGroup,
|
TaggedPointGroup,
|
||||||
TaggedFolder,
|
TaggedFolder,
|
||||||
|
TaggedWeedPointer,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { fakeResource } from "../fake_resource";
|
import { fakeResource } from "../fake_resource";
|
||||||
import {
|
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 {
|
export function fakeSavedGarden(): TaggedSavedGarden {
|
||||||
return fakeResource("SavedGarden", {
|
return fakeResource("SavedGarden", {
|
||||||
id: idCounter++,
|
id: idCounter++,
|
||||||
|
@ -289,6 +303,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
||||||
show_sensor_readings: false,
|
show_sensor_readings: false,
|
||||||
show_plants: true,
|
show_plants: true,
|
||||||
show_points: true,
|
show_points: true,
|
||||||
|
show_weeds: true,
|
||||||
x_axis_inverted: false,
|
x_axis_inverted: false,
|
||||||
y_axis_inverted: false,
|
y_axis_inverted: false,
|
||||||
z_axis_inverted: true,
|
z_axis_inverted: true,
|
||||||
|
|
|
@ -316,6 +316,27 @@ const tr15: TaggedResource = {
|
||||||
"uuid": "Tool.15.50"
|
"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 = {
|
const log: TaggedLog = {
|
||||||
kind: "Log",
|
kind: "Log",
|
||||||
specialStatus: SpecialStatus.SAVED,
|
specialStatus: SpecialStatus.SAVED,
|
||||||
|
@ -345,6 +366,7 @@ export const FAKE_RESOURCES: TaggedResource[] = [
|
||||||
tr0,
|
tr0,
|
||||||
tr14,
|
tr14,
|
||||||
tr15,
|
tr15,
|
||||||
|
tr16,
|
||||||
log,
|
log,
|
||||||
];
|
];
|
||||||
const KIND: keyof TaggedResource = "kind"; // Safety first, kids.
|
const KIND: keyof TaggedResource = "kind"; // Safety first, kids.
|
||||||
|
|
|
@ -334,6 +334,7 @@ const MUST_CONFIRM_LIST: ResourceName[] = [
|
||||||
"Regimen",
|
"Regimen",
|
||||||
"Image",
|
"Image",
|
||||||
"SavedGarden",
|
"SavedGarden",
|
||||||
|
"PointGroup",
|
||||||
];
|
];
|
||||||
|
|
||||||
const confirmationChecker = (resourceName: ResourceName, force = false) =>
|
const confirmationChecker = (resourceName: ResourceName, force = false) =>
|
||||||
|
|
|
@ -773,7 +773,7 @@ export namespace Content {
|
||||||
trim(`Click and drag or use the inputs to draw a weed.`);
|
trim(`Click and drag or use the inputs to draw a weed.`);
|
||||||
|
|
||||||
export const BOX_SELECT_DESCRIPTION =
|
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.`);
|
Press the back arrow to exit.`);
|
||||||
|
|
||||||
export const SAVED_GARDENS =
|
export const SAVED_GARDENS =
|
||||||
|
@ -1139,7 +1139,8 @@ export enum Actions {
|
||||||
|
|
||||||
// Designer
|
// Designer
|
||||||
SEARCH_QUERY_CHANGE = "SEARCH_QUERY_CHANGE",
|
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_PLANT = "TOGGLE_HOVERED_PLANT",
|
||||||
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
|
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
|
||||||
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
|
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_OK = "OF_SEARCH_RESULTS_OK",
|
||||||
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",
|
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",
|
||||||
CHOOSE_LOCATION = "CHOOSE_LOCATION",
|
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",
|
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
||||||
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
||||||
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
|
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
|
||||||
|
|
|
@ -185,23 +185,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%panel-item-base {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
.plant-search-item,
|
.plant-search-item,
|
||||||
.group-search-item {
|
.group-search-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
img {
|
img {
|
||||||
margin: 0 1rem 0 0;
|
margin-right: 0.5rem;
|
||||||
height: 4rem;
|
height: 3rem;
|
||||||
width: 4rem;
|
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 {
|
.plant-search-item-age {
|
||||||
@extend %panel-item-base;
|
@extend %panel-item-base;
|
||||||
|
@ -209,6 +219,7 @@
|
||||||
.group-item-count {
|
.group-item-count {
|
||||||
@extend %panel-item-base;
|
@extend %panel-item-base;
|
||||||
padding-top: 0.6rem;
|
padding-top: 0.6rem;
|
||||||
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
.plant-search-item-name {
|
.plant-search-item-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -219,24 +230,27 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
.weed-search-item,
|
||||||
.point-search-item {
|
.point-search-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
.saucer {
|
.saucer {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 1rem 0 0;
|
height: 3rem;
|
||||||
height: 2rem;
|
width: 3rem;
|
||||||
width: 2rem;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.weed-search-item-info,
|
||||||
.point-search-item-info {
|
.point-search-item-info {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding-top: 0.6rem;
|
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
|
line-height: 3rem;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
.weed-search-item-name,
|
||||||
.point-search-item-name {
|
.point-search-item-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -244,7 +258,34 @@
|
||||||
width: 40%;
|
width: 40%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.map-point {
|
||||||
|
cursor: pointer !important;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
stroke-opacity: 0.3;
|
stroke-opacity: 0.3;
|
||||||
fill-opacity: 0.1;
|
fill-opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-weed {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weed-image,
|
||||||
.plant-image {
|
.plant-image {
|
||||||
transform-origin: bottom;
|
transform-origin: bottom;
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
|
@ -337,6 +384,9 @@
|
||||||
fill: $white;
|
fill: $white;
|
||||||
stroke: $white;
|
stroke: $white;
|
||||||
}
|
}
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -291,6 +291,7 @@
|
||||||
.panel-action-buttons {
|
.panel-action-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
|
height: 19rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $panel_medium_light_gray;
|
background: $panel_medium_light_gray;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
@ -307,6 +308,9 @@
|
||||||
float: left;
|
float: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.filter-search {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
.plant-status-bulk-update {
|
.plant-status-bulk-update {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -321,15 +325,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.panel-content {
|
.panel-content {
|
||||||
padding-top: 15rem;
|
padding-top: 19rem;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
max-height: calc(100vh - 13rem);
|
max-height: calc(100vh - 13rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
.plant-search-item,
|
|
||||||
.group-search-item { pointer-events: none; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,9 +379,13 @@
|
||||||
|
|
||||||
.weed-info-panel-content,
|
.weed-info-panel-content,
|
||||||
.point-info-panel-content {
|
.point-info-panel-content {
|
||||||
.saucer {
|
.point-color-input {
|
||||||
margin: 1rem;
|
div[class*=col-] {
|
||||||
margin-left: 2rem;
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.saucer {
|
||||||
|
margin-top: 4.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fb-button & .red {
|
.fb-button & .red {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -557,22 +563,8 @@
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
.tool-search-item,
|
.tool-search-item,
|
||||||
.tool-slot-search-item {
|
.tool-slot-search-item {
|
||||||
line-height: 4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: -15px;
|
margin-left: -15px;
|
||||||
margin-right: -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 {
|
.filter-search {
|
||||||
.bp3-button {
|
.bp3-button {
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
|
@ -585,13 +577,6 @@
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.tool-slot-position-info {
|
|
||||||
padding: 0;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.mounted-tool-header {
|
.mounted-tool-header {
|
||||||
display: flex;
|
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,
|
.weeds-inventory-panel,
|
||||||
.zones-inventory-panel,
|
.zones-inventory-panel,
|
||||||
.groups-panel {
|
.groups-panel {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import * as React from "react";
|
||||||
import { RawFarmDesigner as FarmDesigner } from "../index";
|
import { RawFarmDesigner as FarmDesigner } from "../index";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { Props } from "../interfaces";
|
import { Props } from "../interfaces";
|
||||||
import { GardenMapLegendProps } from "../map/interfaces";
|
|
||||||
import { bot } from "../../__test_support__/fake_state/bot";
|
import { bot } from "../../__test_support__/fake_state/bot";
|
||||||
import {
|
import {
|
||||||
fakeImage, fakeWebAppConfig,
|
fakeImage, fakeWebAppConfig,
|
||||||
|
@ -28,6 +27,8 @@ import {
|
||||||
import { fakeState } from "../../__test_support__/fake_state";
|
import { fakeState } from "../../__test_support__/fake_state";
|
||||||
import { edit } from "../../api/crud";
|
import { edit } from "../../api/crud";
|
||||||
import { BooleanSetting } from "../../session_keys";
|
import { BooleanSetting } from "../../session_keys";
|
||||||
|
import { GardenMapLegend } from "../map/legend/garden_map_legend";
|
||||||
|
import { GardenMap } from "../map/garden_map";
|
||||||
|
|
||||||
describe("<FarmDesigner/>", () => {
|
describe("<FarmDesigner/>", () => {
|
||||||
const fakeProps = (): Props => ({
|
const fakeProps = (): Props => ({
|
||||||
|
@ -36,6 +37,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
designer: fakeDesignerState(),
|
designer: fakeDesignerState(),
|
||||||
hoveredPlant: undefined,
|
hoveredPlant: undefined,
|
||||||
genericPoints: [],
|
genericPoints: [],
|
||||||
|
weeds: [],
|
||||||
allPoints: [],
|
allPoints: [],
|
||||||
plants: [],
|
plants: [],
|
||||||
toolSlots: [],
|
toolSlots: [],
|
||||||
|
@ -67,8 +69,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
|
|
||||||
it("loads default map settings", () => {
|
it("loads default map settings", () => {
|
||||||
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
|
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
|
||||||
const legendProps =
|
const legendProps = wrapper.find(GardenMapLegend).props();
|
||||||
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
|
|
||||||
expect(legendProps.legendMenuOpen).toBeFalsy();
|
expect(legendProps.legendMenuOpen).toBeFalsy();
|
||||||
expect(legendProps.showPlants).toBeTruthy();
|
expect(legendProps.showPlants).toBeTruthy();
|
||||||
expect(legendProps.showPoints).toBeTruthy();
|
expect(legendProps.showPoints).toBeTruthy();
|
||||||
|
@ -76,8 +77,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
expect(legendProps.showFarmbot).toBeTruthy();
|
expect(legendProps.showFarmbot).toBeTruthy();
|
||||||
expect(legendProps.showImages).toBeFalsy();
|
expect(legendProps.showImages).toBeFalsy();
|
||||||
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
|
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
|
||||||
// tslint:disable-next-line:no-any
|
const gardenMapProps = wrapper.find(GardenMap).props();
|
||||||
const gardenMapProps = wrapper.find("GardenMap").props() as any;
|
|
||||||
expect(gardenMapProps.gridSize.x).toEqual(2900);
|
expect(gardenMapProps.gridSize.x).toEqual(2900);
|
||||||
expect(gardenMapProps.gridSize.y).toEqual(1400);
|
expect(gardenMapProps.gridSize.y).toEqual(1400);
|
||||||
});
|
});
|
||||||
|
@ -90,8 +90,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
image2.body.created_at = "2001-01-01T00:00:00.000Z";
|
image2.body.created_at = "2001-01-01T00:00:00.000Z";
|
||||||
p.latestImages = [image1, image2];
|
p.latestImages = [image1, image2];
|
||||||
const wrapper = mount(<FarmDesigner {...p} />);
|
const wrapper = mount(<FarmDesigner {...p} />);
|
||||||
const legendProps =
|
const legendProps = wrapper.find(GardenMapLegend).props();
|
||||||
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
|
|
||||||
expect(legendProps.imageAgeInfo)
|
expect(legendProps.imageAgeInfo)
|
||||||
.toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 });
|
.toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 });
|
||||||
});
|
});
|
||||||
|
@ -137,4 +136,18 @@ describe("<FarmDesigner/>", () => {
|
||||||
bot_origin_quadrant: 2
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { designer } from "../reducer";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { ReduxAction } from "../../redux/interfaces";
|
import { ReduxAction } from "../../redux/interfaces";
|
||||||
import {
|
import {
|
||||||
HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult,
|
HoveredPlantPayl, DrawnPointPayl, CropLiveSearchResult, DrawnWeedPayl,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import { BotPosition } from "../../devices/interfaces";
|
import { BotPosition } from "../../devices/interfaces";
|
||||||
import {
|
import {
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
} from "../../__test_support__/fake_crop_search_result";
|
} from "../../__test_support__/fake_crop_search_result";
|
||||||
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
||||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
|
import { PointType } from "farmbot";
|
||||||
|
|
||||||
describe("designer reducer", () => {
|
describe("designer reducer", () => {
|
||||||
const oldState = fakeDesignerState;
|
const oldState = fakeDesignerState;
|
||||||
|
@ -24,13 +25,22 @@ describe("designer reducer", () => {
|
||||||
expect(newState.cropSearchInProgress).toEqual(true);
|
expect(newState.cropSearchInProgress).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects plants", () => {
|
it("selects points", () => {
|
||||||
const action: ReduxAction<string[]> = {
|
const action: ReduxAction<string[]> = {
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: ["plantUuid"]
|
payload: ["pointUuid"]
|
||||||
};
|
};
|
||||||
const newState = designer(oldState(), action);
|
const newState = designer(oldState(), action);
|
||||||
expect(newState.selectedPlants).toEqual(["plantUuid"]);
|
expect(newState.selectedPoints).toEqual(["pointUuid"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets selection point type", () => {
|
||||||
|
const action: ReduxAction<PointType[] | undefined> = {
|
||||||
|
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||||
|
payload: ["Plant"],
|
||||||
|
};
|
||||||
|
const newState = designer(oldState(), action);
|
||||||
|
expect(newState.selectionPointType).toEqual(["Plant"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets hovered plant", () => {
|
it("sets hovered plant", () => {
|
||||||
|
@ -84,25 +94,49 @@ describe("designer reducer", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets current point data", () => {
|
it("sets current point data", () => {
|
||||||
const action: ReduxAction<CurrentPointPayl> = {
|
const action: ReduxAction<DrawnPointPayl> = {
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: { cx: 10, cy: 20, r: 30, color: "red" }
|
payload: { cx: 10, cy: 20, r: 30, color: "red" }
|
||||||
};
|
};
|
||||||
const newState = designer(oldState(), action);
|
const newState = designer(oldState(), action);
|
||||||
expect(newState.currentPoint).toEqual({
|
expect(newState.drawnPoint).toEqual({
|
||||||
cx: 10, cy: 20, r: 30, color: "red"
|
cx: 10, cy: 20, r: 30, color: "red"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses current point color", () => {
|
it("uses current point color", () => {
|
||||||
const action: ReduxAction<CurrentPointPayl> = {
|
const action: ReduxAction<DrawnPointPayl> = {
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: { cx: 10, cy: 20, r: 30 }
|
payload: { cx: 10, cy: 20, r: 30 }
|
||||||
};
|
};
|
||||||
const state = oldState();
|
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);
|
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<DrawnWeedPayl> = {
|
||||||
|
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<DrawnWeedPayl> = {
|
||||||
|
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"
|
cx: 10, cy: 20, r: 30, color: "red"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -156,4 +190,14 @@ describe("designer reducer", () => {
|
||||||
const newState = designer(state, action);
|
const newState = designer(state, action);
|
||||||
expect(newState.tryGroupSortType).toEqual("random");
|
expect(newState.tryGroupSortType).toEqual("random");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enables edit group area in map mode", () => {
|
||||||
|
const state = oldState();
|
||||||
|
state.editGroupAreaInMap = false;
|
||||||
|
const action: ReduxAction<boolean> = {
|
||||||
|
type: Actions.EDIT_GROUP_AREA_IN_MAP, payload: true
|
||||||
|
};
|
||||||
|
const newState = designer(state, action);
|
||||||
|
expect(newState.editGroupAreaInMap).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,7 +51,7 @@ describe("mapStateToProps()", () => {
|
||||||
const state = fakeState();
|
const state = fakeState();
|
||||||
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
|
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
|
||||||
const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0];
|
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(mapStateToProps(state).selectedPlant).toEqual(
|
||||||
expect.objectContaining({ uuid: plantUuid }));
|
expect.objectContaining({ uuid: plantUuid }));
|
||||||
});
|
});
|
||||||
|
|
|
@ -70,6 +70,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
legend_menu_open: init(BooleanSetting.legend_menu_open, false),
|
legend_menu_open: init(BooleanSetting.legend_menu_open, false),
|
||||||
show_plants: init(BooleanSetting.show_plants, true),
|
show_plants: init(BooleanSetting.show_plants, true),
|
||||||
show_points: init(BooleanSetting.show_points, true),
|
show_points: init(BooleanSetting.show_points, true),
|
||||||
|
show_weeds: init(BooleanSetting.show_weeds, true),
|
||||||
show_spread: init(BooleanSetting.show_spread, false),
|
show_spread: init(BooleanSetting.show_spread, false),
|
||||||
show_farmbot: init(BooleanSetting.show_farmbot, true),
|
show_farmbot: init(BooleanSetting.show_farmbot, true),
|
||||||
show_images: init(BooleanSetting.show_images, false),
|
show_images: init(BooleanSetting.show_images, false),
|
||||||
|
@ -116,6 +117,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
legend_menu_open,
|
legend_menu_open,
|
||||||
show_plants,
|
show_plants,
|
||||||
show_points,
|
show_points,
|
||||||
|
show_weeds,
|
||||||
show_spread,
|
show_spread,
|
||||||
show_farmbot,
|
show_farmbot,
|
||||||
show_images,
|
show_images,
|
||||||
|
@ -155,6 +157,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
legendMenuOpen={legend_menu_open}
|
legendMenuOpen={legend_menu_open}
|
||||||
showPlants={show_plants}
|
showPlants={show_plants}
|
||||||
showPoints={show_points}
|
showPoints={show_points}
|
||||||
|
showWeeds={show_weeds}
|
||||||
showSpread={show_spread}
|
showSpread={show_spread}
|
||||||
showFarmbot={show_farmbot}
|
showFarmbot={show_farmbot}
|
||||||
showImages={show_images}
|
showImages={show_images}
|
||||||
|
@ -181,6 +184,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
<GardenMap
|
<GardenMap
|
||||||
showPoints={show_points}
|
showPoints={show_points}
|
||||||
showPlants={show_plants}
|
showPlants={show_plants}
|
||||||
|
showWeeds={show_weeds}
|
||||||
showSpread={show_spread}
|
showSpread={show_spread}
|
||||||
showFarmbot={show_farmbot}
|
showFarmbot={show_farmbot}
|
||||||
showImages={show_images}
|
showImages={show_images}
|
||||||
|
@ -192,6 +196,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
designer={this.props.designer}
|
designer={this.props.designer}
|
||||||
plants={this.props.plants}
|
plants={this.props.plants}
|
||||||
genericPoints={this.props.genericPoints}
|
genericPoints={this.props.genericPoints}
|
||||||
|
weeds={this.props.weeds}
|
||||||
allPoints={this.props.allPoints}
|
allPoints={this.props.allPoints}
|
||||||
toolSlots={this.props.toolSlots}
|
toolSlots={this.props.toolSlots}
|
||||||
botLocationData={this.props.botLocationData}
|
botLocationData={this.props.botLocationData}
|
||||||
|
|
|
@ -11,8 +11,10 @@ import {
|
||||||
TaggedSensor,
|
TaggedSensor,
|
||||||
TaggedPoint,
|
TaggedPoint,
|
||||||
TaggedPointGroup,
|
TaggedPointGroup,
|
||||||
|
TaggedWeedPointer,
|
||||||
|
PointType,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { SlotWithTool, ResourceIndex } from "../resources/interfaces";
|
import { SlotWithTool, ResourceIndex, UUID } from "../resources/interfaces";
|
||||||
import {
|
import {
|
||||||
BotPosition, StepsPerMmXY, BotLocationData, ShouldDisplay,
|
BotPosition, StepsPerMmXY, BotLocationData, ShouldDisplay,
|
||||||
} from "../devices/interfaces";
|
} from "../devices/interfaces";
|
||||||
|
@ -48,6 +50,7 @@ export interface State extends TypeCheckerHint {
|
||||||
legend_menu_open: boolean;
|
legend_menu_open: boolean;
|
||||||
show_plants: boolean;
|
show_plants: boolean;
|
||||||
show_points: boolean;
|
show_points: boolean;
|
||||||
|
show_weeds: boolean;
|
||||||
show_spread: boolean;
|
show_spread: boolean;
|
||||||
show_farmbot: boolean;
|
show_farmbot: boolean;
|
||||||
show_images: boolean;
|
show_images: boolean;
|
||||||
|
@ -63,6 +66,7 @@ export interface Props {
|
||||||
designer: DesignerState;
|
designer: DesignerState;
|
||||||
hoveredPlant: TaggedPlant | undefined;
|
hoveredPlant: TaggedPlant | undefined;
|
||||||
genericPoints: TaggedGenericPointer[];
|
genericPoints: TaggedGenericPointer[];
|
||||||
|
weeds: TaggedWeedPointer[];
|
||||||
allPoints: TaggedPoint[];
|
allPoints: TaggedPoint[];
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
toolSlots: SlotWithTool[];
|
toolSlots: SlotWithTool[];
|
||||||
|
@ -106,7 +110,8 @@ export interface Crop {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DesignerState {
|
export interface DesignerState {
|
||||||
selectedPlants: string[] | undefined;
|
selectedPoints: UUID[] | undefined;
|
||||||
|
selectionPointType: PointType[] | undefined;
|
||||||
hoveredPlant: HoveredPlantPayl;
|
hoveredPlant: HoveredPlantPayl;
|
||||||
hoveredPoint: string | undefined;
|
hoveredPoint: string | undefined;
|
||||||
hoveredPlantListItem: string | undefined;
|
hoveredPlantListItem: string | undefined;
|
||||||
|
@ -115,7 +120,8 @@ export interface DesignerState {
|
||||||
cropSearchResults: CropLiveSearchResult[];
|
cropSearchResults: CropLiveSearchResult[];
|
||||||
cropSearchInProgress: boolean;
|
cropSearchInProgress: boolean;
|
||||||
chosenLocation: BotPosition;
|
chosenLocation: BotPosition;
|
||||||
currentPoint: CurrentPointPayl | undefined;
|
drawnPoint: DrawnPointPayl | undefined;
|
||||||
|
drawnWeed: DrawnWeedPayl | undefined;
|
||||||
openedSavedGarden: string | undefined;
|
openedSavedGarden: string | undefined;
|
||||||
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
||||||
editGroupAreaInMap: boolean;
|
editGroupAreaInMap: boolean;
|
||||||
|
@ -181,6 +187,7 @@ export interface FarmEventState {
|
||||||
export interface GardenMapProps {
|
export interface GardenMapProps {
|
||||||
showPlants: boolean | undefined;
|
showPlants: boolean | undefined;
|
||||||
showPoints: boolean | undefined;
|
showPoints: boolean | undefined;
|
||||||
|
showWeeds: boolean | undefined;
|
||||||
showSpread: boolean | undefined;
|
showSpread: boolean | undefined;
|
||||||
showFarmbot: boolean | undefined;
|
showFarmbot: boolean | undefined;
|
||||||
showImages: boolean | undefined;
|
showImages: boolean | undefined;
|
||||||
|
@ -189,6 +196,7 @@ export interface GardenMapProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
designer: DesignerState;
|
designer: DesignerState;
|
||||||
genericPoints: TaggedGenericPointer[];
|
genericPoints: TaggedGenericPointer[];
|
||||||
|
weeds: TaggedWeedPointer[];
|
||||||
allPoints: TaggedPoint[];
|
allPoints: TaggedPoint[];
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
toolSlots: SlotWithTool[];
|
toolSlots: SlotWithTool[];
|
||||||
|
@ -279,7 +287,15 @@ export interface CameraCalibrationData {
|
||||||
calibrationZ: string | undefined;
|
calibrationZ: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentPointPayl {
|
export interface DrawnPointPayl {
|
||||||
|
name?: string;
|
||||||
|
cx: number;
|
||||||
|
cy: number;
|
||||||
|
r: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrawnWeedPayl {
|
||||||
name?: string;
|
name?: string;
|
||||||
cx: number;
|
cx: number;
|
||||||
cy: number;
|
cy: number;
|
||||||
|
|
|
@ -16,8 +16,9 @@ jest.mock("../../point_groups/group_detail", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPlant,
|
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPoint,
|
||||||
setHoveredPlant,
|
setHoveredPlant,
|
||||||
|
mapPointClickAction,
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { MovePlantProps } from "../../interfaces";
|
import { MovePlantProps } from "../../interfaces";
|
||||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||||
|
@ -74,7 +75,7 @@ describe("closePlantInfo()", () => {
|
||||||
closePlantInfo(dispatch)();
|
closePlantInfo(dispatch)();
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
payload: undefined, type: Actions.SELECT_PLANT
|
payload: undefined, type: Actions.SELECT_POINT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@ describe("closePlantInfo()", () => {
|
||||||
closePlantInfo(dispatch)();
|
closePlantInfo(dispatch)();
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
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 dispatch = jest.fn();
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
|
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
|
||||||
expect(dispatch).toHaveBeenCalledWith(selectPlant(["fakeUuid"]));
|
expect(dispatch).toHaveBeenCalledWith(selectPoint(["fakeUuid"]));
|
||||||
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
|
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
@ -136,6 +137,18 @@ describe("clickMapPlant", () => {
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
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", () => {
|
it("removes a point from the current group if group editor is active", () => {
|
||||||
mockPath = "/app/designer/groups/1";
|
mockPath = "/app/designer/groups/1";
|
||||||
mockGroup.body.point_ids = [1, 2];
|
mockGroup.body.point_ids = [1, 2];
|
||||||
|
@ -162,7 +175,7 @@ describe("clickMapPlant", () => {
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT, payload: [plant.uuid]
|
type: Actions.SELECT_POINT, payload: [plant.uuid]
|
||||||
});
|
});
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -173,13 +186,39 @@ describe("clickMapPlant", () => {
|
||||||
const plant = fakePlant();
|
const plant = fakePlant();
|
||||||
plant.uuid = "fakePlantUuid";
|
plant.uuid = "fakePlantUuid";
|
||||||
state.resources = buildResourceIndex([plant]);
|
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 dispatch = jest.fn();
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT, payload: []
|
type: Actions.SELECT_POINT, payload: []
|
||||||
});
|
});
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -6,15 +6,17 @@ jest.mock("../actions", () => ({
|
||||||
import { Mode } from "../interfaces";
|
import { Mode } from "../interfaces";
|
||||||
let mockMode = Mode.none;
|
let mockMode = Mode.none;
|
||||||
let mockAtPlant = true;
|
let mockAtPlant = true;
|
||||||
|
let mockInteractionAllow = true;
|
||||||
jest.mock("../util", () => ({
|
jest.mock("../util", () => ({
|
||||||
getMode: () => mockMode,
|
getMode: () => mockMode,
|
||||||
getMapSize: () => ({ h: 100, w: 100 }),
|
getMapSize: () => ({ h: 100, w: 100 }),
|
||||||
getGardenCoordinates: jest.fn(),
|
getGardenCoordinates: jest.fn(),
|
||||||
transformXY: jest.fn(() => ({ qx: 0, qy: 0 })),
|
transformXY: jest.fn(() => ({ qx: 0, qy: 0 })),
|
||||||
transformForQuadrant: jest.fn(),
|
transformForQuadrant: jest.fn(),
|
||||||
maybeNoPointer: jest.fn(),
|
|
||||||
round: jest.fn(),
|
round: jest.fn(),
|
||||||
cursorAtPlant: () => mockAtPlant,
|
cursorAtPlant: () => mockAtPlant,
|
||||||
|
allowInteraction: () => mockInteractionAllow,
|
||||||
|
allowGroupAreaInteraction: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../layers/plants/plant_actions", () => ({
|
jest.mock("../layers/plants/plant_actions", () => ({
|
||||||
|
@ -81,6 +83,7 @@ const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
|
||||||
const fakeProps = (): GardenMapProps => ({
|
const fakeProps = (): GardenMapProps => ({
|
||||||
showPoints: true,
|
showPoints: true,
|
||||||
showPlants: true,
|
showPlants: true,
|
||||||
|
showWeeds: true,
|
||||||
showSpread: false,
|
showSpread: false,
|
||||||
showFarmbot: false,
|
showFarmbot: false,
|
||||||
showImages: false,
|
showImages: false,
|
||||||
|
@ -92,6 +95,7 @@ const fakeProps = (): GardenMapProps => ({
|
||||||
designer: fakeDesignerState(),
|
designer: fakeDesignerState(),
|
||||||
plants: [],
|
plants: [],
|
||||||
genericPoints: [],
|
genericPoints: [],
|
||||||
|
weeds: [],
|
||||||
allPoints: [],
|
allPoints: [],
|
||||||
toolSlots: [],
|
toolSlots: [],
|
||||||
botLocationData: {
|
botLocationData: {
|
||||||
|
@ -286,7 +290,22 @@ describe("<GardenMap/>", () => {
|
||||||
wrapper.find(".drop-area-svg").simulate("mouseDown", {
|
wrapper.find(".drop-area-svg").simulate("mouseDown", {
|
||||||
pageX: 1, pageY: 2
|
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(<GardenMap {...fakeProps()} />);
|
||||||
|
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(getGardenCoordinates).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ pageX: 1, pageY: 2 }));
|
expect.objectContaining({ pageX: 1, pageY: 2 }));
|
||||||
});
|
});
|
||||||
|
@ -297,7 +316,20 @@ describe("<GardenMap/>", () => {
|
||||||
wrapper.find(".drop-area-svg").simulate("mouseMove", {
|
wrapper.find(".drop-area-svg").simulate("mouseMove", {
|
||||||
pageX: 10, pageY: 20
|
pageX: 10, pageY: 20
|
||||||
});
|
});
|
||||||
expect(resizePoint).toHaveBeenCalled();
|
expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: "point"
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets drawn weed radius", () => {
|
||||||
|
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||||
|
mockMode = Mode.createWeed;
|
||||||
|
wrapper.find(".drop-area-svg").simulate("mouseMove", {
|
||||||
|
pageX: 10, pageY: 20
|
||||||
|
});
|
||||||
|
expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: "weed"
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lays eggs", () => {
|
it("lays eggs", () => {
|
||||||
|
@ -350,7 +382,7 @@ describe("<GardenMap/>", () => {
|
||||||
it("closes panel", () => {
|
it("closes panel", () => {
|
||||||
mockMode = Mode.boxSelect;
|
mockMode = Mode.boxSelect;
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.designer.selectedPlants = undefined;
|
p.designer.selectedPoints = undefined;
|
||||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
wrapper.instance().closePanel()();
|
wrapper.instance().closePanel()();
|
||||||
expect(closePlantInfo).toHaveBeenCalled();
|
expect(closePlantInfo).toHaveBeenCalled();
|
||||||
|
@ -366,7 +398,7 @@ describe("<GardenMap/>", () => {
|
||||||
it("doesn't close panel: box select", () => {
|
it("doesn't close panel: box select", () => {
|
||||||
mockMode = Mode.boxSelect;
|
mockMode = Mode.boxSelect;
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.designer.selectedPlants = [fakePlant().uuid];
|
p.designer.selectedPoints = [fakePlant().uuid];
|
||||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
wrapper.instance().closePanel()();
|
wrapper.instance().closePanel()();
|
||||||
expect(closePlantInfo).not.toHaveBeenCalled();
|
expect(closePlantInfo).not.toHaveBeenCalled();
|
||||||
|
@ -375,7 +407,7 @@ describe("<GardenMap/>", () => {
|
||||||
it("doesn't close panel: move mode", () => {
|
it("doesn't close panel: move mode", () => {
|
||||||
mockMode = Mode.moveTo;
|
mockMode = Mode.moveTo;
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.designer.selectedPlants = [fakePlant().uuid];
|
p.designer.selectedPoints = [fakePlant().uuid];
|
||||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
wrapper.instance().closePanel()();
|
wrapper.instance().closePanel()();
|
||||||
expect(closePlantInfo).not.toHaveBeenCalled();
|
expect(closePlantInfo).not.toHaveBeenCalled();
|
||||||
|
@ -404,6 +436,46 @@ describe("<GardenMap/>", () => {
|
||||||
expect(wrapper.instance().state.isDragging).toBe(true);
|
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<GardenMap>(<GardenMap {...p} />);
|
||||||
|
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<GardenMap>(<GardenMap {...p} />);
|
||||||
|
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<GardenMap>(<GardenMap {...p} />);
|
||||||
|
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<GardenMap>(<GardenMap {...p} />);
|
||||||
|
const allowed = wrapper.instance().interactions("Weed");
|
||||||
|
expect(allowed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it("unswapped height and width", () => {
|
it("unswapped height and width", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.getConfigValue = () => false;
|
p.getConfigValue = () => false;
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {
|
||||||
mapPanelClassName,
|
mapPanelClassName,
|
||||||
getMode,
|
getMode,
|
||||||
cursorAtPlant,
|
cursorAtPlant,
|
||||||
|
allowInteraction,
|
||||||
|
allowGroupAreaInteraction,
|
||||||
} from "../util";
|
} from "../util";
|
||||||
import { McuParams } from "farmbot";
|
import { McuParams } from "farmbot";
|
||||||
import {
|
import {
|
||||||
|
@ -32,13 +34,37 @@ import {
|
||||||
} from "../../../__test_support__/map_transform_props";
|
} from "../../../__test_support__/map_transform_props";
|
||||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||||
|
|
||||||
describe("Utils", () => {
|
describe("round()", () => {
|
||||||
it("rounds a number", () => {
|
it("rounds a number", () => {
|
||||||
expect(round(44)).toEqual(40);
|
expect(round(44)).toEqual(40);
|
||||||
expect(round(98)).toEqual(100);
|
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()", () => {
|
describe("translateScreenToGarden()", () => {
|
||||||
it("translates screen coords to garden coords: zoomLvl = 1", () => {
|
it("translates screen coords to garden coords: zoomLvl = 1", () => {
|
||||||
const result = translateScreenToGarden({
|
const result = translateScreenToGarden({
|
||||||
|
@ -344,6 +370,10 @@ describe("getMode()", () => {
|
||||||
expect(getMode()).toEqual(Mode.points);
|
expect(getMode()).toEqual(Mode.points);
|
||||||
mockPath = "/app/designer/points/add";
|
mockPath = "/app/designer/points/add";
|
||||||
expect(getMode()).toEqual(Mode.createPoint);
|
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";
|
mockPath = "/app/designer/gardens";
|
||||||
mockGardenOpen = true;
|
mockGardenOpen = true;
|
||||||
expect(getMode()).toEqual(Mode.templateView);
|
expect(getMode()).toEqual(Mode.templateView);
|
||||||
|
@ -396,27 +426,37 @@ describe("getGardenCoordinates()", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("mapPanelClassName()", () => {
|
describe("allowInteraction()", () => {
|
||||||
it("returns correct panel status: short panel", () => {
|
it("allows interaction", () => {
|
||||||
Object.defineProperty(window, "innerWidth", {
|
mockPath = "/app/designer/plants";
|
||||||
value: 400,
|
expect(allowInteraction()).toBeTruthy();
|
||||||
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", () => {
|
it("disallows interaction", () => {
|
||||||
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";
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ export function movePlant(payload: MovePlantProps) {
|
||||||
return edit(tr, update);
|
return edit(tr, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectPlant = (payload: string[] | undefined) => {
|
export const selectPoint = (payload: string[] | undefined) => {
|
||||||
return { type: Actions.SELECT_PLANT, payload };
|
return { type: Actions.SELECT_POINT, payload };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
||||||
|
@ -52,16 +52,16 @@ const addOrRemoveFromGroup =
|
||||||
};
|
};
|
||||||
|
|
||||||
const addOrRemoveFromSelection =
|
const addOrRemoveFromSelection =
|
||||||
(clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => {
|
(clickedPointUuid: UUID, selectedPoints: UUID[] | undefined) => {
|
||||||
const nextSelected =
|
const nextSelected =
|
||||||
(selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid);
|
(selectedPoints || []).filter(uuid => uuid !== clickedPointUuid);
|
||||||
if (!(selectedPlants?.includes(clickedPlantUuid))) {
|
if (!(selectedPoints?.includes(clickedPointUuid))) {
|
||||||
nextSelected.push(clickedPlantUuid);
|
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) => {
|
return (dispatch: Function, getState: GetState) => {
|
||||||
switch (getMode()) {
|
switch (getMode()) {
|
||||||
case Mode.editGroup:
|
case Mode.editGroup:
|
||||||
|
@ -69,11 +69,11 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
|
||||||
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
|
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
|
||||||
break;
|
break;
|
||||||
case Mode.boxSelect:
|
case Mode.boxSelect:
|
||||||
const { selectedPlants } = getState().resources.consumers.farm_designer;
|
const { selectedPoints } = getState().resources.consumers.farm_designer;
|
||||||
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPlants));
|
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPoints));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
dispatch(selectPlant([clickedPlantUuid]));
|
dispatch(selectPoint([clickedPlantUuid]));
|
||||||
dispatch(setHoveredPlant(clickedPlantUuid, icon));
|
dispatch(setHoveredPlant(clickedPlantUuid, icon));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unselectPlant = (dispatch: Function) => () => {
|
export const unselectPlant = (dispatch: Function) => () => {
|
||||||
dispatch(selectPlant(undefined));
|
dispatch(selectPoint(undefined));
|
||||||
dispatch(setHoveredPlant(undefined));
|
dispatch(setHoveredPlant(undefined));
|
||||||
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
|
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
|
||||||
};
|
};
|
||||||
|
@ -104,3 +104,14 @@ export const setDragIcon =
|
||||||
e.dataTransfer.setDragImage
|
e.dataTransfer.setDragImage
|
||||||
&& e.dataTransfer.setDragImage(dragImg, width / 2, height / 2);
|
&& 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -55,6 +55,9 @@ describe("resizeBox", () => {
|
||||||
const fakeProps = (): ResizeSelectionBoxProps => ({
|
const fakeProps = (): ResizeSelectionBoxProps => ({
|
||||||
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
||||||
plants: [],
|
plants: [],
|
||||||
|
allPoints: [],
|
||||||
|
selectionPointType: undefined,
|
||||||
|
getConfigValue: () => true,
|
||||||
gardenCoords: { x: 100, y: 200 },
|
gardenCoords: { x: 100, y: 200 },
|
||||||
setMapState: jest.fn(),
|
setMapState: jest.fn(),
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
|
@ -68,7 +71,7 @@ describe("resizeBox", () => {
|
||||||
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -113,7 +116,7 @@ describe("resizeBox", () => {
|
||||||
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: [plant.uuid]
|
payload: [plant.uuid]
|
||||||
});
|
});
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
|
||||||
|
@ -135,7 +138,7 @@ describe("startNewSelectionBox", () => {
|
||||||
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
|
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -157,7 +160,7 @@ describe("startNewSelectionBox", () => {
|
||||||
startNewSelectionBox(p);
|
startNewSelectionBox(p);
|
||||||
expect(p.setMapState).not.toHaveBeenCalled();
|
expect(p.setMapState).not.toHaveBeenCalled();
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,18 +3,20 @@ import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
|
||||||
import { SelectionBoxData } from "./selection_box";
|
import { SelectionBoxData } from "./selection_box";
|
||||||
import { GardenMapState } from "../../interfaces";
|
import { GardenMapState } from "../../interfaces";
|
||||||
import { history } from "../../../history";
|
import { history } from "../../../history";
|
||||||
import { selectPlant } from "../actions";
|
import { selectPoint } from "../actions";
|
||||||
import { getMode } from "../util";
|
import { getMode } from "../util";
|
||||||
import { editGtLtCriteria } from "../../point_groups/criteria";
|
import { editGtLtCriteria } from "../../point_groups/criteria";
|
||||||
import { TaggedPointGroup } from "farmbot";
|
import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot";
|
||||||
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
||||||
import { overwrite } from "../../../api/crud";
|
import { overwrite } from "../../../api/crud";
|
||||||
import { unpackUUID } from "../../../util";
|
import { unpackUUID } from "../../../util";
|
||||||
import { UUID } from "../../../resources/interfaces";
|
import { UUID } from "../../../resources/interfaces";
|
||||||
|
import { getFilteredPoints } from "../../plants/select_plants";
|
||||||
|
import { GetWebAppConfigValue } from "../../../config_storage/actions";
|
||||||
|
|
||||||
/** Return all plants within the selection box. */
|
/** Return all plants within the selection box. */
|
||||||
export const getSelected = (
|
export const getSelected = (
|
||||||
plants: TaggedPlant[],
|
plants: (TaggedPlant | TaggedPoint)[],
|
||||||
box: SelectionBoxData | undefined,
|
box: SelectionBoxData | undefined,
|
||||||
): string[] | undefined => {
|
): string[] | undefined => {
|
||||||
const arraySelected = plants.filter(p => {
|
const arraySelected = plants.filter(p => {
|
||||||
|
@ -35,6 +37,9 @@ export const getSelected = (
|
||||||
export interface ResizeSelectionBoxProps {
|
export interface ResizeSelectionBoxProps {
|
||||||
selectionBox: SelectionBoxData | undefined;
|
selectionBox: SelectionBoxData | undefined;
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
|
allPoints: TaggedPoint[];
|
||||||
|
selectionPointType: PointType[] | undefined;
|
||||||
|
getConfigValue: GetWebAppConfigValue;
|
||||||
gardenCoords: AxisNumberProperty | undefined;
|
gardenCoords: AxisNumberProperty | undefined;
|
||||||
setMapState: (x: Partial<GardenMapState>) => void;
|
setMapState: (x: Partial<GardenMapState>) => void;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
@ -54,11 +59,16 @@ export const resizeBox = (props: ResizeSelectionBoxProps) => {
|
||||||
props.setMapState({ selectionBox: newSelectionBox });
|
props.setMapState({ selectionBox: newSelectionBox });
|
||||||
if (props.plantActions) {
|
if (props.plantActions) {
|
||||||
// Select all plants within the updated selection box
|
// 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) {
|
if (payload && getMode() === Mode.none) {
|
||||||
history.push("/app/designer/plants/select");
|
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) {
|
if (props.plantActions) {
|
||||||
// Clear the previous plant selection when starting a new selection box
|
// 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);
|
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
|
||||||
if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
|
if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
|
||||||
props.dispatch(overwrite(props.group, nextGroupBody));
|
props.dispatch(overwrite(props.group, nextGroupBody));
|
||||||
props.dispatch(selectPlant(undefined));
|
props.dispatch(selectPoint(undefined));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { startNewPoint, resizePoint } from "../drawn_point_actions";
|
import {
|
||||||
|
startNewPoint, resizePoint, StartNewPointProps, ResizePointProps,
|
||||||
|
} from "../drawn_point_actions";
|
||||||
import { Actions } from "../../../../constants";
|
import { Actions } from "../../../../constants";
|
||||||
|
|
||||||
describe("startNewPoint", () => {
|
describe("startNewPoint", () => {
|
||||||
const fakeProps = () => ({
|
const fakeProps = (): StartNewPointProps => ({
|
||||||
gardenCoords: { x: 100, y: 200 },
|
gardenCoords: { x: 100, y: 200 },
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
setMapState: jest.fn(),
|
setMapState: jest.fn(),
|
||||||
|
type: "point",
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts point", () => {
|
it("starts point", () => {
|
||||||
|
@ -13,15 +16,25 @@ describe("startNewPoint", () => {
|
||||||
startNewPoint(p);
|
startNewPoint(p);
|
||||||
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
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 }
|
payload: { cx: 100, cy: 200, r: 0 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't start point", () => {
|
it("doesn't start point", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
// tslint:disable-next-line:no-any
|
p.gardenCoords = undefined;
|
||||||
p.gardenCoords = undefined as any;
|
|
||||||
startNewPoint(p);
|
startNewPoint(p);
|
||||||
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
||||||
expect(p.dispatch).not.toHaveBeenCalled();
|
expect(p.dispatch).not.toHaveBeenCalled();
|
||||||
|
@ -29,18 +42,29 @@ describe("startNewPoint", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resizePoint", () => {
|
describe("resizePoint", () => {
|
||||||
const fakeProps = () => ({
|
const fakeProps = (): ResizePointProps => ({
|
||||||
gardenCoords: { x: 100, y: 200 },
|
gardenCoords: { x: 100, y: 200 },
|
||||||
currentPoint: { cx: 100, cy: 200, r: 0 },
|
drawnPoint: { cx: 100, cy: 200, r: 0 },
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
|
type: "point",
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes point", () => {
|
it("resizes point", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
resizePoint(p);
|
resizePoint(p);
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
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 }
|
payload: { cx: 100, cy: 200, r: 0 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,13 +12,12 @@ describe("<DrawnPoint/>", () => {
|
||||||
cx: 10,
|
cx: 10,
|
||||||
cy: 20,
|
cy: 20,
|
||||||
r: 30,
|
r: 30,
|
||||||
color: "red"
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders point", () => {
|
it("renders point", () => {
|
||||||
const wrapper = svgMount(<DrawnPoint {...fakeProps()} />);
|
const wrapper = svgMount(<DrawnPoint {...fakeProps()} />);
|
||||||
expect(wrapper.find("g").props().stroke).toEqual("red");
|
expect(wrapper.find("g").props().stroke).toEqual("green");
|
||||||
expect(wrapper.find("circle").first().props()).toEqual({
|
expect(wrapper.find("circle").first().props()).toEqual({
|
||||||
id: "point-radius", strokeDasharray: "4 5",
|
id: "point-radius", strokeDasharray: "4 5",
|
||||||
cx: 10, cy: 20, r: 30,
|
cx: 10, cy: 20, r: 30,
|
||||||
|
@ -28,4 +27,11 @@ describe("<DrawnPoint/>", () => {
|
||||||
cx: 10, cy: 20, r: 2,
|
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(<DrawnPoint {...p} />);
|
||||||
|
expect(wrapper.find("g").props().stroke).toEqual("red");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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("<DrawnWeed />", () => {
|
||||||
|
const fakeProps = (): DrawnWeedProps => ({
|
||||||
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
data: {
|
||||||
|
cx: 10,
|
||||||
|
cy: 20,
|
||||||
|
r: 30,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders weed", () => {
|
||||||
|
const wrapper = svgMount(<DrawnWeed {...fakeProps()} />);
|
||||||
|
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(<DrawnWeed {...p} />);
|
||||||
|
const stop = wrapper.find("stop").first().props();
|
||||||
|
expect(stop.stopColor).toEqual("orange");
|
||||||
|
expect(stop.stopOpacity).toEqual(0.5);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { MapTransformProps } from "../interfaces";
|
import { MapTransformProps } from "../interfaces";
|
||||||
import { transformXY } from "../util";
|
import { transformXY } from "../util";
|
||||||
import { CurrentPointPayl } from "../../interfaces";
|
import { DrawnPointPayl } from "../../interfaces";
|
||||||
|
|
||||||
export interface DrawnPointProps {
|
export interface DrawnPointProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
data: CurrentPointPayl | undefined;
|
data: DrawnPointPayl | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DrawnPoint(props: DrawnPointProps) {
|
export function DrawnPoint(props: DrawnPointProps) {
|
||||||
|
|
|
@ -1,37 +1,47 @@
|
||||||
import { Actions } from "../../../constants";
|
import { Actions } from "../../../constants";
|
||||||
import { AxisNumberProperty } from "../interfaces";
|
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. */
|
/** Create a new point. */
|
||||||
export const startNewPoint = (props: {
|
export const startNewPoint = (props: StartNewPointProps) => {
|
||||||
gardenCoords: AxisNumberProperty | undefined,
|
|
||||||
dispatch: Function,
|
|
||||||
setMapState: Function,
|
|
||||||
}) => {
|
|
||||||
props.setMapState({ isDragging: true });
|
props.setMapState({ isDragging: true });
|
||||||
const center = props.gardenCoords;
|
const center = props.gardenCoords;
|
||||||
if (center) {
|
if (center) {
|
||||||
// Set the center of a new point
|
// Set the center of a new point
|
||||||
props.dispatch({
|
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 }
|
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. */
|
/** Resize a point. */
|
||||||
export const resizePoint = (props: {
|
export const resizePoint = (props: ResizePointProps) => {
|
||||||
gardenCoords: AxisNumberProperty | undefined,
|
|
||||||
currentPoint: CurrentPointPayl | undefined,
|
|
||||||
dispatch: Function,
|
|
||||||
isDragging: boolean | undefined,
|
|
||||||
}) => {
|
|
||||||
const edge = props.gardenCoords;
|
const edge = props.gardenCoords;
|
||||||
if (edge && props.currentPoint && !!props.isDragging) {
|
if (edge && props.drawnPoint && !!props.isDragging) {
|
||||||
const { cx, cy } = props.currentPoint;
|
const { cx, cy } = props.drawnPoint;
|
||||||
// Adjust the radius of the point being created
|
// Adjust the radius of the point being created
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: props.type == "weed"
|
||||||
|
? Actions.SET_DRAWN_WEED_DATA
|
||||||
|
: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: {
|
payload: {
|
||||||
cx, cy, // Center was set by click, radius is adjusted by drag
|
cx, cy, // Center was set by click, radius is adjusted by drag
|
||||||
r: Math.round(Math.sqrt(
|
r: Math.round(Math.sqrt(
|
||||||
|
|
|
@ -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 <g id={ID} />; }
|
||||||
|
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 <g id={ID}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id={"DrawnWeedGradient"}>
|
||||||
|
<stop offset="90%" stopColor={color} stopOpacity={stopOpacity} />
|
||||||
|
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<circle
|
||||||
|
id={"weed-radius"}
|
||||||
|
cx={qx}
|
||||||
|
cy={qy}
|
||||||
|
r={r}
|
||||||
|
fill={"url(#DrawnWeedGradient)"} />
|
||||||
|
</g>;
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from "./interfaces";
|
} from "./interfaces";
|
||||||
import { GardenMapProps, GardenMapState } from "../interfaces";
|
import { GardenMapProps, GardenMapState } from "../interfaces";
|
||||||
import {
|
import {
|
||||||
getMapSize, getGardenCoordinates, getMode, cursorAtPlant,
|
getMapSize, getGardenCoordinates, getMode, cursorAtPlant, allowInteraction,
|
||||||
} from "./util";
|
} from "./util";
|
||||||
import {
|
import {
|
||||||
Grid, MapBackground,
|
Grid, MapBackground,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
PlantLayer,
|
PlantLayer,
|
||||||
SpreadLayer,
|
SpreadLayer,
|
||||||
PointLayer,
|
PointLayer,
|
||||||
|
WeedLayer,
|
||||||
ToolSlotLayer,
|
ToolSlotLayer,
|
||||||
FarmBotLayer,
|
FarmBotLayer,
|
||||||
ImageLayer,
|
ImageLayer,
|
||||||
|
@ -34,9 +35,11 @@ import { NNPath } from "../point_groups/paths";
|
||||||
import { history } from "../../history";
|
import { history } from "../../history";
|
||||||
import { ZonesLayer } from "./layers/zones/zones_layer";
|
import { ZonesLayer } from "./layers/zones/zones_layer";
|
||||||
import { ErrorBoundary } from "../../error_boundary";
|
import { ErrorBoundary } from "../../error_boundary";
|
||||||
import { TaggedPoint, TaggedPointGroup } from "farmbot";
|
import { TaggedPoint, TaggedPointGroup, PointType } from "farmbot";
|
||||||
import { findGroupFromUrl } from "../point_groups/group_detail";
|
import { findGroupFromUrl } from "../point_groups/group_detail";
|
||||||
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
||||||
|
import { DrawnWeed } from "./drawn_point/drawn_weed";
|
||||||
|
import { UUID } from "../../resources/interfaces";
|
||||||
|
|
||||||
export class GardenMap extends
|
export class GardenMap extends
|
||||||
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
||||||
|
@ -81,6 +84,10 @@ export class GardenMap extends
|
||||||
pointsSelectedByGroup(this.group, this.props.allPoints) : [];
|
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. */
|
/** Save the current plant (if needed) and reset drag state. */
|
||||||
endDrag = () => {
|
endDrag = () => {
|
||||||
maybeSavePlantLocation({
|
maybeSavePlantLocation({
|
||||||
|
@ -94,7 +101,7 @@ export class GardenMap extends
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
shouldDisplay: this.props.shouldDisplay,
|
shouldDisplay: this.props.shouldDisplay,
|
||||||
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
|
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
|
||||||
boxSelected: this.props.designer.selectedPlants,
|
boxSelected: this.props.designer.selectedPoints,
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
isDragging: false, qPageX: 0, qPageY: 0,
|
isDragging: false, qPageX: 0, qPageY: 0,
|
||||||
|
@ -152,6 +159,15 @@ export class GardenMap extends
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
setMapState: this.setMapState,
|
setMapState: this.setMapState,
|
||||||
|
type: "point",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Mode.createWeed:
|
||||||
|
startNewPoint({
|
||||||
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
|
dispatch: this.props.dispatch,
|
||||||
|
setMapState: this.setMapState,
|
||||||
|
type: "weed",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Mode.clickToAdd:
|
case Mode.clickToAdd:
|
||||||
|
@ -163,8 +179,8 @@ export class GardenMap extends
|
||||||
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
|
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
|
||||||
switch (getMode()) {
|
switch (getMode()) {
|
||||||
case Mode.moveTo:
|
case Mode.moveTo:
|
||||||
break;
|
|
||||||
case Mode.createPoint:
|
case Mode.createPoint:
|
||||||
|
case Mode.createWeed:
|
||||||
case Mode.clickToAdd:
|
case Mode.clickToAdd:
|
||||||
case Mode.editPlant:
|
case Mode.editPlant:
|
||||||
break;
|
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. */
|
/** Return the selected plant, mode-allowing. */
|
||||||
getPlant = (): TaggedPlant | undefined => {
|
getPlant = (): TaggedPlant | undefined => {
|
||||||
switch (getMode()) {
|
return allowInteraction()
|
||||||
case Mode.boxSelect:
|
? this.props.selectedPlant
|
||||||
case Mode.moveTo:
|
: undefined;
|
||||||
case Mode.points:
|
}
|
||||||
case Mode.createPoint:
|
|
||||||
return undefined; // For modes without plant interaction
|
get currentPoint(): UUID | undefined {
|
||||||
default:
|
return this.props.designer.selectedPoints?.[0];
|
||||||
return this.props.selectedPlant;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
|
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
|
||||||
|
@ -273,15 +298,28 @@ export class GardenMap extends
|
||||||
case Mode.createPoint:
|
case Mode.createPoint:
|
||||||
resizePoint({
|
resizePoint({
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
currentPoint: this.props.designer.currentPoint,
|
drawnPoint: this.props.designer.drawnPoint,
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
isDragging: this.state.isDragging,
|
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;
|
break;
|
||||||
case Mode.editGroup:
|
case Mode.editGroup:
|
||||||
resizeBox({
|
resizeBox({
|
||||||
selectionBox: this.state.selectionBox,
|
selectionBox: this.state.selectionBox,
|
||||||
plants: this.props.plants,
|
plants: this.props.plants,
|
||||||
|
allPoints: this.props.allPoints,
|
||||||
|
selectionPointType: this.props.designer.selectionPointType,
|
||||||
|
getConfigValue: this.props.getConfigValue,
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
setMapState: this.setMapState,
|
setMapState: this.setMapState,
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
|
@ -293,6 +331,9 @@ export class GardenMap extends
|
||||||
resizeBox({
|
resizeBox({
|
||||||
selectionBox: this.state.selectionBox,
|
selectionBox: this.state.selectionBox,
|
||||||
plants: this.props.plants,
|
plants: this.props.plants,
|
||||||
|
allPoints: this.props.allPoints,
|
||||||
|
selectionPointType: this.props.designer.selectionPointType,
|
||||||
|
getConfigValue: this.props.getConfigValue,
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
setMapState: this.setMapState,
|
setMapState: this.setMapState,
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
|
@ -308,7 +349,7 @@ export class GardenMap extends
|
||||||
case Mode.moveTo:
|
case Mode.moveTo:
|
||||||
return () => { };
|
return () => { };
|
||||||
case Mode.boxSelect:
|
case Mode.boxSelect:
|
||||||
return this.props.designer.selectedPlants
|
return this.props.designer.selectedPoints
|
||||||
? () => { }
|
? () => { }
|
||||||
: closePlantInfo(this.props.dispatch);
|
: closePlantInfo(this.props.dispatch);
|
||||||
default:
|
default:
|
||||||
|
@ -362,6 +403,7 @@ export class GardenMap extends
|
||||||
botSize={this.props.botSize}
|
botSize={this.props.botSize}
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
groups={this.props.groups}
|
groups={this.props.groups}
|
||||||
|
startDrag={this.startDragOnBackground}
|
||||||
currentGroup={this.group?.uuid} />
|
currentGroup={this.group?.uuid} />
|
||||||
SensorReadingsLayer = () => <SensorReadingsLayer
|
SensorReadingsLayer = () => <SensorReadingsLayer
|
||||||
visible={!!this.props.showSensorReadings}
|
visible={!!this.props.showSensorReadings}
|
||||||
|
@ -385,7 +427,20 @@ export class GardenMap extends
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
hoveredPoint={this.props.designer.hoveredPoint}
|
hoveredPoint={this.props.designer.hoveredPoint}
|
||||||
visible={!!this.props.showPoints}
|
visible={!!this.props.showPoints}
|
||||||
|
interactions={this.interactions("GenericPointer")}
|
||||||
genericPoints={this.props.genericPoints} />
|
genericPoints={this.props.genericPoints} />
|
||||||
|
WeedLayer = () => <WeedLayer
|
||||||
|
mapTransformProps={this.mapTransformProps}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
hoveredPoint={this.props.designer.hoveredPoint}
|
||||||
|
visible={!!this.props.showWeeds}
|
||||||
|
spreadVisible={!!this.props.showSpread}
|
||||||
|
currentPoint={this.currentPoint}
|
||||||
|
boxSelected={this.props.designer.selectedPoints}
|
||||||
|
groupSelected={this.groupSelected}
|
||||||
|
interactions={this.interactions("Weed")}
|
||||||
|
weeds={this.props.weeds}
|
||||||
|
animate={this.animate} />
|
||||||
PlantLayer = () => <PlantLayer
|
PlantLayer = () => <PlantLayer
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
|
@ -395,10 +450,11 @@ export class GardenMap extends
|
||||||
hoveredPlant={this.props.hoveredPlant}
|
hoveredPlant={this.props.hoveredPlant}
|
||||||
dragging={!!this.state.isDragging}
|
dragging={!!this.state.isDragging}
|
||||||
editing={this.isEditing}
|
editing={this.isEditing}
|
||||||
boxSelected={this.props.designer.selectedPlants}
|
boxSelected={this.props.designer.selectedPoints}
|
||||||
groupSelected={this.pointsSelectedByGroup.map(point => point.uuid)}
|
groupSelected={this.groupSelected}
|
||||||
zoomLvl={this.props.zoomLvl}
|
zoomLvl={this.props.zoomLvl}
|
||||||
activeDragXY={this.state.activeDragXY}
|
activeDragXY={this.state.activeDragXY}
|
||||||
|
interactions={this.interactions("Plant")}
|
||||||
animate={this.animate} />
|
animate={this.animate} />
|
||||||
ToolSlotLayer = () => <ToolSlotLayer
|
ToolSlotLayer = () => <ToolSlotLayer
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
|
@ -406,6 +462,7 @@ export class GardenMap extends
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
hoveredToolSlot={this.props.designer.hoveredToolSlot}
|
hoveredToolSlot={this.props.designer.hoveredToolSlot}
|
||||||
botPositionX={this.props.botLocationData.position.x}
|
botPositionX={this.props.botLocationData.position.x}
|
||||||
|
interactions={this.interactions("ToolSlot")}
|
||||||
slots={this.props.toolSlots} />
|
slots={this.props.toolSlots} />
|
||||||
FarmBotLayer = () => <FarmBotLayer
|
FarmBotLayer = () => <FarmBotLayer
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
|
@ -443,8 +500,10 @@ export class GardenMap extends
|
||||||
chosenLocation={this.props.designer.chosenLocation}
|
chosenLocation={this.props.designer.chosenLocation}
|
||||||
mapTransformProps={this.mapTransformProps} />
|
mapTransformProps={this.mapTransformProps} />
|
||||||
DrawnPoint = () => <DrawnPoint
|
DrawnPoint = () => <DrawnPoint
|
||||||
data={this.props.designer.currentPoint}
|
data={this.props.designer.drawnPoint}
|
||||||
key={"currentPoint"}
|
mapTransformProps={this.mapTransformProps} />
|
||||||
|
DrawnWeed = () => <DrawnWeed
|
||||||
|
data={this.props.designer.drawnWeed}
|
||||||
mapTransformProps={this.mapTransformProps} />
|
mapTransformProps={this.mapTransformProps} />
|
||||||
GroupOrder = () => <GroupOrder
|
GroupOrder = () => <GroupOrder
|
||||||
group={this.group}
|
group={this.group}
|
||||||
|
@ -468,6 +527,7 @@ export class GardenMap extends
|
||||||
<this.SensorReadingsLayer />
|
<this.SensorReadingsLayer />
|
||||||
<this.SpreadLayer />
|
<this.SpreadLayer />
|
||||||
<this.PointLayer />
|
<this.PointLayer />
|
||||||
|
<this.WeedLayer />
|
||||||
<this.PlantLayer />
|
<this.PlantLayer />
|
||||||
<this.ToolSlotLayer />
|
<this.ToolSlotLayer />
|
||||||
<this.FarmBotLayer />
|
<this.FarmBotLayer />
|
||||||
|
@ -476,6 +536,7 @@ export class GardenMap extends
|
||||||
<this.SelectionBox />
|
<this.SelectionBox />
|
||||||
<this.TargetCoordinate />
|
<this.TargetCoordinate />
|
||||||
<this.DrawnPoint />
|
<this.DrawnPoint />
|
||||||
|
<this.DrawnWeed />
|
||||||
<this.GroupOrder />
|
<this.GroupOrder />
|
||||||
<this.NNPath />
|
<this.NNPath />
|
||||||
<this.Bugs />
|
<this.Bugs />
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
TaggedPlantPointer,
|
TaggedPlantPointer,
|
||||||
TaggedGenericPointer,
|
TaggedGenericPointer,
|
||||||
TaggedPlantTemplate,
|
TaggedPlantTemplate,
|
||||||
|
TaggedWeedPointer,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { State, BotOriginQuadrant } from "../interfaces";
|
import { State, BotOriginQuadrant } from "../interfaces";
|
||||||
import { BotPosition, BotLocationData } from "../../devices/interfaces";
|
import { BotPosition, BotLocationData } from "../../devices/interfaces";
|
||||||
|
@ -22,9 +23,10 @@ export interface PlantLayerProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
zoomLvl: number;
|
zoomLvl: number;
|
||||||
activeDragXY: BotPosition | undefined;
|
activeDragXY: BotPosition | undefined;
|
||||||
boxSelected: string[] | undefined;
|
boxSelected: UUID[] | undefined;
|
||||||
groupSelected: UUID[];
|
groupSelected: UUID[];
|
||||||
animate: boolean;
|
animate: boolean;
|
||||||
|
interactions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GardenMapLegendProps {
|
export interface GardenMapLegendProps {
|
||||||
|
@ -33,6 +35,7 @@ export interface GardenMapLegendProps {
|
||||||
legendMenuOpen: boolean;
|
legendMenuOpen: boolean;
|
||||||
showPlants: boolean;
|
showPlants: boolean;
|
||||||
showPoints: boolean;
|
showPoints: boolean;
|
||||||
|
showWeeds: boolean;
|
||||||
showSpread: boolean;
|
showSpread: boolean;
|
||||||
showFarmbot: boolean;
|
showFarmbot: boolean;
|
||||||
showImages: boolean;
|
showImages: boolean;
|
||||||
|
@ -80,6 +83,17 @@ export interface GardenPointProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GardenWeedProps {
|
||||||
|
mapTransformProps: MapTransformProps;
|
||||||
|
weed: TaggedWeedPointer;
|
||||||
|
hovered: boolean;
|
||||||
|
current: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
animate: boolean;
|
||||||
|
spreadVisible: boolean;
|
||||||
|
dispatch: Function;
|
||||||
|
}
|
||||||
|
|
||||||
interface DragHelpersBaseProps {
|
interface DragHelpersBaseProps {
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
|
@ -152,7 +166,9 @@ export enum Mode {
|
||||||
addPlant = "addPlant",
|
addPlant = "addPlant",
|
||||||
moveTo = "moveTo",
|
moveTo = "moveTo",
|
||||||
points = "points",
|
points = "points",
|
||||||
|
weeds = "weeds",
|
||||||
createPoint = "createPoint",
|
createPoint = "createPoint",
|
||||||
|
createWeed = "createWeed",
|
||||||
templateView = "templateView",
|
templateView = "templateView",
|
||||||
editGroup = "editGroup",
|
editGroup = "editGroup",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from "./farmbot/farmbot_layer";
|
export * from "./farmbot/farmbot_layer";
|
||||||
export * from "./plants/plant_layer";
|
export * from "./plants/plant_layer";
|
||||||
export * from "./points/point_layer";
|
export * from "./points/point_layer";
|
||||||
|
export * from "./weeds/weed_layer";
|
||||||
export * from "./spread/spread_layer";
|
export * from "./spread/spread_layer";
|
||||||
export * from "./tool_slots/tool_slot_layer";
|
export * from "./tool_slots/tool_slot_layer";
|
||||||
export * from "./images/image_layer";
|
export * from "./images/image_layer";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
let mockPath = "/app/designer/plants";
|
let mockPath = "/app/designer/plants";
|
||||||
jest.mock("../../../../../history", () => ({
|
jest.mock("../../../../../history", () => ({
|
||||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
getPathArray: jest.fn(() => mockPath.split("/"))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -31,6 +31,7 @@ describe("<PlantLayer/>", () => {
|
||||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||||
animate: true,
|
animate: true,
|
||||||
hoveredPlant: undefined,
|
hoveredPlant: undefined,
|
||||||
|
interactions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows plants", () => {
|
it("shows plants", () => {
|
||||||
|
@ -59,14 +60,19 @@ describe("<PlantLayer/>", () => {
|
||||||
it("is in clickable mode", () => {
|
it("is in clickable mode", () => {
|
||||||
mockPath = "/app/designer/plants";
|
mockPath = "/app/designer/plants";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = true;
|
||||||
|
p.plants[0].body.id = 1;
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||||
expect(wrapper.find("Link").props().style).toEqual({});
|
expect(wrapper.find("Link").props().style).toEqual({
|
||||||
|
cursor: "pointer"
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is in non-clickable mode", () => {
|
it("is in non-clickable mode", () => {
|
||||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
|
p.plants[0].body.id = 1;
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||||
expect(wrapper.find("Link").props().style)
|
expect(wrapper.find("Link").props().style)
|
||||||
.toEqual({ pointerEvents: "none" });
|
.toEqual({ pointerEvents: "none" });
|
||||||
|
@ -111,22 +117,12 @@ describe("<PlantLayer/>", () => {
|
||||||
expect(wrapper.find("GardenPlant").props().selected).toEqual(true);
|
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(<PlantLayer {...p} />);
|
|
||||||
expect((wrapper.find("Link").props()).style).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't allow clicking of unsaved plants", () => {
|
it("doesn't allow clicking of unsaved plants", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const plant = fakePlant();
|
p.interactions = false;
|
||||||
plant.body.id = 0;
|
p.plants[0].body.id = 0;
|
||||||
p.plants = [plant];
|
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||||
expect((wrapper.find("Link").props()).style)
|
expect(wrapper.find("Link").props().style)
|
||||||
.toEqual({ pointerEvents: "none" });
|
.toEqual({ pointerEvents: "none" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||||
import { GardenPlant } from "./garden_plant";
|
import { GardenPlant } from "./garden_plant";
|
||||||
import { PlantLayerProps, Mode } from "../../interfaces";
|
import { PlantLayerProps, Mode } from "../../interfaces";
|
||||||
import { unpackUUID } from "../../../../util";
|
import { unpackUUID } from "../../../../util";
|
||||||
import { maybeNoPointer, getMode } from "../../util";
|
import { getMode } from "../../util";
|
||||||
import { Link } from "../../../../link";
|
import { Link } from "../../../../link";
|
||||||
|
|
||||||
export function PlantLayer(props: PlantLayerProps) {
|
export function PlantLayer(props: PlantLayerProps) {
|
||||||
|
@ -44,9 +44,12 @@ export function PlantLayer(props: PlantLayerProps) {
|
||||||
activeDragXY={activeDragXY}
|
activeDragXY={activeDragXY}
|
||||||
hovered={hovered}
|
hovered={hovered}
|
||||||
animate={animate} />;
|
animate={animate} />;
|
||||||
|
const style: React.SVGProps<SVGGElement>["style"] =
|
||||||
|
(props.interactions && p.body.id)
|
||||||
|
? { cursor: "pointer" } : { pointerEvents: "none" };
|
||||||
const wrapperProps = {
|
const wrapperProps = {
|
||||||
className: "plant-link-wrapper",
|
className: "plant-link-wrapper",
|
||||||
style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
|
style,
|
||||||
key: p.uuid,
|
key: p.uuid,
|
||||||
};
|
};
|
||||||
return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect)
|
return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect)
|
||||||
|
|
|
@ -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 * as React from "react";
|
||||||
import { GardenPoint } from "../garden_point";
|
import { GardenPoint } from "../garden_point";
|
||||||
|
@ -55,10 +58,9 @@ describe("<GardenPoint/>", () => {
|
||||||
|
|
||||||
it("opens point info", () => {
|
it("opens point info", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.point.body.name = "weed";
|
|
||||||
const wrapper = svgMount(<GardenPoint {...p} />);
|
const wrapper = svgMount(<GardenPoint {...p} />);
|
||||||
wrapper.find("g").simulate("click");
|
wrapper.find("g").simulate("click");
|
||||||
expect(history.push).toHaveBeenCalledWith(
|
expect(history.push).toHaveBeenCalledWith(
|
||||||
`/app/designer/weeds/${p.point.body.id}`);
|
`/app/designer/points/${p.point.body.id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,10 +19,12 @@ describe("<PointLayer/>", () => {
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
hoveredPoint: undefined,
|
hoveredPoint: undefined,
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
|
interactions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows points", () => {
|
it("shows points", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
const wrapper = svgMount(<PointLayer {...p} />);
|
const wrapper = svgMount(<PointLayer {...p} />);
|
||||||
const layer = wrapper.find("#point-layer");
|
const layer = wrapper.find("#point-layer");
|
||||||
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
|
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
|
||||||
|
@ -40,6 +42,7 @@ describe("<PointLayer/>", () => {
|
||||||
it("allows point mode interaction", () => {
|
it("allows point mode interaction", () => {
|
||||||
mockPath = "/app/designer/points";
|
mockPath = "/app/designer/points";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = true;
|
||||||
const wrapper = svgMount(<PointLayer {...p} />);
|
const wrapper = svgMount(<PointLayer {...p} />);
|
||||||
const layer = wrapper.find("#point-layer");
|
const layer = wrapper.find("#point-layer");
|
||||||
expect(layer.props().style).toEqual({});
|
expect(layer.props().style).toEqual({});
|
||||||
|
|
|
@ -2,8 +2,7 @@ import * as React from "react";
|
||||||
import { GardenPointProps } from "../../interfaces";
|
import { GardenPointProps } from "../../interfaces";
|
||||||
import { transformXY } from "../../util";
|
import { transformXY } from "../../util";
|
||||||
import { Actions } from "../../../../constants";
|
import { Actions } from "../../../../constants";
|
||||||
import { history } from "../../../../history";
|
import { mapPointClickAction } from "../../actions";
|
||||||
import { isAWeed } from "../../../points/weeds_inventory";
|
|
||||||
|
|
||||||
export const GardenPoint = (props: GardenPointProps) => {
|
export const GardenPoint = (props: GardenPointProps) => {
|
||||||
|
|
||||||
|
@ -19,11 +18,11 @@ export const GardenPoint = (props: GardenPointProps) => {
|
||||||
const { id, x, y, meta } = point.body;
|
const { id, x, y, meta } = point.body;
|
||||||
const { qx, qy } = transformXY(x, y, mapTransformProps);
|
const { qx, qy } = transformXY(x, y, mapTransformProps);
|
||||||
const color = meta.color || "green";
|
const color = meta.color || "green";
|
||||||
const panel = isAWeed(point.body.name, meta.type) ? "weeds" : "points";
|
return <g id={`point-${id}`} className={"map-point"} stroke={color}
|
||||||
return <g id={"point-" + id} className={"map-point"} stroke={color}
|
|
||||||
onMouseEnter={iconHover("start")}
|
onMouseEnter={iconHover("start")}
|
||||||
onMouseLeave={iconHover("end")}
|
onMouseLeave={iconHover("end")}
|
||||||
onClick={() => history.push(`/app/designer/${panel}/${id}`)}>
|
onClick={mapPointClickAction(props.dispatch, point.uuid,
|
||||||
|
`/app/designer/points/${id}`)}>
|
||||||
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius}
|
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius}
|
||||||
fill={hovered ? color : "transparent"} />
|
fill={hovered ? color : "transparent"} />
|
||||||
<circle id="point-center" cx={qx} cy={qy} r={2} />
|
<circle id="point-center" cx={qx} cy={qy} r={2} />
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { TaggedGenericPointer } from "farmbot";
|
import { TaggedGenericPointer } from "farmbot";
|
||||||
import { GardenPoint } from "./garden_point";
|
import { GardenPoint } from "./garden_point";
|
||||||
import { MapTransformProps, Mode } from "../../interfaces";
|
import { MapTransformProps } from "../../interfaces";
|
||||||
import { getMode } from "../../util";
|
|
||||||
|
|
||||||
export interface PointLayerProps {
|
export interface PointLayerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -10,13 +9,14 @@ export interface PointLayerProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
hoveredPoint: string | undefined;
|
hoveredPoint: string | undefined;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
interactions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PointLayer(props: PointLayerProps) {
|
export function PointLayer(props: PointLayerProps) {
|
||||||
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
|
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
|
||||||
const style: React.CSSProperties =
|
const style: React.CSSProperties =
|
||||||
getMode() === Mode.points ? {} : { pointerEvents: "none" };
|
props.interactions ? {} : { pointerEvents: "none" };
|
||||||
return <g id="point-layer" style={style}>
|
return <g id={"point-layer"} style={style}>
|
||||||
{visible &&
|
{visible &&
|
||||||
genericPoints.map(p =>
|
genericPoints.map(p =>
|
||||||
<GardenPoint
|
<GardenPoint
|
||||||
|
|
|
@ -38,6 +38,7 @@ describe("<ToolSlotLayer/>", () => {
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
hoveredToolSlot: undefined,
|
hoveredToolSlot: undefined,
|
||||||
|
interactions: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
it("toggles visibility off", () => {
|
it("toggles visibility off", () => {
|
||||||
|
@ -61,9 +62,19 @@ describe("<ToolSlotLayer/>", () => {
|
||||||
expect(history.push).not.toHaveBeenCalled();
|
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(<ToolSlotLayer {...p} />);
|
||||||
|
expect(wrapper.find("g").props().style)
|
||||||
|
.toEqual({ cursor: "pointer" });
|
||||||
|
});
|
||||||
|
|
||||||
it("is in non-clickable mode", () => {
|
it("is in non-clickable mode", () => {
|
||||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||||
expect(wrapper.find("g").props().style)
|
expect(wrapper.find("g").props().style)
|
||||||
.toEqual({ pointerEvents: "none" });
|
.toEqual({ pointerEvents: "none" });
|
||||||
|
|
|
@ -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 * as React from "react";
|
||||||
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
|
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
|
||||||
|
|
|
@ -2,7 +2,6 @@ import * as React from "react";
|
||||||
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
|
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
|
||||||
import { ToolSlotPoint } from "./tool_slot_point";
|
import { ToolSlotPoint } from "./tool_slot_point";
|
||||||
import { MapTransformProps } from "../../interfaces";
|
import { MapTransformProps } from "../../interfaces";
|
||||||
import { maybeNoPointer } from "../../util";
|
|
||||||
|
|
||||||
export interface ToolSlotLayerProps {
|
export interface ToolSlotLayerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -11,6 +10,7 @@ export interface ToolSlotLayerProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
hoveredToolSlot: UUID | undefined;
|
hoveredToolSlot: UUID | undefined;
|
||||||
|
interactions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
||||||
|
@ -18,7 +18,9 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
||||||
|
|
||||||
return <g
|
return <g
|
||||||
id="toolslot-layer"
|
id="toolslot-layer"
|
||||||
style={maybeNoPointer({ cursor: "pointer" })}>
|
style={props.interactions
|
||||||
|
? { cursor: "pointer" }
|
||||||
|
: { pointerEvents: "none" }}>
|
||||||
{visible &&
|
{visible &&
|
||||||
slots.map(slot =>
|
slots.map(slot =>
|
||||||
<ToolSlotPoint
|
<ToolSlotPoint
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { MapTransformProps } from "../../interfaces";
|
||||||
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
|
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
|
||||||
import { ToolLabel } from "./tool_label";
|
import { ToolLabel } from "./tool_label";
|
||||||
import { includes } from "lodash";
|
import { includes } from "lodash";
|
||||||
import { history } from "../../../../history";
|
|
||||||
import { t } from "../../../../i18next_wrapper";
|
import { t } from "../../../../i18next_wrapper";
|
||||||
|
import { mapPointClickAction } from "../../actions";
|
||||||
|
|
||||||
export interface TSPProps {
|
export interface TSPProps {
|
||||||
slot: SlotWithTool;
|
slot: SlotWithTool;
|
||||||
|
@ -30,25 +30,27 @@ export const reduceToolName = (raw: string | undefined) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolSlotPoint = (props: TSPProps) => {
|
export const ToolSlotPoint = (props: TSPProps) => {
|
||||||
|
const { tool, toolSlot } = props.slot;
|
||||||
const {
|
const {
|
||||||
id, x, y, pullout_direction, gantry_mounted
|
id, x, y, pullout_direction, gantry_mounted
|
||||||
} = props.slot.toolSlot.body;
|
} = toolSlot.body;
|
||||||
const { mapTransformProps, botPositionX } = props;
|
const { mapTransformProps, botPositionX } = props;
|
||||||
const { quadrant, xySwap } = mapTransformProps;
|
const { quadrant, xySwap } = mapTransformProps;
|
||||||
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
|
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
|
||||||
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
|
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
|
||||||
const toolName = props.slot.tool ? props.slot.tool.body.name : t("Empty");
|
const toolName = tool ? tool.body.name : t("Empty");
|
||||||
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
|
const hovered = toolSlot.uuid === props.hoveredToolSlot;
|
||||||
const toolProps = {
|
const toolProps = {
|
||||||
x: qx,
|
x: qx,
|
||||||
y: qy,
|
y: qy,
|
||||||
hovered,
|
hovered,
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
uuid: props.slot.toolSlot.uuid,
|
uuid: toolSlot.uuid,
|
||||||
xySwap,
|
xySwap,
|
||||||
};
|
};
|
||||||
return <g id={"toolslot-" + id}
|
return <g id={"toolslot-" + id}
|
||||||
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}>
|
onClick={mapPointClickAction(props.dispatch, toolSlot.uuid,
|
||||||
|
`/app/designer/tool-slots/${id}`)}>
|
||||||
{pullout_direction && !gantry_mounted &&
|
{pullout_direction && !gantry_mounted &&
|
||||||
<ToolbaySlot
|
<ToolbaySlot
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
jest.mock("../../../../../history", () => ({
|
||||||
|
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("<GardenWeed />", () => {
|
||||||
|
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(<GardenWeed {...p} />);
|
||||||
|
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(<GardenWeed {...p} />);
|
||||||
|
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(<GardenWeed {...p} />);
|
||||||
|
expect(wrapper.find(".soil-cloud").length).toEqual(1);
|
||||||
|
expect(wrapper.find("image").hasClass("animate")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovers weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
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(<GardenWeed {...p} />);
|
||||||
|
expect(wrapper.find("#weed-radius").props().opacity).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("un-hovers weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
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(<GardenWeed {...p} />);
|
||||||
|
wrapper.find("g").first().simulate("click");
|
||||||
|
expect(history.push).toHaveBeenCalledWith(
|
||||||
|
`/app/designer/weeds/${p.weed.body.id}`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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("<WeedLayer/>", () => {
|
||||||
|
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(<WeedLayer {...p} />);
|
||||||
|
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(<WeedLayer {...p} />);
|
||||||
|
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(<WeedLayer {...p} />);
|
||||||
|
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(<WeedLayer {...p} />);
|
||||||
|
const layer = wrapper.find("#weeds-layer");
|
||||||
|
expect(layer.find(GardenWeed).props().selected).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 <g id={`weed-${id}`} className={`map-weed ${color}`}
|
||||||
|
onMouseEnter={iconHover("start")}
|
||||||
|
onMouseLeave={iconHover("end")}
|
||||||
|
onClick={mapPointClickAction(props.dispatch, weed.uuid,
|
||||||
|
`/app/designer/weeds/${id}`)}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id={`Weed${id}Gradient`}>
|
||||||
|
<stop offset="90%" stopColor={color} stopOpacity={stopOpacity} />
|
||||||
|
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{animate &&
|
||||||
|
<circle
|
||||||
|
className="soil-cloud"
|
||||||
|
cx={qx}
|
||||||
|
cy={qy}
|
||||||
|
r={radius}
|
||||||
|
fill={Color.soilCloud}
|
||||||
|
fillOpacity={0} />}
|
||||||
|
|
||||||
|
{props.spreadVisible &&
|
||||||
|
<circle
|
||||||
|
id={"weed-radius"}
|
||||||
|
cx={qx}
|
||||||
|
cy={qy}
|
||||||
|
r={radius}
|
||||||
|
fill={`url(#Weed${id}Gradient)`}
|
||||||
|
opacity={hovered ? 1 : 0.5} />}
|
||||||
|
|
||||||
|
<g id="weed-icon">
|
||||||
|
<image
|
||||||
|
className={className}
|
||||||
|
xlinkHref={DEFAULT_WEED_ICON}
|
||||||
|
height={iconRadius * 2}
|
||||||
|
width={iconRadius * 2}
|
||||||
|
x={qx - iconRadius}
|
||||||
|
y={qy - iconRadius} />
|
||||||
|
</g>
|
||||||
|
</g>;
|
||||||
|
};
|
|
@ -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 <g id={"weeds-layer"} style={props.interactions
|
||||||
|
? { cursor: "pointer" } : { pointerEvents: "none" }}>
|
||||||
|
{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 <GardenWeed
|
||||||
|
weed={p}
|
||||||
|
key={p.uuid}
|
||||||
|
hovered={hovered}
|
||||||
|
current={current}
|
||||||
|
selected={selectedByBox || selectedByGroup}
|
||||||
|
animate={props.animate}
|
||||||
|
spreadVisible={props.spreadVisible}
|
||||||
|
dispatch={props.dispatch}
|
||||||
|
mapTransformProps={mapTransformProps} />;
|
||||||
|
})}
|
||||||
|
</g>;
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ describe("<ZonesLayer />", () => {
|
||||||
y: { value: 1500, isDefault: true }
|
y: { value: 1500, isDefault: true }
|
||||||
},
|
},
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
startDrag: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
@ -70,15 +71,15 @@ describe("<ZonesLayer />", () => {
|
||||||
p.groups[0].body.id = 1;
|
p.groups[0].body.id = 1;
|
||||||
p.currentGroup = p.groups[0].uuid;
|
p.currentGroup = p.groups[0].uuid;
|
||||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||||
expect(wrapper.html())
|
expect(wrapper.html()).toEqual(
|
||||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
"<svg><g class=\"zones-layer\" style=\"cursor: pointer;\"></g></svg>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't render current group's zones", () => {
|
it("doesn't render current group's zones", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.visible = false;
|
p.visible = false;
|
||||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||||
expect(wrapper.html())
|
expect(wrapper.html()).toEqual(
|
||||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
"<svg><g class=\"zones-layer\" style=\"cursor: pointer;\"></g></svg>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { MapTransformProps, BotSize } from "../../interfaces";
|
||||||
import { transformXY } from "../../util";
|
import { transformXY } from "../../util";
|
||||||
import { isUndefined } from "lodash";
|
import { isUndefined } from "lodash";
|
||||||
import { UUID } from "../../../../resources/interfaces";
|
import { UUID } from "../../../../resources/interfaces";
|
||||||
|
import { history } from "../../../../history";
|
||||||
|
|
||||||
export interface ZonesProps {
|
export interface ZonesProps {
|
||||||
currentGroup: UUID | undefined;
|
currentGroup: UUID | undefined;
|
||||||
|
@ -43,6 +44,9 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => {
|
||||||
return ZoneType.none;
|
return ZoneType.none;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openGroup = (id: number | undefined) =>
|
||||||
|
() => history.push(`/app/designer/groups/${id}`);
|
||||||
|
|
||||||
/** Bounds for area selected by criteria or bot extents. */
|
/** Bounds for area selected by criteria or bot extents. */
|
||||||
const getBoundary = (props: GetBoundaryProps): Boundary => {
|
const getBoundary = (props: GetBoundaryProps): Boundary => {
|
||||||
const { criteria } = props.group.body;
|
const { criteria } = props.group.body;
|
||||||
|
@ -85,7 +89,8 @@ const zone0D = (props: ZonesProps) =>
|
||||||
/** Coordinates selected by both x and y number equal values. */
|
/** Coordinates selected by both x and y number equal values. */
|
||||||
export const Zones0D = (props: ZonesProps) => {
|
export const Zones0D = (props: ZonesProps) => {
|
||||||
const current = props.group.uuid == props.currentGroup;
|
const current = props.group.uuid == props.currentGroup;
|
||||||
return <g id={`zones-0D-${props.group.body.id}`}
|
const { id } = props.group.body;
|
||||||
|
return <g id={`zones-0D-${id}`} onClick={openGroup(id)}
|
||||||
className={current ? "current" : ""}>
|
className={current ? "current" : ""}>
|
||||||
{zone0D(props).map((point, i) =>
|
{zone0D(props).map((point, i) =>
|
||||||
<circle key={i} cx={point.x} cy={point.y} r={5} />)}
|
<circle key={i} cx={point.x} cy={point.y} r={5} />)}
|
||||||
|
@ -126,7 +131,8 @@ const zone1D = (props: ZonesProps) => {
|
||||||
/** Lines selected by an x or y number equal value. */
|
/** Lines selected by an x or y number equal value. */
|
||||||
export const Zones1D = (props: ZonesProps) => {
|
export const Zones1D = (props: ZonesProps) => {
|
||||||
const current = props.group.uuid == props.currentGroup;
|
const current = props.group.uuid == props.currentGroup;
|
||||||
return <g id={`zones-1D-${props.group.body.id}`}
|
const { id } = props.group.body;
|
||||||
|
return <g id={`zones-1D-${id}`} onClick={openGroup(id)}
|
||||||
className={current ? "current" : ""}>
|
className={current ? "current" : ""}>
|
||||||
{zone1D(props).map((line, i) =>
|
{zone1D(props).map((line, i) =>
|
||||||
<line key={i} x1={line.x1} y1={line.y1}
|
<line key={i} x1={line.x1} y1={line.y1}
|
||||||
|
@ -153,7 +159,8 @@ const zone2D = (boundary: Boundary, mapTransformProps: MapTransformProps) => {
|
||||||
export const Zones2D = (props: ZonesProps) => {
|
export const Zones2D = (props: ZonesProps) => {
|
||||||
const zone = zone2D(getBoundary(props), props.mapTransformProps);
|
const zone = zone2D(getBoundary(props), props.mapTransformProps);
|
||||||
const current = props.group.uuid == props.currentGroup;
|
const current = props.group.uuid == props.currentGroup;
|
||||||
return <g id={`zones-2D-${props.group.body.id}`}
|
const { id } = props.group.body;
|
||||||
|
return <g id={`zones-2D-${id}`} onClick={openGroup(id)}
|
||||||
className={current ? "current" : ""}>
|
className={current ? "current" : ""}>
|
||||||
{!zone.selectsAll &&
|
{!zone.selectsAll &&
|
||||||
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height} />}
|
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height} />}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { TaggedPointGroup } from "farmbot";
|
||||||
import { MapTransformProps, BotSize } from "../../interfaces";
|
import { MapTransformProps, BotSize } from "../../interfaces";
|
||||||
import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones";
|
import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones";
|
||||||
import { UUID } from "../../../../resources/interfaces";
|
import { UUID } from "../../../../resources/interfaces";
|
||||||
|
import { allowGroupAreaInteraction } from "../../util";
|
||||||
|
|
||||||
export interface ZonesLayerProps {
|
export interface ZonesLayerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -10,6 +11,7 @@ export interface ZonesLayerProps {
|
||||||
groups: TaggedPointGroup[];
|
groups: TaggedPointGroup[];
|
||||||
botSize: BotSize;
|
botSize: BotSize;
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
|
startDrag(e: React.MouseEvent<SVGElement>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ZonesLayer(props: ZonesLayerProps) {
|
export function ZonesLayer(props: ZonesLayerProps) {
|
||||||
|
@ -17,7 +19,9 @@ export function ZonesLayer(props: ZonesLayerProps) {
|
||||||
const commonProps = { botSize, mapTransformProps, currentGroup };
|
const commonProps = { botSize, mapTransformProps, currentGroup };
|
||||||
const visible = (group: TaggedPointGroup) =>
|
const visible = (group: TaggedPointGroup) =>
|
||||||
props.visible || (group.uuid == currentGroup);
|
props.visible || (group.uuid == currentGroup);
|
||||||
return <g className="zones-layer">
|
return <g className="zones-layer" style={allowGroupAreaInteraction()
|
||||||
|
? { cursor: "pointer" }
|
||||||
|
: { pointerEvents: "none" }} onMouseDown={props.startDrag}>
|
||||||
{groups.map(group => visible(group) &&
|
{groups.map(group => visible(group) &&
|
||||||
getZoneType(group) === ZoneType.area &&
|
getZoneType(group) === ZoneType.area &&
|
||||||
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
||||||
|
|
|
@ -35,6 +35,7 @@ describe("<GardenMapLegend />", () => {
|
||||||
legendMenuOpen: true,
|
legendMenuOpen: true,
|
||||||
showPlants: false,
|
showPlants: false,
|
||||||
showPoints: false,
|
showPoints: false,
|
||||||
|
showWeeds: false,
|
||||||
showSpread: false,
|
showSpread: false,
|
||||||
showFarmbot: false,
|
showFarmbot: false,
|
||||||
showImages: false,
|
showImages: false,
|
||||||
|
|
|
@ -59,6 +59,10 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
||||||
popover={DevSettings.futureFeaturesEnabled()
|
popover={DevSettings.futureFeaturesEnabled()
|
||||||
? <PointsSubMenu toggle={toggle} getConfigValue={getConfigValue} />
|
? <PointsSubMenu toggle={toggle} getConfigValue={getConfigValue} />
|
||||||
: undefined} />
|
: undefined} />
|
||||||
|
<LayerToggle
|
||||||
|
value={props.showWeeds}
|
||||||
|
label={t("Weeds?")}
|
||||||
|
onClick={toggle(BooleanSetting.show_weeds)} />
|
||||||
<LayerToggle
|
<LayerToggle
|
||||||
value={props.showSpread}
|
value={props.showSpread}
|
||||||
label={t("Spread?")}
|
label={t("Spread?")}
|
||||||
|
|
|
@ -299,10 +299,14 @@ export const getMode = (): Mode => {
|
||||||
if (pathArray[4] === "select") { return Mode.boxSelect; }
|
if (pathArray[4] === "select") { return Mode.boxSelect; }
|
||||||
if (pathArray[4] === "crop_search") { return Mode.addPlant; }
|
if (pathArray[4] === "crop_search") { return Mode.addPlant; }
|
||||||
if (pathArray[3] === "move_to") { return Mode.moveTo; }
|
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; }
|
if (pathArray[4] === "add") { return Mode.createPoint; }
|
||||||
return Mode.points;
|
return Mode.points;
|
||||||
}
|
}
|
||||||
|
if (pathArray[3] === "weeds") {
|
||||||
|
if (pathArray[4] === "add") { return Mode.createWeed; }
|
||||||
|
return Mode.weeds;
|
||||||
|
}
|
||||||
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
|
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
|
||||||
}
|
}
|
||||||
return Mode.none;
|
return Mode.none;
|
||||||
|
@ -337,18 +341,28 @@ export const getGardenCoordinates = (props: {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maybeNoPointer =
|
export const allowInteraction = () => {
|
||||||
(defaultStyle: React.CSSProperties): React.SVGProps<SVGGElement>["style"] => {
|
switch (getMode()) {
|
||||||
switch (getMode()) {
|
case Mode.clickToAdd:
|
||||||
case Mode.clickToAdd:
|
case Mode.moveTo:
|
||||||
case Mode.moveTo:
|
case Mode.createPoint:
|
||||||
case Mode.points:
|
case Mode.createWeed:
|
||||||
case Mode.createPoint:
|
return false;
|
||||||
return { pointerEvents: "none" };
|
default:
|
||||||
default:
|
return true;
|
||||||
return defaultStyle;
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
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. */
|
/** Check if the cursor is within the selected plant indicator area. */
|
||||||
export const cursorAtPlant =
|
export const cursorAtPlant =
|
||||||
|
|
|
@ -2,7 +2,17 @@ jest.mock("../../../open_farm/cached_crop", () => ({
|
||||||
maybeGetCachedPlantIcon: jest.fn(),
|
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 * as React from "react";
|
||||||
import {
|
import {
|
||||||
|
@ -12,9 +22,11 @@ import { shallow, mount } from "enzyme";
|
||||||
import {
|
import {
|
||||||
fakePlant, fakePlantTemplate,
|
fakePlant, fakePlantTemplate,
|
||||||
} from "../../../__test_support__/fake_state/resources";
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
import { Actions } from "../../../constants";
|
|
||||||
import { push } from "../../../history";
|
import { push } from "../../../history";
|
||||||
import { maybeGetCachedPlantIcon } from "../../../open_farm/cached_crop";
|
import { maybeGetCachedPlantIcon } from "../../../open_farm/cached_crop";
|
||||||
|
import {
|
||||||
|
mapPointClickAction, setHoveredPlant, selectPoint,
|
||||||
|
} from "../../map/actions";
|
||||||
|
|
||||||
describe("<PlantInventoryItem />", () => {
|
describe("<PlantInventoryItem />", () => {
|
||||||
const fakeProps = (): PlantInventoryItemProps => ({
|
const fakeProps = (): PlantInventoryItemProps => ({
|
||||||
|
@ -40,48 +52,43 @@ describe("<PlantInventoryItem />", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||||
wrapper.simulate("mouseEnter");
|
wrapper.simulate("mouseEnter");
|
||||||
expect(p.dispatch).toBeCalledWith({
|
expect(setHoveredPlant).toBeCalledWith(p.plant.uuid, "");
|
||||||
payload: {
|
|
||||||
icon: "",
|
|
||||||
plantUUID: p.plant.uuid
|
|
||||||
},
|
|
||||||
type: Actions.TOGGLE_HOVERED_PLANT
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hover end", () => {
|
it("hover end", () => {
|
||||||
const p = fakeProps();
|
const wrapper = shallow(<PlantInventoryItem {...fakeProps()} />);
|
||||||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
|
||||||
wrapper.simulate("mouseLeave");
|
wrapper.simulate("mouseLeave");
|
||||||
expect(p.dispatch).toBeCalledWith({
|
expect(setHoveredPlant).toBeCalledWith(undefined, "");
|
||||||
payload: {
|
|
||||||
icon: "",
|
|
||||||
plantUUID: undefined
|
|
||||||
},
|
|
||||||
type: Actions.TOGGLE_HOVERED_PLANT
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects plant", () => {
|
it("selects plant", () => {
|
||||||
|
mockPath = "/app/designer/plants";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||||
wrapper.simulate("click");
|
wrapper.simulate("click");
|
||||||
expect(p.dispatch).toBeCalledWith({
|
expect(mapPointClickAction).not.toHaveBeenCalled();
|
||||||
payload: [p.plant.uuid],
|
expect(selectPoint).toBeCalledWith([p.plant.uuid]);
|
||||||
type: Actions.SELECT_PLANT
|
|
||||||
});
|
|
||||||
expect(push).toHaveBeenCalledWith("/app/designer/plants/" + p.plant.body.id);
|
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(<PlantInventoryItem {...p} />);
|
||||||
|
wrapper.simulate("click");
|
||||||
|
expect(mapPointClickAction).toHaveBeenCalledWith(expect.any(Function),
|
||||||
|
p.plant.uuid);
|
||||||
|
expect(push).not.toHaveBeenCalled();
|
||||||
|
expect(setHoveredPlant).toHaveBeenCalledWith(undefined, "");
|
||||||
|
});
|
||||||
|
|
||||||
it("selects plant template", () => {
|
it("selects plant template", () => {
|
||||||
|
mockPath = "/app/designer/plants";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.plant = fakePlantTemplate();
|
p.plant = fakePlantTemplate();
|
||||||
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
const wrapper = shallow(<PlantInventoryItem {...p} />);
|
||||||
wrapper.simulate("click");
|
wrapper.simulate("click");
|
||||||
expect(p.dispatch).toBeCalledWith({
|
expect(selectPoint).toBeCalledWith([p.plant.uuid]);
|
||||||
payload: [p.plant.uuid],
|
|
||||||
type: Actions.SELECT_PLANT
|
|
||||||
});
|
|
||||||
expect(push).toHaveBeenCalledWith(
|
expect(push).toHaveBeenCalledWith(
|
||||||
"/app/designer/gardens/templates/" + p.plant.body.id);
|
"/app/designer/gardens/templates/" + p.plant.body.id);
|
||||||
});
|
});
|
||||||
|
@ -94,4 +101,12 @@ describe("<PlantInventoryItem />", () => {
|
||||||
expect(maybeGetCachedPlantIcon).toHaveBeenCalledWith("strawberry",
|
expect(maybeGetCachedPlantIcon).toHaveBeenCalledWith("strawberry",
|
||||||
img.instance(), expect.any(Function));
|
img.instance(), expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets icon", () => {
|
||||||
|
const wrapper =
|
||||||
|
mount<PlantInventoryItem>(<PlantInventoryItem {...fakeProps()} />);
|
||||||
|
expect(wrapper.state().icon).toEqual("");
|
||||||
|
wrapper.instance().updateStateIcon("fake icon");
|
||||||
|
expect(wrapper.state().icon).toEqual("fake icon");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,11 +15,15 @@ jest.mock("../../../account/dev/dev_support", () => ({
|
||||||
jest.mock("../../point_groups/actions", () => ({ createGroup: jest.fn() }));
|
jest.mock("../../point_groups/actions", () => ({ createGroup: jest.fn() }));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { mount } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
import {
|
import {
|
||||||
RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps,
|
RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps,
|
||||||
|
getFilteredPoints, GetFilteredPointsProps,
|
||||||
} from "../select_plants";
|
} 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 { Actions, Content } from "../../../constants";
|
||||||
import { clickButton } from "../../../__test_support__/helpers";
|
import { clickButton } from "../../../__test_support__/helpers";
|
||||||
import { destroy } from "../../../api/crud";
|
import { destroy } from "../../../api/crud";
|
||||||
|
@ -41,9 +45,16 @@ describe("<SelectPlants />", () => {
|
||||||
plant2.body.name = "Blueberry";
|
plant2.body.name = "Blueberry";
|
||||||
return {
|
return {
|
||||||
selected: ["plant.1"],
|
selected: ["plant.1"],
|
||||||
|
selectionPointType: undefined,
|
||||||
|
getConfigValue: () => true,
|
||||||
plants: [plant1, plant2],
|
plants: [plant1, plant2],
|
||||||
dispatch: jest.fn(x => x),
|
dispatch: jest.fn(x => x),
|
||||||
gardenOpen: undefined,
|
gardenOpen: undefined,
|
||||||
|
allPoints: [],
|
||||||
|
xySwap: false,
|
||||||
|
quadrant: 2,
|
||||||
|
isActive: () => false,
|
||||||
|
tools: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +63,53 @@ describe("<SelectPlants />", () => {
|
||||||
expect(wrapper.text()).toContain("Strawberry");
|
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(<SelectPlants {...p} />);
|
||||||
|
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(<SelectPlants {...p} />);
|
||||||
|
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(<SelectPlants {...p} />);
|
||||||
|
expect(wrapper.text()).toContain(tool.body.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears point section type", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
|
wrapper.unmount();
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("displays multiple selected plants", () => {
|
it("displays multiple selected plants", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.selected = ["plant.1", "plant.2"];
|
p.selected = ["plant.1", "plant.2"];
|
||||||
|
@ -88,31 +146,42 @@ describe("<SelectPlants />", () => {
|
||||||
expect(wrapper.text()).not.toContain("Strawberry Plant");
|
expect(wrapper.text()).not.toContain("Strawberry Plant");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("changes selection type", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
|
||||||
|
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", () => {
|
it("selects all", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.dispatch = jest.fn();
|
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
clickButton(wrapper, 1, "select all");
|
clickButton(wrapper, 2, "select all");
|
||||||
expect(p.dispatch).toHaveBeenCalledWith(
|
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", () => {
|
it("selects none", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.dispatch = jest.fn();
|
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
clickButton(wrapper, 0, "select none");
|
clickButton(wrapper, 1, "select none");
|
||||||
expect(p.dispatch).toHaveBeenCalledWith(
|
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", () => {
|
it("confirms deletion of selected plants", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.selected = ["plant.1", "plant.2"];
|
p.selected = ["plant.1", "plant.2"];
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
expect(wrapper.text()).toContain("Delete");
|
|
||||||
window.confirm = jest.fn();
|
window.confirm = jest.fn();
|
||||||
wrapper.find("button").at(2).simulate("click");
|
clickButton(wrapper, DELETE_BTN_INDEX, "Delete");
|
||||||
expect(window.confirm).toHaveBeenCalledWith(
|
expect(window.confirm).toHaveBeenCalledWith(
|
||||||
"Are you sure you want to delete 2 plants?");
|
"Are you sure you want to delete 2 plants?");
|
||||||
});
|
});
|
||||||
|
@ -122,9 +191,8 @@ describe("<SelectPlants />", () => {
|
||||||
mockDestroy = jest.fn(() => Promise.resolve());
|
mockDestroy = jest.fn(() => Promise.resolve());
|
||||||
p.selected = ["plant.1", "plant.2"];
|
p.selected = ["plant.1", "plant.2"];
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
expect(wrapper.text()).toContain("Delete");
|
|
||||||
window.confirm = () => true;
|
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.1", true);
|
||||||
expect(destroy).toHaveBeenCalledWith("plant.2", true);
|
expect(destroy).toHaveBeenCalledWith("plant.2", true);
|
||||||
});
|
});
|
||||||
|
@ -134,19 +202,17 @@ describe("<SelectPlants />", () => {
|
||||||
mockDestroy = jest.fn(() => Promise.resolve());
|
mockDestroy = jest.fn(() => Promise.resolve());
|
||||||
p.selected = undefined;
|
p.selected = undefined;
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
expect(wrapper.text()).toContain("Delete");
|
clickButton(wrapper, DELETE_BTN_INDEX, "Delete");
|
||||||
wrapper.find("button").at(2).simulate("click");
|
|
||||||
expect(destroy).not.toHaveBeenCalled();
|
expect(destroy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("errors when deleting selected plants", () => {
|
it("errors when deleting selected plants", async () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
mockDestroy = jest.fn(() => Promise.reject());
|
mockDestroy = jest.fn(() => Promise.reject());
|
||||||
p.selected = ["plant.1", "plant.2"];
|
p.selected = ["plant.1", "plant.2"];
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
expect(wrapper.text()).toContain("Delete");
|
|
||||||
window.confirm = () => true;
|
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.1", true);
|
||||||
expect(destroy).toHaveBeenCalledWith("plant.2", true);
|
expect(destroy).toHaveBeenCalledWith("plant.2", true);
|
||||||
});
|
});
|
||||||
|
@ -183,3 +249,43 @@ describe("mapStateToProps", () => {
|
||||||
expect(result.dispatch).toBe(state.dispatch);
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DEFAULT_ICON } from "../../open_farm/icons";
|
import { DEFAULT_ICON } from "../../open_farm/icons";
|
||||||
import { push } from "../../history";
|
import { push } from "../../history";
|
||||||
import { TaggedPlant } from "../map/interfaces";
|
import { TaggedPlant, Mode } from "../map/interfaces";
|
||||||
import { unpackUUID } from "../../util";
|
import { unpackUUID } from "../../util";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { maybeGetCachedPlantIcon } from "../../open_farm/cached_crop";
|
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 { plantAge } from "./map_state_to_props";
|
||||||
|
import { getMode } from "../map/util";
|
||||||
|
|
||||||
export interface PlantInventoryItemProps {
|
export interface PlantInventoryItemProps {
|
||||||
plant: TaggedPlant;
|
plant: TaggedPlant;
|
||||||
|
@ -21,9 +22,10 @@ interface PlantInventoryItemState {
|
||||||
// The individual plants that show up in the farm designer sub nav.
|
// The individual plants that show up in the farm designer sub nav.
|
||||||
export class PlantInventoryItem extends
|
export class PlantInventoryItem extends
|
||||||
React.Component<PlantInventoryItemProps, PlantInventoryItemState> {
|
React.Component<PlantInventoryItemProps, PlantInventoryItemState> {
|
||||||
|
|
||||||
state: PlantInventoryItemState = { icon: "" };
|
state: PlantInventoryItemState = { icon: "" };
|
||||||
|
|
||||||
|
updateStateIcon = (i: string) => this.setState({ icon: i });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { plant, dispatch } = this.props;
|
const { plant, dispatch } = this.props;
|
||||||
const plantId = (plant.body.id || "ERR_NO_PLANT_ID").toString();
|
const plantId = (plant.body.id || "ERR_NO_PLANT_ID").toString();
|
||||||
|
@ -36,17 +38,21 @@ export class PlantInventoryItem extends
|
||||||
};
|
};
|
||||||
|
|
||||||
const click = () => {
|
const click = () => {
|
||||||
const plantCategory =
|
if (getMode() == Mode.boxSelect) {
|
||||||
unpackUUID(plant.uuid).kind === "PlantTemplate"
|
mapPointClickAction(dispatch, plant.uuid)();
|
||||||
? "gardens/templates"
|
toggle("leave");
|
||||||
: "plants";
|
} else {
|
||||||
push(`/app/designer/${plantCategory}/${plantId}`);
|
const plantCategory =
|
||||||
dispatch(selectPlant([plant.uuid]));
|
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<HTMLImageElement>) =>
|
const onLoad = (e: React.SyntheticEvent<HTMLImageElement>) =>
|
||||||
maybeGetCachedPlantIcon(slug, e.currentTarget, updateStateIcon);
|
maybeGetCachedPlantIcon(slug, e.currentTarget, this.updateStateIcon);
|
||||||
|
|
||||||
// Name given from OpenFarm's API.
|
// Name given from OpenFarm's API.
|
||||||
const label = plant.body.name || "Unknown plant";
|
const label = plant.body.name || "Unknown plant";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from "react-redux";
|
||||||
import { Everything } from "../../interfaces";
|
import { Everything } from "../../interfaces";
|
||||||
import { PlantInventoryItem } from "./plant_inventory_item";
|
import { PlantInventoryItem } from "./plant_inventory_item";
|
||||||
import { destroy } from "../../api/crud";
|
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 { Actions, Content } from "../../constants";
|
||||||
import { TaggedPlant } from "../map/interfaces";
|
import { TaggedPlant } from "../map/interfaces";
|
||||||
import { getPlants } from "../state_to_props";
|
import { getPlants } from "../state_to_props";
|
||||||
|
@ -16,19 +16,73 @@ import { createGroup } from "../point_groups/actions";
|
||||||
import { PanelColor } from "../panel_header";
|
import { PanelColor } from "../panel_header";
|
||||||
import { error } from "../../toast/toast";
|
import { error } from "../../toast/toast";
|
||||||
import { PlantStatusBulkUpdate } from "./edit_plant_status";
|
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 => ({
|
export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
||||||
selected: props.resources.consumers.farm_designer.selectedPlants,
|
Plant: { label: t("Plants"), value: "Plant" },
|
||||||
plants: getPlants(props.resources),
|
GenericPointer: { label: t("Points"), value: "GenericPointer" },
|
||||||
dispatch: props.dispatch,
|
Weed: { label: t("Weeds"), value: "Weed" },
|
||||||
gardenOpen: props.resources.consumers.farm_designer.openedSavedGarden,
|
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 {
|
export interface SelectPlantsProps {
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
|
allPoints: TaggedPoint[];
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
selected: string[] | undefined;
|
selected: UUID[] | undefined;
|
||||||
|
selectionPointType: PointType[] | undefined;
|
||||||
|
getConfigValue: GetWebAppConfigValue;
|
||||||
gardenOpen: string | undefined;
|
gardenOpen: string | undefined;
|
||||||
|
xySwap: boolean;
|
||||||
|
quadrant: BotOriginQuadrant;
|
||||||
|
isActive(id: number | undefined): boolean;
|
||||||
|
tools: TaggedTool[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
||||||
|
@ -42,6 +96,11 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => this.props.dispatch({
|
||||||
|
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
get selected() { return this.props.selected || []; }
|
get selected() { return this.props.selected || []; }
|
||||||
|
|
||||||
destroySelected = (plantUUIDs: string[] | undefined) => {
|
destroySelected = (plantUUIDs: string[] | undefined) => {
|
||||||
|
@ -56,18 +115,32 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectionPointType() {
|
||||||
|
const selectionPointTypes = this.props.selectionPointType || ["Plant"];
|
||||||
|
return selectionPointTypes.length > 1 ? "All" : selectionPointTypes[0];
|
||||||
|
}
|
||||||
|
|
||||||
ActionButtons = () =>
|
ActionButtons = () =>
|
||||||
<div className="panel-action-buttons">
|
<div className="panel-action-buttons">
|
||||||
|
<FBSelect
|
||||||
|
list={POINTER_TYPE_LIST()}
|
||||||
|
selectedItem={POINTER_TYPE_DDI_LOOKUP()[this.selectionPointType]}
|
||||||
|
onChange={ddi => {
|
||||||
|
this.props.dispatch(selectPoint(undefined));
|
||||||
|
this.props.dispatch({
|
||||||
|
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||||
|
payload: ddi.value == "All" ? POINTER_TYPES : [ddi.value],
|
||||||
|
});
|
||||||
|
}} />
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
<button className="fb-button gray"
|
<button className="fb-button gray"
|
||||||
title={t("Select none")}
|
title={t("Select none")}
|
||||||
onClick={() => this.props.dispatch(selectPlant(undefined))}>
|
onClick={() => this.props.dispatch(selectPoint(undefined))}>
|
||||||
{t("Select none")}
|
{t("Select none")}
|
||||||
</button>
|
</button>
|
||||||
<button className="fb-button gray"
|
<button className="fb-button gray"
|
||||||
title={t("Select all")}
|
title={t("Select all")}
|
||||||
onClick={() => this.props
|
onClick={() => this.props.dispatch(selectPoint(this.allPointUuids))}>
|
||||||
.dispatch(selectPlant(this.props.plants.map(p => p.uuid)))}>
|
|
||||||
{t("Select all")}
|
{t("Select all")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,42 +158,152 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
||||||
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>
|
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>
|
||||||
{t("Create group")}
|
{t("Create group")}
|
||||||
</button>
|
</button>
|
||||||
<PlantStatusBulkUpdate
|
{this.selectionPointType == "Plant" &&
|
||||||
plants={this.props.plants}
|
<PlantStatusBulkUpdate
|
||||||
selected={this.selected}
|
plants={this.props.plants}
|
||||||
dispatch={this.props.dispatch} />
|
selected={this.selected}
|
||||||
|
dispatch={this.props.dispatch} />}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
render() {
|
get filteredPoints() {
|
||||||
const { plants, dispatch } = this.props;
|
const { plants, allPoints, selectionPointType, getConfigValue } = this.props;
|
||||||
const selectedPlantData =
|
return getFilteredPoints({
|
||||||
this.selected.map(uuid => plants.filter(p => p.uuid == uuid)[0]);
|
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<string, Record<"singular" | "plural", string>> = {
|
||||||
|
"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 <DesignerPanel panelName={"plant-selection"}
|
return <DesignerPanel panelName={"plant-selection"}
|
||||||
panelColor={PanelColor.lightGray}>
|
panelColor={PanelColor.lightGray}>
|
||||||
<DesignerPanelHeader
|
<DesignerPanelHeader
|
||||||
panelName={"plant-selection"}
|
panelName={"plant-selection"}
|
||||||
panelColor={PanelColor.lightGray}
|
panelColor={PanelColor.lightGray}
|
||||||
blackText={true}
|
blackText={true}
|
||||||
title={t("{{length}} plants selected",
|
title={t("{{length}} {{name}} selected",
|
||||||
{ length: this.selected.length })}
|
{ length: this.selected.length, name: this.itemName })}
|
||||||
backTo={"/app/designer/plants"}
|
backTo={"/app/designer/plants"}
|
||||||
onBack={unselectPlant(dispatch)}
|
onBack={unselectPlant(dispatch)}
|
||||||
description={Content.BOX_SELECT_DESCRIPTION} />
|
description={Content.BOX_SELECT_DESCRIPTION} />
|
||||||
<this.ActionButtons />
|
<this.ActionButtons />
|
||||||
|
|
||||||
<DesignerPanelContent panelName={"plant-selection"}>
|
<DesignerPanelContent panelName={"plant-selection"}>
|
||||||
{selectedPlantData && selectedPlantData[0] &&
|
{this.selectedPointData.map(p => {
|
||||||
selectedPlantData.map(p =>
|
if (p.kind == "PlantTemplate" || p.body.pointer_type == "Plant") {
|
||||||
<PlantInventoryItem
|
return <PlantInventoryItem
|
||||||
key={p.uuid}
|
key={p.uuid}
|
||||||
plant={p}
|
plant={p as TaggedPlant}
|
||||||
hovered={false}
|
hovered={false}
|
||||||
dispatch={dispatch} />)}
|
dispatch={dispatch} />;
|
||||||
|
} else {
|
||||||
|
switch (p.body.pointer_type) {
|
||||||
|
case "GenericPointer":
|
||||||
|
return <PointInventoryItem
|
||||||
|
key={p.uuid}
|
||||||
|
tpp={p as TaggedGenericPointer}
|
||||||
|
hovered={false}
|
||||||
|
dispatch={this.props.dispatch} />;
|
||||||
|
case "Weed":
|
||||||
|
return <WeedInventoryItem
|
||||||
|
key={p.uuid}
|
||||||
|
tpp={p as TaggedWeedPointer}
|
||||||
|
hovered={false}
|
||||||
|
dispatch={this.props.dispatch} />;
|
||||||
|
case "ToolSlot":
|
||||||
|
return <ToolSlotInventoryItem
|
||||||
|
key={p.uuid}
|
||||||
|
hovered={false}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
toolSlot={p as TaggedToolSlotPointer}
|
||||||
|
isActive={this.props.isActive}
|
||||||
|
tools={this.props.tools}
|
||||||
|
xySwap={this.props.xySwap}
|
||||||
|
quadrant={this.props.quadrant}
|
||||||
|
hideDropdown={true} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
</DesignerPanelContent>
|
</DesignerPanelContent>
|
||||||
</DesignerPanel>;
|
</DesignerPanel>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectPlants = connect(mapStateToProps)(RawSelectPlants);
|
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] : []),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
|
@ -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 React from "react";
|
||||||
import {
|
import {
|
||||||
GroupInventoryItem, GroupInventoryItemProps,
|
GroupInventoryItem, GroupInventoryItemProps,
|
||||||
|
@ -6,6 +17,7 @@ import {
|
||||||
fakePointGroup, fakePlant,
|
fakePointGroup, fakePlant,
|
||||||
} from "../../../__test_support__/fake_state/resources";
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
import { destroy } from "../../../api/crud";
|
||||||
|
|
||||||
describe("<GroupInventoryItem />", () => {
|
describe("<GroupInventoryItem />", () => {
|
||||||
const fakeProps = (): GroupInventoryItemProps => ({
|
const fakeProps = (): GroupInventoryItemProps => ({
|
||||||
|
@ -32,4 +44,21 @@ describe("<GroupInventoryItem />", () => {
|
||||||
expect(x.text()).toContain("woosh");
|
expect(x.text()).toContain("woosh");
|
||||||
expect(x.find(".hovered").length).toBe(1);
|
expect(x.find(".hovered").length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens group", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = mount(<GroupInventoryItem {...p} />);
|
||||||
|
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(<GroupInventoryItem {...p} />);
|
||||||
|
wrapper.find("div").first().simulate("click");
|
||||||
|
expect(p.onClick).not.toHaveBeenCalled();
|
||||||
|
expect(destroy).toHaveBeenCalledWith(p.group.uuid);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,10 +9,11 @@ jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
PointGroupItem, PointGroupItemProps, genericPointIcon, OTHER_POINT_ICON,
|
PointGroupItem, PointGroupItemProps, genericPointIcon, OTHER_POINT_ICON,
|
||||||
|
genericWeedIcon,
|
||||||
} from "../point_group_item";
|
} from "../point_group_item";
|
||||||
import { shallow } from "enzyme";
|
import { shallow, mount } from "enzyme";
|
||||||
import {
|
import {
|
||||||
fakePlant, fakePointGroup, fakePoint, fakeToolSlot,
|
fakePlant, fakePointGroup, fakePoint, fakeToolSlot, fakeWeed,
|
||||||
} from "../../../__test_support__/fake_state/resources";
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
import {
|
import {
|
||||||
maybeGetCachedPlantIcon, setImgSrc,
|
maybeGetCachedPlantIcon, setImgSrc,
|
||||||
|
@ -22,7 +23,8 @@ import { overwrite } from "../../../api/crud";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { imgEvent } from "../../../__test_support__/fake_html_events";
|
import { imgEvent } from "../../../__test_support__/fake_html_events";
|
||||||
import { error } from "../../../toast/toast";
|
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("<PointGroupItem/>", () => {
|
describe("<PointGroupItem/>", () => {
|
||||||
const fakeProps = (): PointGroupItemProps => ({
|
const fakeProps = (): PointGroupItemProps => ({
|
||||||
|
@ -61,25 +63,36 @@ describe("<PointGroupItem/>", () => {
|
||||||
expect(i.setState).toHaveBeenCalledWith({ icon: "fake icon" });
|
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<PointGroupItem>(<PointGroupItem {...p} />);
|
||||||
|
expect(wrapper.find("img").props().src).toEqual(DEFAULT_ICON);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays point icon", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.point = fakePoint();
|
p.point = fakePoint();
|
||||||
const i = new PointGroupItem(p);
|
const wrapper = mount<PointGroupItem>(<PointGroupItem {...p} />);
|
||||||
const fakeImgEvent = imgEvent();
|
expect(wrapper.find("img").props().src).toEqual(
|
||||||
i.maybeGetCachedIcon(fakeImgEvent);
|
|
||||||
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
|
|
||||||
expect(setImgSrc).toHaveBeenCalledWith(expect.any(Object),
|
|
||||||
svgToUrl(genericPointIcon(undefined)));
|
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<PointGroupItem>(<PointGroupItem {...p} />);
|
||||||
|
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();
|
const p = fakeProps();
|
||||||
p.point = fakeToolSlot();
|
p.point = fakeToolSlot();
|
||||||
const i = new PointGroupItem(p);
|
const wrapper = mount<PointGroupItem>(<PointGroupItem {...p} />);
|
||||||
const fakeImgEvent = imgEvent();
|
expect(wrapper.find("img").props().src).toEqual(
|
||||||
i.maybeGetCachedIcon(fakeImgEvent);
|
|
||||||
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
|
|
||||||
expect(setImgSrc).toHaveBeenCalledWith(expect.any(Object),
|
|
||||||
svgToUrl(OTHER_POINT_ICON));
|
svgToUrl(OTHER_POINT_ICON));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { TaggedPointGroup } from "farmbot";
|
import { TaggedPointGroup, PointType } from "farmbot";
|
||||||
import { PointGroup, Point } from "farmbot/dist/resources/api_resources";
|
import { PointGroup } from "farmbot/dist/resources/api_resources";
|
||||||
|
|
||||||
export type PointGroupCriteria = PointGroup["criteria"];
|
export type PointGroupCriteria = PointGroup["criteria"];
|
||||||
export type StringEqCriteria = PointGroupCriteria["string_eq"];
|
export type StringEqCriteria = PointGroupCriteria["string_eq"];
|
||||||
export type PointerType = Point["pointer_type"];
|
export type PointerType = PointType;
|
||||||
export type StrAndNumCriteriaKeys = (keyof Omit<PointGroupCriteria, "day">)[];
|
export type StrAndNumCriteriaKeys = (keyof Omit<PointGroupCriteria, "day">)[];
|
||||||
export type EqCriteria<T> = Record<string, T[] | undefined>;
|
export type EqCriteria<T> = Record<string, T[] | undefined>;
|
||||||
|
|
||||||
export const POINTER_TYPES: PointerType[] =
|
export const POINTER_TYPES: PointerType[] =
|
||||||
["Plant", "GenericPointer", "ToolSlot"];
|
["Plant", "GenericPointer", "ToolSlot", "Weed"];
|
||||||
|
|
||||||
export const DEFAULT_CRITERIA: Readonly<PointGroupCriteria> = {
|
export const DEFAULT_CRITERIA: Readonly<PointGroupCriteria> = {
|
||||||
day: { op: "<", days_ago: 0 },
|
day: { op: "<", days_ago: 0 },
|
||||||
|
|
|
@ -2,6 +2,9 @@ import React from "react";
|
||||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { pointsSelectedByGroup } from "./criteria";
|
import { pointsSelectedByGroup } from "./criteria";
|
||||||
|
import { ErrorBoundary } from "../../error_boundary";
|
||||||
|
import { DevSettings } from "../../account/dev/dev_support";
|
||||||
|
import { destroy } from "../../api/crud";
|
||||||
|
|
||||||
export interface GroupInventoryItemProps {
|
export interface GroupInventoryItemProps {
|
||||||
group: TaggedPointGroup;
|
group: TaggedPointGroup;
|
||||||
|
@ -12,15 +15,30 @@ export interface GroupInventoryItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupInventoryItem(props: GroupInventoryItemProps) {
|
export function GroupInventoryItem(props: GroupInventoryItemProps) {
|
||||||
const count = pointsSelectedByGroup(props.group, props.allPoints).length;
|
const { group } = props;
|
||||||
|
const delMode = DevSettings.quickDeleteEnabled();
|
||||||
return <div
|
return <div
|
||||||
onClick={props.onClick}
|
onClick={delMode ? () => props.dispatch(destroy(group.uuid)) : props.onClick}
|
||||||
className={`group-search-item ${props.hovered ? "hovered" : ""}`}>
|
className={["group-search-item",
|
||||||
|
props.hovered ? "hovered" : "",
|
||||||
|
delMode ? "quick-del" : ""].join(" ")}>
|
||||||
<span className="group-search-item-name">
|
<span className="group-search-item-name">
|
||||||
{props.group.body.name}
|
{group.body.name}
|
||||||
</span>
|
</span>
|
||||||
<i className="group-item-count">
|
<ErrorBoundary fallback={<i className="group-item-count">{t("? items")}</i>}>
|
||||||
{t("{{count}} items", { count })}
|
<GroupItemCount group={group} allPoints={props.allPoints} />
|
||||||
</i>
|
</ErrorBoundary>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GroupItemCountProps {
|
||||||
|
group: TaggedPointGroup;
|
||||||
|
allPoints: TaggedPoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupItemCount = (props: GroupItemCountProps) => {
|
||||||
|
const count = pointsSelectedByGroup(props.group, props.allPoints).length;
|
||||||
|
return <i className="group-item-count">
|
||||||
|
{t("{{count}} items", { count })}
|
||||||
|
</i>;
|
||||||
|
};
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
|
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 { setHoveredPlant } from "../map/actions";
|
||||||
import { TaggedPointGroup, uuid, TaggedPoint } from "farmbot";
|
import { TaggedPointGroup, uuid, TaggedPoint } from "farmbot";
|
||||||
import { overwrite } from "../../api/crud";
|
import { overwrite } from "../../api/crud";
|
||||||
import { error } from "../../toast/toast";
|
import { error } from "../../toast/toast";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
|
import { DEFAULT_WEED_ICON } from "../map/layers/weeds/garden_weed";
|
||||||
|
|
||||||
export interface PointGroupItemProps {
|
export interface PointGroupItemProps {
|
||||||
point: TaggedPoint;
|
point: TaggedPoint;
|
||||||
|
@ -25,9 +26,23 @@ const removePoint = (group: TaggedPointGroup, pointId: number) => {
|
||||||
|
|
||||||
export const genericPointIcon = (color: string | undefined) =>
|
export const genericPointIcon = (color: string | undefined) =>
|
||||||
`<svg xmlns='http://www.w3.org/2000/svg'
|
`<svg xmlns='http://www.w3.org/2000/svg'
|
||||||
fill='none' stroke-width='1.5' stroke='${color || "gray"}'>
|
fill='none' stroke-width='1.5' stroke='${color || "green"}'>
|
||||||
<circle cx='15' cy='15' r='12' />
|
<circle cx='15' cy='15' r='12' />
|
||||||
<circle cx='15' cy='15' r='2' />
|
<circle cx='15' cy='15' r='2' />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
export const genericWeedIcon = (color: string | undefined) =>
|
||||||
|
`<svg xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id='WeedGradient'>
|
||||||
|
<stop offset='90%' stop-color='${color || "red"}'
|
||||||
|
stop-opacity='0.25'></stop>
|
||||||
|
<stop offset='100%' stop-color='${color || "red"}'
|
||||||
|
stop-opacity='0'></stop>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<circle id='weed-radius' cx='15' cy='15' r='14'
|
||||||
|
fill='url(#WeedGradient)' opacity='0.5'></circle>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|
||||||
export const OTHER_POINT_ICON =
|
export const OTHER_POINT_ICON =
|
||||||
|
@ -66,36 +81,46 @@ export class PointGroupItem
|
||||||
|
|
||||||
maybeGetCachedIcon = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
maybeGetCachedIcon = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
const img = e.currentTarget;
|
const img = e.currentTarget;
|
||||||
switch (this.props.point.body.pointer_type) {
|
if (this.props.point.body.pointer_type == "Plant") {
|
||||||
case "Plant":
|
const slug = this.props.point.body.openfarm_slug;
|
||||||
const slug = this.props.point.body.openfarm_slug;
|
maybeGetCachedPlantIcon(slug, img, this.setIconState);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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() {
|
render() {
|
||||||
return <span
|
return <span
|
||||||
key={this.key}
|
key={this.key}
|
||||||
|
className={"group-item-icon"}
|
||||||
onMouseEnter={this.enter}
|
onMouseEnter={this.enter}
|
||||||
onMouseLeave={this.leave}
|
onMouseLeave={this.leave}
|
||||||
onClick={this.click}>
|
onClick={this.click}>
|
||||||
|
{this.props.point.body.pointer_type == "Weed" &&
|
||||||
|
<img className={"weed-icon"}
|
||||||
|
src={DEFAULT_WEED_ICON}
|
||||||
|
width={32}
|
||||||
|
height={32} />}
|
||||||
<img
|
<img
|
||||||
style={{
|
style={{
|
||||||
border: this.criteriaIcon ? "1px solid gray" : "none",
|
border: this.criteriaIcon ? "1px solid gray" : "none",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
background: this.props.hovered ? "lightgray" : "none",
|
background: this.props.hovered ? "lightgray" : "none",
|
||||||
}}
|
}}
|
||||||
src={DEFAULT_ICON}
|
src={this.initIcon}
|
||||||
onLoad={this.maybeGetCachedIcon}
|
onLoad={this.maybeGetCachedIcon}
|
||||||
width={32}
|
width={32}
|
||||||
height={32} />
|
height={32} />
|
||||||
|
|
|
@ -23,30 +23,29 @@ import { deletePoints } from "../../../farmware/weed_detector/actions";
|
||||||
import { Actions } from "../../../constants";
|
import { Actions } from "../../../constants";
|
||||||
import { clickButton } from "../../../__test_support__/helpers";
|
import { clickButton } from "../../../__test_support__/helpers";
|
||||||
import { fakeState } from "../../../__test_support__/fake_state";
|
import { fakeState } from "../../../__test_support__/fake_state";
|
||||||
import { CurrentPointPayl } from "../../interfaces";
|
import { DrawnPointPayl } from "../../interfaces";
|
||||||
import { inputEvent } from "../../../__test_support__/fake_html_events";
|
import { inputEvent } from "../../../__test_support__/fake_html_events";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
const FAKE_POINT: CurrentPointPayl =
|
const FAKE_POINT: DrawnPointPayl =
|
||||||
({ name: "My Point", cx: 13, cy: 22, r: 345, color: "red" });
|
({ name: "My Point", cx: 13, cy: 22, r: 345, color: "red" });
|
||||||
|
|
||||||
describe("mapStateToProps", () => {
|
describe("mapStateToProps", () => {
|
||||||
it("maps state to props", () => {
|
it("maps state to props: drawn point", () => {
|
||||||
const state = fakeState();
|
const state = fakeState();
|
||||||
state
|
state.resources.consumers.farm_designer.drawnPoint = FAKE_POINT;
|
||||||
.resources
|
const props = mapStateToProps(state);
|
||||||
.consumers
|
expect(props.drawnPoint?.cx).toEqual(13);
|
||||||
.farm_designer
|
expect(props.drawnPoint?.cy).toEqual(22);
|
||||||
.currentPoint = FAKE_POINT;
|
});
|
||||||
const result = mapStateToProps(state);
|
|
||||||
const { currentPoint } = result;
|
it("maps state to props: drawn weed", () => {
|
||||||
expect(currentPoint).toBeTruthy();
|
const state = fakeState();
|
||||||
if (currentPoint) {
|
state.resources.consumers.farm_designer.drawnPoint = undefined;
|
||||||
expect(currentPoint.cx).toEqual(13);
|
state.resources.consumers.farm_designer.drawnWeed = FAKE_POINT;
|
||||||
expect(currentPoint.cy).toEqual(22);
|
const props = mapStateToProps(state);
|
||||||
} else {
|
expect(props.drawnPoint?.cx).toEqual(13);
|
||||||
fail("Nope");
|
expect(props.drawnPoint?.cy).toEqual(22);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,17 +56,11 @@ describe("<CreatePoints />", () => {
|
||||||
|
|
||||||
const fakeProps = (): CreatePointsProps => ({
|
const fakeProps = (): CreatePointsProps => ({
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
currentPoint: undefined,
|
drawnPoint: undefined,
|
||||||
deviceY: 1.23,
|
deviceY: 1.23,
|
||||||
deviceX: 3.21
|
deviceX: 3.21
|
||||||
});
|
});
|
||||||
|
|
||||||
const fakeInstance = () => {
|
|
||||||
const props = fakeProps();
|
|
||||||
props.currentPoint = FAKE_POINT;
|
|
||||||
return new CreatePoints(props);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders for points", () => {
|
it("renders for points", () => {
|
||||||
mockPath = "/app/designer";
|
mockPath = "/app/designer";
|
||||||
const wrapper = mount(<CreatePoints {...fakeProps()} />);
|
const wrapper = mount(<CreatePoints {...fakeProps()} />);
|
||||||
|
@ -83,13 +76,15 @@ describe("<CreatePoints />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates specific fields", () => {
|
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"));
|
i.updateValue("color")(inputEvent("cheerful hue"));
|
||||||
expect(i.props.currentPoint).toBeTruthy();
|
expect(i.props.drawnPoint).toBeTruthy();
|
||||||
const expected = cloneDeep(FAKE_POINT);
|
const expected = cloneDeep(FAKE_POINT);
|
||||||
expected.color = "cheerful hue";
|
expected.color = "cheerful hue";
|
||||||
expect(i.props.dispatch).toHaveBeenCalledWith({
|
expect(i.props.dispatch).toHaveBeenCalledWith({
|
||||||
type: "SET_CURRENT_POINT_DATA",
|
type: "SET_DRAWN_POINT_DATA",
|
||||||
payload: expected,
|
payload: expected,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -103,25 +98,27 @@ describe("<CreatePoints />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads default point data", () => {
|
it("loads default point data", () => {
|
||||||
const i = fakeInstance();
|
const p = fakeProps();
|
||||||
|
p.drawnPoint = FAKE_POINT;
|
||||||
|
const i = new CreatePoints(p);
|
||||||
i.loadDefaultPoint();
|
i.loadDefaultPoint();
|
||||||
expect(i.props.dispatch).toHaveBeenCalledWith({
|
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 },
|
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";
|
mockPath = "/app/designer/weeds/add";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.currentPoint = { cx: 0, cy: 0, r: 100 };
|
p.drawnPoint = { cx: 0, cy: 0, r: 100 };
|
||||||
const panel = mount<CreatePoints>(<CreatePoints {...p} />);
|
const panel = mount<CreatePoints>(<CreatePoints {...p} />);
|
||||||
const wrapper = shallow(panel.instance().PointProperties());
|
const wrapper = shallow(panel.instance().PointProperties());
|
||||||
wrapper.find("BlurableInput").first().simulate("commit", {
|
wrapper.find("BlurableInput").first().simulate("commit", {
|
||||||
currentTarget: { value: "new name" }
|
currentTarget: { value: "new name" }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
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",
|
cx: 0, cy: 0, r: 100, name: "new name", color: "red",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -148,7 +145,7 @@ describe("<CreatePoints />", () => {
|
||||||
expect(initSave).toHaveBeenCalledWith("Point", {
|
expect(initSave).toHaveBeenCalledWith("Point", {
|
||||||
meta: { color: "red", created_by: "farm-designer", type: "weed" },
|
meta: { color: "red", created_by: "farm-designer", type: "weed" },
|
||||||
name: "Created Weed",
|
name: "Created Weed",
|
||||||
pointer_type: "GenericPointer",
|
pointer_type: "Weed",
|
||||||
radius: 30, x: 10, y: 20, z: 0,
|
radius: 30, x: 10, y: 20, z: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -167,7 +164,8 @@ describe("<CreatePoints />", () => {
|
||||||
p.dispatch = jest.fn(x => x());
|
p.dispatch = jest.fn(x => x());
|
||||||
button.simulate("click");
|
button.simulate("click");
|
||||||
expect(deletePoints).toHaveBeenCalledWith("points", {
|
expect(deletePoints).toHaveBeenCalledWith("points", {
|
||||||
created_by: "farm-designer", type: "point"
|
pointer_type: "GenericPointer",
|
||||||
|
meta: { created_by: "farm-designer" }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -186,26 +184,41 @@ describe("<CreatePoints />", () => {
|
||||||
p.dispatch = jest.fn(x => x());
|
p.dispatch = jest.fn(x => x());
|
||||||
button.simulate("click");
|
button.simulate("click");
|
||||||
expect(deletePoints).toHaveBeenCalledWith("points", {
|
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();
|
const p = fakeProps();
|
||||||
p.currentPoint = { cx: 0, cy: 0, r: 0 };
|
p.drawnPoint = { cx: 0, cy: 0, r: 0 };
|
||||||
const wrapper = mount<CreatePoints>(<CreatePoints {...p} />);
|
const wrapper = mount<CreatePoints>(<CreatePoints {...p} />);
|
||||||
const PP = wrapper.instance().PointProperties;
|
const PP = wrapper.instance().PointProperties;
|
||||||
const component = shallow(<PP />);
|
const component = shallow(<PP />);
|
||||||
component.find("ColorPicker").simulate("change", "red");
|
component.find("ColorPicker").simulate("change", "red");
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
payload: { color: "red", cx: 0, cy: 0, r: 0 },
|
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<CreatePoints>(<CreatePoints {...p} />);
|
||||||
|
const PP = wrapper.instance().PointProperties;
|
||||||
|
const component = shallow(<PP />);
|
||||||
|
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", () => {
|
it("updates value", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.currentPoint = { cx: 0, cy: 0, r: 0 };
|
p.drawnPoint = { cx: 0, cy: 0, r: 0 };
|
||||||
const wrapper = shallow<CreatePoints>(<CreatePoints {...p} />);
|
const wrapper = shallow<CreatePoints>(<CreatePoints {...p} />);
|
||||||
const PP = wrapper.instance().PointProperties;
|
const PP = wrapper.instance().PointProperties;
|
||||||
const component = shallow(<PP />);
|
const component = shallow(<PP />);
|
||||||
|
@ -214,13 +227,13 @@ describe("<CreatePoints />", () => {
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
payload: { cx: 10, cy: 0, r: 0, color: "green" },
|
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", () => {
|
it("fills the state with point data", () => {
|
||||||
const p = fakeProps();
|
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<CreatePoints>(<CreatePoints {...p} />);
|
const wrapper = shallow<CreatePoints>(<CreatePoints {...p} />);
|
||||||
const i = wrapper.instance();
|
const i = wrapper.instance();
|
||||||
expect(i.state).toEqual({});
|
expect(i.state).toEqual({});
|
||||||
|
@ -239,7 +252,7 @@ describe("<CreatePoints />", () => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 * as React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
|
@ -8,21 +16,41 @@ import {
|
||||||
import { fakePoint } from "../../../__test_support__/fake_state/resources";
|
import { fakePoint } from "../../../__test_support__/fake_state/resources";
|
||||||
import { push } from "../../../history";
|
import { push } from "../../../history";
|
||||||
import { Actions } from "../../../constants";
|
import { Actions } from "../../../constants";
|
||||||
|
import { mapPointClickAction } from "../../map/actions";
|
||||||
|
|
||||||
describe("<PointInventoryItem> />", () => {
|
describe("<PointInventoryItem> />", () => {
|
||||||
const fakeProps = (): PointInventoryItemProps => ({
|
const fakeProps = (): PointInventoryItemProps => ({
|
||||||
tpp: fakePoint(),
|
tpp: fakePoint(),
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
hovered: false,
|
hovered: false,
|
||||||
navName: "points",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("navigates to point", () => {
|
it("navigates to point", () => {
|
||||||
|
mockPath = "/app/designer/points";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.tpp.body.id = 1;
|
p.tpp.body.id = 1;
|
||||||
const wrapper = shallow(<PointInventoryItem {...p} />);
|
const wrapper = shallow(<PointInventoryItem {...p} />);
|
||||||
wrapper.simulate("click");
|
wrapper.simulate("click");
|
||||||
|
expect(mapPointClickAction).not.toHaveBeenCalled();
|
||||||
expect(push).toHaveBeenCalledWith("/app/designer/points/1");
|
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(<PointInventoryItem {...p} />);
|
||||||
|
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", () => {
|
it("hovers point", () => {
|
||||||
|
|
|
@ -11,10 +11,10 @@ import {
|
||||||
BlurableInput,
|
BlurableInput,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
} from "../../ui/index";
|
} from "../../ui/index";
|
||||||
import { CurrentPointPayl } from "../interfaces";
|
import { DrawnPointPayl } from "../interfaces";
|
||||||
import { Actions, Content } from "../../constants";
|
import { Actions, Content } from "../../constants";
|
||||||
import { deletePoints } from "../../farmware/weed_detector/actions";
|
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 {
|
import {
|
||||||
DesignerPanel,
|
DesignerPanel,
|
||||||
DesignerPanelHeader,
|
DesignerPanelHeader,
|
||||||
|
@ -29,9 +29,10 @@ import { success } from "../../toast/toast";
|
||||||
|
|
||||||
export function mapStateToProps(props: Everything): CreatePointsProps {
|
export function mapStateToProps(props: Everything): CreatePointsProps {
|
||||||
const { position } = props.bot.hardware.location_data;
|
const { position } = props.bot.hardware.location_data;
|
||||||
|
const { drawnPoint, drawnWeed } = props.resources.consumers.farm_designer;
|
||||||
return {
|
return {
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
currentPoint: props.resources.consumers.farm_designer.currentPoint,
|
drawnPoint: drawnPoint || drawnWeed,
|
||||||
deviceX: position.x || 0,
|
deviceX: position.x || 0,
|
||||||
deviceY: position.y || 0,
|
deviceY: position.y || 0,
|
||||||
};
|
};
|
||||||
|
@ -39,14 +40,14 @@ export function mapStateToProps(props: Everything): CreatePointsProps {
|
||||||
|
|
||||||
export interface CreatePointsProps {
|
export interface CreatePointsProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
currentPoint: CurrentPointPayl | undefined;
|
drawnPoint: DrawnPointPayl | undefined;
|
||||||
deviceX: number;
|
deviceX: number;
|
||||||
deviceY: number;
|
deviceY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreatePointsState = Partial<CurrentPointPayl>;
|
type CreatePointsState = Partial<DrawnPointPayl>;
|
||||||
|
|
||||||
const DEFAULTS: CurrentPointPayl = {
|
const DEFAULTS: DrawnPointPayl = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
cx: 1,
|
cx: 1,
|
||||||
cy: 1,
|
cy: 1,
|
||||||
|
@ -61,10 +62,10 @@ export class RawCreatePoints
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
attr = <T extends (keyof CurrentPointPayl & keyof CreatePointsState)>(key: T,
|
attr = <T extends (keyof DrawnPointPayl & keyof CreatePointsState)>(key: T,
|
||||||
fallback = DEFAULTS[key]): CurrentPointPayl[T] => {
|
fallback = DEFAULTS[key]): DrawnPointPayl[T] => {
|
||||||
const p = this.props.currentPoint;
|
const p = this.props.drawnPoint;
|
||||||
const userValue = this.state[key] as CurrentPointPayl[T] | undefined;
|
const userValue = this.state[key] as DrawnPointPayl[T] | undefined;
|
||||||
const propValue = p ? p[key] : fallback;
|
const propValue = p ? p[key] : fallback;
|
||||||
if (typeof userValue === "undefined") {
|
if (typeof userValue === "undefined") {
|
||||||
return propValue;
|
return propValue;
|
||||||
|
@ -81,7 +82,7 @@ export class RawCreatePoints
|
||||||
|
|
||||||
get defaultColor() { return this.panel == "weeds" ? "red" : "green"; }
|
get defaultColor() { return this.panel == "weeds" ? "red" : "green"; }
|
||||||
|
|
||||||
getPointData = (): CurrentPointPayl => {
|
getPointData = (): DrawnPointPayl => {
|
||||||
return {
|
return {
|
||||||
name: this.attr("name"),
|
name: this.attr("name"),
|
||||||
cx: this.attr("cx"),
|
cx: this.attr("cx"),
|
||||||
|
@ -93,7 +94,9 @@ export class RawCreatePoints
|
||||||
|
|
||||||
cancel = () => {
|
cancel = () => {
|
||||||
this.props.dispatch({
|
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
|
payload: undefined
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -106,14 +109,16 @@ export class RawCreatePoints
|
||||||
|
|
||||||
loadDefaultPoint = () => {
|
loadDefaultPoint = () => {
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: this.panel == "weeds"
|
||||||
|
? Actions.SET_DRAWN_WEED_DATA
|
||||||
|
: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: {
|
payload: {
|
||||||
name: this.defaultName,
|
name: this.defaultName,
|
||||||
cx: DEFAULTS.cx,
|
cx: DEFAULTS.cx,
|
||||||
cy: DEFAULTS.cy,
|
cy: DEFAULTS.cy,
|
||||||
r: DEFAULTS.r,
|
r: DEFAULTS.r,
|
||||||
color: this.defaultColor,
|
color: this.defaultColor,
|
||||||
} as CurrentPointPayl
|
} as DrawnPointPayl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +134,7 @@ export class RawCreatePoints
|
||||||
updateValue = (key: keyof CreatePointsState) => {
|
updateValue = (key: keyof CreatePointsState) => {
|
||||||
return (e: React.SyntheticEvent<HTMLInputElement>) => {
|
return (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
const { value } = e.currentTarget;
|
const { value } = e.currentTarget;
|
||||||
if (this.props.currentPoint) {
|
if (this.props.drawnPoint) {
|
||||||
const point = this.getPointData();
|
const point = this.getPointData();
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "name":
|
case "name":
|
||||||
|
@ -143,7 +148,9 @@ export class RawCreatePoints
|
||||||
point[key] = intValue;
|
point[key] = intValue;
|
||||||
}
|
}
|
||||||
this.props.dispatch({
|
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
|
payload: point
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -155,7 +162,9 @@ export class RawCreatePoints
|
||||||
const point = this.getPointData();
|
const point = this.getPointData();
|
||||||
point.color = color;
|
point.color = color;
|
||||||
this.props.dispatch({
|
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
|
payload: point
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -163,8 +172,8 @@ export class RawCreatePoints
|
||||||
get panel() { return getPathArray()[3] || "points"; }
|
get panel() { return getPathArray()[3] || "points"; }
|
||||||
|
|
||||||
createPoint = () => {
|
createPoint = () => {
|
||||||
const body: GenericPointer = {
|
const body: GenericPointer | WeedPointer = {
|
||||||
pointer_type: "GenericPointer",
|
pointer_type: this.panel == "weeds" ? "Weed" : "GenericPointer",
|
||||||
name: this.attr("name") || this.defaultName,
|
name: this.attr("name") || this.defaultName,
|
||||||
meta: {
|
meta: {
|
||||||
color: this.attr("color") || this.defaultColor,
|
color: this.attr("color") || this.defaultColor,
|
||||||
|
@ -247,8 +256,9 @@ export class RawCreatePoints
|
||||||
</button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
DeleteAllPoints = (type: "point" | "weed") =>
|
DeleteAllPoints = (type: "point" | "weed") => {
|
||||||
<Row>
|
const meta = { created_by: "farm-designer" };
|
||||||
|
return <Row>
|
||||||
<div className="delete-row">
|
<div className="delete-row">
|
||||||
<label>{t("delete")}</label>
|
<label>{t("delete")}</label>
|
||||||
<p>{type === "weed"
|
<p>{type === "weed"
|
||||||
|
@ -261,7 +271,8 @@ export class RawCreatePoints
|
||||||
? t("Delete all the weeds you have created?")
|
? t("Delete all the weeds you have created?")
|
||||||
: t("Delete all the points you have created?"))) {
|
: t("Delete all the points you have created?"))) {
|
||||||
this.props.dispatch(deletePoints("points", {
|
this.props.dispatch(deletePoints("points", {
|
||||||
created_by: "farm-designer", type,
|
pointer_type: type === "weed" ? "Weed" : "GenericPointer",
|
||||||
|
meta,
|
||||||
}));
|
}));
|
||||||
this.cancel();
|
this.cancel();
|
||||||
}
|
}
|
||||||
|
@ -271,7 +282,8 @@ export class RawCreatePoints
|
||||||
: t("Delete all created points")}
|
: t("Delete all created points")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const panelType = this.panel == "weeds" ? Panel.Weeds : Panel.Points;
|
const panelType = this.panel == "weeds" ? Panel.Weeds : Panel.Points;
|
||||||
|
|
|
@ -3,16 +3,20 @@ import { t } from "../../i18next_wrapper";
|
||||||
import { getDevice } from "../../device";
|
import { getDevice } from "../../device";
|
||||||
import { destroy, edit, save } from "../../api/crud";
|
import { destroy, edit, save } from "../../api/crud";
|
||||||
import { ResourceColor } from "../../interfaces";
|
import { ResourceColor } from "../../interfaces";
|
||||||
import { TaggedGenericPointer } from "farmbot";
|
import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot";
|
||||||
import { ListItem } from "../plants/plant_panel";
|
import { ListItem } from "../plants/plant_panel";
|
||||||
import { round } from "lodash";
|
import { round } from "lodash";
|
||||||
import { Row, Col, BlurableInput, ColorPicker } from "../../ui";
|
import { Row, Col, BlurableInput, ColorPicker } from "../../ui";
|
||||||
import { parseIntInput } from "../../util";
|
import { parseIntInput } from "../../util";
|
||||||
import { UUID } from "../../resources/interfaces";
|
import { UUID } from "../../resources/interfaces";
|
||||||
|
|
||||||
|
type PointUpdate =
|
||||||
|
Partial<TaggedGenericPointer["body"] | TaggedWeedPointer["body"]>;
|
||||||
|
|
||||||
export const updatePoint =
|
export const updatePoint =
|
||||||
(point: TaggedGenericPointer | undefined, dispatch: Function) =>
|
(point: TaggedGenericPointer | TaggedWeedPointer | undefined,
|
||||||
(update: Partial<TaggedGenericPointer["body"]>) => {
|
dispatch: Function) =>
|
||||||
|
(update: PointUpdate) => {
|
||||||
if (point) {
|
if (point) {
|
||||||
dispatch(edit(point, update));
|
dispatch(edit(point, update));
|
||||||
dispatch(save(point.uuid));
|
dispatch(save(point.uuid));
|
||||||
|
@ -20,18 +24,21 @@ export const updatePoint =
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EditPointPropertiesProps {
|
export interface EditPointPropertiesProps {
|
||||||
point: TaggedGenericPointer;
|
point: TaggedGenericPointer | TaggedWeedPointer;
|
||||||
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
|
updatePoint(update: PointUpdate): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditPointProperties = (props: EditPointPropertiesProps) =>
|
export const EditPointProperties = (props: EditPointPropertiesProps) =>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<div className={"point-name-input"}>
|
<Row>
|
||||||
<EditPointName
|
<EditPointName
|
||||||
name={props.point.body.name}
|
name={props.point.body.name}
|
||||||
updatePoint={props.updatePoint} />
|
updatePoint={props.updatePoint} />
|
||||||
</div>
|
<EditPointColor
|
||||||
|
color={props.point.body.meta.color}
|
||||||
|
updatePoint={props.updatePoint} />
|
||||||
|
</Row>
|
||||||
</li>
|
</li>
|
||||||
<ListItem name={t("Location")}>
|
<ListItem name={t("Location")}>
|
||||||
<EditPointLocation
|
<EditPointLocation
|
||||||
|
@ -43,11 +50,6 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
|
||||||
radius={props.point.body.radius}
|
radius={props.point.body.radius}
|
||||||
updatePoint={props.updatePoint} />
|
updatePoint={props.updatePoint} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem name={t("Color")}>
|
|
||||||
<EditPointColor
|
|
||||||
color={props.point.body.meta.color}
|
|
||||||
updatePoint={props.updatePoint} />
|
|
||||||
</ListItem>
|
|
||||||
</ul>;
|
</ul>;
|
||||||
|
|
||||||
export interface PointActionsProps {
|
export interface PointActionsProps {
|
||||||
|
@ -76,13 +78,13 @@ export const PointActions = ({ x, y, z, uuid, dispatch }: PointActionsProps) =>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
export interface EditPointNameProps {
|
export interface EditPointNameProps {
|
||||||
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
|
updatePoint(update: PointUpdate): void;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditPointName = (props: EditPointNameProps) =>
|
export const EditPointName = (props: EditPointNameProps) =>
|
||||||
<Row>
|
<div className={"point-name-input"}>
|
||||||
<Col xs={12}>
|
<Col xs={10}>
|
||||||
<label>{t("Name")}</label>
|
<label>{t("Name")}</label>
|
||||||
<BlurableInput
|
<BlurableInput
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -90,10 +92,10 @@ export const EditPointName = (props: EditPointNameProps) =>
|
||||||
value={props.name}
|
value={props.name}
|
||||||
onCommit={e => props.updatePoint({ name: e.currentTarget.value })} />
|
onCommit={e => props.updatePoint({ name: e.currentTarget.value })} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>;
|
</div>;
|
||||||
|
|
||||||
export interface EditPointLocationProps {
|
export interface EditPointLocationProps {
|
||||||
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
|
updatePoint(update: PointUpdate): void;
|
||||||
xyLocation: Record<"x" | "y", number>;
|
xyLocation: Record<"x" | "y", number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +116,7 @@ export const EditPointLocation = (props: EditPointLocationProps) =>
|
||||||
</Row>;
|
</Row>;
|
||||||
|
|
||||||
export interface EditPointRadiusProps {
|
export interface EditPointRadiusProps {
|
||||||
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
|
updatePoint(update: PointUpdate): void;
|
||||||
radius: number;
|
radius: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,13 +136,15 @@ export const EditPointRadius = (props: EditPointRadiusProps) =>
|
||||||
</Row>;
|
</Row>;
|
||||||
|
|
||||||
export interface EditPointColorProps {
|
export interface EditPointColorProps {
|
||||||
updatePoint(update: Partial<TaggedGenericPointer["body"]>): void;
|
updatePoint(update: PointUpdate): void;
|
||||||
color: string | undefined;
|
color: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditPointColor = (props: EditPointColorProps) =>
|
export const EditPointColor = (props: EditPointColorProps) =>
|
||||||
<Row>
|
<div className={"point-color-input"}>
|
||||||
<ColorPicker
|
<Col xs={2}>
|
||||||
current={(props.color || "green") as ResourceColor}
|
<ColorPicker
|
||||||
onChange={color => props.updatePoint({ meta: { color } })} />
|
current={(props.color || "green") as ResourceColor}
|
||||||
</Row>;
|
onChange={color => props.updatePoint({ meta: { color } })} />
|
||||||
|
</Col>
|
||||||
|
</div>;
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
import { selectAllGenericPointers } from "../../resources/selectors";
|
import { selectAllGenericPointers } from "../../resources/selectors";
|
||||||
import { TaggedGenericPointer } from "farmbot";
|
import { TaggedGenericPointer } from "farmbot";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { isAWeed } from "./weeds_inventory";
|
|
||||||
|
|
||||||
export interface PointsProps {
|
export interface PointsProps {
|
||||||
genericPoints: TaggedGenericPointer[];
|
genericPoints: TaggedGenericPointer[];
|
||||||
|
@ -29,8 +28,7 @@ export function mapStateToProps(props: Everything): PointsProps {
|
||||||
const { hoveredPoint } = props.resources.consumers.farm_designer;
|
const { hoveredPoint } = props.resources.consumers.farm_designer;
|
||||||
return {
|
return {
|
||||||
genericPoints: selectAllGenericPointers(props.resources.index)
|
genericPoints: selectAllGenericPointers(props.resources.index)
|
||||||
.filter(x => !x.body.discarded_at)
|
.filter(x => !x.body.discarded_at),
|
||||||
.filter(x => !isAWeed(x.body.name, x.body.meta.type)),
|
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
hoveredPoint,
|
hoveredPoint,
|
||||||
};
|
};
|
||||||
|
@ -65,7 +63,6 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
|
||||||
.includes(this.state.searchTerm.toLowerCase()))
|
.includes(this.state.searchTerm.toLowerCase()))
|
||||||
.map(p => <PointInventoryItem
|
.map(p => <PointInventoryItem
|
||||||
key={p.uuid}
|
key={p.uuid}
|
||||||
navName={"points"}
|
|
||||||
tpp={p}
|
tpp={p}
|
||||||
hovered={this.props.hoveredPoint === p.uuid}
|
hovered={this.props.hoveredPoint === p.uuid}
|
||||||
dispatch={this.props.dispatch} />)}
|
dispatch={this.props.dispatch} />)}
|
||||||
|
|
|
@ -3,12 +3,15 @@ import { TaggedGenericPointer } from "farmbot";
|
||||||
import { Saucer } from "../../ui";
|
import { Saucer } from "../../ui";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { push } from "../../history";
|
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 {
|
export interface PointInventoryItemProps {
|
||||||
tpp: TaggedGenericPointer;
|
tpp: TaggedGenericPointer;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
hovered: boolean;
|
hovered: boolean;
|
||||||
navName: "points" | "weeds";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The individual points that show up in the farm designer sub nav.
|
// The individual points that show up in the farm designer sub nav.
|
||||||
|
@ -29,13 +32,15 @@ export class PointInventoryItem extends
|
||||||
};
|
};
|
||||||
|
|
||||||
const click = () => {
|
const click = () => {
|
||||||
push(`/app/designer/${this.props.navName}/${pointId}`);
|
if (getMode() == Mode.boxSelect) {
|
||||||
dispatch({ type: Actions.TOGGLE_HOVERED_POINT, payload: [tpp.uuid] });
|
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 <div
|
return <div
|
||||||
className={`point-search-item ${this.props.hovered ? "hovered" : ""}`}
|
className={`point-search-item ${this.props.hovered ? "hovered" : ""}`}
|
||||||
key={pointId}
|
key={pointId}
|
||||||
|
@ -44,7 +49,7 @@ export class PointInventoryItem extends
|
||||||
onClick={click}>
|
onClick={click}>
|
||||||
<Saucer color={point.meta.color || "green"} />
|
<Saucer color={point.meta.color || "green"} />
|
||||||
<span className="point-search-item-name">
|
<span className="point-search-item-name">
|
||||||
{label}
|
{point.name || t("Untitled point")}
|
||||||
</span>
|
</span>
|
||||||
<p className="point-search-item-info">
|
<p className="point-search-item-info">
|
||||||
<i>{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}</i>
|
<i>{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}</i>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { CropLiveSearchResult, CurrentPointPayl } from "./interfaces";
|
import { CropLiveSearchResult, DrawnPointPayl, DrawnWeedPayl } from "./interfaces";
|
||||||
import { generateReducer } from "../redux/generate_reducer";
|
import { generateReducer } from "../redux/generate_reducer";
|
||||||
import { DesignerState, HoveredPlantPayl } from "./interfaces";
|
import { DesignerState, HoveredPlantPayl } from "./interfaces";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { TaggedResource } from "farmbot";
|
import { TaggedResource, PointType } from "farmbot";
|
||||||
import { Actions } from "../constants";
|
import { Actions } from "../constants";
|
||||||
import { BotPosition } from "../devices/interfaces";
|
import { BotPosition } from "../devices/interfaces";
|
||||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
|
import { UUID } from "../resources/interfaces";
|
||||||
|
|
||||||
export const initialState: DesignerState = {
|
export const initialState: DesignerState = {
|
||||||
selectedPlants: undefined,
|
selectedPoints: undefined,
|
||||||
|
selectionPointType: undefined,
|
||||||
hoveredPlant: {
|
hoveredPlant: {
|
||||||
plantUUID: undefined,
|
plantUUID: undefined,
|
||||||
icon: ""
|
icon: ""
|
||||||
|
@ -20,7 +22,8 @@ export const initialState: DesignerState = {
|
||||||
cropSearchResults: [],
|
cropSearchResults: [],
|
||||||
cropSearchInProgress: false,
|
cropSearchInProgress: false,
|
||||||
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
||||||
currentPoint: undefined,
|
drawnPoint: undefined,
|
||||||
|
drawnWeed: undefined,
|
||||||
openedSavedGarden: undefined,
|
openedSavedGarden: undefined,
|
||||||
tryGroupSortType: undefined,
|
tryGroupSortType: undefined,
|
||||||
editGroupAreaInMap: false,
|
editGroupAreaInMap: false,
|
||||||
|
@ -41,10 +44,15 @@ export const designer = generateReducer<DesignerState>(initialState)
|
||||||
s.cropSearchInProgress = false;
|
s.cropSearchInProgress = false;
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
.add<string[] | undefined>(Actions.SELECT_PLANT, (s, { payload }) => {
|
.add<UUID[] | undefined>(Actions.SELECT_POINT, (s, { payload }) => {
|
||||||
s.selectedPlants = payload;
|
s.selectedPoints = payload;
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
|
.add<PointType[] | undefined>(
|
||||||
|
Actions.SET_SELECTION_POINT_TYPE, (s, { payload }) => {
|
||||||
|
s.selectionPointType = payload;
|
||||||
|
return s;
|
||||||
|
})
|
||||||
.add<HoveredPlantPayl>(Actions.TOGGLE_HOVERED_PLANT, (s, { payload }) => {
|
.add<HoveredPlantPayl>(Actions.TOGGLE_HOVERED_PLANT, (s, { payload }) => {
|
||||||
s.hoveredPlant = payload;
|
s.hoveredPlant = payload;
|
||||||
return s;
|
return s;
|
||||||
|
@ -61,12 +69,20 @@ export const designer = generateReducer<DesignerState>(initialState)
|
||||||
s.hoveredToolSlot = payload;
|
s.hoveredToolSlot = payload;
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
.add<CurrentPointPayl | undefined>(
|
.add<DrawnPointPayl | undefined>(
|
||||||
Actions.SET_CURRENT_POINT_DATA, (s, { payload }) => {
|
Actions.SET_DRAWN_POINT_DATA, (s, { payload }) => {
|
||||||
const { color } = (!payload || !payload.color) ?
|
const { color } = (!payload || !payload.color) ?
|
||||||
(s.currentPoint || { color: "green" }) : payload;
|
(s.drawnPoint || { color: "green" }) : payload;
|
||||||
s.currentPoint = payload;
|
s.drawnPoint = payload;
|
||||||
s.currentPoint && (s.currentPoint.color = color);
|
s.drawnPoint && (s.drawnPoint.color = color);
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
.add<DrawnWeedPayl | undefined>(
|
||||||
|
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;
|
return s;
|
||||||
})
|
})
|
||||||
.add<CropLiveSearchResult[]>(Actions.OF_SEARCH_RESULTS_OK, (s, a) => {
|
.add<CropLiveSearchResult[]>(Actions.OF_SEARCH_RESULTS_OK, (s, a) => {
|
||||||
|
@ -75,7 +91,7 @@ export const designer = generateReducer<DesignerState>(initialState)
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
.add<TaggedResource>(Actions.DESTROY_RESOURCE_OK, (s) => {
|
.add<TaggedResource>(Actions.DESTROY_RESOURCE_OK, (s) => {
|
||||||
s.selectedPlants = undefined;
|
s.selectedPoints = undefined;
|
||||||
s.hoveredPlant = { plantUUID: undefined, icon: "" };
|
s.hoveredPlant = { plantUUID: undefined, icon: "" };
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
|
|
|
@ -125,7 +125,7 @@ describe("<SavedGardenHUD />", () => {
|
||||||
clickButton(wrapper, 1, "edit");
|
clickButton(wrapper, 1, "edit");
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
selectAllPointGroups,
|
selectAllPointGroups,
|
||||||
getDeviceAccountSettings,
|
getDeviceAccountSettings,
|
||||||
maybeFindToolById,
|
maybeFindToolById,
|
||||||
|
selectAllWeedPointers,
|
||||||
} from "../resources/selectors";
|
} from "../resources/selectors";
|
||||||
import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
|
import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
|
||||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||||
|
@ -44,10 +45,10 @@ export function mapStateToProps(props: Everything): Props {
|
||||||
const plants = getPlants(props.resources);
|
const plants = getPlants(props.resources);
|
||||||
const findPlant = plantFinder(plants);
|
const findPlant = plantFinder(plants);
|
||||||
|
|
||||||
const { selectedPlants } = props.resources.consumers.farm_designer;
|
const { selectedPoints } = props.resources.consumers.farm_designer;
|
||||||
const selectedPlant = selectedPlants ? findPlant(selectedPlants[0]) : undefined;
|
const selectedPlant = selectedPoints ? findPlant(selectedPoints[0]) : undefined;
|
||||||
const { plantUUID } = props.resources.consumers.farm_designer.hoveredPlant;
|
|
||||||
|
|
||||||
|
const { plantUUID } = props.resources.consumers.farm_designer.hoveredPlant;
|
||||||
const hoveredPlant = findPlant(plantUUID);
|
const hoveredPlant = findPlant(plantUUID);
|
||||||
|
|
||||||
const getConfigValue = getWebAppConfigValue(() => props);
|
const getConfigValue = getWebAppConfigValue(() => props);
|
||||||
|
@ -55,6 +56,7 @@ export function mapStateToProps(props: Everything): Props {
|
||||||
const genericPoints = getConfigValue(BooleanSetting.show_historic_points)
|
const genericPoints = getConfigValue(BooleanSetting.show_historic_points)
|
||||||
? allGenericPoints
|
? allGenericPoints
|
||||||
: allGenericPoints.filter(x => !x.body.discarded_at);
|
: allGenericPoints.filter(x => !x.body.discarded_at);
|
||||||
|
const weeds = selectAllWeedPointers(props.resources.index);
|
||||||
|
|
||||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||||
const { mcu_params } = props.bot.hardware;
|
const { mcu_params } = props.bot.hardware;
|
||||||
|
@ -113,6 +115,7 @@ export function mapStateToProps(props: Everything): Props {
|
||||||
selectedPlant,
|
selectedPlant,
|
||||||
designer: props.resources.consumers.farm_designer,
|
designer: props.resources.consumers.farm_designer,
|
||||||
genericPoints,
|
genericPoints,
|
||||||
|
weeds,
|
||||||
allPoints: selectAllPoints(props.resources.index),
|
allPoints: selectAllPoints(props.resources.index),
|
||||||
toolSlots: joinToolsAndSlot(props.resources.index),
|
toolSlots: joinToolsAndSlot(props.resources.index),
|
||||||
hoveredPlant,
|
hoveredPlant,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
let mockPath = "/app/designer/tools";
|
||||||
jest.mock("../../../history", () => ({
|
jest.mock("../../../history", () => ({
|
||||||
history: { push: jest.fn() },
|
history: { push: jest.fn() },
|
||||||
getPathArray: () => "/app/designer/tools".split("/"),
|
getPathArray: () => mockPath.split("/"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../../../api/crud", () => ({
|
jest.mock("../../../api/crud", () => ({
|
||||||
|
@ -8,6 +9,10 @@ jest.mock("../../../api/crud", () => ({
|
||||||
save: jest.fn(),
|
save: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../map/actions", () => ({
|
||||||
|
mapPointClickAction: jest.fn(() => jest.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockDevice = { readPin: jest.fn(() => Promise.resolve()) };
|
const mockDevice = { readPin: jest.fn(() => Promise.resolve()) };
|
||||||
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
||||||
|
|
||||||
|
@ -28,6 +33,7 @@ import { Content, Actions } from "../../../constants";
|
||||||
import { edit, save } from "../../../api/crud";
|
import { edit, save } from "../../../api/crud";
|
||||||
import { ToolSelection } from "../tool_slot_edit_components";
|
import { ToolSelection } from "../tool_slot_edit_components";
|
||||||
import { ToolsProps } from "../interfaces";
|
import { ToolsProps } from "../interfaces";
|
||||||
|
import { mapPointClickAction } from "../../map/actions";
|
||||||
|
|
||||||
describe("<Tools />", () => {
|
describe("<Tools />", () => {
|
||||||
const fakeProps = (): ToolsProps => ({
|
const fakeProps = (): ToolsProps => ({
|
||||||
|
@ -245,4 +251,37 @@ describe("<ToolSlotInventoryItem />", () => {
|
||||||
wrapper.find(".tool-selection-wrapper").first().simulate("click", e);
|
wrapper.find(".tool-selection-wrapper").first().simulate("click", e);
|
||||||
expect(e.stopPropagation).toHaveBeenCalled();
|
expect(e.stopPropagation).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows tool name", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.hideDropdown = true;
|
||||||
|
const wrapper = mount(<ToolSlotInventoryItem {...p} />);
|
||||||
|
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(<ToolSlotInventoryItem {...p} />);
|
||||||
|
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(<ToolSlotInventoryItem {...p} />);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,9 @@ import { hasUTM } from "../../devices/components/firmware_hardware_support";
|
||||||
import { ToolsProps, ToolsState } from "./interfaces";
|
import { ToolsProps, ToolsState } from "./interfaces";
|
||||||
import { mapStateToProps } from "./state_to_props";
|
import { mapStateToProps } from "./state_to_props";
|
||||||
import { BotOriginQuadrant } from "../interfaces";
|
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 => {
|
const toolStatus = (value: number | undefined): string => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
@ -207,6 +210,7 @@ export interface ToolSlotInventoryItemProps {
|
||||||
isActive(id: number | undefined): boolean;
|
isActive(id: number | undefined): boolean;
|
||||||
xySwap: boolean;
|
xySwap: boolean;
|
||||||
quadrant: BotOriginQuadrant;
|
quadrant: BotOriginQuadrant;
|
||||||
|
hideDropdown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
||||||
|
@ -215,7 +219,14 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
||||||
.filter(tool => tool.body.id == tool_id)[0]?.body.name;
|
.filter(tool => tool.body.id == tool_id)[0]?.body.name;
|
||||||
return <div
|
return <div
|
||||||
className={`tool-slot-search-item ${props.hovered ? "hovered" : ""}`}
|
className={`tool-slot-search-item ${props.hovered ? "hovered" : ""}`}
|
||||||
onClick={() => 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))}
|
onMouseEnter={() => props.dispatch(setToolHover(props.toolSlot.uuid))}
|
||||||
onMouseLeave={() => props.dispatch(setToolHover(undefined))}>
|
onMouseLeave={() => props.dispatch(setToolHover(undefined))}>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -226,20 +237,24 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
|
||||||
xySwap={props.xySwap} quadrant={props.quadrant} />
|
xySwap={props.xySwap} quadrant={props.quadrant} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={6}>
|
<Col xs={6}>
|
||||||
<div className={"tool-selection-wrapper"}
|
{props.hideDropdown
|
||||||
onClick={e => e.stopPropagation()}>
|
? <span className={"tool-slot-search-item-name"}>
|
||||||
<ToolSelection
|
{toolName || t("Empty")}
|
||||||
tools={props.tools}
|
</span>
|
||||||
selectedTool={props.tools
|
: <div className={"tool-selection-wrapper"}
|
||||||
.filter(tool => tool.body.id == tool_id)[0]}
|
onClick={e => e.stopPropagation()}>
|
||||||
onChange={update => {
|
<ToolSelection
|
||||||
props.dispatch(edit(props.toolSlot, update));
|
tools={props.tools}
|
||||||
props.dispatch(save(props.toolSlot.uuid));
|
selectedTool={props.tools
|
||||||
}}
|
.filter(tool => tool.body.id == tool_id)[0]}
|
||||||
isActive={props.isActive}
|
onChange={update => {
|
||||||
filterSelectedTool={false}
|
props.dispatch(edit(props.toolSlot, update));
|
||||||
filterActiveTools={true} />
|
props.dispatch(save(props.toolSlot.uuid));
|
||||||
</div>
|
}}
|
||||||
|
isActive={props.isActive}
|
||||||
|
filterSelectedTool={false}
|
||||||
|
filterActiveTools={true} />
|
||||||
|
</div>}
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={4} className={"tool-slot-position-info"}>
|
<Col xs={4} className={"tool-slot-position-info"}>
|
||||||
<p className="tool-slot-position">
|
<p className="tool-slot-position">
|
||||||
|
|
|
@ -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("<WeedInventoryItem /> />", () => {
|
||||||
|
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(<WeedInventoryItem {...p} />);
|
||||||
|
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(<WeedInventoryItem {...p} />);
|
||||||
|
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(<WeedInventoryItem {...p} />);
|
||||||
|
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(<WeedInventoryItem {...p} />);
|
||||||
|
expect(wrapper.hasClass("hovered")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("un-hovers weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.tpp.body.id = 1;
|
||||||
|
const wrapper = shallow(<WeedInventoryItem {...p} />);
|
||||||
|
wrapper.simulate("mouseLeave");
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.TOGGLE_HOVERED_POINT, payload: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,7 +9,7 @@ import { mount, shallow } from "enzyme";
|
||||||
import {
|
import {
|
||||||
RawEditWeed as EditWeed, EditWeedProps, mapStateToProps,
|
RawEditWeed as EditWeed, EditWeedProps, mapStateToProps,
|
||||||
} from "../weeds_edit";
|
} 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 { fakeState } from "../../../__test_support__/fake_state";
|
||||||
import {
|
import {
|
||||||
buildResourceIndex,
|
buildResourceIndex,
|
||||||
|
@ -32,9 +32,9 @@ describe("<EditWeed />", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
mockPath = "/app/designer/weeds/1";
|
mockPath = "/app/designer/weeds/1";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const point = fakePoint();
|
const weed = fakeWeed();
|
||||||
point.body.id = 1;
|
weed.body.id = 1;
|
||||||
p.findPoint = () => point;
|
p.findPoint = () => weed;
|
||||||
const wrapper = mount(<EditWeed {...p} />);
|
const wrapper = mount(<EditWeed {...p} />);
|
||||||
expect(wrapper.text().toLowerCase()).toContain("edit");
|
expect(wrapper.text().toLowerCase()).toContain("edit");
|
||||||
});
|
});
|
||||||
|
@ -42,9 +42,9 @@ describe("<EditWeed />", () => {
|
||||||
it("goes back", () => {
|
it("goes back", () => {
|
||||||
mockPath = "/app/designer/weeds/1";
|
mockPath = "/app/designer/weeds/1";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const point = fakePoint();
|
const weed = fakeWeed();
|
||||||
point.body.id = 1;
|
weed.body.id = 1;
|
||||||
p.findPoint = () => point;
|
p.findPoint = () => weed;
|
||||||
const wrapper = shallow(<EditWeed {...p} />);
|
const wrapper = shallow(<EditWeed {...p} />);
|
||||||
wrapper.find(DesignerPanelHeader).simulate("back");
|
wrapper.find(DesignerPanelHeader).simulate("back");
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
@ -56,10 +56,10 @@ describe("<EditWeed />", () => {
|
||||||
describe("mapStateToProps()", () => {
|
describe("mapStateToProps()", () => {
|
||||||
it("returns props", () => {
|
it("returns props", () => {
|
||||||
const state = fakeState();
|
const state = fakeState();
|
||||||
const point = fakePoint();
|
const weed = fakeWeed();
|
||||||
point.body.id = 1;
|
weed.body.id = 1;
|
||||||
state.resources = buildResourceIndex([point]);
|
state.resources = buildResourceIndex([weed]);
|
||||||
const props = mapStateToProps(state);
|
const props = mapStateToProps(state);
|
||||||
expect(props.findPoint(1)).toEqual(point);
|
expect(props.findPoint(1)).toEqual(weed);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -4,11 +4,11 @@ import {
|
||||||
RawWeeds as Weeds, WeedsProps, mapStateToProps,
|
RawWeeds as Weeds, WeedsProps, mapStateToProps,
|
||||||
} from "../weeds_inventory";
|
} from "../weeds_inventory";
|
||||||
import { fakeState } from "../../../__test_support__/fake_state";
|
import { fakeState } from "../../../__test_support__/fake_state";
|
||||||
import { fakePoint } from "../../../__test_support__/fake_state/resources";
|
import { fakeWeed } from "../../../__test_support__/fake_state/resources";
|
||||||
|
|
||||||
describe("<Weeds> />", () => {
|
describe("<Weeds> />", () => {
|
||||||
const fakeProps = (): WeedsProps => ({
|
const fakeProps = (): WeedsProps => ({
|
||||||
genericPoints: [],
|
weeds: [],
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
hoveredPoint: undefined,
|
hoveredPoint: undefined,
|
||||||
});
|
});
|
||||||
|
@ -27,9 +27,9 @@ describe("<Weeds> />", () => {
|
||||||
|
|
||||||
it("filters points", () => {
|
it("filters points", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.genericPoints = [fakePoint(), fakePoint()];
|
p.weeds = [fakeWeed(), fakeWeed()];
|
||||||
p.genericPoints[0].body.name = "weed 0";
|
p.weeds[0].body.name = "weed 0";
|
||||||
p.genericPoints[1].body.name = "weed 1";
|
p.weeds[1].body.name = "weed 1";
|
||||||
const wrapper = mount(<Weeds {...p} />);
|
const wrapper = mount(<Weeds {...p} />);
|
||||||
wrapper.setState({ searchTerm: "0" });
|
wrapper.setState({ searchTerm: "0" });
|
||||||
expect(wrapper.text()).toContain("weed 0");
|
expect(wrapper.text()).toContain("weed 0");
|
|
@ -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<WeedInventoryItemProps, {}> {
|
||||||
|
|
||||||
|
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 <div
|
||||||
|
className={`weed-search-item ${this.props.hovered ? "hovered" : ""}`}
|
||||||
|
key={weedId}
|
||||||
|
onMouseEnter={() => toggle("enter")}
|
||||||
|
onMouseLeave={() => toggle("leave")}
|
||||||
|
onClick={click}>
|
||||||
|
<span className={"weed-item-icon"}>
|
||||||
|
<img className={"weed-icon"}
|
||||||
|
src={DEFAULT_WEED_ICON}
|
||||||
|
width={32}
|
||||||
|
height={32} />
|
||||||
|
<img
|
||||||
|
src={svgToUrl(genericWeedIcon(weed.meta.color))}
|
||||||
|
width={32}
|
||||||
|
height={32} />
|
||||||
|
</span>
|
||||||
|
<span className="weed-search-item-name">
|
||||||
|
{weed.name || t("Untitled weed")}
|
||||||
|
</span>
|
||||||
|
<p className="weed-search-item-info">
|
||||||
|
<i>{`(${weed.x}, ${weed.y}) ⌀${weed.radius * 2}`}</i>
|
||||||
|
</p>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,22 +6,22 @@ import {
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { history, getPathArray } from "../../history";
|
import { history, getPathArray } from "../../history";
|
||||||
import { Everything } from "../../interfaces";
|
import { Everything } from "../../interfaces";
|
||||||
import { TaggedGenericPointer } from "farmbot";
|
import { TaggedWeedPointer } from "farmbot";
|
||||||
import { maybeFindGenericPointerById } from "../../resources/selectors";
|
import { maybeFindWeedPointerById } from "../../resources/selectors";
|
||||||
import { Panel } from "../panel_header";
|
import { Panel } from "../panel_header";
|
||||||
import {
|
import {
|
||||||
EditPointProperties, PointActions, updatePoint,
|
EditPointProperties, PointActions, updatePoint,
|
||||||
} from "./point_edit_actions";
|
} from "../points/point_edit_actions";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
|
|
||||||
export interface EditWeedProps {
|
export interface EditWeedProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
findPoint(id: number): TaggedGenericPointer | undefined;
|
findPoint(id: number): TaggedWeedPointer | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapStateToProps = (props: Everything): EditWeedProps => ({
|
export const mapStateToProps = (props: Everything): EditWeedProps => ({
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
findPoint: id => maybeFindGenericPointerById(props.resources.index, id),
|
findPoint: id => maybeFindWeedPointerById(props.resources.index, id),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class RawEditWeed extends React.Component<EditWeedProps, {}> {
|
export class RawEditWeed extends React.Component<EditWeedProps, {}> {
|
|
@ -10,12 +10,12 @@ import {
|
||||||
DesignerPanel, DesignerPanelContent, DesignerPanelTop,
|
DesignerPanel, DesignerPanelContent, DesignerPanelTop,
|
||||||
} from "../designer_panel";
|
} from "../designer_panel";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { TaggedGenericPointer } from "farmbot";
|
import { TaggedWeedPointer } from "farmbot";
|
||||||
import { selectAllGenericPointers } from "../../resources/selectors";
|
import { selectAllWeedPointers } from "../../resources/selectors";
|
||||||
import { PointInventoryItem } from "./point_inventory_item";
|
import { WeedInventoryItem } from "./weed_inventory_item";
|
||||||
|
|
||||||
export interface WeedsProps {
|
export interface WeedsProps {
|
||||||
genericPoints: TaggedGenericPointer[];
|
weeds: TaggedWeedPointer[];
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
hoveredPoint: string | undefined;
|
hoveredPoint: string | undefined;
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,8 @@ interface WeedsState {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isAWeed = (pointName: string, type?: string) =>
|
|
||||||
type == "weed" || pointName.toLowerCase().includes("weed");
|
|
||||||
|
|
||||||
export const mapStateToProps = (props: Everything): WeedsProps => ({
|
export const mapStateToProps = (props: Everything): WeedsProps => ({
|
||||||
genericPoints: selectAllGenericPointers(props.resources.index)
|
weeds: selectAllWeedPointers(props.resources.index),
|
||||||
.filter(x => !x.body.discarded_at)
|
|
||||||
.filter(x => isAWeed(x.body.name, x.body.meta.type)),
|
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
hoveredPoint: props.resources.consumers.farm_designer.hoveredPoint,
|
hoveredPoint: props.resources.consumers.farm_designer.hoveredPoint,
|
||||||
});
|
});
|
||||||
|
@ -54,17 +49,16 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
|
||||||
</DesignerPanelTop>
|
</DesignerPanelTop>
|
||||||
<DesignerPanelContent panelName={"weeds-inventory"}>
|
<DesignerPanelContent panelName={"weeds-inventory"}>
|
||||||
<EmptyStateWrapper
|
<EmptyStateWrapper
|
||||||
notEmpty={this.props.genericPoints.length > 0}
|
notEmpty={this.props.weeds.length > 0}
|
||||||
graphic={EmptyStateGraphic.weeds}
|
graphic={EmptyStateGraphic.weeds}
|
||||||
title={t("No weeds yet.")}
|
title={t("No weeds yet.")}
|
||||||
text={Content.NO_WEEDS}
|
text={Content.NO_WEEDS}
|
||||||
colorScheme={"weeds"}>
|
colorScheme={"weeds"}>
|
||||||
{this.props.genericPoints
|
{this.props.weeds
|
||||||
.filter(p => p.body.name.toLowerCase()
|
.filter(p => p.body.name.toLowerCase()
|
||||||
.includes(this.state.searchTerm.toLowerCase()))
|
.includes(this.state.searchTerm.toLowerCase()))
|
||||||
.map(p => <PointInventoryItem
|
.map(p => <WeedInventoryItem
|
||||||
key={p.uuid}
|
key={p.uuid}
|
||||||
navName={"weeds"}
|
|
||||||
tpp={p}
|
tpp={p}
|
||||||
hovered={this.props.hoveredPoint === p.uuid}
|
hovered={this.props.hoveredPoint === p.uuid}
|
||||||
dispatch={this.props.dispatch} />)}
|
dispatch={this.props.dispatch} />)}
|
|
@ -62,7 +62,8 @@ describe("deletePoints()", () => {
|
||||||
mockDelete = Promise.resolve();
|
mockDelete = Promise.resolve();
|
||||||
mockData = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
mockData = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||||
const dispatch = jest.fn();
|
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",
|
expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search",
|
||||||
{ meta: { created_by: "plant-detection" } });
|
{ meta: { created_by: "plant-detection" } });
|
||||||
await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3");
|
await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3");
|
||||||
|
@ -80,7 +81,8 @@ describe("deletePoints()", () => {
|
||||||
mockDelete = Promise.reject("error");
|
mockDelete = Promise.reject("error");
|
||||||
mockData = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
mockData = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||||
const dispatch = jest.fn();
|
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",
|
expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search",
|
||||||
{ meta: { created_by: "plant-detection" } });
|
{ meta: { created_by: "plant-detection" } });
|
||||||
await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3");
|
await expect(axios.delete).toHaveBeenCalledWith("http://localhost/api/points/1,2,3");
|
||||||
|
@ -98,7 +100,8 @@ describe("deletePoints()", () => {
|
||||||
mockDelete = Promise.resolve();
|
mockDelete = Promise.resolve();
|
||||||
mockData = times(200, () => ({ id: 1 }));
|
mockData = times(200, () => ({ id: 1 }));
|
||||||
const dispatch = jest.fn();
|
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",
|
expect(axios.post).toHaveBeenCalledWith("http://localhost/api/points/search",
|
||||||
{ meta: { created_by: "plant-detection" } });
|
{ meta: { created_by: "plant-detection" } });
|
||||||
await expect(axios.delete).toHaveBeenCalledWith(
|
await expect(axios.delete).toHaveBeenCalledWith(
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe("<WeedDetector />", () => {
|
||||||
expect(wrapper.instance().state.deletionProgress).toBeUndefined();
|
expect(wrapper.instance().state.deletionProgress).toBeUndefined();
|
||||||
clickButton(wrapper, 1, "clear weeds");
|
clickButton(wrapper, 1, "clear weeds");
|
||||||
expect(deletePoints).toHaveBeenCalledWith(
|
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...");
|
expect(wrapper.instance().state.deletionProgress).toEqual("Deleting...");
|
||||||
const fakeProgress = { completed: 50, total: 100, isDone: false };
|
const fakeProgress = { completed: 50, total: 100, isDone: false };
|
||||||
mockDeletePoints.mock.calls[0][2](fakeProgress);
|
mockDeletePoints.mock.calls[0][2](fakeProgress);
|
||||||
|
|
|
@ -5,20 +5,19 @@ import { API } from "../../api";
|
||||||
import { Progress, ProgressCallback, trim } from "../../util";
|
import { Progress, ProgressCallback, trim } from "../../util";
|
||||||
import { getDevice } from "../../device";
|
import { getDevice } from "../../device";
|
||||||
import { noop, chunk } from "lodash";
|
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 { Actions } from "../../constants";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
|
|
||||||
export function deletePoints(
|
export function deletePoints(
|
||||||
pointName: string,
|
pointName: string,
|
||||||
metaQuery: { [key: string]: string },
|
query: Partial<Point>,
|
||||||
cb?: ProgressCallback): Thunk {
|
cb?: ProgressCallback): Thunk {
|
||||||
// TODO: Generalize and add to api/crud.ts
|
// TODO: Generalize and add to api/crud.ts
|
||||||
return async function (dispatch) {
|
return async function (dispatch) {
|
||||||
const URL = API.current.pointSearchPath;
|
const URL = API.current.pointSearchPath;
|
||||||
const QUERY = { meta: metaQuery };
|
|
||||||
try {
|
try {
|
||||||
const resp = await axios.post<GenericPointer[]>(URL, QUERY);
|
const resp = await axios.post<Point[]>(URL, query);
|
||||||
const ids = resp.data.map(x => x.id);
|
const ids = resp.data.map(x => x.id);
|
||||||
// If you delete too many points, you will violate the URL length
|
// If you delete too many points, you will violate the URL length
|
||||||
// limitation of 2,083. Chunking helps fix that.
|
// limitation of 2,083. Chunking helps fix that.
|
||||||
|
|
|
@ -39,7 +39,7 @@ export class WeedDetector
|
||||||
this.setState({ deletionProgress: p.isDone ? "" : percentage });
|
this.setState({ deletionProgress: p.isDone ? "" : percentage });
|
||||||
};
|
};
|
||||||
this.props.dispatch(deletePoints(t("weeds"),
|
this.props.dispatch(deletePoints(t("weeds"),
|
||||||
{ created_by: "plant-detection" }, progress));
|
{ meta: { created_by: "plant-detection" } }, progress));
|
||||||
this.setState({ deletionProgress: t("Deleting...") });
|
this.setState({ deletionProgress: t("Deleting...") });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ describe("isKind()", () => {
|
||||||
describe("groupPointsByType()", () => {
|
describe("groupPointsByType()", () => {
|
||||||
it("returns points", () => {
|
it("returns points", () => {
|
||||||
const points = Selector.groupPointsByType(fakeIndex);
|
const points = Selector.groupPointsByType(fakeIndex);
|
||||||
const expectedKeys = ["Plant", "GenericPointer", "ToolSlot"];
|
const expectedKeys = ["Plant", "GenericPointer", "ToolSlot", "Weed"];
|
||||||
expect(expectedKeys.every(key => key in points)).toBeTruthy();
|
expect(expectedKeys.every(key => key in points)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,15 +10,16 @@ import {
|
||||||
TaggedToolSlotPointer,
|
TaggedToolSlotPointer,
|
||||||
TaggedUser,
|
TaggedUser,
|
||||||
TaggedDevice,
|
TaggedDevice,
|
||||||
|
TaggedWeedPointer,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import {
|
import {
|
||||||
isTaggedPlantPointer,
|
isTaggedPlantPointer,
|
||||||
isTaggedGenericPointer,
|
isTaggedGenericPointer,
|
||||||
isTaggedRegimen,
|
isTaggedRegimen,
|
||||||
isTaggedSequence,
|
isTaggedSequence,
|
||||||
isTaggedTool,
|
|
||||||
isTaggedToolSlotPointer,
|
isTaggedToolSlotPointer,
|
||||||
sanityCheck,
|
sanityCheck,
|
||||||
|
isTaggedWeedPointer,
|
||||||
} from "./tagged_resources";
|
} from "./tagged_resources";
|
||||||
import { betterCompact, bail } from "../util";
|
import { betterCompact, bail } from "../util";
|
||||||
import { findAllById } from "./selectors_by_id";
|
import { findAllById } from "./selectors_by_id";
|
||||||
|
@ -94,6 +95,13 @@ export function selectAllGenericPointers(index: ResourceIndex):
|
||||||
return betterCompact(genericPointers);
|
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[] {
|
export function selectAllPlantPointers(index: ResourceIndex): TaggedPlantPointer[] {
|
||||||
const plantPointers = selectAllActivePoints(index)
|
const plantPointers = selectAllActivePoints(index)
|
||||||
.map(p => isTaggedPlantPointer(p) ? p : undefined);
|
.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. */
|
/** FINDS: All tools that are in use. */
|
||||||
export function toolsInUse(index: ResourceIndex): TaggedTool[] {
|
export function toolsInUse(index: ResourceIndex): TaggedTool[] {
|
||||||
const ids = betterCompact(selectAllToolSlotPointers(index)
|
const ids = betterCompact(selectAllToolSlotPointers(index)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
isTaggedGenericPointer,
|
isTaggedGenericPointer,
|
||||||
isTaggedSavedGarden,
|
isTaggedSavedGarden,
|
||||||
isTaggedFolder,
|
isTaggedFolder,
|
||||||
|
isTaggedWeedPointer,
|
||||||
} from "./tagged_resources";
|
} from "./tagged_resources";
|
||||||
import {
|
import {
|
||||||
ResourceName,
|
ResourceName,
|
||||||
|
@ -125,6 +126,13 @@ export function maybeFindGenericPointerById(index: ResourceIndex, id: number) {
|
||||||
if (resource && isTaggedGenericPointer(resource)) { return resource; }
|
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 */
|
/** Unlike other findById methods, this one allows undefined (missed) values */
|
||||||
export function maybeFindSavedGardenById(index: ResourceIndex, id: number) {
|
export function maybeFindSavedGardenById(index: ResourceIndex, id: number) {
|
||||||
const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)];
|
const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)];
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
TaggedPlantTemplate,
|
TaggedPlantTemplate,
|
||||||
TaggedSavedGarden,
|
TaggedSavedGarden,
|
||||||
TaggedPointGroup,
|
TaggedPointGroup,
|
||||||
|
TaggedWeedPointer,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
|
|
||||||
export interface TaggedResourceBase {
|
export interface TaggedResourceBase {
|
||||||
|
@ -102,6 +103,8 @@ export const isTaggedGenericPointer =
|
||||||
(x: object): x is TaggedGenericPointer => {
|
(x: object): x is TaggedGenericPointer => {
|
||||||
return isTaggedPoint(x) && (x.body.pointer_type === "GenericPointer");
|
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 =
|
export const isTaggedSavedGarden =
|
||||||
(x: object): x is TaggedSavedGarden => is("SavedGarden")(x);
|
(x: object): x is TaggedSavedGarden => is("SavedGarden")(x);
|
||||||
export const isTaggedPlantTemplate =
|
export const isTaggedPlantTemplate =
|
||||||
|
|
|
@ -376,7 +376,7 @@ export const UNBOUND_ROUTES = [
|
||||||
$: "/designer/weeds",
|
$: "/designer/weeds",
|
||||||
getModule,
|
getModule,
|
||||||
key,
|
key,
|
||||||
getChild: () => import("./farm_designer/points/weeds_inventory"),
|
getChild: () => import("./farm_designer/weeds/weeds_inventory"),
|
||||||
childKey: "Weeds"
|
childKey: "Weeds"
|
||||||
}),
|
}),
|
||||||
route({
|
route({
|
||||||
|
@ -392,7 +392,7 @@ export const UNBOUND_ROUTES = [
|
||||||
$: "/designer/weeds/:point_id",
|
$: "/designer/weeds/:point_id",
|
||||||
getModule,
|
getModule,
|
||||||
key,
|
key,
|
||||||
getChild: () => import("./farm_designer/points/weeds_edit"),
|
getChild: () => import("./farm_designer/weeds/weeds_edit"),
|
||||||
childKey: "EditWeed"
|
childKey: "EditWeed"
|
||||||
}),
|
}),
|
||||||
route({
|
route({
|
||||||
|
|
|
@ -30,45 +30,58 @@ describe("locationFormList()", () => {
|
||||||
label: "Generic tool (100, 200, 300)",
|
label: "Generic tool (100, 200, 300)",
|
||||||
value: "1",
|
value: "1",
|
||||||
});
|
});
|
||||||
const plantHeading = items[3];
|
const groupHeading = 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];
|
|
||||||
expect(groupHeading).toEqual({
|
expect(groupHeading).toEqual({
|
||||||
headingId: "PointGroup",
|
headingId: "PointGroup",
|
||||||
label: "Groups",
|
label: "Groups",
|
||||||
value: 0,
|
value: 0,
|
||||||
heading: true,
|
heading: true,
|
||||||
});
|
});
|
||||||
const group = items[9];
|
const group = items[4];
|
||||||
expect(group).toEqual({
|
expect(group).toEqual({
|
||||||
headingId: "PointGroup",
|
headingId: "PointGroup",
|
||||||
label: "Fake",
|
label: "Fake",
|
||||||
value: "1"
|
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"
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ const toolVar = (value: string | number) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
const pointVar = (
|
const pointVar = (
|
||||||
pointer_type: "Plant" | "GenericPointer",
|
pointer_type: "Plant" | "GenericPointer" | "Weed",
|
||||||
value: string | number,
|
value: string | number,
|
||||||
) => ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue =>
|
) => ({ identifierLabel: label, allowedVariableNodes }: NewVarProps): VariableWithAValue =>
|
||||||
createVariableNode(allowedVariableNodes)(label, {
|
createVariableNode(allowedVariableNodes)(label, {
|
||||||
|
@ -123,7 +123,9 @@ const createNewVariable = (props: NewVarProps): VariableNode | undefined => {
|
||||||
if (ddi.isNull) { return nothingVar(props); } // Empty form. Nothing selected yet.
|
if (ddi.isNull) { return nothingVar(props); } // Empty form. Nothing selected yet.
|
||||||
switch (ddi.headingId) {
|
switch (ddi.headingId) {
|
||||||
case "Plant":
|
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 "Tool": return toolVar(ddi.value)(props);
|
||||||
case "parameter": return newParameter(props);
|
case "parameter": return newParameter(props);
|
||||||
case "Coordinate": return manualEntry(ddi.value)(props);
|
case "Coordinate": return manualEntry(ddi.value)(props);
|
||||||
|
|
|
@ -72,17 +72,20 @@ export function locationFormList(resources: ResourceIndex,
|
||||||
const allPoints = selectAllActivePoints(resources);
|
const allPoints = selectAllActivePoints(resources);
|
||||||
const plantDDI = points2ddi(allPoints, "Plant");
|
const plantDDI = points2ddi(allPoints, "Plant");
|
||||||
const genericPointerDDI = points2ddi(allPoints, "GenericPointer");
|
const genericPointerDDI = points2ddi(allPoints, "GenericPointer");
|
||||||
|
const weedDDI = points2ddi(allPoints, "Weed");
|
||||||
const toolDDI = activeToolDDIs(resources);
|
const toolDDI = activeToolDDIs(resources);
|
||||||
return [COORDINATE_DDI()]
|
return [COORDINATE_DDI()]
|
||||||
.concat(additionalItems)
|
.concat(additionalItems)
|
||||||
.concat(heading("Tool"))
|
.concat(heading("Tool"))
|
||||||
.concat(toolDDI)
|
.concat(toolDDI)
|
||||||
|
.concat(displayGroups ? heading("PointGroup") : [])
|
||||||
|
.concat(displayGroups ? groups2Ddi(selectAllPointGroups(resources)) : [])
|
||||||
.concat(heading("Plant"))
|
.concat(heading("Plant"))
|
||||||
.concat(plantDDI)
|
.concat(plantDDI)
|
||||||
.concat(heading("GenericPointer"))
|
.concat(heading("GenericPointer"))
|
||||||
.concat(genericPointerDDI)
|
.concat(genericPointerDDI)
|
||||||
.concat(displayGroups ? heading("PointGroup") : [])
|
.concat(heading("Weed"))
|
||||||
.concat(displayGroups ? groups2Ddi(selectAllPointGroups(resources)) : []);
|
.concat(weedDDI);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */
|
/** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */
|
||||||
|
@ -126,15 +129,6 @@ export function dropDownName(name: string, v?: Record<Xyz, number | undefined>,
|
||||||
return capitalize(label);
|
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 => ({
|
export const COORDINATE_DDI = (vector?: Vector3): DropDownItem => ({
|
||||||
label: vector
|
label: vector
|
||||||
? `${t("Coordinate")} (${vector.x}, ${vector.y}, ${vector.z})`
|
? `${t("Coordinate")} (${vector.x}, ${vector.y}, ${vector.z})`
|
||||||
|
|
|
@ -52,6 +52,16 @@ export function fakeResourceIndex(extra: TaggedResource[] = []): ResourceIndex {
|
||||||
"y": 200,
|
"y": 200,
|
||||||
"z": 300,
|
"z": 300,
|
||||||
}),
|
}),
|
||||||
|
...newTaggedResource("Point", {
|
||||||
|
id: 5,
|
||||||
|
meta: {},
|
||||||
|
name: "Weed 1",
|
||||||
|
pointer_type: "Weed",
|
||||||
|
radius: 15,
|
||||||
|
x: 15,
|
||||||
|
y: 25,
|
||||||
|
z: 35,
|
||||||
|
}),
|
||||||
...newTaggedResource("Tool", {
|
...newTaggedResource("Tool", {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Generic Tool",
|
"name": "Generic Tool",
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { commitStepChanges } from "./mark_as/commit_step_changes";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
|
|
||||||
interface MarkAsState { nextResource: DropDownItem | undefined }
|
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<StepParams, MarkAsState> {
|
export class MarkAs extends React.Component<StepParams, MarkAsState> {
|
||||||
state: MarkAsState = { nextResource: undefined };
|
state: MarkAsState = { nextResource: undefined };
|
||||||
|
@ -57,7 +57,7 @@ export class MarkAs extends React.Component<StepParams, MarkAsState> {
|
||||||
list={actionList(this.state.nextResource, step, this.props.resources)}
|
list={actionList(this.state.nextResource, step, this.props.resources)}
|
||||||
onChange={this.commitSelection}
|
onChange={this.commitSelection}
|
||||||
key={JSON.stringify(rightSide) + JSON.stringify(this.state)}
|
key={JSON.stringify(rightSide) + JSON.stringify(this.state)}
|
||||||
selectedItem={this.state.nextResource ? NONE : rightSide} />
|
selectedItem={this.state.nextResource ? NONE() : rightSide} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</StepContent>
|
</StepContent>
|
||||||
|
|
|
@ -10,7 +10,7 @@ describe("actionList()", () => {
|
||||||
const step = resourceUpdate({ resource_type: "Plant" });
|
const step = resourceUpdate({ resource_type: "Plant" });
|
||||||
const { index } = markAsResourceFixture();
|
const { index } = markAsResourceFixture();
|
||||||
const result = actionList(undefined, step, index);
|
const result = actionList(undefined, step, index);
|
||||||
expect(result).toEqual(PLANT_OPTIONS);
|
expect(result).toEqual(PLANT_OPTIONS());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("provides a list of tool mount actions", () => {
|
it("provides a list of tool mount actions", () => {
|
||||||
|
@ -35,6 +35,16 @@ describe("actionList()", () => {
|
||||||
expect(labels).toContain("Removed");
|
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", () => {
|
it("returns an empty list for all other options", () => {
|
||||||
const ddi = { label: "test case", value: 1, headingId: "USB Cables" };
|
const ddi = { label: "test case", value: 1, headingId: "USB Cables" };
|
||||||
const step = resourceUpdate({});
|
const step = resourceUpdate({});
|
||||||
|
|
|
@ -10,5 +10,9 @@ describe("resourceList()", () => {
|
||||||
expect(headings).toContain("Device");
|
expect(headings).toContain("Device");
|
||||||
expect(headings).toContain("Plants");
|
expect(headings).toContain("Plants");
|
||||||
expect(headings).toContain("Points");
|
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)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,10 @@ import {
|
||||||
selectAllGenericPointers,
|
selectAllGenericPointers,
|
||||||
} from "../../../../resources/selectors";
|
} from "../../../../resources/selectors";
|
||||||
import { DropDownPair } from "../interfaces";
|
import { DropDownPair } from "../interfaces";
|
||||||
|
import { fakeTool } from "../../../../__test_support__/fake_state/resources";
|
||||||
|
import {
|
||||||
|
buildResourceIndex,
|
||||||
|
} from "../../../../__test_support__/resource_index_builder";
|
||||||
describe("unpackStep()", () => {
|
describe("unpackStep()", () => {
|
||||||
function assertGoodness(result: DropDownPair,
|
function assertGoodness(result: DropDownPair,
|
||||||
action_label: string,
|
action_label: string,
|
||||||
|
@ -41,6 +45,23 @@ describe("unpackStep()", () => {
|
||||||
assertGoodness(result, actionLabel, "mounted", label, value);
|
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)", () => {
|
it("unpacks invalid tool_ids (that may have been valid previously)", () => {
|
||||||
const result = unpackStep({
|
const result = unpackStep({
|
||||||
step: resourceUpdate({ label: "mounted_tool_id", value: Infinity }),
|
step: resourceUpdate({ label: "mounted_tool_id", value: Infinity }),
|
||||||
|
|
|
@ -16,7 +16,7 @@ const allToolsAsDDI = (i: ResourceIndex) => {
|
||||||
.filter(x => !!x.body.id)
|
.filter(x => !!x.body.id)
|
||||||
.map(x => {
|
.map(x => {
|
||||||
return {
|
return {
|
||||||
label: `${MOUNTED_TO} ${x.body.name}`,
|
label: `${MOUNTED_TO()} ${x.body.name}`,
|
||||||
value: x.body.id || 0
|
value: x.body.id || 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -25,9 +25,10 @@ const allToolsAsDDI = (i: ResourceIndex) => {
|
||||||
const DEFAULT = "Default";
|
const DEFAULT = "Default";
|
||||||
|
|
||||||
const ACTION_LIST: Dictionary<ListBuilder> = {
|
const ACTION_LIST: Dictionary<ListBuilder> = {
|
||||||
"Device": (i) => [DISMOUNT, ...allToolsAsDDI(i)],
|
"Device": (i) => [DISMOUNT(), ...allToolsAsDDI(i)],
|
||||||
"Plant": () => PLANT_OPTIONS,
|
"Plant": () => PLANT_OPTIONS(),
|
||||||
"GenericPointer": () => POINT_OPTIONS,
|
"GenericPointer": () => POINT_OPTIONS,
|
||||||
|
"Weed": () => POINT_OPTIONS,
|
||||||
[DEFAULT]: () => []
|
[DEFAULT]: () => []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
fakePlant,
|
fakePlant,
|
||||||
fakePoint,
|
fakePoint,
|
||||||
fakeSequence,
|
fakeSequence,
|
||||||
|
fakeWeed,
|
||||||
} from "../../../__test_support__/fake_state/resources";
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
import { betterMerge } from "../../../util";
|
import { betterMerge } from "../../../util";
|
||||||
import { MarkAs } from "../mark_as";
|
import { MarkAs } from "../mark_as";
|
||||||
|
@ -30,6 +31,7 @@ export const markAsResourceFixture = () => buildResourceIndex([
|
||||||
fakePlant(),
|
fakePlant(),
|
||||||
betterMerge(fakeTool(), { body: { name: "T2", id: 2 } }),
|
betterMerge(fakeTool(), { body: { name: "T2", id: 2 } }),
|
||||||
betterMerge(fakePoint(), { body: { name: "my point", id: 7 } }),
|
betterMerge(fakePoint(), { body: { name: "my point", id: 7 } }),
|
||||||
|
betterMerge(fakeWeed(), { body: { name: "weed 1", id: 8 } }),
|
||||||
betterMerge(fakeTool(), { body: { name: "T3", id: undefined } }),
|
betterMerge(fakeTool(), { body: { name: "T3", id: undefined } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { DropDownItem } from "../../../ui";
|
import { DropDownItem } from "../../../ui";
|
||||||
import { t } from "../../../i18next_wrapper";
|
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 */
|
/** Legal "actions" for "Mark As.." block when marking Point resources */
|
||||||
export const POINT_OPTIONS: DropDownItem[] = [
|
export const POINT_OPTIONS: DropDownItem[] = [
|
||||||
|
@ -12,12 +14,7 @@ export const POINT_OPTIONS: DropDownItem[] = [
|
||||||
|
|
||||||
/** Legal "actions" in the "Mark As.." block when operating on
|
/** Legal "actions" in the "Mark As.." block when operating on
|
||||||
* a Plant resource. */
|
* a Plant resource. */
|
||||||
export const PLANT_OPTIONS: DropDownItem[] = [
|
export const PLANT_OPTIONS = PLANT_STAGE_LIST;
|
||||||
{ label: t("Planned"), value: "planned" },
|
|
||||||
{ label: t("Planted"), value: "planted" },
|
|
||||||
{ label: t("Sprouted"), value: "sprouted" },
|
|
||||||
{ label: t("Harvested"), value: "harvested" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const value = 0; // Not used in headings.
|
const value = 0; // Not used in headings.
|
||||||
|
|
||||||
|
@ -35,6 +32,13 @@ export const POINT_HEADER: DropDownItem = {
|
||||||
heading: true
|
heading: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WEED_HEADER: DropDownItem = {
|
||||||
|
headingId: "Weed",
|
||||||
|
label: t("Weeds"),
|
||||||
|
value,
|
||||||
|
heading: true
|
||||||
|
};
|
||||||
|
|
||||||
export const TOP_HALF = [
|
export const TOP_HALF = [
|
||||||
{ headingId: "Device", label: t("Device"), value, heading: true },
|
{ headingId: "Device", label: t("Device"), value, heading: true },
|
||||||
{ headingId: "Device", label: t("Tool Mount"), value },
|
{ headingId: "Device", label: t("Tool Mount"), value },
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { ResourceIndex } from "../../../resources/interfaces";
|
import { ResourceIndex } from "../../../resources/interfaces";
|
||||||
import { DropDownItem } from "../../../ui/fb_select";
|
import { DropDownItem } from "../../../ui/fb_select";
|
||||||
import { selectAllPoints } from "../../../resources/selectors";
|
import { selectAllPoints } from "../../../resources/selectors";
|
||||||
import { TaggedPoint, TaggedPlantPointer } from "farmbot";
|
import { TaggedPoint } from "farmbot";
|
||||||
import { GenericPointer } from "farmbot/dist/resources/api_resources";
|
import { Point } from "farmbot/dist/resources/api_resources";
|
||||||
import { POINT_HEADER, PLANT_HEADER, TOP_HALF } from "./constants";
|
import { POINT_HEADER, PLANT_HEADER, TOP_HALF, WEED_HEADER } from "./constants";
|
||||||
|
|
||||||
/** Filter function to remove resources we don't care about,
|
/** Filter function to remove resources we don't care about,
|
||||||
* such as ToolSlots and unsaved (Plant|Point)'s */
|
* such as ToolSlots and unsaved (Plant|Point)'s */
|
||||||
|
@ -17,26 +17,14 @@ const isRelevant = (x: TaggedPoint) => {
|
||||||
const labelStr =
|
const labelStr =
|
||||||
(n: string, x: number, y: number, z: number) => `${n} (${x}, ${y}, ${z})`;
|
(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. */
|
* for the "Mark As.." step. */
|
||||||
export const pointer2ddi = (i: GenericPointer): DropDownItem => {
|
export const point2ddi = (i: Point): DropDownItem => {
|
||||||
const { x, y, z, name } = i;
|
const { x, y, z, name, id, pointer_type } = i;
|
||||||
return {
|
return {
|
||||||
value: i.id as number,
|
value: id || 0,
|
||||||
label: labelStr(name, x, y, z),
|
label: labelStr(name, x, y, z),
|
||||||
headingId: "GenericPointer"
|
headingId: pointer_type,
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 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"
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,16 +33,18 @@ export const plant2ddi = (i: TaggedPlantPointer["body"]): DropDownItem => {
|
||||||
const pointList =
|
const pointList =
|
||||||
(input: TaggedPoint[]): DropDownItem[] => {
|
(input: TaggedPoint[]): DropDownItem[] => {
|
||||||
const genericPoints: DropDownItem[] = [POINT_HEADER];
|
const genericPoints: DropDownItem[] = [POINT_HEADER];
|
||||||
|
const weeds: DropDownItem[] = [WEED_HEADER];
|
||||||
const plants: DropDownItem[] = [PLANT_HEADER];
|
const plants: DropDownItem[] = [PLANT_HEADER];
|
||||||
input
|
input
|
||||||
.map(x => x.body)
|
.map(x => x.body)
|
||||||
.forEach(body => {
|
.forEach(body => {
|
||||||
switch (body.pointer_type) {
|
switch (body.pointer_type) {
|
||||||
case "GenericPointer": return genericPoints.push(pointer2ddi(body));
|
case "GenericPointer": return genericPoints.push(point2ddi(body));
|
||||||
case "Plant": return plants.push(plant2ddi(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
|
/** Creates a formatted DropDownItem list for the "Resource" (left hand) side of
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import { DropDownItem } from "../../../ui";
|
import { DropDownItem } from "../../../ui";
|
||||||
import {
|
import {
|
||||||
findToolById,
|
findToolById, findPointerByTypeAndId,
|
||||||
findByKindAndId,
|
|
||||||
findPointerByTypeAndId,
|
|
||||||
} from "../../../resources/selectors";
|
} from "../../../resources/selectors";
|
||||||
import { plant2ddi, pointer2ddi } from "./resource_list";
|
import { point2ddi } from "./resource_list";
|
||||||
import { GenericPointer } from "farmbot/dist/resources/api_resources";
|
|
||||||
import { MOUNTED_TO } from "./constants";
|
import { MOUNTED_TO } from "./constants";
|
||||||
import { DropDownPair, StepWithResourceIndex } from "./interfaces";
|
import { DropDownPair, StepWithResourceIndex } from "./interfaces";
|
||||||
import { TaggedPoint, TaggedPlantPointer } from "farmbot";
|
|
||||||
import { t } from "../../../i18next_wrapper";
|
import { t } from "../../../i18next_wrapper";
|
||||||
|
import { PLANT_STAGE_DDI_LOOKUP } from "../../../farm_designer/plants/edit_plant_status";
|
||||||
|
|
||||||
export const TOOL_MOUNT = (): DropDownItem => ({
|
export const TOOL_MOUNT = (): DropDownItem => ({
|
||||||
label: t("Tool Mount"), value: "tool_mount"
|
label: t("Tool Mount"), value: "tool_mount"
|
||||||
|
@ -19,11 +16,11 @@ export const DISMOUNTED = (): DropDownPair => ({
|
||||||
leftSide: TOOL_MOUNT(),
|
leftSide: TOOL_MOUNT(),
|
||||||
rightSide: NOT_IN_USE()
|
rightSide: NOT_IN_USE()
|
||||||
});
|
});
|
||||||
const DEFAULT_TOOL_NAME = "Untitled Tool";
|
const DEFAULT_TOOL_NAME = () => t("Untitled Tool");
|
||||||
const REMOVED_ACTION = { label: "Removed", value: "removed" };
|
const REMOVED_ACTION = () => ({ label: t("Removed"), value: "removed" });
|
||||||
|
|
||||||
const mountedTo = (toolName = DEFAULT_TOOL_NAME): DropDownItem =>
|
const mountedTo = (toolName = DEFAULT_TOOL_NAME()): DropDownItem =>
|
||||||
({ label: `${MOUNTED_TO} ${toolName}`, value: "mounted" });
|
({ label: `${MOUNTED_TO()} ${toolName}`, value: "mounted" });
|
||||||
|
|
||||||
/** The user wants to change the `mounted_tool_id` of their Device. */
|
/** The user wants to change the `mounted_tool_id` of their Device. */
|
||||||
function mountTool(i: StepWithResourceIndex): DropDownPair {
|
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. */
|
/** The user wants to mark a the `discarded_at` attribute of a Point. */
|
||||||
function discardPoint(i: StepWithResourceIndex): DropDownPair {
|
function discardPoint(i: StepWithResourceIndex): DropDownPair {
|
||||||
const { resource_id } = i.step.args;
|
const { resource_id, resource_type } = i.step.args;
|
||||||
const genericPointerBody =
|
const pointerBody =
|
||||||
findPointerByTypeAndId(i.resourceIndex, "GenericPointer", resource_id).body;
|
findPointerByTypeAndId(i.resourceIndex, resource_type, resource_id).body;
|
||||||
return {
|
return {
|
||||||
leftSide: pointer2ddi(genericPointerBody as GenericPointer),
|
leftSide: point2ddi(pointerBody),
|
||||||
rightSide: REMOVED_ACTION
|
rightSide: REMOVED_ACTION(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The user wants to mark a the `plant_stage` attribute of a Plant resource. */
|
/** The user wants to mark a the `plant_stage` attribute of a Plant resource. */
|
||||||
function plantStage(i: StepWithResourceIndex): DropDownPair {
|
function plantStage(i: StepWithResourceIndex): DropDownPair {
|
||||||
const { resource_id, value } = i.step.args;
|
const { resource_id, resource_type, value } = i.step.args;
|
||||||
const r: TaggedPoint = findByKindAndId(i.resourceIndex, "Point", resource_id);
|
const pointerBody =
|
||||||
|
findPointerByTypeAndId(i.resourceIndex, resource_type, resource_id).body;
|
||||||
return {
|
return {
|
||||||
leftSide: plant2ddi(r.body as TaggedPlantPointer["body"]),
|
leftSide: point2ddi(pointerBody),
|
||||||
rightSide: { label: ("" + value), value: ("" + value) }
|
rightSide: PLANT_STAGE_DDI_LOOKUP()["" + value]
|
||||||
|
|| { label: "" + value, value: "" + value },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const BooleanSetting: Record<BooleanConfigKey, BooleanConfigKey> = {
|
||||||
legend_menu_open: "legend_menu_open",
|
legend_menu_open: "legend_menu_open",
|
||||||
show_plants: "show_plants",
|
show_plants: "show_plants",
|
||||||
show_points: "show_points",
|
show_points: "show_points",
|
||||||
|
show_weeds: "show_weeds",
|
||||||
show_historic_points: "show_historic_points",
|
show_historic_points: "show_historic_points",
|
||||||
show_spread: "show_spread",
|
show_spread: "show_spread",
|
||||||
show_farmbot: "show_farmbot",
|
show_farmbot: "show_farmbot",
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
"coveralls": "3.0.11",
|
"coveralls": "3.0.11",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.2",
|
"enzyme-adapter-react-16": "1.15.2",
|
||||||
"farmbot": "9.2.0-rc3",
|
"farmbot": "9.2.3",
|
||||||
"i18next": "19.3.3",
|
"i18next": "19.3.3",
|
||||||
"install": "0.13.0",
|
"install": "0.13.0",
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.15",
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:none;stroke:#ED1C24;stroke-width:11;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st1{fill:none;stroke:#013F09;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st2{fill:#D9E021;stroke:#013F09;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st3{fill:none;stroke:#233518;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st4{fill:#D3E39C;stroke:#233518;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st5{fill:none;stroke:#233518;stroke-width:6;stroke-miterlimit:10;}
|
||||||
|
.st6{fill:#9EC44F;stroke:#386200;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st7{fill:#9EC44F;stroke:#386200;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st8{fill:none;stroke:#386200;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st9{fill:none;stroke:#9EC44F;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st10{fill:#9EC44F;stroke:#386200;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st11{fill:#998004;}
|
||||||
|
.st12{fill:#E5C403;}
|
||||||
|
.st13{fill:none;stroke:#998004;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st14{fill:none;stroke:#7CA440;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st15{fill:none;stroke:#528B16;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st16{fill:#528B16;}
|
||||||
|
.st17{fill:#7CAB09;}
|
||||||
|
.st18{fill:#4E670C;}
|
||||||
|
.st19{fill:none;stroke:#C0CC8F;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st20{fill:#7D8AD7;stroke:#4648A6;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st21{opacity:0.6;fill:none;stroke:#CDD6FF;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st22{opacity:0.4;fill:none;stroke:#CDD6FF;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st23{opacity:0.2;fill:none;stroke:#CDD6FF;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st24{fill:#7CAB09;stroke:#76990A;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st25{fill:none;stroke:#A598D8;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st26{fill:none;stroke:#2B2A6F;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st27{fill:none;stroke:#8A6EDA;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st28{fill:#E4E5E7;stroke:#2B2A6F;stroke-miterlimit:10;}
|
||||||
|
.st29{fill:none;stroke:#C2807D;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st30{fill:#C2807D;stroke:#7C152E;stroke-miterlimit:10;}
|
||||||
|
.st31{fill:#E4E5E7;}
|
||||||
|
.st32{fill:#A598D8;}
|
||||||
|
.st33{fill:#C0CC8F;}
|
||||||
|
.st34{fill:#7D8AD7;}
|
||||||
|
.st35{fill:#AEB9ED;}
|
||||||
|
.st36{fill:#9EA9E5;}
|
||||||
|
.st37{fill:#8E9ADD;}
|
||||||
|
.st38{fill:#76990A;}
|
||||||
|
.st39{fill:#8A6EDA;}
|
||||||
|
.st40{fill:#2B2A6F;}
|
||||||
|
.st41{fill:#C2807D;}
|
||||||
|
.st42{fill:#7C152E;}
|
||||||
|
.st43{fill:none;stroke:#34500C;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st44{fill:#7E9E5B;stroke:#34500C;stroke-miterlimit:10;}
|
||||||
|
.st45{fill:#FE8600;stroke:#F15A24;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st46{fill:#FE8600;}
|
||||||
|
.st47{fill:none;stroke:#F15A24;stroke-miterlimit:10;}
|
||||||
|
.st48{fill:none;stroke:#F15A24;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st49{opacity:0.8;fill:none;stroke:#F15A24;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st50{opacity:0.6;fill:none;stroke:#F15A24;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st51{opacity:0.4;fill:none;stroke:#F15A24;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st52{opacity:0.2;fill:none;stroke:#F15A24;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st53{fill:#F15A24;}
|
||||||
|
.st54{fill:#9CAD23;stroke:#34500C;stroke-miterlimit:10;}
|
||||||
|
.st55{opacity:0.4;fill:none;stroke:#34500C;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st56{fill:none;stroke:#9CAD23;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st57{fill:none;stroke:#34500C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st58{fill:none;stroke:#7CAB09;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st59{fill:none;stroke:#7D9959;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st60{fill:#7CAB09;stroke:#4E670C;stroke-miterlimit:10;}
|
||||||
|
.st61{fill:none;stroke:#7CAB09;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st62{fill:none;stroke:#34500C;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st63{opacity:0.4;fill:#34500C;}
|
||||||
|
.st64{fill:#34500C;}
|
||||||
|
.st65{fill:none;stroke:#34500C;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st66{fill:none;stroke:#34500C;stroke-width:9;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st67{fill:#7E9E5B;}
|
||||||
|
.st68{fill:none;stroke:#34500C;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st69{fill:none;stroke:#9DB269;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st70{fill:none;stroke:#34500C;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st71{fill:none;stroke:#D8DFC4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st72{fill:none;stroke:#DDB907;stroke-width:6;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st73{fill:none;stroke:#FFE200;stroke-miterlimit:10;}
|
||||||
|
.st74{fill:none;stroke:#EDC107;stroke-miterlimit:10;}
|
||||||
|
.st75{fill:none;stroke:#EFAE0C;stroke-miterlimit:10;}
|
||||||
|
.st76{fill:none;stroke:#FFF8CE;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st77{fill:#F46C29;}
|
||||||
|
.st78{fill:#F77426;}
|
||||||
|
.st79{fill:#F97D25;}
|
||||||
|
.st80{fill:#EF4B18;}
|
||||||
|
.st81{fill:#F3672A;}
|
||||||
|
.st82{fill:#9CAD23;}
|
||||||
|
.st83{fill:#7D9959;}
|
||||||
|
.st84{fill:#9DB269;}
|
||||||
|
.st85{fill:#D8DFC4;}
|
||||||
|
.st86{fill:#DDB907;}
|
||||||
|
.st87{fill:#FFE200;}
|
||||||
|
.st88{fill:#EDC107;}
|
||||||
|
.st89{fill:#EFAE0C;}
|
||||||
|
.st90{fill:#FFF8CE;}
|
||||||
|
.st91{clip-path:url(#SVGID_2_);}
|
||||||
|
.st92{clip-path:url(#SVGID_4_);}
|
||||||
|
.st93{fill:#998004;stroke:#998004;stroke-width:6;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st94{fill:#EEF2FF;stroke:#AFADA7;stroke-miterlimit:10;}
|
||||||
|
.st95{fill:#E5C403;stroke:#998004;stroke-miterlimit:10;}
|
||||||
|
.st96{opacity:0.4;fill:none;stroke:#7CA440;stroke-miterlimit:10;}
|
||||||
|
.st97{opacity:0.3;fill:none;stroke:#7CA440;stroke-miterlimit:10;}
|
||||||
|
.st98{opacity:0.2;fill:none;stroke:#7CA440;stroke-miterlimit:10;}
|
||||||
|
.st99{opacity:0.1;fill:none;stroke:#7CA440;stroke-miterlimit:10;}
|
||||||
|
.st100{fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st101{fill:#FFFFFF;}
|
||||||
|
.st102{fill:#EEF2FF;}
|
||||||
|
.st103{fill:#AFADA7;}
|
||||||
|
.st104{fill:none;stroke:#AFADA7;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st105{fill:#7CA440;}
|
||||||
|
.st106{fill:#BEA123;}
|
||||||
|
.st107{fill:none;stroke:#FFFFFF;stroke-width:7.709;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.st108{fill:#9EC44F;stroke:#386200;stroke-miterlimit:10;}
|
||||||
|
.st109{fill:none;stroke:#90AD49;stroke-miterlimit:10;}
|
||||||
|
.st110{fill:#386200;}
|
||||||
|
.st111{fill:#EEF2FF;stroke:#386200;stroke-miterlimit:10;}
|
||||||
|
.st112{fill:none;stroke:#386200;stroke-width:3;stroke-miterlimit:10;}
|
||||||
|
.st113{fill:#9EC44F;}
|
||||||
|
.st114{fill:#90AD49;}
|
||||||
|
.st115{fill:#D8E6BB;}
|
||||||
|
.st116{fill:none;stroke:#000000;stroke-miterlimit:10;}
|
||||||
|
.st117{fill:#A6AE3C;}
|
||||||
|
.st118{fill:none;stroke:#A6AE3C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st119{fill:#4E6014;stroke:#A6AE3C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st120{fill:none;stroke:#70841F;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st121{fill:#D4EA75;stroke:#A6AE3C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st122{fill:none;stroke:#A6AE3C;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st123{fill:#D3E39C;stroke:#A6AE3C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st124{fill:none;stroke:#646800;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st125{fill:#233518;}
|
||||||
|
.st126{fill:#4E6014;}
|
||||||
|
.st127{fill:#70841F;}
|
||||||
|
.st128{fill:#D4EA75;}
|
||||||
|
.st129{fill:#665700;}
|
||||||
|
.st130{fill:none;stroke:#9DB269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st131{fill:none;stroke:#ED1C24;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st132{fill:none;stroke:#253A00;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st133{fill:none;stroke:#253A00;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st134{fill:none;stroke:#253A00;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st135{fill:#8FA50E;stroke:#517006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st136{opacity:0.4;fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st137{fill:none;stroke:#517006;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st138{opacity:0.4;fill:#FFFFFF;}
|
||||||
|
.st139{opacity:0.2;}
|
||||||
|
.st140{fill:none;stroke:#517006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st141{opacity:0.3;fill:none;stroke:#93AA30;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st142{opacity:0.3;fill:#93AA30;}
|
||||||
|
.st143{fill:#93AA30;stroke:#517006;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st144{fill:none;stroke:#507616;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||||
|
.st145{fill:#507616;}
|
||||||
|
.st146{fill:#ED1C24;}
|
||||||
|
.st147{fill:#253A00;}
|
||||||
|
.st148{fill:#2B4403;}
|
||||||
|
.st149{fill:#BFCEAE;}
|
||||||
|
.st150{fill:#3C5B0A;}
|
||||||
|
.st151{fill:#48660E;}
|
||||||
|
.st152{fill:#57752F;}
|
||||||
|
.st153{fill:#FB786C;}
|
||||||
|
.st154{fill:#2B4403;stroke:#2B4403;stroke-width:8;stroke-miterlimit:10;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st146" d="M480.9,136.7c-0.7-15.1-10-28.4-24-34.2C436.9,94,426.5,72,432.8,51.2c6.1-20.1-4.7-41.4-24.5-48.3
|
||||||
|
c-19.8-6.9-41.5,3-49.3,22.5c-8.2,20.7-31,31.4-52.1,24.8c-0.5-0.2-1-0.4-1.7-0.6c-14.9-5.3-31.5-1.2-42.1,10.5
|
||||||
|
c-10.6,11.7-13.2,28.6-6.6,42.9c8.8,18.8,10,39.9,3.4,59.4c-4.9,14.7-13.8,27.4-25.7,36.9c-5.3,4.2-13.2,0.7-13.5-6
|
||||||
|
c-0.5-11.4,1.8-22.8,6.9-33.3c6.6-13.6,4.7-29.8-4.7-41.5s-24.8-17-39.5-13.6c-16.5,3.9-33.1-5.2-38.7-21.2
|
||||||
|
c-6.9-19.8-28.2-30.7-48.3-24.6c-20.1,6-31.8,26.9-26.6,47.2c4.3,16.7-5,33.9-21.3,39.4c-0.4,0.1-0.9,0.2-1.5,0.4
|
||||||
|
C32,150.3,21,163.4,19.3,179.1c-1.7,15.7,6.1,30.9,19.9,38.5c15,8.3,26.1,21.8,31.2,38.1c5.2,16.6,3.8,34.4-4.1,50
|
||||||
|
c-7.9,15.5-5.5,34,5.9,47.1c11.5,13.1,29.5,17.9,46,12.2c0.2-0.1,0.3-0.1,0.5-0.2c16.4-5.2,33.7,2.8,40.4,18.5c0.4,1,0.9,2,1.4,3
|
||||||
|
c13.4,30.7,29.7,62.4,49.4,94.3c7.5,12.1,20.4,18.7,33.6,18.6c6.9,0,13.9-1.9,20.2-5.8c6.7-4.1,11.7-9.9,14.8-16.6
|
||||||
|
c8.1-13.4,32.9-57.4,54-121.2c7.8-17.7,27.2-28.2,46.5-24.6c14.1,2.6,28.4-2.7,37.4-13.8c9-11.1,11.3-26.2,5.9-39.4
|
||||||
|
c-7.5-18.5-8.2-39.1-1.9-57.9c6.8-20.2,20.9-36.8,39.8-47C473.6,165.9,481.6,151.8,480.9,136.7z M237,365.7
|
||||||
|
c-0.8-1.8-1.6-3.5-2.4-5.3c-5.5-12.3-3.3-26.9,6.1-36.6c2.6-2.6,5.7-4.9,9.1-6.5c0.5-0.2,0.9-0.5,1.4-0.7c2.1-1.1,4.6,0,5.1,2.3
|
||||||
|
c1.3,5.9,1.3,12.1-0.1,18.3c-3.4,10.1-7,19.5-10.5,28.3C244.2,369.4,238.7,369.5,237,365.7z"/>
|
||||||
|
<path class="st147" d="M294.3,358.1c-1.1,0-2.2-0.1-3.3-0.5c-6-1.7-9.6-7.9-8.2-14c3.9-16.8,1.5-34.5-7-49.7
|
||||||
|
c-8.5-15.3-22.3-26.8-38.9-32.3c-0.5-0.2-0.9-0.3-1.4-0.5c-6.1-1.9-10.4-7.2-11-13.5c-0.6-6.3,2.7-12.3,8.4-15.2
|
||||||
|
c25.2-12.9,44.1-34.8,53.1-61.6c8.8-26.2,7.2-54.5-4.6-79.6c-2-4.3-1.2-9.4,2-12.9c3.2-3.5,8.2-4.7,12.6-3.2
|
||||||
|
c0.4,0.2,0.9,0.3,1.3,0.5l0.2,0.1c35.1,11.7,73.3-6.1,87-40.4c2.3-5.8,8.9-8.8,14.8-6.7c5.9,2.1,9.2,8.5,7.4,14.5
|
||||||
|
c-10.4,34.3,6.7,70.7,39.7,84.5c4.2,1.8,7,5.7,7.2,10.3c0.2,4.5-2.2,8.8-6.2,10.9c-25.1,13.5-43.9,35.6-52.9,62.4
|
||||||
|
c-8.4,25-7.5,52.4,2.5,76.9c1.6,4,0.9,8.5-1.8,11.8c-2.7,3.3-7,4.9-11.2,4.1c-16.3-3-33.2,0-47.6,8.3c-14.5,8.4-25.5,21.8-31,37.7
|
||||||
|
C303.7,355,299.2,358.1,294.3,358.1z M257.7,245.1c16.2,8.4,29.7,21.4,38.7,37.6c4.2,7.6,7.3,15.7,9.3,24
|
||||||
|
c5.6-5.6,12-10.4,18.9-14.5c13.7-8,29.2-12.2,44.9-12.6c-6.4-24.9-5.5-51.3,2.8-75.8c8.7-26,25-48.4,46.8-64.6
|
||||||
|
c-20.1-14.4-33.4-35.9-37.6-59.7c-19.2,16.9-44.9,25.6-71,23c6.8,24.9,6.1,51.2-2.2,75.9C299,205.8,281.4,229,257.7,245.1z"/>
|
||||||
|
<path class="st147" d="M195.2,380c-4.6,0-9-2.7-10.9-7.2c-5.8-13.8-16.5-24.9-30.2-31.2c-13.7-6.4-29.2-7.4-43.7-2.8
|
||||||
|
c-0.4,0.1-0.8,0.3-1.2,0.4c-5.9,2-12.3,0.3-16.4-4.3c-4.1-4.7-4.9-11.2-2.1-16.7c11.2-22.1,13.2-47.2,5.8-70.6
|
||||||
|
c-7.2-23-22.9-42.1-44.1-53.8c-4.1-2.3-6.5-6.9-6-11.6c0.5-4.7,3.8-8.6,8.4-10c0.4-0.1,0.8-0.2,1.2-0.3l0.1,0
|
||||||
|
c30.5-9.7,48.1-41.6,40.2-72.6c-1.6-6.1,2-12.3,8-14.2c6-1.8,12.4,1.4,14.5,7.4c10.2,29.3,40.6,46,70.8,38.9c4.4-1,9,0.5,11.9,4.1
|
||||||
|
c2.8,3.5,3.4,8.4,1.4,12.5c-10.8,22.4-12.7,47.7-5.2,71.2c6.9,22,21.6,40.8,41.2,53c3.6,2.3,5.8,6.3,5.5,10.6
|
||||||
|
c-0.2,4.3-2.8,8.1-6.6,10c-12.9,6.2-23.3,16.9-29.1,30.1c-5.9,13.3-6.8,28.2-2.5,42.1c1.8,6-1.3,12.3-7.2,14.5
|
||||||
|
C197.9,379.7,196.5,380,195.2,380z M128.2,312.5c12.3-0.1,24.5,2.5,35.9,7.8c6.4,3,12.3,6.7,17.6,11c1.2-6.2,3.1-12.3,5.7-18.2
|
||||||
|
c5.6-12.5,14-23.4,24.6-31.8c-17.1-14.6-29.8-33.7-36.6-55.2c-7.2-22.8-7.4-46.9-0.7-69.6c-20.7-0.3-40.4-8.2-55.4-21.9
|
||||||
|
c-5.3,20.9-18.5,39.4-37.2,51c17.3,14.3,30.1,33.2,36.9,54.9c7.6,24,7.2,49.3-0.7,72.7C121.6,312.8,124.9,312.5,128.2,312.5z"/>
|
||||||
|
<path class="st148" d="M461.3,140.7c0.9-8.8-4-16.9-11.9-20.2C420.1,108.2,405,76.2,414,45.9c2.6-8.9-0.8-18.7-9-23.2
|
||||||
|
c-10.6-5.9-23.5-1-27.8,9.8c-12.1,30.5-45.9,46.2-77,36c-0.5-0.2-1-0.4-1.6-0.6c-8.4-3-17.8,0-22.9,7.6c-3.8,5.7-4.2,13-1.4,19.2
|
||||||
|
c10.8,23.3,12.3,49.5,4.2,73.7c-8.3,24.8-25.8,45.1-49.2,57.1c-0.3,0.2-0.6,0.3-0.9,0.5c-8.8,5-20,0.5-23.1-9.1
|
||||||
|
c0-0.1-0.1-0.2-0.1-0.3c-6.8-21.4-5.1-44.4,4.6-64.9c2.8-6,2.9-13-0.4-18.7c-4.4-7.7-13.2-11.3-21.5-9.4
|
||||||
|
c-26.3,6.2-52.8-8.3-61.7-33.8c-3.4-9.9-14.1-15.3-24.1-12.3c-10,3-15.9,13.4-13.3,23.6c6.9,26.9-8.4,54.5-34.7,63.1
|
||||||
|
c-0.5,0.1-1,0.2-1.4,0.4c-8.2,2.4-13.9,9.8-14.1,18.5c-0.1,7.2,3.9,14,10.3,17.5c19.3,10.8,33.6,28.2,40.3,49.2
|
||||||
|
c6.8,21.5,4.9,44.5-5.3,64.8c-4.2,8.4-3,18.4,3.2,25.4c6.2,7.1,15.9,9.7,24.8,6.6c0.3-0.1,0.7-0.2,1-0.3c5.5-1.7,11.1-2.6,16.6-2.5
|
||||||
|
c18.4,0.2,34.8,11.6,42,28.5c13.8,32.5,30.8,66.3,51.8,100.3c4.5,7.2,12.2,11.2,20.2,11.2c4.2,0,8.4-1.2,12.1-3.5
|
||||||
|
c4.2-2.6,7.3-6.3,9.2-10.6c7.3-12.1,33.4-57.5,54.8-124.3c3.5-10.9,10.7-20.2,20.5-26c0.1,0,0.1-0.1,0.2-0.1
|
||||||
|
c12.8-7.4,27.8-10,42.3-7.4c8.3,1.5,16.8-2.5,20.9-10.2c2.7-5.2,2.8-11.4,0.6-16.8c-9.1-22.7-9.9-48-2.1-71.1
|
||||||
|
c8.3-24.7,25.6-45.2,48.6-57.7C456.4,152.9,460.5,147.3,461.3,140.7z M237.5,402.6c-7.7-14.8-14.6-29.5-20.9-44
|
||||||
|
c-5.1-11.7-4.8-25.1,1.2-36.3c5.1-9.6,13.2-17.6,23.5-22.6c4.7-2.3,8.3-6.3,10-11.1l0,0c1.4-4.1,6.7-5.3,9.6-2
|
||||||
|
c3.1,3.4,5.8,7.2,8,11.3c2,3.6,3.6,7.3,4.8,11.2c2.7,8.3,2.4,17.3-0.3,25.6c-8.8,26.7-18.3,49.7-26.6,67.7
|
||||||
|
C245.1,406.3,239.5,406.4,237.5,402.6z"/>
|
||||||
|
<path class="st67" d="M294.3,346.4c9.5-40.5-13.3-82.3-53.6-95.8c-0.6-0.2-1.1-0.4-1.7-0.6c-3.3-1-3.7-5.5-0.7-7.1
|
||||||
|
c26.8-13.7,48.5-37.5,58.8-68.3c10.1-30.2,7.4-61.6-5.1-88.3c0.5,0.2,1,0.4,1.6,0.6c41.3,13.8,85.8-7.2,101.7-47.2
|
||||||
|
c-12.2,40.1,8.1,82.7,46.4,98.8c-26.6,14.3-48.1,38.2-58.4,69c-9.7,28.9-7.9,58.9,2.7,85.1C346.9,285.5,307.5,307.8,294.3,346.4z"
|
||||||
|
/>
|
||||||
|
<path class="st67" d="M195.1,368.2c-14.3-33.9-52.4-51.9-88.2-40.6c-0.5,0.2-1,0.3-1.5,0.5c-2.9,1-5.5-2-4.1-4.7
|
||||||
|
c12-23.8,15.2-52.1,6.5-79.5c-8.5-26.9-26.8-47.9-49.6-60.6c0.5-0.1,1-0.2,1.4-0.4c36.8-11.6,57.6-49.9,48.2-86.7
|
||||||
|
c12.2,35,48.8,54.9,84.6,46.5c-11.6,24.1-14.5,52.3-5.9,79.8c8.1,25.7,25,46.2,46.3,59.5C201.1,297.3,184.5,333.7,195.1,368.2z"/>
|
||||||
|
<path class="st149" d="M133.5,200.4c-0.7,0-1.3-0.2-1.9-0.6c-6.6-4.2-13.7-7.7-21.1-10.3c-2-0.7-3.1-3-2.4-5c0.7-2,3-3.1,5-2.4
|
||||||
|
c8,2.8,15.6,6.6,22.7,11.1c1.8,1.2,2.4,3.6,1.2,5.4C136.2,199.8,134.8,200.5,133.5,200.4z"/>
|
||||||
|
<circle class="st149" cx="97.3" cy="181.9" r="3.9"/>
|
||||||
|
<g class="st139">
|
||||||
|
<path class="st150" d="M375.4,248.6c-0.1,0-0.2,0-0.4,0c-27,2.1-50.8,16.6-65.1,38.5c-1.1-27.4-15.4-53.2-39.2-68.5
|
||||||
|
c-0.9,2.5-2.5,4.7-4.8,6.2c27.7,17.5,41.5,50.5,34.3,82.4c-0.5,2,0.8,4.1,2.8,4.6c0.4,0.1,0.7,0.1,1.1,0.1c1.6,0,3.1-1.1,3.6-2.7
|
||||||
|
c9.9-29.7,36.5-50.5,67.8-52.9c0,0,0.1,0,0.1,0C374.9,253.9,374.8,251.1,375.4,248.6z"/>
|
||||||
|
<path class="st150" d="M383.1,210.4c-0.3-2.1-2.3-3.6-4.4-3.3c-24.2,3.5-45.3,16.9-58.5,36.5c-0.5-22.2-9.6-43.7-25.9-59.6
|
||||||
|
c-0.6,2.7-2,5.2-4.1,6.9c18.7,19.1,26.4,46.6,20.1,72.6c-0.5,2,0.7,4.1,2.7,4.7c0.4,0.1,0.8,0.2,1.1,0.2c1.6,0,3.1-1,3.7-2.6
|
||||||
|
c9.6-27.3,33.4-46.8,62.1-51C382,214.6,383.4,212.6,383.1,210.4z"/>
|
||||||
|
<path class="st150" d="M325,164.3c2-0.9,2.9-3.2,2-5.2c-4.4-9.9-10.6-18.8-18.3-26.4l-2.5-2.5c-0.9-0.9-2-1.2-3.1-1.1
|
||||||
|
c0.9,2.9,0.9,6.1-0.1,9l0.2,0.2c7,6.9,12.6,14.9,16.6,24c0.7,1.5,2.1,2.3,3.6,2.3C323.9,164.6,324.5,164.5,325,164.3z"/>
|
||||||
|
<path class="st150" d="M404,162.9c-17-0.3-33.6,4.4-48,13.6c-14.7,9.4-26,23-32.7,39.1c-0.8,2,0.1,4.3,2.1,5.1
|
||||||
|
c0.5,0.2,1,0.3,1.5,0.3c1.5,0,3-0.9,3.6-2.4c12.3-29.7,41.1-48.5,73.3-47.9c2.1,0,3.9-1.7,4-3.8
|
||||||
|
C407.9,164.7,406.2,162.9,404,162.9z"/>
|
||||||
|
<path class="st150" d="M342.2,130c0.4,0,0.8-0.1,1.2-0.2c2.1-0.7,3.2-2.9,2.5-4.9c-4.2-13.1-11.4-25-21.1-34.6
|
||||||
|
c-2,1.6-4.6,2.7-7.8,2.7c-0.1,0-0.2,0-0.3,0c0.2,0.4,0.4,0.7,0.8,1c9.7,9.1,17,20.6,21,33.3C339,128.9,340.6,130,342.2,130z"/>
|
||||||
|
<path class="st150" d="M417.1,122.4c-33.8,0-64.9,20.3-78.5,51.4c-0.9,2,0.1,4.3,2,5.1c0.5,0.2,1.1,0.3,1.6,0.3
|
||||||
|
c1.5,0,2.9-0.9,3.6-2.4c12.8-29.5,43-48.2,75.1-46.6c0.7,0,1.3-0.1,1.9-0.4C419.9,128.3,418,125.6,417.1,122.4z"/>
|
||||||
|
<path class="st150" d="M393,55.4c-2.4,0-4.4-0.6-6.1-1.5l-35.5,89.2c-0.8,2,0.2,4.3,2.2,5.1c0.5,0.2,0.9,0.3,1.4,0.3
|
||||||
|
c1.6,0,3.1-0.9,3.7-2.5l36.1-90.6C394.2,55.4,393.6,55.4,393,55.4z"/>
|
||||||
|
<path class="st150" d="M206.8,254.9c-1.3-1.7-3.8-2-5.5-0.7c-18.3,14.3-28.3,35.6-28.3,57.6c-14.2-17.8-36.4-28.6-60.2-28.5
|
||||||
|
c0.6,2.6,0.5,5.3-0.3,7.8c27.8-0.3,53.2,16,63.6,41.2c0.6,1.5,2.1,2.4,3.6,2.4c0.4,0,0.9-0.1,1.3-0.2c2-0.7,3-2.8,2.4-4.8
|
||||||
|
c-7.6-25.5,1.3-52.7,22.6-69.4C207.8,259.1,208.1,256.6,206.8,254.9z"/>
|
||||||
|
<path class="st150" d="M189.5,228.9c-2.1-1.7-3.5-4.1-4.2-6.7c-0.2,0.1-0.4,0.3-0.6,0.5c-15.6,14-24.2,33.2-24.5,53.1
|
||||||
|
c-11.7-15.2-29.2-25.7-48.9-28.8c-2.1-0.3-4.1,1.1-4.5,3.3c-0.3,2.1,1.1,4.1,3.3,4.5c24,3.7,44.2,19.6,52.8,41.6
|
||||||
|
c0.6,1.5,2.1,2.5,3.7,2.5c0.4,0,0.8-0.1,1.2-0.2c2-0.7,3.1-2.7,2.5-4.8C163.9,270.4,171.2,245.5,189.5,228.9z"/>
|
||||||
|
<path class="st150" d="M182,187.2c-1-3-0.9-6.4,0.2-9.4c-0.4,0.1-0.7,0.3-1.1,0.5c-20.1,13.2-32,34.2-33.3,56.8
|
||||||
|
c-11.5-16.4-29.4-27.7-50.2-31c0,0-0.1,0-0.1,0l-8.2-1c-1.5-0.2-2.9,0.5-3.7,1.6c1.9,1.6,3.2,3.8,3.9,6.3l6.9,0.9
|
||||||
|
c24.9,4,45.3,20.9,53.3,44.1c0.6,1.6,2.1,2.6,3.7,2.6c0.4,0,0.7,0,1.1-0.2c2-0.6,3.2-2.6,2.7-4.6C151.3,228.5,161,202.8,182,187.2
|
||||||
|
z"/>
|
||||||
|
<path class="st150" d="M172.1,143.3c-26.8,16-40.7,46.9-34.7,76.8c0.4,1.9,2,3.2,3.9,3.1c0.2,0,0.5,0,0.7-0.1
|
||||||
|
c2.1-0.4,3.5-2.5,3.1-4.6c-5.4-26.7,7.1-54.3,31-68.6l6.2-3.7L172.1,143.3z"/>
|
||||||
|
<path class="st150" d="M140.4,190.1l-23.3-77.3c-1.9,1.4-4.3,2.2-7.3,2.3c-0.1,0-0.1,0-0.2,0l23.3,77.3c0.5,1.7,2.1,2.8,3.8,2.8
|
||||||
|
c0.4,0,0.7-0.1,1.1-0.2C139.8,194.4,141,192.2,140.4,190.1z"/>
|
||||||
|
</g>
|
||||||
|
<path class="st151" d="M443.3,135.1c-36.8-15.4-55.9-55.9-44.3-94.1c0.5-1.7-0.1-3.5-1.6-4.4c-2.1-1.2-4.6-0.3-5.5,1.8
|
||||||
|
c-15.2,38.3-57.7,58-96.8,45.1c-0.2-0.1-0.6-0.2-0.9-0.3c-0.2-0.1-0.5-0.2-0.7-0.3c-1.1-0.4-2.3-0.4-3.3,0.3
|
||||||
|
c-1.7,1-2.3,3.1-1.5,4.8c12.6,27.1,14.3,57.4,4.9,85.6c-9.7,28.8-29.9,52.3-57,66.1c-3.1,1.6-4.7,5-3.9,8.5
|
||||||
|
c0.6,2.7,2.8,4.8,5.4,5.6c0.5,0.1,0.9,0.3,1.4,0.4c18.5,6.2,33.9,19,43.4,36c3.6,6.6,6.3,13.5,7.9,20.7c1.9,8.4,1.4,17.1-1.2,25.3
|
||||||
|
c-14.5,45.2-31.3,80.2-42.1,100.4c-1.9,3.6-7.1,3.7-9.1,0.1c-14.5-25.2-27.5-51.5-38.9-78.6c-2.8-6.7-3.9-14.1-2.8-21.3
|
||||||
|
c0.9-5.9,2.6-11.7,5-17.3c6.6-14.8,18.2-26.8,32.7-33.9c0.9-0.4,1.7-1.2,2.1-2.1c0.7-1.8,0-3.8-1.6-4.8
|
||||||
|
c-21.3-13.2-37.1-33.5-44.6-57.3c-8-25.4-6-52.7,5.7-76.9c0.8-1.7,0.3-3.7-1.3-4.9c-1-0.7-2.2-0.9-3.3-0.6
|
||||||
|
c-34.1,8-68.4-10.9-79.9-44c-0.7-2-2.8-3.1-4.8-2.5c-2,0.6-3.2,2.7-2.7,4.7c9,35-10.9,70.9-45.3,81.9c-0.2,0.1-0.6,0.2-0.8,0.2
|
||||||
|
c-0.2,0-0.4,0.1-0.6,0.1c-1.2,0.3-2.3,1.1-2.8,2.3c-0.8,1.9,0,4,1.7,5c23,12.7,39.9,33.4,47.8,58.3c8,25.5,5.8,52.6-6.3,76.5
|
||||||
|
c-1.6,3.2-0.7,7,2.3,9.2c2,1.4,4.5,1.7,6.8,0.9c0.4-0.1,0.8-0.3,1.2-0.4c16.3-5.2,33.8-4,49.3,3.2c7.6,3.5,14.3,8.3,20,14.2
|
||||||
|
c1.3,1.4,2.4,3,3.1,4.7l0,0c15.6,38.6,34.4,75.9,56,110.9c1.5,2.4,4.1,3.7,6.7,3.7c1.4,0,2.8-0.4,4-1.2c1.2-0.8,3.3-4.2,3.3-4.2
|
||||||
|
c5.8-9.4,33.1-55.6,55-125.3c3.1-9.9,8.9-18.7,17-25.1c3-2.4,6.3-4.7,9.7-6.6c16-9.3,34.9-12.6,53.1-9.2c1.3,0.2,2.7-0.3,3.6-1.3
|
||||||
|
c0.9-1.1,1.1-2.5,0.6-3.8c-10.7-26.3-11.6-55.6-2.6-82.5c9.6-28.7,29.7-52.4,56.6-66.9c0.9-0.5,1.6-1.3,1.9-2.3
|
||||||
|
C446,137.7,445,135.8,443.3,135.1z M160.8,327.4c-10.3-4.8-21.4-7.2-32.5-7.1c-4.5,0-8.9,0.5-13.4,1.3c-1.1,0.2-2.7,0.6-4.3,1
|
||||||
|
c-2,0.5-3.8-1.5-3-3.5c10.5-24.2,11.9-51.1,3.9-76.4c-7.1-22.4-21-41.6-39.8-55.3c-1.9-1.4-1.6-4.3,0.5-5.3
|
||||||
|
c22.6-11.4,37.7-33.2,41.1-57.6c0.4-2.7,3.7-3.7,5.4-1.7c15.4,17.6,38.3,27.3,62.2,25.7c2.2-0.2,3.9,2.1,3.1,4.2
|
||||||
|
c-8.1,22.9-8.6,47.8-1.2,71.1c7,22.2,20.8,41.7,39.4,55.7c1.4,1.1,1.3,3.2-0.1,4.2c-12,8-21.6,19.3-27.6,32.7
|
||||||
|
c-1.2,2.7-2.2,5.4-3.1,8.1c-0.8,2.6-4.4,2.6-5.4,0.1c-17.3-45.8-32-93.2-43.8-141.1c-1-4.2-5.3-6.8-9.5-5.7c-4.2,1-6.8,5.3-5.7,9.5
|
||||||
|
c11.6,47.4,26.1,94.2,43.1,139.6c0.9,2.5-1.7,4.8-4,3.5C164.4,329.2,162.6,328.3,160.8,327.4z M379.8,206.4
|
||||||
|
c-8.5,25.2-8.7,52.5-0.7,77.9c0.6,1.8-0.9,3.6-2.8,3.5c-16.6-0.9-33.2,3-47.7,11.4c-3.4,2-6.6,4.1-9.6,6.5
|
||||||
|
c-1.7,1.3-4.2-0.3-3.6-2.4c2.7-10.7,5.3-21.8,7.5-33.3c10.6-53.8,28.2-99.1,41-127.6c1.8-4.1-0.1-8.9-4.4-10.5
|
||||||
|
c-3.9-1.5-8.3,0.5-10,4.3c-13.2,29.3-31.2,75.7-42,130.8c-2.3,11.5-4.8,22.6-7.6,33.3c-0.6,2.2-3.7,2.3-4.4,0.2
|
||||||
|
c-1.6-4.7-3.7-9.3-6.1-13.8c-9.3-16.7-23.8-29.8-41.3-37.2c-0.6-0.2-1.5-0.6-2.5-1c-2.2-0.8-2.4-3.8-0.4-4.9
|
||||||
|
c26.2-15,45.8-38.8,55.5-67.6c8.6-25.7,8.5-53.2-0.2-78.7c-0.7-2.1,1-4.1,3.2-3.7c5.5,1,11,1.5,16.5,1.5
|
||||||
|
c24.1-0.2,47.1-10.6,63.1-28.5c1.7-1.9,4.8-0.8,4.9,1.7c1.5,27.9,16.7,53.7,41.4,68.4c1.8,1.1,1.9,3.7,0.1,4.8
|
||||||
|
C406.3,156.9,388.8,179.6,379.8,206.4z"/>
|
||||||
|
<path class="st149" d="M332.4,181.1c-0.5,0-1-0.1-1.4-0.3c-2-0.8-3-3-2.3-5.1c4.3-11.2,8.9-22.4,13.8-33.3c0.9-2,3.2-2.9,5.2-2
|
||||||
|
c2,0.9,2.8,3.2,2,5.2c-4.8,10.7-9.4,21.8-13.7,32.9C335.4,180.2,333.9,181.1,332.4,181.1z"/>
|
||||||
|
<g>
|
||||||
|
<path class="st152" d="M195.3,362.1c-1.6,0-3-0.9-3.7-2.5c-1.8-4.5-3.6-9.1-5.3-13.6c-0.8-2,0.3-4.3,2.3-5c2-0.8,4.3,0.3,5,2.3
|
||||||
|
c1.7,4.5,3.5,9,5.3,13.5c0.8,2-0.2,4.3-2.2,5.1C196.3,362,195.8,362.1,195.3,362.1z"/>
|
||||||
|
<path class="st152" d="M226.9,428.1c-1.4,0-2.8-0.7-3.5-2c-9.4-17.4-18.3-35.5-26.3-53.7c-0.9-2,0-4.3,2-5.2c2-0.9,4.3,0,5.2,2
|
||||||
|
c7.9,18.1,16.7,35.9,26.1,53.2c1,1.9,0.3,4.3-1.6,5.3C228.1,427.9,227.5,428,226.9,428.1z"/>
|
||||||
|
|
||||||
|
<ellipse transform="matrix(8.600912e-02 -0.9963 0.9963 8.600912e-02 -82.5032 619.1811)" class="st152" cx="296.2" cy="354.6" rx="3.6" ry="3.6"/>
|
||||||
|
<path class="st152" d="M283.2,392c-0.5,0-1-0.1-1.5-0.3c-2-0.8-2.9-3.1-2.1-5.1c3-7.1,5.8-14.5,8.5-21.8c0.8-2,3-3.1,5-2.3
|
||||||
|
c2,0.8,3.1,3,2.3,5c-2.7,7.4-5.7,14.8-8.6,22C286.2,391.1,284.7,392,283.2,392z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 22 KiB |
Loading…
Reference in New Issue