diff --git a/frontend/__test_support__/fake_dispatch.ts b/frontend/__test_support__/fake_dispatch.ts
new file mode 100644
index 000000000..c660325e8
--- /dev/null
+++ b/frontend/__test_support__/fake_dispatch.ts
@@ -0,0 +1,2 @@
+export const mockDispatch = (innerDispatch = jest.fn()) =>
+ jest.fn(x => typeof x === "function" && x(innerDispatch));
diff --git a/frontend/constants.ts b/frontend/constants.ts
index a4f964298..4153ee19e 100644
--- a/frontend/constants.ts
+++ b/frontend/constants.ts
@@ -2,6 +2,27 @@ import { trim } from "./util";
export namespace ToolTips {
+ // Farm Designer: Groups
+ export const SORT_DESCRIPTION =
+ trim(`When executing a sequence over a Group of locations, FarmBot will
+ travel to each group member in the order of the chosen sort method.
+ If the random option is chosen, FarmBot will travel in a random order
+ every time, so the ordering shown below will only be representative.`);
+
+ export const CRITERIA_SELECTION_COUNT =
+ trim(`Filter additions can only be removed by changing filters.
+ Click and drag in the map to modify selection filters.
+ Filters will be applied at the time of sequence execution. The final
+ selection at that time may differ from the selection currently
+ displayed.`);
+
+ export const CRITERIA_ALPHA_FEATURE =
+ trim(`Group filters is a new feature under active development.
+ Use with caution.`);
+
+ export const DOT_NOTATION_TIP =
+ trim(`Tip: Use dot notation (i.e., 'meta.color') to access meta fields.`);
+
// Controls
export const MOVE =
trim(`Use these manual control buttons to move FarmBot in realtime.
@@ -12,7 +33,12 @@ export namespace ToolTips {
export const WEBCAM =
trim(`If you have a webcam, you can view the video stream in this widget.
- Press the edit button to update and save your webcam URL.`);
+ Press the edit button to update and save your webcam URL.
+ Note: Some webcam services do not allow webcam feeds to be embedded in
+ other sites. If you see a web browser error after adding a webcam feed,
+ there is unfortunately nothing FarmBot can do to fix the problem.
+ Please contact your webcam's customer support to see if the security
+ policy for embedding feeds into other sites can be changed.`);
export const PERIPHERALS =
trim(`Use these toggle switches to control FarmBot's peripherals in
@@ -26,10 +52,19 @@ export namespace ToolTips {
export const SENSOR_HISTORY =
trim(`View and filter historical sensor reading data.`);
- // Device
- export const OS_SETTINGS =
- trim(`View and change device settings.`);
+ // FarmBot OS Settings: Firmware
+ export const FIRMWARE_VALUE_API =
+ trim(`Firmware value from your choice in the dropdown to the left, as
+ understood by the Web App.`);
+ export const FIRMWARE_VALUE_FBOS =
+ trim(`Firmware value reported from the firmware, as understood by
+ FarmBot OS.`);
+
+ export const FIRMWARE_VALUE_MCU =
+ trim(`Firmware value reported from the firmware.`);
+
+ // Hardware Settings
export const HW_SETTINGS =
trim(`Change settings of your FarmBot hardware with the fields below.
Caution: Changing these settings to extreme values can cause hardware
@@ -38,18 +73,6 @@ export namespace ToolTips {
Tip: Recalibrate FarmBot after changing settings and test a
few sequences to verify that everything works as expected.`);
- export const PIN_BINDINGS =
- trim(`Assign an action or sequence to execute when a Raspberry Pi
- GPIO pin is activated.`);
-
- export const PIN_BINDING_WARNING =
- trim(`Warning: Binding to a pin without a physical button and
- pull-down resistor connected may put FarmBot into an unstable state.`);
-
- // Connectivity
- export const CONNECTIVITY =
- trim(`Diagnose connectivity issues with FarmBot and the browser.`);
-
// Hardware Settings: Homing and Calibration
export const HOMING_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
@@ -202,13 +225,16 @@ export namespace ToolTips {
trim(`The number of the pin to guard. This pin will be set to the specified
state after the duration specified by TIMEOUT.`);
+ // Hardware Settings: Pin Bindings
+ export const PIN_BINDINGS =
+ trim(`Assign an action or sequence to execute when a Raspberry Pi
+ GPIO pin is activated.`);
+
+ export const PIN_BINDING_WARNING =
+ trim(`Warning: Binding to a pin without a physical button and
+ pull-down resistor connected may put FarmBot into an unstable state.`);
+
// Farmware
- export const FARMWARE =
- trim(`Manage Farmware (plugins).`);
-
- export const FARMWARE_LIST =
- trim(`View, select, and install new Farmware.`);
-
export const PHOTOS =
trim(`Take and view photos with your FarmBot's camera.`);
@@ -232,9 +258,6 @@ export namespace ToolTips {
You can also edit, copy, and delete existing sequences;
assign a color; and give your commands custom names.`);
- export const SEQUENCE_LIST =
- trim(`Here is the list of all of your sequences. Click one to edit.`);
-
export const DEFAULT_VALUE =
trim(`Select a location to be used as the default value for this variable.
If the sequence is ever run without the variable explicitly set to
@@ -312,6 +335,7 @@ export namespace ToolTips {
export const TAKE_PHOTO =
trim(`Snaps a photo using the device camera. Select the camera type
on the Device page.`);
+
export const EMERGENCY_LOCK =
trim(`Stops a device from moving until it is unlocked by a user.`);
@@ -363,20 +387,6 @@ export namespace ToolTips {
growing at the same or different times. Multiple regimens can be
applied to any one plant.`);
- export const REGIMEN_LIST =
- trim(`This is a list of all of your regimens. Click one to begin
- editing it.`);
-
- // Tools
- export const TOOL_LIST =
- trim(`This is a list of all your FarmBot tools and seed containers.
- Click the Edit button to add, edit, or delete tools or seed containers.`);
-
- export const TOOLBAY_LIST =
- trim(`Tool slots are where you store your FarmBot tools and seed
- containers, which should be reflective of your real FarmBot hardware
- configuration.`);
-
// Logs
export const LOGS =
trim(`View and filter log messages.`);
@@ -399,16 +409,6 @@ export namespace ToolTips {
export const FIRMWARE_DEBUG_MESSAGES =
trim(`Log all debug received from firmware (clears after refresh).`);
-
- export const MESSAGES =
- trim(`View messages.`);
-
- // App
- export const LABS =
- trim(`Customize your web app experience.`);
-
- export const TOURS =
- trim(`Take a guided tour of the Web App.`);
}
export namespace Content {
@@ -512,11 +512,9 @@ export namespace Content {
real account at`);
// App Settings
- export const CONFIRM_STEP_DELETION =
- trim(`Show a confirmation dialog when deleting a sequence step.`);
-
- export const CONFIRM_SEQUENCE_DELETION =
- trim(`Show a confirmation dialog when deleting a sequence.`);
+ export const TIME_FORMAT_24_HOUR =
+ trim(`Display time using the 24-hour notation,
+ i.e., 23:00 instead of 11:00pm`);
export const HIDE_WEBCAM_WIDGET =
trim(`If not using a webcam, use this setting to remove the
@@ -526,14 +524,6 @@ export namespace Content {
trim(`If not using sensors, use this setting to remove the
widget from the Controls page.`);
- export const DYNAMIC_MAP_SIZE =
- trim(`Change the garden map size based on axis length.
- A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
- the HARDWARE widget. Overrides MAP SIZE values.`);
-
- export const PLANT_ANIMATIONS =
- trim(`Enable plant animations in the garden map.`);
-
export const BROWSER_SPEAK_LOGS =
trim(`Have the browser also read aloud log messages on the
"Speak" channel that are spoken by FarmBot.`);
@@ -546,22 +536,25 @@ export namespace Content {
trim(`Warning! When enabled, any unsaved changes
will be discarded when refreshing or closing the page. Are you sure?`);
- export const DISCARD_UNSAVED_SEQUENCE_CHANGES =
- trim(`Don't ask about saving sequence work before
- closing browser tab. Warning: may cause loss of data.`);
+ export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
+ trim(`Confirm when unlocking FarmBot after an emergency stop.`);
- export const DISCARD_UNSAVED_SEQUENCE_CHANGES_CONFIRM =
- trim(`Warning! When enabled, any unsaved changes to sequences
- will be discarded when refreshing or closing the page. Are you sure?`);
+ export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
+ trim(`Warning! When disabled, clicking the UNLOCK button will immediately
+ unlock FarmBot instead of confirming that it is safe to do so.
+ As a result, double-clicking the E-STOP button may not stop FarmBot.
+ Are you sure you want to disable this feature?`);
- export const VIRTUAL_TRAIL =
- trim(`Display a virtual trail for FarmBot in the garden map to show
- movement and watering history while the map is open. Toggling this setting
- will clear data for the current trail.`);
+ export const USER_INTERFACE_READ_ONLY_MODE =
+ trim(`Disallow account data changes. This does
+ not prevent Farmwares or FarmBot OS from changing settings.`);
- export const TIME_FORMAT_24_HOUR =
- trim(`Display time using the 24-hour notation,
- i.e., 23:00 instead of 11:00pm`);
+ // Sequence Settings
+ export const CONFIRM_STEP_DELETION =
+ trim(`Show a confirmation dialog when deleting a sequence step.`);
+
+ export const CONFIRM_SEQUENCE_DELETION =
+ trim(`Show a confirmation dialog when deleting a sequence.`);
export const SHOW_PINS =
trim(`Show raw pin lists in Read Sensor, Control Peripheral, and
@@ -570,18 +563,27 @@ export namespace Content {
export const EXPAND_STEP_OPTIONS =
trim(`Choose whether advanced step options are open or closed by default.`);
- export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
- trim(`Confirm when unlocking FarmBot after an emergency stop.`);
+ export const DISCARD_UNSAVED_SEQUENCE_CHANGES =
+ trim(`Don't ask about saving sequence work before
+ closing browser tab. Warning: may cause loss of data.`);
- export const USER_INTERFACE_READ_ONLY_MODE =
- trim(`Disallow account data changes. This does
- not prevent Farmwares or FarmBot OS from changing settings.`);
+ export const DISCARD_UNSAVED_SEQUENCE_CHANGES_CONFIRM =
+ trim(`Warning! When enabled, any unsaved changes to sequences
+ will be discarded when refreshing or closing the page. Are you sure?`);
- export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
- trim(`Warning! When disabled, clicking the UNLOCK button will immediately
- unlock FarmBot instead of confirming that it is safe to do so.
- As a result, double-clicking the E-STOP button may not stop FarmBot.
- Are you sure you want to disable this feature?`);
+ // Farm Designer Settings
+ export const PLANT_ANIMATIONS =
+ trim(`Enable plant animations in the garden map.`);
+
+ export const VIRTUAL_TRAIL =
+ trim(`Display a virtual trail for FarmBot in the garden map to show
+ movement and watering history while the map is open. Toggling this setting
+ will clear data for the current trail.`);
+
+ export const DYNAMIC_MAP_SIZE =
+ trim(`Change the garden map size based on axis length.
+ A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
+ the HARDWARE widget. Overrides MAP SIZE values.`);
export const MAP_SIZE =
trim(`Specify custom map dimensions (in millimeters).
@@ -600,13 +602,41 @@ export namespace Content {
export const CONFIRM_PLANT_DELETION =
trim(`Show a confirmation dialog when deleting a plant.`);
- // Device
- export const NOT_HTTPS =
- trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
+ // FarmBot OS Settings
+ export const DIFFERENT_TZ_WARNING =
+ trim(`Note: The selected timezone for your FarmBot is different than
+ your local browser time.`);
- export const CONTACT_SYSADMIN =
- trim(`Please contact the system(s) administrator(s) and ask them to enable
- HTTPS://`);
+ export const OS_BETA_RELEASES =
+ trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
+ FarmBot system stability. Are you sure?`);
+
+ export const DEVICE_NEVER_SEEN =
+ trim(`The device has never been seen. Most likely,
+ there is a network connectivity issue on the device's end.`);
+
+ export const TOO_OLD_TO_UPDATE =
+ trim(`Please re-flash your FarmBot's SD card.`);
+
+ export const OS_AUTO_UPDATE =
+ trim(`When enabled, FarmBot OS will automatically download and install
+ software updates at the chosen time.`);
+
+ export const AUTO_SYNC =
+ trim(`When enabled, device resources such as sequences and regimens
+ will be sent to the device automatically. This removes the need to push
+ "SYNC" after making changes in the web app. Changes to running
+ sequences and regimens while auto sync is enabled will result in
+ instantaneous change.`);
+
+ // FarmBot OS Settings: Power and Reset
+ export const RESTART_FARMBOT =
+ trim(`This will restart FarmBot's Raspberry Pi and controller
+ software.`);
+
+ export const SHUTDOWN_FARMBOT =
+ trim(`This will shutdown FarmBot's Raspberry Pi. To turn it
+ back on, unplug FarmBot and plug it back in.`);
export const FACTORY_RESET_WARNING =
trim(`Factory resetting your FarmBot will destroy all data on the device,
@@ -624,10 +654,6 @@ export namespace Content {
not delete data stored in your web app account. Are you sure you wish
to continue?`);
- export const MCU_RESET_ALERT =
- trim(`Warning: This will reset all hardware settings to the default values.
- Are you sure you wish to continue?`);
-
export const AUTO_FACTORY_RESET =
trim(`Automatically factory reset when the WiFi network cannot be
detected. Useful for network changes.`);
@@ -636,54 +662,26 @@ export namespace Content {
trim(`Time in minutes to attempt connecting to WiFi before a factory
reset.`);
- export const DIFFERENT_TZ_WARNING =
- trim(`Note: The selected timezone for your FarmBot is different than
- your local browser time.`);
+ export const NOT_HTTPS =
+ trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
- export const RESTART_FARMBOT =
- trim(`This will restart FarmBot's Raspberry Pi and controller
- software.`);
+ export const CONTACT_SYSADMIN =
+ trim(`Please contact the system(s) administrator(s) and ask them to enable
+ HTTPS://`);
+ // FarmBot OS Settings: Firmware
export const RESTART_FIRMWARE =
trim(`Restart the Farmduino or Arduino firmware.`);
- export const OS_AUTO_UPDATE =
- trim(`When enabled, FarmBot OS will automatically download and install
- software updates at the chosen time.`);
-
- export const AUTO_SYNC =
- trim(`When enabled, device resources such as sequences and regimens
- will be sent to the device automatically. This removes the need to push
- "SYNC" after making changes in the web app. Changes to running
- sequences and regimens while auto sync is enabled will result in
- instantaneous change.`);
-
- export const SHUTDOWN_FARMBOT =
- trim(`This will shutdown FarmBot's Raspberry Pi. To turn it
- back on, unplug FarmBot and plug it back in.`);
-
- export const OS_BETA_RELEASES =
- trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
- FarmBot system stability. Are you sure?`);
-
- export const DIAGNOSTIC_CHECK =
- trim(`Save snapshot of FarmBot OS system information, including
- user and device identity, to the database. A code will be returned
- that you can provide in support requests to allow FarmBot to look up
- data relevant to the issue to help us identify the problem.`);
-
- export const DEVICE_NEVER_SEEN =
- trim(`The device has never been seen. Most likely,
- there is a network connectivity issue on the device's end.`);
-
- export const TOO_OLD_TO_UPDATE =
- trim(`Please re-flash your FarmBot's SD card.`);
-
- // Hardware Settings
+ // Hardware Settings: Danger Zone
export const RESTORE_DEFAULT_HARDWARE_SETTINGS =
trim(`Restoring hardware parameter defaults will destroy the
current settings, resetting them to default values.`);
+ export const MCU_RESET_ALERT =
+ trim(`Warning: This will reset all hardware settings to the default values.
+ Are you sure you wish to continue?`);
+
// App
export const APP_LOAD_TIMEOUT_MESSAGE =
trim(`App could not be fully loaded, we recommend you try
@@ -711,10 +709,6 @@ export namespace Content {
broken and may break or otherwise hinder your usage of the rest of the
app. This feature may disappear or break at any time.`);
- export const NEW_TOS =
- trim(`Before logging in, you must agree to our latest Terms of Service and
- Privacy Policy`);
-
export const FORCE_REFRESH_CONFIRM =
trim(`A new version of the FarmBot web app has been released.
Refresh page?`);
@@ -819,7 +813,8 @@ export namespace Content {
export const MOUNTED_TOOL =
trim(`The tool currently mounted to the UTM can be set here or by using
- a MARK AS step in a sequence.`);
+ a MARK AS step in a sequence. Use the verify button or read the tool
+ verification pin in a sequence to verify that a tool is attached.`);
// Farm Events
export const NOTHING_SCHEDULED =
@@ -831,10 +826,6 @@ export namespace Content {
regimen tasks. Consider rescheduling this event to tomorrow if
this is a concern.`);
- export const INVALID_RUN_TIME =
- trim(`This event does not appear to have a valid run time.
- Perhaps you entered bad dates?`);
-
export const FARM_EVENT_TZ_WARNING =
trim(`Note: Times displayed according to FarmBot's local time, which
is currently different from your browser's time. Timezone data is
@@ -849,27 +840,14 @@ export namespace Content {
trim(`You haven't made any sequences or regimens yet. To add an event,
first create a sequence or regimen.`);
- // Groups
- export const SORT_DESCRIPTION =
- trim(`When executing a sequence over a Group of locations, FarmBot will
- travel to each group member in the order of the chosen sort method.
- If the random option is chosen, FarmBot will travel in a random order
- every time, so the ordering shown below will only be representative.`);
-
- export const CRITERIA_SELECTION_COUNT =
- trim(`Criteria additions can only be removed by changing criteria.
- Click and drag in the map to modify selection criteria.
- Criteria will be applied at the time of sequence execution. The final
- selection at that time may differ from the selection currently
- displayed.`);
-
// Farmware
export const NO_IMAGES_YET =
trim(`You haven't yet taken any photos with your FarmBot.
Once you do, they will show up here.`);
export const PROCESSING_PHOTO =
- trim(`Processing now. Results usually available in one minute.`);
+ trim(`Processing now. Results usually available in one minute.
+ Check log messages for result status.`);
export const NOT_AVAILABLE_WHEN_OFFLINE =
trim(`Not available when device is offline.`);
@@ -1020,7 +998,6 @@ export enum DeviceSetting {
powerAndReset = `Power and Reset`,
restartFarmbot = `Restart Farmbot`,
shutdownFarmbot = `Shutdown Farmbot`,
- restartFirmware = `Restart Firmware`,
factoryReset = `Factory Reset`,
autoFactoryReset = `Automatic Factory Reset`,
connectionAttemptPeriod = `Connection Attempt Period`,
@@ -1038,6 +1015,7 @@ export enum DeviceSetting {
// Firmware
firmwareSection = `Firmware`,
+ restartFirmware = `Restart Firmware`,
flashFirmware = `Flash firmware`,
}
diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss
index 8a58a0dec..a778aef14 100644
--- a/frontend/css/farm_designer/farm_designer_panels.scss
+++ b/frontend/css/farm_designer/farm_designer_panels.scss
@@ -367,9 +367,13 @@
margin-top: 1rem;
}
}
- .saucer {
- margin: 1rem;
- margin-left: 2rem;
+ .point-color-input {
+ div[class*=col-] {
+ padding-left: 0.5rem;
+ }
+ .saucer {
+ margin-top: 2.75rem;
+ }
}
.delete-row {
margin: 1.5rem;
@@ -391,8 +395,27 @@
display: block;
margin-top: 3rem;
}
+ font-size: 1.4rem;
p {
margin-top: 1rem;
+ margin-bottom: 0.5rem !important;
+ font-size: 1.2rem;
+ }
+ .weed-removal-method-section {
+ .weed-removal-method {
+ display: flex;
+ input {
+ margin: 0;
+ width: 10%;
+ box-shadow: none;
+ }
+ label {
+ margin: 0;
+ margin-top: auto;
+ font-size: 1.25rem;
+ font-weight: normal;
+ }
+ }
}
}
@@ -517,6 +540,7 @@
margin-top: 1rem;
p {
font-size: 1.25rem;
+ margin-bottom: 0.5rem !important;
}
}
input {
@@ -836,6 +860,11 @@
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 5rem;
+ .clear-day-criteria,
+ .clear-point-ids,
+ .clear-criteria {
+ margin-top: 0.2rem;
+ }
.group-member-display {
i[class*=fa-caret-] {
float: right;
@@ -862,20 +891,43 @@
.criteria-heading {
margin-top: 0;
}
+ .alpha-icon {
+ display: inline;
+ float: none !important;
+ margin-left: 1rem;
+ color: $orange;
+ font-size: 1.4rem;
+ }
+ p {
+ &.category {
+ display: block;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ text-transform: none;
+ font-size: 1.2rem;
+ font-weight: bold;
+ }
+ }
+ .bp3-popover-wrapper {
+ float: right;
+ }
.fb-button {
margin-top: 0.5rem;
}
+ .point-type-section,
+ .criteria-checkbox-list-item {
+ .fb-checkbox {
+ display: inline;
+ margin-right: 1rem;
+ vertical-align: top;
+ }
+ p {
+ display: inline;
+ text-transform: uppercase;
+ }
+ }
.point-type-checkboxes {
.point-type-section {
- .fb-checkbox {
- display: inline;
- margin-right: 1rem;
- vertical-align: top;
- }
- p {
- display: inline;
- text-transform: uppercase;
- }
.point-type-checkbox {
position: relative;
height: 2rem;
@@ -894,19 +946,9 @@
}
}
.plant-criteria-options,
+ .weed-criteria-options,
.point-criteria-options,
.tool-criteria-options {
- margin-left: 3rem;
- p {
- &.category {
- display: block;
- padding-top: 1rem;
- padding-bottom: 1rem;
- text-transform: none;
- font-size: 1.2rem;
- font-weight: bold;
- }
- }
hr {
margin: 0.5rem;
}
@@ -936,7 +978,13 @@
margin-top: 1rem;
}
.day-criteria {
- p {
+ .criteria-checkbox-list-item {
+ margin-bottom: 1rem;
+ p {
+ vertical-align: middle;
+ }
+ }
+ .days-old-text {
display: inline;
vertical-align: bottom;
}
@@ -944,6 +992,7 @@
line-height: 1.75rem;
}
}
+ .number-eq-criteria,
.string-eq-criteria {
margin-top: 1rem;
.row {
@@ -970,19 +1019,13 @@
font-size: 1.2rem;
}
}
- .fb-toggle-button {
- width: 85px;
- margin-top: 0;
- &.red {
- background: $dark_gray !important;
- }
- }
- .clear-criteria {
- margin-top: 2rem;
- }
.basic,
.advanced {
margin-left: 1rem;
+ .filter-search {
+ height: 3rem;
+ margin-bottom: 1rem;
+ }
.day-criteria {
.row {
margin-left: 0;
@@ -994,6 +1037,12 @@
}
}
.advanced {
+ .bp3-popover-wrapper {
+ display: inline;
+ float: none;
+ margin-left: 1rem;
+ font-size: 1.4rem;
+ }
.row {
margin-left: 0;
}
@@ -1011,29 +1060,28 @@
}
}
}
- .criteria-point-count-breakdown {
- margin-bottom: 1rem;
- .manual-group-member-count,
- .criteria-group-member-count {
- margin-left: 2rem;
- div {
- display: inline;
- padding: 0.25rem;
- font-size: 1.2rem;
- border: 1px solid $panel_light_blue;
- }
- p {
- display: inline;
- margin-left: 1rem;
- }
- }
- .criteria-group-member-count {
- div {
- border: 1px solid gray;
- border-radius: 5px;
- }
- }
+ }
+}
+
+.group-member-count-breakdown {
+ margin-bottom: 1rem;
+ .manual-group-member-count,
+ .criteria-group-member-count {
+ div {
+ display: inline;
+ padding: 0.25rem;
+ font-size: 1.2rem;
}
+ p {
+ display: inline;
+ margin-left: 1rem;
+ }
+ }
+}
+
+.criteria-options-menu {
+ label {
+ margin-right: 1rem;
}
}
@@ -1042,6 +1090,7 @@
display: inline-block;
.row {
margin-left: 0;
+ margin-right: -2.5rem;
div[class*=col-] {
padding: 0;
text-align: center;
@@ -1057,16 +1106,28 @@
margin-top: 0.5rem;
}
}
+ button {
+ margin-top: 2rem !important;
+ }
.edit-in-map {
float: right;
button {
margin: 1rem !important;
width: 5rem !important;
+ margin-right: 0 !important;
}
label {
margin-top: 1.1rem !important;
}
}
+ .location-selection-warning {
+ i,
+ p {
+ display: inline;
+ margin-right: 1rem;
+ color: $darkest_red;
+ }
+ }
}
.weeds-inventory-panel,
diff --git a/frontend/css/global.scss b/frontend/css/global.scss
index de81ef2a6..9e1e23ae6 100644
--- a/frontend/css/global.scss
+++ b/frontend/css/global.scss
@@ -1424,6 +1424,11 @@ ul {
button {
float: none !important;
}
+ .bp3-popover-wrapper {
+ display: inline;
+ margin-left: 0.5rem;
+ font-size: 1.3rem;
+ }
}
.problem-alert {
diff --git a/frontend/css/inputs.scss b/frontend/css/inputs.scss
index b7ee32649..e617ebca0 100644
--- a/frontend/css/inputs.scss
+++ b/frontend/css/inputs.scss
@@ -127,6 +127,16 @@ select {
background: $white;
margin-top: 0;
cursor: pointer;
+ &:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: $white;
+ opacity: 0.5;
+ }
&:checked:after {
content: "";
position: absolute;
diff --git a/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx b/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx
index 48f859534..37f8d39f6 100644
--- a/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx
+++ b/frontend/devices/components/fbos_settings/firmware_hardware_status.tsx
@@ -8,6 +8,8 @@ import { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces";
import { Alert } from "farmbot";
import { isFwHardwareValue, boardType } from "../firmware_hardware_support";
+import { Help } from "../../../ui";
+import { ToolTips } from "../../../constants";
export interface FirmwareHardwareStatusIconProps {
firmwareHardware: string | undefined;
@@ -59,10 +61,13 @@ export const FirmwareHardwareStatusDetails =
(props: FirmwareHardwareStatusDetailsProps) => {
return
{t("Web App")}
+
{lookup(props.apiFirmwareValue) || t("unknown")}
{t("FarmBot OS")}
+
{lookup(props.botFirmwareValue) || t("unknown")}
{t("Arduino/Farmduino")}
+
{lookup(props.mcuFirmwareValue) || t("unknown")}
", () => {
raw_encoders: { x: undefined, y: undefined, z: undefined },
},
botMcuParams: bot.hardware.mcu_params,
- stepsPerMmXY: { x: undefined, y: undefined },
+ botSize: {
+ x: { value: 3000, isDefault: true },
+ y: { value: 1500, isDefault: true },
+ },
peripherals: [],
eStopStatus: false,
latestImages: [],
diff --git a/frontend/farm_designer/__tests__/state_to_props_test.tsx b/frontend/farm_designer/__tests__/state_to_props_test.tsx
index f155e4cd6..aea704001 100644
--- a/frontend/farm_designer/__tests__/state_to_props_test.tsx
+++ b/frontend/farm_designer/__tests__/state_to_props_test.tsx
@@ -1,4 +1,4 @@
-import { mapStateToProps, getPlants } from "../state_to_props";
+import { mapStateToProps, getPlants, botSize } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import {
buildResourceIndex, fakeDevice,
@@ -11,6 +11,7 @@ import {
fakeWebAppConfig,
fakeFarmwareEnv,
fakeSensorReading,
+ fakeFirmwareConfig,
} from "../../__test_support__/fake_state/resources";
import { WebAppConfig } from "farmbot/dist/resources/configs/web_app";
import { generateUuid } from "../../resources/util";
@@ -40,13 +41,6 @@ describe("mapStateToProps()", () => {
checkValue(2, true);
});
- it("stepsPerMm is defined", () => {
- const state = fakeState();
- state.bot.hardware.mcu_params.movement_step_per_mm_x = 3;
- state.bot.hardware.mcu_params.movement_step_per_mm_y = 4;
- expect(mapStateToProps(state).stepsPerMmXY).toEqual({ x: 3, y: 4 });
- });
-
it("returns selected plant", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
@@ -144,3 +138,45 @@ describe("getPlants()", () => {
expect.objectContaining({ rotation: "15" }));
});
});
+
+describe("botSize()", () => {
+ it("returns default bot size", () => {
+ const state = fakeState();
+ expect(botSize(state)).toEqual({
+ x: { value: 2900, isDefault: true },
+ y: { value: 1400, isDefault: true },
+ });
+ });
+
+ it("returns map setting bot size", () => {
+ const state = fakeState();
+ const webAppConfig = fakeWebAppConfig();
+ webAppConfig.body.map_size_x = 1000;
+ webAppConfig.body.map_size_y = 1000;
+ state.resources = buildResourceIndex([fakeDevice(), webAppConfig]);
+ expect(botSize(state)).toEqual({
+ x: { value: 1000, isDefault: true },
+ y: { value: 1000, isDefault: true },
+ });
+ });
+
+ it("returns axis length setting bot size", () => {
+ const state = fakeState();
+ const firmwareConfig = fakeFirmwareConfig();
+ firmwareConfig.body.movement_step_per_mm_x = 2;
+ firmwareConfig.body.movement_step_per_mm_y = 4;
+ firmwareConfig.body.movement_stop_at_max_x = 1;
+ firmwareConfig.body.movement_stop_at_max_y = 1;
+ firmwareConfig.body.movement_axis_nr_steps_x = 100;
+ firmwareConfig.body.movement_axis_nr_steps_y = 100;
+ const webAppConfig = fakeWebAppConfig();
+ webAppConfig.body.map_size_x = 1000;
+ webAppConfig.body.map_size_y = 1000;
+ state.resources = buildResourceIndex([
+ fakeDevice(), firmwareConfig, webAppConfig]);
+ expect(mapStateToProps(state).botSize).toEqual({
+ x: { value: 50, isDefault: false },
+ y: { value: 25, isDefault: false },
+ });
+ });
+});
diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx
index 0685d8218..67df9865b 100755
--- a/frontend/farm_designer/index.tsx
+++ b/frontend/farm_designer/index.tsx
@@ -11,8 +11,7 @@ import { NumericSetting, BooleanSetting } from "../session_keys";
import { isUndefined, last, isFinite } from "lodash";
import { AxisNumberProperty, BotSize } from "./map/interfaces";
import {
- getBotSize, round, getPanelStatus, MapPanelStatus, mapPanelClassName,
- getMapPadding,
+ round, getPanelStatus, MapPanelStatus, mapPanelClassName, getMapPadding,
} from "./map/util";
import {
calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex,
@@ -126,11 +125,6 @@ export class RawFarmDesigner extends React.Component
> {
zoom_level
} = this.state;
- const botSize = getBotSize(
- this.props.botMcuParams,
- this.props.stepsPerMmXY,
- getDefaultAxisLength(this.props.getConfigValue));
-
const stopAtHome = {
x: !!this.props.botMcuParams.movement_stop_at_home_x,
y: !!this.props.botMcuParams.movement_stop_at_home_y
@@ -167,6 +161,7 @@ export class RawFarmDesigner extends React.Component> {
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
getConfigValue={this.props.getConfigValue}
+ shouldDisplay={this.props.shouldDisplay}
imageAgeInfo={imageAgeInfo} />
@@ -200,12 +195,12 @@ export class RawFarmDesigner extends React.Component> {
allPoints={this.props.allPoints}
toolSlots={this.props.toolSlots}
botLocationData={this.props.botLocationData}
- botSize={botSize}
+ botSize={this.props.botSize}
stopAtHome={stopAtHome}
hoveredPlant={this.props.hoveredPlant}
zoomLvl={zoom_level}
botOriginQuadrant={this.getBotOriginQuadrant()}
- gridSize={getGridSize(this.props.getConfigValue, botSize)}
+ gridSize={getGridSize(this.props.getConfigValue, this.props.botSize)}
gridOffset={gridOffset}
peripherals={this.props.peripherals}
eStopStatus={this.props.eStopStatus}
diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts
index beb9f55b1..6ebf62f9f 100644
--- a/frontend/farm_designer/interfaces.ts
+++ b/frontend/farm_designer/interfaces.ts
@@ -16,7 +16,7 @@ import {
} from "farmbot";
import { SlotWithTool, ResourceIndex, UUID } from "../resources/interfaces";
import {
- BotPosition, StepsPerMmXY, BotLocationData, ShouldDisplay,
+ BotPosition, BotLocationData, ShouldDisplay,
} from "../devices/interfaces";
import { isNumber } from "lodash";
import { McuParams, TaggedCrop } from "farmbot";
@@ -73,7 +73,7 @@ export interface Props {
crops: TaggedCrop[];
botLocationData: BotLocationData;
botMcuParams: McuParams;
- stepsPerMmXY: StepsPerMmXY;
+ botSize: BotSize;
peripherals: { label: string, value: boolean }[];
eStopStatus: boolean;
latestImages: TaggedImage[];
diff --git a/frontend/farm_designer/map/__tests__/actions_test.ts b/frontend/farm_designer/map/__tests__/actions_test.ts
index 744c533f9..408989a4b 100644
--- a/frontend/farm_designer/map/__tests__/actions_test.ts
+++ b/frontend/farm_designer/map/__tests__/actions_test.ts
@@ -4,10 +4,9 @@ jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
}));
-jest.mock("../../../api/crud", () => ({
- edit: jest.fn(),
- overwrite: jest.fn(),
-}));
+jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
+
+jest.mock("../../point_groups/actions", () => ({ overwriteGroup: jest.fn() }));
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
const mockGroup = fakePointGroup();
@@ -22,7 +21,7 @@ import {
} from "../actions";
import { MovePlantProps } from "../../interfaces";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
-import { edit, overwrite } from "../../../api/crud";
+import { edit } from "../../../api/crud";
import { Actions } from "../../../constants";
import { DEFAULT_ICON, svgToUrl } from "../../../open_farm/icons";
import { history } from "../../../history";
@@ -31,6 +30,8 @@ import { GetState } from "../../../redux/interfaces";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
+import { overwriteGroup } from "../../point_groups/actions";
+import { mockDispatch } from "../../../__test_support__/fake_dispatch";
describe("movePlant", () => {
it.each<[string, Record<"x" | "y", number>, Record<"x" | "y", number>]>([
@@ -128,12 +129,13 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.body.id = 23;
state.resources = buildResourceIndex([plant]);
- const dispatch = jest.fn();
+ const dispatch = mockDispatch();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
- expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
- name: "Fake", point_ids: [1, 23]
- }));
+ expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
+ expect.objectContaining({
+ name: "Fake", point_ids: [1, 23]
+ }));
expect(dispatch).toHaveBeenCalledTimes(1);
});
@@ -142,10 +144,10 @@ describe("clickMapPlant", () => {
mockGroup.body.point_ids = [1];
const state = fakeState();
state.resources = buildResourceIndex([]);
- const dispatch = jest.fn();
+ const dispatch = mockDispatch();
const getState: GetState = jest.fn(() => state);
clickMapPlant("missing plant uuid", "fakeIcon")(dispatch, getState);
- expect(overwrite).not.toHaveBeenCalled();
+ expect(overwriteGroup).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledTimes(1);
});
@@ -156,12 +158,13 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.body.id = 2;
state.resources = buildResourceIndex([plant]);
- const dispatch = jest.fn();
+ const dispatch = mockDispatch();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
- expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
- name: "Fake", point_ids: [1]
- }));
+ expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
+ expect.objectContaining({
+ name: "Fake", point_ids: [1]
+ }));
expect(dispatch).toHaveBeenCalledTimes(1);
});
diff --git a/frontend/farm_designer/map/__tests__/garden_map_test.tsx b/frontend/farm_designer/map/__tests__/garden_map_test.tsx
index 91bfadd25..116cd7537 100644
--- a/frontend/farm_designer/map/__tests__/garden_map_test.tsx
+++ b/frontend/farm_designer/map/__tests__/garden_map_test.tsx
@@ -456,6 +456,16 @@ describe(" ", () => {
expect(allowed).toBeTruthy();
});
+ it("allows interactions: group edit", () => {
+ mockMode = Mode.editGroup;
+ mockInteractionAllow = true;
+ const p = fakeProps();
+ p.designer.selectionPointType = undefined;
+ const wrapper = mount( );
+ const allowed = wrapper.instance().interactions("Plant");
+ expect(allowed).toBeTruthy();
+ });
+
it("disallows interactions: default", () => {
mockMode = Mode.none;
mockInteractionAllow = false;
diff --git a/frontend/farm_designer/map/actions.ts b/frontend/farm_designer/map/actions.ts
index 3be43c4f3..2a1bf910f 100644
--- a/frontend/farm_designer/map/actions.ts
+++ b/frontend/farm_designer/map/actions.ts
@@ -1,6 +1,6 @@
import { MovePlantProps, DraggableEvent } from "../interfaces";
import { defensiveClone } from "../../util";
-import { edit, overwrite } from "../../api/crud";
+import { edit } from "../../api/crud";
import { history } from "../../history";
import { Actions } from "../../constants";
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
@@ -12,6 +12,7 @@ import { TaggedPoint } from "farmbot";
import { getMode } from "../map/util";
import { ResourceIndex, UUID } from "../../resources/interfaces";
import { selectAllPointGroups } from "../../resources/selectors";
+import { overwriteGroup } from "../point_groups/actions";
export function movePlant(payload: MovePlantProps) {
const tr = payload.plant;
@@ -33,23 +34,24 @@ export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
});
const addOrRemoveFromGroup =
- (clickedPlantUuid: UUID, resources: ResourceIndex) => {
- const group = findGroupFromUrl(selectAllPointGroups(resources));
- const point =
- resources.references[clickedPlantUuid] as TaggedPoint | undefined;
- if (group && point?.body.id) {
- type Body = (typeof group)["body"];
- const nextGroup: Body = ({
- ...group.body,
- point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
- });
- if (!group.body.point_ids.includes(point.body.id)) {
- nextGroup.point_ids.push(point.body.id);
+ (clickedPlantUuid: UUID, resources: ResourceIndex) =>
+ (dispatch: Function) => {
+ const group = findGroupFromUrl(selectAllPointGroups(resources));
+ const point =
+ resources.references[clickedPlantUuid] as TaggedPoint | undefined;
+ if (group && point?.body.id) {
+ type Body = (typeof group)["body"];
+ const nextGroup: Body = ({
+ ...group.body,
+ point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
+ });
+ if (!group.body.point_ids.includes(point.body.id)) {
+ nextGroup.point_ids.push(point.body.id);
+ }
+ nextGroup.point_ids = uniq(nextGroup.point_ids);
+ dispatch(overwriteGroup(group, nextGroup));
}
- nextGroup.point_ids = uniq(nextGroup.point_ids);
- return overwrite(group, nextGroup);
- }
- };
+ };
const addOrRemoveFromSelection =
(clickedPointUuid: UUID, selectedPoints: UUID[] | undefined) => {
diff --git a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx
index 29382f009..e8ae2cfbc 100644
--- a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx
+++ b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.tsx
@@ -8,9 +8,8 @@ jest.mock("../../../point_groups/criteria", () => ({
editGtLtCriteria: jest.fn(),
}));
-jest.mock("../../../../api/crud", () => ({
- overwrite: jest.fn(),
- save: jest.fn(),
+jest.mock("../../../point_groups/actions", () => ({
+ overwriteGroup: jest.fn(),
}));
import {
@@ -25,8 +24,8 @@ import {
import { Actions } from "../../../../constants";
import { history } from "../../../../history";
import { editGtLtCriteria } from "../../../point_groups/criteria";
-import { overwrite, save } from "../../../../api/crud";
import { cloneDeep } from "lodash";
+import { overwriteGroup } from "../../../point_groups/actions";
describe("getSelected", () => {
it("returns some", () => {
@@ -189,8 +188,7 @@ describe("maybeUpdateGroup()", () => {
expectedBody && (expectedBody.point_ids = [
plant1.body.id || 0, plant2.body.id || 0,
]);
- expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
- expect(save).not.toHaveBeenCalled();
+ expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedBody);
});
it("updates criteria", () => {
@@ -214,7 +212,6 @@ describe("maybeUpdateGroup()", () => {
maybeUpdateGroup(p);
expect(p.dispatch).not.toHaveBeenCalled();
expect(editGtLtCriteria).not.toHaveBeenCalled();
- expect(overwrite).not.toHaveBeenCalled();
- expect(save).not.toHaveBeenCalled();
+ expect(overwriteGroup).not.toHaveBeenCalled();
});
});
diff --git a/frontend/farm_designer/map/background/selection_box_actions.tsx b/frontend/farm_designer/map/background/selection_box_actions.tsx
index a9f819a10..e6a76792b 100644
--- a/frontend/farm_designer/map/background/selection_box_actions.tsx
+++ b/frontend/farm_designer/map/background/selection_box_actions.tsx
@@ -8,11 +8,11 @@ import { getMode } from "../util";
import { editGtLtCriteria } from "../../point_groups/criteria";
import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot";
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
-import { overwrite } from "../../../api/crud";
import { unpackUUID } from "../../../util";
import { UUID } from "../../../resources/interfaces";
import { getFilteredPoints } from "../../plants/select_plants";
import { GetWebAppConfigValue } from "../../../config_storage/actions";
+import { overwriteGroup } from "../../point_groups/actions";
/** Return all plants within the selection box. */
export const getSelected = (
@@ -109,19 +109,20 @@ export interface MaybeUpdateGroupProps {
export const maybeUpdateGroup =
(props: MaybeUpdateGroupProps) => {
- if (props.selectionBox && props.group) {
+ const { group } = props;
+ if (props.selectionBox && group) {
if (props.editGroupAreaInMap
&& props.shouldDisplay(Feature.criteria_groups)) {
- props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
+ props.dispatch(editGtLtCriteria(group, props.selectionBox));
} else {
- const nextGroupBody = cloneDeep(props.group.body);
+ const nextGroupBody = cloneDeep(group.body);
props.boxSelected?.map(uuid => {
const { kind, remoteId } = unpackUUID(uuid);
remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId);
});
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
- if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
- props.dispatch(overwrite(props.group, nextGroupBody));
+ if (!isEqual(group.body.point_ids, nextGroupBody.point_ids)) {
+ props.dispatch(overwriteGroup(group, nextGroupBody));
props.dispatch(selectPoint(undefined));
}
}
diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx
index ed89c0ce7..c184eeaac 100644
--- a/frontend/farm_designer/map/garden_map.tsx
+++ b/frontend/farm_designer/map/garden_map.tsx
@@ -40,6 +40,7 @@ import { findGroupFromUrl } from "../point_groups/group_detail";
import { pointsSelectedByGroup } from "../point_groups/criteria";
import { DrawnWeed } from "./drawn_point/drawn_weed";
import { UUID } from "../../resources/interfaces";
+import { throttle } from "lodash";
export class GardenMap extends
React.Component> {
@@ -89,7 +90,7 @@ export class GardenMap extends
}
/** Save the current plant (if needed) and reset drag state. */
- endDrag = () => {
+ endDrag = throttle(() => {
maybeSavePlantLocation({
plant: this.getPlant(),
isDragging: this.state.isDragging,
@@ -109,7 +110,7 @@ export class GardenMap extends
activeDragSpread: undefined,
selectionBox: undefined
});
- }
+ }, 400);
getGardenCoordinates =
(e: React.DragEvent | React.MouseEvent):
@@ -215,6 +216,7 @@ export class GardenMap extends
interactions = (pointerType: PointType): boolean => {
if (allowInteraction()) {
switch (getMode()) {
+ case Mode.editGroup:
case Mode.boxSelect:
return (this.props.designer.selectionPointType || ["Plant"])
.includes(pointerType);
diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts
index 46655ce5f..ea5339078 100644
--- a/frontend/farm_designer/map/interfaces.ts
+++ b/frontend/farm_designer/map/interfaces.ts
@@ -5,7 +5,9 @@ import {
TaggedWeedPointer,
} from "farmbot";
import { State, BotOriginQuadrant } from "../interfaces";
-import { BotPosition, BotLocationData } from "../../devices/interfaces";
+import {
+ BotPosition, BotLocationData, ShouldDisplay,
+} from "../../devices/interfaces";
import { GetWebAppConfigValue } from "../../config_storage/actions";
import { TimeSettings } from "../../interfaces";
import { UUID } from "../../resources/interfaces";
@@ -48,6 +50,7 @@ export interface GardenMapLegendProps {
imageAgeInfo: { newestDate: string, toOldest: number };
gardenId?: number;
className?: string;
+ shouldDisplay: ShouldDisplay;
}
export type MapTransformProps = {
diff --git a/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx b/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx
index e58d7ed08..6eef8fec5 100644
--- a/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx
+++ b/frontend/farm_designer/map/layers/zones/__tests__/zones_layer_test.tsx
@@ -7,6 +7,7 @@ import {
import {
fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props";
+import { ReactWrapper } from "enzyme";
describe(" ", () => {
const fakeProps = (): ZonesLayerProps => ({
@@ -26,6 +27,27 @@ describe(" ", () => {
expect(wrapper.find(".zones-layer").length).toEqual(1);
});
+ const expectSolid = (zone2D: ReactWrapper) => {
+ const zoneProps = zone2D.find("rect").props();
+ expect(zoneProps.fill).toEqual(undefined);
+ expect(zoneProps.stroke).toEqual(undefined);
+ expect(zoneProps.strokeDasharray).toEqual(undefined);
+ expect(zoneProps.strokeWidth).toEqual(undefined);
+ };
+
+ const expectOutline = (zone2D: ReactWrapper) => {
+ const zoneProps = zone2D.find("rect").props();
+ expect(zoneProps.fill).toEqual("none");
+ expect(zoneProps.stroke).toEqual("white");
+ expect(zoneProps.strokeDasharray).toEqual(15);
+ expect(zoneProps.strokeWidth).toEqual(4);
+ };
+
+ const expectNone = (zone2D: ReactWrapper) => {
+ expect(zone2D.html()).toEqual(
+ " ");
+ };
+
it("renders current group's zones: 2D", () => {
const p = fakeProps();
p.visible = false;
@@ -38,6 +60,7 @@ describe(" ", () => {
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
+ expectSolid(wrapper.find("#zones-2D-1"));
expect(wrapper.find("#zones-2D-2").length).toEqual(0);
});
@@ -50,19 +73,22 @@ describe(" ", () => {
const wrapper = svgMount( );
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
- expect(wrapper.find("#zones-2D-1").length).toEqual(0);
+ expect(wrapper.find("#zones-2D-1").length).toEqual(1);
+ expectNone(wrapper.find("#zones-2D-1"));
});
it("renders current group's zones: 0D", () => {
const p = fakeProps();
p.visible = false;
p.groups[0].body.id = 1;
+ p.groups[0].body.criteria.number_gt = { x: 10 };
p.groups[0].body.criteria.number_eq = { x: [100], y: [100] };
p.currentGroup = p.groups[0].uuid;
const wrapper = svgMount( );
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
- expect(wrapper.find("#zones-2D-1").length).toEqual(0);
+ expect(wrapper.find("#zones-2D-1").length).toEqual(1);
+ expectOutline(wrapper.find("#zones-2D-1"));
});
it("renders current group's zones: none", () => {
@@ -72,7 +98,12 @@ describe(" ", () => {
p.currentGroup = p.groups[0].uuid;
const wrapper = svgMount( );
expect(wrapper.html()).toEqual(
- " ");
+ `
+
+
+
+
+ `.replace(/[ ]{2,}/g, "").replace(/[^\S ]/g, ""));
});
it("doesn't render current group's zones", () => {
diff --git a/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx b/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx
index 0ca53f5c8..fcc548ac1 100644
--- a/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx
+++ b/frontend/farm_designer/map/layers/zones/__tests__/zones_test.tsx
@@ -1,7 +1,9 @@
+jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
+
import * as React from "react";
import { svgMount } from "../../../../../__test_support__/svg_mount";
import {
- Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType,
+ Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType, spaceSelected,
} from "../zones";
import {
fakePointGroup,
@@ -10,6 +12,7 @@ import {
fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props";
import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces";
+import { history } from "../../../../../history";
const fakeProps = (): ZonesProps => ({
group: fakePointGroup(),
@@ -57,6 +60,15 @@ describe(" ", () => {
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
expect(wrapper.find("circle").length).toEqual(2);
});
+
+ it("opens group", () => {
+ const p = fakeProps();
+ p.group.body.id = 1;
+ p.group.body.criteria.number_eq = { x: [100], y: [200, 300] };
+ const wrapper = svgMount( );
+ wrapper.find("#zones-0D-1").simulate("click");
+ expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
+ });
});
describe(" ", () => {
@@ -104,6 +116,15 @@ describe(" ", () => {
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
expect(wrapper.find("line").length).toEqual(2);
});
+
+ it("opens group", () => {
+ const p = fakeProps();
+ p.group.body.id = 1;
+ p.group.body.criteria.number_eq = { x: [], y: [200, 300] };
+ const wrapper = svgMount( );
+ wrapper.find("#zones-1D-1").simulate("click");
+ expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
+ });
});
describe(" ", () => {
@@ -137,6 +158,16 @@ describe(" ", () => {
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
expect(wrapper.find("rect").length).toEqual(1);
});
+
+ it("opens group", () => {
+ const p = fakeProps();
+ p.group.body.id = 1;
+ p.group.body.criteria.number_gt = { x: 100, y: 200 };
+ p.group.body.criteria.number_lt = { x: 300, y: 400 };
+ const wrapper = svgMount( );
+ wrapper.find("#zones-2D-1").simulate("click");
+ expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
+ });
});
describe("getZoneType()", () => {
@@ -163,3 +194,58 @@ describe("getZoneType()", () => {
expect(getZoneType(group)).toEqual(ZoneType.points);
});
});
+
+describe("spaceSelected()", () => {
+ const botSize = {
+ x: { value: 3000, isDefault: true },
+ y: { value: 1500, isDefault: true }
+ };
+
+ it("is selected: area", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_eq = {};
+ group.body.criteria.number_lt = {};
+ group.body.criteria.number_gt = {};
+ expect(spaceSelected(group, botSize)).toBeTruthy();
+ });
+
+ it("isn't selected: area", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_eq = {};
+ group.body.criteria.number_lt = { x: 100 };
+ group.body.criteria.number_gt = { x: 200 };
+ expect(spaceSelected(group, botSize)).toBeFalsy();
+ });
+
+ it("is selected: lines", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_eq = { x: [0] };
+ group.body.criteria.number_lt = {};
+ group.body.criteria.number_gt = {};
+ expect(spaceSelected(group, botSize)).toBeTruthy();
+ });
+
+ it("isn't selected: lines", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_eq = { x: [0] };
+ group.body.criteria.number_lt = {};
+ group.body.criteria.number_gt = { x: 100 };
+ expect(spaceSelected(group, botSize)).toBeFalsy();
+ });
+
+ it("is selected: points", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_eq = { x: [0], y: [0] };
+ group.body.criteria.number_lt = {};
+ group.body.criteria.number_gt = {};
+ expect(spaceSelected(group, botSize)).toBeTruthy();
+ });
+
+ it("isn't selected: points", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_eq = { x: [0], y: [0] };
+ group.body.criteria.number_lt = { x: 0 };
+ group.body.criteria.number_gt = {};
+ expect(spaceSelected(group, botSize)).toBeFalsy();
+ });
+});
diff --git a/frontend/farm_designer/map/layers/zones/zones.tsx b/frontend/farm_designer/map/layers/zones/zones.tsx
index 5de295154..ab1dca346 100644
--- a/frontend/farm_designer/map/layers/zones/zones.tsx
+++ b/frontend/farm_designer/map/layers/zones/zones.tsx
@@ -52,10 +52,10 @@ const getBoundary = (props: GetBoundaryProps): Boundary => {
const { criteria } = props.group.body;
const gt = criteria.number_gt;
const lt = criteria.number_lt;
- const x1 = gt.x || 0;
- const x2 = lt.x || props.botSize.x.value;
- const y1 = gt.y || 0;
- const y2 = lt.y || props.botSize.y.value;
+ const x1 = gt.x ?? (0 - 0.01);
+ const x2 = lt.x ?? (props.botSize.x.value + 0.01);
+ const y1 = gt.y ?? (0 - 0.01);
+ const y2 = lt.y ?? (props.botSize.y.value + 0.01);
const selectsAll = !(gt.x || lt.x || gt.y || lt.y);
return { x1, x2, y1, y2, selectsAll };
};
@@ -114,11 +114,11 @@ const zone1D = (props: ZonesProps) => {
const boundary = getBoundary(props);
return getLines(boundary, props.group).map(line => {
const min = transformXY(
- line.x || boundary.x1,
- line.y || boundary.y1, props.mapTransformProps);
+ line.x ?? boundary.x1,
+ line.y ?? boundary.y1, props.mapTransformProps);
const max = transformXY(
- line.x || boundary.x2,
- line.y || boundary.y2, props.mapTransformProps);
+ line.x ?? boundary.x2,
+ line.y ?? boundary.y2, props.mapTransformProps);
return {
x1: min.qx,
y1: min.qy,
@@ -144,25 +144,59 @@ export const Zones1D = (props: ZonesProps) => {
const zone2D = (boundary: Boundary, mapTransformProps: MapTransformProps) => {
const position = transformXY(boundary.x1, boundary.y1, mapTransformProps);
const { xySwap, quadrant } = mapTransformProps;
- const xLength = boundary.x2 - boundary.x1;
- const yLength = boundary.y2 - boundary.y1;
+ const xLength = Math.max(0, boundary.x2 - boundary.x1);
+ const yLength = Math.max(0, boundary.y2 - boundary.y1);
+ const width = xySwap ? yLength : xLength;
+ const height = xySwap ? xLength : yLength;
return {
- x: [1, 4].includes(quadrant) ? position.qx - xLength : position.qx,
- y: [3, 4].includes(quadrant) ? position.qy - yLength : position.qy,
- width: xySwap ? yLength : xLength,
- height: xySwap ? xLength : yLength,
+ x: [1, 4].includes(quadrant) ? position.qx - width : position.qx,
+ y: [3, 4].includes(quadrant) ? position.qy - height : position.qy,
+ width,
+ height,
selectsAll: boundary.selectsAll,
};
};
/** Area selected by x and y number gt/lt values. */
export const Zones2D = (props: ZonesProps) => {
- const zone = zone2D(getBoundary(props), props.mapTransformProps);
const current = props.group.uuid == props.currentGroup;
+ const zone = zone2D(getBoundary(props), props.mapTransformProps);
+ const not2D = getZoneType(props.group) !== ZoneType.area;
+ const rectProps: React.SVGProps = not2D ? {
+ stroke: current ? "white" : "black",
+ strokeWidth: 4,
+ strokeDasharray: 15,
+ fill: "none",
+ } : {};
const { id } = props.group.body;
return
{!zone.selectsAll &&
- }
+ }
;
};
+
+/** Determine if location criteria selects some space. */
+export const spaceSelected =
+ (group: TaggedPointGroup, botSize: BotSize) => {
+ const boundary = getBoundary({ group, botSize });
+ const area = {
+ width: Math.max(0, boundary.x2 - boundary.x1),
+ height: Math.max(0, boundary.y2 - boundary.y1),
+ };
+ const lines = getLines(boundary, group);
+ const points = getPoints(boundary, group);
+ switch (getZoneType(group)) {
+ case ZoneType.none:
+ case ZoneType.area:
+ return (area.width > 0) && (area.height > 0);
+ case ZoneType.lines:
+ return lines.length > 0;
+ case ZoneType.points:
+ return points.length > 0;
+ }
+ };
diff --git a/frontend/farm_designer/map/layers/zones/zones_layer.tsx b/frontend/farm_designer/map/layers/zones/zones_layer.tsx
index 020878126..30d41cb8a 100644
--- a/frontend/farm_designer/map/layers/zones/zones_layer.tsx
+++ b/frontend/farm_designer/map/layers/zones/zones_layer.tsx
@@ -23,7 +23,6 @@ export function ZonesLayer(props: ZonesLayerProps) {
? { cursor: "pointer" }
: { pointerEvents: "none" }} onMouseDown={props.startDrag}>
{groups.map(group => visible(group) &&
- getZoneType(group) === ZoneType.area &&
)}
{groups.map(group => visible(group) &&
getZoneType(group) === ZoneType.lines &&
diff --git a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
index a35de71a2..eeb7f0121 100644
--- a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
+++ b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
@@ -46,6 +46,7 @@ describe(" ", () => {
timeSettings: fakeTimeSettings(),
getConfigValue: jest.fn(),
imageAgeInfo: { newestDate: "", toOldest: 1 },
+ shouldDisplay: () => true,
});
it("renders", () => {
diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx
index d21f4d986..5b5305dc2 100644
--- a/frontend/farm_designer/map/legend/garden_map_legend.tsx
+++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx
@@ -10,6 +10,7 @@ import { GetWebAppConfigValue } from "../../../config_storage/actions";
import { BooleanSetting } from "../../../session_keys";
import { DevSettings } from "../../../account/dev/dev_support";
import { t } from "../../../i18next_wrapper";
+import { Feature } from "../../../devices/interfaces";
export const ZoomControls = ({ zoom, getConfigValue }: {
zoom: (value: number) => () => void,
@@ -81,7 +82,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
dispatch={props.dispatch}
getConfigValue={getConfigValue}
imageAgeInfo={props.imageAgeInfo} />} />
- {DevSettings.futureFeaturesEnabled() &&
+ {props.shouldDisplay(Feature.criteria_groups) &&
{
@@ -31,6 +31,15 @@ describe("mapStateToProps()", () => {
expect(result.findPlant("10")).toEqual(
expect.objectContaining({ uuid }));
});
+
+ it("returns getConfigValue()", () => {
+ const state = fakeState();
+ const webAppConfig = fakeWebAppConfig();
+ webAppConfig.body.show_plants = false;
+ state.resources = buildResourceIndex([webAppConfig]);
+ const result = mapStateToProps(state);
+ expect(result.getConfigValue("show_plants")).toEqual(false);
+ });
});
describe("plantAge()", () => {
@@ -48,3 +57,20 @@ describe("plantAge()", () => {
expect(plantAge(plant)).toBeGreaterThan(100);
});
});
+
+describe("formatPlantInfo()", () => {
+ it("returns info for plant", () => {
+ const plant = fakePlant();
+ plant.body.plant_stage = "planted";
+ const result = formatPlantInfo(plant);
+ expect(result.meta).toEqual({});
+ expect(result.plantStatus).toEqual("planted");
+ });
+
+ it("returns info for plant template", () => {
+ const plant = fakePlantTemplate();
+ const result = formatPlantInfo(plant);
+ expect(result.meta).toBeUndefined();
+ expect(result.plantStatus).toEqual("planned");
+ });
+});
diff --git a/frontend/farm_designer/plants/__tests__/plant_panel_test.tsx b/frontend/farm_designer/plants/__tests__/plant_panel_test.tsx
index 3c9b172d1..38e3b8bd1 100644
--- a/frontend/farm_designer/plants/__tests__/plant_panel_test.tsx
+++ b/frontend/farm_designer/plants/__tests__/plant_panel_test.tsx
@@ -48,9 +48,12 @@ describe(" ", () => {
it("renders: editing", () => {
const p = fakeProps();
+ p.info.meta = { meta_key: "meta value", gridId: "1", key: undefined };
const wrapper = mount( );
const txt = wrapper.text().toLowerCase();
expect(txt).toContain("1 days old");
+ expect(txt).toContain("meta value");
+ expect(txt).not.toContain("gridId");
const x = wrapper.find("input").at(1).props().value;
const y = wrapper.find("input").at(2).props().value;
expect(x).toEqual(12);
@@ -59,6 +62,7 @@ describe(" ", () => {
it("calls destroy", () => {
const p = fakeProps();
+ p.info.meta = undefined;
const wrapper = mount( );
clickButton(wrapper, 2, "Delete");
expect(p.onDestroy).toHaveBeenCalledWith("Plant.0.0");
diff --git a/frontend/farm_designer/plants/__tests__/select_plants_test.tsx b/frontend/farm_designer/plants/__tests__/select_plants_test.tsx
index d61e0c039..c270da8cb 100644
--- a/frontend/farm_designer/plants/__tests__/select_plants_test.tsx
+++ b/frontend/farm_designer/plants/__tests__/select_plants_test.tsx
@@ -18,11 +18,12 @@ import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps,
- getFilteredPoints, GetFilteredPointsProps,
+ getFilteredPoints, GetFilteredPointsProps, validPointTypes,
} from "../select_plants";
import {
fakePlant, fakePoint, fakeWeed, fakeToolSlot, fakeTool,
fakePlantTemplate,
+ fakeWebAppConfig,
} from "../../../__test_support__/fake_state/resources";
import { Actions, Content } from "../../../constants";
import { clickButton } from "../../../__test_support__/helpers";
@@ -30,6 +31,10 @@ import { destroy } from "../../../api/crud";
import { createGroup } from "../../point_groups/actions";
import { fakeState } from "../../../__test_support__/fake_state";
import { error } from "../../../toast/toast";
+import { mockDispatch } from "../../../__test_support__/fake_dispatch";
+import {
+ buildResourceIndex,
+} from "../../../__test_support__/resource_index_builder";
describe(" ", () => {
beforeEach(function () {
@@ -85,6 +90,20 @@ describe(" ", () => {
expect(wrapper.text()).toContain(weed.body.name);
});
+ it("displays selected points and weeds", () => {
+ const p = fakeProps();
+ const point = fakePoint();
+ point.body.name = "fake point";
+ const weed = fakeWeed();
+ weed.body.name = "fake weed";
+ p.allPoints = [point, weed];
+ p.selected = [point.uuid, weed.uuid];
+ p.selectionPointType = ["GenericPointer", "Weed"];
+ const wrapper = mount( );
+ expect(wrapper.text()).toContain(point.body.name);
+ expect(wrapper.text()).toContain(weed.body.name);
+ });
+
it("displays selected slot", () => {
const p = fakeProps();
const tool = fakeTool();
@@ -100,11 +119,13 @@ describe(" ", () => {
expect(wrapper.text()).toContain(tool.body.name);
});
- it("clears point section type", () => {
+ it("clears point selection type", () => {
const p = fakeProps();
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
const wrapper = mount( );
wrapper.unmount();
- expect(p.dispatch).toHaveBeenCalledWith({
+ expect(dispatch).toHaveBeenCalledWith({
type: Actions.SET_SELECTION_POINT_TYPE,
payload: undefined,
});
@@ -148,13 +169,29 @@ describe(" ", () => {
it("changes selection type", () => {
const p = fakeProps();
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
const wrapper = mount( );
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
actionsWrapper.find("FBSelect").first().simulate("change",
{ label: "", value: "All" });
- expect(p.dispatch).toHaveBeenCalledWith({
+ expect(dispatch).toHaveBeenCalledWith({
type: Actions.SET_SELECTION_POINT_TYPE,
- payload: ["Plant", "GenericPointer", "ToolSlot", "Weed"],
+ payload: ["Plant", "GenericPointer", "Weed", "ToolSlot"],
+ });
+ });
+
+ it("changes selection type: Plant pointer_type", () => {
+ const p = fakeProps();
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
+ const wrapper = mount( );
+ const actionsWrapper = shallow(wrapper.instance().ActionButtons());
+ actionsWrapper.find("FBSelect").first().simulate("change",
+ { label: "", value: "Plant" });
+ expect(dispatch).toHaveBeenCalledWith({
+ type: Actions.SET_SELECTION_POINT_TYPE,
+ payload: ["Plant"],
});
});
@@ -213,8 +250,8 @@ describe(" ", () => {
const wrapper = mount( );
window.confirm = () => true;
await clickButton(wrapper, DELETE_BTN_INDEX, "Delete");
- expect(destroy).toHaveBeenCalledWith("plant.1", true);
- expect(destroy).toHaveBeenCalledWith("plant.2", true);
+ await expect(destroy).toHaveBeenCalledWith("plant.1", true);
+ await expect(destroy).toHaveBeenCalledWith("plant.2", true);
});
it("shows other buttons", () => {
@@ -248,6 +285,14 @@ describe("mapStateToProps", () => {
expect(result.plants.length).toBe(2);
expect(result.dispatch).toBe(state.dispatch);
});
+
+ it("returns quadrant", () => {
+ const state = fakeState();
+ const webAppConfig = fakeWebAppConfig();
+ webAppConfig.body.bot_origin_quadrant = 2;
+ state.resources = buildResourceIndex([webAppConfig]);
+ expect(mapStateToProps(state).quadrant).toEqual(2);
+ });
});
describe("getFilteredPoints()", () => {
@@ -289,3 +334,13 @@ describe("getFilteredPoints()", () => {
expect(getFilteredPoints(p)).toEqual([slot]);
});
});
+
+describe("validPointTypes()", () => {
+ it("returns valid pointer types", () => {
+ expect(validPointTypes(["Plant"])).toEqual(["Plant"]);
+ });
+
+ it("returns undefined", () => {
+ expect(validPointTypes(["nope"])).toEqual(undefined);
+ });
+});
diff --git a/frontend/farm_designer/plants/map_state_to_props.tsx b/frontend/farm_designer/plants/map_state_to_props.tsx
index 090474570..8a72184fa 100644
--- a/frontend/farm_designer/plants/map_state_to_props.tsx
+++ b/frontend/farm_designer/plants/map_state_to_props.tsx
@@ -5,7 +5,7 @@ import {
maybeFindPlantById, maybeFindPlantTemplateById, maybeGetTimeSettings,
} from "../../resources/selectors";
import { history } from "../../history";
-import { PlantStage } from "farmbot";
+import { PlantStage, TaggedPoint } from "farmbot";
import { TaggedPlant } from "../map/interfaces";
import { isNumber, get } from "lodash";
import { getWebAppConfigValue } from "../../config_storage/actions";
@@ -47,17 +47,18 @@ export interface FormattedPlantInfo {
plantedAt: moment.Moment;
slug: string;
plantStatus: PlantStage;
+ meta?: Record;
}
/** Get date planted or fallback to creation date. */
-const plantDate = (plant: TaggedPlant): moment.Moment => {
+const plantDate = (plant: TaggedPlant | TaggedPoint): moment.Moment => {
const plantedAt = get(plant, "body.planted_at");
const createdAt = get(plant, "body.created_at", moment());
return plantedAt ? moment(plantedAt) : moment(createdAt);
};
/** Compare planted or created date vs time now to determine age. */
-export const plantAge = (plant: TaggedPlant): number => {
+export const plantAge = (plant: TaggedPlant | TaggedPoint): number => {
const currentDate = moment();
const daysOld = currentDate.diff(plantDate(plant), "days") + 1;
return daysOld;
@@ -73,6 +74,7 @@ export function formatPlantInfo(plant: TaggedPlant): FormattedPlantInfo {
y: plant.body.y,
uuid: plant.uuid,
plantedAt: plantDate(plant),
- plantStatus: get(plant, "plant_stage", "planned"),
+ plantStatus: get(plant, "body.plant_stage", "planned"),
+ meta: plant.kind == "Point" ? plant.body.meta : undefined,
};
}
diff --git a/frontend/farm_designer/plants/plant_panel.tsx b/frontend/farm_designer/plants/plant_panel.tsx
index ab5b3d81d..0a0af236e 100644
--- a/frontend/farm_designer/plants/plant_panel.tsx
+++ b/frontend/farm_designer/plants/plant_panel.tsx
@@ -186,6 +186,14 @@ export function PlantPanel(props: PlantPanelProps) {
updatePlant={updatePlant} />
: t(startCase(plantStatus))}
+ {Object.entries(info.meta || []).map(([key, value]) => {
+ switch (key) {
+ case "gridId":
+ return
;
+ default:
+ return {value || ""} ;
+ }
+ })}
;
diff --git a/frontend/farm_designer/plants/select_plants.tsx b/frontend/farm_designer/plants/select_plants.tsx
index 5cebb3302..5ad15c75f 100644
--- a/frontend/farm_designer/plants/select_plants.tsx
+++ b/frontend/farm_designer/plants/select_plants.tsx
@@ -28,7 +28,9 @@ import {
} from "../../resources/selectors";
import { PointInventoryItem } from "../points/point_inventory_item";
import { ToolSlotInventoryItem } from "../tools";
-import { getWebAppConfigValue, GetWebAppConfigValue } from "../../config_storage/actions";
+import {
+ getWebAppConfigValue, GetWebAppConfigValue,
+} from "../../config_storage/actions";
import { BooleanSetting, NumericSetting } from "../../session_keys";
import { isBotOriginQuadrant, BotOriginQuadrant } from "../interfaces";
import { isActive } from "../tools/edit_tool";
@@ -36,12 +38,27 @@ import { uniq } from "lodash";
import { POINTER_TYPES } from "../point_groups/criteria/interfaces";
import { WeedInventoryItem } from "../weeds/weed_inventory_item";
+// tslint:disable-next-line:no-any
+export const isPointType = (x: any): x is PointType => POINTER_TYPES.includes(x);
+
+export const validPointTypes =
+ (pointerTypes: unknown[] | undefined): PointType[] | undefined => {
+ const validValues = (pointerTypes || [])
+ .filter(x => isPointType(x)).map(x => x as PointType);
+ return validValues.length > 0 ? validValues : undefined;
+ };
+
+export const setSelectionPointType = (payload: PointType[] | undefined) =>
+ (dispatch: Function) =>
+ dispatch({ type: Actions.SET_SELECTION_POINT_TYPE, payload });
+
export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
Plant: { label: t("Plants"), value: "Plant" },
GenericPointer: { label: t("Points"), value: "GenericPointer" },
Weed: { label: t("Weeds"), value: "Weed" },
ToolSlot: { label: t("Slots"), value: "ToolSlot" },
All: { label: t("All"), value: "All" },
+ Other: { label: t("Other"), value: "Other" },
});
export const POINTER_TYPE_LIST = () => [
POINTER_TYPE_DDI_LOOKUP().Plant,
@@ -96,10 +113,8 @@ export class RawSelectPlants extends React.Component {
}
}
- componentWillUnmount = () => this.props.dispatch({
- type: Actions.SET_SELECTION_POINT_TYPE,
- payload: undefined,
- });
+ componentWillUnmount = () =>
+ this.props.dispatch(setSelectionPointType(undefined));
get selected() { return this.props.selected || []; }
@@ -127,10 +142,8 @@ export class RawSelectPlants extends React.Component {
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],
- });
+ this.props.dispatch(setSelectionPointType(
+ ddi.value == "All" ? POINTER_TYPES : validPointTypes([ddi.value])));
}} />
({
init: jest.fn(() => ({ payload: { uuid: "???" } })),
+ overwrite: jest.fn(),
save: jest.fn()
}));
@@ -10,18 +11,19 @@ jest.mock("../../../resources/selectors", () => ({
selectAllRegimens: jest.fn()
}));
-import { createGroup } from "../actions";
-import { init, save } from "../../../api/crud";
+import { createGroup, overwriteGroup } from "../actions";
+import { init, save, overwrite } from "../../../api/crud";
import { history } from "../../../history";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import {
- fakePoint, fakePlant, fakeToolSlot,
+ fakePoint, fakePlant, fakeToolSlot, fakePointGroup,
} from "../../../__test_support__/fake_state/resources";
import { DeepPartial } from "redux";
import { Everything } from "../../../interfaces";
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
+import { cloneDeep } from "lodash";
describe("group action creators and thunks", () => {
it("creates groups", async () => {
@@ -44,3 +46,14 @@ describe("group action creators and thunks", () => {
.toHaveBeenCalledWith("/app/designer/groups/323232332");
});
});
+
+describe("overwriteGroup()", () => {
+ it("overwrites and saves", () => {
+ const group = fakePointGroup();
+ const newGroupBody = cloneDeep(group.body);
+ newGroupBody.point_ids = [1, 2, 3];
+ overwriteGroup(group, newGroupBody)(jest.fn());
+ expect(overwrite).toHaveBeenCalledWith(group, newGroupBody);
+ expect(save).toHaveBeenCalledWith(group.uuid);
+ });
+});
diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx
index 6b7e42592..ed0a7be4a 100644
--- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx
+++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx
@@ -6,6 +6,12 @@ jest.mock("../../../api/crud", () => ({
jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
+jest.mock("../../plants/select_plants", () => ({
+ setSelectionPointType: jest.fn(),
+ validPointTypes: jest.fn(),
+ POINTER_TYPE_LIST: () => [],
+}));
+
import React from "react";
import {
GroupDetailActive, GroupDetailActiveProps,
@@ -14,9 +20,10 @@ import { mount, shallow } from "enzyme";
import {
fakePointGroup, fakePlant,
} from "../../../__test_support__/fake_state/resources";
-import { save, edit } from "../../../api/crud";
+import { edit } from "../../../api/crud";
import { SpecialStatus } from "farmbot";
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
+import { setSelectionPointType } from "../../plants/select_plants";
describe(" ", () => {
const fakeProps = (): GroupDetailActiveProps => {
@@ -35,26 +42,14 @@ describe(" ", () => {
slugs: [],
hovered: undefined,
editGroupAreaInMap: false,
+ botSize: {
+ x: { value: 3000, isDefault: true },
+ y: { value: 1500, isDefault: true },
+ },
+ selectionPointType: undefined,
};
};
- it("saves", () => {
- const p = fakeProps();
- const el = new GroupDetailActive(p);
- el.saveGroup();
- expect(p.dispatch).toHaveBeenCalled();
- expect(save).toHaveBeenCalledWith(p.group.uuid);
- });
-
- it("is already saved", () => {
- const p = fakeProps();
- p.group.specialStatus = SpecialStatus.SAVED;
- const el = new GroupDetailActive(p);
- el.saveGroup();
- expect(p.dispatch).not.toHaveBeenCalled();
- expect(save).not.toHaveBeenCalled();
- });
-
it("toggles icon view", () => {
const p = fakeProps();
const wrapper = mount( );
@@ -68,54 +63,24 @@ describe(" ", () => {
p.group.specialStatus = SpecialStatus.SAVED;
const wrapper = mount( );
expect(wrapper.find("input").first().prop("defaultValue")).toContain("XYZ");
- expect(wrapper.find(".groups-list-wrapper").length).toEqual(1);
- expect(wrapper.text()).not.toContain("saving");
- });
-
- it("shows saving indicator", () => {
- const p = fakeProps();
- p.group.specialStatus = SpecialStatus.DIRTY;
- const wrapper = mount( );
- expect(wrapper.text()).toContain("saving");
- });
-
- it("changes group name", () => {
- const NEW_NAME = "new group name";
- const wrapper = shallow( );
- wrapper.find("input").first().simulate("change", {
- currentTarget: { value: NEW_NAME }
- });
- expect(edit).toHaveBeenCalledWith(expect.any(Object), { name: NEW_NAME });
- });
-
- it("changes the sort type", () => {
- const p = fakeProps();
- const { dispatch } = p;
- const el = new GroupDetailActive(p);
- el.changeSortType("random");
- expect(dispatch).toHaveBeenCalled();
- expect(edit).toHaveBeenCalledWith({
- body: {
- name: "XYZ",
- point_ids: [1],
- sort_type: "xy_ascending",
- criteria: DEFAULT_CRITERIA
- },
- kind: "PointGroup",
- specialStatus: "DIRTY",
- uuid: p.group.uuid,
- },
- { sort_type: "random" });
+ expect(wrapper.find(".group-member-display").length).toEqual(1);
});
it("unmounts", () => {
- window.clearInterval = jest.fn();
const p = fakeProps();
- const el = new GroupDetailActive(p);
- // tslint:disable-next-line:no-any
- el.state.timerId = 123 as any;
- el.componentWillUnmount && el.componentWillUnmount();
- expect(clearInterval).toHaveBeenCalledWith(123);
+ p.group.body.criteria.string_eq.pointer_type = ["Weed"];
+ const wrapper = mount( );
+ wrapper.unmount();
+ expect(setSelectionPointType).toHaveBeenCalledWith(undefined);
+ });
+
+ it("changes group name", () => {
+ const p = fakeProps();
+ const wrapper = shallow( );
+ wrapper.find("input").first().simulate("blur", {
+ currentTarget: { value: "new group name" }
+ });
+ expect(edit).toHaveBeenCalledWith(p.group, { name: "new group name" });
});
it("shows paths", () => {
diff --git a/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx
index 70ba6bce3..0f397f2f5 100644
--- a/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx
+++ b/frontend/farm_designer/point_groups/__tests__/group_list_panel_test.tsx
@@ -3,6 +3,10 @@ jest.mock("../../../history", () => ({
history: { push: jest.fn() }
}));
+jest.mock("../actions", () => ({
+ createGroup: jest.fn(),
+}));
+
import React from "react";
import { mount, shallow } from "enzyme";
import {
@@ -16,6 +20,8 @@ import { fakeState } from "../../../__test_support__/fake_state";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
+import { createGroup } from "../actions";
+import { DesignerPanelTop } from "../../designer_panel";
describe(" ", () => {
const fakeProps = (): GroupListPanelProps => {
@@ -39,6 +45,13 @@ describe(" ", () => {
};
};
+ it("creates new group", () => {
+ const p = fakeProps();
+ const wrapper = shallow( );
+ wrapper.find(DesignerPanelTop).simulate("click");
+ expect(createGroup).toHaveBeenCalledWith({ pointUuids: [] });
+ });
+
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow( );
@@ -66,7 +79,9 @@ describe(" ", () => {
const wrapper = mount( );
expect(wrapper.text().toLowerCase()).toContain("no groups yet");
});
+});
+describe("mapStateToProps()", () => {
it("maps state to props", () => {
const state = fakeState();
const group = fakePointGroup();
diff --git a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx
index 2aef96cb3..1b2b615f6 100644
--- a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx
+++ b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx
@@ -1,4 +1,7 @@
-jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
+jest.mock("../../../api/crud", () => ({
+ edit: jest.fn(),
+ save: jest.fn(),
+}));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx
index 75a32c755..577f5ae30 100644
--- a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx
+++ b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx
@@ -4,7 +4,7 @@ jest.mock("../../../open_farm/cached_crop", () => ({
}));
jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() }));
-jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
+jest.mock("../actions", () => ({ overwriteGroup: jest.fn() }));
import React from "react";
import {
@@ -19,16 +19,17 @@ import {
maybeGetCachedPlantIcon, setImgSrc,
} from "../../../open_farm/cached_crop";
import { setHoveredPlant } from "../../map/actions";
-import { overwrite } from "../../../api/crud";
import { cloneDeep } from "lodash";
import { imgEvent } from "../../../__test_support__/fake_html_events";
import { error } from "../../../toast/toast";
import { svgToUrl, DEFAULT_ICON } from "../../../open_farm/icons";
import { DEFAULT_WEED_ICON } from "../../map/layers/weeds/garden_weed";
+import { overwriteGroup } from "../actions";
+import { mockDispatch } from "../../../__test_support__/fake_dispatch";
describe(" ", () => {
const fakeProps = (): PointGroupItemProps => ({
- dispatch: jest.fn(),
+ dispatch: mockDispatch(),
point: fakePlant(),
group: fakePointGroup(),
hovered: true
@@ -56,6 +57,16 @@ describe(" ", () => {
expect(setImgSrc).not.toHaveBeenCalled();
});
+ it("doesn't fetch non-plant icon", async () => {
+ const p = fakeProps();
+ p.point = fakeWeed();
+ const i = new PointGroupItem(p);
+ const fakeImgEvent = imgEvent();
+ await i.maybeGetCachedIcon(fakeImgEvent);
+ expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
+ expect(setImgSrc).not.toHaveBeenCalled();
+ });
+
it("sets icon in state", () => {
const i = new PointGroupItem(fakeProps());
i.setState = jest.fn();
@@ -121,7 +132,7 @@ describe(" ", () => {
expect(i.props.dispatch).toHaveBeenCalledTimes(2);
const expectedGroupBody = cloneDeep(p.group.body);
expectedGroupBody.point_ids = [];
- expect(overwrite).toHaveBeenCalledWith(p.group, expectedGroupBody);
+ expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedGroupBody);
expect(setHoveredPlant).toHaveBeenCalledWith(undefined);
});
@@ -132,9 +143,9 @@ describe(" ", () => {
const i = new PointGroupItem(p);
i.click();
expect(i.props.dispatch).not.toHaveBeenCalled();
- expect(overwrite).not.toHaveBeenCalled();
+ expect(overwriteGroup).not.toHaveBeenCalled();
expect(setHoveredPlant).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
- "Cannot remove points selected by criteria.");
+ "Cannot remove points selected by filters.");
});
});
diff --git a/frontend/farm_designer/point_groups/actions.ts b/frontend/farm_designer/point_groups/actions.ts
index 876aad9ff..e51e0548c 100644
--- a/frontend/farm_designer/point_groups/actions.ts
+++ b/frontend/farm_designer/point_groups/actions.ts
@@ -1,12 +1,13 @@
import { betterCompact } from "../../util";
import { PointGroup } from "farmbot/dist/resources/api_resources";
-import { init, save } from "../../api/crud";
+import { init, save, overwrite } from "../../api/crud";
import { history } from "../../history";
import { GetState } from "../../redux/interfaces";
import { findPointGroup } from "../../resources/selectors";
import { t } from "../../i18next_wrapper";
import { UUID } from "../../resources/interfaces";
import { DEFAULT_CRITERIA } from "./criteria/interfaces";
+import { TaggedPointGroup } from "farmbot";
interface CreateGroupProps {
pointUuids: UUID[];
@@ -35,3 +36,10 @@ export const createGroup = ({ pointUuids, groupName }: CreateGroupProps) =>
history.push("/app/designer/groups/" + (id ? id : ""));
});
};
+
+export const overwriteGroup =
+ (group: TaggedPointGroup, newGroupBody: PointGroup) =>
+ (dispatch: Function) => {
+ dispatch(overwrite(group, newGroupBody));
+ dispatch(save(group.uuid));
+ };
diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx
index 0f13b9c73..dee0acf3f 100644
--- a/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx
+++ b/frontend/farm_designer/point_groups/criteria/__tests__/component_test.tsx
@@ -1,19 +1,25 @@
-jest.mock("../../../../api/crud", () => ({
- overwrite: jest.fn(),
- save: jest.fn(),
-}));
+jest.mock("../../actions", () => ({ overwriteGroup: jest.fn() }));
+
+jest.mock("../edit", () => ({ togglePointTypeCriteria: jest.fn() }));
import React from "react";
-import { mount } from "enzyme";
-import { GroupCriteria, GroupPointCountBreakdown } from "..";
+import { mount, shallow } from "enzyme";
+import {
+ GroupCriteria, GroupPointCountBreakdown, PointTypeSelection,
+ togglePointTypeCriteria,
+} from "..";
import {
GroupCriteriaProps, GroupPointCountBreakdownProps, DEFAULT_CRITERIA,
+ PointTypeSelectionProps,
} from "../interfaces";
import {
- fakePointGroup,
+ fakePointGroup, fakePoint,
} from "../../../../__test_support__/fake_state/resources";
import { cloneDeep } from "lodash";
-import { overwrite, save } from "../../../../api/crud";
+import { Checkbox } from "../../../../ui";
+import { Actions } from "../../../../constants";
+import { overwriteGroup } from "../../actions";
+import { mockDispatch } from "../../../../__test_support__/fake_dispatch";
describe(" ", () => {
const fakeProps = (): GroupCriteriaProps => ({
@@ -21,41 +27,162 @@ describe(" ", () => {
group: fakePointGroup(),
slugs: [],
editGroupAreaInMap: false,
+ botSize: {
+ x: { value: 3000, isDefault: true },
+ y: { value: 1500, isDefault: true },
+ },
+ selectionPointType: undefined,
});
it("renders", () => {
const wrapper = mount( );
- ["criteria", "age selection"].map(string =>
+ ["filters", "age"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
});
- it("clears criteria", () => {
+ it("mounts", () => {
const p = fakeProps();
- const wrapper = mount( );
- wrapper.find("button").last().simulate("click");
- const expectedBody = cloneDeep(p.group.body);
- expectedBody.criteria = DEFAULT_CRITERIA;
- expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
- expect(save).toHaveBeenCalledWith(p.group.uuid);
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
+ p.group.body.criteria.string_eq.pointer_type = ["Weed"];
+ mount( );
+ expect(dispatch).toHaveBeenCalledWith({
+ type: Actions.SET_SELECTION_POINT_TYPE,
+ payload: ["Weed"],
+ });
});
it("toggles advanced view", () => {
- const wrapper = mount( );
- expect(wrapper.text()).not.toContain("number criteria");
- wrapper.find("ToggleButton").first().simulate("click");
- expect(wrapper.text()).toContain("number criteria");
+ const wrapper = mount( );
+ expect(wrapper.text()).not.toContain("numbers");
+ const menu = mount(wrapper.instance().AdvancedToggleMenu());
+ menu.find("ToggleButton").first().simulate("click");
+ expect(wrapper.text()).toContain("numbers");
+ });
+
+ it("shows day criteria in advanced view", () => {
+ const wrapper = mount( );
+ wrapper.setState({ advanced: true });
+ expect(wrapper.text()).toContain("day");
+ });
+
+ it("changes day criteria", () => {
+ const wrapper = mount( );
+ expect(wrapper.state().dayChanged).toBeFalsy();
+ wrapper.instance().changeDay(true);
+ expect(wrapper.state().dayChanged).toBeTruthy();
});
});
describe(" ", () => {
const fakeProps = (): GroupPointCountBreakdownProps => ({
- manualCount: 1,
- totalCount: 3,
+ group: fakePointGroup(),
+ dispatch: jest.fn(),
+ shouldDisplay: () => true,
+ pointsSelectedByGroup: [],
+ iconDisplay: true,
+ hovered: undefined,
});
- it("renders", () => {
- const wrapper = mount( );
- ["1manually selected", "2selected by criteria"].map(string =>
+ it("renders point counts", () => {
+ const p = fakeProps();
+ const point1 = fakePoint();
+ point1.body.id = 1;
+ const point2 = fakePoint();
+ point2.body.id = 2;
+ const point3 = fakePoint();
+ point3.body.id = 3;
+ p.pointsSelectedByGroup = [point1, point2, point3];
+ p.group.body.point_ids = [1];
+ const wrapper = mount( );
+ ["1manually selected", "2selected by filters"].map(string =>
expect(wrapper.text()).toContain(string));
});
+
+ it("renders point counts: undefined ids", () => {
+ const p = fakeProps();
+ const point1 = fakePoint();
+ point1.body.id = undefined;
+ const point2 = fakePoint();
+ point2.body.id = undefined;
+ p.pointsSelectedByGroup = [point1, point2];
+ p.group.body.point_ids = [];
+ const wrapper = mount( );
+ ["0manually selected", "2selected by filters"].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ it("clears point ids", () => {
+ const p = fakeProps();
+ p.group.body.point_ids = [1, 2];
+ const wrapper = mount( );
+ window.confirm = () => true;
+ wrapper.find("button").first().simulate("click");
+ const expectedBody = cloneDeep(p.group.body);
+ expectedBody.point_ids = [];
+ expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedBody);
+ });
+
+ it("doesn't clear point ids", () => {
+ const p = fakeProps();
+ p.group.body.point_ids = [1, 2];
+ const wrapper = mount( );
+ window.confirm = () => false;
+ wrapper.find("button").first().simulate("click");
+ expect(overwriteGroup).not.toHaveBeenCalled();
+ });
+
+ it("clears criteria", () => {
+ const p = fakeProps();
+ const wrapper = mount( );
+ window.confirm = () => true;
+ wrapper.find("button").last().simulate("click");
+ const expectedBody = cloneDeep(p.group.body);
+ expectedBody.criteria = DEFAULT_CRITERIA;
+ expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedBody);
+ });
+
+ it("doesn't clear criteria", () => {
+ const p = fakeProps();
+ const wrapper = mount( );
+ window.confirm = () => false;
+ wrapper.find("button").last().simulate("click");
+ expect(overwriteGroup).not.toHaveBeenCalled();
+ });
+});
+
+describe(" ", () => {
+ const fakeProps = (): PointTypeSelectionProps => ({
+ dispatch: jest.fn(),
+ group: fakePointGroup(),
+ pointTypes: [],
+ });
+
+ it("selects pointer_type", () => {
+ const p = fakeProps();
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
+ const wrapper = shallow( );
+ wrapper.find("FBSelect").simulate("change", { label: "", value: "Plant" });
+ expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant", true);
+ expect(dispatch).toHaveBeenCalledWith({
+ type: Actions.SET_SELECTION_POINT_TYPE,
+ payload: ["Plant"],
+ });
+ });
+
+ it("doesn't select pointer_type", () => {
+ const p = fakeProps();
+ const wrapper = shallow( );
+ wrapper.find("FBSelect").simulate("change", { label: "", value: "nope" });
+ expect(togglePointTypeCriteria).not.toHaveBeenCalled();
+ });
+
+ it("changes pointer_type", () => {
+ const p = fakeProps();
+ p.pointTypes = ["Plant", "Weed"];
+ const wrapper = shallow( );
+ wrapper.find(Checkbox).first().simulate("change");
+ expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant");
+ });
});
diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts
index 3bcebcef8..32ea0d7cc 100644
--- a/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts
+++ b/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts
@@ -1,7 +1,4 @@
-jest.mock("../../../../api/crud", () => ({
- overwrite: jest.fn(),
- save: jest.fn(),
-}));
+jest.mock("../../actions", () => ({ overwriteGroup: jest.fn() }));
import {
editCriteria, toggleEqCriteria,
@@ -15,10 +12,11 @@ import {
import {
fakePointGroup,
} from "../../../../__test_support__/fake_state/resources";
-import { overwrite, save } from "../../../../api/crud";
import { cloneDeep } from "lodash";
import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces";
import { inputEvent } from "../../../../__test_support__/fake_html_events";
+import { error } from "../../../../toast/toast";
+import { overwriteGroup } from "../../actions";
describe("editCriteria()", () => {
it("edits criteria: all empty", () => {
@@ -27,15 +25,13 @@ describe("editCriteria()", () => {
editCriteria(group, {})(jest.fn());
const expectedBody = cloneDeep(group.body);
expectedBody.criteria = DEFAULT_CRITERIA;
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("edits criteria: empty update", () => {
const group = fakePointGroup();
editCriteria(group, {})(jest.fn());
- expect(overwrite).toHaveBeenCalledWith(group, group.body);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, group.body);
});
it("edits criteria: full update", () => {
@@ -50,8 +46,7 @@ describe("editCriteria()", () => {
editCriteria(group, criteria)(jest.fn());
const expectedBody = cloneDeep(group.body);
expectedBody.criteria = criteria;
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
});
@@ -91,15 +86,14 @@ describe("toggleAndEditEqCriteria()", () => {
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.string_eq = { openfarm_slug: ["mint"] };
toggleAndEditEqCriteria(group, "openfarm_slug", "mint")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("toggles criteria on for point type", () => {
const group = fakePointGroup();
const expectedBody = cloneDeep(group.body);
group.body.criteria.string_eq = {
- pointer_type: ["GenericPointer", "Plant", "ToolSlot"],
+ pointer_type: ["GenericPointer", "Plant", "ToolSlot", "Weed"],
openfarm_slug: ["apple"],
"meta.color": ["red"],
};
@@ -107,19 +101,18 @@ describe("toggleAndEditEqCriteria()", () => {
pullout_direction: [0]
};
expectedBody.criteria.string_eq = {
- pointer_type: ["Plant"],
+ pointer_type: ["GenericPointer", "Plant", "ToolSlot", "Weed"],
openfarm_slug: ["apple", "mint"],
};
expectedBody.criteria.number_eq = {};
toggleAndEditEqCriteria(group, "openfarm_slug", "mint", "Plant")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("toggles off", () => {
const group = fakePointGroup();
group.body.criteria.string_eq = {
- pointer_type: ["GenericPointer", "Plant", "ToolSlot"],
+ pointer_type: ["GenericPointer", "Plant", "ToolSlot", "Weed"],
openfarm_slug: ["mint"],
"meta.color": ["red"],
};
@@ -129,8 +122,7 @@ describe("toggleAndEditEqCriteria()", () => {
const expectedBody = cloneDeep(group.body);
delete expectedBody.criteria.string_eq.openfarm_slug;
toggleAndEditEqCriteria(group, "openfarm_slug", "mint", "Plant")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("toggles on: empty criteria", () => {
@@ -145,8 +137,7 @@ describe("toggleAndEditEqCriteria()", () => {
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.number_eq = { pullout_direction: [0] };
toggleAndEditEqCriteria(group, "pullout_direction", 0, "ToolSlot")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
});
@@ -159,10 +150,12 @@ describe("togglePointTypeCriteria()", () => {
"meta.color": ["red"],
};
const expectedBody = cloneDeep(group.body);
- expectedBody.criteria.string_eq.pointer_type?.push("Plant");
+ expectedBody.criteria.string_eq = {
+ pointer_type: ["GenericPointer", "Plant"],
+ openfarm_slug: ["mint"],
+ };
togglePointTypeCriteria(group, "Plant")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("toggles off", () => {
@@ -175,11 +168,10 @@ describe("togglePointTypeCriteria()", () => {
};
expectedBody.criteria.string_eq = {
pointer_type: ["GenericPointer"],
- "meta.color": ["red"],
+ openfarm_slug: ["mint"],
};
togglePointTypeCriteria(group, "Plant")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("toggles on: empty criteria", () => {
@@ -188,8 +180,7 @@ describe("togglePointTypeCriteria()", () => {
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.string_eq = { pointer_type: ["Plant"] };
togglePointTypeCriteria(group, "Plant")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("toggles off: empty criteria", () => {
@@ -201,8 +192,19 @@ describe("togglePointTypeCriteria()", () => {
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.string_eq = {};
togglePointTypeCriteria(group, "ToolSlot")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
+ });
+
+ it("clears other pointer types", () => {
+ const group = fakePointGroup();
+ group.body.criteria.string_eq = {
+ pointer_type: ["Plant", "ToolSlot"],
+ "plant_stage": ["planned"],
+ };
+ const expectedBody = cloneDeep(group.body);
+ expectedBody.criteria.string_eq = { pointer_type: ["Weed"] };
+ togglePointTypeCriteria(group, "Weed", true)(dispatch);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
});
@@ -212,9 +214,8 @@ describe("clearCriteriaField()", () => {
const expectedBody = cloneDeep(group.body);
group.body.criteria.string_eq = { plant_stage: ["planted"] };
expectedBody.criteria.string_eq = {};
- clearCriteriaField(group, ["string_eq"], "plant_stage")(dispatch);
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ clearCriteriaField(group, ["string_eq"], ["plant_stage"])(dispatch);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
});
@@ -226,16 +227,14 @@ describe("editGtLtCriteria()", () => {
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.number_gt = { x: 0, y: 2 };
expectedBody.criteria.number_lt = { x: 3, y: 4 };
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("doesn't edit criteria", () => {
const group = fakePointGroup();
const box = { x0: undefined, y0: 2, x1: 3, y1: 4 };
editGtLtCriteria(group, box)(dispatch);
- expect(overwrite).not.toHaveBeenCalled();
- expect(save).not.toHaveBeenCalled();
+ expect(overwriteGroup).not.toHaveBeenCalled();
});
});
@@ -247,8 +246,7 @@ describe("removeEqCriteriaValue()", () => {
"string_eq", "plant_stage", "planned")(dispatch);
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.string_eq = { plant_stage: ["planted"] };
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
});
@@ -259,8 +257,37 @@ describe("editGtLtCriteriaField()", () => {
editGtLtCriteriaField(group, "number_lt", "radius")(e)(dispatch);
const expectedBody = cloneDeep(group.body);
expectedBody.criteria.number_lt = { radius: 1 };
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(error).not.toHaveBeenCalled();
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
+ });
+
+ it("errors when changing value: lt", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_gt = { radius: 1 };
+ const e = inputEvent("0");
+ editGtLtCriteriaField(group, "number_lt", "radius")(e)(dispatch);
+ expect(error).toHaveBeenCalledWith("Value must be greater than 1.");
+ expect(overwriteGroup).not.toHaveBeenCalled();
+ });
+
+ it("errors when changing value: gt", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_lt = { radius: 0 };
+ const e = inputEvent("1");
+ editGtLtCriteriaField(group, "number_gt", "radius")(e)(dispatch);
+ expect(error).toHaveBeenCalledWith("Value must be less than 0.");
+ expect(overwriteGroup).not.toHaveBeenCalled();
+ });
+
+ it("doesn't error when removing value", () => {
+ const group = fakePointGroup();
+ group.body.criteria.number_lt = { radius: 0 };
+ const e = inputEvent("");
+ editGtLtCriteriaField(group, "number_gt", "radius")(e)(dispatch);
+ const expectedBody = cloneDeep(group.body);
+ expectedBody.criteria.number_gt = { radius: undefined };
+ expect(error).not.toHaveBeenCalled();
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
it("clears incompatible criteria", () => {
@@ -272,7 +299,7 @@ describe("editGtLtCriteriaField()", () => {
group, "number_lt", "radius", "GenericPointer",
)(e)(dispatch);
expectedBody.criteria.number_lt = { radius: 1 };
- expect(overwrite).toHaveBeenCalledWith(group, expectedBody);
- expect(save).toHaveBeenCalledWith(group.uuid);
+ expect(error).not.toHaveBeenCalled();
+ expect(overwriteGroup).toHaveBeenCalledWith(group, expectedBody);
});
});
diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx
deleted file mode 100644
index de5c4eecf..000000000
--- a/frontend/farm_designer/point_groups/criteria/__tests__/presets_test.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-jest.mock("../edit", () => ({
- togglePointTypeCriteria: jest.fn(),
- toggleAndEditEqCriteria: jest.fn(),
- clearCriteriaField: jest.fn(),
-}));
-
-import React from "react";
-import { mount, shallow } from "enzyme";
-import {
- CheckboxSelections, togglePointTypeCriteria, clearCriteriaField,
-} from "..";
-import { CheckboxSelectionsProps } from "../interfaces";
-import {
- fakePointGroup,
-} from "../../../../__test_support__/fake_state/resources";
-import { Checkbox } from "../../../../ui";
-
-describe(" ", () => {
- const fakeProps = (): CheckboxSelectionsProps => ({
- dispatch: jest.fn(),
- group: fakePointGroup(),
- slugs: ["mint"],
- });
-
- it("renders all criteria", () => {
- const STRINGS = [
- "planted", "mint",
- "farm designer", "radius", "green",
- "positive x",
- ];
- const wrapper = mount( );
- STRINGS.map(string =>
- expect(wrapper.text().toLowerCase()).not.toContain(string.toLowerCase()));
- wrapper.setState({ Plant: true, GenericPointer: true, ToolSlot: true });
- STRINGS.map(string =>
- expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
- });
-
- it("clears sub criteria", () => {
- const p = fakeProps();
- p.group.body.criteria.string_eq = { plant_stage: ["planned"] };
- const wrapper = mount( );
- wrapper.setState({ Plant: true, GenericPointer: false, ToolSlot: false });
- wrapper.find(".plant-criteria-options")
- .find("input").first().simulate("change");
- expect(clearCriteriaField).toHaveBeenCalledWith(
- p.group, ["string_eq"], "plant_stage",
- );
- });
-
- it("toggles section", () => {
- const wrapper =
- shallow( );
- expect(wrapper.state().Plant).toBeFalsy();
- wrapper.instance().toggleMore("Plant")();
- expect(wrapper.state().Plant).toBeTruthy();
- });
-
- it("toggles point type", () => {
- const p = fakeProps();
- const wrapper = mount( );
- wrapper.find("input").first().simulate("change");
- expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant");
- });
-
- it("stops propagation", () => {
- const wrapper = mount( );
- const e = { stopPropagation: jest.fn() };
- wrapper.find(".fb-checkbox").first().simulate("click", e);
- expect(e.stopPropagation).toHaveBeenCalled();
- });
-
- it("is not disabled", () => {
- const p = fakeProps();
- const wrapper = mount( );
- const pointTypeBoxes = wrapper.find(".point-type-checkbox").find("input");
- expect(pointTypeBoxes.first().props().disabled).toBeFalsy();
- expect(pointTypeBoxes.at(1).props().disabled).toBeFalsy();
- expect(pointTypeBoxes.last().props().disabled).toBeFalsy();
- });
-
- it("is disabled", () => {
- const p = fakeProps();
- p.group.body.criteria.string_eq = { plant_stage: ["planted"] };
- p.group.body.criteria.number_eq = { pullout_direction: [0] };
- p.group.body.criteria.number_gt = { radius: 0 };
- const wrapper = mount( );
- const pointTypeBoxes = wrapper.find(".point-type-checkbox").find(Checkbox);
- expect(pointTypeBoxes.first().props().disabled).toBeTruthy();
- expect(pointTypeBoxes.at(1).props().disabled).toBeTruthy();
- expect(pointTypeBoxes.last().props().disabled).toBeTruthy();
- });
-});
diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts
index 7592f8cc9..31918c6fe 100644
--- a/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts
+++ b/frontend/farm_designer/point_groups/criteria/__tests__/selected_test.ts
@@ -1,4 +1,6 @@
-import { eqCriteriaSelected, criteriaHasKey, hasSubCriteria, typeDisabled } from "..";
+import {
+ eqCriteriaSelected, criteriaHasKey, hasSubCriteria, typeDisabled,
+} from "..";
import { DEFAULT_CRITERIA, PointGroupCriteria } from "../interfaces";
import { cloneDeep } from "lodash";
@@ -6,14 +8,35 @@ const fakeCriteria = (): PointGroupCriteria =>
cloneDeep(DEFAULT_CRITERIA);
describe("eqCriteriaSelected()", () => {
- it("returns selected", () => {
+ it("returns selected numbers", () => {
const criteria = fakeCriteria();
criteria.number_eq = { pullout_direction: [0] };
const result = eqCriteriaSelected(criteria)("pullout_direction", 0);
expect(result).toEqual(true);
});
- it("returns not selected", () => {
+ it("returns numbers not selected", () => {
+ const criteria = fakeCriteria();
+ criteria.number_eq = {};
+ const result = eqCriteriaSelected(criteria)("pullout_direction", 0);
+ expect(result).toEqual(false);
+ });
+
+ it("returns selected strings", () => {
+ const criteria = fakeCriteria();
+ criteria.string_eq = { plant_stage: ["planted"] };
+ const result = eqCriteriaSelected(criteria)("plant_stage", "planted");
+ expect(result).toEqual(true);
+ });
+
+ it("returns strings not selected", () => {
+ const criteria = fakeCriteria();
+ criteria.string_eq = {};
+ const result = eqCriteriaSelected(criteria)("plant_stage", "planted");
+ expect(result).toEqual(false);
+ });
+
+ it("returns other not selected", () => {
const criteria = fakeCriteria();
const result = eqCriteriaSelected(criteria)(
"pullout_direction", false as unknown as string);
@@ -35,22 +58,43 @@ describe("criteriaHasKey()", () => {
const result = criteriaHasKey(criteria, ["string_eq"], "plant_stage");
expect(result).toBeFalsy();
});
+
+ it("has key", () => {
+ const criteria = fakeCriteria();
+ criteria.number_eq = { x: [0] };
+ const result = criteriaHasKey(criteria, ["number_eq"], "x");
+ expect(result).toBeTruthy();
+ });
});
describe("hasSubCriteria()", () => {
- it("has criteria", () => {
+ it("has string criteria", () => {
const criteria = fakeCriteria();
criteria.string_eq = { plant_stage: ["planted"] };
const result = hasSubCriteria(criteria)("Plant");
expect(result).toBeTruthy();
});
- it("doesn't have criteria", () => {
+ it("doesn't have string criteria", () => {
const criteria = fakeCriteria();
criteria.string_eq = { "meta.color": ["red"] };
const result = hasSubCriteria(criteria)("Plant");
expect(result).toBeFalsy();
});
+
+ it("has number criteria", () => {
+ const criteria = fakeCriteria();
+ criteria.number_eq = { pullout_direction: [0] };
+ const result = hasSubCriteria(criteria)("ToolSlot");
+ expect(result).toBeTruthy();
+ });
+
+ it("doesn't have number criteria", () => {
+ const criteria = fakeCriteria();
+ criteria.number_eq = {};
+ const result = hasSubCriteria(criteria)("ToolSlot");
+ expect(result).toBeFalsy();
+ });
});
describe("typeDisabled()", () => {
diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx
index 9a315dd6d..4e2e98317 100644
--- a/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx
+++ b/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx
@@ -3,6 +3,7 @@ jest.mock("../edit", () => ({
editGtLtCriteriaField: jest.fn(() => jest.fn()),
removeEqCriteriaValue: jest.fn(),
clearCriteriaField: jest.fn(),
+ clearLocationCriteria: jest.fn(),
}));
import React from "react";
@@ -21,15 +22,15 @@ import {
import {
EqCriteriaSelectionProps,
NumberCriteriaProps,
- CriteriaSelectionProps,
DEFAULT_CRITERIA,
LocationSelectionProps,
NumberLtGtInputProps,
+ DaySelectionProps,
} from "../interfaces";
import {
fakePointGroup,
} from "../../../../__test_support__/fake_state/resources";
-import { FBSelect } from "../../../../ui";
+import { FBSelect, Checkbox } from "../../../../ui";
import { Actions } from "../../../../constants";
describe(" />", () => {
@@ -88,16 +89,26 @@ describe(" ", () => {
expect(clearCriteriaField).toHaveBeenCalledWith(
p.group,
["number_gt"],
- "x",
+ ["x"],
);
});
});
describe(" ", () => {
- const fakeProps = (): CriteriaSelectionProps => ({
+ const fakeProps = (): DaySelectionProps => ({
criteria: DEFAULT_CRITERIA,
group: fakePointGroup(),
dispatch: jest.fn(),
+ dayChanged: true,
+ changeDay: jest.fn(),
+ advanced: false,
+ });
+
+ it("shows label", () => {
+ const p = fakeProps();
+ p.advanced = true;
+ const wrapper = shallow( );
+ expect(wrapper.html()).toContain("label");
});
it("changes operator", () => {
@@ -121,6 +132,16 @@ describe(" ", () => {
{ day: { days_ago: 1, op: "<" } },
);
});
+
+ it("resets day criteria to default", () => {
+ const p = fakeProps();
+ p.group.body.criteria.day = { op: ">", days_ago: 1 };
+ const wrapper = shallow( );
+ wrapper.find(Checkbox).simulate("change");
+ expect(editCriteria).toHaveBeenCalledWith(p.group, {
+ day: { op: "<", days_ago: 0 }
+ });
+ });
});
describe(" ", () => {
@@ -165,15 +186,46 @@ describe(" ", () => {
group: fakePointGroup(),
dispatch: jest.fn(),
editGroupAreaInMap: false,
+ botSize: {
+ x: { value: 3000, isDefault: true },
+ y: { value: 1500, isDefault: true },
+ },
+ });
+
+ it("clears location criteria", () => {
+ const p = fakeProps();
+ const wrapper = mount( );
+ wrapper.find("input").first().simulate("change");
+ expect(clearCriteriaField).toHaveBeenCalledWith(
+ p.group,
+ ["number_lt", "number_gt"],
+ ["x", "y"],
+ );
});
it("toggles selection box behavior", () => {
const p = fakeProps();
const wrapper = mount( );
- wrapper.find("button").first().simulate("click");
+ wrapper.find("button").last().simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.EDIT_GROUP_AREA_IN_MAP,
payload: true
});
});
+
+ it("doesn't display selection warning", () => {
+ const p = fakeProps();
+ p.group.body.criteria.number_gt = {};
+ p.group.body.criteria.number_gt = {};
+ const wrapper = mount( );
+ expect(wrapper.text().toLowerCase()).not.toContain("invalid selection");
+ });
+
+ it("displays selection warning", () => {
+ const p = fakeProps();
+ p.group.body.criteria.number_lt = { x: 100 };
+ p.group.body.criteria.number_gt = { x: 200 };
+ const wrapper = mount( );
+ expect(wrapper.text().toLowerCase()).toContain("invalid selection");
+ });
});
diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx
index 7bd37dfdf..8de6f1154 100644
--- a/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx
+++ b/frontend/farm_designer/point_groups/criteria/__tests__/subcriteria_test.tsx
@@ -5,11 +5,65 @@ jest.mock("../edit", () => ({
import React from "react";
import { mount } from "enzyme";
import { toggleAndEditEqCriteria } from "..";
-import { CheckboxListProps } from "../interfaces";
+import { CheckboxListProps, SubCriteriaSectionProps } from "../interfaces";
import {
fakePointGroup,
} from "../../../../__test_support__/fake_state/resources";
-import { CheckboxList } from "../subcriteria";
+import { CheckboxList, SubCriteriaSection } from "../subcriteria";
+
+describe(" ", () => {
+ const fakeProps = (): SubCriteriaSectionProps => ({
+ dispatch: Function,
+ group: fakePointGroup(),
+ disabled: false,
+ pointerTypes: [],
+ slugs: [],
+ });
+
+ it("doesn't return criteria", () => {
+ const p = fakeProps();
+ p.pointerTypes = [];
+ const wrapper = mount( );
+ expect(wrapper.text()).toEqual("");
+ });
+
+ it("doesn't return incompatible criteria", () => {
+ const p = fakeProps();
+ p.pointerTypes = ["Plant", "Weed"];
+ const wrapper = mount( );
+ expect(wrapper.text()).toEqual("");
+ });
+
+ it("returns plant criteria", () => {
+ const p = fakeProps();
+ p.pointerTypes = ["Plant"];
+ p.slugs = ["strawberry-guava"];
+ const wrapper = mount( );
+ expect(wrapper.text().toLowerCase()).toContain("stage");
+ expect(wrapper.text()).toContain("Strawberry guava");
+ });
+
+ it("returns point criteria", () => {
+ const p = fakeProps();
+ p.pointerTypes = ["GenericPointer"];
+ const wrapper = mount( );
+ expect(wrapper.text().toLowerCase()).toContain("color");
+ });
+
+ it("returns weed criteria", () => {
+ const p = fakeProps();
+ p.pointerTypes = ["Weed"];
+ const wrapper = mount( );
+ expect(wrapper.text().toLowerCase()).toContain("source");
+ });
+
+ it("returns tool slot criteria", () => {
+ const p = fakeProps();
+ p.pointerTypes = ["ToolSlot"];
+ const wrapper = mount( );
+ expect(wrapper.text().toLowerCase()).toContain("direction");
+ });
+});
describe(" ", () => {
const fakeProps = (): CheckboxListProps => ({
diff --git a/frontend/farm_designer/point_groups/criteria/add.tsx b/frontend/farm_designer/point_groups/criteria/add.tsx
index 7ecefb3b8..03db27e2d 100644
--- a/frontend/farm_designer/point_groups/criteria/add.tsx
+++ b/frontend/farm_designer/point_groups/criteria/add.tsx
@@ -48,7 +48,7 @@ export class AddEqCriteria
@@ -97,7 +97,7 @@ export class AddNumberCriteria
diff --git a/frontend/farm_designer/point_groups/criteria/apply.ts b/frontend/farm_designer/point_groups/criteria/apply.ts
index c284ad2ff..8b09404d3 100644
--- a/frontend/farm_designer/point_groups/criteria/apply.ts
+++ b/frontend/farm_designer/point_groups/criteria/apply.ts
@@ -8,6 +8,11 @@ const eqCriteriaEmpty =
(eqCriteria: Record) =>
every(Object.values(eqCriteria).map(values => !values?.length));
+/** Check if day criteria field is unset. */
+export const dayCriteriaEmpty =
+ (dayCriteria: { op: ">" | "<", days_ago: number }) =>
+ isEqual(dayCriteria, { op: "<", days_ago: 0 });
+
/** Check if a point matches the criteria in the provided category. */
const checkCriteria =
(criteria: PointGroupCriteria, now: moment.Moment) =>
@@ -31,11 +36,11 @@ const checkCriteria =
? point.body.planted_at
: point.body.created_at);
const compareDate = moment(now)
- .subtract(criteria[criteriaKey].days_ago, "days");
- const matchesDays = criteria[criteriaKey].op == "<"
+ .subtract(criteria.day.days_ago, "days");
+ const matchesDays = criteria.day.op == "<"
? pointDate.isAfter(compareDate)
: pointDate.isBefore(compareDate);
- return matchesDays || !criteria[criteriaKey].days_ago;
+ return matchesDays || dayCriteriaEmpty(criteria.day);
}
};
diff --git a/frontend/farm_designer/point_groups/criteria/component.tsx b/frontend/farm_designer/point_groups/criteria/component.tsx
index 5e0432ecd..acbfed7b7 100644
--- a/frontend/farm_designer/point_groups/criteria/component.tsx
+++ b/frontend/farm_designer/point_groups/criteria/component.tsx
@@ -1,81 +1,211 @@
import * as React from "react";
import { t } from "../../../i18next_wrapper";
-import { overwrite, save } from "../../../api/crud";
import {
- DaySelection, EqCriteriaSelection,
- NumberCriteriaSelection, LocationSelection, CheckboxSelections,
+ DaySelection, EqCriteriaSelection, SubCriteriaSection,
+ NumberCriteriaSelection, LocationSelection, togglePointTypeCriteria,
} from ".";
import {
GroupCriteriaProps, GroupPointCountBreakdownProps, GroupCriteriaState,
- DEFAULT_CRITERIA, ClearCriteriaProps,
+ DEFAULT_CRITERIA, ClearCriteriaProps, ClearPointIdsProps, POINTER_TYPES,
+ PointerType,
+ PointTypeSelectionProps,
} from "./interfaces";
import { ToggleButton } from "../../../controls/toggle_button";
+import { Popover } from "@blueprintjs/core";
+import { selectPoint } from "../../map/actions";
+import { FBSelect, Checkbox, Help } from "../../../ui";
+import {
+ POINTER_TYPE_LIST, POINTER_TYPE_DDI_LOOKUP, isPointType, validPointTypes,
+ setSelectionPointType,
+} from "../../plants/select_plants";
+import { ToolTips } from "../../../constants";
+import { overwriteGroup } from "../actions";
+import { sortGroupBy } from "../point_group_sort";
+import { PointGroupItem } from "../point_group_item";
+import { Feature } from "../../../devices/interfaces";
+import { TaggedPoint } from "farmbot";
+
+export const CRITERIA_POINT_TYPE_LOOKUP =
+ (): Record => ({
+ Plant: t("Plants"),
+ GenericPointer: t("Points"),
+ Weed: t("Weeds"),
+ ToolSlot: t("Slots"),
+ });
export class GroupCriteria extends
React.Component {
- state: GroupCriteriaState = { advanced: false, clearCount: 0 };
- render() {
- const { group, dispatch, slugs } = this.props;
- const criteria = group.body.criteria;
- const commonProps = { group, criteria, dispatch };
- return
-
{t("criteria")}
+ state: GroupCriteriaState = {
+ advanced: false, clearCount: 0, dayChanged: false
+ };
+
+ componentDidMount() {
+ const { pointer_type } = this.props.group.body.criteria.string_eq;
+ this.props.dispatch(setSelectionPointType(validPointTypes(pointer_type)));
+ }
+
+ AdvancedToggleMenu = () =>
+
+ {t("advanced mode")}
this.setState({ advanced: !this.state.advanced })} />
+ toggleValue={this.state.advanced}
+ customText={{ textTrue: t("on"), textFalse: t("off") }}
+ toggleAction={() =>
+ this.setState({ advanced: !this.state.advanced })} />
+
+
+ changeDay = (state: boolean) => this.setState({ dayChanged: state });
+
+ render() {
+ const { group, dispatch, slugs } = this.props;
+ const { criteria } = group.body;
+ const commonProps = { group, criteria, dispatch };
+ const dayProps = {
+ dayChanged: this.state.dayChanged,
+ changeDay: this.changeDay,
+ advanced: this.state.advanced,
+ };
+ const pointTypes = validPointTypes(criteria.string_eq.pointer_type) || [];
+ return
+
{t("filters")}
+
+
+
+
+
{!this.state.advanced
?
-
-
-
+
+
+
+ {!pointTypes.includes("ToolSlot") &&
+
}
+
:
-
- {t("string criteria")}
+
+ {t("strings")}
+
{...commonProps}
type={"string"} eqCriteria={criteria.string_eq}
criteriaKey={"string_eq"} />
- {t("number criteria")}
+ {t("numbers")}
{...commonProps}
type={"number"} eqCriteria={criteria.number_eq}
criteriaKey={"number_eq"} />
}
-
;
}
}
/** Reset all group criteria to defaults. */
const ClearCriteria = (props: ClearCriteriaProps) =>
-
{
- props.dispatch(overwrite(props.group, {
- ...props.group.body, criteria: DEFAULT_CRITERIA
- }));
- props.dispatch(save(props.group.uuid));
+ if (confirm(t("Clear all group filters?"))) {
+ props.dispatch(overwriteGroup(props.group, {
+ ...props.group.body, criteria: DEFAULT_CRITERIA
+ }));
+ }
}}>
- {t("clear all criteria")}
+ {t("clear")}
+ ;
+
+/** Clear manually selected points. */
+const ClearPointIds = (props: ClearPointIdsProps) =>
+
{
+ if (confirm(t("Remove all manual selections?"))) {
+ props.dispatch(overwriteGroup(props.group, {
+ ...props.group.body, point_ids: []
+ }));
+ props.dispatch(selectPoint(undefined));
+ }
+ }}>
+ {t("clear")}
;
/** Show counts of manual and criteria selections. */
-export const GroupPointCountBreakdown = (props: GroupPointCountBreakdownProps) =>
-
-
-
- {props.manualCount}
+export const GroupPointCountBreakdown =
+ (props: GroupPointCountBreakdownProps) => {
+ const manuallyAddedIds = props.group.body.point_ids;
+ const sortedPoints =
+ sortGroupBy(props.group.body.sort_type, props.pointsSelectedByGroup);
+ const manualPoints = sortedPoints
+ .filter(p => manuallyAddedIds.includes(p.body.id || 0));
+ const criteriaPoints = sortedPoints
+ .filter(p => !manuallyAddedIds.includes(p.body.id || 0));
+ const generatePointIcons = (point: TaggedPoint) =>
+
;
+ return
+
+
+ {manualPoints.length}
+
+
{t("manually selected")}
+
-
{t("manually selected")}
-
-
-
- {props.totalCount - props.manualCount}
-
-
{t("selected by criteria")}
-
+ {props.iconDisplay && manualPoints.length > 0 &&
+
+ {manualPoints.map(generatePointIcons)}
+
}
+ {props.shouldDisplay(Feature.criteria_groups) &&
+
+
+
+ {criteriaPoints.length}
+
+
{t("selected by filters")}
+
+
+ {props.iconDisplay && criteriaPoints.length > 0 &&
+
+ {criteriaPoints.map(generatePointIcons)}
+
}
+
}
+
;
+ };
+
+/** Select pointer_type string equal criteria,
+ * which determines if any additional criteria is shown. */
+export const PointTypeSelection = (props: PointTypeSelectionProps) =>
+
+
{t("Select all")}
+
{
+ if (isPointType(ddi.value)) {
+ props.dispatch(togglePointTypeCriteria(props.group, ddi.value, true));
+ props.dispatch(setSelectionPointType([ddi.value]));
+ }
+ }} />
+ {props.pointTypes.length > 1 &&
+ POINTER_TYPES.map(pointerType =>
+
+
+ props.dispatch(togglePointTypeCriteria(props.group, pointerType))}
+ checked={props.pointTypes.includes(pointerType)}
+ title={CRITERIA_POINT_TYPE_LOOKUP()[pointerType]} />
+ {CRITERIA_POINT_TYPE_LOOKUP()[pointerType]}
+ )}
;
diff --git a/frontend/farm_designer/point_groups/criteria/edit.ts b/frontend/farm_designer/point_groups/criteria/edit.ts
index 551f7ed1c..03683b38f 100644
--- a/frontend/farm_designer/point_groups/criteria/edit.ts
+++ b/frontend/farm_designer/point_groups/criteria/edit.ts
@@ -1,11 +1,13 @@
-import { overwrite, save } from "../../../api/crud";
import { TaggedPointGroup } from "farmbot";
-import { cloneDeep, isNumber } from "lodash";
+import { cloneDeep, isNumber, isUndefined } from "lodash";
import { SelectionBoxData } from "../../map/background";
import {
PointGroupCriteria, POINTER_TYPES, EqCriteria, PointerType,
StrAndNumCriteriaKeys,
} from "./interfaces";
+import { error } from "../../../toast/toast";
+import { t } from "../../../i18next_wrapper";
+import { overwriteGroup } from "../actions";
/** Update and save group criteria. */
export const editCriteria =
@@ -18,8 +20,7 @@ export const editCriteria =
number_gt: update.number_gt || group.body.criteria.number_gt,
number_lt: update.number_lt || group.body.criteria.number_lt,
};
- dispatch(overwrite(group, { ...group.body, criteria }));
- dispatch(save(group.uuid));
+ dispatch(overwriteGroup(group, { ...group.body, criteria }));
};
/** Toggle string or number equal criteria. */
@@ -64,29 +65,28 @@ export const toggleAndEditEqCriteria =
(
};
/** Clear incompatible criteria. */
-const clearSubCriteria = (
+export const clearSubCriteria = (
pointerTypes: PointerType[],
tempCriteria: PointGroupCriteria,
) => {
const toggleStrEq = toggleEqCriteria(tempCriteria.string_eq, "off");
const toggleNumEq = toggleEqCriteria(tempCriteria.number_eq, "off");
+ const toggleStrEqMapper = (key: string) =>
+ tempCriteria.string_eq[key]?.map(value => toggleStrEq(key, value));
if (pointerTypes.includes("Plant")) {
- Object.entries(tempCriteria.string_eq).map(([key, values]) =>
- ["openfarm_slug", "plant_stage"].includes(key)
- && values?.map(v => toggleStrEq(key, v)));
- toggleStrEq("pointer_type", "Plant");
+ ["openfarm_slug", "plant_stage"].map(toggleStrEqMapper);
}
- if (pointerTypes.includes("GenericPointer")) {
- Object.entries(tempCriteria.string_eq).map(([key, values]) =>
- key.includes("meta") && values?.map(v => toggleStrEq(key, v)));
+ if (pointerTypes.includes("Weed")) {
+ ["meta.created_by"].map(toggleStrEqMapper);
+ }
+ if (pointerTypes.includes("GenericPointer") && pointerTypes.includes("Weed")) {
+ ["meta.color"].map(toggleStrEqMapper);
delete tempCriteria.number_lt.radius;
delete tempCriteria.number_gt.radius;
- toggleStrEq("pointer_type", "GenericPointer");
}
if (pointerTypes.includes("ToolSlot")) {
tempCriteria.number_eq.pullout_direction?.map(value =>
toggleNumEq("pullout_direction", value));
- toggleStrEq("pointer_type", "ToolSlot");
}
};
@@ -95,13 +95,14 @@ const clearSubCriteria = (
* When removing pointer_type criteria, clear pointer_type-specific criteria.
*/
export const togglePointTypeCriteria =
- (group: TaggedPointGroup, pointerType: PointerType) =>
+ (group: TaggedPointGroup, pointerType: PointerType, clear = false) =>
(dispatch: Function) => {
const tempCriteria = cloneDeep(group.body.criteria);
- const wasOn = tempCriteria.string_eq.pointer_type?.includes(pointerType);
const toggle = toggleEqCriteria(tempCriteria.string_eq);
+ clear && (tempCriteria.string_eq.pointer_type = []);
toggle("pointer_type", pointerType);
- wasOn && clearSubCriteria([pointerType], tempCriteria);
+ clearSubCriteria(
+ POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
dispatch(editCriteria(group, tempCriteria));
};
@@ -109,11 +110,12 @@ export const togglePointTypeCriteria =
export const clearCriteriaField = (
group: TaggedPointGroup,
categories: StrAndNumCriteriaKeys,
- field: string,
+ fields: string[],
) =>
(dispatch: Function) => {
const tempCriteria = cloneDeep(group.body.criteria);
- categories.map(category => delete tempCriteria[category][field]);
+ categories.map(category => fields.map(field =>
+ delete tempCriteria[category][field]));
dispatch(editCriteria(group, tempCriteria));
};
@@ -163,7 +165,23 @@ export const editGtLtCriteriaField = (
const tempCriteria = cloneDeep(group.body.criteria);
pointerType && clearSubCriteria(
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
- tempCriteria[criteriaField][criteriaKey] =
- parseInt(e.currentTarget.value);
+ const value = e.currentTarget.value != ""
+ ? parseInt(e.currentTarget.value)
+ : undefined;
+ if (!isUndefined(value)) {
+ const ltValue = tempCriteria.number_lt[criteriaKey];
+ const gtValue = tempCriteria.number_gt[criteriaKey];
+ if (criteriaField == "number_lt" && !isUndefined(gtValue) &&
+ !(value > gtValue)) {
+ return error(t("Value must be greater than {{ num }}.",
+ { num: gtValue }));
+ }
+ if (criteriaField == "number_gt" && !isUndefined(ltValue) &&
+ !(value < ltValue)) {
+ return error(t("Value must be less than {{ num }}.",
+ { num: ltValue }));
+ }
+ }
+ tempCriteria[criteriaField][criteriaKey] = value;
dispatch(editCriteria(group, tempCriteria));
};
diff --git a/frontend/farm_designer/point_groups/criteria/index.tsx b/frontend/farm_designer/point_groups/criteria/index.tsx
index de7ffac22..213592c56 100644
--- a/frontend/farm_designer/point_groups/criteria/index.tsx
+++ b/frontend/farm_designer/point_groups/criteria/index.tsx
@@ -2,7 +2,6 @@ export * from "./add";
export * from "./apply";
export * from "./component";
export * from "./edit";
-export * from "./presets";
export * from "./selected";
export * from "./show";
export * from "./subcriteria";
diff --git a/frontend/farm_designer/point_groups/criteria/interfaces.ts b/frontend/farm_designer/point_groups/criteria/interfaces.ts
index 7b22f8b79..88c35cf15 100644
--- a/frontend/farm_designer/point_groups/criteria/interfaces.ts
+++ b/frontend/farm_designer/point_groups/criteria/interfaces.ts
@@ -1,5 +1,8 @@
-import { TaggedPointGroup, PointType } from "farmbot";
+import { TaggedPointGroup, PointType, TaggedPoint } from "farmbot";
import { PointGroup } from "farmbot/dist/resources/api_resources";
+import { BotSize } from "../../map/interfaces";
+import { ShouldDisplay } from "../../../devices/interfaces";
+import { UUID } from "../../../resources/interfaces";
export type PointGroupCriteria = PointGroup["criteria"];
export type StringEqCriteria = PointGroupCriteria["string_eq"];
@@ -8,7 +11,7 @@ export type StrAndNumCriteriaKeys = (keyof Omit)[];
export type EqCriteria = Record;
export const POINTER_TYPES: PointerType[] =
- ["Plant", "GenericPointer", "ToolSlot", "Weed"];
+ ["Plant", "GenericPointer", "Weed", "ToolSlot"];
export const DEFAULT_CRITERIA: Readonly = {
day: { op: "<", days_ago: 0 },
@@ -23,11 +26,14 @@ export interface GroupCriteriaProps {
group: TaggedPointGroup;
slugs: string[];
editGroupAreaInMap: boolean;
+ botSize: BotSize;
+ selectionPointType: PointType[] | undefined;
}
export interface GroupCriteriaState {
advanced: boolean;
clearCount: number;
+ dayChanged: boolean;
}
export interface ClearCriteriaProps {
@@ -35,9 +41,24 @@ export interface ClearCriteriaProps {
group: TaggedPointGroup;
}
+export interface ClearPointIdsProps {
+ dispatch: Function;
+ group: TaggedPointGroup;
+}
+
export interface GroupPointCountBreakdownProps {
- manualCount: number;
- totalCount: number;
+ group: TaggedPointGroup;
+ dispatch: Function;
+ shouldDisplay: ShouldDisplay;
+ pointsSelectedByGroup: TaggedPoint[];
+ iconDisplay: boolean;
+ hovered: UUID | undefined;
+}
+
+export interface PointTypeSelectionProps {
+ dispatch: Function;
+ group: TaggedPointGroup;
+ pointTypes: PointerType[];
}
export interface CriteriaSelectionProps {
@@ -46,8 +67,15 @@ export interface CriteriaSelectionProps {
dispatch: Function;
}
+export interface DaySelectionProps extends CriteriaSelectionProps {
+ dayChanged: boolean;
+ changeDay(state: boolean): void;
+ advanced: boolean;
+}
+
export interface LocationSelectionProps extends CriteriaSelectionProps {
editGroupAreaInMap: boolean;
+ botSize: BotSize;
}
export interface EqCriteriaSelectionProps extends CriteriaSelectionProps {
@@ -84,6 +112,10 @@ export interface SubCriteriaProps {
disabled: boolean;
}
+export interface PointSubCriteriaProps extends SubCriteriaProps {
+ pointerType: PointerType;
+}
+
export interface PlantSubCriteriaProps extends SubCriteriaProps {
slugs: string[];
}
@@ -92,6 +124,7 @@ export interface CheckboxSelectionsProps {
dispatch: Function;
group: TaggedPointGroup;
slugs: string[];
+ pointerTypes: PointType[] | undefined;
}
export interface CheckboxSelectionsState {
@@ -111,16 +144,26 @@ export interface NumberLtGtInputProps {
pointerType?: PointerType;
}
+export interface SubCriteriaSectionProps {
+ dispatch: Function;
+ group: TaggedPointGroup;
+ disabled: boolean;
+ pointerTypes: PointerType[];
+ slugs: string[];
+}
+
export interface ClearCategoryProps {
group: TaggedPointGroup;
criteriaCategories: StrAndNumCriteriaKeys;
- criteriaKey: string;
+ criteriaKeys: string[];
dispatch: Function;
}
+export type CheckboxListItem = { label: string, value: T, color?: string };
+
export interface CheckboxListProps {
criteriaKey: string;
- list: { label: string, value: T }[];
+ list: CheckboxListItem[];
dispatch: Function;
group: TaggedPointGroup;
pointerType: PointerType;
diff --git a/frontend/farm_designer/point_groups/criteria/presets.tsx b/frontend/farm_designer/point_groups/criteria/presets.tsx
deleted file mode 100644
index 7b73580e6..000000000
--- a/frontend/farm_designer/point_groups/criteria/presets.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as React from "react";
-import { t } from "../../../i18next_wrapper";
-import {
- togglePointTypeCriteria,
- eqCriteriaSelected,
- hasSubCriteria,
- typeDisabled,
- PlantCriteria,
- PointCriteria,
- ToolCriteria,
-} from ".";
-import {
- CheckboxSelectionsProps,
- CheckboxSelectionsState,
- PointerType,
-} from "./interfaces";
-import { Checkbox } from "../../../ui";
-
-const CRITERIA_POINT_TYPES =
- (): { label: string, pointerType: PointerType }[] => [
- { label: t("Plants"), pointerType: "Plant" },
- { label: t("Points and Weeds"), pointerType: "GenericPointer" },
- { label: t("Slots"), pointerType: "ToolSlot" },
- ];
-
-export class CheckboxSelections extends React.Component
- > {
- state: CheckboxSelectionsState = {
- Plant: false, GenericPointer: false, ToolSlot: false, Weed: false
- };
-
- toggleMore = (section: keyof CheckboxSelectionsState) => () =>
- this.setState({ [section]: !this.state[section] });
-
- render() {
- const { group, dispatch, slugs } = this.props;
- const { criteria } = group.body;
- const selected = eqCriteriaSelected(criteria);
- return
- {CRITERIA_POINT_TYPES().map(({ label, pointerType }, index) => {
- const typeSelected = selected("pointer_type", pointerType);
- const partial = hasSubCriteria(criteria)(pointerType) && !typeSelected;
- return
-
-
- dispatch(togglePointTypeCriteria(group, pointerType))}
- checked={typeSelected}
- partial={partial}
- title={t(label)}
- disabled={typeDisabled(criteria, pointerType)}
- onClick={e => e.stopPropagation()} />
- {label}
-
-
- {this.state.Plant && pointerType == "Plant" &&
-
}
- {this.state.GenericPointer && pointerType == "GenericPointer" &&
-
}
- {this.state.ToolSlot && pointerType == "ToolSlot" &&
-
}
-
;
- })}
-
;
- }
-}
diff --git a/frontend/farm_designer/point_groups/criteria/selected.ts b/frontend/farm_designer/point_groups/criteria/selected.ts
index 6a0c84a49..cf92f3de1 100644
--- a/frontend/farm_designer/point_groups/criteria/selected.ts
+++ b/frontend/farm_designer/point_groups/criteria/selected.ts
@@ -37,10 +37,15 @@ export const criteriaHasKey = (
key: string,
) =>
some(categories.map(category => {
- if (category == "string_eq") {
- return strCriteriaHasKey(criteria.string_eq)(key);
- } else {
- return numCriteriaHasKey(criteria)(key);
+ switch (category) {
+ case "string_eq":
+ return strCriteriaHasKey(criteria.string_eq)(key);
+ case "number_eq":
+ return (criteria.number_eq[key]?.length || 0) > 0;
+ case "number_lt":
+ return isNumber(criteria.number_lt[key]);
+ case "number_gt":
+ return isNumber(criteria.number_gt[key]);
}
}));
@@ -52,9 +57,12 @@ export const hasSubCriteria = (criteria: PointGroupCriteria) =>
switch (pointerType) {
case "GenericPointer":
return !!(
- selected("meta.type")
+ selected("meta.color")
+ || numSelected("radius"));
+ case "Weed":
+ return !!(
+ selected("meta.created_by")
|| selected("meta.color")
- || selected("meta.created_by")
|| numSelected("radius"));
case "Plant":
return !!(
diff --git a/frontend/farm_designer/point_groups/criteria/show.tsx b/frontend/farm_designer/point_groups/criteria/show.tsx
index bb403735f..42f6140fc 100644
--- a/frontend/farm_designer/point_groups/criteria/show.tsx
+++ b/frontend/farm_designer/point_groups/criteria/show.tsx
@@ -1,20 +1,25 @@
import * as React from "react";
-import { Row, Col, FBSelect, DropDownItem } from "../../../ui";
+import { Row, Col, FBSelect, DropDownItem, Checkbox } from "../../../ui";
import {
AddEqCriteria, editCriteria, AddNumberCriteria,
editGtLtCriteriaField,
removeEqCriteriaValue,
clearCriteriaField,
+ dayCriteriaEmpty,
+ ClearCategory,
} from ".";
import {
- EqCriteriaSelectionProps, NumberCriteriaProps,
- CriteriaSelectionProps, LocationSelectionProps,
+ EqCriteriaSelectionProps,
+ NumberCriteriaProps,
+ LocationSelectionProps,
NumberLtGtInputProps,
PointGroupCriteria,
+ DaySelectionProps,
} from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { ToggleButton } from "../../../controls/toggle_button";
import { Actions } from "../../../constants";
+import { spaceSelected } from "../../map/layers/zones/zones";
/** Add and view string or number equal criteria. */
export class EqCriteriaSelection
@@ -39,7 +44,7 @@ export class EqCriteriaSelection
dispatch(removeEqCriteriaValue(
group, eqCriteria, criteriaKey, key, value))}>
@@ -73,7 +78,7 @@ export const NumberCriteriaSelection = (props: NumberCriteriaProps) => {
props.dispatch(clearCriteriaField(
- props.group, [props.criteriaKey], key))}>
+ props.group, [props.criteriaKey], [key]))}>
@@ -88,34 +93,59 @@ const DAY_OPERATOR_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
});
/** Edit and view day criteria. */
-export const DaySelection = (props: CriteriaSelectionProps) => {
- const { group, criteria, dispatch } = props;
+export const DaySelection = (props: DaySelectionProps) => {
+ const { group, criteria, dispatch, advanced } = props;
const dayCriteria = criteria.day;
+ const noDayCriteria = !advanced &&
+ dayCriteriaEmpty(dayCriteria) && !props.dayChanged;
return
-
{t("Age selection")}
+ {advanced
+ ?
{t("Age")}
+ :
{t("Age")}
}
+ {!advanced &&
+
+
{
+ dispatch(editCriteria(group, { day: { op: "<", days_ago: 0 } }));
+ props.changeDay(false);
+ }}
+ checked={noDayCriteria}
+ disabled={noDayCriteria}
+ title={t("clear age selection")}
+ customDisabledText={t("age selection empty")} />
+ {t("all")}
+ }
"]]}
- selectedItem={DAY_OPERATOR_DDI_LOOKUP()[dayCriteria.op]}
- onChange={ddi => dispatch(editCriteria(group, {
- day: {
- days_ago: dayCriteria.days_ago,
- op: ddi.value as PointGroupCriteria["day"]["op"]
- }
- }))} />
+ selectedItem={noDayCriteria
+ ? { label: t("Select one"), value: "" }
+ : DAY_OPERATOR_DDI_LOOKUP()[dayCriteria.op]}
+ onChange={ddi => {
+ dispatch(editCriteria(group, {
+ day: {
+ days_ago: dayCriteria.days_ago,
+ op: ddi.value as PointGroupCriteria["day"]["op"]
+ }
+ }));
+ props.changeDay(true);
+ }} />
- {
const { op } = dayCriteria;
const days_ago = parseInt(e.currentTarget.value);
dispatch(editCriteria(group, { day: { days_ago, op } }));
+ props.changeDay(true);
}} />
- {t("days old")}
+ {t("days old")}
;
@@ -160,7 +190,17 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
/** Form inputs to define a 2D group criteria area. */
export const LocationSelection = (props: LocationSelectionProps) =>
-
{t("Location selection")}
+
{t("Location")}
+
+ {!spaceSelected(props.group, props.botSize) &&
+
+
+
{t("Invalid selection.")}
+
}
{["x", "y"].map((axis: "x" | "y") =>
{
+ const { group, dispatch, disabled } = props;
+ const pointTypes = props.pointerTypes.sort();
+ if (pointTypes.length > 1 &&
+ !isEqual(pointTypes, ["GenericPointer", "Weed"])) {
+ return
;
+ }
+ switch (pointTypes[0]) {
+ case "Plant":
+ return ;
+ case "GenericPointer":
+ return ;
+ case "Weed":
+ return ;
+ case "ToolSlot":
+ return ;
+ default:
+ return
;
+ }
+};
/** "All" (any) checkbox to show or choose state of criteria subcategory. */
-const ClearCategory = (props: ClearCategoryProps) => {
- const { group, criteriaCategories, criteriaKey, dispatch } = props;
+export const ClearCategory = (props: ClearCategoryProps) => {
+ const { group, criteriaCategories, criteriaKeys, dispatch } = props;
const all =
- !criteriaHasKey(group.body.criteria, criteriaCategories, criteriaKey);
+ !some(criteriaKeys.map(criteriaKey =>
+ criteriaHasKey(group.body.criteria, criteriaCategories, criteriaKey)));
return
- dispatch(clearCriteriaField(group, criteriaCategories, criteriaKey))}
+ dispatch(clearCriteriaField(group, criteriaCategories, criteriaKeys))}
checked={all}
disabled={all}
title={t("clear selections")}
@@ -42,13 +76,14 @@ export const CheckboxList =
const selected = eqCriteriaSelected(criteria);
const toggle = toggleAndEditEqCriteria;
return
- {props.list.map(({ label, value }: { label: string, value: T }, index) =>
+ {props.list.map(({ label, value, color }: CheckboxListItem
, index) =>
props.dispatch(toggle(
props.group, props.criteriaKey, value, props.pointerType))}
checked={selected(props.criteriaKey, value)}
title={t(label)}
+ color={color}
disabled={props.disabled} />
{label}
)}
@@ -71,7 +106,7 @@ const PlantStage = (props: SubCriteriaProps) =>
disabled={props.disabled}
@@ -89,7 +124,7 @@ const PlantType = (props: PlantSubCriteriaProps) =>
disabled={props.disabled}
@@ -103,49 +138,40 @@ const PlantType = (props: PlantSubCriteriaProps) =>
({ label: capitalize(slug).replace("-", " "), value: slug }))} />
;
-/** Criteria specific to map points. */
-export const PointCriteria = (props: SubCriteriaProps) => {
+/** Criteria specific to weeds. */
+export const WeedCriteria = (props: SubCriteriaProps) => {
const { group, dispatch, disabled } = props;
- const commonProps = { group, dispatch, disabled };
- return
-
+ const pointerType: PointType = "Weed";
+ const commonProps = { group, dispatch, disabled, pointerType };
+ return
;
};
-const PointType = (props: SubCriteriaProps) =>
-
-
{t("Type")}
-
-
+/** Criteria specific to map points. */
+export const PointCriteria = (props: SubCriteriaProps) => {
+ const { group, dispatch, disabled } = props;
+ const pointerType: PointType = "GenericPointer";
+ const commonProps = { group, dispatch, disabled, pointerType };
+ return
+
+
;
+};
-const PointSource = (props: SubCriteriaProps) =>
+const PointSource = (props: PointSubCriteriaProps) =>
;
-const Radius = (props: SubCriteriaProps) =>
+const Radius = (props: PointSubCriteriaProps) =>
{t("Radius")}
inputWidth={3}
labelWidth={2}
group={props.group}
- pointerType={"GenericPointer"}
+ pointerType={props.pointerType}
dispatch={props.dispatch} />
;
-const Color = (props: SubCriteriaProps) =>
+const Color = (props: PointSubCriteriaProps) =>
;
@@ -217,7 +242,7 @@ const PulloutDirection = (props: SubCriteriaProps) =>
disabled={props.disabled}
diff --git a/frontend/farm_designer/point_groups/group_detail.tsx b/frontend/farm_designer/point_groups/group_detail.tsx
index 4ea507907..ad91e1a5e 100644
--- a/frontend/farm_designer/point_groups/group_detail.tsx
+++ b/frontend/farm_designer/point_groups/group_detail.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
import { connect } from "react-redux";
import { Everything } from "../../interfaces";
-import { TaggedPointGroup, TaggedPoint } from "farmbot";
+import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot";
import {
selectAllActivePoints, selectAllPlantPointers, selectAllPointGroups,
} from "../../resources/selectors";
@@ -16,6 +16,8 @@ import {
} from "../designer_panel";
import { Panel } from "../panel_header";
import { t } from "../../i18next_wrapper";
+import { BotSize } from "../map/interfaces";
+import { botSize } from "../state_to_props";
interface GroupDetailProps {
dispatch: Function;
@@ -25,6 +27,8 @@ interface GroupDetailProps {
slugs: string[];
hovered: UUID | undefined;
editGroupAreaInMap: boolean;
+ botSize: BotSize;
+ selectionPointType: PointType[] | undefined;
}
/** Find a group from a URL-provided ID. */
@@ -36,7 +40,7 @@ export const findGroupFromUrl = (groups: TaggedPointGroup[]) => {
};
function mapStateToProps(props: Everything): GroupDetailProps {
- const { hoveredPlantListItem, editGroupAreaInMap } =
+ const { hoveredPlantListItem, editGroupAreaInMap, selectionPointType } =
props.resources.consumers.farm_designer;
return {
allPoints: selectAllActivePoints(props.resources.index),
@@ -47,6 +51,8 @@ function mapStateToProps(props: Everything): GroupDetailProps {
.map(p => p.body.openfarm_slug)),
hovered: hoveredPlantListItem,
editGroupAreaInMap,
+ botSize: botSize(props),
+ selectionPointType,
};
}
diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx
index 3818cc2a6..24d67dedc 100644
--- a/frontend/farm_designer/point_groups/group_detail_active.tsx
+++ b/frontend/farm_designer/point_groups/group_detail_active.tsx
@@ -1,20 +1,19 @@
import * as React from "react";
import { t } from "../../i18next_wrapper";
-import { TaggedPointGroup, TaggedPoint } from "farmbot";
+import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot";
import { DeleteButton } from "../../ui/delete_button";
import { save, edit } from "../../api/crud";
-import { sortGroupBy } from "./point_group_sort";
-import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
-import { PointGroupItem } from "./point_group_item";
import { Paths } from "./paths";
import { Feature, ShouldDisplay } from "../../devices/interfaces";
import { ErrorBoundary } from "../../error_boundary";
import {
GroupCriteria, GroupPointCountBreakdown, pointsSelectedByGroup,
} from "./criteria";
-import { Content } from "../../constants";
+import { ToolTips } from "../../constants";
import { UUID } from "../../resources/interfaces";
import { Help } from "../../ui";
+import { BotSize } from "../map/interfaces";
+import { setSelectionPointType } from "../plants/select_plants";
export interface GroupDetailActiveProps {
dispatch: Function;
@@ -24,10 +23,11 @@ export interface GroupDetailActiveProps {
slugs: string[];
hovered: UUID | undefined;
editGroupAreaInMap: boolean;
+ botSize: BotSize;
+ selectionPointType: PointType[] | undefined;
}
interface GroupDetailActiveState {
- timerId?: ReturnType;
iconDisplay: boolean;
}
@@ -35,51 +35,12 @@ export class GroupDetailActive
extends React.Component {
state: GroupDetailActiveState = { iconDisplay: true };
- update = ({ currentTarget }: React.SyntheticEvent) => {
- this.props.dispatch(edit(this.props.group, { name: currentTarget.value }));
- };
-
get pointsSelectedByGroup() {
return pointsSelectedByGroup(this.props.group, this.props.allPoints);
}
- get icons() {
- const sortedPoints =
- sortGroupBy(this.props.group.body.sort_type, this.pointsSelectedByGroup);
- return sortedPoints.map(point => {
- return ;
- });
- }
-
- get saved(): boolean {
- return !this.props.group.specialStatus;
- }
-
- saveGroup = () => {
- if (!this.saved) {
- this.props.dispatch(save(this.props.group.uuid));
- }
- }
-
- changeSortType = (sort_type: PointGroupSortType) => {
- const { dispatch, group } = this.props;
- dispatch(edit(group, { sort_type }));
- }
-
- componentDidMount() {
- // There are better ways to do this.
- this.setState({ timerId: setInterval(this.saveGroup, 900) });
- }
-
- componentWillUnmount() {
- const { timerId } = this.state;
- (typeof timerId == "number") && clearInterval(timerId);
- }
+ componentWillUnmount = () =>
+ this.props.dispatch(setSelectionPointType(undefined));
toggleIconShow = () => this.setState({ iconDisplay: !this.state.iconDisplay });
@@ -87,24 +48,29 @@ export class GroupDetailActive
const { group, dispatch } = this.props;
return
{t("GROUP NAME")}
- {this.saved ? "" : " saving..."}
+ onBlur={e => {
+ const newGroupName = e.currentTarget.value;
+ if (newGroupName != "" && newGroupName != this.props.group.body.name) {
+ this.props.dispatch(edit(this.props.group, { name: newGroupName }));
+ this.props.dispatch(save(this.props.group.uuid));
+ }
+ }} />
{this.props.shouldDisplay(Feature.criteria_groups) &&
}
+ group={group} slugs={this.props.slugs} botSize={this.props.botSize}
+ editGroupAreaInMap={this.props.editGroupAreaInMap}
+ selectionPointType={this.props.selectionPointType} />}
{props.group.body.sort_type == "random" &&
}
-
+const GroupMemberDisplay = (props: GroupMemberDisplayProps) => {
+ return
- {t("GROUP MEMBERS ({{count}})", { count: props.icons.length })}
+ {t("GROUP MEMBERS ({{count}})", {
+ count: props.pointsSelectedByGroup.length
+ })}
+ ? t(ToolTips.CRITERIA_SELECTION_COUNT) : ""}`} />
- {props.shouldDisplay(Feature.criteria_groups) &&
-
}
- {props.iconDisplay &&
-
- {props.icons}
-
}
+
;
+};
diff --git a/frontend/farm_designer/point_groups/group_list_panel.tsx b/frontend/farm_designer/point_groups/group_list_panel.tsx
index a1e93035f..ce79dea79 100644
--- a/frontend/farm_designer/point_groups/group_list_panel.tsx
+++ b/frontend/farm_designer/point_groups/group_list_panel.tsx
@@ -10,9 +10,12 @@ import { findAll } from "../../resources/find_all";
import { TaggedPointGroup, TaggedPoint } from "farmbot";
import { history } from "../../history";
import { GroupInventoryItem } from "./group_inventory_item";
-import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
+import {
+ EmptyStateWrapper, EmptyStateGraphic,
+} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import { selectAllActivePoints } from "../../resources/selectors";
+import { createGroup } from "./actions";
export interface GroupListPanelProps {
dispatch: Function;
@@ -32,8 +35,8 @@ export function mapStateToProps(props: Everything): GroupListPanelProps {
};
}
-export class RawGroupListPanel extends React.Component
{
-
+export class RawGroupListPanel
+ extends React.Component {
state: State = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent) => {
@@ -47,7 +50,7 @@ export class RawGroupListPanel extends React.Component
this.props.dispatch(createGroup({ pointUuids: [] }))}
title={t("Add group")}>
{
dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })}
onMouseLeave={() =>
dispatch({ type: Actions.TRY_SORT_TYPE, payload: undefined })}
- onClick={() =>
- sortTypeKey == "nn"
- ? error(t("Not supported yet."))
- : dispatch(edit(group, { sort_type: sortTypeKey }))}>
+ onClick={() => {
+ if (sortTypeKey == "nn") {
+ error(t("Not supported yet."));
+ } else {
+ dispatch(edit(group, { sort_type: sortTypeKey }));
+ dispatch(save(group.uuid));
+ }
+ }}>
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
diff --git a/frontend/farm_designer/point_groups/point_group_item.tsx b/frontend/farm_designer/point_groups/point_group_item.tsx
index f242a420e..a3444f6ad 100644
--- a/frontend/farm_designer/point_groups/point_group_item.tsx
+++ b/frontend/farm_designer/point_groups/point_group_item.tsx
@@ -3,10 +3,11 @@ import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { maybeGetCachedPlantIcon } from "../../open_farm/cached_crop";
import { setHoveredPlant } from "../map/actions";
import { TaggedPointGroup, uuid, TaggedPoint } from "farmbot";
-import { overwrite } from "../../api/crud";
import { error } from "../../toast/toast";
import { t } from "../../i18next_wrapper";
import { DEFAULT_WEED_ICON } from "../map/layers/weeds/garden_weed";
+import { uniq } from "lodash";
+import { overwriteGroup } from "./actions";
export interface PointGroupItemProps {
point: TaggedPoint;
@@ -17,12 +18,13 @@ export interface PointGroupItemProps {
interface PointGroupItemState { icon: string; }
-const removePoint = (group: TaggedPointGroup, pointId: number) => {
- type Body = (typeof group)["body"];
- const nextGroup: Body = { ...group.body };
- nextGroup.point_ids = nextGroup.point_ids.filter(x => x !== pointId);
- return overwrite(group, nextGroup);
-};
+const removePoint = (group: TaggedPointGroup, pointId: number) =>
+ (dispatch: Function) => {
+ type Body = (typeof group)["body"];
+ const nextGroup: Body = { ...group.body };
+ nextGroup.point_ids = uniq(nextGroup.point_ids.filter(x => x !== pointId));
+ dispatch(overwriteGroup(group, nextGroup));
+ };
export const genericPointIcon = (color: string | undefined) =>
`
{
if (this.criteriaIcon) {
- return error(t("Cannot remove points selected by criteria."));
+ return error(t("Cannot remove points selected by filters."));
}
this.props.dispatch(
removePoint(this.props.group, this.props.point.body.id || 0));
@@ -115,11 +117,7 @@ export class PointGroupItem
width={32}
height={32} />}
", () => {
it("renders for points", () => {
mockPath = "/app/designer";
const wrapper = mount( );
- ["add point", "delete", "x", "y", "radius", "color"]
+ ["add point", "delete", "x", "y", "radius"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});
it("renders for weeds", () => {
mockPath = "/app/designer/weeds/add";
const wrapper = mount( );
- ["add weed", "delete", "x", "y", "radius", "color"]
+ ["add weed", "delete", "x", "y", "radius"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});
diff --git a/frontend/farm_designer/points/__tests__/point_edit_actions_test.tsx b/frontend/farm_designer/points/__tests__/point_edit_actions_test.tsx
index d234109d0..b06c3e1a6 100644
--- a/frontend/farm_designer/points/__tests__/point_edit_actions_test.tsx
+++ b/frontend/farm_designer/points/__tests__/point_edit_actions_test.tsx
@@ -4,11 +4,14 @@ jest.mock("../../../api/crud", () => ({
}));
import * as React from "react";
-import { shallow } from "enzyme";
+import { shallow, mount } from "enzyme";
import {
EditPointLocation, EditPointLocationProps,
EditPointRadius, EditPointRadiusProps,
- EditPointColor, EditPointColorProps, updatePoint, EditPointName, EditPointNameProps,
+ EditPointColor, EditPointColorProps, updatePoint, EditPointName,
+ EditPointNameProps,
+ AdditionalWeedProperties,
+ EditPointPropertiesProps,
} from "../point_edit_actions";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { edit, save } from "../../../api/crud";
@@ -89,3 +92,30 @@ describe(" ", () => {
expect(p.updatePoint).toHaveBeenCalledWith({ meta: { color: "blue" } });
});
});
+
+describe(" ", () => {
+ const fakeProps = (): EditPointPropertiesProps => ({
+ point: fakePoint(),
+ updatePoint: jest.fn(),
+ });
+
+ it("renders unknown source", () => {
+ const p = fakeProps();
+ p.point.body.meta = {
+ meta_key: "meta value", created_by: undefined, key: undefined
+ };
+ const wrapper = mount( );
+ expect(wrapper.text()).toContain("unknown");
+ expect(wrapper.text()).toContain("meta value");
+ });
+
+ it("changes method", () => {
+ const p = fakeProps();
+ p.point.body.meta = { removal_method: "automatic" };
+ const wrapper = shallow( );
+ wrapper.find("input").last().simulate("change");
+ expect(p.updatePoint).toHaveBeenCalledWith({
+ meta: { removal_method: "manual" }
+ });
+ });
+});
diff --git a/frontend/farm_designer/points/__tests__/point_info_test.tsx b/frontend/farm_designer/points/__tests__/point_info_test.tsx
index 3cae6e321..f43785201 100644
--- a/frontend/farm_designer/points/__tests__/point_info_test.tsx
+++ b/frontend/farm_designer/points/__tests__/point_info_test.tsx
@@ -45,8 +45,24 @@ describe(" ", () => {
it("renders with points", () => {
mockPath = "/app/designer/points/1";
- const wrapper = mount( );
+ const p = fakeProps();
+ const point = fakePoint();
+ point.body.meta = { meta_key: "meta value" };
+ p.findPoint = () => point;
+ const wrapper = mount( );
expect(wrapper.text()).toContain("Edit point");
+ expect(wrapper.text()).toContain("meta value");
+ });
+
+ it("doesn't render duplicate values", () => {
+ mockPath = "/app/designer/points/1";
+ const p = fakeProps();
+ const point = fakePoint();
+ point.body.meta = { color: "red", meta_key: undefined };
+ p.findPoint = () => point;
+ const wrapper = mount( );
+ expect(wrapper.text()).toContain("Edit point");
+ expect(wrapper.text()).not.toContain("red");
});
it("moves the device to a particular point", () => {
diff --git a/frontend/farm_designer/points/create_points.tsx b/frontend/farm_designer/points/create_points.tsx
index 62d3a0083..417b9ed1d 100644
--- a/frontend/farm_designer/points/create_points.tsx
+++ b/frontend/farm_designer/points/create_points.tsx
@@ -196,14 +196,25 @@ export class RawCreatePoints
PointProperties = () =>
-
- {t("Name")}
-
-
+
+
+
+ {t("Name")}
+
+
+
+
+
+
+
+
+
@@ -238,13 +249,6 @@ export class RawCreatePoints
-
-
-
-
-
PointActions = () =>
diff --git a/frontend/farm_designer/points/point_edit_actions.tsx b/frontend/farm_designer/points/point_edit_actions.tsx
index a0cec53b3..d36324493 100644
--- a/frontend/farm_designer/points/point_edit_actions.tsx
+++ b/frontend/farm_designer/points/point_edit_actions.tsx
@@ -5,10 +5,11 @@ import { destroy, edit, save } from "../../api/crud";
import { ResourceColor } from "../../interfaces";
import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot";
import { ListItem } from "../plants/plant_panel";
-import { round } from "lodash";
+import { round, cloneDeep } from "lodash";
import { Row, Col, BlurableInput, ColorPicker } from "../../ui";
import { parseIntInput } from "../../util";
import { UUID } from "../../resources/interfaces";
+import { plantAge } from "../plants/map_state_to_props";
type PointUpdate =
Partial;
@@ -52,6 +53,51 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
;
+export const AdditionalWeedProperties = (props: EditPointPropertiesProps) =>
+
+
+ {`${plantAge(props.point)} ${t("days old")}`}
+
+ {Object.entries(props.point.body.meta).map(([key, value]) => {
+ switch (key) {
+ case "color":
+ case "type": return
;
+ case "created_by":
+ return
+ {SOURCE_LOOKUP()[value || ""] || t("unknown")}
+ ;
+ case "removal_method":
+ return
+
+ {REMOVAL_METHODS.map(method =>
+
+ {
+ const newMeta = cloneDeep(props.point.body.meta);
+ newMeta.removal_method = method;
+ props.updatePoint({ meta: newMeta });
+ }} />
+ {t(method)}
+
)}
+
+ ;
+ default:
+ return
+ {value || ""}
+ ;
+ }
+ })}
+ ;
+
+const REMOVAL_METHODS = ["automatic", "manual"];
+
+const SOURCE_LOOKUP = (): Record => ({
+ "plant-detection": t("Weed Detector"),
+ "farm-designer": t("Farm Designer"),
+});
+
export interface PointActionsProps {
x: number;
y: number;
diff --git a/frontend/farm_designer/points/point_info.tsx b/frontend/farm_designer/points/point_info.tsx
index f30f08c8c..f71ec2b99 100644
--- a/frontend/farm_designer/points/point_info.tsx
+++ b/frontend/farm_designer/points/point_info.tsx
@@ -13,6 +13,7 @@ import { Actions } from "../../constants";
import {
EditPointProperties, updatePoint, PointActions,
} from "./point_edit_actions";
+import { ListItem } from "../plants/plant_panel";
export interface EditPointProps {
dispatch: Function;
@@ -50,6 +51,21 @@ export class RawEditPoint extends React.Component {
?
+
+ {Object.entries(this.point.body.meta).map(([key, value]) => {
+ switch (key) {
+ case "color":
+ case "created_by":
+ case "type":
+ return
;
+ default:
+ return
+ {value || ""}
+ ;
+ }
+ })}
+
(uuid: string | undefined): TaggedPlant =>
@@ -62,12 +64,6 @@ export function mapStateToProps(props: Everything): Props {
const { mcu_params } = props.bot.hardware;
const firmwareSettings = fwConfig || mcu_params;
- const fw = firmwareSettings;
- const stepsPerMmXY = {
- x: calcMicrostepsPerMm(fw.movement_step_per_mm_x, fw.movement_microsteps_x),
- y: calcMicrostepsPerMm(fw.movement_step_per_mm_y, fw.movement_microsteps_y),
- };
-
const mountedToolId =
getDeviceAccountSettings(props.resources.index).body.mounted_tool_id;
const mountedToolName =
@@ -122,7 +118,7 @@ export function mapStateToProps(props: Everything): Props {
plants,
botLocationData: validBotLocationData(props.bot.hardware.location_data),
botMcuParams: firmwareSettings,
- stepsPerMmXY,
+ botSize: botSize(props),
peripherals,
eStopStatus: props.bot.hardware.informational_settings.locked,
latestImages,
@@ -136,3 +132,19 @@ export function mapStateToProps(props: Everything): Props {
mountedToolName,
};
}
+
+export const botSize = (props: Everything): BotSize => {
+ const getConfigValue = getWebAppConfigValue(() => props);
+ const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
+ const { mcu_params } = props.bot.hardware;
+ const firmwareSettings = fwConfig || mcu_params;
+ const fw = firmwareSettings;
+ const stepsPerMmXY = {
+ x: calcMicrostepsPerMm(fw.movement_step_per_mm_x, fw.movement_microsteps_x),
+ y: calcMicrostepsPerMm(fw.movement_step_per_mm_y, fw.movement_microsteps_y),
+ };
+ return getBotSize(
+ firmwareSettings,
+ stepsPerMmXY,
+ getDefaultAxisLength(getConfigValue));
+};
diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx
index 6451ce41a..f1e52cc35 100644
--- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx
+++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx
@@ -17,6 +17,7 @@ import { initSave, init, destroy } from "../../../api/crud";
import { history } from "../../../history";
import { FirmwareHardware } from "farmbot";
import { AddToolProps } from "../interfaces";
+import { mockDispatch } from "../../../__test_support__/fake_dispatch";
describe(" ", () => {
const fakeProps = (): AddToolProps => ({
@@ -58,7 +59,7 @@ describe(" ", () => {
it("saves", async () => {
mockSave = () => Promise.resolve();
const p = fakeProps();
- p.dispatch = jest.fn(x => typeof x === "function" && x());
+ p.dispatch = mockDispatch();
const wrapper = shallow( );
wrapper.setState({ toolName: "Foo" });
await wrapper.find(SaveBtn).simulate("click");
@@ -70,7 +71,7 @@ describe(" ", () => {
it("removes unsaved tool on exit", async () => {
mockSave = () => Promise.reject();
const p = fakeProps();
- p.dispatch = jest.fn(x => typeof x === "function" && x());
+ p.dispatch = mockDispatch();
const wrapper = shallow( );
wrapper.setState({ toolName: "Foo" });
await wrapper.find(SaveBtn).simulate("click");
diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx
index 9d5c93c39..6085f88e6 100644
--- a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx
+++ b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx
@@ -42,10 +42,12 @@ describe(" ", () => {
it("renders", () => {
const p = fakeProps();
- p.findToolSlot = () => fakeToolSlot();
+ const toolSlot = fakeToolSlot();
+ toolSlot.body.meta = { meta_key: "meta value" };
+ p.findToolSlot = () => toolSlot;
const wrapper = mount( );
["edit slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container",
- "direction", "gantry-mounted",
+ "direction", "gantry-mounted", "meta value",
].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});
diff --git a/frontend/farm_designer/tools/edit_tool_slot.tsx b/frontend/farm_designer/tools/edit_tool_slot.tsx
index 29772b1c4..828796e49 100644
--- a/frontend/farm_designer/tools/edit_tool_slot.tsx
+++ b/frontend/farm_designer/tools/edit_tool_slot.tsx
@@ -52,6 +52,17 @@ export class RawEditToolSlot extends React.Component {
quadrant={this.props.quadrant}
isActive={this.props.isActive}
updateToolSlot={this.updateSlot(toolSlot)} />
+
+ {Object.entries(toolSlot.body.meta).map(([key, value]) => {
+ switch (key) {
+ default:
+ return
+ {key}
+ {value}
+ ;
+ }
+ })}
+
{
?
+
", () => {
const fakeProps = (): EditZoneProps => ({
dispatch: jest.fn(),
findZone: () => undefined,
+ botSize: {
+ x: { value: 3000, isDefault: true },
+ y: { value: 1500, isDefault: true },
+ },
});
it("redirects", () => {
diff --git a/frontend/farm_designer/zones/edit_zone.tsx b/frontend/farm_designer/zones/edit_zone.tsx
index 33a1e84a7..e762f2ac0 100644
--- a/frontend/farm_designer/zones/edit_zone.tsx
+++ b/frontend/farm_designer/zones/edit_zone.tsx
@@ -11,16 +11,20 @@ import { selectAllPointGroups } from "../../resources/selectors";
import { TaggedPointGroup } from "farmbot";
import { edit, save } from "../../api/crud";
import { LocationSelection } from "../point_groups/criteria";
+import { BotSize } from "../map/interfaces";
+import { botSize } from "../state_to_props";
export interface EditZoneProps {
dispatch: Function;
findZone(id: number): TaggedPointGroup | undefined;
+ botSize: BotSize;
}
export const mapStateToProps = (props: Everything): EditZoneProps => ({
dispatch: props.dispatch,
findZone: id => selectAllPointGroups(props.resources.index)
.filter(g => g.body.id == id)[0],
+ botSize: botSize(props),
});
export class RawEditZone extends React.Component
{
@@ -54,6 +58,7 @@ export class RawEditZone extends React.Component {
group={zone}
criteria={zone.body.criteria}
dispatch={this.props.dispatch}
+ botSize={this.props.botSize}
editGroupAreaInMap={true} />
: {t("Redirecting")}... }
diff --git a/frontend/sequences/step_tiles/__tests__/tile_set_servo_angle_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_set_servo_angle_test.tsx
index bcd5f8d7c..7532a1543 100644
--- a/frontend/sequences/step_tiles/__tests__/tile_set_servo_angle_test.tsx
+++ b/frontend/sequences/step_tiles/__tests__/tile_set_servo_angle_test.tsx
@@ -10,6 +10,7 @@ import { SetServoAngle } from "farmbot";
import { emptyState } from "../../../resources/reducer";
import { StepParams } from "../../interfaces";
import { editStep } from "../../../api/crud";
+import { mockDispatch } from "../../../__test_support__/fake_dispatch";
describe(" ", () => {
const currentStep: SetServoAngle = {
@@ -23,7 +24,7 @@ describe(" ", () => {
const fakeProps = (): StepParams => ({
currentSequence: fakeSequence(),
currentStep: currentStep,
- dispatch: jest.fn((fn: Function) => typeof fn === "function" && fn()),
+ dispatch: mockDispatch(),
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
diff --git a/frontend/ui/checkbox.tsx b/frontend/ui/checkbox.tsx
index 70c146d92..e1f9b1701 100644
--- a/frontend/ui/checkbox.tsx
+++ b/frontend/ui/checkbox.tsx
@@ -9,6 +9,7 @@ interface CheckboxProps {
partial?: boolean;
onClick?: (e: React.FormEvent) => void;
customDisabledText?: string;
+ color?: string;
}
export const Checkbox = (props: CheckboxProps) =>
@@ -19,6 +20,7 @@ export const Checkbox = (props: CheckboxProps) =>
title={props.disabled ? props.customDisabledText ?? t("incompatible") : ""}
onClick={props.onClick}>
diff --git a/frontend/ui/help.tsx b/frontend/ui/help.tsx
index d697c4529..fb44fd7ee 100644
--- a/frontend/ui/help.tsx
+++ b/frontend/ui/help.tsx
@@ -9,6 +9,7 @@ interface HelpProps {
onHover?: boolean;
position?: PopoverPosition;
customIcon?: string;
+ customClass?: string;
}
export function Help(props: HelpProps) {
@@ -17,6 +18,7 @@ export function Help(props: HelpProps) {
interactionKind={props.onHover
? PopoverInteractionKind.HOVER
: PopoverInteractionKind.CLICK}
+ className={props.customClass}
popoverClassName={"help"}>
{t(props.text)}