Compare commits
59 Commits
Author | SHA1 | Date |
---|---|---|
server | 53bc33fc3e | |
Rick Carlino | 1a7ee04d0b | |
gabrielburnworth | 4a7a683ba7 | |
Rick Carlino | 8700d50c81 | |
gabrielburnworth | 3bab5694b8 | |
Rick Carlino | e205214ba4 | |
gabrielburnworth | 73e9daed05 | |
gabrielburnworth | 426f97ddc2 | |
Rick Carlino | 88e526cce3 | |
Rick Carlino | 9e14c2125d | |
Rick Carlino | 889c78c77a | |
Rick Carlino | 3b1dbe2209 | |
gabrielburnworth | 980d39f70d | |
Rick Carlino | 461f4c2509 | |
gabrielburnworth | d2176fd6ea | |
gabrielburnworth | c9511593a3 | |
Rick Carlino | 66553d143d | |
Rick Carlino | 696350343b | |
Rick Carlino | 1f773c44fc | |
Rick Carlino | 87c22d4a96 | |
Rick Carlino | 9f35dd9992 | |
gabrielburnworth | 14bf5216e0 | |
gabrielburnworth | 1d196d633a | |
Rick Carlino | de607e3e3a | |
gabrielburnworth | d931cd1b84 | |
gabrielburnworth | b3f93dd678 | |
Rick Carlino | 7f9ecd450d | |
gabrielburnworth | 69462e4b60 | |
gabrielburnworth | d3732aed20 | |
Rick Carlino | 6213028f0f | |
gabrielburnworth | 281813369e | |
Rick Carlino | 1014eece5f | |
gabrielburnworth | 6f484ab2e3 | |
Rick Carlino | 0bd6d9a967 | |
gabrielburnworth | 25b2f18c4c | |
Rick Carlino | 1556084dbd | |
Rick Carlino | 0571100229 | |
Rick Carlino | d6909f439c | |
Rick Carlino | 36b5c90b65 | |
Rick Carlino | f3ac957485 | |
Rick Carlino | 6f834517ca | |
Rick Carlino | 44c3f7dc4e | |
Rick Carlino | 5bb77c1c14 | |
Rick Carlino | 3ee1478a58 | |
Rick Carlino | df9e0ef26b | |
Rick Carlino | 0e02ca06ee | |
Rick Carlino | 643bcb1a37 | |
Rick Carlino | 88b20a73ea | |
Rick Carlino | e8a8165635 | |
Rick Carlino | 588d4eb36e | |
Rick Carlino | efea80b593 | |
gabrielburnworth | c75d93f3c4 | |
gabrielburnworth | bee1e0e074 | |
Rick Carlino | 4375a935f0 | |
Rick Carlino | 7d5fe7c9f6 | |
Rick Carlino | e801d53d51 | |
Rick Carlino | 046035ab9e | |
Rick Carlino | 52b481e831 | |
Rick Carlino | 1e1b405c32 |
|
@ -16,9 +16,9 @@ module CeleryScriptSettingsBag
|
|||
end
|
||||
|
||||
PIN_TYPE_MAP = { "Peripheral" => Peripheral,
|
||||
"Sensor" => Sensor,
|
||||
"BoxLed3" => BoxLed,
|
||||
"BoxLed4" => BoxLed }
|
||||
"Sensor" => Sensor,
|
||||
"BoxLed3" => BoxLed,
|
||||
"BoxLed4" => BoxLed }
|
||||
ALLOWED_AXIS = %w(x y z all)
|
||||
ALLOWED_ASSERTION_TYPES = %w(abort recover abort_recover continue)
|
||||
ALLOWED_CHANGES = %w(add remove update)
|
||||
|
@ -33,16 +33,16 @@ module CeleryScriptSettingsBag
|
|||
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant Weed)
|
||||
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot Weed GenericPointer)
|
||||
ALLOWED_RPC_NODES = %w(assertion calibrate change_ownership
|
||||
check_updates dump_info emergency_lock
|
||||
check_updates emergency_lock
|
||||
emergency_unlock execute execute_script
|
||||
factory_reset find_home flash_firmware home
|
||||
install_farmware install_first_party_farmware _if
|
||||
move_absolute move_relative power_off read_pin
|
||||
read_status reboot remove_farmware resource_update
|
||||
read_status reboot remove_farmware update_resource
|
||||
send_message set_servo_angle set_user_env sync
|
||||
take_photo toggle_pin update_farmware wait
|
||||
write_pin zero)
|
||||
ALLOWED_SPEC_ACTION = %w(dump_info emergency_lock emergency_unlock power_off
|
||||
ALLOWED_SPEC_ACTION = %w(emergency_lock emergency_unlock power_off
|
||||
read_status reboot sync take_photo)
|
||||
ANY_VARIABLE = %i(tool coordinate point identifier)
|
||||
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
|
||||
|
@ -73,8 +73,7 @@ module CeleryScriptSettingsBag
|
|||
ONLY_ONE_COORD = "Move Absolute does not accept a group of locations " \
|
||||
"as input. Please change your selection to a single" \
|
||||
" location."
|
||||
PLANT_STAGES = %w(planned planted harvested sprouted)
|
||||
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
|
||||
PLANT_STAGES = %w(planned planted harvested sprouted removed)
|
||||
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
|
||||
MISC_ENUM_ERR = '"%s" is not valid. Allowed values: %s'
|
||||
MAX_WAIT_MS = 1000 * 60 * 3 # Three Minutes
|
||||
|
@ -82,6 +81,13 @@ module CeleryScriptSettingsBag
|
|||
"A single wait node cannot exceed #{MAX_WAIT_MS / 1000 / 60} minutes. " +
|
||||
"Consider lowering the wait time or using multiple WAIT blocks."
|
||||
Corpus = CeleryScript::Corpus.new
|
||||
THIS_IS_DEPRECATED = {
|
||||
args: [:resource_type, :resource_id, :label, :value],
|
||||
tags: [:function, :api_writer, :network_user],
|
||||
blk: ->(n) do
|
||||
n.invalidate!("Deprecated `mark_as` detected. Delete it and re-add")
|
||||
end,
|
||||
}
|
||||
|
||||
CORPUS_VALUES = {
|
||||
boolean: [TrueClass, FalseClass],
|
||||
|
@ -278,6 +284,9 @@ module CeleryScriptSettingsBag
|
|||
lua: {
|
||||
defn: [v(:string)],
|
||||
},
|
||||
resource: {
|
||||
defn: [n(:identifier), n(:resource)],
|
||||
},
|
||||
}.map do |(name, conf)|
|
||||
blk = conf[:blk]
|
||||
defn = conf.fetch(:defn)
|
||||
|
@ -317,10 +326,6 @@ module CeleryScriptSettingsBag
|
|||
args: [:x, :y, :z],
|
||||
tags: [:data, :location_like],
|
||||
},
|
||||
dump_info: {
|
||||
tags: [:function, :network_user, :disk_user, :api_writer],
|
||||
docs: "Sends an info dump to server administrators for troubleshooting.",
|
||||
},
|
||||
emergency_lock: {
|
||||
tags: [:function, :firmware_user, :control_flow],
|
||||
},
|
||||
|
@ -513,15 +518,22 @@ module CeleryScriptSettingsBag
|
|||
tags: [:function, :firmware_user, :rpi_user],
|
||||
blk: ->(n) { no_rpi_analog(n) },
|
||||
},
|
||||
resource_update: {
|
||||
args: RESOURCE_UPDATE_ARGS,
|
||||
tags: [:function, :api_writer, :network_user],
|
||||
# DEPRECATED- Get rid of this node ASAP -RC 15 APR 2020
|
||||
resource_update: THIS_IS_DEPRECATED,
|
||||
resource: {
|
||||
args: [:resource_type, :resource_id],
|
||||
tags: [:network_user],
|
||||
blk: ->(n) do
|
||||
resource_type = n.args.fetch(:resource_type).value
|
||||
resource_id = n.args.fetch(:resource_id).value
|
||||
check_resource_type(n, resource_type, resource_id, Device.current)
|
||||
end,
|
||||
},
|
||||
update_resource: {
|
||||
args: [:resource],
|
||||
body: [:pair],
|
||||
tags: [:function, :api_writer, :network_user],
|
||||
},
|
||||
point_group: {
|
||||
args: [:point_group_id],
|
||||
tags: [:data, :list_like],
|
||||
|
@ -529,7 +541,7 @@ module CeleryScriptSettingsBag
|
|||
resource_id = n.args.fetch(:point_group_id).value
|
||||
check_resource_type(n, "PointGroup", resource_id, Device.current)
|
||||
end,
|
||||
}
|
||||
},
|
||||
}.map { |(name, list)| Corpus.node(name, **list) }
|
||||
|
||||
HASH = Corpus.as_json
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
class PinBinding < ApplicationRecord
|
||||
OFF_LIMITS = [
|
||||
2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 24, 25, 27
|
||||
OFF_LIMITS = [
|
||||
2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 24, 25, 27,
|
||||
]
|
||||
BAD_PIN_NUM = \
|
||||
"The following pin numbers cannot be used: %s" % OFF_LIMITS.join(", ")
|
||||
BAD_PIN_NUM = "The following pin numbers cannot be used: %s" % OFF_LIMITS.join(", ")
|
||||
|
||||
belongs_to :device
|
||||
belongs_to :sequence
|
||||
enum special_action: { dump_info: "dump_info",
|
||||
emergency_lock: "emergency_lock",
|
||||
enum special_action: { emergency_lock: "emergency_lock",
|
||||
emergency_unlock: "emergency_unlock",
|
||||
power_off: "power_off",
|
||||
read_status: "read_status",
|
||||
reboot: "reboot",
|
||||
sync: "sync",
|
||||
take_photo: "take_photo" }
|
||||
power_off: "power_off",
|
||||
read_status: "read_status",
|
||||
reboot: "reboot",
|
||||
sync: "sync",
|
||||
take_photo: "take_photo" }
|
||||
validates :pin_num, uniqueness: { scope: :device }
|
||||
|
||||
def fancy_name
|
||||
|
|
|
@ -27,11 +27,19 @@ module Points
|
|||
end
|
||||
|
||||
def execute
|
||||
Point.transaction { point.update!(inputs.except(:point)) && point }
|
||||
Point.transaction { point.update!(update_params) && point }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def merged_meta_fields
|
||||
@merged_meta_fields ||= (point.meta || {}).merge(meta || {})
|
||||
end
|
||||
|
||||
def update_params
|
||||
@update_params ||= inputs.except(:point).merge(meta: merged_meta_fields)
|
||||
end
|
||||
|
||||
def new_tool_id?
|
||||
raw_inputs.key?("tool_id")
|
||||
end
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
class BasePointSerializer < ApplicationSerializer
|
||||
attributes :device_id, :name, :pointer_type, :meta, :x, :y, :z
|
||||
|
||||
# PROBLEM:
|
||||
# * Users need a mutable way to mark a plant's creation time => `planted_at`
|
||||
# * DB Admin needs to know the _real_ created_at time.
|
||||
# * We can't change field names (or destroy data) that is in use by legacy devices
|
||||
#
|
||||
# SOLUTION:
|
||||
# * Don't allow users to modify `created_at`
|
||||
# * Provide `planted_at` if possible.
|
||||
# * Always provide `planted_at` if it is available
|
||||
# * Provide a read-only view of `created_at` if `planted_at` is `nil`
|
||||
def planted_at
|
||||
object.planted_at || object.created_at
|
||||
end
|
||||
|
||||
def created_at
|
||||
planted_at
|
||||
end
|
||||
|
||||
def meta
|
||||
object.meta || {}
|
||||
end
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
class WeedSerializer < BasePointSerializer
|
||||
attributes :radius, :discarded_at
|
||||
attributes :radius, :discarded_at, :plant_stage
|
||||
end
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# How to install FarmBot Web API on a Debian Buster (10) Machine
|
||||
|
||||
# IMPORTANT NOTE: Resources are limited and Farmbot, inc. cannot provide
|
||||
# longterm support to self-hosted users. If you have never administered a
|
||||
# Ruby on Rails application, we highly advise stopping now. this presents an
|
||||
# extremely high risk of data loss. Free hosting is provided at
|
||||
# https://my.farm.bot and eliminates the risks and troubles of self-hosting.
|
||||
#
|
||||
# You are highly encouraged to use the my.farm.bot servers. Self hosted
|
||||
# documentation is provided with the assumption that you have experience with
|
||||
# Ruby/Javascript development.
|
||||
#
|
||||
# Self-hosting a Farmbot server is not a simple task.
|
||||
|
||||
# Remove old (possibly broke) docker versions
|
||||
sudo apt-get remove docker docker-engine docker.io
|
||||
|
||||
# Install docker
|
||||
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common gnupg2 --yes
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
|
||||
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable" --yes
|
||||
sudo apt-get update --yes
|
||||
sudo apt-get install docker-ce --yes
|
||||
sudo docker run hello-world # Should run!
|
||||
# Install docker-compose
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
# Install FarmBot Web App
|
||||
# âš SKIP THIS STEP IF UPGRADING!
|
||||
git clone https://github.com/FarmBot/Farmbot-Web-App --depth=5 --branch=master
|
||||
|
||||
cd Farmbot-Web-App
|
||||
|
||||
#snap install micro --classic # Don't like `micro`? vim, nano, etc are fine, too.
|
||||
cp example.env .env # âš SKIP THIS STEP IF UPGRADING!
|
||||
|
||||
# == This is a very important step!!! ==
|
||||
#
|
||||
# Open `.env` in a text editor and change all the values.
|
||||
#
|
||||
# == Nothing will work if you skip this step!!! ==
|
||||
|
||||
vim .env # âš SKIP THIS STEP IF UPGRADING!
|
||||
# ^ This is the most important step
|
||||
# READ NOTE ABOVE. Very important!
|
||||
|
||||
# Install the correct version of bundler for the project
|
||||
sudo docker-compose run web gem install bundler:2.1.4
|
||||
# Install application specific Ruby dependencies
|
||||
sudo docker-compose run web bundle install
|
||||
# Install application specific Javascript deps
|
||||
sudo docker-compose run web npm install
|
||||
# Create a database in PostgreSQL
|
||||
sudo docker-compose run web bundle exec rails db:create db:migrate
|
||||
# Generate a set of *.pem files for data encryption
|
||||
sudo docker-compose run web rake keys:generate # âš SKIP THIS STEP IF UPGRADING!
|
||||
# Build the UI assets via ParcelJS
|
||||
sudo docker-compose run web rake assets:precompile
|
||||
# Run the server! ٩(^‿^)۶
|
||||
# NOTE: DONT TRY TO LOGIN until you see a message similar to this:
|
||||
# "✨ Built in 44.92s"
|
||||
# THIS MAY TAKE A VERY LONG TIME ON SLOW MACHINES (~3 minutes on DigitalOcean)
|
||||
# You will just get an empty screen otherwise.
|
||||
# This only happens during initialization
|
||||
sudo docker-compose up
|
||||
|
||||
# At this point, setup is complete. Content should be visible at ===============
|
||||
# http://YOUR_HOST:3000/.
|
||||
|
||||
# You can optionally verify installation by running unit tests.
|
||||
|
||||
# Create the database for the app to use:
|
||||
sudo docker-compose run -e RAILS_ENV=test web bundle exec rails db:setup
|
||||
# Run the tests in the "test" RAILS_ENV:
|
||||
sudo docker-compose run -e RAILS_ENV=test web rspec spec
|
||||
# Run user-interface unit tests REQUIRES AT LEAST 4 GB OF RAM:
|
||||
sudo docker-compose run web npm run test
|
||||
|
||||
# === BEGIN OPTIONAL UPGRADES
|
||||
# To update to later versions of FarmBot,
|
||||
# shut down the server, create a database backup
|
||||
# and run commands below.
|
||||
git pull https://github.com/FarmBot/Farmbot-Web-App.git master
|
||||
sudo docker-compose build
|
||||
sudo docker-compose run web bundle install # <== âš UPGRADE USERS ONLY
|
||||
sudo docker-compose run web npm install # <== âš UPGRADE USERS ONLY
|
||||
sudo docker-compose run web rails db:migrate # <== âš UPGRADE USERS ONLY
|
||||
# === END OPTIONAL UPGRADES ^
|
||||
|
|
@ -19,4 +19,5 @@ export const fakeDesignerState = (): DesignerState => ({
|
|||
openedSavedGarden: undefined,
|
||||
tryGroupSortType: undefined,
|
||||
editGroupAreaInMap: false,
|
||||
settingsSearchTerm: "",
|
||||
});
|
||||
|
|
|
@ -134,7 +134,6 @@ export function fakeToolSlot(): TaggedToolSlotPointer {
|
|||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
radius: 25,
|
||||
pointer_type: "ToolSlot",
|
||||
meta: {},
|
||||
tool_id: undefined,
|
||||
|
@ -181,6 +180,7 @@ export function fakeWeed(): TaggedWeedPointer {
|
|||
y: 400,
|
||||
z: 0,
|
||||
radius: 100,
|
||||
plant_stage: "planned",
|
||||
meta: { created_by: "plant-detection", color: "red" }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ jest.mock("../toast/toast", () => ({
|
|||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
busy: jest.fn(),
|
||||
removeToast: jest.fn(),
|
||||
}));
|
||||
|
|
|
@ -265,7 +265,6 @@ const tr11: TaggedPoint = {
|
|||
"pointer_type": "ToolSlot",
|
||||
"pullout_direction": 0,
|
||||
"gantry_mounted": false,
|
||||
"radius": 25,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"z": 10,
|
||||
|
@ -329,6 +328,7 @@ const tr16: TaggedPoint = {
|
|||
},
|
||||
name: "untitled",
|
||||
pointer_type: "Weed",
|
||||
plant_stage: "planned",
|
||||
radius: 10,
|
||||
x: 490,
|
||||
y: 421,
|
||||
|
|
|
@ -37,7 +37,9 @@ import { getDevice } from "../../../device";
|
|||
import { talk } from "browser-speech";
|
||||
import { MessageType } from "../../../sequences/interfaces";
|
||||
import { FbjsEventName } from "farmbot/dist/constants";
|
||||
import { info, error, success, warning, fun, busy } from "../../../toast/toast";
|
||||
import {
|
||||
info, error, success, warning, fun, busy, removeToast,
|
||||
} from "../../../toast/toast";
|
||||
import { onLogs } from "../../log_handlers";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { globalQueue } from "../../batch_queue";
|
||||
|
@ -177,7 +179,8 @@ describe("onOffline", () => {
|
|||
jest.resetAllMocks();
|
||||
onOffline();
|
||||
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
|
||||
expect(error).toHaveBeenCalledWith(Content.MQTT_DISCONNECTED);
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
Content.MQTT_DISCONNECTED, "Error", "red", "offline");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -186,13 +189,17 @@ describe("onOnline", () => {
|
|||
jest.resetAllMocks();
|
||||
onOnline();
|
||||
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
|
||||
expect(removeToast).toHaveBeenCalledWith("offline");
|
||||
});
|
||||
});
|
||||
|
||||
describe("onReconnect", () => {
|
||||
onReconnect();
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
"Attempting to reconnect to the message broker", "Offline", "yellow");
|
||||
describe("onReconnect()", () => {
|
||||
it("sends reconnect toast", () => {
|
||||
onReconnect();
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
"Attempting to reconnect to the message broker",
|
||||
"Offline", "yellow", "offline");
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeLastClientConnected", () => {
|
||||
|
@ -268,7 +275,8 @@ describe("onPublicBroadcast", () => {
|
|||
console.log = jest.fn();
|
||||
onPublicBroadcast({});
|
||||
expectBroadcastLog();
|
||||
expect(window.alert).toHaveBeenCalledWith(Content.FORCE_REFRESH_CANCEL_WARNING);
|
||||
expect(window.alert).toHaveBeenCalledWith(
|
||||
Content.FORCE_REFRESH_CANCEL_WARNING);
|
||||
expect(location.assign).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,9 @@ import { Log } from "farmbot/dist/resources/api_resources";
|
|||
import { Farmbot, BotStateTree, TaggedResource } from "farmbot";
|
||||
import { FbjsEventName } from "farmbot/dist/constants";
|
||||
import { noop } from "lodash";
|
||||
import { success, error, info, warning, fun, busy } from "../toast/toast";
|
||||
import {
|
||||
success, error, info, warning, fun, busy, removeToast,
|
||||
} from "../toast/toast";
|
||||
import { HardwareState } from "../devices/interfaces";
|
||||
import { GetState, ReduxAction } from "../redux/interfaces";
|
||||
import { Content, Actions } from "../constants";
|
||||
|
@ -102,11 +104,6 @@ export function readStatus() {
|
|||
.then(() => { commandOK(noun); }, commandErr(noun));
|
||||
}
|
||||
|
||||
export const onOffline = () => {
|
||||
dispatchNetworkDown("user.mqtt", now());
|
||||
error(t(Content.MQTT_DISCONNECTED));
|
||||
};
|
||||
|
||||
export const changeLastClientConnected = (bot: Farmbot) => () => {
|
||||
bot.setUserEnv({
|
||||
"LAST_CLIENT_CONNECTED": JSON.stringify(new Date())
|
||||
|
@ -157,14 +154,20 @@ export function onMalformed() {
|
|||
}
|
||||
}
|
||||
|
||||
export const onOnline =
|
||||
() => {
|
||||
success(t("Reconnected to the message broker."), t("Online"));
|
||||
dispatchNetworkUp("user.mqtt", now());
|
||||
};
|
||||
export const onReconnect =
|
||||
() => warning(t("Attempting to reconnect to the message broker"),
|
||||
t("Offline"), "yellow");
|
||||
export const onOnline = () => {
|
||||
removeToast("offline");
|
||||
success(t("Reconnected to the message broker."), t("Online"));
|
||||
dispatchNetworkUp("user.mqtt", now());
|
||||
};
|
||||
|
||||
export const onReconnect = () =>
|
||||
warning(t("Attempting to reconnect to the message broker"),
|
||||
t("Offline"), "yellow", "offline");
|
||||
|
||||
export const onOffline = () => {
|
||||
dispatchNetworkDown("user.mqtt", now());
|
||||
error(t(Content.MQTT_DISCONNECTED), t("Error"), "red", "offline");
|
||||
};
|
||||
|
||||
export function onPublicBroadcast(payl: unknown) {
|
||||
console.log(FbjsEventName.publicBroadcast, payl);
|
||||
|
|
|
@ -346,10 +346,7 @@ export namespace ToolTips {
|
|||
trim(`The Mark As step allows FarmBot to programmatically edit the
|
||||
properties of the UTM, plants, and weeds from within a sequence.
|
||||
For example, you can mark a plant as "planted" during a seeding
|
||||
sequence or delete a weed after removing it.`);
|
||||
|
||||
export const REBOOT =
|
||||
trim(`Power cycle FarmBot's onboard computer.`);
|
||||
sequence or mark a weed as "removed" after removing it.`);
|
||||
|
||||
export const SET_SERVO_ANGLE =
|
||||
trim(`Move a servo to the provided angle. An angle of 90 degrees
|
||||
|
@ -362,6 +359,9 @@ export namespace ToolTips {
|
|||
export const MOVE_TO_HOME =
|
||||
trim(`Move FarmBot to home for the provided axis.`);
|
||||
|
||||
export const ASSERTION =
|
||||
trim(`Evaluate Lua commands. For power users and software developers.`);
|
||||
|
||||
export const FIRMWARE_ACTION =
|
||||
trim(`FarmBot OS or micro-controller firmware action.`);
|
||||
|
||||
|
@ -740,6 +740,15 @@ export namespace Content {
|
|||
encoders, stall detection, or endstops enabled for the chosen axis.
|
||||
Enable endstops, encoders, or stall detection from the Device page for: `);
|
||||
|
||||
export const REBOOT_STEP =
|
||||
trim(`Power cycle FarmBot's onboard computer.`);
|
||||
|
||||
export const SHUTDOWN_STEP =
|
||||
trim(`Power down FarmBot's onboard computer.`);
|
||||
|
||||
export const ESTOP_STEP =
|
||||
trim(`Unlocking a device requires user intervention.`);
|
||||
|
||||
export const IN_USE =
|
||||
trim(`Used in another resource. Protected from deletion.`);
|
||||
|
||||
|
@ -923,6 +932,8 @@ export namespace TourContent {
|
|||
}
|
||||
|
||||
export enum DeviceSetting {
|
||||
axisHeadingLabels = ``,
|
||||
|
||||
// Homing and calibration
|
||||
homingAndCalibration = `Homing and Calibration`,
|
||||
homing = `Homing`,
|
||||
|
@ -974,6 +985,11 @@ export enum DeviceSetting {
|
|||
|
||||
// Pin Guard
|
||||
pinGuard = `Pin Guard`,
|
||||
pinGuard1 = `Pin Guard 1`,
|
||||
pinGuard2 = `Pin Guard 2`,
|
||||
pinGuard3 = `Pin Guard 3`,
|
||||
pinGuard4 = `Pin Guard 4`,
|
||||
pinGuard5 = `Pin Guard 5`,
|
||||
|
||||
// Danger Zone
|
||||
dangerZone = `Danger Zone`,
|
||||
|
@ -981,6 +997,8 @@ export enum DeviceSetting {
|
|||
|
||||
// Pin Bindings
|
||||
pinBindings = `Pin Bindings`,
|
||||
savedPinBindings = `Saved pin bindings`,
|
||||
addNewPinBinding = `Add new pin binding`,
|
||||
|
||||
// FarmBot OS
|
||||
farmbot = `FarmBot`,
|
||||
|
@ -1131,6 +1149,7 @@ export enum Actions {
|
|||
SET_DRAWN_WEED_DATA = "SET_DRAWN_WEED_DATA",
|
||||
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
||||
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
||||
SET_SETTINGS_SEARCH_TERM = "SET_SETTINGS_SEARCH_TERM",
|
||||
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
|
||||
|
||||
// Regimens
|
||||
|
|
|
@ -38,6 +38,7 @@ $pink: #ebb;
|
|||
$light_red: #e99;
|
||||
$red: #e66;
|
||||
$dark_red: #f00;
|
||||
$medium_dark_red: #c00;
|
||||
$darkest_red: #900;
|
||||
$panel_green: #35761b;
|
||||
$panel_light_green: #f3f9f1;
|
||||
|
|
|
@ -158,6 +158,17 @@
|
|||
left: 1rem;
|
||||
cursor: default !important;
|
||||
}
|
||||
.fa-times {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0.5rem;
|
||||
color: $darkest_red;
|
||||
font-size: 1.3rem;
|
||||
&:hover {
|
||||
color: $medium_dark_red;
|
||||
}
|
||||
}
|
||||
}
|
||||
input {
|
||||
background: transparent;
|
||||
|
@ -553,10 +564,10 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
.more-bugs,
|
||||
.select-mode,
|
||||
.move-to-mode {
|
||||
button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
margin: auto;
|
||||
margin-top: 1rem;
|
||||
p {
|
||||
text-align: center;
|
||||
padding-top: 2rem;
|
||||
|
|
|
@ -291,10 +291,19 @@
|
|||
.panel-action-buttons {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
height: 19rem;
|
||||
height: 16rem;
|
||||
width: 100%;
|
||||
background: $panel_medium_light_gray;
|
||||
padding: 0.5rem;
|
||||
&.status {
|
||||
height: 20rem;
|
||||
}
|
||||
&.more {
|
||||
height: 23rem;
|
||||
}
|
||||
&.more.status {
|
||||
height: 26rem;
|
||||
}
|
||||
button {
|
||||
margin: 0.5rem;
|
||||
float: left;
|
||||
|
@ -303,10 +312,12 @@
|
|||
min-width: -webkit-fill-available;
|
||||
margin-bottom: 0px;
|
||||
margin-left: .5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
.button-row {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.filter-search {
|
||||
padding-right: 1rem;
|
||||
|
@ -323,15 +334,35 @@
|
|||
line-height: 4.1rem;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin-right: 1rem;
|
||||
line-height: 2.5rem;
|
||||
p {
|
||||
display: inline;
|
||||
font-size: 1.4rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-content {
|
||||
padding-top: 19rem;
|
||||
padding-top: 16rem;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
padding-bottom: 5rem;
|
||||
max-height: calc(100vh - 13rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
&.status {
|
||||
padding-top: 20rem;
|
||||
}
|
||||
&.more {
|
||||
padding-top: 23rem;
|
||||
}
|
||||
&.more.status {
|
||||
padding-top: 26rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -773,19 +804,101 @@
|
|||
}
|
||||
}
|
||||
|
||||
.no-pad {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-panel-content {
|
||||
max-height: calc(100vh - 15rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-top: 5rem;
|
||||
padding: 0;
|
||||
margin-top: 6rem;
|
||||
padding-bottom: 5rem;
|
||||
button {
|
||||
margin-top: 1.75rem;
|
||||
.section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
p {
|
||||
padding: 0.5rem;
|
||||
.bulk-expand-controls {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.row:first-child {
|
||||
margin-right: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.row:nth-child(2) {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
.label-headings {
|
||||
margin-right: 2rem;
|
||||
label {
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
.release-notes-wrapper {
|
||||
float: right !important;
|
||||
}
|
||||
.network-not-found-timer {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.pin-guard-input-row {
|
||||
.row {
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
.pin-bindings {
|
||||
margin-right: 1rem;
|
||||
.row {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
div[class*=col-] {
|
||||
padding: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.bindings-list {
|
||||
margin-left: -5px;
|
||||
.binding-action {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
.pin-binding-input-rows {
|
||||
margin-right: 1rem;
|
||||
margin-left: -15px;
|
||||
label {
|
||||
margin-left: 1rem !important;
|
||||
}
|
||||
.green {
|
||||
float: left;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.row:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.stock-pin-bindings-button {
|
||||
display: inline;
|
||||
button {
|
||||
margin: 0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fb-button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
label {
|
||||
margin: 0 !important;
|
||||
line-height: 3rem;
|
||||
}
|
||||
.bp3-popover-wrapper {
|
||||
display: inline;
|
||||
float: none;
|
||||
}
|
||||
.map-size-inputs {
|
||||
.row {
|
||||
|
@ -795,6 +908,31 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
.help-icon {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.all-settings-content {
|
||||
max-height: calc(100vh - 22rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
.expandable-header {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.designer-settings {
|
||||
max-height: calc(100vh - 14rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-right: -10px;
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.designer-setting {
|
||||
&.disabled {
|
||||
input {
|
||||
|
|
|
@ -1314,6 +1314,50 @@ ul {
|
|||
}
|
||||
}
|
||||
|
||||
.update-resource-step {
|
||||
.update-resource-step-resource {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.update-resource-pair {
|
||||
margin-top: 0;
|
||||
margin-right: -2rem;
|
||||
div[class*=col-] {
|
||||
padding: 0;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
.custom-meta-field {
|
||||
position: relative;
|
||||
input {
|
||||
height: 3rem;
|
||||
}
|
||||
.fa-undo {
|
||||
position: absolute;
|
||||
top: 0.65rem;
|
||||
right: 0.5rem;
|
||||
color: $medium_light_gray;
|
||||
&:hover {
|
||||
color: $dark_gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-field-warning {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
i,
|
||||
p {
|
||||
display: inline;
|
||||
cursor: default !important;
|
||||
margin-right: 0.5rem;
|
||||
color: $darkest_red;
|
||||
}
|
||||
.did-you-mean {
|
||||
cursor: pointer !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.farmware-name-manual-input {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
&.take-photo-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.resource-update-step {
|
||||
&.update-resource-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.set-servo-angle-step {
|
||||
|
@ -136,6 +136,9 @@
|
|||
&.reboot-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.shutdown-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.unknown-step {
|
||||
background: $gray;
|
||||
}
|
||||
|
@ -226,7 +229,7 @@
|
|||
&.take-photo-step a {
|
||||
color: $dark_brown;
|
||||
}
|
||||
&.resource-update-step {
|
||||
&.update-resource-step {
|
||||
background: $light_brown;
|
||||
}
|
||||
&.set-servo-angle-step {
|
||||
|
@ -253,6 +256,9 @@
|
|||
&.emergency-stop-step {
|
||||
background: $light_red;
|
||||
}
|
||||
&.shutdown-step {
|
||||
background: $light_brown;
|
||||
}
|
||||
&.reboot-step {
|
||||
background: $light_brown;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
jest.mock("../../redux/store", () => ({ store: jest.fn() }));
|
||||
|
||||
import { botReducer, initialState } from "../reducer";
|
||||
import { Actions } from "../../constants";
|
||||
import { ControlPanelState, BotState } from "../interfaces";
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
jest.mock("../../actions", () => ({
|
||||
toggleControlPanel: jest.fn(),
|
||||
bulkToggleControlPanel: jest.fn(),
|
||||
}));
|
||||
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
const mockState = fakeState();
|
||||
jest.mock("../../../redux/store", () => ({
|
||||
store: { getState: () => mockState },
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
|
@ -9,7 +16,7 @@ import {
|
|||
} from "../maybe_highlight";
|
||||
import { DeviceSetting } from "../../../constants";
|
||||
import { panelState } from "../../../__test_support__/control_panel_state";
|
||||
import { toggleControlPanel } from "../../actions";
|
||||
import { toggleControlPanel, bulkToggleControlPanel } from "../../actions";
|
||||
|
||||
describe("<Highlight />", () => {
|
||||
const fakeProps = (): HighlightProps => ({
|
||||
|
@ -25,6 +32,24 @@ describe("<Highlight />", () => {
|
|||
wrapper.instance().componentDidMount();
|
||||
expect(wrapper.state().className).toEqual("unhighlight");
|
||||
});
|
||||
|
||||
it("doesn't hide: no search term", () => {
|
||||
mockState.resources.consumers.farm_designer.settingsSearchTerm = "";
|
||||
const wrapper = mount(<Highlight {...fakeProps()} />);
|
||||
expect(wrapper.find("div").first().props().hidden).toEqual(false);
|
||||
});
|
||||
|
||||
it("doesn't hide: matches search term", () => {
|
||||
mockState.resources.consumers.farm_designer.settingsSearchTerm = "motor";
|
||||
const wrapper = mount(<Highlight {...fakeProps()} />);
|
||||
expect(wrapper.find("div").first().props().hidden).toEqual(false);
|
||||
});
|
||||
|
||||
it("hides", () => {
|
||||
mockState.resources.consumers.farm_designer.settingsSearchTerm = "encoder";
|
||||
const wrapper = mount(<Highlight {...fakeProps()} />);
|
||||
expect(wrapper.find("div").first().props().hidden).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeHighlight()", () => {
|
||||
|
@ -78,4 +103,11 @@ describe("maybeOpenPanel()", () => {
|
|||
maybeOpenPanel(panelState())(jest.fn());
|
||||
expect(toggleControlPanel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes other panels", () => {
|
||||
location.search = "?highlight=motors";
|
||||
maybeOpenPanel(panelState(), true)(jest.fn());
|
||||
expect(toggleControlPanel).toHaveBeenCalledWith("motors");
|
||||
expect(bulkToggleControlPanel).toHaveBeenCalledWith(false, true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,11 +9,12 @@ import { settingToggle } from "../../actions";
|
|||
import {
|
||||
buildResourceIndex,
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { DeviceSetting } from "../../../constants";
|
||||
|
||||
describe("<PinGuardMCUInputGroup/>", () => {
|
||||
const fakeProps = (): PinGuardMCUInputGroupProps => {
|
||||
return {
|
||||
label: "Pin Guard 1",
|
||||
label: DeviceSetting.pinGuard1,
|
||||
pinNumKey: "pin_guard_1_pin_nr",
|
||||
timeoutKey: "pin_guard_1_time_out",
|
||||
activeStateKey: "pin_guard_1_active_state",
|
||||
|
|
|
@ -13,11 +13,12 @@ import {
|
|||
import { TaggedFirmwareConfig } from "farmbot";
|
||||
import { FBSelect } from "../../../ui";
|
||||
import { updateMCU } from "../../actions";
|
||||
import { DeviceSetting } from "../../../constants";
|
||||
|
||||
describe("<PinNumberDropdown />", () => {
|
||||
const fakeProps =
|
||||
(firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({
|
||||
label: "Pin Guard 1",
|
||||
label: DeviceSetting.pinGuard1,
|
||||
pinNumKey: "pin_guard_1_pin_nr",
|
||||
timeoutKey: "pin_guard_1_time_out",
|
||||
activeStateKey: "pin_guard_1_active_state",
|
||||
|
|
|
@ -43,16 +43,6 @@ describe("<BoardType/>", () => {
|
|||
expect(wrapper.text()).toContain("Farmduino");
|
||||
});
|
||||
|
||||
it("sets sending status", () => {
|
||||
const wrapper = mount<BoardType>(<BoardType {...fakeProps()} />);
|
||||
expect(wrapper.state().sending).toBeFalsy();
|
||||
const p = fakeProps();
|
||||
p.sourceFbosConfig = () => ({ value: true, consistent: false });
|
||||
wrapper.setProps(p);
|
||||
wrapper.mount();
|
||||
expect(wrapper.state().sending).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls updateConfig", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount<BoardType>(<BoardType {...p} />);
|
||||
|
|
|
@ -11,12 +11,15 @@ import { OsUpdateButton } from "../os_update_button";
|
|||
import { OsUpdateButtonProps } from "../interfaces";
|
||||
import { ShouldDisplay } from "../../../interfaces";
|
||||
import { Content } from "../../../../constants";
|
||||
import { ConfigurationName } from "farmbot";
|
||||
|
||||
const UPDATE_CHANNEL = "update_channel" as ConfigurationName;
|
||||
|
||||
describe("<OsUpdateButton/>", () => {
|
||||
beforeEach(() => {
|
||||
bot.currentOSVersion = "6.1.6";
|
||||
bot.hardware.informational_settings.controller_version = "6.1.6";
|
||||
bot.hardware.configuration.beta_opt_in = false;
|
||||
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = "stable";
|
||||
});
|
||||
|
||||
const fakeProps = (): OsUpdateButtonProps => ({
|
||||
|
@ -33,7 +36,6 @@ describe("<OsUpdateButton/>", () => {
|
|||
availableVersion: string | undefined;
|
||||
availableBetaVersion: string | undefined;
|
||||
availableBetaCommit: string | undefined;
|
||||
betaOptIn: boolean | undefined;
|
||||
onBeta: boolean | undefined;
|
||||
update_available?: boolean | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
|
@ -46,7 +48,6 @@ describe("<OsUpdateButton/>", () => {
|
|||
availableVersion: "6.1.6",
|
||||
availableBetaVersion: undefined,
|
||||
availableBetaCommit: undefined,
|
||||
betaOptIn: false,
|
||||
onBeta: false,
|
||||
shouldDisplay: () => false,
|
||||
update_channel: "stable",
|
||||
|
@ -104,7 +105,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
expected: Results) => {
|
||||
const {
|
||||
installedVersion, installedCommit, onBeta, update_available,
|
||||
availableVersion, availableBetaVersion, availableBetaCommit, betaOptIn,
|
||||
availableVersion, availableBetaVersion, availableBetaCommit,
|
||||
shouldDisplay, update_channel,
|
||||
} = testProps;
|
||||
bot.hardware.informational_settings.controller_version = installedVersion;
|
||||
|
@ -115,9 +116,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
bot.currentOSVersion = availableVersion;
|
||||
bot.currentBetaOSVersion = availableBetaVersion;
|
||||
bot.currentBetaOSCommit = availableBetaCommit;
|
||||
bot.hardware.configuration.beta_opt_in = betaOptIn;
|
||||
// tslint:disable-next-line:no-any
|
||||
(bot.hardware.configuration as any).update_channel = update_channel;
|
||||
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = update_channel;
|
||||
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = shouldDisplay;
|
||||
|
@ -156,7 +155,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.6";
|
||||
testProps.availableVersion = undefined;
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
const expectedResults = cantConnect("release server");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
@ -166,7 +165,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
testProps.installedVersion = "6.1.6";
|
||||
testProps.availableVersion = undefined;
|
||||
testProps.availableBetaVersion = "6.1.7-beta";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
const expectedResults = updateNeeded("6.1.7-beta");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
@ -175,7 +174,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.6";
|
||||
testProps.availableBetaVersion = undefined;
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
const expectedResults = upToDate("6.1.6");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
@ -205,7 +204,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.5";
|
||||
testProps.availableBetaVersion = "7.0.0-beta";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
const expectedResults = updateNeeded("7.0.0-beta");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
@ -214,7 +213,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.6";
|
||||
testProps.availableBetaVersion = "6.1.6-beta";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
const expectedResults = upToDate("6.1.6");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
@ -223,7 +222,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.6";
|
||||
testProps.availableBetaVersion = "6.1.6-beta";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
testProps.onBeta = true;
|
||||
const expectedResults = updateNeeded("6.1.6");
|
||||
testButtonState(testProps, expectedResults);
|
||||
|
@ -233,7 +232,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.6";
|
||||
testProps.availableBetaVersion = "6.1.6-beta";
|
||||
testProps.betaOptIn = false;
|
||||
testProps.update_channel = "stable";
|
||||
testProps.onBeta = true;
|
||||
const expectedResults = updateNeeded("6.1.6");
|
||||
testButtonState(testProps, expectedResults);
|
||||
|
@ -243,7 +242,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.7";
|
||||
testProps.availableBetaVersion = "6.1.7-beta";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
testProps.onBeta = true;
|
||||
const expectedResults = upToDate("6.1.7-beta");
|
||||
testButtonState(testProps, expectedResults);
|
||||
|
@ -253,7 +252,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.7-beta";
|
||||
testProps.availableBetaVersion = "6.1.7-beta";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
const expectedResults = upToDate("6.1.7-beta");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
@ -264,7 +263,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
testProps.installedCommit = "old commit";
|
||||
testProps.availableBetaVersion = "7.0.0-beta";
|
||||
testProps.availableBetaCommit = "new commit";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
testProps.onBeta = true;
|
||||
const expectedResults = updateNeeded("7.0.0-beta");
|
||||
testButtonState(testProps, expectedResults);
|
||||
|
@ -273,7 +272,7 @@ describe("<OsUpdateButton/>", () => {
|
|||
it("handles installed version newer than available (beta enabled)", () => {
|
||||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.7";
|
||||
testProps.betaOptIn = true;
|
||||
testProps.update_channel = "beta";
|
||||
testProps.onBeta = false;
|
||||
testProps.availableBetaVersion = "6.1.7-beta";
|
||||
const expectedResults = upToDate("6.1.7-beta");
|
||||
|
@ -308,16 +307,6 @@ describe("<OsUpdateButton/>", () => {
|
|||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
||||
it("doesn't use update_channel value", () => {
|
||||
const testProps = defaultTestProps();
|
||||
testProps.installedVersion = "6.1.6";
|
||||
testProps.shouldDisplay = () => false;
|
||||
testProps.update_channel = "beta";
|
||||
testProps.availableBetaVersion = "6.1.7-beta";
|
||||
const expectedResults = upToDate("6.1.6");
|
||||
testButtonState(testProps, expectedResults);
|
||||
});
|
||||
|
||||
it("compares release candidates: newer", () => {
|
||||
const testProps = defaultTestProps();
|
||||
testProps.availableVersion = "6.1.5";
|
||||
|
|
|
@ -13,17 +13,7 @@ import { Highlight } from "../maybe_highlight";
|
|||
import { DeviceSetting } from "../../../constants";
|
||||
import { DevSettings } from "../../../account/dev/dev_support";
|
||||
|
||||
interface BoardTypeState { sending: boolean }
|
||||
|
||||
export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
||||
state = {
|
||||
sending: this.sending
|
||||
};
|
||||
|
||||
UNSAFE_componentWillReceiveProps() {
|
||||
this.setState({ sending: this.sending });
|
||||
}
|
||||
|
||||
export class BoardType extends React.Component<BoardTypeProps, {}> {
|
||||
get sending() {
|
||||
return !this.props.sourceFbosConfig("firmware_hardware").consistent;
|
||||
}
|
||||
|
@ -39,15 +29,14 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
|||
if (selectedItem && isFwHardwareValue(firmware_hardware)) {
|
||||
info(t("Sending firmware configuration..."), t("Sending"));
|
||||
this.props.dispatch(updateConfig({ firmware_hardware }));
|
||||
this.setState({ sending: true });
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
FirmwareSelection = () =>
|
||||
<FBSelect
|
||||
key={this.props.firmwareHardware}
|
||||
extraClass={this.state.sending ? "dim" : ""}
|
||||
key={this.props.firmwareHardware + "" + this.sending}
|
||||
extraClass={this.sending ? "dim" : ""}
|
||||
list={getFirmwareChoices()}
|
||||
selectedItem={this.selectedBoard}
|
||||
onChange={this.sendOffConfig} />
|
||||
|
|
|
@ -263,7 +263,7 @@ export function FbosDetails(props: FbosDetailsProps) {
|
|||
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled,
|
||||
wifi_level_percent, cpu_usage, private_ip,
|
||||
} = props.botInfoSettings;
|
||||
const { last_ota, last_ota_checkup } = props.deviceAccount.body;
|
||||
const { last_ota, last_ota_checkup, fbos_version } = props.deviceAccount.body;
|
||||
const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---";
|
||||
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
|
||||
|
||||
|
@ -273,6 +273,7 @@ export function FbosDetails(props: FbosDetailsProps) {
|
|||
botToMqttLastSeen={props.botToMqttLastSeen}
|
||||
timeSettings={props.timeSettings}
|
||||
device={props.deviceAccount} />
|
||||
<p><b>{t("Version last seen")}: </b>{fbos_version}</p>
|
||||
<p><b>{t("Environment")}: </b>{env}</p>
|
||||
<CommitDisplay title={t("Commit")}
|
||||
repo={FarmBotRepo.FarmBotOS} commit={commit} />
|
||||
|
|
|
@ -4,7 +4,7 @@ import { SemverResult, semverCompare } from "../../../util";
|
|||
import { OsUpdateButtonProps } from "./interfaces";
|
||||
import { checkControllerUpdates } from "../../actions";
|
||||
import { isString } from "lodash";
|
||||
import { BotState, Feature } from "../../interfaces";
|
||||
import { BotState } from "../../interfaces";
|
||||
import { Content } from "../../../constants";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
|
||||
|
@ -154,9 +154,8 @@ export const OsUpdateButton = (props: OsUpdateButtonProps) => {
|
|||
const { controller_version } = bot.hardware.informational_settings;
|
||||
|
||||
/** FBOS beta release opt-in setting. */
|
||||
const betaOptIn = props.shouldDisplay(Feature.use_update_channel)
|
||||
? sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable"
|
||||
: !!sourceFbosConfig("beta_opt_in").value;
|
||||
const betaOptIn =
|
||||
sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable";
|
||||
/** FBOS update availability. */
|
||||
const buttonStatusProps = buttonVersionStatus({ bot, betaOptIn });
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ export function DangerZone(props: DangerZoneProps) {
|
|||
<Highlight settingName={DeviceSetting.resetHardwareParams}>
|
||||
<Row>
|
||||
<Col xs={newFormat ? 8 : 4}>
|
||||
<label>
|
||||
<label style={{ lineHeight: "1.5rem" }}>
|
||||
{t(DeviceSetting.resetHardwareParams)}
|
||||
</label>
|
||||
</Col>
|
||||
|
|
|
@ -44,7 +44,7 @@ export function PinGuard(props: PinGuardProps) {
|
|||
</Col>
|
||||
</Row>}
|
||||
<PinGuardMCUInputGroup
|
||||
label={t("Pin Guard {{ num }}", { num: 1 })}
|
||||
label={DeviceSetting.pinGuard1}
|
||||
pinNumKey={"pin_guard_1_pin_nr"}
|
||||
timeoutKey={"pin_guard_1_time_out"}
|
||||
activeStateKey={"pin_guard_1_active_state"}
|
||||
|
@ -52,7 +52,7 @@ export function PinGuard(props: PinGuardProps) {
|
|||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinGuardMCUInputGroup
|
||||
label={t("Pin Guard {{ num }}", { num: 2 })}
|
||||
label={DeviceSetting.pinGuard2}
|
||||
pinNumKey={"pin_guard_2_pin_nr"}
|
||||
timeoutKey={"pin_guard_2_time_out"}
|
||||
activeStateKey={"pin_guard_2_active_state"}
|
||||
|
@ -60,7 +60,7 @@ export function PinGuard(props: PinGuardProps) {
|
|||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinGuardMCUInputGroup
|
||||
label={t("Pin Guard {{ num }}", { num: 3 })}
|
||||
label={DeviceSetting.pinGuard3}
|
||||
pinNumKey={"pin_guard_3_pin_nr"}
|
||||
timeoutKey={"pin_guard_3_time_out"}
|
||||
activeStateKey={"pin_guard_3_active_state"}
|
||||
|
@ -68,7 +68,7 @@ export function PinGuard(props: PinGuardProps) {
|
|||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinGuardMCUInputGroup
|
||||
label={t("Pin Guard {{ num }}", { num: 4 })}
|
||||
label={DeviceSetting.pinGuard4}
|
||||
pinNumKey={"pin_guard_4_pin_nr"}
|
||||
timeoutKey={"pin_guard_4_time_out"}
|
||||
activeStateKey={"pin_guard_4_active_state"}
|
||||
|
@ -76,7 +76,7 @@ export function PinGuard(props: PinGuardProps) {
|
|||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinGuardMCUInputGroup
|
||||
label={t("Pin Guard {{ num }}", { num: 5 })}
|
||||
label={DeviceSetting.pinGuard5}
|
||||
pinNumKey={"pin_guard_5_pin_nr"}
|
||||
timeoutKey={"pin_guard_5_time_out"}
|
||||
activeStateKey={"pin_guard_5_active_state"}
|
||||
|
|
|
@ -2,28 +2,32 @@ import * as React from "react";
|
|||
import { Row, Col } from "../../../ui/index";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { DevSettings } from "../../../account/dev/dev_support";
|
||||
import { Highlight } from "../maybe_highlight";
|
||||
import { DeviceSetting } from "../../../constants";
|
||||
|
||||
export function SpacePanelHeader() {
|
||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||
const width = newFormat ? 4 : 2;
|
||||
const offset = newFormat ? 0 : 6;
|
||||
return <div className="label-headings">
|
||||
<Row>
|
||||
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
|
||||
<label>
|
||||
{t("X AXIS")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={width} className={"centered-button-div"}>
|
||||
<label>
|
||||
{t("Y AXIS")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={width} className={"centered-button-div"}>
|
||||
<label>
|
||||
{t("Z AXIS")}
|
||||
</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<Highlight settingName={DeviceSetting.axisHeadingLabels}>
|
||||
<Row>
|
||||
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
|
||||
<label>
|
||||
{t("X AXIS")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={width} className={"centered-button-div"}>
|
||||
<label>
|
||||
{t("Y AXIS")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={width} className={"centered-button-div"}>
|
||||
<label>
|
||||
{t("Z AXIS")}
|
||||
</label>
|
||||
</Col>
|
||||
</Row>
|
||||
</Highlight>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ export interface NumericMCUInputGroupProps {
|
|||
export interface PinGuardMCUInputGroupProps {
|
||||
sourceFwConfig: SourceFwConfig;
|
||||
dispatch: Function;
|
||||
label: string;
|
||||
label: DeviceSetting;
|
||||
pinNumKey: McuParamName;
|
||||
timeoutKey: McuParamName;
|
||||
activeStateKey: McuParamName;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { store } from "../../redux/store";
|
||||
import { ControlPanelState } from "../interfaces";
|
||||
import { toggleControlPanel, bulkToggleControlPanel } from "../actions";
|
||||
import { urlFriendly } from "../../util";
|
||||
|
@ -56,6 +57,11 @@ const ERROR_HANDLING_PANEL = [
|
|||
];
|
||||
const PIN_GUARD_PANEL = [
|
||||
DeviceSetting.pinGuard,
|
||||
DeviceSetting.pinGuard1,
|
||||
DeviceSetting.pinGuard2,
|
||||
DeviceSetting.pinGuard3,
|
||||
DeviceSetting.pinGuard4,
|
||||
DeviceSetting.pinGuard5,
|
||||
];
|
||||
const DANGER_ZONE_PANEL = [
|
||||
DeviceSetting.dangerZone,
|
||||
|
@ -63,6 +69,8 @@ const DANGER_ZONE_PANEL = [
|
|||
];
|
||||
const PIN_BINDINGS_PANEL = [
|
||||
DeviceSetting.pinBindings,
|
||||
DeviceSetting.savedPinBindings,
|
||||
DeviceSetting.addNewPinBinding,
|
||||
];
|
||||
const POWER_AND_RESET_PANEL = [
|
||||
DeviceSetting.powerAndReset,
|
||||
|
@ -183,6 +191,7 @@ export interface HighlightProps {
|
|||
| (React.ReactChild | false)[]
|
||||
| (React.ReactChild | React.ReactChild[])[];
|
||||
className?: string;
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
interface HighlightState {
|
||||
|
@ -200,11 +209,19 @@ export class Highlight extends React.Component<HighlightProps, HighlightState> {
|
|||
}
|
||||
}
|
||||
|
||||
get searchTerm() {
|
||||
const { resources } = store.getState();
|
||||
return resources.consumers.farm_designer.settingsSearchTerm;
|
||||
}
|
||||
|
||||
render() {
|
||||
const show = !this.searchTerm ||
|
||||
this.props.settingName.toLowerCase().includes(this.searchTerm);
|
||||
return <div className={[
|
||||
this.props.className,
|
||||
this.state.className,
|
||||
].join(" ")}>
|
||||
].join(" ")}
|
||||
hidden={!show}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PinNumberDropdown } from "./pin_number_dropdown";
|
|||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { Position } from "@blueprintjs/core";
|
||||
import { Highlight } from "./maybe_highlight";
|
||||
|
||||
export class PinGuardMCUInputGroup
|
||||
extends React.Component<PinGuardMCUInputGroupProps> {
|
||||
|
@ -50,7 +51,7 @@ export class PinGuardMCUInputGroup
|
|||
? <Row>
|
||||
<Col xs={3}>
|
||||
<label>
|
||||
{label}
|
||||
{t(label)}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
|
@ -63,46 +64,48 @@ export class PinGuardMCUInputGroup
|
|||
<this.State />
|
||||
</Col>
|
||||
</Row>
|
||||
: <div className={"pin-guard-input-row"}>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<label>
|
||||
{label}
|
||||
</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={5} xsOffset={1} className="no-pad">
|
||||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
|
||||
position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
<this.Number />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={5} xsOffset={1} className="no-pad">
|
||||
<label>
|
||||
{t("Timeout (sec)")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
<this.Timeout />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={5} xsOffset={1} className="no-pad">
|
||||
<label>
|
||||
{t("To State")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
<this.State />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
: <Highlight settingName={label}>
|
||||
<div className={"pin-guard-input-row"}>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<label>
|
||||
{t(label)}
|
||||
</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={5} xsOffset={1} className="no-pad">
|
||||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
|
||||
position={Position.TOP_RIGHT} />
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
<this.Number />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={5} xsOffset={1} className="no-pad">
|
||||
<label>
|
||||
{t("Timeout (sec)")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
<this.Timeout />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={5} xsOffset={1} className="no-pad">
|
||||
<label>
|
||||
{t("To State")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={5} className="no-pad">
|
||||
<this.State />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Highlight>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,13 @@ export function Diagnosis(props: DiagnosisProps) {
|
|||
<div className={"saucer-connector last " + diagnosisColor} />
|
||||
</Col>
|
||||
<Col xs={10} className={"connectivity-diagnosis"}>
|
||||
<p className="blinking">
|
||||
{t("Always")}
|
||||
<a className="blinking" href="/app/device?highlight=farmbot_os">
|
||||
<u>{t("upgrade FarmBot OS")}</u>
|
||||
</a>
|
||||
{t("before troubleshooting.")}
|
||||
</p>
|
||||
<p>
|
||||
{diagnose(props)}
|
||||
</p>
|
||||
|
|
|
@ -81,6 +81,7 @@ export enum Feature {
|
|||
ota_update_hour = "ota_update_hour",
|
||||
rpi_led_control = "rpi_led_control",
|
||||
sensors = "sensors",
|
||||
update_resource = "update_resource",
|
||||
use_update_channel = "use_update_channel",
|
||||
variables = "variables",
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ export const specialActionLabelLookup: { [x: string]: string } = {
|
|||
|
||||
export const specialActionList: DropDownItem[] =
|
||||
Object.values(PinBindingSpecialAction)
|
||||
.filter(action => action != PinBindingSpecialAction.dump_info)
|
||||
.map((action: PinBindingSpecialAction) =>
|
||||
({ label: specialActionLabelLookup[action], value: action }));
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from "farmbot/dist/resources/api_resources";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { DeviceSetting } from "../../constants";
|
||||
|
||||
export class PinBindingInputGroup
|
||||
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
|
||||
|
@ -129,7 +130,7 @@ export class PinBindingInputGroup
|
|||
render() {
|
||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||
return <div className="pin-binding-input-rows">
|
||||
{newFormat && <Row><label>{t("add new pin binding")}</label></Row>}
|
||||
{newFormat && <Row><label>{t(DeviceSetting.addNewPinBinding)}</label></Row>}
|
||||
{newFormat && <this.Number />}
|
||||
{newFormat && <Row>
|
||||
<Col xs={5}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { Row, Col, Help } from "../../ui";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { ToolTips, DeviceSetting } from "../../constants";
|
||||
import { selectAllPinBindings } from "../../resources/selectors";
|
||||
import { PinBindingsContentProps, PinBindingListItems } from "./interfaces";
|
||||
import { PinBindingsList } from "./pin_bindings_list";
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
} from "farmbot/dist/resources/api_resources";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { Highlight } from "../components/maybe_highlight";
|
||||
|
||||
/** Width of UI columns in Pin Bindings widget. */
|
||||
export enum PinBindingColWidth {
|
||||
|
@ -73,32 +74,38 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
|
|||
const pinBindings = apiPinBindings(resources);
|
||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||
return <div className="pin-bindings">
|
||||
<Row>
|
||||
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
|
||||
position={Position.TOP_RIGHT} />}
|
||||
<StockPinBindingsButton
|
||||
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
||||
<Popover
|
||||
position={Position.TOP_RIGHT}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
portalClassName={"bindings-warning-icon"}
|
||||
popoverClassName={"help"}>
|
||||
<i className="fa fa-exclamation-triangle" />
|
||||
<div className={"pin-binding-warning"}>
|
||||
{t(ToolTips.PIN_BINDING_WARNING)}
|
||||
</div>
|
||||
</Popover>
|
||||
</Row>
|
||||
<Highlight settingName={DeviceSetting.pinBindings}>
|
||||
<Row>
|
||||
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
|
||||
position={Position.TOP_RIGHT} />}
|
||||
<StockPinBindingsButton
|
||||
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
||||
<Popover
|
||||
position={Position.TOP_RIGHT}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
portalClassName={"bindings-warning-icon"}
|
||||
popoverClassName={"help"}>
|
||||
<i className="fa fa-exclamation-triangle" />
|
||||
<div className={"pin-binding-warning"}>
|
||||
{t(ToolTips.PIN_BINDING_WARNING)}
|
||||
</div>
|
||||
</Popover>
|
||||
</Row>
|
||||
</Highlight>
|
||||
<div className={"pin-bindings-list-and-input"}>
|
||||
{!newFormat && <PinBindingsListHeader />}
|
||||
<PinBindingsList
|
||||
pinBindings={pinBindings}
|
||||
dispatch={dispatch}
|
||||
resources={resources} />
|
||||
<PinBindingInputGroup
|
||||
pinBindings={pinBindings}
|
||||
dispatch={dispatch}
|
||||
resources={resources} />
|
||||
<Highlight settingName={DeviceSetting.savedPinBindings}>
|
||||
<PinBindingsList
|
||||
pinBindings={pinBindings}
|
||||
dispatch={dispatch}
|
||||
resources={resources} />
|
||||
</Highlight>
|
||||
<Highlight settingName={DeviceSetting.addNewPinBinding}>
|
||||
<PinBindingInputGroup
|
||||
pinBindings={pinBindings}
|
||||
dispatch={dispatch}
|
||||
resources={resources} />
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import { DevSettings } from "../../account/dev/dev_support";
|
|||
import {
|
||||
PinBindingType, PinBindingSpecialAction,
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
import { DeviceSetting } from "../../constants";
|
||||
|
||||
export const PinBindingsList = (props: PinBindingsListProps) => {
|
||||
const { pinBindings, resources, dispatch } = props;
|
||||
|
@ -41,7 +42,7 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
|
|||
|
||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||
return <div className={"bindings-list"}>
|
||||
{newFormat && <Row><label>{t("saved pin bindings")}</label></Row>}
|
||||
{newFormat && <Row><label>{t(DeviceSetting.savedPinBindings)}</label></Row>}
|
||||
{pinBindings
|
||||
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
|
||||
.map(x => {
|
||||
|
|
|
@ -33,7 +33,7 @@ describe("<MoveTo />", () => {
|
|||
it("moves to location: bot's current z value", () => {
|
||||
const wrapper = mount(<MoveTo {...fakeProps()} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 30 });
|
||||
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
|
||||
});
|
||||
|
||||
it("goes back", () => {
|
||||
|
|
|
@ -191,6 +191,16 @@ describe("designer reducer", () => {
|
|||
expect(newState.tryGroupSortType).toEqual("random");
|
||||
});
|
||||
|
||||
it("sets settings search term", () => {
|
||||
const state = oldState();
|
||||
state.settingsSearchTerm = "";
|
||||
const action: ReduxAction<string> = {
|
||||
type: Actions.SET_SETTINGS_SEARCH_TERM, payload: "random"
|
||||
};
|
||||
const newState = designer(state, action);
|
||||
expect(newState.settingsSearchTerm).toEqual("random");
|
||||
});
|
||||
|
||||
it("enables edit group area in map mode", () => {
|
||||
const state = oldState();
|
||||
state.editGroupAreaInMap = false;
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
jest.mock("../../config_storage/actions", () => ({
|
||||
getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }),
|
||||
setWebAppConfigValue: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import {
|
||||
RawDesignerSettings as DesignerSettings, DesignerSettingsProps,
|
||||
mapStateToProps,
|
||||
} from "../settings";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { BooleanSetting, NumericSetting } from "../../session_keys";
|
||||
import { setWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
||||
const getSetting =
|
||||
(wrapper: ReactWrapper, position: number, containsString: string) => {
|
||||
const setting = wrapper.find(".designer-setting").at(position);
|
||||
expect(setting.text().toLowerCase())
|
||||
.toContain(containsString.toLowerCase());
|
||||
return setting;
|
||||
};
|
||||
|
||||
describe("<DesignerSettings />", () => {
|
||||
const fakeProps = (): DesignerSettingsProps => ({
|
||||
dispatch: jest.fn(),
|
||||
getConfigValue: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders settings", () => {
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("size");
|
||||
const settings = wrapper.find(".designer-setting");
|
||||
expect(settings.length).toEqual(7);
|
||||
});
|
||||
|
||||
it("renders defaultOn setting", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => undefined;
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
const confirmDeletion = getSetting(wrapper, 6, "confirm plant");
|
||||
expect(confirmDeletion.find("button").text()).toEqual("on");
|
||||
});
|
||||
|
||||
it("toggles setting", () => {
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
const trailSetting = getSetting(wrapper, 1, "trail");
|
||||
trailSetting.find("button").simulate("click");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(BooleanSetting.display_trail, true);
|
||||
});
|
||||
|
||||
it("changes origin", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => 2;
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
const originSetting = getSetting(wrapper, 5, "origin");
|
||||
originSetting.find("div").last().simulate("click");
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
NumericSetting.bot_origin_quadrant, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props", () => {
|
||||
const props = mapStateToProps(fakeState());
|
||||
const value = props.getConfigValue(BooleanSetting.show_plants);
|
||||
expect(value).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -18,8 +18,6 @@ import { generateUuid } from "../../resources/util";
|
|||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
const DISCARDED_AT = "2018-01-01T00:00:00.000Z";
|
||||
|
||||
it("hovered plantUUID is undefined", () => {
|
||||
const state = fakeState();
|
||||
state.resources.consumers.farm_designer.hoveredPlant = {
|
||||
|
@ -55,11 +53,8 @@ describe("mapStateToProps()", () => {
|
|||
const webAppConfig = fakeWebAppConfig();
|
||||
(webAppConfig.body as WebAppConfig).show_historic_points = true;
|
||||
const point1 = fakePoint();
|
||||
point1.body.discarded_at = undefined;
|
||||
const point2 = fakePoint();
|
||||
point2.body.discarded_at = DISCARDED_AT;
|
||||
const point3 = fakePoint();
|
||||
point3.body.discarded_at = DISCARDED_AT;
|
||||
state.resources = buildResourceIndex([
|
||||
webAppConfig, point1, point2, point3, fakeDevice(),
|
||||
]);
|
||||
|
@ -71,15 +66,12 @@ describe("mapStateToProps()", () => {
|
|||
const webAppConfig = fakeWebAppConfig();
|
||||
(webAppConfig.body as WebAppConfig).show_historic_points = false;
|
||||
const point1 = fakePoint();
|
||||
point1.body.discarded_at = undefined;
|
||||
const point2 = fakePoint();
|
||||
point2.body.discarded_at = DISCARDED_AT;
|
||||
const point3 = fakePoint();
|
||||
point3.body.discarded_at = DISCARDED_AT;
|
||||
state.resources = buildResourceIndex([
|
||||
webAppConfig, point1, point2, point3, fakeDevice(),
|
||||
]);
|
||||
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
|
||||
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
|
||||
});
|
||||
|
||||
it("returns sensor readings", () => {
|
||||
|
|
|
@ -545,7 +545,10 @@ describe("<RepeatForm />", () => {
|
|||
const fakeProps = (): RepeatFormProps => ({
|
||||
isRegimen: false,
|
||||
fieldGet: jest.fn(key =>
|
||||
"" + ({ endDate: "2017-07-26" } as FarmEventViewModel)[key]),
|
||||
"" + ({
|
||||
endDate: "2017-07-26", endTime: "08:57",
|
||||
startDate: "2017-07-25", startTime: "08:57"
|
||||
} as FarmEventViewModel)[key]),
|
||||
fieldSet: jest.fn(),
|
||||
timeSettings: fakeTimeSettings(),
|
||||
});
|
||||
|
|
|
@ -125,6 +125,7 @@ export interface DesignerState {
|
|||
openedSavedGarden: string | undefined;
|
||||
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
||||
editGroupAreaInMap: boolean;
|
||||
settingsSearchTerm: string;
|
||||
}
|
||||
|
||||
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
import * as React from "react";
|
||||
import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu";
|
||||
import { shallow, mount } from "enzyme";
|
||||
jest.mock("../../../../../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
fakeWebAppConfig,
|
||||
} from "../../../../../__test_support__/fake_state/resources";
|
||||
import { StringConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import { setWebAppConfigValue } from "../../../../../config_storage/actions";
|
||||
import {
|
||||
fakeTimeSettings,
|
||||
} from "../../../../../__test_support__/fake_time_settings";
|
||||
|
||||
const mockConfig = fakeWebAppConfig();
|
||||
jest.mock("../../../../../resources/selectors", () => ({
|
||||
getWebAppConfig: () => mockConfig,
|
||||
assertUuid: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../config_storage/actions", () => ({
|
||||
setWebAppConfigValue: jest.fn(),
|
||||
}));
|
||||
import * as React from "react";
|
||||
import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu";
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
||||
import { StringConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import {
|
||||
fakeTimeSettings,
|
||||
} from "../../../../../__test_support__/fake_time_settings";
|
||||
import { edit, save } from "../../../../../api/crud";
|
||||
import { fakeState } from "../../../../../__test_support__/fake_state";
|
||||
import {
|
||||
buildResourceIndex,
|
||||
} from "../../../../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("<ImageFilterMenu />", () => {
|
||||
mockConfig.body.photo_filter_begin = "";
|
||||
|
@ -45,13 +51,19 @@ describe("<ImageFilterMenu />", () => {
|
|||
["endDate", "photo_filter_end", 2],
|
||||
])("sets filter: %s", (filter, key, i) => {
|
||||
const p = fakeProps();
|
||||
const state = fakeState();
|
||||
const config = fakeWebAppConfig();
|
||||
state.resources = buildResourceIndex([config]);
|
||||
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "2001-01-03" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("2001-01-03");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(key, "2001-01-03T00:00:00.000Z");
|
||||
expect(edit).toHaveBeenCalledWith(config, {
|
||||
[key]: "2001-01-03T00:00:00.000Z"
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(config.uuid);
|
||||
});
|
||||
|
||||
it.each<[
|
||||
|
@ -61,14 +73,64 @@ describe("<ImageFilterMenu />", () => {
|
|||
["endTime", "photo_filter_end", 3],
|
||||
])("sets filter: %s", (filter, key, i) => {
|
||||
const p = fakeProps();
|
||||
const state = fakeState();
|
||||
const config = fakeWebAppConfig();
|
||||
state.resources = buildResourceIndex([config]);
|
||||
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "05:00" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("05:00");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(key, "2001-01-03T05:00:00.000Z");
|
||||
expect(edit).toHaveBeenCalledWith(config, {
|
||||
[key]: "2001-01-03T05:00:00.000Z"
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(config.uuid);
|
||||
});
|
||||
|
||||
it.each<[
|
||||
"beginDate" | "endDate",
|
||||
"photo_filter_begin" | "photo_filter_end",
|
||||
number
|
||||
]>([
|
||||
["beginDate", "photo_filter_begin", 0],
|
||||
["endDate", "photo_filter_end", 2],
|
||||
])("unsets filter: %s", (filter, key, i) => {
|
||||
const p = fakeProps();
|
||||
const state = fakeState();
|
||||
const config = fakeWebAppConfig();
|
||||
state.resources = buildResourceIndex([config]);
|
||||
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual(undefined);
|
||||
// tslint:disable-next-line:no-null-keyword
|
||||
expect(edit).toHaveBeenCalledWith(config, { [key]: null });
|
||||
expect(save).toHaveBeenCalledWith(config.uuid);
|
||||
});
|
||||
|
||||
it.each<[
|
||||
"beginTime" | "endTime", number
|
||||
]>([
|
||||
["beginTime", 1],
|
||||
["endTime", 3],
|
||||
])("doesn't set filter: %s", (filter, i) => {
|
||||
const p = fakeProps();
|
||||
const state = fakeState();
|
||||
const config = fakeWebAppConfig();
|
||||
state.resources = buildResourceIndex([config]);
|
||||
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "05:00" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("05:00");
|
||||
expect(edit).not.toHaveBeenCalled();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads values from config", () => {
|
||||
|
@ -83,14 +145,34 @@ describe("<ImageFilterMenu />", () => {
|
|||
|
||||
it("changes slider", () => {
|
||||
const p = fakeProps();
|
||||
const state = fakeState();
|
||||
const config = fakeWebAppConfig();
|
||||
state.resources = buildResourceIndex([config]);
|
||||
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
|
||||
p.getConfigValue = () => undefined;
|
||||
p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z";
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.instance().sliderChange(1);
|
||||
expect(wrapper.instance().state.slider).toEqual(1);
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith("photo_filter_begin", "2001-01-02T00:00:00.000Z");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith("photo_filter_end", "2001-01-03T00:00:00.000Z");
|
||||
expect(edit).toHaveBeenCalledWith(config, {
|
||||
photo_filter_begin: "2001-01-02T00:00:00.000Z",
|
||||
photo_filter_end: "2001-01-03T00:00:00.000Z",
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(config.uuid);
|
||||
});
|
||||
|
||||
it("doesn't update config", () => {
|
||||
const p = fakeProps();
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([]);
|
||||
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
|
||||
p.getConfigValue = () => 1;
|
||||
p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z";
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.instance().sliderChange(1);
|
||||
expect(wrapper.instance().state.slider).toEqual(1);
|
||||
expect(edit).not.toHaveBeenCalled();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("displays slider labels", () => {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { BlurableInput } from "../../../../ui/index";
|
||||
import { offsetTime } from "../../../farm_events/edit_fe_form";
|
||||
import {
|
||||
setWebAppConfigValue, GetWebAppConfigValue,
|
||||
} from "../../../../config_storage/actions";
|
||||
import { GetWebAppConfigValue } from "../../../../config_storage/actions";
|
||||
import moment from "moment";
|
||||
import {
|
||||
formatDate, formatTime,
|
||||
|
@ -11,8 +9,13 @@ import {
|
|||
import { Slider } from "@blueprintjs/core";
|
||||
import { t } from "../../../../i18next_wrapper";
|
||||
import { TimeSettings } from "../../../../interfaces";
|
||||
import { StringConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import { GetState } from "../../../../redux/interfaces";
|
||||
import { getWebAppConfig } from "../../../../resources/getters";
|
||||
import { edit, save } from "../../../../api/crud";
|
||||
import { isString, isUndefined } from "lodash";
|
||||
|
||||
interface ImageFilterMenuState {
|
||||
interface FullImageFilterMenuState {
|
||||
beginDate: string | undefined;
|
||||
beginTime: string | undefined;
|
||||
endDate: string | undefined;
|
||||
|
@ -20,6 +23,8 @@ interface ImageFilterMenuState {
|
|||
slider: number;
|
||||
}
|
||||
|
||||
type ImageFilterMenuState = Partial<FullImageFilterMenuState>;
|
||||
|
||||
export interface ImageFilterMenuProps {
|
||||
timeSettings: TimeSettings;
|
||||
dispatch: Function;
|
||||
|
@ -28,26 +33,48 @@ export interface ImageFilterMenuProps {
|
|||
}
|
||||
|
||||
export class ImageFilterMenu
|
||||
extends React.Component<ImageFilterMenuProps, Partial<ImageFilterMenuState>> {
|
||||
constructor(props: ImageFilterMenuProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
extends React.Component<ImageFilterMenuProps, ImageFilterMenuState> {
|
||||
state: ImageFilterMenuState = {};
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
const { newestDate, toOldest } = this.props.imageAgeInfo;
|
||||
componentDidMount() {
|
||||
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
|
||||
this.setState({
|
||||
slider: toOldest + 1 - (beginDatetime
|
||||
? Math.abs(moment(beginDatetime.toString())
|
||||
.diff(moment(newestDate).clone(), "days")) : 0)
|
||||
});
|
||||
if (isString(beginDatetime) || isUndefined(beginDatetime)) {
|
||||
this.updateSliderState(beginDatetime);
|
||||
}
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps() {
|
||||
this.updateState();
|
||||
}
|
||||
updateSliderState = (begin: string | undefined) => {
|
||||
const { newestDate, toOldest } = this.props.imageAgeInfo;
|
||||
const offset = begin ? Math.abs(moment(begin.toString())
|
||||
.diff(moment(newestDate).clone(), "days")) : 0;
|
||||
this.setState({ slider: toOldest + 1 - offset });
|
||||
};
|
||||
|
||||
setValues = (update: StringValueUpdate) => {
|
||||
Object.entries(update).map(([key, value]) => {
|
||||
switch (key) {
|
||||
case "photo_filter_begin":
|
||||
this.updateSliderState(value);
|
||||
value
|
||||
? this.setState({
|
||||
beginDate: formatDate(value.toString(), this.props.timeSettings),
|
||||
beginTime: formatTime(value.toString(), this.props.timeSettings),
|
||||
})
|
||||
: this.setState({ beginDate: undefined, beginTime: undefined });
|
||||
break;
|
||||
case "photo_filter_end":
|
||||
value
|
||||
? this.setState({
|
||||
endDate: formatDate(value.toString(), this.props.timeSettings),
|
||||
endTime: formatTime(value.toString(), this.props.timeSettings),
|
||||
})
|
||||
: this.setState({ endDate: undefined, endTime: undefined });
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.props.dispatch(setWebAppConfigValues(update));
|
||||
};
|
||||
|
||||
updateState = () => {
|
||||
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
|
||||
|
@ -70,27 +97,27 @@ export class ImageFilterMenu
|
|||
const input = e.currentTarget.value;
|
||||
this.setState({ [datetime]: input });
|
||||
const { beginDate, beginTime, endDate, endTime } = this.state;
|
||||
const { dispatch, timeSettings } = this.props;
|
||||
const { timeSettings } = this.props;
|
||||
let value = undefined;
|
||||
switch (datetime) {
|
||||
case "beginDate":
|
||||
value = offsetTime(input, beginTime || "00:00", timeSettings);
|
||||
dispatch(setWebAppConfigValue("photo_filter_begin", value));
|
||||
this.setValues({ photo_filter_begin: value });
|
||||
break;
|
||||
case "beginTime":
|
||||
if (beginDate) {
|
||||
value = offsetTime(beginDate, input, timeSettings);
|
||||
dispatch(setWebAppConfigValue("photo_filter_begin", value));
|
||||
this.setValues({ photo_filter_begin: value });
|
||||
}
|
||||
break;
|
||||
case "endDate":
|
||||
value = offsetTime(input, endTime || "00:00", timeSettings);
|
||||
dispatch(setWebAppConfigValue("photo_filter_end", value));
|
||||
this.setValues({ photo_filter_end: value });
|
||||
break;
|
||||
case "endTime":
|
||||
if (endDate) {
|
||||
value = offsetTime(endDate, input, timeSettings);
|
||||
dispatch(setWebAppConfigValue("photo_filter_end", value));
|
||||
this.setValues({ photo_filter_end: value });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -100,13 +127,12 @@ export class ImageFilterMenu
|
|||
sliderChange = (slider: number) => {
|
||||
const { newestDate, toOldest } = this.props.imageAgeInfo;
|
||||
this.setState({ slider });
|
||||
const { dispatch, timeSettings } = this.props;
|
||||
const { timeSettings } = this.props;
|
||||
const calcDate = (day: number) =>
|
||||
moment(newestDate).subtract(toOldest - day, "days").toISOString();
|
||||
const begin = offsetTime(calcDate(slider - 1), "00:00", timeSettings);
|
||||
const end = offsetTime(calcDate(slider), "00:00", timeSettings);
|
||||
dispatch(setWebAppConfigValue("photo_filter_begin", begin));
|
||||
dispatch(setWebAppConfigValue("photo_filter_end", end));
|
||||
this.setValues({ photo_filter_begin: begin, photo_filter_end: end });
|
||||
}
|
||||
|
||||
renderLabel = (day: number) => {
|
||||
|
@ -191,3 +217,14 @@ export class ImageFilterMenu
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
type StringValueUpdate = Partial<Record<StringConfigKey, string | undefined>>;
|
||||
|
||||
const setWebAppConfigValues = (update: StringValueUpdate) =>
|
||||
(dispatch: Function, getState: GetState) => {
|
||||
const webAppConfig = getWebAppConfig(getState().resources.index);
|
||||
if (webAppConfig) {
|
||||
dispatch(edit(webAppConfig, update));
|
||||
dispatch(save(webAppConfig.uuid));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -73,7 +73,7 @@ export class SpreadCircle extends
|
|||
React.Component<SpreadCircleProps, SpreadCircleState> {
|
||||
state: SpreadCircleState = { spread: undefined };
|
||||
|
||||
UNSAFE_componentWillMount = () => {
|
||||
componentDidMount = () => {
|
||||
cachedCrop(this.props.plant.body.openfarm_slug)
|
||||
.then(({ spread }) => this.setState({ spread }));
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ describe("<ToolSlotLayer/>", () => {
|
|||
pointer_type: "ToolSlot",
|
||||
tool_id: undefined,
|
||||
name: "Name",
|
||||
radius: 50,
|
||||
x: 1,
|
||||
y: 2,
|
||||
z: 3,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { BooleanSetting } from "../../../session_keys";
|
|||
import { DevSettings } from "../../../account/dev/dev_support";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { Feature } from "../../../devices/interfaces";
|
||||
import { SelectModeLink } from "../../plants/select_plants";
|
||||
|
||||
export const ZoomControls = ({ zoom, getConfigValue }: {
|
||||
zoom: (value: number) => () => void,
|
||||
|
@ -109,6 +110,7 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
|
|||
<ZoomControls zoom={props.zoom} getConfigValue={props.getConfigValue} />
|
||||
<LayerToggles {...props} />
|
||||
<MoveModeLink />
|
||||
<SelectModeLink />
|
||||
<BugsControls />
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -43,7 +43,7 @@ interface MoveToFormState {
|
|||
}
|
||||
|
||||
export class MoveToForm extends React.Component<MoveToFormProps, MoveToFormState> {
|
||||
state = { z: undefined };
|
||||
state = { z: this.props.chosenLocation.z };
|
||||
|
||||
get vector(): { x: number, y: number, z: number } {
|
||||
const { chosenLocation } = this.props;
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
jest.mock("../../../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { EditPlantStatusProps } from "../plant_panel";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
fakePlant, fakeWeed,
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { edit } from "../../../api/crud";
|
||||
import {
|
||||
EditPlantStatus, PlantStatusBulkUpdateProps, PlantStatusBulkUpdate,
|
||||
EditWeedStatus, EditWeedStatusProps,
|
||||
} from "../edit_plant_status";
|
||||
|
||||
describe("<EditPlantStatus />", () => {
|
||||
const fakeProps = (): EditPlantStatusProps => ({
|
||||
uuid: "Plant.0.0",
|
||||
plantStatus: "planned",
|
||||
updatePlant: jest.fn(),
|
||||
});
|
||||
|
||||
it("changes stage to planted", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<EditPlantStatus {...p} />);
|
||||
wrapper.find("FBSelect").simulate("change", { value: "planted" });
|
||||
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
|
||||
plant_stage: "planted",
|
||||
planted_at: expect.stringContaining("Z")
|
||||
});
|
||||
});
|
||||
|
||||
it("changes stage to planned", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<EditPlantStatus {...p} />);
|
||||
wrapper.find("FBSelect").simulate("change", { value: "planned" });
|
||||
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
|
||||
plant_stage: "planned",
|
||||
planted_at: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<PlantStatusBulkUpdate />", () => {
|
||||
const fakeProps = (): PlantStatusBulkUpdateProps => ({
|
||||
allPoints: [],
|
||||
selected: [],
|
||||
dispatch: jest.fn(),
|
||||
pointerType: "Plant",
|
||||
});
|
||||
|
||||
it("doesn't update plant statuses", () => {
|
||||
const p = fakeProps();
|
||||
const plant1 = fakePlant();
|
||||
const plant2 = fakePlant();
|
||||
p.allPoints = [plant1, plant2];
|
||||
p.selected = [plant1.uuid];
|
||||
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
|
||||
window.confirm = jest.fn(() => false);
|
||||
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect(edit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates plant statuses", () => {
|
||||
const p = fakeProps();
|
||||
const plant1 = fakePlant();
|
||||
const plant2 = fakePlant();
|
||||
const plant3 = fakePlant();
|
||||
p.allPoints = [plant1, plant2, plant3];
|
||||
p.selected = [plant1.uuid, plant2.uuid];
|
||||
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
|
||||
window.confirm = jest.fn(() => true);
|
||||
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
|
||||
expect(window.confirm).toHaveBeenCalledWith(
|
||||
"Change status to 'planted' for 2 items?");
|
||||
expect(edit).toHaveBeenCalledTimes(2);
|
||||
expect(edit).toHaveBeenCalledWith(plant1, {
|
||||
plant_stage: "planted",
|
||||
planted_at: expect.stringContaining("Z"),
|
||||
});
|
||||
expect(edit).toHaveBeenCalledWith(plant2, {
|
||||
plant_stage: "planted",
|
||||
planted_at: expect.stringContaining("Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates weed statuses", () => {
|
||||
const p = fakeProps();
|
||||
p.pointerType = "Weed";
|
||||
const weed1 = fakeWeed();
|
||||
const weed2 = fakeWeed();
|
||||
const weed3 = fakeWeed();
|
||||
p.allPoints = [weed1, weed2, weed3];
|
||||
p.selected = [weed1.uuid, weed2.uuid];
|
||||
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
|
||||
window.confirm = jest.fn(() => true);
|
||||
wrapper.find("FBSelect").simulate("change", { label: "", value: "removed" });
|
||||
expect(window.confirm).toHaveBeenCalledWith(
|
||||
"Change status to 'removed' for 2 items?");
|
||||
expect(edit).toHaveBeenCalledTimes(2);
|
||||
expect(edit).toHaveBeenCalledWith(weed1, { plant_stage: "removed" });
|
||||
expect(edit).toHaveBeenCalledWith(weed2, { plant_stage: "removed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("<EditWeedStatus />", () => {
|
||||
const fakeProps = (): EditWeedStatusProps => ({
|
||||
weed: fakeWeed(),
|
||||
updateWeed: jest.fn(),
|
||||
});
|
||||
|
||||
it("updates weed status", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<EditWeedStatus {...p} />);
|
||||
wrapper.find("FBSelect").simulate("change", { label: "", value: "removed" });
|
||||
expect(p.updateWeed).toHaveBeenCalledWith({ plant_stage: "removed" });
|
||||
});
|
||||
});
|
|
@ -1,13 +1,8 @@
|
|||
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
PlantPanel, PlantPanelProps, EditPlantStatusProps,
|
||||
PlantPanel, PlantPanelProps,
|
||||
EditDatePlantedProps, EditDatePlanted, EditPlantLocationProps,
|
||||
EditPlantLocation,
|
||||
} from "../plant_panel";
|
||||
|
@ -18,16 +13,12 @@ import { clickButton } from "../../../__test_support__/helpers";
|
|||
import { history } from "../../../history";
|
||||
import moment from "moment";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { edit } from "../../../api/crud";
|
||||
import {
|
||||
EditPlantStatus, PlantStatusBulkUpdateProps, PlantStatusBulkUpdate,
|
||||
} from "../edit_plant_status";
|
||||
|
||||
describe("<PlantPanel/>", () => {
|
||||
const info: FormattedPlantInfo = {
|
||||
x: 12,
|
||||
y: 34,
|
||||
z: 0,
|
||||
id: undefined,
|
||||
name: "tomato",
|
||||
uuid: "Plant.0.0",
|
||||
|
@ -101,75 +92,11 @@ describe("<PlantPanel/>", () => {
|
|||
expect(history.push).toHaveBeenCalledWith("/app/designer/move_to");
|
||||
expect(innerDispatch).toHaveBeenLastCalledWith({
|
||||
type: Actions.CHOOSE_LOCATION,
|
||||
payload: { x: 12, y: 34, z: undefined }
|
||||
payload: { x: 12, y: 34, z: 0 }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<EditPlantStatus />", () => {
|
||||
const fakeProps = (): EditPlantStatusProps => ({
|
||||
uuid: "Plant.0.0",
|
||||
plantStatus: "planned",
|
||||
updatePlant: jest.fn(),
|
||||
});
|
||||
|
||||
it("changes stage to planted", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<EditPlantStatus {...p} />);
|
||||
wrapper.find("FBSelect").simulate("change", { value: "planted" });
|
||||
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
|
||||
plant_stage: "planted",
|
||||
planted_at: expect.stringContaining("Z")
|
||||
});
|
||||
});
|
||||
|
||||
it("changes stage to planned", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<EditPlantStatus {...p} />);
|
||||
wrapper.find("FBSelect").simulate("change", { value: "planned" });
|
||||
expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
|
||||
plant_stage: "planned",
|
||||
planted_at: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<PlantStatusBulkUpdate />", () => {
|
||||
const fakeProps = (): PlantStatusBulkUpdateProps => ({
|
||||
plants: [],
|
||||
selected: [],
|
||||
dispatch: jest.fn(),
|
||||
});
|
||||
|
||||
it("doesn't update plant statuses", () => {
|
||||
const p = fakeProps();
|
||||
const plant1 = fakePlant();
|
||||
const plant2 = fakePlant();
|
||||
p.plants = [plant1, plant2];
|
||||
p.selected = [plant1.uuid];
|
||||
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
|
||||
window.confirm = jest.fn(() => false);
|
||||
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect(edit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates plant statuses", () => {
|
||||
const p = fakeProps();
|
||||
const plant1 = fakePlant();
|
||||
const plant2 = fakePlant();
|
||||
const plant3 = fakePlant();
|
||||
p.plants = [plant1, plant2, plant3];
|
||||
p.selected = [plant1.uuid, plant2.uuid];
|
||||
const wrapper = shallow(<PlantStatusBulkUpdate {...p} />);
|
||||
window.confirm = jest.fn(() => true);
|
||||
wrapper.find("FBSelect").simulate("change", { label: "", value: "planted" });
|
||||
expect(window.confirm).toHaveBeenCalledWith(
|
||||
"Change the plant status to 'planted' for 2 plants?");
|
||||
expect(edit).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<EditDatePlanted />", () => {
|
||||
const fakeProps = (): EditDatePlantedProps => ({
|
||||
uuid: "Plant.0.0",
|
||||
|
@ -193,7 +120,7 @@ describe("<EditDatePlanted />", () => {
|
|||
describe("<EditPlantLocation />", () => {
|
||||
const fakeProps = (): EditPlantLocationProps => ({
|
||||
uuid: "Plant.0.0",
|
||||
xyLocation: { x: 1, y: 2 },
|
||||
plantLocation: { x: 1, y: 2, z: 0 },
|
||||
updatePlant: jest.fn(),
|
||||
});
|
||||
|
||||
|
|
|
@ -18,12 +18,13 @@ import * as React from "react";
|
|||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
RawSelectPlants as SelectPlants, SelectPlantsProps, mapStateToProps,
|
||||
getFilteredPoints, GetFilteredPointsProps, validPointTypes,
|
||||
getFilteredPoints, GetFilteredPointsProps, validPointTypes, SelectModeLink,
|
||||
} from "../select_plants";
|
||||
import {
|
||||
fakePlant, fakePoint, fakeWeed, fakeToolSlot, fakeTool,
|
||||
fakePlantTemplate,
|
||||
fakeWebAppConfig,
|
||||
fakePointGroup,
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { Actions, Content } from "../../../constants";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
|
@ -35,6 +36,8 @@ import { mockDispatch } from "../../../__test_support__/fake_dispatch";
|
|||
import {
|
||||
buildResourceIndex,
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { history } from "../../../history";
|
||||
import { POINTER_TYPES } from "../../point_groups/criteria/interfaces";
|
||||
|
||||
describe("<SelectPlants />", () => {
|
||||
beforeEach(function () {
|
||||
|
@ -60,6 +63,7 @@ describe("<SelectPlants />", () => {
|
|||
quadrant: 2,
|
||||
isActive: () => false,
|
||||
tools: [],
|
||||
groups: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -211,7 +215,93 @@ describe("<SelectPlants />", () => {
|
|||
{ payload: undefined, type: Actions.SELECT_POINT });
|
||||
});
|
||||
|
||||
const DELETE_BTN_INDEX = 3;
|
||||
it("toggles more", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
|
||||
expect(wrapper.state().more).toEqual(false);
|
||||
expect(wrapper.find(".select-more").props().hidden).toBeTruthy();
|
||||
expect(wrapper.html()).not.toContain(" more status");
|
||||
wrapper.find(".more").simulate("click");
|
||||
expect(wrapper.state().more).toEqual(true);
|
||||
expect(wrapper.find(".select-more").props().hidden).toBeFalsy();
|
||||
expect(wrapper.html()).toContain(" more status");
|
||||
});
|
||||
|
||||
it("selects group items", () => {
|
||||
const p = fakeProps();
|
||||
p.selected = undefined;
|
||||
const group = fakePointGroup();
|
||||
group.body.id = 1;
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 1;
|
||||
group.body.point_ids = [1];
|
||||
p.groups = [group];
|
||||
p.allPoints = [plant];
|
||||
const dispatch = jest.fn();
|
||||
p.dispatch = mockDispatch(dispatch);
|
||||
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
|
||||
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
|
||||
expect(wrapper.state().group_id).toEqual(undefined);
|
||||
actionsWrapper.find("FBSelect").at(1).simulate("change", {
|
||||
label: "", value: 1
|
||||
});
|
||||
expect(wrapper.state().group_id).toEqual(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||
payload: ["Plant"],
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenLastCalledWith({
|
||||
type: Actions.SELECT_POINT,
|
||||
payload: [plant.uuid],
|
||||
});
|
||||
});
|
||||
|
||||
it("selects selection type", () => {
|
||||
const p = fakeProps();
|
||||
const group0 = fakePointGroup();
|
||||
group0.body.id = 0;
|
||||
const group1 = fakePointGroup();
|
||||
group1.body.id = 1;
|
||||
group1.body.criteria.string_eq = { pointer_type: ["Plant"] };
|
||||
p.groups = [group0, group1];
|
||||
const dispatch = jest.fn();
|
||||
p.dispatch = mockDispatch(dispatch);
|
||||
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
|
||||
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
|
||||
actionsWrapper.find("FBSelect").at(1).simulate("change", {
|
||||
label: "", value: 1
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||
payload: ["Plant"],
|
||||
});
|
||||
});
|
||||
|
||||
it("selects selection type without criteria", () => {
|
||||
const p = fakeProps();
|
||||
const group = fakePointGroup();
|
||||
group.body.id = 1;
|
||||
group.body.criteria.string_eq = {};
|
||||
const plant = fakePlant();
|
||||
plant.body.id = 1;
|
||||
const weed = fakeWeed();
|
||||
weed.body.id = 2;
|
||||
group.body.point_ids = [1, 2];
|
||||
p.groups = [group];
|
||||
const dispatch = jest.fn();
|
||||
p.dispatch = mockDispatch(dispatch);
|
||||
const wrapper = mount<SelectPlants>(<SelectPlants {...p} />);
|
||||
const actionsWrapper = shallow(wrapper.instance().ActionButtons());
|
||||
actionsWrapper.find("FBSelect").at(1).simulate("change", {
|
||||
label: "", value: 1
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||
payload: POINTER_TYPES,
|
||||
});
|
||||
});
|
||||
|
||||
const DELETE_BTN_INDEX = 4;
|
||||
|
||||
it("confirms deletion of selected plants", () => {
|
||||
const p = fakeProps();
|
||||
|
@ -344,3 +434,11 @@ describe("validPointTypes()", () => {
|
|||
expect(validPointTypes(["nope"])).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SelectModeLink />", () => {
|
||||
it("navigates to panel", () => {
|
||||
const wrapper = shallow(<SelectModeLink />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { FBSelect, DropDownItem } from "../../ui";
|
||||
import { PlantOptions } from "../interfaces";
|
||||
import { PlantStage } from "farmbot";
|
||||
import { PlantStage, TaggedWeedPointer, PointType, TaggedPoint } from "farmbot";
|
||||
import moment from "moment";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { TaggedPlant } from "../map/interfaces";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import { edit, save } from "../../api/crud";
|
||||
import { EditPlantStatusProps } from "./plant_panel";
|
||||
|
@ -14,14 +13,21 @@ export const PLANT_STAGE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
|
|||
planted: { label: t("Planted"), value: "planted" },
|
||||
sprouted: { label: t("Sprouted"), value: "sprouted" },
|
||||
harvested: { label: t("Harvested"), value: "harvested" },
|
||||
removed: { label: t("Removed"), value: "removed" },
|
||||
});
|
||||
export const PLANT_STAGE_LIST = () => [
|
||||
PLANT_STAGE_DDI_LOOKUP().planned,
|
||||
PLANT_STAGE_DDI_LOOKUP().planted,
|
||||
PLANT_STAGE_DDI_LOOKUP().sprouted,
|
||||
PLANT_STAGE_DDI_LOOKUP().harvested,
|
||||
PLANT_STAGE_DDI_LOOKUP().removed,
|
||||
];
|
||||
|
||||
export const WEED_STATUSES = ["removed"];
|
||||
const WEED_STAGE_DDI_LOOKUP = (): Record<string, DropDownItem> => ({
|
||||
removed: PLANT_STAGE_DDI_LOOKUP().removed,
|
||||
});
|
||||
|
||||
/** Change `planted_at` value based on `plant_stage` update. */
|
||||
const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => {
|
||||
const update: PlantOptions = { plant_stage };
|
||||
|
@ -46,33 +52,52 @@ export function EditPlantStatus(props: EditPlantStatusProps) {
|
|||
}
|
||||
|
||||
export interface PlantStatusBulkUpdateProps {
|
||||
plants: TaggedPlant[];
|
||||
allPoints: TaggedPoint[];
|
||||
selected: UUID[];
|
||||
dispatch: Function;
|
||||
pointerType: PointType;
|
||||
}
|
||||
|
||||
/** Update `plant_stage` for multiple plants at once. */
|
||||
export const PlantStatusBulkUpdate = (props: PlantStatusBulkUpdateProps) =>
|
||||
<div className="plant-status-bulk-update">
|
||||
<p>{t("update plant status to")}</p>
|
||||
<p>{t("update status to")}</p>
|
||||
<FBSelect
|
||||
key={JSON.stringify(props.selected)}
|
||||
list={PLANT_STAGE_LIST()}
|
||||
list={PLANT_STAGE_LIST().filter(ddi =>
|
||||
props.pointerType == "Plant" || WEED_STATUSES.includes("" + ddi.value))}
|
||||
selectedItem={undefined}
|
||||
customNullLabel={t("Select a status")}
|
||||
onChange={ddi => {
|
||||
const plant_stage = ddi.value as PlantStage;
|
||||
const update = getUpdateByPlantStage(plant_stage);
|
||||
const plants = props.plants.filter(plant =>
|
||||
props.selected.includes(plant.uuid)
|
||||
&& plant.kind === "Point"
|
||||
&& plant.body.plant_stage != plant_stage);
|
||||
plants.length > 0 && confirm(
|
||||
t("Change the plant status to '{{ status }}' for {{ num }} plants?",
|
||||
{ status: plant_stage, num: plants.length }))
|
||||
&& plants.map(plant => {
|
||||
props.dispatch(edit(plant, update));
|
||||
props.dispatch(save(plant.uuid));
|
||||
const update = props.pointerType == "Plant"
|
||||
? getUpdateByPlantStage(plant_stage)
|
||||
: { plant_stage };
|
||||
const points = props.allPoints.filter(point =>
|
||||
props.selected.includes(point.uuid)
|
||||
&& point.kind === "Point"
|
||||
&& (point.body.pointer_type == "Plant"
|
||||
|| point.body.pointer_type == "Weed")
|
||||
&& point.body.plant_stage != plant_stage);
|
||||
points.length > 0 && confirm(
|
||||
t("Change status to '{{ status }}' for {{ num }} items?",
|
||||
{ status: plant_stage, num: points.length }))
|
||||
&& points.map(point => {
|
||||
props.dispatch(edit(point, update));
|
||||
props.dispatch(save(point.uuid));
|
||||
});
|
||||
}} />
|
||||
</div>;
|
||||
|
||||
export interface EditWeedStatusProps {
|
||||
weed: TaggedWeedPointer;
|
||||
updateWeed(update: Partial<TaggedWeedPointer["body"]>): void;
|
||||
}
|
||||
|
||||
/** Select a `plant_stage` for a weed. */
|
||||
export const EditWeedStatus = (props: EditWeedStatusProps) =>
|
||||
<FBSelect
|
||||
list={PLANT_STAGE_LIST().filter(ddi => WEED_STATUSES.includes("" + ddi.value))}
|
||||
selectedItem={WEED_STAGE_DDI_LOOKUP()[props.weed.body.plant_stage]}
|
||||
onChange={ddi =>
|
||||
props.updateWeed({ plant_stage: ddi.value as PlantStage })} />;
|
||||
|
|
|
@ -40,6 +40,7 @@ export function mapStateToProps(props: Everything): EditPlantInfoProps {
|
|||
export interface FormattedPlantInfo {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
id: number | undefined;
|
||||
name: string;
|
||||
uuid: string;
|
||||
|
@ -72,6 +73,7 @@ export function formatPlantInfo(plant: TaggedPlant): FormattedPlantInfo {
|
|||
daysOld: plantAge(plant),
|
||||
x: plant.body.x,
|
||||
y: plant.body.y,
|
||||
z: plant.body.z,
|
||||
uuid: plant.uuid,
|
||||
plantedAt: plantDate(plant),
|
||||
plantStatus: get(plant, "body.plant_stage", "planned"),
|
||||
|
|
|
@ -4,7 +4,7 @@ import { round } from "../map/util";
|
|||
import { history } from "../../history";
|
||||
import { BlurableInput, Row, Col } from "../../ui";
|
||||
import { PlantOptions } from "../interfaces";
|
||||
import { PlantStage } from "farmbot";
|
||||
import { PlantStage, Xyz } from "farmbot";
|
||||
import { Moment } from "moment";
|
||||
import moment from "moment";
|
||||
import { Actions } from "../../constants";
|
||||
|
@ -51,19 +51,19 @@ export const EditDatePlanted = (props: EditDatePlantedProps) => {
|
|||
};
|
||||
|
||||
export interface EditPlantLocationProps extends EditPlantProperty {
|
||||
xyLocation: Record<"x" | "y", number>;
|
||||
plantLocation: Record<Xyz, number>;
|
||||
}
|
||||
|
||||
export const EditPlantLocation = (props: EditPlantLocationProps) => {
|
||||
const { xyLocation, updatePlant, uuid } = props;
|
||||
const { plantLocation, updatePlant, uuid } = props;
|
||||
return <Row>
|
||||
{["x", "y"].map((axis: "x" | "y") =>
|
||||
<Col xs={6} key={axis}>
|
||||
{["x", "y", "z"].map((axis: Xyz) =>
|
||||
<Col xs={4} key={axis}>
|
||||
<label style={{ marginTop: 0 }}>{t("{{axis}} (mm)", { axis })}</label>
|
||||
<BlurableInput
|
||||
type="number"
|
||||
value={xyLocation[axis]}
|
||||
min={0}
|
||||
value={plantLocation[axis]}
|
||||
min={axis == "z" ? undefined : 0}
|
||||
onCommit={e => updatePlant(uuid, {
|
||||
[axis]: round(parseIntInput(e.currentTarget.value))
|
||||
})} />
|
||||
|
@ -71,11 +71,11 @@ export const EditPlantLocation = (props: EditPlantLocationProps) => {
|
|||
</Row>;
|
||||
};
|
||||
|
||||
const chooseLocation = (to: Record<"x" | "y", number | undefined>) =>
|
||||
const chooseLocation = (to: Record<Xyz, number | undefined>) =>
|
||||
(dispatch: Function): Promise<void> => {
|
||||
dispatch({
|
||||
type: Actions.CHOOSE_LOCATION,
|
||||
payload: { x: to.x, y: to.y, z: undefined }
|
||||
payload: { x: to.x, y: to.y, z: to.z }
|
||||
});
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
@ -83,6 +83,7 @@ const chooseLocation = (to: Record<"x" | "y", number | undefined>) =>
|
|||
interface MoveToPlantProps {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
|
@ -90,8 +91,9 @@ const MoveToPlant = (props: MoveToPlantProps) =>
|
|||
<button className="fb-button gray no-float"
|
||||
style={{ marginTop: "1rem" }}
|
||||
title={t("Move to this plant")}
|
||||
onClick={() => props.dispatch(chooseLocation({ x: props.x, y: props.y }))
|
||||
.then(() => history.push("/app/designer/move_to"))}>
|
||||
onClick={() =>
|
||||
props.dispatch(chooseLocation({ x: props.x, y: props.y, z: props.z }))
|
||||
.then(() => history.push("/app/designer/move_to"))}>
|
||||
{t("Move FarmBot to this plant")}
|
||||
</button>;
|
||||
|
||||
|
@ -141,7 +143,7 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
info, onDestroy, updatePlant, dispatch, inSavedGarden, timeSettings
|
||||
} = props;
|
||||
const { slug, plantedAt, daysOld, uuid, plantStatus } = info;
|
||||
const { x, y } = info;
|
||||
const { x, y, z } = info;
|
||||
const destroy = () => onDestroy(uuid);
|
||||
return <DesignerPanelContent panelName={"plants"}>
|
||||
<label>
|
||||
|
@ -174,10 +176,10 @@ export function PlantPanel(props: PlantPanelProps) {
|
|||
</Row>}
|
||||
<ListItem name={t("Location")}>
|
||||
<EditPlantLocation uuid={uuid}
|
||||
xyLocation={{ x, y }}
|
||||
plantLocation={{ x, y, z }}
|
||||
updatePlant={updatePlant} />
|
||||
</ListItem>
|
||||
<MoveToPlant x={x} y={y} dispatch={dispatch} />
|
||||
<MoveToPlant x={x} y={y} z={z} dispatch={dispatch} />
|
||||
<ListItem name={t("Status")}>
|
||||
{(!inSavedGarden)
|
||||
? <EditPlantStatus
|
||||
|
|
|
@ -21,10 +21,12 @@ import {
|
|||
PointType, TaggedPoint, TaggedGenericPointer, TaggedToolSlotPointer,
|
||||
TaggedTool,
|
||||
TaggedWeedPointer,
|
||||
TaggedPointGroup,
|
||||
} from "farmbot";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import {
|
||||
selectAllActivePoints, selectAllToolSlotPointers, selectAllTools,
|
||||
selectAllPointGroups,
|
||||
} from "../../resources/selectors";
|
||||
import { PointInventoryItem } from "../points/point_inventory_item";
|
||||
import { ToolSlotInventoryItem } from "../tools";
|
||||
|
@ -37,6 +39,7 @@ import { isActive } from "../tools/edit_tool";
|
|||
import { uniq } from "lodash";
|
||||
import { POINTER_TYPES } from "../point_groups/criteria/interfaces";
|
||||
import { WeedInventoryItem } from "../weeds/weed_inventory_item";
|
||||
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
export const isPointType = (x: any): x is PointType => POINTER_TYPES.includes(x);
|
||||
|
@ -82,6 +85,7 @@ export const mapStateToProps = (props: Everything): SelectPlantsProps => {
|
|||
dispatch: props.dispatch,
|
||||
gardenOpen: props.resources.consumers.farm_designer.openedSavedGarden,
|
||||
tools: selectAllTools(props.resources.index),
|
||||
groups: selectAllPointGroups(props.resources.index),
|
||||
isActive: isActive(selectAllToolSlotPointers(props.resources.index)),
|
||||
xySwap,
|
||||
quadrant,
|
||||
|
@ -100,9 +104,18 @@ export interface SelectPlantsProps {
|
|||
quadrant: BotOriginQuadrant;
|
||||
isActive(id: number | undefined): boolean;
|
||||
tools: TaggedTool[];
|
||||
groups: TaggedPointGroup[];
|
||||
}
|
||||
|
||||
export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
||||
interface SelectPlantsState {
|
||||
group_id: number | undefined;
|
||||
more: boolean;
|
||||
}
|
||||
|
||||
export class RawSelectPlants
|
||||
extends React.Component<SelectPlantsProps, SelectPlantsState> {
|
||||
state: SelectPlantsState = { group_id: undefined, more: false };
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, selected } = this.props;
|
||||
if (selected && selected.length == 1) {
|
||||
|
@ -135,27 +148,83 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
|||
return selectionPointTypes.length > 1 ? "All" : selectionPointTypes[0];
|
||||
}
|
||||
|
||||
get groupDDILookup(): Record<number, DropDownItem> {
|
||||
const lookup: Record<number, DropDownItem> = {};
|
||||
this.props.groups.map(group => {
|
||||
const { id } = group.body;
|
||||
const groupName = group.body.name;
|
||||
const count = pointsSelectedByGroup(group, this.props.allPoints).length;
|
||||
const label = `${groupName} (${t("{{count}} items", { count })})`;
|
||||
id && (lookup[id] = { label, value: id });
|
||||
});
|
||||
return lookup;
|
||||
}
|
||||
|
||||
selectGroup = (ddi: DropDownItem) => {
|
||||
const group_id = parseInt("" + ddi.value);
|
||||
this.setState({ group_id });
|
||||
const group = this.props.groups
|
||||
.filter(pg => pg.body.id == group_id)[0];
|
||||
const points = pointsSelectedByGroup(group, this.props.allPoints);
|
||||
const pointUuids = points.map(p => p.uuid);
|
||||
const pointerTypes =
|
||||
group.body.criteria.string_eq.pointer_type as PointType[] | undefined;
|
||||
const uniqPointTypes = uniq(points.map(p => p.body.pointer_type));
|
||||
const pointTypes =
|
||||
uniqPointTypes.length == 1 ? [uniqPointTypes[0]] : undefined;
|
||||
this.props.dispatch(setSelectionPointType(
|
||||
pointerTypes || pointTypes || POINTER_TYPES));
|
||||
this.props.dispatch(selectPoint(pointUuids));
|
||||
}
|
||||
|
||||
ActionButtons = () =>
|
||||
<div className="panel-action-buttons">
|
||||
<FBSelect
|
||||
<div className={["panel-action-buttons",
|
||||
this.state.more ? "more" : "",
|
||||
["Plant", "Weed"].includes(this.selectionPointType) ? "status" : "",
|
||||
].join(" ")}>
|
||||
<label>{t("selection type")}</label>
|
||||
<FBSelect key={this.selectionPointType}
|
||||
list={POINTER_TYPE_LIST()}
|
||||
selectedItem={POINTER_TYPE_DDI_LOOKUP()[this.selectionPointType]}
|
||||
onChange={ddi => {
|
||||
this.props.dispatch(selectPoint(undefined));
|
||||
this.setState({ group_id: undefined });
|
||||
this.props.dispatch(setSelectionPointType(
|
||||
ddi.value == "All" ? POINTER_TYPES : validPointTypes([ddi.value])));
|
||||
}} />
|
||||
<div className="button-row">
|
||||
<button className="fb-button gray"
|
||||
title={t("Select none")}
|
||||
onClick={() => this.props.dispatch(selectPoint(undefined))}>
|
||||
onClick={() => {
|
||||
this.setState({ group_id: undefined });
|
||||
this.props.dispatch(selectPoint(undefined));
|
||||
}}>
|
||||
{t("Select none")}
|
||||
</button>
|
||||
<button className="fb-button gray"
|
||||
title={t("Select all")}
|
||||
onClick={() => this.props.dispatch(selectPoint(this.allPointUuids))}>
|
||||
onClick={() => {
|
||||
this.setState({ group_id: undefined });
|
||||
this.props.dispatch(selectPoint(this.allPointUuids));
|
||||
}}>
|
||||
{t("Select all")}
|
||||
</button>
|
||||
<div className="more"
|
||||
onClick={() => this.setState({ more: !this.state.more })}>
|
||||
<p>{this.state.more ? t("Less") : t("More")}</p>
|
||||
<i className={`fa fa-caret-${this.state.more ? "up" : "down"}`}
|
||||
title={this.state.more ? t("less") : t("more")} />
|
||||
</div>
|
||||
<div className={"select-more"} hidden={!this.state.more}>
|
||||
<label>{t("select all in group")}</label>
|
||||
<FBSelect key={`${this.selectionPointType}-${this.state.group_id}`}
|
||||
list={Object.values(this.groupDDILookup)}
|
||||
selectedItem={this.state.group_id
|
||||
? this.groupDDILookup[this.state.group_id]
|
||||
: undefined}
|
||||
customNullLabel={t("Select a group")}
|
||||
onChange={this.selectGroup} />
|
||||
</div>
|
||||
</div>
|
||||
<label>{t("SELECTION ACTIONS")}</label>
|
||||
<div className="button-row">
|
||||
|
@ -171,9 +240,10 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
|||
: error(t(Content.ERROR_PLANT_TEMPLATE_GROUP))}>
|
||||
{t("Create group")}
|
||||
</button>
|
||||
{this.selectionPointType == "Plant" &&
|
||||
{(this.selectionPointType == "Plant" || this.selectionPointType == "Weed") &&
|
||||
<PlantStatusBulkUpdate
|
||||
plants={this.props.plants}
|
||||
pointerType={this.selectionPointType}
|
||||
allPoints={this.props.allPoints}
|
||||
selected={this.selected}
|
||||
dispatch={this.props.dispatch} />}
|
||||
</div>
|
||||
|
@ -227,7 +297,11 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
|
|||
description={Content.BOX_SELECT_DESCRIPTION} />
|
||||
<this.ActionButtons />
|
||||
|
||||
<DesignerPanelContent panelName={"plant-selection"}>
|
||||
<DesignerPanelContent panelName={"plant-selection"}
|
||||
className={[
|
||||
this.state.more ? "more" : "",
|
||||
["Plant", "Weed"].includes(this.selectionPointType) ? "status" : "",
|
||||
].join(" ")}>
|
||||
{this.selectedPointData.map(p => {
|
||||
if (p.kind == "PlantTemplate" || p.body.pointer_type == "Plant") {
|
||||
return <PlantInventoryItem
|
||||
|
@ -320,3 +394,13 @@ const getVisibleLayers = (getConfigValue: GetWebAppConfigValue): PointType[] =>
|
|||
...(showFarmbot ? [PointerType.ToolSlot] : []),
|
||||
];
|
||||
};
|
||||
|
||||
export const SelectModeLink = () =>
|
||||
<div className="select-mode">
|
||||
<button
|
||||
className="fb-button gray"
|
||||
title={t("open point select panel")}
|
||||
onClick={() => history.push("/app/designer/plants/select")}>
|
||||
{t("select")}
|
||||
</button>
|
||||
</div>;
|
||||
|
|
|
@ -199,7 +199,7 @@ describe("togglePointTypeCriteria()", () => {
|
|||
const group = fakePointGroup();
|
||||
group.body.criteria.string_eq = {
|
||||
pointer_type: ["Plant", "ToolSlot"],
|
||||
"plant_stage": ["planned"],
|
||||
"openfarm_slug": ["mint"],
|
||||
};
|
||||
const expectedBody = cloneDeep(group.body);
|
||||
expectedBody.criteria.string_eq = { pointer_type: ["Weed"] };
|
||||
|
|
|
@ -107,7 +107,7 @@ describe("typeDisabled()", () => {
|
|||
|
||||
it("isn't disabled", () => {
|
||||
const criteria = fakeCriteria();
|
||||
criteria.string_eq = { plant_stage: ["planted"] };
|
||||
criteria.string_eq = { openfarm_slug: ["mint"] };
|
||||
const result = typeDisabled(criteria, "Plant");
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
|
|
@ -161,7 +161,6 @@ describe("<NumberLtGtInput />", () => {
|
|||
p.group,
|
||||
"number_gt",
|
||||
"x",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -175,7 +174,6 @@ describe("<NumberLtGtInput />", () => {
|
|||
p.group,
|
||||
"number_lt",
|
||||
"x",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -81,6 +81,6 @@ describe("<CheckboxList />", () => {
|
|||
expect(wrapper.text()).toContain("label");
|
||||
wrapper.find("input").first().simulate("change");
|
||||
expect(toggleAndEditEqCriteria).toHaveBeenCalledWith(
|
||||
p.group, "openfarm_slug", "value", "Plant");
|
||||
p.group, "openfarm_slug", "value");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,7 +60,7 @@ export const toggleAndEditEqCriteria = <T extends string | number>(
|
|||
const wasOff = !tempEqCriteria[key]?.includes(value);
|
||||
toggleEqCriteria<T>(tempEqCriteria)(key, value);
|
||||
pointerType && wasOff && clearSubCriteria(
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria, key);
|
||||
dispatch(editCriteria(group, tempCriteria));
|
||||
};
|
||||
|
||||
|
@ -68,16 +68,17 @@ export const toggleAndEditEqCriteria = <T extends string | number>(
|
|||
export const clearSubCriteria = (
|
||||
pointerTypes: PointerType[],
|
||||
tempCriteria: PointGroupCriteria,
|
||||
keepKey: string,
|
||||
) => {
|
||||
const toggleStrEq = toggleEqCriteria<string>(tempCriteria.string_eq, "off");
|
||||
const toggleNumEq = toggleEqCriteria<number>(tempCriteria.number_eq, "off");
|
||||
const toggleStrEqMapper = (key: string) =>
|
||||
const toggleStrEqMapper = (key: string) => key != keepKey &&
|
||||
tempCriteria.string_eq[key]?.map(value => toggleStrEq(key, value));
|
||||
if (pointerTypes.includes("Plant")) {
|
||||
["openfarm_slug", "plant_stage"].map(toggleStrEqMapper);
|
||||
}
|
||||
if (pointerTypes.includes("Weed")) {
|
||||
["meta.created_by"].map(toggleStrEqMapper);
|
||||
["meta.created_by", "plant_stage"].map(toggleStrEqMapper);
|
||||
}
|
||||
if (pointerTypes.includes("GenericPointer") && pointerTypes.includes("Weed")) {
|
||||
["meta.color"].map(toggleStrEqMapper);
|
||||
|
@ -101,8 +102,8 @@ export const togglePointTypeCriteria =
|
|||
const toggle = toggleEqCriteria<string>(tempCriteria.string_eq);
|
||||
clear && (tempCriteria.string_eq.pointer_type = []);
|
||||
toggle("pointer_type", pointerType);
|
||||
clearSubCriteria(
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
|
||||
clearSubCriteria(POINTER_TYPES.filter(x => x != pointerType),
|
||||
tempCriteria, "pointer_type");
|
||||
dispatch(editCriteria(group, tempCriteria));
|
||||
};
|
||||
|
||||
|
@ -164,7 +165,7 @@ export const editGtLtCriteriaField = (
|
|||
(dispatch: Function) => {
|
||||
const tempCriteria = cloneDeep(group.body.criteria);
|
||||
pointerType && clearSubCriteria(
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria);
|
||||
POINTER_TYPES.filter(x => x != pointerType), tempCriteria, criteriaKey);
|
||||
const value = e.currentTarget.value != ""
|
||||
? parseInt(e.currentTarget.value)
|
||||
: undefined;
|
||||
|
|
|
@ -62,6 +62,7 @@ export const hasSubCriteria = (criteria: PointGroupCriteria) =>
|
|||
case "Weed":
|
||||
return !!(
|
||||
selected("meta.created_by")
|
||||
|| selected("plant_stage")
|
||||
|| selected("meta.color")
|
||||
|| numSelected("radius"));
|
||||
case "Plant":
|
||||
|
|
|
@ -153,7 +153,7 @@ export const DaySelection = (props: DaySelectionProps) => {
|
|||
|
||||
/** Edit number < and > criteria. */
|
||||
export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
|
||||
const { group, dispatch, criteriaKey, pointerType } = props;
|
||||
const { group, dispatch, criteriaKey } = props;
|
||||
const gtCriteria = props.group.body.criteria.number_gt;
|
||||
const ltCriteria = props.group.body.criteria.number_lt;
|
||||
return <Row>
|
||||
|
@ -164,7 +164,7 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
|
|||
defaultValue={gtCriteria[criteriaKey]}
|
||||
disabled={props.disabled}
|
||||
onBlur={e => dispatch(editGtLtCriteriaField(
|
||||
group, "number_gt", criteriaKey, pointerType)(e))} />
|
||||
group, "number_gt", criteriaKey)(e))} />
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<p>{"<"}</p>
|
||||
|
@ -182,7 +182,7 @@ export const NumberLtGtInput = (props: NumberLtGtInputProps) => {
|
|||
defaultValue={ltCriteria[criteriaKey]}
|
||||
disabled={props.disabled}
|
||||
onBlur={e => dispatch(editGtLtCriteriaField(
|
||||
group, "number_lt", criteriaKey, pointerType)(e))} />
|
||||
group, "number_lt", criteriaKey)(e))} />
|
||||
</Col>
|
||||
</Row>;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
SubCriteriaSectionProps,
|
||||
CheckboxListItem,
|
||||
} from "./interfaces";
|
||||
import { PLANT_STAGE_LIST } from "../../plants/edit_plant_status";
|
||||
import { PLANT_STAGE_LIST, WEED_STATUSES } from "../../plants/edit_plant_status";
|
||||
import { DIRECTION_CHOICES } from "../../tools/tool_slot_edit_components";
|
||||
import { Checkbox } from "../../../ui";
|
||||
import { PointType } from "farmbot";
|
||||
|
@ -80,7 +80,7 @@ export const CheckboxList =
|
|||
<div className="criteria-checkbox-list-item" key={index}>
|
||||
<Checkbox
|
||||
onChange={() => props.dispatch(toggle<T>(
|
||||
props.group, props.criteriaKey, value, props.pointerType))}
|
||||
props.group, props.criteriaKey, value))}
|
||||
checked={selected(props.criteriaKey, value)}
|
||||
title={t(label)}
|
||||
color={color}
|
||||
|
@ -95,14 +95,16 @@ export const PlantCriteria = (props: PlantSubCriteriaProps) => {
|
|||
const { group, dispatch, disabled } = props;
|
||||
const commonProps = { group, dispatch, disabled };
|
||||
return <div className={"plant-criteria-options"}>
|
||||
<PlantStage {...commonProps} />
|
||||
<PlantStage {...commonProps} pointerType={"Plant"} />
|
||||
<PlantType {...commonProps} slugs={props.slugs} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PlantStage = (props: SubCriteriaProps) =>
|
||||
const PlantStage = (props: PointSubCriteriaProps) =>
|
||||
<div className={"plant-stage-criteria"}>
|
||||
<p className={"category"}>{t("Stage")}</p>
|
||||
<p className={"category"}>
|
||||
{props.pointerType == "Plant" ? t("Stage") : t("Status")}
|
||||
</p>
|
||||
<ClearCategory
|
||||
group={props.group}
|
||||
criteriaCategories={["string_eq"]}
|
||||
|
@ -110,12 +112,15 @@ const PlantStage = (props: SubCriteriaProps) =>
|
|||
dispatch={props.dispatch} />
|
||||
<CheckboxList<string>
|
||||
disabled={props.disabled}
|
||||
pointerType={"Plant"}
|
||||
pointerType={props.pointerType}
|
||||
criteriaKey={"plant_stage"}
|
||||
group={props.group}
|
||||
dispatch={props.dispatch}
|
||||
list={PLANT_STAGE_LIST().map(ddi =>
|
||||
({ label: ddi.label, value: "" + ddi.value }))} />
|
||||
list={PLANT_STAGE_LIST().filter(ddi =>
|
||||
props.pointerType == "Plant" || WEED_STATUSES.includes("" + ddi.value))
|
||||
.map(ddi => ({ label: ddi.label, value: "" + ddi.value }))
|
||||
.concat(props.pointerType == "Weed"
|
||||
? [{ label: t("Remaining"), value: "planned" }] : [])} />
|
||||
</div>;
|
||||
|
||||
const PlantType = (props: PlantSubCriteriaProps) =>
|
||||
|
@ -145,6 +150,7 @@ export const WeedCriteria = (props: SubCriteriaProps) => {
|
|||
const commonProps = { group, dispatch, disabled, pointerType };
|
||||
return <div className={"weed-criteria-options"}>
|
||||
<PointSource {...commonProps} />
|
||||
<PlantStage {...commonProps} />
|
||||
<Color {...commonProps} />
|
||||
<Radius {...commonProps} />
|
||||
</div>;
|
||||
|
|
|
@ -78,12 +78,12 @@ describe("<CreatePoints />", () => {
|
|||
it("updates specific fields", () => {
|
||||
const p = fakeProps();
|
||||
p.drawnPoint = FAKE_POINT;
|
||||
const i = new CreatePoints(p);
|
||||
i.updateValue("color")(inputEvent("cheerful hue"));
|
||||
expect(i.props.drawnPoint).toBeTruthy();
|
||||
const wrapper = mount<CreatePoints>(<CreatePoints {...p} />);
|
||||
wrapper.instance().updateValue("color")(inputEvent("cheerful hue"));
|
||||
expect(wrapper.instance().props.drawnPoint).toBeTruthy();
|
||||
const expected = cloneDeep(FAKE_POINT);
|
||||
expected.color = "cheerful hue";
|
||||
expect(i.props.dispatch).toHaveBeenCalledWith({
|
||||
expect(wrapper.instance().props.dispatch).toHaveBeenCalledWith({
|
||||
type: "SET_DRAWN_POINT_DATA",
|
||||
payload: expected,
|
||||
});
|
||||
|
@ -133,6 +133,7 @@ describe("<CreatePoints />", () => {
|
|||
meta: { color: "green", created_by: "farm-designer", type: "point" },
|
||||
name: "Created Point",
|
||||
pointer_type: "GenericPointer",
|
||||
plant_stage: "planned",
|
||||
radius: 30, x: 10, y: 20, z: 0,
|
||||
});
|
||||
});
|
||||
|
@ -146,6 +147,7 @@ describe("<CreatePoints />", () => {
|
|||
meta: { color: "red", created_by: "farm-designer", type: "weed" },
|
||||
name: "Created Weed",
|
||||
pointer_type: "Weed",
|
||||
plant_stage: "planned",
|
||||
radius: 30, x: 10, y: 20, z: 0,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,9 +11,11 @@ import {
|
|||
EditPointColor, EditPointColorProps, updatePoint, EditPointName,
|
||||
EditPointNameProps,
|
||||
AdditionalWeedProperties,
|
||||
EditPointPropertiesProps,
|
||||
AdditionalWeedPropertiesProps,
|
||||
} from "../point_edit_actions";
|
||||
import { fakePoint } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakePoint, fakeWeed,
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { edit, save } from "../../../api/crud";
|
||||
|
||||
describe("updatePoint()", () => {
|
||||
|
@ -94,8 +96,8 @@ describe("<EditPointColor />", () => {
|
|||
});
|
||||
|
||||
describe("<AdditionalWeedProperties />", () => {
|
||||
const fakeProps = (): EditPointPropertiesProps => ({
|
||||
point: fakePoint(),
|
||||
const fakeProps = (): AdditionalWeedPropertiesProps => ({
|
||||
point: fakeWeed(),
|
||||
updatePoint: jest.fn(),
|
||||
});
|
||||
|
||||
|
|
|
@ -69,9 +69,8 @@ describe("mapStateToProps()", () => {
|
|||
const state = fakeState();
|
||||
const point = fakePoint();
|
||||
const discarded = fakePoint();
|
||||
discarded.body.discarded_at = "2016-05-22T05:00:00.000Z";
|
||||
state.resources = buildResourceIndex([point, discarded]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.genericPoints).toEqual([point]);
|
||||
expect(props.genericPoints).toEqual([point, discarded]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -122,7 +122,7 @@ export class RawCreatePoints
|
|||
});
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.loadDefaultPoint();
|
||||
}
|
||||
|
||||
|
@ -183,6 +183,7 @@ export class RawCreatePoints
|
|||
x: this.attr("cx"),
|
||||
y: this.attr("cy"),
|
||||
z: 0,
|
||||
plant_stage: "planned",
|
||||
radius: this.attr("r"),
|
||||
};
|
||||
this.props.dispatch(initSave("Point", body));
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Row, Col, BlurableInput, ColorPicker } from "../../ui";
|
|||
import { parseIntInput } from "../../util";
|
||||
import { UUID } from "../../resources/interfaces";
|
||||
import { plantAge } from "../plants/map_state_to_props";
|
||||
import { EditWeedStatus } from "../plants/edit_plant_status";
|
||||
|
||||
type PointUpdate =
|
||||
Partial<TaggedGenericPointer["body"] | TaggedWeedPointer["body"]>;
|
||||
|
@ -29,6 +30,11 @@ export interface EditPointPropertiesProps {
|
|||
updatePoint(update: PointUpdate): void;
|
||||
}
|
||||
|
||||
export interface AdditionalWeedPropertiesProps {
|
||||
point: TaggedWeedPointer;
|
||||
updatePoint(update: PointUpdate): void;
|
||||
}
|
||||
|
||||
export const EditPointProperties = (props: EditPointPropertiesProps) =>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -53,22 +59,25 @@ export const EditPointProperties = (props: EditPointPropertiesProps) =>
|
|||
</ListItem>
|
||||
</ul>;
|
||||
|
||||
export const AdditionalWeedProperties = (props: EditPointPropertiesProps) =>
|
||||
export const AdditionalWeedProperties = (props: AdditionalWeedPropertiesProps) =>
|
||||
<ul className="additional-weed-properties">
|
||||
<ListItem name={t("Age")}>
|
||||
{`${plantAge(props.point)} ${t("days old")}`}
|
||||
</ListItem>
|
||||
<ListItem name={t("Status")}>
|
||||
<EditWeedStatus weed={props.point} updateWeed={props.updatePoint} />
|
||||
</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")}>
|
||||
return <ListItem name={t("Source")} key={key}>
|
||||
{SOURCE_LOOKUP()[value || ""] || t("unknown")}
|
||||
</ListItem>;
|
||||
case "removal_method":
|
||||
return <ListItem name={t("Removal method")}>
|
||||
return <ListItem name={t("Removal method")} key={key}>
|
||||
<div className="weed-removal-method-section">
|
||||
{REMOVAL_METHODS.map(method =>
|
||||
<div className={"weed-removal-method"} key={method}>
|
||||
|
@ -84,7 +93,7 @@ export const AdditionalWeedProperties = (props: EditPointPropertiesProps) =>
|
|||
</div>
|
||||
</ListItem>;
|
||||
default:
|
||||
return <ListItem name={key}>
|
||||
return <ListItem name={key} key={key}>
|
||||
{value || ""}
|
||||
</ListItem>;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export function mapStateToProps(props: Everything): PointsProps {
|
|||
const { hoveredPoint } = props.resources.consumers.farm_designer;
|
||||
return {
|
||||
genericPoints: selectAllGenericPointers(props.resources.index)
|
||||
.filter(x => !x.body.discarded_at),
|
||||
.filter(x => x),
|
||||
dispatch: props.dispatch,
|
||||
hoveredPoint,
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ export const initialState: DesignerState = {
|
|||
openedSavedGarden: undefined,
|
||||
tryGroupSortType: undefined,
|
||||
editGroupAreaInMap: false,
|
||||
settingsSearchTerm: "",
|
||||
};
|
||||
|
||||
export const designer = generateReducer<DesignerState>(initialState)
|
||||
|
@ -107,6 +108,10 @@ export const designer = generateReducer<DesignerState>(initialState)
|
|||
s.tryGroupSortType = payload;
|
||||
return s;
|
||||
})
|
||||
.add<string>(Actions.SET_SETTINGS_SEARCH_TERM, (s, { payload }) => {
|
||||
s.settingsSearchTerm = payload;
|
||||
return s;
|
||||
})
|
||||
.add<boolean>(Actions.EDIT_GROUP_AREA_IN_MAP, (s, { payload }) => {
|
||||
s.editGroupAreaInMap = payload;
|
||||
return s;
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { Everything } from "../interfaces";
|
||||
import { connect } from "react-redux";
|
||||
import { Content } from "../constants";
|
||||
import { DesignerPanel, DesignerPanelContent } from "./designer_panel";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import {
|
||||
GetWebAppConfigValue, getWebAppConfigValue, setWebAppConfigValue,
|
||||
} from "../config_storage/actions";
|
||||
import { Row, Col } from "../ui";
|
||||
import { ToggleButton } from "../controls/toggle_button";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
import { BooleanSetting, NumericSetting } from "../session_keys";
|
||||
import { resetVirtualTrail } from "./map/layers/farmbot/bot_trail";
|
||||
import { MapSizeInputs } from "./map_size_setting";
|
||||
import { DesignerNavTabs, Panel } from "./panel_header";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({
|
||||
dispatch: props.dispatch,
|
||||
getConfigValue: getWebAppConfigValue(() => props),
|
||||
});
|
||||
|
||||
export interface DesignerSettingsProps {
|
||||
dispatch: Function;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
export class RawDesignerSettings
|
||||
extends React.Component<DesignerSettingsProps, {}> {
|
||||
|
||||
render() {
|
||||
const { getConfigValue, dispatch } = this.props;
|
||||
const settingsProps = { getConfigValue, dispatch };
|
||||
return <DesignerPanel panelName={"settings"} panel={Panel.Settings}>
|
||||
<DesignerNavTabs />
|
||||
<DesignerPanelContent panelName={"settings"}>
|
||||
{DESIGNER_SETTINGS(settingsProps).map(setting =>
|
||||
<Setting key={setting.title} {...setting} {...settingsProps} />)}
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
}
|
||||
}
|
||||
|
||||
interface SettingDescriptionProps {
|
||||
setting?: BooleanConfigKey;
|
||||
title: string;
|
||||
description: string;
|
||||
invert?: boolean;
|
||||
callback?: () => void;
|
||||
children?: React.ReactChild;
|
||||
defaultOn?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface SettingProps
|
||||
extends DesignerSettingsProps, SettingDescriptionProps { }
|
||||
|
||||
const Setting = (props: SettingProps) => {
|
||||
const { title, setting, callback, defaultOn } = props;
|
||||
const raw_value = setting ? props.getConfigValue(setting) : undefined;
|
||||
const value = (defaultOn && isUndefined(raw_value)) ? true : !!raw_value;
|
||||
return <div
|
||||
className={`designer-setting ${props.disabled ? "disabled" : ""}`}>
|
||||
<Row>
|
||||
<Col xs={9}>
|
||||
<label>{t(title)}</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
{setting && <ToggleButton
|
||||
toggleValue={props.invert ? !value : value}
|
||||
toggleAction={() => {
|
||||
props.dispatch(setWebAppConfigValue(setting, !value));
|
||||
callback?.();
|
||||
}}
|
||||
title={`${t("toggle")} ${title}`}
|
||||
customText={{ textFalse: t("off"), textTrue: t("on") }} />}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<p>{t(props.description)}</p>
|
||||
</Row>
|
||||
{props.children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const DESIGNER_SETTINGS =
|
||||
(settingsProps: DesignerSettingsProps): SettingDescriptionProps[] => ([
|
||||
{
|
||||
title: t("Display plant animations"),
|
||||
description: t(Content.PLANT_ANIMATIONS),
|
||||
setting: BooleanSetting.disable_animations,
|
||||
invert: true
|
||||
},
|
||||
{
|
||||
title: t("Display virtual FarmBot trail"),
|
||||
description: t(Content.VIRTUAL_TRAIL),
|
||||
setting: BooleanSetting.display_trail,
|
||||
callback: resetVirtualTrail,
|
||||
},
|
||||
{
|
||||
title: t("Dynamic map size"),
|
||||
description: t(Content.DYNAMIC_MAP_SIZE),
|
||||
setting: BooleanSetting.dynamic_map,
|
||||
},
|
||||
{
|
||||
title: t("Map size"),
|
||||
description: t(Content.MAP_SIZE),
|
||||
children: <MapSizeInputs {...settingsProps} />,
|
||||
disabled: !!settingsProps.getConfigValue(BooleanSetting.dynamic_map),
|
||||
},
|
||||
{
|
||||
title: t("Rotate map"),
|
||||
description: t(Content.MAP_SWAP_XY),
|
||||
setting: BooleanSetting.xy_swap,
|
||||
},
|
||||
{
|
||||
title: t("Map origin"),
|
||||
description: t(Content.MAP_ORIGIN),
|
||||
children: <OriginSelector {...settingsProps} />
|
||||
},
|
||||
{
|
||||
title: t("Confirm plant deletion"),
|
||||
description: t(Content.CONFIRM_PLANT_DELETION),
|
||||
setting: BooleanSetting.confirm_plant_deletion,
|
||||
defaultOn: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const OriginSelector = (props: DesignerSettingsProps) => {
|
||||
const quadrant = props.getConfigValue(NumericSetting.bot_origin_quadrant);
|
||||
const update = (value: number) => () => props.dispatch(setWebAppConfigValue(
|
||||
NumericSetting.bot_origin_quadrant, value));
|
||||
return <div className="farmbot-origin">
|
||||
<div className="quadrants">
|
||||
{[2, 1, 3, 4].map(q =>
|
||||
<div key={"quadrant_" + q}
|
||||
className={`quadrant ${quadrant === q ? "selected" : ""}`}
|
||||
onClick={update(q)} />)}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const DesignerSettings = connect(mapStateToProps)(RawDesignerSettings);
|
|
@ -0,0 +1,35 @@
|
|||
jest.mock("../../map/layers/farmbot/bot_trail", () => ({
|
||||
resetVirtualTrail: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { PlainDesignerSettings } from "../farm_designer_settings";
|
||||
import { DesignerSettingsPropsBase } from "../interfaces";
|
||||
import { resetVirtualTrail } from "../../map/layers/farmbot/bot_trail";
|
||||
|
||||
describe("<PlainDesignerSettings />", () => {
|
||||
const fakeProps = (): DesignerSettingsPropsBase => ({
|
||||
dispatch: jest.fn(),
|
||||
getConfigValue: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<div>{PlainDesignerSettings(fakeProps())}</div>);
|
||||
expect(wrapper.text().toLowerCase()).toContain("plant animations");
|
||||
});
|
||||
|
||||
it("doesn't call callback", () => {
|
||||
const wrapper = mount(<div>{PlainDesignerSettings(fakeProps())}</div>);
|
||||
expect(wrapper.find("label").at(0).text()).toContain("animations");
|
||||
wrapper.find("button").at(0).simulate("click");
|
||||
expect(resetVirtualTrail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls callback", () => {
|
||||
const wrapper = mount(<div>{PlainDesignerSettings(fakeProps())}</div>);
|
||||
expect(wrapper.find("label").at(1).text()).toContain("trail");
|
||||
wrapper.find("button").at(1).simulate("click");
|
||||
expect(resetVirtualTrail).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,172 @@
|
|||
jest.mock("../../../config_storage/actions", () => ({
|
||||
getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }),
|
||||
setWebAppConfigValue: jest.fn(),
|
||||
}));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../account/dev/dev_support", () => ({
|
||||
DevSettings: {
|
||||
futureFeaturesEnabled: () => mockDev,
|
||||
overriddenFbosVersion: jest.fn(),
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("../../../devices/components/maybe_highlight", () => ({
|
||||
maybeOpenPanel: jest.fn(),
|
||||
Highlight: (p: { children: React.ReactChild }) => <div>{p.children}</div>,
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import { RawDesignerSettings as DesignerSettings } from "..";
|
||||
import { DesignerSettingsProps } from "../interfaces";
|
||||
import { BooleanSetting, NumericSetting } from "../../../session_keys";
|
||||
import { setWebAppConfigValue } from "../../../config_storage/actions";
|
||||
import {
|
||||
buildResourceIndex, fakeDevice,
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { Actions } from "../../../constants";
|
||||
import { Motors } from "../hardware_settings";
|
||||
import { SearchField } from "../../../ui/search_field";
|
||||
import { maybeOpenPanel } from "../../../devices/components/maybe_highlight";
|
||||
|
||||
const getSetting =
|
||||
(wrapper: ReactWrapper, position: number, containsString: string) => {
|
||||
const setting = wrapper.find(".designer-setting").at(position);
|
||||
expect(setting.text().toLowerCase())
|
||||
.toContain(containsString.toLowerCase());
|
||||
return setting;
|
||||
};
|
||||
|
||||
describe("<DesignerSettings />", () => {
|
||||
beforeEach(() => {
|
||||
mockDev = false;
|
||||
});
|
||||
|
||||
const fakeProps = (): DesignerSettingsProps => ({
|
||||
dispatch: jest.fn(),
|
||||
getConfigValue: jest.fn(),
|
||||
firmwareConfig: undefined,
|
||||
sourceFwConfig: () => ({ value: 10, consistent: true }),
|
||||
sourceFbosConfig: () => ({ value: 10, consistent: true }),
|
||||
resources: buildResourceIndex().index,
|
||||
deviceAccount: fakeDevice(),
|
||||
env: {},
|
||||
alerts: [],
|
||||
shouldDisplay: jest.fn(),
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
timeSettings: fakeTimeSettings(),
|
||||
bot: bot,
|
||||
searchTerm: "",
|
||||
});
|
||||
|
||||
it("renders settings", () => {
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("size");
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("pin");
|
||||
const settings = wrapper.find(".designer-setting");
|
||||
expect(settings.length).toEqual(7);
|
||||
});
|
||||
|
||||
it("renders all settings", () => {
|
||||
mockDev = true;
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("pin");
|
||||
});
|
||||
|
||||
it("mounts", () => {
|
||||
mount(<DesignerSettings {...fakeProps()} />);
|
||||
expect(maybeOpenPanel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unmounts", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
wrapper.unmount();
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
|
||||
payload: { open: false, all: true },
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.TOGGLE_CONTROL_PANEL_OPTION,
|
||||
payload: "farmbot_os",
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SETTINGS_SEARCH_TERM,
|
||||
payload: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("changes search term", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DesignerSettings {...p} />);
|
||||
wrapper.find(SearchField).simulate("change", "setting");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
|
||||
payload: { open: true, all: true },
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SETTINGS_SEARCH_TERM,
|
||||
payload: "setting",
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches firmware_hardware", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
p.sourceFbosConfig = () => ({ value: "arduino", consistent: true });
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
expect(wrapper.find(Motors).props().firmwareHardware).toEqual("arduino");
|
||||
});
|
||||
|
||||
it("expands all", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
clickButton(wrapper, 0, "expand all");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
|
||||
payload: { open: true, all: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("collapses all", () => {
|
||||
mockDev = true;
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
clickButton(wrapper, 1, "collapse all");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
|
||||
payload: { open: false, all: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders defaultOn setting", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => undefined;
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
const confirmDeletion = getSetting(wrapper, 6, "confirm plant");
|
||||
expect(confirmDeletion.find("button").text()).toEqual("on");
|
||||
});
|
||||
|
||||
it("toggles setting", () => {
|
||||
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
|
||||
const trailSetting = getSetting(wrapper, 1, "trail");
|
||||
trailSetting.find("button").simulate("click");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(BooleanSetting.display_trail, true);
|
||||
});
|
||||
|
||||
it("changes origin", () => {
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => 2;
|
||||
const wrapper = mount(<DesignerSettings {...p} />);
|
||||
const originSetting = getSetting(wrapper, 5, "origin");
|
||||
originSetting.find("div").last().simulate("click");
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
NumericSetting.bot_origin_quadrant, 4);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
jest.mock("../../../config_storage/actions", () => ({
|
||||
getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }),
|
||||
setWebAppConfigValue: jest.fn(),
|
||||
}));
|
||||
|
||||
import { mapStateToProps } from "../state_to_props";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props", () => {
|
||||
const props = mapStateToProps(fakeState());
|
||||
const value = props.getConfigValue(BooleanSetting.show_plants);
|
||||
expect(value).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
import * as React from "react";
|
||||
import { Content, DeviceSetting } from "../../constants";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { setWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { ToggleButton } from "../../controls/toggle_button";
|
||||
import { BooleanSetting, NumericSetting } from "../../session_keys";
|
||||
import { resetVirtualTrail } from "../map/layers/farmbot/bot_trail";
|
||||
import { MapSizeInputs } from "../map_size_setting";
|
||||
import { isUndefined } from "lodash";
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import { Header } from "../../devices/components/hardware_settings/header";
|
||||
import { Highlight } from "../../devices/components/maybe_highlight";
|
||||
import {
|
||||
DesignerSettingsSectionProps, SettingProps,
|
||||
DesignerSettingsPropsBase, SettingDescriptionProps,
|
||||
} from "./interfaces";
|
||||
|
||||
export const Designer = (props: DesignerSettingsSectionProps) => {
|
||||
const { getConfigValue, dispatch, controlPanelState } = props;
|
||||
const settingsProps = { getConfigValue, dispatch };
|
||||
return <Highlight className={"section"}
|
||||
settingName={DeviceSetting.farmDesigner}>
|
||||
<Header
|
||||
title={DeviceSetting.farmDesigner}
|
||||
panel={"farm_designer"}
|
||||
dispatch={dispatch}
|
||||
expanded={controlPanelState.farm_designer} />
|
||||
<Collapse isOpen={!!controlPanelState.farm_designer}>
|
||||
{PlainDesignerSettings(settingsProps)}
|
||||
</Collapse>
|
||||
</Highlight>;
|
||||
};
|
||||
|
||||
export const PlainDesignerSettings =
|
||||
(settingsProps: DesignerSettingsPropsBase) =>
|
||||
DESIGNER_SETTINGS(settingsProps).map(setting =>
|
||||
<Setting key={setting.title} {...setting} {...settingsProps} />);
|
||||
|
||||
const Setting = (props: SettingProps) => {
|
||||
const { title, setting, callback, defaultOn } = props;
|
||||
const raw_value = setting ? props.getConfigValue(setting) : undefined;
|
||||
const value = (defaultOn && isUndefined(raw_value)) ? true : !!raw_value;
|
||||
return <Highlight settingName={title}>
|
||||
<div
|
||||
className={`designer-setting ${props.disabled ? "disabled" : ""}`}>
|
||||
<Row>
|
||||
<Col xs={9}>
|
||||
<label>{t(title)}</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
{setting && <ToggleButton
|
||||
toggleValue={props.invert ? !value : value}
|
||||
toggleAction={() => {
|
||||
props.dispatch(setWebAppConfigValue(setting, !value));
|
||||
callback?.();
|
||||
}}
|
||||
title={`${t("toggle")} ${title}`}
|
||||
customText={{ textFalse: t("off"), textTrue: t("on") }} />}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<p>{t(props.description)}</p>
|
||||
</Row>
|
||||
{props.children}
|
||||
</div>
|
||||
</Highlight>;
|
||||
};
|
||||
|
||||
const DESIGNER_SETTINGS =
|
||||
(settingsProps: DesignerSettingsPropsBase): SettingDescriptionProps[] => ([
|
||||
{
|
||||
title: DeviceSetting.animations,
|
||||
description: t(Content.PLANT_ANIMATIONS),
|
||||
setting: BooleanSetting.disable_animations,
|
||||
invert: true
|
||||
},
|
||||
{
|
||||
title: DeviceSetting.trail,
|
||||
description: t(Content.VIRTUAL_TRAIL),
|
||||
setting: BooleanSetting.display_trail,
|
||||
callback: resetVirtualTrail,
|
||||
},
|
||||
{
|
||||
title: DeviceSetting.dynamicMap,
|
||||
description: t(Content.DYNAMIC_MAP_SIZE),
|
||||
setting: BooleanSetting.dynamic_map,
|
||||
},
|
||||
{
|
||||
title: DeviceSetting.mapSize,
|
||||
description: t(Content.MAP_SIZE),
|
||||
children: <MapSizeInputs {...settingsProps} />,
|
||||
disabled: !!settingsProps.getConfigValue(BooleanSetting.dynamic_map),
|
||||
},
|
||||
{
|
||||
title: DeviceSetting.rotateMap,
|
||||
description: t(Content.MAP_SWAP_XY),
|
||||
setting: BooleanSetting.xy_swap,
|
||||
},
|
||||
{
|
||||
title: DeviceSetting.mapOrigin,
|
||||
description: t(Content.MAP_ORIGIN),
|
||||
children: <OriginSelector {...settingsProps} />
|
||||
},
|
||||
{
|
||||
title: DeviceSetting.confirmPlantDeletion,
|
||||
description: t(Content.CONFIRM_PLANT_DELETION),
|
||||
setting: BooleanSetting.confirm_plant_deletion,
|
||||
defaultOn: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const OriginSelector = (props: DesignerSettingsPropsBase) => {
|
||||
const quadrant = props.getConfigValue(NumericSetting.bot_origin_quadrant);
|
||||
const update = (value: number) => () => props.dispatch(setWebAppConfigValue(
|
||||
NumericSetting.bot_origin_quadrant, value));
|
||||
return <div className="farmbot-origin">
|
||||
<div className="quadrants">
|
||||
{[2, 1, 3, 4].map(q =>
|
||||
<div key={"quadrant_" + q}
|
||||
className={`quadrant ${quadrant === q ? "selected" : ""}`}
|
||||
onClick={update(q)} />)}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export * from "../../devices/components/fbos_settings/power_and_reset";
|
||||
export * from "../../devices/components/fbos_settings/firmware";
|
||||
export * from "../../devices/components/farmbot_os_settings";
|
|
@ -0,0 +1,9 @@
|
|||
export * from "../../devices/components/hardware_settings/homing_and_calibration";
|
||||
export * from "../../devices/components/hardware_settings/motors";
|
||||
export * from "../../devices/components/hardware_settings/encoders";
|
||||
export * from "../../devices/components/hardware_settings/endstops";
|
||||
export * from "../../devices/components/hardware_settings/error_handling";
|
||||
export * from "../../devices/components/hardware_settings/pin_bindings";
|
||||
export * from "../../devices/components/hardware_settings/pin_guard";
|
||||
export * from "../../devices/components/hardware_settings/danger_zone";
|
||||
export * from "../../devices/components/firmware_hardware_support";
|
|
@ -0,0 +1,135 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { DesignerPanel, DesignerPanelContent } from "../designer_panel";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { DesignerNavTabs, Panel } from "../panel_header";
|
||||
import {
|
||||
bulkToggleControlPanel, MCUFactoryReset, toggleControlPanel,
|
||||
} from "../../devices/actions";
|
||||
import { FarmBotSettings, Firmware, PowerAndReset } from "./fbos_settings";
|
||||
import {
|
||||
HomingAndCalibration, Motors, Encoders, EndStops, ErrorHandling,
|
||||
PinGuard, DangerZone, PinBindings, isFwHardwareValue,
|
||||
} from "./hardware_settings";
|
||||
import { DevSettings } from "../../account/dev/dev_support";
|
||||
import { maybeOpenPanel } from "../../devices/components/maybe_highlight";
|
||||
import { isBotOnlineFromState } from "../../devices/must_be_online";
|
||||
import { DesignerSettingsProps } from "./interfaces";
|
||||
import { Designer, PlainDesignerSettings } from "./farm_designer_settings";
|
||||
import { SearchField } from "../../ui/search_field";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
export class RawDesignerSettings
|
||||
extends React.Component<DesignerSettingsProps, {}> {
|
||||
|
||||
componentDidMount = () =>
|
||||
this.props.dispatch(maybeOpenPanel(this.props.bot.controlPanelState, true));
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.props.dispatch(bulkToggleControlPanel(false, true));
|
||||
this.props.dispatch(toggleControlPanel("farmbot_os"));
|
||||
this.props.dispatch({
|
||||
type: Actions.SET_SETTINGS_SEARCH_TERM,
|
||||
payload: ""
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getConfigValue, dispatch, firmwareConfig,
|
||||
sourceFwConfig, sourceFbosConfig, resources,
|
||||
} = this.props;
|
||||
const { controlPanelState } = this.props.bot;
|
||||
const settingsProps = { getConfigValue, dispatch };
|
||||
const commonProps = { dispatch, controlPanelState };
|
||||
const { value } = this.props.sourceFbosConfig("firmware_hardware");
|
||||
const firmwareHardware = isFwHardwareValue(value) ? value : undefined;
|
||||
const botOnline = isBotOnlineFromState(this.props.bot);
|
||||
return <DesignerPanel panelName={"settings"} panel={Panel.Settings}>
|
||||
<DesignerNavTabs />
|
||||
<DesignerPanelContent panelName={"settings"}>
|
||||
<SearchField
|
||||
placeholder={t("Search settings...")}
|
||||
searchTerm={this.props.searchTerm}
|
||||
onChange={searchTerm => {
|
||||
dispatch(bulkToggleControlPanel(true, true));
|
||||
dispatch({
|
||||
type: Actions.SET_SETTINGS_SEARCH_TERM,
|
||||
payload: searchTerm
|
||||
});
|
||||
}} />
|
||||
{DevSettings.futureFeaturesEnabled() ?
|
||||
<div className="all-settings">
|
||||
<div className="bulk-expand-controls">
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
onClick={() => dispatch(bulkToggleControlPanel(true, true))}>
|
||||
{t("Expand All")}
|
||||
</button>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
onClick={() => dispatch(bulkToggleControlPanel(false, true))}>
|
||||
{t("Collapse All")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="all-settings-content">
|
||||
<FarmBotSettings
|
||||
bot={this.props.bot}
|
||||
env={this.props.env}
|
||||
alerts={this.props.alerts}
|
||||
saveFarmwareEnv={this.props.saveFarmwareEnv}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
botOnline={botOnline}
|
||||
timeSettings={this.props.timeSettings}
|
||||
device={this.props.deviceAccount} />
|
||||
<Firmware
|
||||
bot={this.props.bot}
|
||||
alerts={this.props.alerts}
|
||||
dispatch={this.props.dispatch}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
botOnline={botOnline}
|
||||
timeSettings={this.props.timeSettings} />
|
||||
<PowerAndReset {...commonProps}
|
||||
sourceFbosConfig={sourceFbosConfig}
|
||||
botOnline={botOnline} />
|
||||
<HomingAndCalibration {...commonProps}
|
||||
bot={this.props.bot}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareConfig={firmwareConfig}
|
||||
firmwareHardware={firmwareHardware}
|
||||
botOnline={botOnline} />
|
||||
<Motors {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<Encoders {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<EndStops {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<ErrorHandling {...commonProps}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<PinBindings {...commonProps}
|
||||
resources={resources}
|
||||
firmwareHardware={firmwareHardware} />
|
||||
<PinGuard {...commonProps}
|
||||
resources={resources}
|
||||
sourceFwConfig={sourceFwConfig} />
|
||||
<DangerZone {...commonProps}
|
||||
onReset={MCUFactoryReset}
|
||||
botOnline={botOnline} />
|
||||
<Designer {...commonProps}
|
||||
getConfigValue={getConfigValue} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className="designer-settings">
|
||||
{PlainDesignerSettings(settingsProps)}
|
||||
</div>}
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
}
|
||||
}
|
||||
|
||||
export const DesignerSettings = connect(mapStateToProps)(RawDesignerSettings);
|
|
@ -0,0 +1,53 @@
|
|||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import {
|
||||
SourceFwConfig, SourceFbosConfig, UserEnv, ShouldDisplay,
|
||||
SaveFarmwareEnv, BotState, ControlPanelState,
|
||||
} from "../../devices/interfaces";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
import { TaggedDevice, Alert } from "farmbot";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { DeviceSetting } from "../../constants";
|
||||
import {
|
||||
BooleanConfigKey as WebAppBooleanConfigKey,
|
||||
} from "farmbot/dist/resources/configs/web_app";
|
||||
|
||||
export interface DesignerSettingsPropsBase {
|
||||
dispatch: Function;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
export interface DesignerSettingsProps extends DesignerSettingsPropsBase {
|
||||
firmwareConfig: FirmwareConfig | undefined;
|
||||
sourceFwConfig: SourceFwConfig;
|
||||
sourceFbosConfig: SourceFbosConfig;
|
||||
resources: ResourceIndex;
|
||||
deviceAccount: TaggedDevice;
|
||||
env: UserEnv;
|
||||
alerts: Alert[];
|
||||
shouldDisplay: ShouldDisplay;
|
||||
saveFarmwareEnv: SaveFarmwareEnv;
|
||||
timeSettings: TimeSettings;
|
||||
bot: BotState;
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export interface DesignerSettingsSectionProps {
|
||||
dispatch: Function;
|
||||
controlPanelState: ControlPanelState;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
export interface SettingDescriptionProps {
|
||||
setting?: WebAppBooleanConfigKey;
|
||||
title: DeviceSetting;
|
||||
description: string;
|
||||
invert?: boolean;
|
||||
callback?: () => void;
|
||||
children?: React.ReactChild;
|
||||
defaultOn?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SettingProps
|
||||
extends DesignerSettingsPropsBase, SettingDescriptionProps { }
|
|
@ -0,0 +1,35 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
import { getWebAppConfigValue } from "../../config_storage/actions";
|
||||
import { validFwConfig, validFbosConfig } from "../../util";
|
||||
import { getFirmwareConfig, getFbosConfig } from "../../resources/getters";
|
||||
import {
|
||||
sourceFwConfigValue, sourceFbosConfigValue,
|
||||
} from "../../devices/components/source_config_value";
|
||||
import {
|
||||
getDeviceAccountSettings, maybeGetTimeSettings,
|
||||
} from "../../resources/selectors";
|
||||
import {
|
||||
saveOrEditFarmwareEnv, getShouldDisplayFn, getEnv,
|
||||
} from "../../farmware/state_to_props";
|
||||
import { getAllAlerts } from "../../messages/state_to_props";
|
||||
import { DesignerSettingsProps } from "./interfaces";
|
||||
|
||||
export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({
|
||||
dispatch: props.dispatch,
|
||||
getConfigValue: getWebAppConfigValue(() => props),
|
||||
firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index)),
|
||||
sourceFwConfig: sourceFwConfigValue(validFwConfig(getFirmwareConfig(
|
||||
props.resources.index)), props.bot.hardware.mcu_params),
|
||||
sourceFbosConfig: sourceFbosConfigValue(validFbosConfig(getFbosConfig(
|
||||
props.resources.index)), props.bot.hardware.configuration),
|
||||
resources: props.resources.index,
|
||||
deviceAccount: getDeviceAccountSettings(props.resources.index),
|
||||
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
|
||||
env: getEnv(props.resources.index, getShouldDisplayFn(
|
||||
props.resources.index, props.bot), props.bot),
|
||||
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
alerts: getAllAlerts(props.resources),
|
||||
bot: props.bot,
|
||||
searchTerm: props.resources.consumers.farm_designer.settingsSearchTerm,
|
||||
});
|
|
@ -57,7 +57,7 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const allGenericPoints = selectAllGenericPointers(props.resources.index);
|
||||
const genericPoints = getConfigValue(BooleanSetting.show_historic_points)
|
||||
? allGenericPoints
|
||||
: allGenericPoints.filter(x => !x.body.discarded_at);
|
||||
: allGenericPoints.filter(x => x);
|
||||
const weeds = selectAllWeedPointers(props.resources.index);
|
||||
|
||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
|
|
|
@ -43,7 +43,7 @@ describe("<AddToolSlot />", () => {
|
|||
"direction", "gantry-mounted",
|
||||
].map(string => expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
expect(init).toHaveBeenCalledWith("Point", {
|
||||
pointer_type: "ToolSlot", name: "Slot", radius: 0, meta: {},
|
||||
pointer_type: "ToolSlot", name: "Slot", meta: {},
|
||||
x: 0, y: 0, z: 0, tool_id: undefined,
|
||||
pullout_direction: ToolPulloutDirection.NONE,
|
||||
gantry_mounted: false,
|
||||
|
@ -119,7 +119,7 @@ describe("<AddToolSlot />", () => {
|
|||
const wrapper = mount(<AddToolSlot {...p} />);
|
||||
expect(wrapper.text().toLowerCase()).not.toContain("tool");
|
||||
expect(init).toHaveBeenCalledWith("Point", {
|
||||
pointer_type: "ToolSlot", name: "Slot", radius: 0, meta: {},
|
||||
pointer_type: "ToolSlot", name: "Slot", meta: {},
|
||||
x: 0, y: 0, z: 0, tool_id: undefined,
|
||||
pullout_direction: ToolPulloutDirection.NONE,
|
||||
gantry_mounted: true,
|
||||
|
|
|
@ -21,7 +21,7 @@ export class RawAddToolSlot
|
|||
|
||||
componentDidMount() {
|
||||
const action = init("Point", {
|
||||
pointer_type: "ToolSlot", name: t("Slot"), radius: 0, meta: {},
|
||||
pointer_type: "ToolSlot", name: t("Slot"), meta: {},
|
||||
x: 0, y: 0, z: 0, tool_id: undefined,
|
||||
pullout_direction: ToolPulloutDirection.NONE,
|
||||
gantry_mounted: !hasUTM(this.props.firmwareHardware) ? true : false,
|
||||
|
|
|
@ -124,7 +124,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
return isBotOnline(this.props.syncStatus, this.props.botToMqttStatus);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (window.innerWidth > 450) {
|
||||
this.props.dispatch({
|
||||
type: Actions.SELECT_FARMWARE,
|
||||
|
|
|
@ -26,6 +26,8 @@ jest.mock("../../api", () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
jest.mock("../laptop_splash", () => ({ LaptopSplash: () => <div /> }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { FrontPage, setField, PartialFormEvent } from "../front_page";
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { LaptopSplash } from "../laptop_splash";
|
||||
|
||||
describe("<LaptopSplash />", () => {
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<LaptopSplash className={""} />);
|
||||
expect(wrapper.find("video").length).toEqual(1);
|
||||
});
|
||||
});
|
|
@ -117,7 +117,6 @@ describe("<Logs />", () => {
|
|||
|
||||
it("shows filtered overall filter status", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<Logs {...p} />);
|
||||
const state = fakeLogsState();
|
||||
state.assertion = 2;
|
||||
|
@ -129,10 +128,9 @@ describe("<Logs />", () => {
|
|||
|
||||
it("shows unfiltered overall filter status", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => false;
|
||||
const wrapper = mount(<Logs {...p} />);
|
||||
const state = fakeLogsState();
|
||||
state.assertion = 2;
|
||||
state.assertion = 3;
|
||||
wrapper.setState(state);
|
||||
const filterBtn = wrapper.find("button").first();
|
||||
expect(filterBtn.text().toLowerCase()).toEqual("filter");
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Filters } from "../interfaces";
|
|||
import { startCase } from "lodash";
|
||||
import { MESSAGE_TYPES, MessageType } from "../../sequences/interfaces";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Feature, ShouldDisplay } from "../../devices/interfaces";
|
||||
|
||||
const MENU_ORDER: string[] = [
|
||||
MessageType.success,
|
||||
|
@ -26,11 +25,9 @@ const menuSort = (a: string, b: string) =>
|
|||
|
||||
/** Get log filter keys from LogsState. */
|
||||
export const filterStateKeys =
|
||||
(state: LogsState, shouldDisplay: ShouldDisplay) =>
|
||||
(state: LogsState) =>
|
||||
Object.keys(state)
|
||||
.filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key))
|
||||
.filter(key => shouldDisplay(Feature.assertion_block)
|
||||
|| key !== "assertion");
|
||||
.filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key));
|
||||
|
||||
export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
|
||||
/** Filter level 0: logs hidden. */
|
||||
|
@ -56,7 +53,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
|
|||
{t("normal")}
|
||||
</button>
|
||||
</fieldset>
|
||||
{filterStateKeys(props.state, props.shouldDisplay).sort(menuSort)
|
||||
{filterStateKeys(props.state).sort(menuSort)
|
||||
.map((logType: keyof Filters) =>
|
||||
<fieldset key={logType}>
|
||||
<label>
|
||||
|
|
|
@ -76,7 +76,7 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
|
|||
|
||||
/** Determine if log type filters are active. */
|
||||
get filterActive() {
|
||||
const filterKeys = filterStateKeys(this.state, this.props.shouldDisplay);
|
||||
const filterKeys = filterStateKeys(this.state);
|
||||
const filterValues = filterKeys
|
||||
.map((key: keyof Filters) => this.state[key]);
|
||||
// Filters active if every log type level is not equal to 3 (max verbosity)
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
import { regimensReducer, RegimenState } from "../reducer";
|
||||
import { regimensReducer, RegimenState, newWeek } from "../reducer";
|
||||
import { Actions } from "../../constants";
|
||||
import { popWeek, pushWeek, selectDays, deselectDays } from "../bulk_scheduler/actions";
|
||||
import {
|
||||
popWeek, pushWeek, selectDays, deselectDays,
|
||||
} from "../bulk_scheduler/actions";
|
||||
import { defensiveClone } from "../../util";
|
||||
import { Week } from "../bulk_scheduler/interfaces";
|
||||
|
||||
const week = newWeek();
|
||||
Object.entries(week.days).map(([day, _]: [keyof Week["days"], boolean]) => {
|
||||
week.days[day] = true;
|
||||
});
|
||||
week.days.day7 = false;
|
||||
|
||||
const STATE: RegimenState = {
|
||||
dailyOffsetMs: 300000,
|
||||
selectedSequenceUUID: "Sequence.71.167",
|
||||
currentRegimen: "Regimen.4.56",
|
||||
weeks: [
|
||||
{
|
||||
"days": {
|
||||
"day1": true,
|
||||
"day2": true,
|
||||
"day3": true,
|
||||
"day4": true,
|
||||
"day5": true,
|
||||
"day6": true,
|
||||
"day7": false
|
||||
}
|
||||
},
|
||||
],
|
||||
weeks: [week],
|
||||
schedulerOpen: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { render } from "enzyme";
|
||||
import { WeekRow } from "../bulk_scheduler/week_row";
|
||||
import { WeekRowProps } from "../bulk_scheduler/interfaces";
|
||||
import { betterMerge } from "../../util";
|
||||
|
||||
function weekProps(p?: Partial<WeekRowProps>): WeekRowProps {
|
||||
return betterMerge({
|
||||
dispatch: jest.fn(),
|
||||
index: 0,
|
||||
week: {
|
||||
"days": {
|
||||
"day1": false,
|
||||
"day2": false,
|
||||
"day3": false,
|
||||
"day4": false,
|
||||
"day5": false,
|
||||
"day6": false,
|
||||
"day7": false
|
||||
}
|
||||
}
|
||||
}, p || {});
|
||||
}
|
||||
|
||||
describe("<WeekRow/>", () => {
|
||||
it("renders week 1 day numbers", () => {
|
||||
const wrapper = render(<WeekRow {...weekProps() } />);
|
||||
const txt = wrapper.text();
|
||||
expect(txt).toEqual("Week 11234567");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<WeekRow/>", () => {
|
||||
it("renders week 2 day numbers", () => {
|
||||
const wrapper = render(<WeekRow {...weekProps({ index: 1 }) } />);
|
||||
const txt = wrapper.text();
|
||||
expect(txt).toEqual("Week 2891011121314");
|
||||
});
|
||||
});
|
|
@ -19,6 +19,7 @@ import { arrayUnwrap } from "../../../resources/util";
|
|||
import { overwrite } from "../../../api/crud";
|
||||
import { fakeVariableNameSet } from "../../../__test_support__/fake_variables";
|
||||
import { error, warning } from "../../../toast/toast";
|
||||
import { newWeek } from "../../reducer";
|
||||
|
||||
const sequence_id = 23;
|
||||
const regimen_id = 32;
|
||||
|
@ -53,18 +54,9 @@ describe("commitBulkEditor()", () => {
|
|||
state.resources.consumers.regimens.currentRegimen = regimenUuid;
|
||||
state.resources.consumers.regimens.selectedSequenceUUID = sequenceUuid;
|
||||
state.resources.consumers.regimens.dailyOffsetMs = 2000;
|
||||
state.resources.consumers.regimens.weeks = [{
|
||||
days:
|
||||
{
|
||||
day1: true,
|
||||
day2: false,
|
||||
day3: false,
|
||||
day4: false,
|
||||
day5: false,
|
||||
day6: false,
|
||||
day7: false
|
||||
}
|
||||
}];
|
||||
const week = newWeek();
|
||||
week.days.day1 = true;
|
||||
state.resources.consumers.regimens.weeks = [week];
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { groupRegimenItemsByWeek } from "../group_regimen_items_by_week";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { newWeek } from "../../reducer";
|
||||
|
||||
describe("groupRegimenItemsByWeek()", () => {
|
||||
it("groups regimen items by week", () => {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.id = 1;
|
||||
const week1 = newWeek();
|
||||
week1.days.day1 = true;
|
||||
const week2 = newWeek();
|
||||
const week3 = newWeek();
|
||||
week3.days.day2 = true;
|
||||
week3.days.day4 = true;
|
||||
const { day1, day2, day3, day4, day5, day6, day7 } = week3.days;
|
||||
week3.days = { day1, day4, day3, day2, day5, day6, day7 };
|
||||
const weeks = [week1, week2, week3];
|
||||
const result = groupRegimenItemsByWeek(weeks, 100, sequence.body);
|
||||
expect(result).toEqual([
|
||||
{ time_offset: 100, sequence_id: 1 },
|
||||
{ time_offset: 1296000100, sequence_id: 1 },
|
||||
{ time_offset: 1468800100, sequence_id: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles missing sequence id", () => {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.id = undefined;
|
||||
const week = newWeek();
|
||||
week.days.day1 = true;
|
||||
const result = groupRegimenItemsByWeek([week], 0, sequence.body);
|
||||
expect(result).toEqual([
|
||||
{ time_offset: 0, sequence_id: -1 },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -8,20 +8,12 @@ import {
|
|||
import { Actions } from "../../../constants";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { AddButton } from "../add_button";
|
||||
import { newWeek } from "../../reducer";
|
||||
|
||||
describe("<BulkScheduler />", () => {
|
||||
const weeks = [{
|
||||
days:
|
||||
{
|
||||
day1: true,
|
||||
day2: false,
|
||||
day3: false,
|
||||
day4: false,
|
||||
day5: false,
|
||||
day6: false,
|
||||
day7: false
|
||||
}
|
||||
}];
|
||||
const week = newWeek();
|
||||
week.days.day1 = true;
|
||||
const weeks = [week];
|
||||
|
||||
function fakeProps(): BulkEditorProps {
|
||||
const sequence = fakeSequence();
|
||||
|
|
|
@ -3,20 +3,12 @@ import { mount } from "enzyme";
|
|||
import { WeekGrid } from "../week_grid";
|
||||
import { WeekGridProps } from "../interfaces";
|
||||
import { Actions } from "../../../constants";
|
||||
import { newWeek } from "../../reducer";
|
||||
|
||||
describe("<WeekGrid />", () => {
|
||||
const weeks = [{
|
||||
days:
|
||||
{
|
||||
day1: true,
|
||||
day2: false,
|
||||
day3: false,
|
||||
day4: false,
|
||||
day5: false,
|
||||
day6: false,
|
||||
day7: false
|
||||
}
|
||||
}];
|
||||
const week = newWeek();
|
||||
week.days.day1 = true;
|
||||
const weeks = [week];
|
||||
|
||||
it("renders", () => {
|
||||
const props: WeekGridProps = { weeks, dispatch: jest.fn() };
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react";
|
||||
import { render, mount } from "enzyme";
|
||||
import { WeekRow } from "../week_row";
|
||||
import { WeekRowProps } from "../interfaces";
|
||||
import { betterMerge } from "../../../util";
|
||||
import { newWeek } from "../../reducer";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("<WeekRow/>", () => {
|
||||
const fakeProps = (p?: Partial<WeekRowProps>): WeekRowProps =>
|
||||
betterMerge({
|
||||
dispatch: jest.fn(),
|
||||
index: 0,
|
||||
week: newWeek()
|
||||
}, p || {});
|
||||
|
||||
it("renders week 1 day numbers", () => {
|
||||
const wrapper = render(<WeekRow {...fakeProps()} />);
|
||||
expect(wrapper.text()).toEqual("Week 11234567");
|
||||
});
|
||||
|
||||
it("renders week 2 day numbers", () => {
|
||||
const wrapper = render(<WeekRow {...fakeProps({ index: 1 })} />);
|
||||
expect(wrapper.text()).toEqual("Week 2891011121314");
|
||||
});
|
||||
|
||||
it("selects day", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<WeekRow {...p} />);
|
||||
wrapper.find("input").first().simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.TOGGLE_DAY,
|
||||
payload: { week: 0, day: 1 },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,17 +6,15 @@ import { t } from "../../i18next_wrapper";
|
|||
export function WeekRow({ index, dispatch, week }: WeekRowProps) {
|
||||
return <div className="week-row">
|
||||
<label className="week-label">{t("Week")} {index + 1}</label>
|
||||
{
|
||||
DAYS.map(function (day, i) {
|
||||
const id = `${index}-${day}`;
|
||||
return <Day day={i + 1}
|
||||
week={index}
|
||||
dispatch={dispatch}
|
||||
id={id}
|
||||
key={id}
|
||||
active={week.days[day]} />;
|
||||
})
|
||||
}
|
||||
{DAYS.map(function (day, i) {
|
||||
const id = `${index}-${day}`;
|
||||
return <Day day={i + 1}
|
||||
week={index}
|
||||
dispatch={dispatch}
|
||||
id={id}
|
||||
key={id}
|
||||
active={week.days[day]} />;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue