commit
73422eb8ea
|
@ -0,0 +1,2 @@
|
|||
export const mockDispatch = (innerDispatch = jest.fn()) =>
|
||||
jest.fn(x => typeof x === "function" && x(innerDispatch));
|
|
@ -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`,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1424,6 +1424,11 @@ ul {
|
|||
button {
|
||||
float: none !important;
|
||||
}
|
||||
.bp3-popover-wrapper {
|
||||
display: inline;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.problem-alert {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <div className="firmware-hardware-status-details">
|
||||
<label>{t("Web App")}</label>
|
||||
<Help text={ToolTips.FIRMWARE_VALUE_API} />
|
||||
<p>{lookup(props.apiFirmwareValue) || t("unknown")}</p>
|
||||
<label>{t("FarmBot OS")}</label>
|
||||
<Help text={ToolTips.FIRMWARE_VALUE_FBOS} />
|
||||
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
|
||||
<label>{t("Arduino/Farmduino")}</label>
|
||||
<Help text={ToolTips.FIRMWARE_VALUE_MCU} />
|
||||
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
|
||||
<FirmwareAlerts
|
||||
alerts={props.alerts}
|
||||
|
|
|
@ -48,7 +48,10 @@ describe("<FarmDesigner/>", () => {
|
|||
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: [],
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Props, Partial<State>> {
|
|||
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<Props, Partial<State>> {
|
|||
dispatch={this.props.dispatch}
|
||||
timeSettings={this.props.timeSettings}
|
||||
getConfigValue={this.props.getConfigValue}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
imageAgeInfo={imageAgeInfo} />
|
||||
|
||||
<DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
|
||||
|
@ -200,12 +195,12 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
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}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -456,6 +456,16 @@ describe("<GardenMap/>", () => {
|
|||
expect(allowed).toBeTruthy();
|
||||
});
|
||||
|
||||
it("allows interactions: group edit", () => {
|
||||
mockMode = Mode.editGroup;
|
||||
mockInteractionAllow = true;
|
||||
const p = fakeProps();
|
||||
p.designer.selectionPointType = undefined;
|
||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||
const allowed = wrapper.instance().interactions("Plant");
|
||||
expect(allowed).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disallows interactions: default", () => {
|
||||
mockMode = Mode.none;
|
||||
mockInteractionAllow = false;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<GardenMapProps, Partial<GardenMapState>> {
|
||||
|
@ -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<HTMLElement> | React.MouseEvent<SVGElement>):
|
||||
|
@ -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);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
import {
|
||||
fakeMapTransformProps,
|
||||
} from "../../../../../__test_support__/map_transform_props";
|
||||
import { ReactWrapper } from "enzyme";
|
||||
|
||||
describe("<ZonesLayer />", () => {
|
||||
const fakeProps = (): ZonesLayerProps => ({
|
||||
|
@ -26,6 +27,27 @@ describe("<ZonesLayer />", () => {
|
|||
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(
|
||||
"<g id=\"zones-2D-1\" class=\"current\"></g>");
|
||||
};
|
||||
|
||||
it("renders current group's zones: 2D", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
|
@ -38,6 +60,7 @@ describe("<ZonesLayer />", () => {
|
|||
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("<ZonesLayer />", () => {
|
|||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
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(<ZonesLayer {...p} />);
|
||||
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("<ZonesLayer />", () => {
|
|||
p.currentGroup = p.groups[0].uuid;
|
||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||
expect(wrapper.html()).toEqual(
|
||||
"<svg><g class=\"zones-layer\" style=\"cursor: pointer;\"></g></svg>");
|
||||
`<svg>
|
||||
<g class=\"zones-layer\" style=\"cursor: pointer;\">
|
||||
<g id=\"zones-2D-1\" class=\"current\">
|
||||
</g>
|
||||
</g>
|
||||
</svg>`.replace(/[ ]{2,}/g, "").replace(/[^\S ]/g, ""));
|
||||
});
|
||||
|
||||
it("doesn't render current group's zones", () => {
|
||||
|
|
|
@ -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("<Zones0D />", () => {
|
|||
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(<Zones0D {...p} />);
|
||||
wrapper.find("#zones-0D-1").simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Zones1D />", () => {
|
||||
|
@ -104,6 +116,15 @@ describe("<Zones1D />", () => {
|
|||
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(<Zones1D {...p} />);
|
||||
wrapper.find("#zones-1D-1").simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Zones2D />", () => {
|
||||
|
@ -137,6 +158,16 @@ describe("<Zones2D />", () => {
|
|||
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(<Zones2D {...p} />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<SVGElement> = not2D ? {
|
||||
stroke: current ? "white" : "black",
|
||||
strokeWidth: 4,
|
||||
strokeDasharray: 15,
|
||||
fill: "none",
|
||||
} : {};
|
||||
const { id } = props.group.body;
|
||||
return <g id={`zones-2D-${id}`} onClick={openGroup(id)}
|
||||
className={current ? "current" : ""}>
|
||||
{!zone.selectsAll &&
|
||||
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height} />}
|
||||
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height}
|
||||
stroke={rectProps.stroke}
|
||||
strokeWidth={rectProps.strokeWidth}
|
||||
strokeDasharray={rectProps.strokeDasharray}
|
||||
fill={rectProps.fill} />}
|
||||
</g>;
|
||||
};
|
||||
|
||||
/** 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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 &&
|
||||
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
||||
{groups.map(group => visible(group) &&
|
||||
getZoneType(group) === ZoneType.lines &&
|
||||
|
|
|
@ -46,6 +46,7 @@ describe("<GardenMapLegend />", () => {
|
|||
timeSettings: fakeTimeSettings(),
|
||||
getConfigValue: jest.fn(),
|
||||
imageAgeInfo: { newestDate: "", toOldest: 1 },
|
||||
shouldDisplay: () => true,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
|
|
@ -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) &&
|
||||
<LayerToggle
|
||||
value={props.showZones}
|
||||
label={t("areas?")}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { mapStateToProps, plantAge } from "../map_state_to_props";
|
||||
import { mapStateToProps, plantAge, formatPlantInfo } from "../map_state_to_props";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
buildResourceIndex,
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
fakePlant, fakePlantTemplate,
|
||||
fakePlant, fakePlantTemplate, fakeWebAppConfig,
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,9 +48,12 @@ describe("<PlantPanel/>", () => {
|
|||
|
||||
it("renders: editing", () => {
|
||||
const p = fakeProps();
|
||||
p.info.meta = { meta_key: "meta value", gridId: "1", key: undefined };
|
||||
const wrapper = mount(<PlantPanel {...p} />);
|
||||
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("<PlantPanel/>", () => {
|
|||
|
||||
it("calls destroy", () => {
|
||||
const p = fakeProps();
|
||||
p.info.meta = undefined;
|
||||
const wrapper = mount(<PlantPanel {...p} />);
|
||||
clickButton(wrapper, 2, "Delete");
|
||||
expect(p.onDestroy).toHaveBeenCalledWith("Plant.0.0");
|
||||
|
|
|
@ -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("<SelectPlants />", () => {
|
||||
beforeEach(function () {
|
||||
|
@ -85,6 +90,20 @@ describe("<SelectPlants />", () => {
|
|||
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(<SelectPlants {...p} />);
|
||||
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("<SelectPlants />", () => {
|
|||
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(<SelectPlants {...p} />);
|
||||
wrapper.unmount();
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||
payload: undefined,
|
||||
});
|
||||
|
@ -148,13 +169,29 @@ describe("<SelectPlants />", () => {
|
|||
|
||||
it("changes selection type", () => {
|
||||
const p = fakeProps();
|
||||
const dispatch = jest.fn();
|
||||
p.dispatch = mockDispatch(dispatch);
|
||||
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
|
||||
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
|
||||
actionsWrapper.find("FBSelect").first().simulate("change",
|
||||
{ label: "", value: "All" });
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
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<SelectPlants>(<SelectPlants {...p} />);
|
||||
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("<SelectPlants />", () => {
|
|||
const wrapper = mount(<SelectPlants {...p} />);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<string, string | undefined>;
|
||||
}
|
||||
|
||||
/** 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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -186,6 +186,14 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
updatePlant={updatePlant} />
|
||||
: t(startCase(plantStatus))}
|
||||
</ListItem>
|
||||
{Object.entries(info.meta || []).map(([key, value]) => {
|
||||
switch (key) {
|
||||
case "gridId":
|
||||
return <div key={key} className={`meta-${key}-not-displayed`} />;
|
||||
default:
|
||||
return <ListItem key={key} name={key}>{value || ""}</ListItem>;
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
<DeleteButtons destroy={destroy} />
|
||||
</DesignerPanelContent>;
|
||||
|
|
|
@ -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<SelectPlantsProps, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
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<SelectPlantsProps, {}> {
|
|||
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])));
|
||||
}} />
|
||||
<div className="button-row">
|
||||
<button className="fb-button gray"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
jest.mock("../../../api/crud", () => ({
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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("<GroupDetailActive/>", () => {
|
||||
const fakeProps = (): GroupDetailActiveProps => {
|
||||
|
@ -35,26 +42,14 @@ describe("<GroupDetailActive/>", () => {
|
|||
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<GroupDetailActive>(<GroupDetailActive {...p} />);
|
||||
|
@ -68,54 +63,24 @@ describe("<GroupDetailActive/>", () => {
|
|||
p.group.specialStatus = SpecialStatus.SAVED;
|
||||
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||
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(<GroupDetailActive {...p} />);
|
||||
expect(wrapper.text()).toContain("saving");
|
||||
});
|
||||
|
||||
it("changes group name", () => {
|
||||
const NEW_NAME = "new group name";
|
||||
const wrapper = shallow(<GroupDetailActive {...fakeProps()} />);
|
||||
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(<GroupDetailActive {...p} />);
|
||||
wrapper.unmount();
|
||||
expect(setSelectionPointType).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("changes group name", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GroupDetailActive {...p} />);
|
||||
wrapper.find("input").first().simulate("blur", {
|
||||
currentTarget: { value: "new group name" }
|
||||
});
|
||||
expect(edit).toHaveBeenCalledWith(p.group, { name: "new group name" });
|
||||
});
|
||||
|
||||
it("shows paths", () => {
|
||||
|
|
|
@ -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("<GroupListPanel />", () => {
|
||||
const fakeProps = (): GroupListPanelProps => {
|
||||
|
@ -39,6 +45,13 @@ describe("<GroupListPanel />", () => {
|
|||
};
|
||||
};
|
||||
|
||||
it("creates new group", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GroupListPanel {...p} />);
|
||||
wrapper.find(DesignerPanelTop).simulate("click");
|
||||
expect(createGroup).toHaveBeenCalledWith({ pointUuids: [] });
|
||||
});
|
||||
|
||||
it("changes search term", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<GroupListPanel>(<GroupListPanel {...p} />);
|
||||
|
@ -66,7 +79,9 @@ describe("<GroupListPanel />", () => {
|
|||
const wrapper = mount(<GroupListPanel {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("no groups yet");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("maps state to props", () => {
|
||||
const state = fakeState();
|
||||
const group = fakePointGroup();
|
||||
|
|
|
@ -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", () => ({
|
||||
|
|
|
@ -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("<PointGroupItem/>", () => {
|
||||
const fakeProps = (): PointGroupItemProps => ({
|
||||
dispatch: jest.fn(),
|
||||
dispatch: mockDispatch(),
|
||||
point: fakePlant(),
|
||||
group: fakePointGroup(),
|
||||
hovered: true
|
||||
|
@ -56,6 +57,16 @@ describe("<PointGroupItem/>", () => {
|
|||
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("<PointGroupItem/>", () => {
|
|||
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("<PointGroupItem/>", () => {
|
|||
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.");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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("<GroupCriteria />", () => {
|
||||
const fakeProps = (): GroupCriteriaProps => ({
|
||||
|
@ -21,41 +27,162 @@ describe("<GroupCriteria />", () => {
|
|||
group: fakePointGroup(),
|
||||
slugs: [],
|
||||
editGroupAreaInMap: false,
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true },
|
||||
},
|
||||
selectionPointType: undefined,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<GroupCriteria {...fakeProps()} />);
|
||||
["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(<GroupCriteria {...p} />);
|
||||
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(<GroupCriteria {...p} />);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||
payload: ["Weed"],
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles advanced view", () => {
|
||||
const wrapper = mount(<GroupCriteria {...fakeProps()} />);
|
||||
expect(wrapper.text()).not.toContain("number criteria");
|
||||
wrapper.find("ToggleButton").first().simulate("click");
|
||||
expect(wrapper.text()).toContain("number criteria");
|
||||
const wrapper = mount<GroupCriteria>(<GroupCriteria {...fakeProps()} />);
|
||||
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<GroupCriteria>(<GroupCriteria {...fakeProps()} />);
|
||||
wrapper.setState({ advanced: true });
|
||||
expect(wrapper.text()).toContain("day");
|
||||
});
|
||||
|
||||
it("changes day criteria", () => {
|
||||
const wrapper = mount<GroupCriteria>(<GroupCriteria {...fakeProps()} />);
|
||||
expect(wrapper.state().dayChanged).toBeFalsy();
|
||||
wrapper.instance().changeDay(true);
|
||||
expect(wrapper.state().dayChanged).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<GroupPointCountBreakdown />", () => {
|
||||
const fakeProps = (): GroupPointCountBreakdownProps => ({
|
||||
manualCount: 1,
|
||||
totalCount: 3,
|
||||
group: fakePointGroup(),
|
||||
dispatch: jest.fn(),
|
||||
shouldDisplay: () => true,
|
||||
pointsSelectedByGroup: [],
|
||||
iconDisplay: true,
|
||||
hovered: undefined,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<GroupPointCountBreakdown {...fakeProps()} />);
|
||||
["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(<GroupPointCountBreakdown {...p} />);
|
||||
["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(<GroupPointCountBreakdown {...p} />);
|
||||
["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(<GroupPointCountBreakdown {...p} />);
|
||||
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(<GroupPointCountBreakdown {...p} />);
|
||||
window.confirm = () => false;
|
||||
wrapper.find("button").first().simulate("click");
|
||||
expect(overwriteGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears criteria", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<GroupPointCountBreakdown {...p} />);
|
||||
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(<GroupPointCountBreakdown {...p} />);
|
||||
window.confirm = () => false;
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(overwriteGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<PointTypeSelection />", () => {
|
||||
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(<PointTypeSelection {...p} />);
|
||||
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(<PointTypeSelection {...p} />);
|
||||
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(<PointTypeSelection {...p} />);
|
||||
wrapper.find(Checkbox).first().simulate("change");
|
||||
expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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("<CheckboxSelections />", () => {
|
||||
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(<CheckboxSelections {...fakeProps()} />);
|
||||
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(<CheckboxSelections {...p} />);
|
||||
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<CheckboxSelections>(<CheckboxSelections {...fakeProps()} />);
|
||||
expect(wrapper.state().Plant).toBeFalsy();
|
||||
wrapper.instance().toggleMore("Plant")();
|
||||
expect(wrapper.state().Plant).toBeTruthy();
|
||||
});
|
||||
|
||||
it("toggles point type", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<CheckboxSelections {...p} />);
|
||||
wrapper.find("input").first().simulate("change");
|
||||
expect(togglePointTypeCriteria).toHaveBeenCalledWith(p.group, "Plant");
|
||||
});
|
||||
|
||||
it("stops propagation", () => {
|
||||
const wrapper = mount(<CheckboxSelections {...fakeProps()} />);
|
||||
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(<CheckboxSelections {...p} />);
|
||||
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(<CheckboxSelections {...p} />);
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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()", () => {
|
||||
|
|
|
@ -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("<EqCriteriaSelection<string> />", () => {
|
||||
|
@ -88,16 +89,26 @@ describe("<NumberCriteriaSelection />", () => {
|
|||
expect(clearCriteriaField).toHaveBeenCalledWith(
|
||||
p.group,
|
||||
["number_gt"],
|
||||
"x",
|
||||
["x"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DaySelection />", () => {
|
||||
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(<DaySelection {...p} />);
|
||||
expect(wrapper.html()).toContain("label");
|
||||
});
|
||||
|
||||
it("changes operator", () => {
|
||||
|
@ -121,6 +132,16 @@ describe("<DaySelection />", () => {
|
|||
{ 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(<DaySelection {...p} />);
|
||||
wrapper.find(Checkbox).simulate("change");
|
||||
expect(editCriteria).toHaveBeenCalledWith(p.group, {
|
||||
day: { op: "<", days_ago: 0 }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<NumberLtGtInput />", () => {
|
||||
|
@ -165,15 +186,46 @@ describe("<LocationSelection />", () => {
|
|||
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(<LocationSelection {...p} />);
|
||||
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(<LocationSelection {...p} />);
|
||||
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(<LocationSelection {...p} />);
|
||||
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(<LocationSelection {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("invalid selection");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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("<SubCriteriaSection />", () => {
|
||||
const fakeProps = (): SubCriteriaSectionProps => ({
|
||||
dispatch: Function,
|
||||
group: fakePointGroup(),
|
||||
disabled: false,
|
||||
pointerTypes: [],
|
||||
slugs: [],
|
||||
});
|
||||
|
||||
it("doesn't return criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.pointerTypes = [];
|
||||
const wrapper = mount(<SubCriteriaSection {...p} />);
|
||||
expect(wrapper.text()).toEqual("");
|
||||
});
|
||||
|
||||
it("doesn't return incompatible criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.pointerTypes = ["Plant", "Weed"];
|
||||
const wrapper = mount(<SubCriteriaSection {...p} />);
|
||||
expect(wrapper.text()).toEqual("");
|
||||
});
|
||||
|
||||
it("returns plant criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.pointerTypes = ["Plant"];
|
||||
p.slugs = ["strawberry-guava"];
|
||||
const wrapper = mount(<SubCriteriaSection {...p} />);
|
||||
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(<SubCriteriaSection {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("color");
|
||||
});
|
||||
|
||||
it("returns weed criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.pointerTypes = ["Weed"];
|
||||
const wrapper = mount(<SubCriteriaSection {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("source");
|
||||
});
|
||||
|
||||
it("returns tool slot criteria", () => {
|
||||
const p = fakeProps();
|
||||
p.pointerTypes = ["ToolSlot"];
|
||||
const wrapper = mount(<SubCriteriaSection {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("direction");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<CheckboxList />", () => {
|
||||
const fakeProps = (): CheckboxListProps<string> => ({
|
||||
|
|
|
@ -48,7 +48,7 @@ export class AddEqCriteria<T extends string | number>
|
|||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button green"
|
||||
title={t("add criteria")}
|
||||
title={t("add filter")}
|
||||
onClick={this.commit}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
|
@ -97,7 +97,7 @@ export class AddNumberCriteria
|
|||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button green"
|
||||
title={t("add number criteria")}
|
||||
title={t("add number filter")}
|
||||
onClick={this.commit}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
|
|
|
@ -8,6 +8,11 @@ const eqCriteriaEmpty =
|
|||
(eqCriteria: Record<string, (string | number)[] | undefined>) =>
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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<PointerType, string> => ({
|
||||
Plant: t("Plants"),
|
||||
GenericPointer: t("Points"),
|
||||
Weed: t("Weeds"),
|
||||
ToolSlot: t("Slots"),
|
||||
});
|
||||
|
||||
export class GroupCriteria extends
|
||||
React.Component<GroupCriteriaProps, GroupCriteriaState> {
|
||||
state: GroupCriteriaState = { advanced: false, clearCount: 0 };
|
||||
render() {
|
||||
const { group, dispatch, slugs } = this.props;
|
||||
const criteria = group.body.criteria;
|
||||
const commonProps = { group, criteria, dispatch };
|
||||
return <div className="group-criteria">
|
||||
<label className="criteria-heading">{t("criteria")}</label>
|
||||
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 = () =>
|
||||
<div className="criteria-options-menu">
|
||||
<label>{t("advanced mode")}</label>
|
||||
<ToggleButton
|
||||
title={t("toggle advanced view")}
|
||||
toggleValue={!this.state.advanced}
|
||||
customText={{ textTrue: t("basic"), textFalse: t("advanced") }}
|
||||
toggleAction={() => this.setState({ advanced: !this.state.advanced })} />
|
||||
toggleValue={this.state.advanced}
|
||||
customText={{ textTrue: t("on"), textFalse: t("off") }}
|
||||
toggleAction={() =>
|
||||
this.setState({ advanced: !this.state.advanced })} />
|
||||
</div>
|
||||
|
||||
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 <div className="group-criteria">
|
||||
<label className="criteria-heading">{t("filters")}</label>
|
||||
<Help text={t(ToolTips.CRITERIA_ALPHA_FEATURE)}
|
||||
customIcon={"exclamation-triangle"} customClass={"alpha-icon"} />
|
||||
<Popover>
|
||||
<i className="fa fa-gear dark" />
|
||||
<this.AdvancedToggleMenu />
|
||||
</Popover>
|
||||
{!this.state.advanced
|
||||
? <div className={"basic"}>
|
||||
<CheckboxSelections group={group} dispatch={dispatch} slugs={slugs} />
|
||||
<DaySelection {...commonProps} />
|
||||
<LocationSelection {...commonProps}
|
||||
<PointTypeSelection {...commonProps} pointTypes={pointTypes} />
|
||||
<div className={"point-type-checkboxes"}>
|
||||
<SubCriteriaSection pointerTypes={pointTypes}
|
||||
disabled={false} group={group} dispatch={dispatch} slugs={slugs} />
|
||||
</div>
|
||||
{!pointTypes.includes("ToolSlot") &&
|
||||
<DaySelection {...commonProps} {...dayProps} />}
|
||||
<LocationSelection {...commonProps} botSize={this.props.botSize}
|
||||
editGroupAreaInMap={this.props.editGroupAreaInMap} />
|
||||
</div>
|
||||
: <div className={"advanced"}>
|
||||
<DaySelection {...commonProps} />
|
||||
<label>{t("string criteria")}</label>
|
||||
<DaySelection {...commonProps} {...dayProps} />
|
||||
<label>{t("strings")}</label>
|
||||
<Help text={t(ToolTips.DOT_NOTATION_TIP)} />
|
||||
<EqCriteriaSelection<string> {...commonProps}
|
||||
type={"string"} eqCriteria={criteria.string_eq}
|
||||
criteriaKey={"string_eq"} />
|
||||
<label>{t("number criteria")}</label>
|
||||
<label>{t("numbers")}</label>
|
||||
<EqCriteriaSelection<number> {...commonProps}
|
||||
type={"number"} eqCriteria={criteria.number_eq}
|
||||
criteriaKey={"number_eq"} />
|
||||
<NumberCriteriaSelection {...commonProps} criteriaKey={"number_lt"} />
|
||||
<NumberCriteriaSelection {...commonProps} criteriaKey={"number_gt"} />
|
||||
</div>}
|
||||
<ClearCriteria dispatch={dispatch} group={group} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset all group criteria to defaults. */
|
||||
const ClearCriteria = (props: ClearCriteriaProps) =>
|
||||
<button className="clear-criteria fb-button red no-float"
|
||||
title={t("clear all criteria")}
|
||||
<button className="clear-criteria fb-button red"
|
||||
title={t("clear all filters")}
|
||||
onClick={() => {
|
||||
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")}
|
||||
</button>;
|
||||
|
||||
/** Clear manually selected points. */
|
||||
const ClearPointIds = (props: ClearPointIdsProps) =>
|
||||
<button className="clear-point-ids fb-button red"
|
||||
title={t("clear manual selections")}
|
||||
onClick={() => {
|
||||
if (confirm(t("Remove all manual selections?"))) {
|
||||
props.dispatch(overwriteGroup(props.group, {
|
||||
...props.group.body, point_ids: []
|
||||
}));
|
||||
props.dispatch(selectPoint(undefined));
|
||||
}
|
||||
}}>
|
||||
{t("clear")}
|
||||
</button>;
|
||||
|
||||
/** Show counts of manual and criteria selections. */
|
||||
export const GroupPointCountBreakdown = (props: GroupPointCountBreakdownProps) =>
|
||||
<div className={"criteria-point-count-breakdown"}>
|
||||
<div className={"manual-group-member-count"}>
|
||||
<div className={"manual-selection-count"}>
|
||||
{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) =>
|
||||
<PointGroupItem
|
||||
key={point.uuid}
|
||||
hovered={point.uuid === props.hovered}
|
||||
group={props.group}
|
||||
point={point}
|
||||
dispatch={props.dispatch} />;
|
||||
return <div className={"group-member-count-breakdown"}>
|
||||
<div className={"manual-group-member-count"}>
|
||||
<div className={"manual-selection-count"}>
|
||||
{manualPoints.length}
|
||||
</div>
|
||||
<p>{t("manually selected")}</p>
|
||||
<ClearPointIds dispatch={props.dispatch} group={props.group} />
|
||||
</div>
|
||||
<p>{t("manually selected")}</p>
|
||||
</div>
|
||||
<div className={"criteria-group-member-count"}>
|
||||
<div className={"criteria-selection-count"}>
|
||||
{props.totalCount - props.manualCount}
|
||||
</div>
|
||||
<p>{t("selected by criteria")}</p>
|
||||
</div>
|
||||
{props.iconDisplay && manualPoints.length > 0 &&
|
||||
<div className="groups-list-wrapper">
|
||||
{manualPoints.map(generatePointIcons)}
|
||||
</div>}
|
||||
{props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<div className={"group-member-section"}>
|
||||
<div className={"criteria-group-member-count"}>
|
||||
<div className={"criteria-selection-count"}>
|
||||
{criteriaPoints.length}
|
||||
</div>
|
||||
<p>{t("selected by filters")}</p>
|
||||
<ClearCriteria dispatch={props.dispatch} group={props.group} />
|
||||
</div>
|
||||
{props.iconDisplay && criteriaPoints.length > 0 &&
|
||||
<div className="groups-list-wrapper">
|
||||
{criteriaPoints.map(generatePointIcons)}
|
||||
</div>}
|
||||
</div>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
/** Select pointer_type string equal criteria,
|
||||
* which determines if any additional criteria is shown. */
|
||||
export const PointTypeSelection = (props: PointTypeSelectionProps) =>
|
||||
<div className={"point-type-selection"}>
|
||||
<p className={"category"}>{t("Select all")}</p>
|
||||
<FBSelect
|
||||
key={JSON.stringify(props.group.body.criteria)}
|
||||
list={POINTER_TYPE_LIST().slice(0, -1)}
|
||||
customNullLabel={t("Select one")}
|
||||
selectedItem={props.pointTypes[0]
|
||||
? POINTER_TYPE_DDI_LOOKUP()[props.pointTypes[0]]
|
||||
: undefined}
|
||||
onChange={ddi => {
|
||||
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 =>
|
||||
<div className="point-type-section" key={pointerType}>
|
||||
<Checkbox
|
||||
onChange={() =>
|
||||
props.dispatch(togglePointTypeCriteria(props.group, pointerType))}
|
||||
checked={props.pointTypes.includes(pointerType)}
|
||||
title={CRITERIA_POINT_TYPE_LOOKUP()[pointerType]} />
|
||||
<p>{CRITERIA_POINT_TYPE_LOOKUP()[pointerType]}</p>
|
||||
</div>)}
|
||||
</div>;
|
||||
|
|
|
@ -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 = <T extends string | number>(
|
|||
};
|
||||
|
||||
/** Clear incompatible criteria. */
|
||||
const clearSubCriteria = (
|
||||
export const clearSubCriteria = (
|
||||
pointerTypes: PointerType[],
|
||||
tempCriteria: PointGroupCriteria,
|
||||
) => {
|
||||
const toggleStrEq = toggleEqCriteria<string>(tempCriteria.string_eq, "off");
|
||||
const toggleNumEq = toggleEqCriteria<number>(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<string>(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));
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<PointGroupCriteria, "day">)[];
|
|||
export type EqCriteria<T> = Record<string, T[] | undefined>;
|
||||
|
||||
export const POINTER_TYPES: PointerType[] =
|
||||
["Plant", "GenericPointer", "ToolSlot", "Weed"];
|
||||
["Plant", "GenericPointer", "Weed", "ToolSlot"];
|
||||
|
||||
export const DEFAULT_CRITERIA: Readonly<PointGroupCriteria> = {
|
||||
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<T> 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<T> = { label: string, value: T, color?: string };
|
||||
|
||||
export interface CheckboxListProps<T> {
|
||||
criteriaKey: string;
|
||||
list: { label: string, value: T }[];
|
||||
list: CheckboxListItem<T>[];
|
||||
dispatch: Function;
|
||||
group: TaggedPointGroup;
|
||||
pointerType: PointerType;
|
||||
|
|
|
@ -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
|
||||
<CheckboxSelectionsProps, Partial<CheckboxSelectionsState>> {
|
||||
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<string>(criteria);
|
||||
return <div className={"point-type-checkboxes"}>
|
||||
{CRITERIA_POINT_TYPES().map(({ label, pointerType }, index) => {
|
||||
const typeSelected = selected("pointer_type", pointerType);
|
||||
const partial = hasSubCriteria(criteria)(pointerType) && !typeSelected;
|
||||
return <div className="point-type-section" key={index}>
|
||||
<div className="point-type-checkbox"
|
||||
onClick={this.toggleMore(pointerType)}>
|
||||
<Checkbox
|
||||
onChange={() =>
|
||||
dispatch(togglePointTypeCriteria(group, pointerType))}
|
||||
checked={typeSelected}
|
||||
partial={partial}
|
||||
title={t(label)}
|
||||
disabled={typeDisabled(criteria, pointerType)}
|
||||
onClick={e => e.stopPropagation()} />
|
||||
<p>{label}</p>
|
||||
<i className={
|
||||
`fa fa-caret-${this.state[pointerType] ? "up" : "down"}`}
|
||||
title={this.state[pointerType]
|
||||
? t("hide additional criteria")
|
||||
: t("show additional criteria")} />
|
||||
</div>
|
||||
{this.state.Plant && pointerType == "Plant" &&
|
||||
<PlantCriteria
|
||||
disabled={!typeSelected && !partial}
|
||||
group={group} dispatch={dispatch} slugs={slugs} />}
|
||||
{this.state.GenericPointer && pointerType == "GenericPointer" &&
|
||||
<PointCriteria
|
||||
disabled={!typeSelected && !partial}
|
||||
group={group} dispatch={dispatch} />}
|
||||
{this.state.ToolSlot && pointerType == "ToolSlot" &&
|
||||
<ToolCriteria
|
||||
disabled={!typeSelected && !partial}
|
||||
group={group} dispatch={dispatch} />}
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -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 !!(
|
||||
|
|
|
@ -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<T extends string | number>
|
||||
|
@ -39,7 +44,7 @@ export class EqCriteriaSelection<T extends string | number>
|
|||
</Col>
|
||||
<Col xs={2}>
|
||||
<button className="fb-button red"
|
||||
title={t("remove criteria")}
|
||||
title={t("remove filter")}
|
||||
onClick={() => dispatch(removeEqCriteriaValue(
|
||||
group, eqCriteria, criteriaKey, key, value))}>
|
||||
<i className="fa fa-minus" />
|
||||
|
@ -73,7 +78,7 @@ export const NumberCriteriaSelection = (props: NumberCriteriaProps) => {
|
|||
<button className="fb-button red"
|
||||
title={t("remove number criteria")}
|
||||
onClick={() => props.dispatch(clearCriteriaField(
|
||||
props.group, [props.criteriaKey], key))}>
|
||||
props.group, [props.criteriaKey], [key]))}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
|
@ -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 <div className="day-criteria">
|
||||
<label>{t("Age selection")}</label>
|
||||
{advanced
|
||||
? <label>{t("Age")}</label>
|
||||
: <p className={"category"}>{t("Age")}</p>}
|
||||
{!advanced &&
|
||||
<div className="criteria-checkbox-list-item">
|
||||
<Checkbox
|
||||
onChange={() => {
|
||||
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")} />
|
||||
<p>{t("all")}</p>
|
||||
</div>}
|
||||
<Row>
|
||||
<Col xs={5}>
|
||||
<FBSelect key={JSON.stringify(criteria)}
|
||||
list={[DAY_OPERATOR_DDI_LOOKUP()["<"],
|
||||
DAY_OPERATOR_DDI_LOOKUP()[">"]]}
|
||||
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);
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<input type="number" value={dayCriteria.days_ago} name="days_ago"
|
||||
<input type="number" name="days_ago"
|
||||
value={noDayCriteria ? "" : dayCriteria.days_ago}
|
||||
disabled={noDayCriteria}
|
||||
onChange={e => {
|
||||
const { op } = dayCriteria;
|
||||
const days_ago = parseInt(e.currentTarget.value);
|
||||
dispatch(editCriteria(group, { day: { days_ago, op } }));
|
||||
props.changeDay(true);
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<p>{t("days old")}</p>
|
||||
<p className={"days-old-text"}>{t("days old")}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
@ -160,7 +190,17 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
|
|||
/** Form inputs to define a 2D group criteria area. */
|
||||
export const LocationSelection = (props: LocationSelectionProps) =>
|
||||
<div className="location-criteria">
|
||||
<label>{t("Location selection")}</label>
|
||||
<p className={"category"}>{t("Location")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["number_lt", "number_gt"]}
|
||||
criteriaKeys={["x", "y"]}
|
||||
dispatch={props.dispatch} />
|
||||
{!spaceSelected(props.group, props.botSize) &&
|
||||
<div className="location-selection-warning">
|
||||
<i className="fa fa-exclamation-triangle" />
|
||||
<p>{t("Invalid selection.")}</p>
|
||||
</div>}
|
||||
{["x", "y"].map((axis: "x" | "y") =>
|
||||
<NumberLtGtInput
|
||||
key={axis}
|
||||
|
@ -170,7 +210,7 @@ export const LocationSelection = (props: LocationSelectionProps) =>
|
|||
<div className={"edit-in-map"}>
|
||||
<ToggleButton
|
||||
title={props.editGroupAreaInMap
|
||||
? t("map boxes will change location criteria")
|
||||
? t("map boxes will change location filter")
|
||||
: t("map boxes will manually add plants")}
|
||||
customText={{ textFalse: t("off"), textTrue: t("on") }}
|
||||
toggleValue={props.editGroupAreaInMap}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { capitalize, uniq } from "lodash";
|
||||
import { capitalize, uniq, some, isEqual } from "lodash";
|
||||
import {
|
||||
NumberLtGtInput,
|
||||
toggleAndEditEqCriteria,
|
||||
|
@ -13,20 +13,54 @@ import {
|
|||
SubCriteriaProps,
|
||||
PlantSubCriteriaProps,
|
||||
ClearCategoryProps,
|
||||
PointSubCriteriaProps,
|
||||
SubCriteriaSectionProps,
|
||||
CheckboxListItem,
|
||||
} from "./interfaces";
|
||||
import { PLANT_STAGE_LIST } from "../../plants/edit_plant_status";
|
||||
import { DIRECTION_CHOICES } from "../../tools/tool_slot_edit_components";
|
||||
import { Checkbox } from "../../../ui";
|
||||
import { PointType } from "farmbot";
|
||||
|
||||
export const SubCriteriaSection = (props: SubCriteriaSectionProps) => {
|
||||
const { group, dispatch, disabled } = props;
|
||||
const pointTypes = props.pointerTypes.sort();
|
||||
if (pointTypes.length > 1 &&
|
||||
!isEqual(pointTypes, ["GenericPointer", "Weed"])) {
|
||||
return <div className={"criteria-checkboxes"} />;
|
||||
}
|
||||
switch (pointTypes[0]) {
|
||||
case "Plant":
|
||||
return <PlantCriteria
|
||||
disabled={disabled}
|
||||
group={group} dispatch={dispatch} slugs={props.slugs} />;
|
||||
case "GenericPointer":
|
||||
return <PointCriteria
|
||||
disabled={disabled}
|
||||
group={group} dispatch={dispatch} />;
|
||||
case "Weed":
|
||||
return <WeedCriteria
|
||||
disabled={disabled}
|
||||
group={group} dispatch={dispatch} />;
|
||||
case "ToolSlot":
|
||||
return <ToolCriteria
|
||||
disabled={disabled}
|
||||
group={group} dispatch={dispatch} />;
|
||||
default:
|
||||
return <div className={"criteria-checkboxes"} />;
|
||||
}
|
||||
};
|
||||
|
||||
/** "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 <div className="criteria-checkbox-list-item">
|
||||
<Checkbox
|
||||
onChange={() =>
|
||||
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<T>(criteria);
|
||||
const toggle = toggleAndEditEqCriteria;
|
||||
return <div className={"criteria-checkbox-list"}>
|
||||
{props.list.map(({ label, value }: { label: string, value: T }, index) =>
|
||||
{props.list.map(({ label, value, color }: CheckboxListItem<T>, index) =>
|
||||
<div className="criteria-checkbox-list-item" key={index}>
|
||||
<Checkbox
|
||||
onChange={() => props.dispatch(toggle<T>(
|
||||
props.group, props.criteriaKey, value, props.pointerType))}
|
||||
checked={selected(props.criteriaKey, value)}
|
||||
title={t(label)}
|
||||
color={color}
|
||||
disabled={props.disabled} />
|
||||
<p>{label}</p>
|
||||
</div>)}
|
||||
|
@ -71,7 +106,7 @@ const PlantStage = (props: SubCriteriaProps) =>
|
|||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"plant_stage"}
|
||||
criteriaKeys={["plant_stage"]}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList<string>
|
||||
disabled={props.disabled}
|
||||
|
@ -89,7 +124,7 @@ const PlantType = (props: PlantSubCriteriaProps) =>
|
|||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"openfarm_slug"}
|
||||
criteriaKeys={["openfarm_slug"]}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList<string>
|
||||
disabled={props.disabled}
|
||||
|
@ -103,49 +138,40 @@ const PlantType = (props: PlantSubCriteriaProps) =>
|
|||
({ label: capitalize(slug).replace("-", " "), value: slug }))} />
|
||||
</div>;
|
||||
|
||||
/** 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 <div className={"point-criteria-options"}>
|
||||
<PointType {...commonProps} />
|
||||
const pointerType: PointType = "Weed";
|
||||
const commonProps = { group, dispatch, disabled, pointerType };
|
||||
return <div className={"weed-criteria-options"}>
|
||||
<PointSource {...commonProps} />
|
||||
<Color {...commonProps} />
|
||||
<Radius {...commonProps} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PointType = (props: SubCriteriaProps) =>
|
||||
<div className={"point-type-criteria"}>
|
||||
<p className={"category"}>{t("Type")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"meta.type"}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList
|
||||
disabled={props.disabled}
|
||||
pointerType={"GenericPointer"}
|
||||
criteriaKey={"meta.type"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={[
|
||||
{ label: t("Weeds"), value: "weed" },
|
||||
{ label: t("Points"), value: "point" },
|
||||
]} />
|
||||
/** 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 <div className={"point-criteria-options"}>
|
||||
<Color {...commonProps} />
|
||||
<Radius {...commonProps} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PointSource = (props: SubCriteriaProps) =>
|
||||
const PointSource = (props: PointSubCriteriaProps) =>
|
||||
<div className={"point-source-criteria"}>
|
||||
<p className={"category"}>{t("Source")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"meta.created_by"}
|
||||
criteriaKeys={["meta.created_by"]}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList
|
||||
disabled={props.disabled}
|
||||
pointerType={"GenericPointer"}
|
||||
pointerType={props.pointerType}
|
||||
criteriaKey={"meta.created_by"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
|
@ -155,13 +181,13 @@ const PointSource = (props: SubCriteriaProps) =>
|
|||
]} />
|
||||
</div>;
|
||||
|
||||
const Radius = (props: SubCriteriaProps) =>
|
||||
const Radius = (props: PointSubCriteriaProps) =>
|
||||
<div className={"radius-criteria"}>
|
||||
<p className={"category"}>{t("Radius")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["number_gt", "number_lt"]}
|
||||
criteriaKey={"radius"}
|
||||
criteriaKeys={["radius"]}
|
||||
dispatch={props.dispatch} />
|
||||
<div className={"lt-gt-criteria"}>
|
||||
<NumberLtGtInput
|
||||
|
@ -170,35 +196,34 @@ const Radius = (props: SubCriteriaProps) =>
|
|||
inputWidth={3}
|
||||
labelWidth={2}
|
||||
group={props.group}
|
||||
pointerType={"GenericPointer"}
|
||||
pointerType={props.pointerType}
|
||||
dispatch={props.dispatch} />
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const Color = (props: SubCriteriaProps) =>
|
||||
const Color = (props: PointSubCriteriaProps) =>
|
||||
<div className={"color-criteria"}>
|
||||
<p className={"category"}>{t("Color")}</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
criteriaKey={"meta.color"}
|
||||
criteriaKeys={["meta.color"]}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList
|
||||
disabled={props.disabled}
|
||||
pointerType={"GenericPointer"}
|
||||
pointerType={props.pointerType}
|
||||
criteriaKey={"meta.color"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={[
|
||||
{ label: t("Green"), value: "green" },
|
||||
{ label: t("Red"), value: "red" },
|
||||
{ label: t("Cyan"), value: "cyan" },
|
||||
{ label: t("Blue"), value: "blue" },
|
||||
{ label: t("Yellow"), value: "yellow" },
|
||||
{ label: t("Orange"), value: "orange" },
|
||||
{ label: t("Purple"), value: "purple" },
|
||||
{ label: t("Pink"), value: "pink" },
|
||||
{ label: t("Gray"), value: "gray" },
|
||||
{ label: t("Green"), value: "green", color: "green" },
|
||||
{ label: t("Red"), value: "red", color: "red" },
|
||||
{ label: t("Blue"), value: "blue", color: "blue" },
|
||||
{ label: t("Yellow"), value: "yellow", color: "yellow" },
|
||||
{ label: t("Orange"), value: "orange", color: "orange" },
|
||||
{ label: t("Purple"), value: "purple", color: "purple" },
|
||||
{ label: t("Pink"), value: "pink", color: "pink" },
|
||||
{ label: t("Gray"), value: "gray", color: "gray" },
|
||||
]} />
|
||||
</div>;
|
||||
|
||||
|
@ -217,7 +242,7 @@ const PulloutDirection = (props: SubCriteriaProps) =>
|
|||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["number_eq"]}
|
||||
criteriaKey={"pullout_direction"}
|
||||
criteriaKeys={["pullout_direction"]}
|
||||
dispatch={props.dispatch} />
|
||||
<CheckboxList<number>
|
||||
disabled={props.disabled}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<typeof setInterval>;
|
||||
iconDisplay: boolean;
|
||||
}
|
||||
|
||||
|
@ -35,51 +35,12 @@ export class GroupDetailActive
|
|||
extends React.Component<GroupDetailActiveProps, GroupDetailActiveState> {
|
||||
state: GroupDetailActiveState = { iconDisplay: true };
|
||||
|
||||
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
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 <PointGroupItem
|
||||
key={point.uuid}
|
||||
hovered={point.uuid === this.props.hovered}
|
||||
group={this.props.group}
|
||||
point={point}
|
||||
dispatch={this.props.dispatch} />;
|
||||
});
|
||||
}
|
||||
|
||||
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 <ErrorBoundary>
|
||||
<label>{t("GROUP NAME")}</label>
|
||||
<i style={{ float: "right" }}>{this.saved ? "" : " saving..."}</i>
|
||||
<input
|
||||
name="name"
|
||||
defaultValue={group.body.name}
|
||||
onChange={this.update}
|
||||
onBlur={this.saveGroup} />
|
||||
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));
|
||||
}
|
||||
}} />
|
||||
<GroupSortSelection group={group} dispatch={dispatch}
|
||||
pointsSelectedByGroup={this.pointsSelectedByGroup} />
|
||||
<GroupMemberDisplay group={group} dispatch={dispatch}
|
||||
pointsSelectedByGroup={this.pointsSelectedByGroup}
|
||||
icons={this.icons}
|
||||
hovered={this.props.hovered}
|
||||
iconDisplay={this.state.iconDisplay}
|
||||
toggleIconShow={this.toggleIconShow}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
{this.props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupCriteria dispatch={dispatch}
|
||||
group={group} slugs={this.props.slugs}
|
||||
editGroupAreaInMap={this.props.editGroupAreaInMap} />}
|
||||
group={group} slugs={this.props.slugs} botSize={this.props.botSize}
|
||||
editGroupAreaInMap={this.props.editGroupAreaInMap}
|
||||
selectionPointType={this.props.selectionPointType} />}
|
||||
<DeleteButton
|
||||
className="group-delete-btn"
|
||||
dispatch={dispatch}
|
||||
|
@ -130,7 +96,7 @@ const GroupSortSelection = (props: GroupSortSelectionProps) =>
|
|||
</label>
|
||||
{props.group.body.sort_type == "random" &&
|
||||
<Help
|
||||
text={Content.SORT_DESCRIPTION}
|
||||
text={ToolTips.SORT_DESCRIPTION}
|
||||
customIcon={"exclamation-triangle"} />}
|
||||
<Paths
|
||||
key={JSON.stringify(props.pointsSelectedByGroup
|
||||
|
@ -145,32 +111,34 @@ interface GroupMemberDisplayProps {
|
|||
dispatch: Function;
|
||||
pointsSelectedByGroup: TaggedPoint[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
icons: JSX.Element[];
|
||||
iconDisplay: boolean;
|
||||
toggleIconShow(): void;
|
||||
hovered: UUID | undefined;
|
||||
}
|
||||
|
||||
/** View group point counts and icon list. */
|
||||
const GroupMemberDisplay = (props: GroupMemberDisplayProps) =>
|
||||
<div className="group-member-display">
|
||||
const GroupMemberDisplay = (props: GroupMemberDisplayProps) => {
|
||||
return <div className="group-member-display">
|
||||
<label>
|
||||
{t("GROUP MEMBERS ({{count}})", { count: props.icons.length })}
|
||||
{t("GROUP MEMBERS ({{count}})", {
|
||||
count: props.pointsSelectedByGroup.length
|
||||
})}
|
||||
</label>
|
||||
<Help text={`${t("Click plants in map to add or remove.")} ${(
|
||||
props.shouldDisplay(Feature.criteria_groups) &&
|
||||
props.pointsSelectedByGroup.length != props.group.body.point_ids.length)
|
||||
? t(Content.CRITERIA_SELECTION_COUNT) : ""}`} />
|
||||
? t(ToolTips.CRITERIA_SELECTION_COUNT) : ""}`} />
|
||||
<i onClick={props.toggleIconShow}
|
||||
className={`fa fa-caret-${props.iconDisplay ? "up" : "down"}`}
|
||||
title={props.iconDisplay
|
||||
? t("hide icons")
|
||||
: t("show icons")} />
|
||||
{props.shouldDisplay(Feature.criteria_groups) &&
|
||||
<GroupPointCountBreakdown
|
||||
manualCount={props.group.body.point_ids.length}
|
||||
totalCount={props.pointsSelectedByGroup.length} />}
|
||||
{props.iconDisplay &&
|
||||
<div className="groups-list-wrapper">
|
||||
{props.icons}
|
||||
</div>}
|
||||
<GroupPointCountBreakdown
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
shouldDisplay={props.shouldDisplay}
|
||||
iconDisplay={props.iconDisplay}
|
||||
hovered={props.hovered}
|
||||
pointsSelectedByGroup={props.pointsSelectedByGroup} />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -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<GroupListPanelProps, State> {
|
||||
|
||||
export class RawGroupListPanel
|
||||
extends React.Component<GroupListPanelProps, State> {
|
||||
state: State = { searchTerm: "" };
|
||||
|
||||
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
|
@ -47,7 +50,7 @@ export class RawGroupListPanel extends React.Component<GroupListPanelProps, Stat
|
|||
<DesignerNavTabs />
|
||||
<DesignerPanelTop
|
||||
panel={Panel.Groups}
|
||||
linkTo={"/app/designer/plants/select"}
|
||||
onClick={() => this.props.dispatch(createGroup({ pointUuids: [] }))}
|
||||
title={t("Add group")}>
|
||||
<input type="text"
|
||||
name="searchTerm"
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Color } from "../../ui";
|
|||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Actions } from "../../constants";
|
||||
import { edit } from "../../api/crud";
|
||||
import { edit, save } from "../../api/crud";
|
||||
import { TaggedPointGroup, TaggedPoint } from "farmbot";
|
||||
import { error } from "../../toast/toast";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
|
@ -73,10 +73,14 @@ export const PathInfoBar = (props: PathInfoBarProps) => {
|
|||
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));
|
||||
}
|
||||
}}>
|
||||
<div className={"sort-path-info-bar"}
|
||||
style={{ width: `${normalizedLength}%` }}>
|
||||
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
|
||||
|
|
|
@ -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) =>
|
||||
`<svg xmlns='http://www.w3.org/2000/svg'
|
||||
|
@ -65,7 +67,7 @@ export class PointGroupItem
|
|||
|
||||
click = () => {
|
||||
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} />}
|
||||
<img
|
||||
style={{
|
||||
border: this.criteriaIcon ? "1px solid gray" : "none",
|
||||
borderRadius: "5px",
|
||||
background: this.props.hovered ? "lightgray" : "none",
|
||||
}}
|
||||
style={{ background: this.props.hovered ? "lightgray" : "none" }}
|
||||
src={this.initIcon}
|
||||
onLoad={this.maybeGetCachedIcon}
|
||||
width={32}
|
||||
|
|
|
@ -64,14 +64,14 @@ describe("<CreatePoints />", () => {
|
|||
it("renders for points", () => {
|
||||
mockPath = "/app/designer";
|
||||
const wrapper = mount(<CreatePoints {...fakeProps()} />);
|
||||
["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(<CreatePoints {...fakeProps()} />);
|
||||
["add weed", "delete", "x", "y", "radius", "color"]
|
||||
["add weed", "delete", "x", "y", "radius"]
|
||||
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
|
||||
|
|
|
@ -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("<EditPointColor />", () => {
|
|||
expect(p.updatePoint).toHaveBeenCalledWith({ meta: { color: "blue" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AdditionalWeedProperties />", () => {
|
||||
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(<AdditionalWeedProperties {...p} />);
|
||||
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(<AdditionalWeedProperties {...p} />);
|
||||
wrapper.find("input").last().simulate("change");
|
||||
expect(p.updatePoint).toHaveBeenCalledWith({
|
||||
meta: { removal_method: "manual" }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,8 +45,24 @@ describe("<EditPoint />", () => {
|
|||
|
||||
it("renders with points", () => {
|
||||
mockPath = "/app/designer/points/1";
|
||||
const wrapper = mount(<EditPoint {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
const point = fakePoint();
|
||||
point.body.meta = { meta_key: "meta value" };
|
||||
p.findPoint = () => point;
|
||||
const wrapper = mount(<EditPoint {...p} />);
|
||||
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(<EditPoint {...p} />);
|
||||
expect(wrapper.text()).toContain("Edit point");
|
||||
expect(wrapper.text()).not.toContain("red");
|
||||
});
|
||||
|
||||
it("moves the device to a particular point", () => {
|
||||
|
|
|
@ -196,14 +196,25 @@ export class RawCreatePoints
|
|||
PointProperties = () =>
|
||||
<ul>
|
||||
<li>
|
||||
<div className={"point-name-input"}>
|
||||
<label>{t("Name")}</label>
|
||||
<BlurableInput
|
||||
name="name"
|
||||
type="text"
|
||||
onCommit={this.updateValue("name")}
|
||||
value={this.attr("name") || this.defaultName} />
|
||||
</div>
|
||||
<Row>
|
||||
<div className={"point-name-input"}>
|
||||
<Col xs={10}>
|
||||
<label>{t("Name")}</label>
|
||||
<BlurableInput
|
||||
name="name"
|
||||
type="text"
|
||||
onCommit={this.updateValue("name")}
|
||||
value={this.attr("name") || this.defaultName} />
|
||||
</Col>
|
||||
</div>
|
||||
<div className={"point-color-input"}>
|
||||
<Col xs={2}>
|
||||
<ColorPicker
|
||||
current={(this.attr("color") || this.defaultColor) as ResourceColor}
|
||||
onChange={this.changeColor} />
|
||||
</Col>
|
||||
</div>
|
||||
</Row>
|
||||
</li>
|
||||
<ListItem name={t("Location")}>
|
||||
<Row>
|
||||
|
@ -238,13 +249,6 @@ export class RawCreatePoints
|
|||
</Col>
|
||||
</Row>
|
||||
</ListItem>
|
||||
<ListItem name={t("Color")}>
|
||||
<Row>
|
||||
<ColorPicker
|
||||
current={(this.attr("color") || this.defaultColor) as ResourceColor}
|
||||
onChange={this.changeColor} />
|
||||
</Row>
|
||||
</ListItem>
|
||||
</ul>
|
||||
|
||||
PointActions = () =>
|
||||
|
|
|
@ -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<TaggedGenericPointer["body"] | TaggedWeedPointer["body"]>;
|
||||
|
@ -52,6 +53,51 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
|
|||
</ListItem>
|
||||
</ul>;
|
||||
|
||||
export const AdditionalWeedProperties = (props: EditPointPropertiesProps) =>
|
||||
<ul className="additional-weed-properties">
|
||||
<ListItem name={t("Age")}>
|
||||
{`${plantAge(props.point)} ${t("days old")}`}
|
||||
</ListItem>
|
||||
{Object.entries(props.point.body.meta).map(([key, value]) => {
|
||||
switch (key) {
|
||||
case "color":
|
||||
case "type": return <div key={key}
|
||||
className={`meta-${key}-not-displayed`} />;
|
||||
case "created_by":
|
||||
return <ListItem name={t("Source")}>
|
||||
{SOURCE_LOOKUP()[value || ""] || t("unknown")}
|
||||
</ListItem>;
|
||||
case "removal_method":
|
||||
return <ListItem name={t("Removal method")}>
|
||||
<div className="weed-removal-method-section">
|
||||
{REMOVAL_METHODS.map(method =>
|
||||
<div className={"weed-removal-method"} key={method}>
|
||||
<input type="radio" name="weed-removal-method"
|
||||
checked={value == method}
|
||||
onChange={() => {
|
||||
const newMeta = cloneDeep(props.point.body.meta);
|
||||
newMeta.removal_method = method;
|
||||
props.updatePoint({ meta: newMeta });
|
||||
}} />
|
||||
<label>{t(method)}</label>
|
||||
</div>)}
|
||||
</div>
|
||||
</ListItem>;
|
||||
default:
|
||||
return <ListItem name={key}>
|
||||
{value || ""}
|
||||
</ListItem>;
|
||||
}
|
||||
})}
|
||||
</ul>;
|
||||
|
||||
const REMOVAL_METHODS = ["automatic", "manual"];
|
||||
|
||||
const SOURCE_LOOKUP = (): Record<string, string> => ({
|
||||
"plant-detection": t("Weed Detector"),
|
||||
"farm-designer": t("Farm Designer"),
|
||||
});
|
||||
|
||||
export interface PointActionsProps {
|
||||
x: number;
|
||||
y: number;
|
||||
|
|
|
@ -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<EditPointProps, {}> {
|
|||
? <div className={"point-panel-content-wrapper"}>
|
||||
<EditPointProperties point={this.point}
|
||||
updatePoint={updatePoint(this.point, this.props.dispatch)} />
|
||||
<ul className="meta">
|
||||
{Object.entries(this.point.body.meta).map(([key, value]) => {
|
||||
switch (key) {
|
||||
case "color":
|
||||
case "created_by":
|
||||
case "type":
|
||||
return <div key={key}
|
||||
className={`meta-${key}-not-displayed`} />;
|
||||
default:
|
||||
return <ListItem key={key} name={key}>
|
||||
{value || ""}
|
||||
</ListItem>;
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
<PointActions
|
||||
x={this.point.body.x}
|
||||
y={this.point.body.y}
|
||||
|
|
|
@ -19,13 +19,15 @@ import {
|
|||
import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
import { Props } from "./interfaces";
|
||||
import { TaggedPlant } from "./map/interfaces";
|
||||
import { TaggedPlant, BotSize } from "./map/interfaces";
|
||||
import { RestResources } from "../resources/interfaces";
|
||||
import { isString, uniq, chain } from "lodash";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
|
||||
import { getFirmwareConfig } from "../resources/getters";
|
||||
import { calcMicrostepsPerMm } from "../controls/move/direction_axes_props";
|
||||
import { getBotSize } from "./map/util";
|
||||
import { getDefaultAxisLength } from ".";
|
||||
|
||||
const plantFinder = (plants: TaggedPlant[]) =>
|
||||
(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));
|
||||
};
|
||||
|
|
|
@ -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("<AddTool />", () => {
|
||||
const fakeProps = (): AddToolProps => ({
|
||||
|
@ -58,7 +59,7 @@ describe("<AddTool />", () => {
|
|||
it("saves", async () => {
|
||||
mockSave = () => Promise.resolve();
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => typeof x === "function" && x());
|
||||
p.dispatch = mockDispatch();
|
||||
const wrapper = shallow<AddTool>(<AddTool {...p} />);
|
||||
wrapper.setState({ toolName: "Foo" });
|
||||
await wrapper.find(SaveBtn).simulate("click");
|
||||
|
@ -70,7 +71,7 @@ describe("<AddTool />", () => {
|
|||
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<AddTool>(<AddTool {...p} />);
|
||||
wrapper.setState({ toolName: "Foo" });
|
||||
await wrapper.find(SaveBtn).simulate("click");
|
||||
|
|
|
@ -42,10 +42,12 @@ describe("<EditToolSlot />", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
p.findToolSlot = () => fakeToolSlot();
|
||||
const toolSlot = fakeToolSlot();
|
||||
toolSlot.body.meta = { meta_key: "meta value" };
|
||||
p.findToolSlot = () => toolSlot;
|
||||
const wrapper = mount(<EditToolSlot {...p} />);
|
||||
["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));
|
||||
});
|
||||
|
||||
|
|
|
@ -52,6 +52,17 @@ export class RawEditToolSlot extends React.Component<EditToolSlotProps> {
|
|||
quadrant={this.props.quadrant}
|
||||
isActive={this.props.isActive}
|
||||
updateToolSlot={this.updateSlot(toolSlot)} />
|
||||
<ul className="meta">
|
||||
{Object.entries(toolSlot.body.meta).map(([key, value]) => {
|
||||
switch (key) {
|
||||
default:
|
||||
return <li key={key}>
|
||||
<label>{key}</label>
|
||||
<div>{value}</div>
|
||||
</li>;
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
<button
|
||||
className="fb-button gray no-float"
|
||||
title={t("move to this location")}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { TaggedWeedPointer } from "farmbot";
|
|||
import { maybeFindWeedPointerById } from "../../resources/selectors";
|
||||
import { Panel } from "../panel_header";
|
||||
import {
|
||||
EditPointProperties, PointActions, updatePoint,
|
||||
EditPointProperties, PointActions, updatePoint, AdditionalWeedProperties,
|
||||
} from "../points/point_edit_actions";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
|
@ -50,6 +50,8 @@ export class RawEditWeed extends React.Component<EditWeedProps, {}> {
|
|||
? <div className={"weed-panel-content-wrapper"}>
|
||||
<EditPointProperties point={this.point}
|
||||
updatePoint={updatePoint(this.point, this.props.dispatch)} />
|
||||
<AdditionalWeedProperties point={this.point}
|
||||
updatePoint={updatePoint(this.point, this.props.dispatch)} />
|
||||
<PointActions
|
||||
x={this.point.body.x}
|
||||
y={this.point.body.y}
|
||||
|
|
|
@ -25,6 +25,10 @@ describe("<EditZone />", () => {
|
|||
const fakeProps = (): EditZoneProps => ({
|
||||
dispatch: jest.fn(),
|
||||
findZone: () => undefined,
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true },
|
||||
},
|
||||
});
|
||||
|
||||
it("redirects", () => {
|
||||
|
|
|
@ -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<EditZoneProps, {}> {
|
||||
|
@ -54,6 +58,7 @@ export class RawEditZone extends React.Component<EditZoneProps, {}> {
|
|||
group={zone}
|
||||
criteria={zone.body.criteria}
|
||||
dispatch={this.props.dispatch}
|
||||
botSize={this.props.botSize}
|
||||
editGroupAreaInMap={true} />
|
||||
</div>
|
||||
: <span>{t("Redirecting")}...</span>}
|
||||
|
|
|
@ -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("<TileSetServoAngle/>", () => {
|
||||
const currentStep: SetServoAngle = {
|
||||
|
@ -23,7 +24,7 @@ describe("<TileSetServoAngle/>", () => {
|
|||
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,
|
||||
|
|
|
@ -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}>
|
||||
<input type="checkbox"
|
||||
className={props.color ?? ""}
|
||||
title={props.title}
|
||||
onChange={props.onChange}
|
||||
checked={props.checked} />
|
||||
|
|
|
@ -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"}>
|
||||
<i className={`fa fa-${props.customIcon || "question-circle"} help-icon`} />
|
||||
<div className={"help-text-content"}>{t(props.text)}</div>
|
||||
|
|
Loading…
Reference in New Issue