Compare commits
77 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 | bf0a03d11d | |
gabrielburnworth | ec757b1b29 | |
gabrielburnworth | 3c3b120b9b | |
Rick Carlino | 046035ab9e | |
Rick Carlino | 52b481e831 | |
Rick Carlino | 73422eb8ea | |
gabrielburnworth | b087e08f13 | |
Rick Carlino | 1e1b405c32 | |
Rick Carlino | dccea4e474 | |
Rick Carlino | 91d86bad0c | |
Rick Carlino | bfe4df68a8 | |
Rick Carlino | dd6a43d901 | |
Rick Carlino | 726cd6d4e7 | |
Rick Carlino | d730cd9260 | |
Rick Carlino | eb8cfd3c91 | |
Rick Carlino | 8f9bd4a5e7 | |
gabrielburnworth | 3ebf434945 | |
Rick Carlino | 92a7194c6e | |
gabrielburnworth | 24ad841d7f | |
Rick Carlino | b45f806309 | |
Rick Carlino | bda30bae09 |
2
Gemfile
2
Gemfile
|
@ -35,7 +35,7 @@ group :development, :test do
|
||||||
gem "hashdiff"
|
gem "hashdiff"
|
||||||
gem "pry-rails"
|
gem "pry-rails"
|
||||||
gem "pry"
|
gem "pry"
|
||||||
gem "rspec-rails", "4.0.0.beta3"
|
gem "rspec-rails"
|
||||||
gem "rspec"
|
gem "rspec"
|
||||||
gem 'rspec_junit_formatter'
|
gem 'rspec_junit_formatter'
|
||||||
gem "simplecov"
|
gem "simplecov"
|
||||||
|
|
36
Gemfile.lock
36
Gemfile.lock
|
@ -69,11 +69,11 @@ GEM
|
||||||
zeitwerk (~> 2.2)
|
zeitwerk (~> 2.2)
|
||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
amq-protocol (2.3.0)
|
amq-protocol (2.3.1)
|
||||||
bcrypt (3.1.13)
|
bcrypt (3.1.13)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bunny (2.14.4)
|
bunny (2.15.0)
|
||||||
amq-protocol (~> 2.3, >= 2.3.0)
|
amq-protocol (~> 2.3, >= 2.3.1)
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
|
@ -104,12 +104,12 @@ GEM
|
||||||
activerecord (>= 4.2, < 7)
|
activerecord (>= 4.2, < 7)
|
||||||
docile (1.3.2)
|
docile (1.3.2)
|
||||||
erubi (1.9.0)
|
erubi (1.9.0)
|
||||||
factory_bot (5.1.1)
|
factory_bot (5.1.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
factory_bot_rails (5.1.1)
|
factory_bot_rails (5.1.1)
|
||||||
factory_bot (~> 5.1.0)
|
factory_bot (~> 5.1.0)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
faker (2.10.2)
|
faker (2.11.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (0.17.3)
|
faraday (0.17.3)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -167,7 +167,7 @@ GEM
|
||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.0)
|
kaminari-core (= 1.2.0)
|
||||||
kaminari-core (1.2.0)
|
kaminari-core (1.2.0)
|
||||||
loofah (2.4.0)
|
loofah (2.5.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -175,14 +175,14 @@ GEM
|
||||||
marcel (0.3.3)
|
marcel (0.3.3)
|
||||||
mimemagic (~> 0.3.2)
|
mimemagic (~> 0.3.2)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
method_source (0.9.2)
|
method_source (1.0.0)
|
||||||
mimemagic (0.3.4)
|
mimemagic (0.3.4)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.14.0)
|
minitest (5.14.0)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
mutations (0.9.0)
|
mutations (0.9.1)
|
||||||
activesupport
|
activesupport
|
||||||
nio4r (2.5.2)
|
nio4r (2.5.2)
|
||||||
nokogiri (1.10.9)
|
nokogiri (1.10.9)
|
||||||
|
@ -193,9 +193,9 @@ GEM
|
||||||
rack
|
rack
|
||||||
rake (>= 0.8.1)
|
rake (>= 0.8.1)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pry (0.12.2)
|
pry (0.13.1)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.3)
|
public_suffix (4.0.3)
|
||||||
|
@ -267,14 +267,14 @@ GEM
|
||||||
rspec-mocks (3.9.1)
|
rspec-mocks (3.9.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.9.0)
|
||||||
rspec-rails (4.0.0.beta3)
|
rspec-rails (4.0.0)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
rspec-core (~> 3.8)
|
rspec-core (~> 3.9)
|
||||||
rspec-expectations (~> 3.8)
|
rspec-expectations (~> 3.9)
|
||||||
rspec-mocks (~> 3.8)
|
rspec-mocks (~> 3.9)
|
||||||
rspec-support (~> 3.8)
|
rspec-support (~> 3.9)
|
||||||
rspec-support (3.9.2)
|
rspec-support (3.9.2)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.4.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
|
@ -300,7 +300,7 @@ GEM
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
thor (1.0.1)
|
thor (1.0.1)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tzinfo (1.2.6)
|
tzinfo (1.2.7)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
url (0.3.2)
|
url (0.3.2)
|
||||||
|
@ -348,7 +348,7 @@ DEPENDENCIES
|
||||||
request_store
|
request_store
|
||||||
rollbar
|
rollbar
|
||||||
rspec
|
rspec
|
||||||
rspec-rails (= 4.0.0.beta3)
|
rspec-rails
|
||||||
rspec_junit_formatter
|
rspec_junit_formatter
|
||||||
scenic
|
scenic
|
||||||
secure_headers
|
secure_headers
|
||||||
|
|
|
@ -16,9 +16,9 @@ module CeleryScriptSettingsBag
|
||||||
end
|
end
|
||||||
|
|
||||||
PIN_TYPE_MAP = { "Peripheral" => Peripheral,
|
PIN_TYPE_MAP = { "Peripheral" => Peripheral,
|
||||||
"Sensor" => Sensor,
|
"Sensor" => Sensor,
|
||||||
"BoxLed3" => BoxLed,
|
"BoxLed3" => BoxLed,
|
||||||
"BoxLed4" => BoxLed }
|
"BoxLed4" => BoxLed }
|
||||||
ALLOWED_AXIS = %w(x y z all)
|
ALLOWED_AXIS = %w(x y z all)
|
||||||
ALLOWED_ASSERTION_TYPES = %w(abort recover abort_recover continue)
|
ALLOWED_ASSERTION_TYPES = %w(abort recover abort_recover continue)
|
||||||
ALLOWED_CHANGES = %w(add remove update)
|
ALLOWED_CHANGES = %w(add remove update)
|
||||||
|
@ -33,16 +33,16 @@ module CeleryScriptSettingsBag
|
||||||
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant Weed)
|
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant Weed)
|
||||||
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot Weed GenericPointer)
|
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot Weed GenericPointer)
|
||||||
ALLOWED_RPC_NODES = %w(assertion calibrate change_ownership
|
ALLOWED_RPC_NODES = %w(assertion calibrate change_ownership
|
||||||
check_updates dump_info emergency_lock
|
check_updates emergency_lock
|
||||||
emergency_unlock execute execute_script
|
emergency_unlock execute execute_script
|
||||||
factory_reset find_home flash_firmware home
|
factory_reset find_home flash_firmware home
|
||||||
install_farmware install_first_party_farmware _if
|
install_farmware install_first_party_farmware _if
|
||||||
move_absolute move_relative power_off read_pin
|
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
|
send_message set_servo_angle set_user_env sync
|
||||||
take_photo toggle_pin update_farmware wait
|
take_photo toggle_pin update_farmware wait
|
||||||
write_pin zero)
|
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)
|
read_status reboot sync take_photo)
|
||||||
ANY_VARIABLE = %i(tool coordinate point identifier)
|
ANY_VARIABLE = %i(tool coordinate point identifier)
|
||||||
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
|
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 " \
|
ONLY_ONE_COORD = "Move Absolute does not accept a group of locations " \
|
||||||
"as input. Please change your selection to a single" \
|
"as input. Please change your selection to a single" \
|
||||||
" location."
|
" location."
|
||||||
PLANT_STAGES = %w(planned planted harvested sprouted)
|
PLANT_STAGES = %w(planned planted harvested sprouted removed)
|
||||||
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
|
|
||||||
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
|
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
|
||||||
MISC_ENUM_ERR = '"%s" is not valid. Allowed values: %s'
|
MISC_ENUM_ERR = '"%s" is not valid. Allowed values: %s'
|
||||||
MAX_WAIT_MS = 1000 * 60 * 3 # Three Minutes
|
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. " +
|
"A single wait node cannot exceed #{MAX_WAIT_MS / 1000 / 60} minutes. " +
|
||||||
"Consider lowering the wait time or using multiple WAIT blocks."
|
"Consider lowering the wait time or using multiple WAIT blocks."
|
||||||
Corpus = CeleryScript::Corpus.new
|
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 = {
|
CORPUS_VALUES = {
|
||||||
boolean: [TrueClass, FalseClass],
|
boolean: [TrueClass, FalseClass],
|
||||||
|
@ -278,6 +284,9 @@ module CeleryScriptSettingsBag
|
||||||
lua: {
|
lua: {
|
||||||
defn: [v(:string)],
|
defn: [v(:string)],
|
||||||
},
|
},
|
||||||
|
resource: {
|
||||||
|
defn: [n(:identifier), n(:resource)],
|
||||||
|
},
|
||||||
}.map do |(name, conf)|
|
}.map do |(name, conf)|
|
||||||
blk = conf[:blk]
|
blk = conf[:blk]
|
||||||
defn = conf.fetch(:defn)
|
defn = conf.fetch(:defn)
|
||||||
|
@ -317,10 +326,6 @@ module CeleryScriptSettingsBag
|
||||||
args: [:x, :y, :z],
|
args: [:x, :y, :z],
|
||||||
tags: [:data, :location_like],
|
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: {
|
emergency_lock: {
|
||||||
tags: [:function, :firmware_user, :control_flow],
|
tags: [:function, :firmware_user, :control_flow],
|
||||||
},
|
},
|
||||||
|
@ -513,15 +518,22 @@ module CeleryScriptSettingsBag
|
||||||
tags: [:function, :firmware_user, :rpi_user],
|
tags: [:function, :firmware_user, :rpi_user],
|
||||||
blk: ->(n) { no_rpi_analog(n) },
|
blk: ->(n) { no_rpi_analog(n) },
|
||||||
},
|
},
|
||||||
resource_update: {
|
# DEPRECATED- Get rid of this node ASAP -RC 15 APR 2020
|
||||||
args: RESOURCE_UPDATE_ARGS,
|
resource_update: THIS_IS_DEPRECATED,
|
||||||
tags: [:function, :api_writer, :network_user],
|
resource: {
|
||||||
|
args: [:resource_type, :resource_id],
|
||||||
|
tags: [:network_user],
|
||||||
blk: ->(n) do
|
blk: ->(n) do
|
||||||
resource_type = n.args.fetch(:resource_type).value
|
resource_type = n.args.fetch(:resource_type).value
|
||||||
resource_id = n.args.fetch(:resource_id).value
|
resource_id = n.args.fetch(:resource_id).value
|
||||||
check_resource_type(n, resource_type, resource_id, Device.current)
|
check_resource_type(n, resource_type, resource_id, Device.current)
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
update_resource: {
|
||||||
|
args: [:resource],
|
||||||
|
body: [:pair],
|
||||||
|
tags: [:function, :api_writer, :network_user],
|
||||||
|
},
|
||||||
point_group: {
|
point_group: {
|
||||||
args: [:point_group_id],
|
args: [:point_group_id],
|
||||||
tags: [:data, :list_like],
|
tags: [:data, :list_like],
|
||||||
|
@ -529,7 +541,7 @@ module CeleryScriptSettingsBag
|
||||||
resource_id = n.args.fetch(:point_group_id).value
|
resource_id = n.args.fetch(:point_group_id).value
|
||||||
check_resource_type(n, "PointGroup", resource_id, Device.current)
|
check_resource_type(n, "PointGroup", resource_id, Device.current)
|
||||||
end,
|
end,
|
||||||
}
|
},
|
||||||
}.map { |(name, list)| Corpus.node(name, **list) }
|
}.map { |(name, list)| Corpus.node(name, **list) }
|
||||||
|
|
||||||
HASH = Corpus.as_json
|
HASH = Corpus.as_json
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
class PinBinding < ApplicationRecord
|
class PinBinding < ApplicationRecord
|
||||||
OFF_LIMITS = [
|
OFF_LIMITS = [
|
||||||
2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 24, 25, 27
|
2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 24, 25, 27,
|
||||||
]
|
]
|
||||||
BAD_PIN_NUM = \
|
BAD_PIN_NUM = "The following pin numbers cannot be used: %s" % OFF_LIMITS.join(", ")
|
||||||
"The following pin numbers cannot be used: %s" % OFF_LIMITS.join(", ")
|
|
||||||
|
|
||||||
belongs_to :device
|
belongs_to :device
|
||||||
belongs_to :sequence
|
belongs_to :sequence
|
||||||
enum special_action: { dump_info: "dump_info",
|
enum special_action: { emergency_lock: "emergency_lock",
|
||||||
emergency_lock: "emergency_lock",
|
|
||||||
emergency_unlock: "emergency_unlock",
|
emergency_unlock: "emergency_unlock",
|
||||||
power_off: "power_off",
|
power_off: "power_off",
|
||||||
read_status: "read_status",
|
read_status: "read_status",
|
||||||
reboot: "reboot",
|
reboot: "reboot",
|
||||||
sync: "sync",
|
sync: "sync",
|
||||||
take_photo: "take_photo" }
|
take_photo: "take_photo" }
|
||||||
validates :pin_num, uniqueness: { scope: :device }
|
validates :pin_num, uniqueness: { scope: :device }
|
||||||
|
|
||||||
def fancy_name
|
def fancy_name
|
||||||
|
|
|
@ -27,11 +27,19 @@ module Points
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
Point.transaction { point.update!(inputs.except(:point)) && point }
|
Point.transaction { point.update!(update_params) && point }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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?
|
def new_tool_id?
|
||||||
raw_inputs.key?("tool_id")
|
raw_inputs.key?("tool_id")
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
class BasePointSerializer < ApplicationSerializer
|
class BasePointSerializer < ApplicationSerializer
|
||||||
attributes :device_id, :name, :pointer_type, :meta, :x, :y, :z
|
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
|
def meta
|
||||||
object.meta || {}
|
object.meta || {}
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
class WeedSerializer < BasePointSerializer
|
class WeedSerializer < BasePointSerializer
|
||||||
attributes :radius, :discarded_at
|
attributes :radius, :discarded_at, :plant_stage
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
class MakeDefaulDeviceNameFarmbot < ActiveRecord::Migration[5.1]
|
class MakeDefaulDeviceNameFarmbot < ActiveRecord::Migration[5.1]
|
||||||
|
|
||||||
def change
|
def change
|
||||||
change_column_default(:devices, :name, "Farmbot")
|
change_column_default(:devices, :name, "Farmbot")
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
class AddShowWeedsToWebAppConfig < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :web_app_configs,
|
||||||
|
:show_weeds,
|
||||||
|
:boolean,
|
||||||
|
default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class UpdateMaxImageCount < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
change_column_default(:devices, :max_images_count, 450)
|
||||||
|
end
|
||||||
|
end
|
|
@ -150,8 +150,8 @@ ALTER SEQUENCE public.alerts_id_seq OWNED BY public.alerts.id;
|
||||||
CREATE TABLE public.ar_internal_metadata (
|
CREATE TABLE public.ar_internal_metadata (
|
||||||
key character varying NOT NULL,
|
key character varying NOT NULL,
|
||||||
value character varying,
|
value character varying,
|
||||||
created_at timestamp without time zone NOT NULL,
|
created_at timestamp(6) without time zone NOT NULL,
|
||||||
updated_at timestamp without time zone NOT NULL
|
updated_at timestamp(6) without time zone NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -262,7 +262,7 @@ CREATE TABLE public.devices (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
name character varying DEFAULT 'FarmBot'::character varying,
|
name character varying DEFAULT 'FarmBot'::character varying,
|
||||||
max_log_count integer DEFAULT 1000,
|
max_log_count integer DEFAULT 1000,
|
||||||
max_images_count integer DEFAULT 100,
|
max_images_count integer DEFAULT 450,
|
||||||
timezone character varying(280),
|
timezone character varying(280),
|
||||||
last_saw_api timestamp without time zone,
|
last_saw_api timestamp without time zone,
|
||||||
last_saw_mq timestamp without time zone,
|
last_saw_mq timestamp without time zone,
|
||||||
|
@ -1364,7 +1364,7 @@ CREATE VIEW public.resource_update_steps AS
|
||||||
edge_nodes.kind,
|
edge_nodes.kind,
|
||||||
edge_nodes.value
|
edge_nodes.value
|
||||||
FROM public.edge_nodes
|
FROM public.edge_nodes
|
||||||
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY (ARRAY[('"GenericPointer"'::character varying)::text, ('"ToolSlot"'::character varying)::text, ('"Plant"'::character varying)::text])))
|
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY ((ARRAY['"GenericPointer"'::character varying, '"ToolSlot"'::character varying, '"Plant"'::character varying])::text[])))
|
||||||
), resource_id AS (
|
), resource_id AS (
|
||||||
SELECT edge_nodes.primary_node_id,
|
SELECT edge_nodes.primary_node_id,
|
||||||
edge_nodes.kind,
|
edge_nodes.kind,
|
||||||
|
@ -1644,8 +1644,7 @@ CREATE TABLE public.users (
|
||||||
agreed_to_terms_at timestamp without time zone,
|
agreed_to_terms_at timestamp without time zone,
|
||||||
confirmation_sent_at timestamp without time zone,
|
confirmation_sent_at timestamp without time zone,
|
||||||
unconfirmed_email character varying,
|
unconfirmed_email character varying,
|
||||||
inactivity_warning_sent_at timestamp without time zone,
|
inactivity_warning_sent_at timestamp without time zone
|
||||||
inactivity_warning_count integer
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1729,7 +1728,9 @@ CREATE TABLE public.web_app_configs (
|
||||||
confirm_sequence_deletion boolean DEFAULT true,
|
confirm_sequence_deletion boolean DEFAULT true,
|
||||||
discard_unsaved_sequences boolean DEFAULT false,
|
discard_unsaved_sequences boolean DEFAULT false,
|
||||||
user_interface_read_only_mode boolean DEFAULT false,
|
user_interface_read_only_mode boolean DEFAULT false,
|
||||||
assertion_log integer DEFAULT 1
|
assertion_log integer DEFAULT 1,
|
||||||
|
show_zones boolean DEFAULT false,
|
||||||
|
show_weeds boolean DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -3376,6 +3377,9 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||||
('20191219212755'),
|
('20191219212755'),
|
||||||
('20191220010646'),
|
('20191220010646'),
|
||||||
('20200116140201'),
|
('20200116140201'),
|
||||||
('20200204192005');
|
('20200204192005'),
|
||||||
|
('20200204230135'),
|
||||||
|
('20200323235926'),
|
||||||
|
('20200412152208');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 ^
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { DesignerState } from "../farm_designer/interfaces";
|
import { DesignerState } from "../farm_designer/interfaces";
|
||||||
|
|
||||||
export const fakeDesignerState = (): DesignerState => ({
|
export const fakeDesignerState = (): DesignerState => ({
|
||||||
selectedPlants: undefined,
|
selectedPoints: undefined,
|
||||||
|
selectionPointType: undefined,
|
||||||
hoveredPlant: {
|
hoveredPlant: {
|
||||||
plantUUID: undefined,
|
plantUUID: undefined,
|
||||||
icon: ""
|
icon: ""
|
||||||
|
@ -13,8 +14,10 @@ export const fakeDesignerState = (): DesignerState => ({
|
||||||
cropSearchResults: [],
|
cropSearchResults: [],
|
||||||
cropSearchInProgress: false,
|
cropSearchInProgress: false,
|
||||||
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
||||||
currentPoint: undefined,
|
drawnPoint: undefined,
|
||||||
|
drawnWeed: undefined,
|
||||||
openedSavedGarden: undefined,
|
openedSavedGarden: undefined,
|
||||||
tryGroupSortType: undefined,
|
tryGroupSortType: undefined,
|
||||||
editGroupAreaInMap: false,
|
editGroupAreaInMap: false,
|
||||||
|
settingsSearchTerm: "",
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const mockDispatch = (innerDispatch = jest.fn()) =>
|
||||||
|
jest.fn(x => typeof x === "function" && x(innerDispatch));
|
|
@ -26,6 +26,7 @@ import {
|
||||||
TaggedAlert,
|
TaggedAlert,
|
||||||
TaggedPointGroup,
|
TaggedPointGroup,
|
||||||
TaggedFolder,
|
TaggedFolder,
|
||||||
|
TaggedWeedPointer,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { fakeResource } from "../fake_resource";
|
import { fakeResource } from "../fake_resource";
|
||||||
import {
|
import {
|
||||||
|
@ -133,7 +134,6 @@ export function fakeToolSlot(): TaggedToolSlotPointer {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
z: 0,
|
z: 0,
|
||||||
radius: 25,
|
|
||||||
pointer_type: "ToolSlot",
|
pointer_type: "ToolSlot",
|
||||||
meta: {},
|
meta: {},
|
||||||
tool_id: undefined,
|
tool_id: undefined,
|
||||||
|
@ -171,6 +171,20 @@ export function fakePoint(): TaggedGenericPointer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fakeWeed(): TaggedWeedPointer {
|
||||||
|
return fakeResource("Point", {
|
||||||
|
id: idCounter++,
|
||||||
|
name: "Weed 1",
|
||||||
|
pointer_type: "Weed",
|
||||||
|
x: 200,
|
||||||
|
y: 400,
|
||||||
|
z: 0,
|
||||||
|
radius: 100,
|
||||||
|
plant_stage: "planned",
|
||||||
|
meta: { created_by: "plant-detection", color: "red" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function fakeSavedGarden(): TaggedSavedGarden {
|
export function fakeSavedGarden(): TaggedSavedGarden {
|
||||||
return fakeResource("SavedGarden", {
|
return fakeResource("SavedGarden", {
|
||||||
id: idCounter++,
|
id: idCounter++,
|
||||||
|
@ -289,6 +303,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
||||||
show_sensor_readings: false,
|
show_sensor_readings: false,
|
||||||
show_plants: true,
|
show_plants: true,
|
||||||
show_points: true,
|
show_points: true,
|
||||||
|
show_weeds: true,
|
||||||
x_axis_inverted: false,
|
x_axis_inverted: false,
|
||||||
y_axis_inverted: false,
|
y_axis_inverted: false,
|
||||||
z_axis_inverted: true,
|
z_axis_inverted: true,
|
||||||
|
|
|
@ -7,4 +7,5 @@ jest.mock("../toast/toast", () => ({
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
warning: jest.fn(),
|
warning: jest.fn(),
|
||||||
busy: jest.fn(),
|
busy: jest.fn(),
|
||||||
|
removeToast: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -265,7 +265,6 @@ const tr11: TaggedPoint = {
|
||||||
"pointer_type": "ToolSlot",
|
"pointer_type": "ToolSlot",
|
||||||
"pullout_direction": 0,
|
"pullout_direction": 0,
|
||||||
"gantry_mounted": false,
|
"gantry_mounted": false,
|
||||||
"radius": 25,
|
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
"z": 10,
|
"z": 10,
|
||||||
|
@ -316,6 +315,28 @@ const tr15: TaggedResource = {
|
||||||
"uuid": "Tool.15.50"
|
"uuid": "Tool.15.50"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tr16: TaggedPoint = {
|
||||||
|
specialStatus: SpecialStatus.SAVED,
|
||||||
|
kind: "Point",
|
||||||
|
body: {
|
||||||
|
id: 1395,
|
||||||
|
created_at: "2017-05-24T20:41:19.889Z",
|
||||||
|
updated_at: "2017-05-24T20:41:19.889Z",
|
||||||
|
meta: {
|
||||||
|
color: "gray",
|
||||||
|
created_by: "plant-detection"
|
||||||
|
},
|
||||||
|
name: "untitled",
|
||||||
|
pointer_type: "Weed",
|
||||||
|
plant_stage: "planned",
|
||||||
|
radius: 10,
|
||||||
|
x: 490,
|
||||||
|
y: 421,
|
||||||
|
z: 5
|
||||||
|
},
|
||||||
|
uuid: "Point.1397.11"
|
||||||
|
};
|
||||||
|
|
||||||
const log: TaggedLog = {
|
const log: TaggedLog = {
|
||||||
kind: "Log",
|
kind: "Log",
|
||||||
specialStatus: SpecialStatus.SAVED,
|
specialStatus: SpecialStatus.SAVED,
|
||||||
|
@ -345,6 +366,7 @@ export const FAKE_RESOURCES: TaggedResource[] = [
|
||||||
tr0,
|
tr0,
|
||||||
tr14,
|
tr14,
|
||||||
tr15,
|
tr15,
|
||||||
|
tr16,
|
||||||
log,
|
log,
|
||||||
];
|
];
|
||||||
const KIND: keyof TaggedResource = "kind"; // Safety first, kids.
|
const KIND: keyof TaggedResource = "kind"; // Safety first, kids.
|
||||||
|
|
|
@ -334,6 +334,7 @@ const MUST_CONFIRM_LIST: ResourceName[] = [
|
||||||
"Regimen",
|
"Regimen",
|
||||||
"Image",
|
"Image",
|
||||||
"SavedGarden",
|
"SavedGarden",
|
||||||
|
"PointGroup",
|
||||||
];
|
];
|
||||||
|
|
||||||
const confirmationChecker = (resourceName: ResourceName, force = false) =>
|
const confirmationChecker = (resourceName: ResourceName, force = false) =>
|
||||||
|
|
|
@ -37,7 +37,9 @@ import { getDevice } from "../../../device";
|
||||||
import { talk } from "browser-speech";
|
import { talk } from "browser-speech";
|
||||||
import { MessageType } from "../../../sequences/interfaces";
|
import { MessageType } from "../../../sequences/interfaces";
|
||||||
import { FbjsEventName } from "farmbot/dist/constants";
|
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 { onLogs } from "../../log_handlers";
|
||||||
import { fakeState } from "../../../__test_support__/fake_state";
|
import { fakeState } from "../../../__test_support__/fake_state";
|
||||||
import { globalQueue } from "../../batch_queue";
|
import { globalQueue } from "../../batch_queue";
|
||||||
|
@ -177,7 +179,8 @@ describe("onOffline", () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
onOffline();
|
onOffline();
|
||||||
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
|
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();
|
jest.resetAllMocks();
|
||||||
onOnline();
|
onOnline();
|
||||||
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
|
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
|
||||||
|
expect(removeToast).toHaveBeenCalledWith("offline");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("onReconnect", () => {
|
describe("onReconnect()", () => {
|
||||||
onReconnect();
|
it("sends reconnect toast", () => {
|
||||||
expect(warning).toHaveBeenCalledWith(
|
onReconnect();
|
||||||
"Attempting to reconnect to the message broker", "Offline", "yellow");
|
expect(warning).toHaveBeenCalledWith(
|
||||||
|
"Attempting to reconnect to the message broker",
|
||||||
|
"Offline", "yellow", "offline");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("changeLastClientConnected", () => {
|
describe("changeLastClientConnected", () => {
|
||||||
|
@ -268,7 +275,8 @@ describe("onPublicBroadcast", () => {
|
||||||
console.log = jest.fn();
|
console.log = jest.fn();
|
||||||
onPublicBroadcast({});
|
onPublicBroadcast({});
|
||||||
expectBroadcastLog();
|
expectBroadcastLog();
|
||||||
expect(window.alert).toHaveBeenCalledWith(Content.FORCE_REFRESH_CANCEL_WARNING);
|
expect(window.alert).toHaveBeenCalledWith(
|
||||||
|
Content.FORCE_REFRESH_CANCEL_WARNING);
|
||||||
expect(location.assign).not.toHaveBeenCalled();
|
expect(location.assign).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,9 @@ import { Log } from "farmbot/dist/resources/api_resources";
|
||||||
import { Farmbot, BotStateTree, TaggedResource } from "farmbot";
|
import { Farmbot, BotStateTree, TaggedResource } from "farmbot";
|
||||||
import { FbjsEventName } from "farmbot/dist/constants";
|
import { FbjsEventName } from "farmbot/dist/constants";
|
||||||
import { noop } from "lodash";
|
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 { HardwareState } from "../devices/interfaces";
|
||||||
import { GetState, ReduxAction } from "../redux/interfaces";
|
import { GetState, ReduxAction } from "../redux/interfaces";
|
||||||
import { Content, Actions } from "../constants";
|
import { Content, Actions } from "../constants";
|
||||||
|
@ -102,11 +104,6 @@ export function readStatus() {
|
||||||
.then(() => { commandOK(noun); }, commandErr(noun));
|
.then(() => { commandOK(noun); }, commandErr(noun));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onOffline = () => {
|
|
||||||
dispatchNetworkDown("user.mqtt", now());
|
|
||||||
error(t(Content.MQTT_DISCONNECTED));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changeLastClientConnected = (bot: Farmbot) => () => {
|
export const changeLastClientConnected = (bot: Farmbot) => () => {
|
||||||
bot.setUserEnv({
|
bot.setUserEnv({
|
||||||
"LAST_CLIENT_CONNECTED": JSON.stringify(new Date())
|
"LAST_CLIENT_CONNECTED": JSON.stringify(new Date())
|
||||||
|
@ -157,14 +154,20 @@ export function onMalformed() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onOnline =
|
export const onOnline = () => {
|
||||||
() => {
|
removeToast("offline");
|
||||||
success(t("Reconnected to the message broker."), t("Online"));
|
success(t("Reconnected to the message broker."), t("Online"));
|
||||||
dispatchNetworkUp("user.mqtt", now());
|
dispatchNetworkUp("user.mqtt", now());
|
||||||
};
|
};
|
||||||
export const onReconnect =
|
|
||||||
() => warning(t("Attempting to reconnect to the message broker"),
|
export const onReconnect = () =>
|
||||||
t("Offline"), "yellow");
|
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) {
|
export function onPublicBroadcast(payl: unknown) {
|
||||||
console.log(FbjsEventName.publicBroadcast, payl);
|
console.log(FbjsEventName.publicBroadcast, payl);
|
||||||
|
|
|
@ -2,6 +2,27 @@ import { trim } from "./util";
|
||||||
|
|
||||||
export namespace ToolTips {
|
export namespace ToolTips {
|
||||||
|
|
||||||
|
// Farm Designer: Groups
|
||||||
|
export const SORT_DESCRIPTION =
|
||||||
|
trim(`When executing a sequence over a Group of locations, FarmBot will
|
||||||
|
travel to each group member in the order of the chosen sort method.
|
||||||
|
If the random option is chosen, FarmBot will travel in a random order
|
||||||
|
every time, so the ordering shown below will only be representative.`);
|
||||||
|
|
||||||
|
export const CRITERIA_SELECTION_COUNT =
|
||||||
|
trim(`Filter additions can only be removed by changing filters.
|
||||||
|
Click and drag in the map to modify selection filters.
|
||||||
|
Filters will be applied at the time of sequence execution. The final
|
||||||
|
selection at that time may differ from the selection currently
|
||||||
|
displayed.`);
|
||||||
|
|
||||||
|
export const CRITERIA_ALPHA_FEATURE =
|
||||||
|
trim(`Group filters is a new feature under active development.
|
||||||
|
Use with caution.`);
|
||||||
|
|
||||||
|
export const DOT_NOTATION_TIP =
|
||||||
|
trim(`Tip: Use dot notation (i.e., 'meta.color') to access meta fields.`);
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
export const MOVE =
|
export const MOVE =
|
||||||
trim(`Use these manual control buttons to move FarmBot in realtime.
|
trim(`Use these manual control buttons to move FarmBot in realtime.
|
||||||
|
@ -12,7 +33,12 @@ export namespace ToolTips {
|
||||||
|
|
||||||
export const WEBCAM =
|
export const WEBCAM =
|
||||||
trim(`If you have a webcam, you can view the video stream in this widget.
|
trim(`If you have a webcam, you can view the video stream in this widget.
|
||||||
Press the edit button to update and save your webcam URL.`);
|
Press the edit button to update and save your webcam URL.
|
||||||
|
Note: Some webcam services do not allow webcam feeds to be embedded in
|
||||||
|
other sites. If you see a web browser error after adding a webcam feed,
|
||||||
|
there is unfortunately nothing FarmBot can do to fix the problem.
|
||||||
|
Please contact your webcam's customer support to see if the security
|
||||||
|
policy for embedding feeds into other sites can be changed.`);
|
||||||
|
|
||||||
export const PERIPHERALS =
|
export const PERIPHERALS =
|
||||||
trim(`Use these toggle switches to control FarmBot's peripherals in
|
trim(`Use these toggle switches to control FarmBot's peripherals in
|
||||||
|
@ -26,10 +52,19 @@ export namespace ToolTips {
|
||||||
export const SENSOR_HISTORY =
|
export const SENSOR_HISTORY =
|
||||||
trim(`View and filter historical sensor reading data.`);
|
trim(`View and filter historical sensor reading data.`);
|
||||||
|
|
||||||
// Device
|
// FarmBot OS Settings: Firmware
|
||||||
export const OS_SETTINGS =
|
export const FIRMWARE_VALUE_API =
|
||||||
trim(`View and change device settings.`);
|
trim(`Firmware value from your choice in the dropdown to the left, as
|
||||||
|
understood by the Web App.`);
|
||||||
|
|
||||||
|
export const FIRMWARE_VALUE_FBOS =
|
||||||
|
trim(`Firmware value reported from the firmware, as understood by
|
||||||
|
FarmBot OS.`);
|
||||||
|
|
||||||
|
export const FIRMWARE_VALUE_MCU =
|
||||||
|
trim(`Firmware value reported from the firmware.`);
|
||||||
|
|
||||||
|
// Hardware Settings
|
||||||
export const HW_SETTINGS =
|
export const HW_SETTINGS =
|
||||||
trim(`Change settings of your FarmBot hardware with the fields below.
|
trim(`Change settings of your FarmBot hardware with the fields below.
|
||||||
Caution: Changing these settings to extreme values can cause hardware
|
Caution: Changing these settings to extreme values can cause hardware
|
||||||
|
@ -38,18 +73,6 @@ export namespace ToolTips {
|
||||||
Tip: Recalibrate FarmBot after changing settings and test a
|
Tip: Recalibrate FarmBot after changing settings and test a
|
||||||
few sequences to verify that everything works as expected.`);
|
few sequences to verify that everything works as expected.`);
|
||||||
|
|
||||||
export const PIN_BINDINGS =
|
|
||||||
trim(`Assign an action or sequence to execute when a Raspberry Pi
|
|
||||||
GPIO pin is activated.`);
|
|
||||||
|
|
||||||
export const PIN_BINDING_WARNING =
|
|
||||||
trim(`Warning: Binding to a pin without a physical button and
|
|
||||||
pull-down resistor connected may put FarmBot into an unstable state.`);
|
|
||||||
|
|
||||||
// Connectivity
|
|
||||||
export const CONNECTIVITY =
|
|
||||||
trim(`Diagnose connectivity issues with FarmBot and the browser.`);
|
|
||||||
|
|
||||||
// Hardware Settings: Homing and Calibration
|
// Hardware Settings: Homing and Calibration
|
||||||
export const HOMING_ENCODERS =
|
export const HOMING_ENCODERS =
|
||||||
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
|
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
|
||||||
|
@ -202,13 +225,16 @@ export namespace ToolTips {
|
||||||
trim(`The number of the pin to guard. This pin will be set to the specified
|
trim(`The number of the pin to guard. This pin will be set to the specified
|
||||||
state after the duration specified by TIMEOUT.`);
|
state after the duration specified by TIMEOUT.`);
|
||||||
|
|
||||||
|
// Hardware Settings: Pin Bindings
|
||||||
|
export const PIN_BINDINGS =
|
||||||
|
trim(`Assign an action or sequence to execute when a Raspberry Pi
|
||||||
|
GPIO pin is activated.`);
|
||||||
|
|
||||||
|
export const PIN_BINDING_WARNING =
|
||||||
|
trim(`Warning: Binding to a pin without a physical button and
|
||||||
|
pull-down resistor connected may put FarmBot into an unstable state.`);
|
||||||
|
|
||||||
// Farmware
|
// Farmware
|
||||||
export const FARMWARE =
|
|
||||||
trim(`Manage Farmware (plugins).`);
|
|
||||||
|
|
||||||
export const FARMWARE_LIST =
|
|
||||||
trim(`View, select, and install new Farmware.`);
|
|
||||||
|
|
||||||
export const PHOTOS =
|
export const PHOTOS =
|
||||||
trim(`Take and view photos with your FarmBot's camera.`);
|
trim(`Take and view photos with your FarmBot's camera.`);
|
||||||
|
|
||||||
|
@ -232,9 +258,6 @@ export namespace ToolTips {
|
||||||
You can also edit, copy, and delete existing sequences;
|
You can also edit, copy, and delete existing sequences;
|
||||||
assign a color; and give your commands custom names.`);
|
assign a color; and give your commands custom names.`);
|
||||||
|
|
||||||
export const SEQUENCE_LIST =
|
|
||||||
trim(`Here is the list of all of your sequences. Click one to edit.`);
|
|
||||||
|
|
||||||
export const DEFAULT_VALUE =
|
export const DEFAULT_VALUE =
|
||||||
trim(`Select a location to be used as the default value for this variable.
|
trim(`Select a location to be used as the default value for this variable.
|
||||||
If the sequence is ever run without the variable explicitly set to
|
If the sequence is ever run without the variable explicitly set to
|
||||||
|
@ -312,6 +335,7 @@ export namespace ToolTips {
|
||||||
export const TAKE_PHOTO =
|
export const TAKE_PHOTO =
|
||||||
trim(`Snaps a photo using the device camera. Select the camera type
|
trim(`Snaps a photo using the device camera. Select the camera type
|
||||||
on the Device page.`);
|
on the Device page.`);
|
||||||
|
|
||||||
export const EMERGENCY_LOCK =
|
export const EMERGENCY_LOCK =
|
||||||
trim(`Stops a device from moving until it is unlocked by a user.`);
|
trim(`Stops a device from moving until it is unlocked by a user.`);
|
||||||
|
|
||||||
|
@ -322,10 +346,7 @@ export namespace ToolTips {
|
||||||
trim(`The Mark As step allows FarmBot to programmatically edit the
|
trim(`The Mark As step allows FarmBot to programmatically edit the
|
||||||
properties of the UTM, plants, and weeds from within a sequence.
|
properties of the UTM, plants, and weeds from within a sequence.
|
||||||
For example, you can mark a plant as "planted" during a seeding
|
For example, you can mark a plant as "planted" during a seeding
|
||||||
sequence or delete a weed after removing it.`);
|
sequence or mark a weed as "removed" after removing it.`);
|
||||||
|
|
||||||
export const REBOOT =
|
|
||||||
trim(`Power cycle FarmBot's onboard computer.`);
|
|
||||||
|
|
||||||
export const SET_SERVO_ANGLE =
|
export const SET_SERVO_ANGLE =
|
||||||
trim(`Move a servo to the provided angle. An angle of 90 degrees
|
trim(`Move a servo to the provided angle. An angle of 90 degrees
|
||||||
|
@ -338,6 +359,9 @@ export namespace ToolTips {
|
||||||
export const MOVE_TO_HOME =
|
export const MOVE_TO_HOME =
|
||||||
trim(`Move FarmBot to home for the provided axis.`);
|
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 =
|
export const FIRMWARE_ACTION =
|
||||||
trim(`FarmBot OS or micro-controller firmware action.`);
|
trim(`FarmBot OS or micro-controller firmware action.`);
|
||||||
|
|
||||||
|
@ -363,20 +387,6 @@ export namespace ToolTips {
|
||||||
growing at the same or different times. Multiple regimens can be
|
growing at the same or different times. Multiple regimens can be
|
||||||
applied to any one plant.`);
|
applied to any one plant.`);
|
||||||
|
|
||||||
export const REGIMEN_LIST =
|
|
||||||
trim(`This is a list of all of your regimens. Click one to begin
|
|
||||||
editing it.`);
|
|
||||||
|
|
||||||
// Tools
|
|
||||||
export const TOOL_LIST =
|
|
||||||
trim(`This is a list of all your FarmBot tools and seed containers.
|
|
||||||
Click the Edit button to add, edit, or delete tools or seed containers.`);
|
|
||||||
|
|
||||||
export const TOOLBAY_LIST =
|
|
||||||
trim(`Tool slots are where you store your FarmBot tools and seed
|
|
||||||
containers, which should be reflective of your real FarmBot hardware
|
|
||||||
configuration.`);
|
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
export const LOGS =
|
export const LOGS =
|
||||||
trim(`View and filter log messages.`);
|
trim(`View and filter log messages.`);
|
||||||
|
@ -399,16 +409,6 @@ export namespace ToolTips {
|
||||||
|
|
||||||
export const FIRMWARE_DEBUG_MESSAGES =
|
export const FIRMWARE_DEBUG_MESSAGES =
|
||||||
trim(`Log all debug received from firmware (clears after refresh).`);
|
trim(`Log all debug received from firmware (clears after refresh).`);
|
||||||
|
|
||||||
export const MESSAGES =
|
|
||||||
trim(`View messages.`);
|
|
||||||
|
|
||||||
// App
|
|
||||||
export const LABS =
|
|
||||||
trim(`Customize your web app experience.`);
|
|
||||||
|
|
||||||
export const TOURS =
|
|
||||||
trim(`Take a guided tour of the Web App.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Content {
|
export namespace Content {
|
||||||
|
@ -512,11 +512,9 @@ export namespace Content {
|
||||||
real account at`);
|
real account at`);
|
||||||
|
|
||||||
// App Settings
|
// App Settings
|
||||||
export const CONFIRM_STEP_DELETION =
|
export const TIME_FORMAT_24_HOUR =
|
||||||
trim(`Show a confirmation dialog when deleting a sequence step.`);
|
trim(`Display time using the 24-hour notation,
|
||||||
|
i.e., 23:00 instead of 11:00pm`);
|
||||||
export const CONFIRM_SEQUENCE_DELETION =
|
|
||||||
trim(`Show a confirmation dialog when deleting a sequence.`);
|
|
||||||
|
|
||||||
export const HIDE_WEBCAM_WIDGET =
|
export const HIDE_WEBCAM_WIDGET =
|
||||||
trim(`If not using a webcam, use this setting to remove the
|
trim(`If not using a webcam, use this setting to remove the
|
||||||
|
@ -526,14 +524,6 @@ export namespace Content {
|
||||||
trim(`If not using sensors, use this setting to remove the
|
trim(`If not using sensors, use this setting to remove the
|
||||||
widget from the Controls page.`);
|
widget from the Controls page.`);
|
||||||
|
|
||||||
export const DYNAMIC_MAP_SIZE =
|
|
||||||
trim(`Change the garden map size based on axis length.
|
|
||||||
A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
|
|
||||||
the HARDWARE widget. Overrides MAP SIZE values.`);
|
|
||||||
|
|
||||||
export const PLANT_ANIMATIONS =
|
|
||||||
trim(`Enable plant animations in the garden map.`);
|
|
||||||
|
|
||||||
export const BROWSER_SPEAK_LOGS =
|
export const BROWSER_SPEAK_LOGS =
|
||||||
trim(`Have the browser also read aloud log messages on the
|
trim(`Have the browser also read aloud log messages on the
|
||||||
"Speak" channel that are spoken by FarmBot.`);
|
"Speak" channel that are spoken by FarmBot.`);
|
||||||
|
@ -546,22 +536,25 @@ export namespace Content {
|
||||||
trim(`Warning! When enabled, any unsaved changes
|
trim(`Warning! When enabled, any unsaved changes
|
||||||
will be discarded when refreshing or closing the page. Are you sure?`);
|
will be discarded when refreshing or closing the page. Are you sure?`);
|
||||||
|
|
||||||
export const DISCARD_UNSAVED_SEQUENCE_CHANGES =
|
export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
|
||||||
trim(`Don't ask about saving sequence work before
|
trim(`Confirm when unlocking FarmBot after an emergency stop.`);
|
||||||
closing browser tab. Warning: may cause loss of data.`);
|
|
||||||
|
|
||||||
export const DISCARD_UNSAVED_SEQUENCE_CHANGES_CONFIRM =
|
export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
|
||||||
trim(`Warning! When enabled, any unsaved changes to sequences
|
trim(`Warning! When disabled, clicking the UNLOCK button will immediately
|
||||||
will be discarded when refreshing or closing the page. Are you sure?`);
|
unlock FarmBot instead of confirming that it is safe to do so.
|
||||||
|
As a result, double-clicking the E-STOP button may not stop FarmBot.
|
||||||
|
Are you sure you want to disable this feature?`);
|
||||||
|
|
||||||
export const VIRTUAL_TRAIL =
|
export const USER_INTERFACE_READ_ONLY_MODE =
|
||||||
trim(`Display a virtual trail for FarmBot in the garden map to show
|
trim(`Disallow account data changes. This does
|
||||||
movement and watering history while the map is open. Toggling this setting
|
not prevent Farmwares or FarmBot OS from changing settings.`);
|
||||||
will clear data for the current trail.`);
|
|
||||||
|
|
||||||
export const TIME_FORMAT_24_HOUR =
|
// Sequence Settings
|
||||||
trim(`Display time using the 24-hour notation,
|
export const CONFIRM_STEP_DELETION =
|
||||||
i.e., 23:00 instead of 11:00pm`);
|
trim(`Show a confirmation dialog when deleting a sequence step.`);
|
||||||
|
|
||||||
|
export const CONFIRM_SEQUENCE_DELETION =
|
||||||
|
trim(`Show a confirmation dialog when deleting a sequence.`);
|
||||||
|
|
||||||
export const SHOW_PINS =
|
export const SHOW_PINS =
|
||||||
trim(`Show raw pin lists in Read Sensor, Control Peripheral, and
|
trim(`Show raw pin lists in Read Sensor, Control Peripheral, and
|
||||||
|
@ -570,18 +563,27 @@ export namespace Content {
|
||||||
export const EXPAND_STEP_OPTIONS =
|
export const EXPAND_STEP_OPTIONS =
|
||||||
trim(`Choose whether advanced step options are open or closed by default.`);
|
trim(`Choose whether advanced step options are open or closed by default.`);
|
||||||
|
|
||||||
export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
|
export const DISCARD_UNSAVED_SEQUENCE_CHANGES =
|
||||||
trim(`Confirm when unlocking FarmBot after an emergency stop.`);
|
trim(`Don't ask about saving sequence work before
|
||||||
|
closing browser tab. Warning: may cause loss of data.`);
|
||||||
|
|
||||||
export const USER_INTERFACE_READ_ONLY_MODE =
|
export const DISCARD_UNSAVED_SEQUENCE_CHANGES_CONFIRM =
|
||||||
trim(`Disallow account data changes. This does
|
trim(`Warning! When enabled, any unsaved changes to sequences
|
||||||
not prevent Farmwares or FarmBot OS from changing settings.`);
|
will be discarded when refreshing or closing the page. Are you sure?`);
|
||||||
|
|
||||||
export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
|
// Farm Designer Settings
|
||||||
trim(`Warning! When disabled, clicking the UNLOCK button will immediately
|
export const PLANT_ANIMATIONS =
|
||||||
unlock FarmBot instead of confirming that it is safe to do so.
|
trim(`Enable plant animations in the garden map.`);
|
||||||
As a result, double-clicking the E-STOP button may not stop FarmBot.
|
|
||||||
Are you sure you want to disable this feature?`);
|
export const VIRTUAL_TRAIL =
|
||||||
|
trim(`Display a virtual trail for FarmBot in the garden map to show
|
||||||
|
movement and watering history while the map is open. Toggling this setting
|
||||||
|
will clear data for the current trail.`);
|
||||||
|
|
||||||
|
export const DYNAMIC_MAP_SIZE =
|
||||||
|
trim(`Change the garden map size based on axis length.
|
||||||
|
A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
|
||||||
|
the HARDWARE widget. Overrides MAP SIZE values.`);
|
||||||
|
|
||||||
export const MAP_SIZE =
|
export const MAP_SIZE =
|
||||||
trim(`Specify custom map dimensions (in millimeters).
|
trim(`Specify custom map dimensions (in millimeters).
|
||||||
|
@ -600,13 +602,41 @@ export namespace Content {
|
||||||
export const CONFIRM_PLANT_DELETION =
|
export const CONFIRM_PLANT_DELETION =
|
||||||
trim(`Show a confirmation dialog when deleting a plant.`);
|
trim(`Show a confirmation dialog when deleting a plant.`);
|
||||||
|
|
||||||
// Device
|
// FarmBot OS Settings
|
||||||
export const NOT_HTTPS =
|
export const DIFFERENT_TZ_WARNING =
|
||||||
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
|
trim(`Note: The selected timezone for your FarmBot is different than
|
||||||
|
your local browser time.`);
|
||||||
|
|
||||||
export const CONTACT_SYSADMIN =
|
export const OS_BETA_RELEASES =
|
||||||
trim(`Please contact the system(s) administrator(s) and ask them to enable
|
trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
|
||||||
HTTPS://`);
|
FarmBot system stability. Are you sure?`);
|
||||||
|
|
||||||
|
export const DEVICE_NEVER_SEEN =
|
||||||
|
trim(`The device has never been seen. Most likely,
|
||||||
|
there is a network connectivity issue on the device's end.`);
|
||||||
|
|
||||||
|
export const TOO_OLD_TO_UPDATE =
|
||||||
|
trim(`Please re-flash your FarmBot's SD card.`);
|
||||||
|
|
||||||
|
export const OS_AUTO_UPDATE =
|
||||||
|
trim(`When enabled, FarmBot OS will automatically download and install
|
||||||
|
software updates at the chosen time.`);
|
||||||
|
|
||||||
|
export const AUTO_SYNC =
|
||||||
|
trim(`When enabled, device resources such as sequences and regimens
|
||||||
|
will be sent to the device automatically. This removes the need to push
|
||||||
|
"SYNC" after making changes in the web app. Changes to running
|
||||||
|
sequences and regimens while auto sync is enabled will result in
|
||||||
|
instantaneous change.`);
|
||||||
|
|
||||||
|
// FarmBot OS Settings: Power and Reset
|
||||||
|
export const RESTART_FARMBOT =
|
||||||
|
trim(`This will restart FarmBot's Raspberry Pi and controller
|
||||||
|
software.`);
|
||||||
|
|
||||||
|
export const SHUTDOWN_FARMBOT =
|
||||||
|
trim(`This will shutdown FarmBot's Raspberry Pi. To turn it
|
||||||
|
back on, unplug FarmBot and plug it back in.`);
|
||||||
|
|
||||||
export const FACTORY_RESET_WARNING =
|
export const FACTORY_RESET_WARNING =
|
||||||
trim(`Factory resetting your FarmBot will destroy all data on the device,
|
trim(`Factory resetting your FarmBot will destroy all data on the device,
|
||||||
|
@ -624,10 +654,6 @@ export namespace Content {
|
||||||
not delete data stored in your web app account. Are you sure you wish
|
not delete data stored in your web app account. Are you sure you wish
|
||||||
to continue?`);
|
to continue?`);
|
||||||
|
|
||||||
export const MCU_RESET_ALERT =
|
|
||||||
trim(`Warning: This will reset all hardware settings to the default values.
|
|
||||||
Are you sure you wish to continue?`);
|
|
||||||
|
|
||||||
export const AUTO_FACTORY_RESET =
|
export const AUTO_FACTORY_RESET =
|
||||||
trim(`Automatically factory reset when the WiFi network cannot be
|
trim(`Automatically factory reset when the WiFi network cannot be
|
||||||
detected. Useful for network changes.`);
|
detected. Useful for network changes.`);
|
||||||
|
@ -636,54 +662,26 @@ export namespace Content {
|
||||||
trim(`Time in minutes to attempt connecting to WiFi before a factory
|
trim(`Time in minutes to attempt connecting to WiFi before a factory
|
||||||
reset.`);
|
reset.`);
|
||||||
|
|
||||||
export const DIFFERENT_TZ_WARNING =
|
export const NOT_HTTPS =
|
||||||
trim(`Note: The selected timezone for your FarmBot is different than
|
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
|
||||||
your local browser time.`);
|
|
||||||
|
|
||||||
export const RESTART_FARMBOT =
|
export const CONTACT_SYSADMIN =
|
||||||
trim(`This will restart FarmBot's Raspberry Pi and controller
|
trim(`Please contact the system(s) administrator(s) and ask them to enable
|
||||||
software.`);
|
HTTPS://`);
|
||||||
|
|
||||||
|
// FarmBot OS Settings: Firmware
|
||||||
export const RESTART_FIRMWARE =
|
export const RESTART_FIRMWARE =
|
||||||
trim(`Restart the Farmduino or Arduino firmware.`);
|
trim(`Restart the Farmduino or Arduino firmware.`);
|
||||||
|
|
||||||
export const OS_AUTO_UPDATE =
|
// Hardware Settings: Danger Zone
|
||||||
trim(`When enabled, FarmBot OS will automatically download and install
|
|
||||||
software updates at the chosen time.`);
|
|
||||||
|
|
||||||
export const AUTO_SYNC =
|
|
||||||
trim(`When enabled, device resources such as sequences and regimens
|
|
||||||
will be sent to the device automatically. This removes the need to push
|
|
||||||
"SYNC" after making changes in the web app. Changes to running
|
|
||||||
sequences and regimens while auto sync is enabled will result in
|
|
||||||
instantaneous change.`);
|
|
||||||
|
|
||||||
export const SHUTDOWN_FARMBOT =
|
|
||||||
trim(`This will shutdown FarmBot's Raspberry Pi. To turn it
|
|
||||||
back on, unplug FarmBot and plug it back in.`);
|
|
||||||
|
|
||||||
export const OS_BETA_RELEASES =
|
|
||||||
trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
|
|
||||||
FarmBot system stability. Are you sure?`);
|
|
||||||
|
|
||||||
export const DIAGNOSTIC_CHECK =
|
|
||||||
trim(`Save snapshot of FarmBot OS system information, including
|
|
||||||
user and device identity, to the database. A code will be returned
|
|
||||||
that you can provide in support requests to allow FarmBot to look up
|
|
||||||
data relevant to the issue to help us identify the problem.`);
|
|
||||||
|
|
||||||
export const DEVICE_NEVER_SEEN =
|
|
||||||
trim(`The device has never been seen. Most likely,
|
|
||||||
there is a network connectivity issue on the device's end.`);
|
|
||||||
|
|
||||||
export const TOO_OLD_TO_UPDATE =
|
|
||||||
trim(`Please re-flash your FarmBot's SD card.`);
|
|
||||||
|
|
||||||
// Hardware Settings
|
|
||||||
export const RESTORE_DEFAULT_HARDWARE_SETTINGS =
|
export const RESTORE_DEFAULT_HARDWARE_SETTINGS =
|
||||||
trim(`Restoring hardware parameter defaults will destroy the
|
trim(`Restoring hardware parameter defaults will destroy the
|
||||||
current settings, resetting them to default values.`);
|
current settings, resetting them to default values.`);
|
||||||
|
|
||||||
|
export const MCU_RESET_ALERT =
|
||||||
|
trim(`Warning: This will reset all hardware settings to the default values.
|
||||||
|
Are you sure you wish to continue?`);
|
||||||
|
|
||||||
// App
|
// App
|
||||||
export const APP_LOAD_TIMEOUT_MESSAGE =
|
export const APP_LOAD_TIMEOUT_MESSAGE =
|
||||||
trim(`App could not be fully loaded, we recommend you try
|
trim(`App could not be fully loaded, we recommend you try
|
||||||
|
@ -711,10 +709,6 @@ export namespace Content {
|
||||||
broken and may break or otherwise hinder your usage of the rest of the
|
broken and may break or otherwise hinder your usage of the rest of the
|
||||||
app. This feature may disappear or break at any time.`);
|
app. This feature may disappear or break at any time.`);
|
||||||
|
|
||||||
export const NEW_TOS =
|
|
||||||
trim(`Before logging in, you must agree to our latest Terms of Service and
|
|
||||||
Privacy Policy`);
|
|
||||||
|
|
||||||
export const FORCE_REFRESH_CONFIRM =
|
export const FORCE_REFRESH_CONFIRM =
|
||||||
trim(`A new version of the FarmBot web app has been released.
|
trim(`A new version of the FarmBot web app has been released.
|
||||||
Refresh page?`);
|
Refresh page?`);
|
||||||
|
@ -746,6 +740,15 @@ export namespace Content {
|
||||||
encoders, stall detection, or endstops enabled for the chosen axis.
|
encoders, stall detection, or endstops enabled for the chosen axis.
|
||||||
Enable endstops, encoders, or stall detection from the Device page for: `);
|
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 =
|
export const IN_USE =
|
||||||
trim(`Used in another resource. Protected from deletion.`);
|
trim(`Used in another resource. Protected from deletion.`);
|
||||||
|
|
||||||
|
@ -773,7 +776,7 @@ export namespace Content {
|
||||||
trim(`Click and drag or use the inputs to draw a weed.`);
|
trim(`Click and drag or use the inputs to draw a weed.`);
|
||||||
|
|
||||||
export const BOX_SELECT_DESCRIPTION =
|
export const BOX_SELECT_DESCRIPTION =
|
||||||
trim(`Drag a box around the plants you would like to select.
|
trim(`Drag a box around the items you would like to select.
|
||||||
Press the back arrow to exit.`);
|
Press the back arrow to exit.`);
|
||||||
|
|
||||||
export const SAVED_GARDENS =
|
export const SAVED_GARDENS =
|
||||||
|
@ -819,7 +822,8 @@ export namespace Content {
|
||||||
|
|
||||||
export const MOUNTED_TOOL =
|
export const MOUNTED_TOOL =
|
||||||
trim(`The tool currently mounted to the UTM can be set here or by using
|
trim(`The tool currently mounted to the UTM can be set here or by using
|
||||||
a MARK AS step in a sequence.`);
|
a MARK AS step in a sequence. Use the verify button or read the tool
|
||||||
|
verification pin in a sequence to verify that a tool is attached.`);
|
||||||
|
|
||||||
// Farm Events
|
// Farm Events
|
||||||
export const NOTHING_SCHEDULED =
|
export const NOTHING_SCHEDULED =
|
||||||
|
@ -831,10 +835,6 @@ export namespace Content {
|
||||||
regimen tasks. Consider rescheduling this event to tomorrow if
|
regimen tasks. Consider rescheduling this event to tomorrow if
|
||||||
this is a concern.`);
|
this is a concern.`);
|
||||||
|
|
||||||
export const INVALID_RUN_TIME =
|
|
||||||
trim(`This event does not appear to have a valid run time.
|
|
||||||
Perhaps you entered bad dates?`);
|
|
||||||
|
|
||||||
export const FARM_EVENT_TZ_WARNING =
|
export const FARM_EVENT_TZ_WARNING =
|
||||||
trim(`Note: Times displayed according to FarmBot's local time, which
|
trim(`Note: Times displayed according to FarmBot's local time, which
|
||||||
is currently different from your browser's time. Timezone data is
|
is currently different from your browser's time. Timezone data is
|
||||||
|
@ -849,27 +849,14 @@ export namespace Content {
|
||||||
trim(`You haven't made any sequences or regimens yet. To add an event,
|
trim(`You haven't made any sequences or regimens yet. To add an event,
|
||||||
first create a sequence or regimen.`);
|
first create a sequence or regimen.`);
|
||||||
|
|
||||||
// Groups
|
|
||||||
export const SORT_DESCRIPTION =
|
|
||||||
trim(`When executing a sequence over a Group of locations, FarmBot will
|
|
||||||
travel to each group member in the order of the chosen sort method.
|
|
||||||
If the random option is chosen, FarmBot will travel in a random order
|
|
||||||
every time, so the ordering shown below will only be representative.`);
|
|
||||||
|
|
||||||
export const CRITERIA_SELECTION_COUNT =
|
|
||||||
trim(`Criteria additions can only be removed by changing criteria.
|
|
||||||
Click and drag in the map to modify selection criteria.
|
|
||||||
Criteria will be applied at the time of sequence execution. The final
|
|
||||||
selection at that time may differ from the selection currently
|
|
||||||
displayed.`);
|
|
||||||
|
|
||||||
// Farmware
|
// Farmware
|
||||||
export const NO_IMAGES_YET =
|
export const NO_IMAGES_YET =
|
||||||
trim(`You haven't yet taken any photos with your FarmBot.
|
trim(`You haven't yet taken any photos with your FarmBot.
|
||||||
Once you do, they will show up here.`);
|
Once you do, they will show up here.`);
|
||||||
|
|
||||||
export const PROCESSING_PHOTO =
|
export const PROCESSING_PHOTO =
|
||||||
trim(`Processing now. Results usually available in one minute.`);
|
trim(`Processing now. Results usually available in one minute.
|
||||||
|
Check log messages for result status.`);
|
||||||
|
|
||||||
export const NOT_AVAILABLE_WHEN_OFFLINE =
|
export const NOT_AVAILABLE_WHEN_OFFLINE =
|
||||||
trim(`Not available when device is offline.`);
|
trim(`Not available when device is offline.`);
|
||||||
|
@ -945,6 +932,8 @@ export namespace TourContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DeviceSetting {
|
export enum DeviceSetting {
|
||||||
|
axisHeadingLabels = ``,
|
||||||
|
|
||||||
// Homing and calibration
|
// Homing and calibration
|
||||||
homingAndCalibration = `Homing and Calibration`,
|
homingAndCalibration = `Homing and Calibration`,
|
||||||
homing = `Homing`,
|
homing = `Homing`,
|
||||||
|
@ -996,6 +985,11 @@ export enum DeviceSetting {
|
||||||
|
|
||||||
// Pin Guard
|
// Pin Guard
|
||||||
pinGuard = `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
|
// Danger Zone
|
||||||
dangerZone = `Danger Zone`,
|
dangerZone = `Danger Zone`,
|
||||||
|
@ -1003,6 +997,8 @@ export enum DeviceSetting {
|
||||||
|
|
||||||
// Pin Bindings
|
// Pin Bindings
|
||||||
pinBindings = `Pin Bindings`,
|
pinBindings = `Pin Bindings`,
|
||||||
|
savedPinBindings = `Saved pin bindings`,
|
||||||
|
addNewPinBinding = `Add new pin binding`,
|
||||||
|
|
||||||
// FarmBot OS
|
// FarmBot OS
|
||||||
farmbot = `FarmBot`,
|
farmbot = `FarmBot`,
|
||||||
|
@ -1020,7 +1016,6 @@ export enum DeviceSetting {
|
||||||
powerAndReset = `Power and Reset`,
|
powerAndReset = `Power and Reset`,
|
||||||
restartFarmbot = `Restart Farmbot`,
|
restartFarmbot = `Restart Farmbot`,
|
||||||
shutdownFarmbot = `Shutdown Farmbot`,
|
shutdownFarmbot = `Shutdown Farmbot`,
|
||||||
restartFirmware = `Restart Firmware`,
|
|
||||||
factoryReset = `Factory Reset`,
|
factoryReset = `Factory Reset`,
|
||||||
autoFactoryReset = `Automatic Factory Reset`,
|
autoFactoryReset = `Automatic Factory Reset`,
|
||||||
connectionAttemptPeriod = `Connection Attempt Period`,
|
connectionAttemptPeriod = `Connection Attempt Period`,
|
||||||
|
@ -1038,6 +1033,7 @@ export enum DeviceSetting {
|
||||||
|
|
||||||
// Firmware
|
// Firmware
|
||||||
firmwareSection = `Firmware`,
|
firmwareSection = `Firmware`,
|
||||||
|
restartFirmware = `Restart Firmware`,
|
||||||
flashFirmware = `Flash firmware`,
|
flashFirmware = `Flash firmware`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1139,7 +1135,8 @@ export enum Actions {
|
||||||
|
|
||||||
// Designer
|
// Designer
|
||||||
SEARCH_QUERY_CHANGE = "SEARCH_QUERY_CHANGE",
|
SEARCH_QUERY_CHANGE = "SEARCH_QUERY_CHANGE",
|
||||||
SELECT_PLANT = "SELECT_PLANT",
|
SELECT_POINT = "SELECT_POINT",
|
||||||
|
SET_SELECTION_POINT_TYPE = "SET_SELECTION_POINT_TYPE",
|
||||||
TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT",
|
TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT",
|
||||||
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
|
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
|
||||||
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
|
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
|
||||||
|
@ -1148,9 +1145,11 @@ export enum Actions {
|
||||||
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",
|
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",
|
||||||
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",
|
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",
|
||||||
CHOOSE_LOCATION = "CHOOSE_LOCATION",
|
CHOOSE_LOCATION = "CHOOSE_LOCATION",
|
||||||
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
|
SET_DRAWN_POINT_DATA = "SET_DRAWN_POINT_DATA",
|
||||||
|
SET_DRAWN_WEED_DATA = "SET_DRAWN_WEED_DATA",
|
||||||
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
||||||
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
||||||
|
SET_SETTINGS_SEARCH_TERM = "SET_SETTINGS_SEARCH_TERM",
|
||||||
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
|
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
|
||||||
|
|
||||||
// Regimens
|
// Regimens
|
||||||
|
|
|
@ -38,6 +38,7 @@ $pink: #ebb;
|
||||||
$light_red: #e99;
|
$light_red: #e99;
|
||||||
$red: #e66;
|
$red: #e66;
|
||||||
$dark_red: #f00;
|
$dark_red: #f00;
|
||||||
|
$medium_dark_red: #c00;
|
||||||
$darkest_red: #900;
|
$darkest_red: #900;
|
||||||
$panel_green: #35761b;
|
$panel_green: #35761b;
|
||||||
$panel_light_green: #f3f9f1;
|
$panel_light_green: #f3f9f1;
|
||||||
|
|
|
@ -120,46 +120,68 @@
|
||||||
|
|
||||||
.thin-search-wrapper {
|
.thin-search-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.text-input-wrapper {
|
.thin-search {
|
||||||
position: relative;
|
.spinner-container {
|
||||||
margin: 1rem;
|
|
||||||
border-bottom: 1px solid $dark_gray;
|
|
||||||
&:before,
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
background: $dark_gray;
|
|
||||||
width: 1px;
|
|
||||||
height: 3px;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
right: 0;
|
right: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
i {
|
.text-input-wrapper {
|
||||||
font-size: 1.5rem;
|
position: relative;
|
||||||
|
margin: 1rem;
|
||||||
|
border-bottom: 1px solid $dark_gray;
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
background: $dark_gray;
|
||||||
|
width: 1px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.fa-search {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.8rem;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fa-search {
|
input {
|
||||||
position: absolute;
|
background: transparent;
|
||||||
top: 0.8rem;
|
box-shadow: none !important;
|
||||||
left: 1rem;
|
padding-left: 3rem !important;
|
||||||
cursor: default !important;
|
font-size: 1.4rem !important;
|
||||||
}
|
&:active,
|
||||||
}
|
&:focus {
|
||||||
input {
|
background: transparent !important;
|
||||||
background: transparent;
|
}
|
||||||
box-shadow: none !important;
|
&::-webkit-input-placeholder {
|
||||||
padding-left: 3rem !important;
|
color: $placeholder_gray;
|
||||||
font-size: 1.4rem !important;
|
}
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
&::-webkit-input-placeholder {
|
|
||||||
color: $placeholder_gray;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,23 +207,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%panel-item-base {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
.plant-search-item,
|
.plant-search-item,
|
||||||
.group-search-item {
|
.group-search-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
img {
|
img {
|
||||||
margin: 0 1rem 0 0;
|
margin-right: 0.5rem;
|
||||||
height: 4rem;
|
height: 3rem;
|
||||||
width: 4rem;
|
width: 3rem;
|
||||||
|
}
|
||||||
|
&.quick-del {
|
||||||
|
&:hover {
|
||||||
|
background: lighten($red, 10%) !important;
|
||||||
|
&:after {
|
||||||
|
content: "x";
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: $darkest_red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
%panel-item-base {
|
|
||||||
text-align: right;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding-top: 1.4rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
.plant-search-item-age {
|
.plant-search-item-age {
|
||||||
@extend %panel-item-base;
|
@extend %panel-item-base;
|
||||||
|
@ -209,6 +241,7 @@
|
||||||
.group-item-count {
|
.group-item-count {
|
||||||
@extend %panel-item-base;
|
@extend %panel-item-base;
|
||||||
padding-top: 0.6rem;
|
padding-top: 0.6rem;
|
||||||
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
.plant-search-item-name {
|
.plant-search-item-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -219,24 +252,27 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
.weed-search-item,
|
||||||
.point-search-item {
|
.point-search-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
.saucer {
|
.saucer {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 1rem 0 0;
|
height: 3rem;
|
||||||
height: 2rem;
|
width: 3rem;
|
||||||
width: 2rem;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.weed-search-item-info,
|
||||||
.point-search-item-info {
|
.point-search-item-info {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding-top: 0.6rem;
|
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
|
line-height: 3rem;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
.weed-search-item-name,
|
||||||
.point-search-item-name {
|
.point-search-item-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -244,19 +280,34 @@
|
||||||
width: 40%;
|
width: 40%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-left: 1rem;
|
margin-left: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
.tool-search-item,
|
||||||
|
.tool-slot-search-item {
|
||||||
.thin-search {
|
line-height: 4rem;
|
||||||
.spinner-container {
|
cursor: pointer;
|
||||||
position: absolute;
|
.row {
|
||||||
top: 0;
|
margin-left: 0;
|
||||||
right: 0;
|
margin-right: 0;
|
||||||
width: 2rem;
|
}
|
||||||
height: 2rem;
|
.tool-slot-search-item-name {
|
||||||
padding: 0;
|
margin-left: -1rem;
|
||||||
margin-right: 1rem;
|
}
|
||||||
|
p {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
&.tool-status,
|
||||||
|
&.tool-slot-position {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.tool-slot-position-info {
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 1.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,11 +335,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-point {
|
.map-point {
|
||||||
|
cursor: pointer !important;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
stroke-opacity: 0.3;
|
stroke-opacity: 0.3;
|
||||||
fill-opacity: 0.1;
|
fill-opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-weed {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weed-image,
|
||||||
.plant-image {
|
.plant-image {
|
||||||
transform-origin: bottom;
|
transform-origin: bottom;
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
|
@ -337,6 +394,9 @@
|
||||||
fill: $white;
|
fill: $white;
|
||||||
stroke: $white;
|
stroke: $white;
|
||||||
}
|
}
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,10 +564,10 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.more-bugs,
|
.more-bugs,
|
||||||
|
.select-mode,
|
||||||
.move-to-mode {
|
.move-to-mode {
|
||||||
button {
|
margin: auto;
|
||||||
margin-right: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
|
||||||
p {
|
p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
|
|
|
@ -291,9 +291,19 @@
|
||||||
.panel-action-buttons {
|
.panel-action-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
|
height: 16rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $panel_medium_light_gray;
|
background: $panel_medium_light_gray;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
&.status {
|
||||||
|
height: 20rem;
|
||||||
|
}
|
||||||
|
&.more {
|
||||||
|
height: 23rem;
|
||||||
|
}
|
||||||
|
&.more.status {
|
||||||
|
height: 26rem;
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -302,10 +312,15 @@
|
||||||
min-width: -webkit-fill-available;
|
min-width: -webkit-fill-available;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
margin-left: .5rem;
|
margin-left: .5rem;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.button-row {
|
.button-row {
|
||||||
float: left;
|
float: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.filter-search {
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
.plant-status-bulk-update {
|
.plant-status-bulk-update {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -319,17 +334,35 @@
|
||||||
line-height: 4.1rem;
|
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 {
|
.panel-content {
|
||||||
padding-top: 15rem;
|
padding-top: 16rem;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
max-height: calc(100vh - 13rem);
|
max-height: calc(100vh - 13rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
.plant-search-item,
|
&.status {
|
||||||
.group-search-item { pointer-events: none; }
|
padding-top: 20rem;
|
||||||
|
}
|
||||||
|
&.more {
|
||||||
|
padding-top: 23rem;
|
||||||
|
}
|
||||||
|
&.more.status {
|
||||||
|
padding-top: 26rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,9 +398,13 @@
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.saucer {
|
.point-color-input {
|
||||||
margin: 1rem;
|
div[class*=col-] {
|
||||||
margin-left: 2rem;
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.saucer {
|
||||||
|
margin-top: 2.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.delete-row {
|
.delete-row {
|
||||||
margin: 1.5rem;
|
margin: 1.5rem;
|
||||||
|
@ -377,16 +414,39 @@
|
||||||
|
|
||||||
.weed-info-panel-content,
|
.weed-info-panel-content,
|
||||||
.point-info-panel-content {
|
.point-info-panel-content {
|
||||||
.saucer {
|
.point-color-input {
|
||||||
margin: 1rem;
|
div[class*=col-] {
|
||||||
margin-left: 2rem;
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.saucer {
|
||||||
|
margin-top: 4.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fb-button & .red {
|
.fb-button & .red {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
font-size: 1.4rem;
|
||||||
p {
|
p {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.weed-removal-method-section {
|
||||||
|
.weed-removal-method {
|
||||||
|
display: flex;
|
||||||
|
input {
|
||||||
|
margin: 0;
|
||||||
|
width: 10%;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
margin: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,6 +571,7 @@
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
p {
|
p {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
|
@ -557,22 +618,8 @@
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
.tool-search-item,
|
.tool-search-item,
|
||||||
.tool-slot-search-item {
|
.tool-slot-search-item {
|
||||||
line-height: 4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: -15px;
|
margin-left: -15px;
|
||||||
margin-right: -15px;
|
margin-right: -15px;
|
||||||
.row {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
line-height: 4rem;
|
|
||||||
&.tool-status,
|
|
||||||
&.tool-slot-position {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter-search {
|
.filter-search {
|
||||||
.bp3-button {
|
.bp3-button {
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
|
@ -585,13 +632,6 @@
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.tool-slot-position-info {
|
|
||||||
padding: 0;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.mounted-tool-header {
|
.mounted-tool-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -650,6 +690,7 @@
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
&.red {
|
&.red {
|
||||||
float: left;
|
float: left;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
|
@ -659,6 +700,19 @@
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
.edit-tool,
|
||||||
|
.add-new-tool {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
.name-error {
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: $dark_red;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
.add-stock-tools {
|
.add-stock-tools {
|
||||||
.filter-search {
|
.filter-search {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
@ -750,19 +804,101 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-pad {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-panel-content {
|
.settings-panel-content {
|
||||||
max-height: calc(100vh - 15rem);
|
padding: 0;
|
||||||
overflow-y: auto;
|
margin-top: 6rem;
|
||||||
overflow-x: hidden;
|
|
||||||
margin-top: 5rem;
|
|
||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
button {
|
.section {
|
||||||
margin-top: 1.75rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
p {
|
.bulk-expand-controls {
|
||||||
padding: 0.5rem;
|
|
||||||
margin-left: 1rem;
|
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;
|
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 {
|
.map-size-inputs {
|
||||||
.row {
|
.row {
|
||||||
|
@ -772,6 +908,31 @@
|
||||||
margin-top: 0.5rem;
|
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 {
|
.designer-setting {
|
||||||
&.disabled {
|
&.disabled {
|
||||||
input {
|
input {
|
||||||
|
@ -807,6 +968,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weed-item-icon,
|
||||||
|
.group-item-icon {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
.weed-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 13%;
|
||||||
|
left: 12%;
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.weeds-inventory-panel,
|
.weeds-inventory-panel,
|
||||||
.zones-inventory-panel,
|
.zones-inventory-panel,
|
||||||
.groups-panel {
|
.groups-panel {
|
||||||
|
@ -824,6 +998,11 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
|
.clear-day-criteria,
|
||||||
|
.clear-point-ids,
|
||||||
|
.clear-criteria {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
.group-member-display {
|
.group-member-display {
|
||||||
i[class*=fa-caret-] {
|
i[class*=fa-caret-] {
|
||||||
float: right;
|
float: right;
|
||||||
|
@ -850,20 +1029,47 @@
|
||||||
.criteria-heading {
|
.criteria-heading {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
.alpha-icon {
|
||||||
|
display: inline;
|
||||||
|
float: none !important;
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: $orange;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
&.category {
|
||||||
|
display: block;
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bp3-popover-wrapper {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
.fb-button {
|
.fb-button {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
.point-type-section,
|
||||||
|
.criteria-checkbox-list-item {
|
||||||
|
.fb-checkbox {
|
||||||
|
display: inline;
|
||||||
|
margin-right: 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
display: inline;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
width: 50%;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
.point-type-checkboxes {
|
.point-type-checkboxes {
|
||||||
.point-type-section {
|
.point-type-section {
|
||||||
.fb-checkbox {
|
|
||||||
display: inline;
|
|
||||||
margin-right: 1rem;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
display: inline;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.point-type-checkbox {
|
.point-type-checkbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
@ -882,19 +1088,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.plant-criteria-options,
|
.plant-criteria-options,
|
||||||
|
.weed-criteria-options,
|
||||||
.point-criteria-options,
|
.point-criteria-options,
|
||||||
.tool-criteria-options {
|
.tool-criteria-options {
|
||||||
margin-left: 3rem;
|
|
||||||
p {
|
|
||||||
&.category {
|
|
||||||
display: block;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
text-transform: none;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hr {
|
hr {
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -924,7 +1120,13 @@
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
.day-criteria {
|
.day-criteria {
|
||||||
p {
|
.criteria-checkbox-list-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
p {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.days-old-text {
|
||||||
display: inline;
|
display: inline;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
@ -932,6 +1134,7 @@
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.number-eq-criteria,
|
||||||
.string-eq-criteria {
|
.string-eq-criteria {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
.row {
|
.row {
|
||||||
|
@ -958,19 +1161,13 @@
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fb-toggle-button {
|
|
||||||
width: 85px;
|
|
||||||
margin-top: 0;
|
|
||||||
&.red {
|
|
||||||
background: $dark_gray !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.clear-criteria {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
.basic,
|
.basic,
|
||||||
.advanced {
|
.advanced {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
.filter-search {
|
||||||
|
height: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
.day-criteria {
|
.day-criteria {
|
||||||
.row {
|
.row {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
@ -982,6 +1179,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.advanced {
|
.advanced {
|
||||||
|
.bp3-popover-wrapper {
|
||||||
|
display: inline;
|
||||||
|
float: none;
|
||||||
|
margin-left: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.filter-search {
|
||||||
|
.bp3-popover-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
.row {
|
.row {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
@ -999,29 +1207,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.criteria-point-count-breakdown {
|
}
|
||||||
margin-bottom: 1rem;
|
}
|
||||||
.manual-group-member-count,
|
|
||||||
.criteria-group-member-count {
|
.group-member-count-breakdown {
|
||||||
margin-left: 2rem;
|
margin-bottom: 1rem;
|
||||||
div {
|
.manual-group-member-count,
|
||||||
display: inline;
|
.criteria-group-member-count {
|
||||||
padding: 0.25rem;
|
div {
|
||||||
font-size: 1.2rem;
|
display: inline;
|
||||||
border: 1px solid $panel_light_blue;
|
padding: 0.25rem;
|
||||||
}
|
font-size: 1.2rem;
|
||||||
p {
|
|
||||||
display: inline;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.criteria-group-member-count {
|
|
||||||
div {
|
|
||||||
border: 1px solid gray;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.criteria-options-menu {
|
||||||
|
label {
|
||||||
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1030,6 +1237,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
.row {
|
.row {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
margin-right: -2.5rem;
|
||||||
div[class*=col-] {
|
div[class*=col-] {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -1045,16 +1253,28 @@
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
margin-top: 2rem !important;
|
||||||
|
}
|
||||||
.edit-in-map {
|
.edit-in-map {
|
||||||
float: right;
|
float: right;
|
||||||
button {
|
button {
|
||||||
margin: 1rem !important;
|
margin: 1rem !important;
|
||||||
width: 5rem !important;
|
width: 5rem !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
margin-top: 1.1rem !important;
|
margin-top: 1.1rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.location-selection-warning {
|
||||||
|
i,
|
||||||
|
p {
|
||||||
|
display: inline;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: $darkest_red;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.weeds-inventory-panel,
|
.weeds-inventory-panel,
|
||||||
|
|
|
@ -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 {
|
.farmware-name-manual-input {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -1424,6 +1468,11 @@ ul {
|
||||||
button {
|
button {
|
||||||
float: none !important;
|
float: none !important;
|
||||||
}
|
}
|
||||||
|
.bp3-popover-wrapper {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-alert {
|
.problem-alert {
|
||||||
|
|
|
@ -127,6 +127,16 @@ select {
|
||||||
background: $white;
|
background: $white;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: $white;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
&:checked:after {
|
&:checked:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
&.take-photo-step {
|
&.take-photo-step {
|
||||||
background: $brown;
|
background: $brown;
|
||||||
}
|
}
|
||||||
&.resource-update-step {
|
&.update-resource-step {
|
||||||
background: $brown;
|
background: $brown;
|
||||||
}
|
}
|
||||||
&.set-servo-angle-step {
|
&.set-servo-angle-step {
|
||||||
|
@ -136,6 +136,9 @@
|
||||||
&.reboot-step {
|
&.reboot-step {
|
||||||
background: $brown;
|
background: $brown;
|
||||||
}
|
}
|
||||||
|
&.shutdown-step {
|
||||||
|
background: $brown;
|
||||||
|
}
|
||||||
&.unknown-step {
|
&.unknown-step {
|
||||||
background: $gray;
|
background: $gray;
|
||||||
}
|
}
|
||||||
|
@ -226,7 +229,7 @@
|
||||||
&.take-photo-step a {
|
&.take-photo-step a {
|
||||||
color: $dark_brown;
|
color: $dark_brown;
|
||||||
}
|
}
|
||||||
&.resource-update-step {
|
&.update-resource-step {
|
||||||
background: $light_brown;
|
background: $light_brown;
|
||||||
}
|
}
|
||||||
&.set-servo-angle-step {
|
&.set-servo-angle-step {
|
||||||
|
@ -253,6 +256,9 @@
|
||||||
&.emergency-stop-step {
|
&.emergency-stop-step {
|
||||||
background: $light_red;
|
background: $light_red;
|
||||||
}
|
}
|
||||||
|
&.shutdown-step {
|
||||||
|
background: $light_brown;
|
||||||
|
}
|
||||||
&.reboot-step {
|
&.reboot-step {
|
||||||
background: $light_brown;
|
background: $light_brown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
jest.mock("../../redux/store", () => ({ store: jest.fn() }));
|
||||||
|
|
||||||
import { botReducer, initialState } from "../reducer";
|
import { botReducer, initialState } from "../reducer";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { ControlPanelState, BotState } from "../interfaces";
|
import { ControlPanelState, BotState } from "../interfaces";
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
jest.mock("../../actions", () => ({
|
jest.mock("../../actions", () => ({
|
||||||
toggleControlPanel: jest.fn(),
|
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";
|
import * as React from "react";
|
||||||
|
@ -9,7 +16,7 @@ import {
|
||||||
} from "../maybe_highlight";
|
} from "../maybe_highlight";
|
||||||
import { DeviceSetting } from "../../../constants";
|
import { DeviceSetting } from "../../../constants";
|
||||||
import { panelState } from "../../../__test_support__/control_panel_state";
|
import { panelState } from "../../../__test_support__/control_panel_state";
|
||||||
import { toggleControlPanel } from "../../actions";
|
import { toggleControlPanel, bulkToggleControlPanel } from "../../actions";
|
||||||
|
|
||||||
describe("<Highlight />", () => {
|
describe("<Highlight />", () => {
|
||||||
const fakeProps = (): HighlightProps => ({
|
const fakeProps = (): HighlightProps => ({
|
||||||
|
@ -25,6 +32,24 @@ describe("<Highlight />", () => {
|
||||||
wrapper.instance().componentDidMount();
|
wrapper.instance().componentDidMount();
|
||||||
expect(wrapper.state().className).toEqual("unhighlight");
|
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()", () => {
|
describe("maybeHighlight()", () => {
|
||||||
|
@ -78,4 +103,11 @@ describe("maybeOpenPanel()", () => {
|
||||||
maybeOpenPanel(panelState())(jest.fn());
|
maybeOpenPanel(panelState())(jest.fn());
|
||||||
expect(toggleControlPanel).not.toHaveBeenCalled();
|
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 {
|
import {
|
||||||
buildResourceIndex,
|
buildResourceIndex,
|
||||||
} from "../../../__test_support__/resource_index_builder";
|
} from "../../../__test_support__/resource_index_builder";
|
||||||
|
import { DeviceSetting } from "../../../constants";
|
||||||
|
|
||||||
describe("<PinGuardMCUInputGroup/>", () => {
|
describe("<PinGuardMCUInputGroup/>", () => {
|
||||||
const fakeProps = (): PinGuardMCUInputGroupProps => {
|
const fakeProps = (): PinGuardMCUInputGroupProps => {
|
||||||
return {
|
return {
|
||||||
label: "Pin Guard 1",
|
label: DeviceSetting.pinGuard1,
|
||||||
pinNumKey: "pin_guard_1_pin_nr",
|
pinNumKey: "pin_guard_1_pin_nr",
|
||||||
timeoutKey: "pin_guard_1_time_out",
|
timeoutKey: "pin_guard_1_time_out",
|
||||||
activeStateKey: "pin_guard_1_active_state",
|
activeStateKey: "pin_guard_1_active_state",
|
||||||
|
|
|
@ -13,11 +13,12 @@ import {
|
||||||
import { TaggedFirmwareConfig } from "farmbot";
|
import { TaggedFirmwareConfig } from "farmbot";
|
||||||
import { FBSelect } from "../../../ui";
|
import { FBSelect } from "../../../ui";
|
||||||
import { updateMCU } from "../../actions";
|
import { updateMCU } from "../../actions";
|
||||||
|
import { DeviceSetting } from "../../../constants";
|
||||||
|
|
||||||
describe("<PinNumberDropdown />", () => {
|
describe("<PinNumberDropdown />", () => {
|
||||||
const fakeProps =
|
const fakeProps =
|
||||||
(firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({
|
(firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({
|
||||||
label: "Pin Guard 1",
|
label: DeviceSetting.pinGuard1,
|
||||||
pinNumKey: "pin_guard_1_pin_nr",
|
pinNumKey: "pin_guard_1_pin_nr",
|
||||||
timeoutKey: "pin_guard_1_time_out",
|
timeoutKey: "pin_guard_1_time_out",
|
||||||
activeStateKey: "pin_guard_1_active_state",
|
activeStateKey: "pin_guard_1_active_state",
|
||||||
|
|
|
@ -43,16 +43,6 @@ describe("<BoardType/>", () => {
|
||||||
expect(wrapper.text()).toContain("Farmduino");
|
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", () => {
|
it("calls updateConfig", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const wrapper = mount<BoardType>(<BoardType {...p} />);
|
const wrapper = mount<BoardType>(<BoardType {...p} />);
|
||||||
|
|
|
@ -11,12 +11,15 @@ import { OsUpdateButton } from "../os_update_button";
|
||||||
import { OsUpdateButtonProps } from "../interfaces";
|
import { OsUpdateButtonProps } from "../interfaces";
|
||||||
import { ShouldDisplay } from "../../../interfaces";
|
import { ShouldDisplay } from "../../../interfaces";
|
||||||
import { Content } from "../../../../constants";
|
import { Content } from "../../../../constants";
|
||||||
|
import { ConfigurationName } from "farmbot";
|
||||||
|
|
||||||
|
const UPDATE_CHANNEL = "update_channel" as ConfigurationName;
|
||||||
|
|
||||||
describe("<OsUpdateButton/>", () => {
|
describe("<OsUpdateButton/>", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
bot.currentOSVersion = "6.1.6";
|
bot.currentOSVersion = "6.1.6";
|
||||||
bot.hardware.informational_settings.controller_version = "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 => ({
|
const fakeProps = (): OsUpdateButtonProps => ({
|
||||||
|
@ -33,7 +36,6 @@ describe("<OsUpdateButton/>", () => {
|
||||||
availableVersion: string | undefined;
|
availableVersion: string | undefined;
|
||||||
availableBetaVersion: string | undefined;
|
availableBetaVersion: string | undefined;
|
||||||
availableBetaCommit: string | undefined;
|
availableBetaCommit: string | undefined;
|
||||||
betaOptIn: boolean | undefined;
|
|
||||||
onBeta: boolean | undefined;
|
onBeta: boolean | undefined;
|
||||||
update_available?: boolean | undefined;
|
update_available?: boolean | undefined;
|
||||||
shouldDisplay: ShouldDisplay;
|
shouldDisplay: ShouldDisplay;
|
||||||
|
@ -46,7 +48,6 @@ describe("<OsUpdateButton/>", () => {
|
||||||
availableVersion: "6.1.6",
|
availableVersion: "6.1.6",
|
||||||
availableBetaVersion: undefined,
|
availableBetaVersion: undefined,
|
||||||
availableBetaCommit: undefined,
|
availableBetaCommit: undefined,
|
||||||
betaOptIn: false,
|
|
||||||
onBeta: false,
|
onBeta: false,
|
||||||
shouldDisplay: () => false,
|
shouldDisplay: () => false,
|
||||||
update_channel: "stable",
|
update_channel: "stable",
|
||||||
|
@ -104,7 +105,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
expected: Results) => {
|
expected: Results) => {
|
||||||
const {
|
const {
|
||||||
installedVersion, installedCommit, onBeta, update_available,
|
installedVersion, installedCommit, onBeta, update_available,
|
||||||
availableVersion, availableBetaVersion, availableBetaCommit, betaOptIn,
|
availableVersion, availableBetaVersion, availableBetaCommit,
|
||||||
shouldDisplay, update_channel,
|
shouldDisplay, update_channel,
|
||||||
} = testProps;
|
} = testProps;
|
||||||
bot.hardware.informational_settings.controller_version = installedVersion;
|
bot.hardware.informational_settings.controller_version = installedVersion;
|
||||||
|
@ -115,9 +116,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
bot.currentOSVersion = availableVersion;
|
bot.currentOSVersion = availableVersion;
|
||||||
bot.currentBetaOSVersion = availableBetaVersion;
|
bot.currentBetaOSVersion = availableBetaVersion;
|
||||||
bot.currentBetaOSCommit = availableBetaCommit;
|
bot.currentBetaOSCommit = availableBetaCommit;
|
||||||
bot.hardware.configuration.beta_opt_in = betaOptIn;
|
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = update_channel;
|
||||||
// tslint:disable-next-line:no-any
|
|
||||||
(bot.hardware.configuration as any).update_channel = update_channel;
|
|
||||||
|
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.shouldDisplay = shouldDisplay;
|
p.shouldDisplay = shouldDisplay;
|
||||||
|
@ -156,7 +155,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.6";
|
testProps.installedVersion = "6.1.6";
|
||||||
testProps.availableVersion = undefined;
|
testProps.availableVersion = undefined;
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
const expectedResults = cantConnect("release server");
|
const expectedResults = cantConnect("release server");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
});
|
});
|
||||||
|
@ -166,7 +165,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
testProps.installedVersion = "6.1.6";
|
testProps.installedVersion = "6.1.6";
|
||||||
testProps.availableVersion = undefined;
|
testProps.availableVersion = undefined;
|
||||||
testProps.availableBetaVersion = "6.1.7-beta";
|
testProps.availableBetaVersion = "6.1.7-beta";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
const expectedResults = updateNeeded("6.1.7-beta");
|
const expectedResults = updateNeeded("6.1.7-beta");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
});
|
});
|
||||||
|
@ -175,7 +174,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.6";
|
testProps.installedVersion = "6.1.6";
|
||||||
testProps.availableBetaVersion = undefined;
|
testProps.availableBetaVersion = undefined;
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
const expectedResults = upToDate("6.1.6");
|
const expectedResults = upToDate("6.1.6");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
});
|
});
|
||||||
|
@ -205,7 +204,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.5";
|
testProps.installedVersion = "6.1.5";
|
||||||
testProps.availableBetaVersion = "7.0.0-beta";
|
testProps.availableBetaVersion = "7.0.0-beta";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
const expectedResults = updateNeeded("7.0.0-beta");
|
const expectedResults = updateNeeded("7.0.0-beta");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
});
|
});
|
||||||
|
@ -214,7 +213,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.6";
|
testProps.installedVersion = "6.1.6";
|
||||||
testProps.availableBetaVersion = "6.1.6-beta";
|
testProps.availableBetaVersion = "6.1.6-beta";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
const expectedResults = upToDate("6.1.6");
|
const expectedResults = upToDate("6.1.6");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
});
|
});
|
||||||
|
@ -223,7 +222,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.6";
|
testProps.installedVersion = "6.1.6";
|
||||||
testProps.availableBetaVersion = "6.1.6-beta";
|
testProps.availableBetaVersion = "6.1.6-beta";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
testProps.onBeta = true;
|
testProps.onBeta = true;
|
||||||
const expectedResults = updateNeeded("6.1.6");
|
const expectedResults = updateNeeded("6.1.6");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
|
@ -233,7 +232,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.6";
|
testProps.installedVersion = "6.1.6";
|
||||||
testProps.availableBetaVersion = "6.1.6-beta";
|
testProps.availableBetaVersion = "6.1.6-beta";
|
||||||
testProps.betaOptIn = false;
|
testProps.update_channel = "stable";
|
||||||
testProps.onBeta = true;
|
testProps.onBeta = true;
|
||||||
const expectedResults = updateNeeded("6.1.6");
|
const expectedResults = updateNeeded("6.1.6");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
|
@ -243,7 +242,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.7";
|
testProps.installedVersion = "6.1.7";
|
||||||
testProps.availableBetaVersion = "6.1.7-beta";
|
testProps.availableBetaVersion = "6.1.7-beta";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
testProps.onBeta = true;
|
testProps.onBeta = true;
|
||||||
const expectedResults = upToDate("6.1.7-beta");
|
const expectedResults = upToDate("6.1.7-beta");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
|
@ -253,7 +252,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.7-beta";
|
testProps.installedVersion = "6.1.7-beta";
|
||||||
testProps.availableBetaVersion = "6.1.7-beta";
|
testProps.availableBetaVersion = "6.1.7-beta";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
const expectedResults = upToDate("6.1.7-beta");
|
const expectedResults = upToDate("6.1.7-beta");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
});
|
});
|
||||||
|
@ -264,7 +263,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
testProps.installedCommit = "old commit";
|
testProps.installedCommit = "old commit";
|
||||||
testProps.availableBetaVersion = "7.0.0-beta";
|
testProps.availableBetaVersion = "7.0.0-beta";
|
||||||
testProps.availableBetaCommit = "new commit";
|
testProps.availableBetaCommit = "new commit";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
testProps.onBeta = true;
|
testProps.onBeta = true;
|
||||||
const expectedResults = updateNeeded("7.0.0-beta");
|
const expectedResults = updateNeeded("7.0.0-beta");
|
||||||
testButtonState(testProps, expectedResults);
|
testButtonState(testProps, expectedResults);
|
||||||
|
@ -273,7 +272,7 @@ describe("<OsUpdateButton/>", () => {
|
||||||
it("handles installed version newer than available (beta enabled)", () => {
|
it("handles installed version newer than available (beta enabled)", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.installedVersion = "6.1.7";
|
testProps.installedVersion = "6.1.7";
|
||||||
testProps.betaOptIn = true;
|
testProps.update_channel = "beta";
|
||||||
testProps.onBeta = false;
|
testProps.onBeta = false;
|
||||||
testProps.availableBetaVersion = "6.1.7-beta";
|
testProps.availableBetaVersion = "6.1.7-beta";
|
||||||
const expectedResults = upToDate("6.1.7-beta");
|
const expectedResults = upToDate("6.1.7-beta");
|
||||||
|
@ -308,16 +307,6 @@ describe("<OsUpdateButton/>", () => {
|
||||||
testButtonState(testProps, expectedResults);
|
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", () => {
|
it("compares release candidates: newer", () => {
|
||||||
const testProps = defaultTestProps();
|
const testProps = defaultTestProps();
|
||||||
testProps.availableVersion = "6.1.5";
|
testProps.availableVersion = "6.1.5";
|
||||||
|
|
|
@ -13,17 +13,7 @@ import { Highlight } from "../maybe_highlight";
|
||||||
import { DeviceSetting } from "../../../constants";
|
import { DeviceSetting } from "../../../constants";
|
||||||
import { DevSettings } from "../../../account/dev/dev_support";
|
import { DevSettings } from "../../../account/dev/dev_support";
|
||||||
|
|
||||||
interface BoardTypeState { sending: boolean }
|
export class BoardType extends React.Component<BoardTypeProps, {}> {
|
||||||
|
|
||||||
export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
|
||||||
state = {
|
|
||||||
sending: this.sending
|
|
||||||
};
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps() {
|
|
||||||
this.setState({ sending: this.sending });
|
|
||||||
}
|
|
||||||
|
|
||||||
get sending() {
|
get sending() {
|
||||||
return !this.props.sourceFbosConfig("firmware_hardware").consistent;
|
return !this.props.sourceFbosConfig("firmware_hardware").consistent;
|
||||||
}
|
}
|
||||||
|
@ -39,15 +29,14 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
||||||
if (selectedItem && isFwHardwareValue(firmware_hardware)) {
|
if (selectedItem && isFwHardwareValue(firmware_hardware)) {
|
||||||
info(t("Sending firmware configuration..."), t("Sending"));
|
info(t("Sending firmware configuration..."), t("Sending"));
|
||||||
this.props.dispatch(updateConfig({ firmware_hardware }));
|
this.props.dispatch(updateConfig({ firmware_hardware }));
|
||||||
this.setState({ sending: true });
|
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FirmwareSelection = () =>
|
FirmwareSelection = () =>
|
||||||
<FBSelect
|
<FBSelect
|
||||||
key={this.props.firmwareHardware}
|
key={this.props.firmwareHardware + "" + this.sending}
|
||||||
extraClass={this.state.sending ? "dim" : ""}
|
extraClass={this.sending ? "dim" : ""}
|
||||||
list={getFirmwareChoices()}
|
list={getFirmwareChoices()}
|
||||||
selectedItem={this.selectedBoard}
|
selectedItem={this.selectedBoard}
|
||||||
onChange={this.sendOffConfig} />
|
onChange={this.sendOffConfig} />
|
||||||
|
|
|
@ -263,7 +263,7 @@ export function FbosDetails(props: FbosDetailsProps) {
|
||||||
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled,
|
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled,
|
||||||
wifi_level_percent, cpu_usage, private_ip,
|
wifi_level_percent, cpu_usage, private_ip,
|
||||||
} = props.botInfoSettings;
|
} = 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 infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---";
|
||||||
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
|
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
|
||||||
|
|
||||||
|
@ -273,6 +273,7 @@ export function FbosDetails(props: FbosDetailsProps) {
|
||||||
botToMqttLastSeen={props.botToMqttLastSeen}
|
botToMqttLastSeen={props.botToMqttLastSeen}
|
||||||
timeSettings={props.timeSettings}
|
timeSettings={props.timeSettings}
|
||||||
device={props.deviceAccount} />
|
device={props.deviceAccount} />
|
||||||
|
<p><b>{t("Version last seen")}: </b>{fbos_version}</p>
|
||||||
<p><b>{t("Environment")}: </b>{env}</p>
|
<p><b>{t("Environment")}: </b>{env}</p>
|
||||||
<CommitDisplay title={t("Commit")}
|
<CommitDisplay title={t("Commit")}
|
||||||
repo={FarmBotRepo.FarmBotOS} commit={commit} />
|
repo={FarmBotRepo.FarmBotOS} commit={commit} />
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { FirmwareAlerts } from "../../../messages/alerts";
|
||||||
import { TimeSettings } from "../../../interfaces";
|
import { TimeSettings } from "../../../interfaces";
|
||||||
import { Alert } from "farmbot";
|
import { Alert } from "farmbot";
|
||||||
import { isFwHardwareValue, boardType } from "../firmware_hardware_support";
|
import { isFwHardwareValue, boardType } from "../firmware_hardware_support";
|
||||||
|
import { Help } from "../../../ui";
|
||||||
|
import { ToolTips } from "../../../constants";
|
||||||
|
|
||||||
export interface FirmwareHardwareStatusIconProps {
|
export interface FirmwareHardwareStatusIconProps {
|
||||||
firmwareHardware: string | undefined;
|
firmwareHardware: string | undefined;
|
||||||
|
@ -59,10 +61,13 @@ export const FirmwareHardwareStatusDetails =
|
||||||
(props: FirmwareHardwareStatusDetailsProps) => {
|
(props: FirmwareHardwareStatusDetailsProps) => {
|
||||||
return <div className="firmware-hardware-status-details">
|
return <div className="firmware-hardware-status-details">
|
||||||
<label>{t("Web App")}</label>
|
<label>{t("Web App")}</label>
|
||||||
|
<Help text={ToolTips.FIRMWARE_VALUE_API} />
|
||||||
<p>{lookup(props.apiFirmwareValue) || t("unknown")}</p>
|
<p>{lookup(props.apiFirmwareValue) || t("unknown")}</p>
|
||||||
<label>{t("FarmBot OS")}</label>
|
<label>{t("FarmBot OS")}</label>
|
||||||
|
<Help text={ToolTips.FIRMWARE_VALUE_FBOS} />
|
||||||
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
|
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
|
||||||
<label>{t("Arduino/Farmduino")}</label>
|
<label>{t("Arduino/Farmduino")}</label>
|
||||||
|
<Help text={ToolTips.FIRMWARE_VALUE_MCU} />
|
||||||
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
|
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
|
||||||
<FirmwareAlerts
|
<FirmwareAlerts
|
||||||
alerts={props.alerts}
|
alerts={props.alerts}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { SemverResult, semverCompare } from "../../../util";
|
||||||
import { OsUpdateButtonProps } from "./interfaces";
|
import { OsUpdateButtonProps } from "./interfaces";
|
||||||
import { checkControllerUpdates } from "../../actions";
|
import { checkControllerUpdates } from "../../actions";
|
||||||
import { isString } from "lodash";
|
import { isString } from "lodash";
|
||||||
import { BotState, Feature } from "../../interfaces";
|
import { BotState } from "../../interfaces";
|
||||||
import { Content } from "../../../constants";
|
import { Content } from "../../../constants";
|
||||||
import { t } from "../../../i18next_wrapper";
|
import { t } from "../../../i18next_wrapper";
|
||||||
|
|
||||||
|
@ -154,9 +154,8 @@ export const OsUpdateButton = (props: OsUpdateButtonProps) => {
|
||||||
const { controller_version } = bot.hardware.informational_settings;
|
const { controller_version } = bot.hardware.informational_settings;
|
||||||
|
|
||||||
/** FBOS beta release opt-in setting. */
|
/** FBOS beta release opt-in setting. */
|
||||||
const betaOptIn = props.shouldDisplay(Feature.use_update_channel)
|
const betaOptIn =
|
||||||
? sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable"
|
sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable";
|
||||||
: !!sourceFbosConfig("beta_opt_in").value;
|
|
||||||
/** FBOS update availability. */
|
/** FBOS update availability. */
|
||||||
const buttonStatusProps = buttonVersionStatus({ bot, betaOptIn });
|
const buttonStatusProps = buttonVersionStatus({ bot, betaOptIn });
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function DangerZone(props: DangerZoneProps) {
|
||||||
<Highlight settingName={DeviceSetting.resetHardwareParams}>
|
<Highlight settingName={DeviceSetting.resetHardwareParams}>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={newFormat ? 8 : 4}>
|
<Col xs={newFormat ? 8 : 4}>
|
||||||
<label>
|
<label style={{ lineHeight: "1.5rem" }}>
|
||||||
{t(DeviceSetting.resetHardwareParams)}
|
{t(DeviceSetting.resetHardwareParams)}
|
||||||
</label>
|
</label>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function PinGuard(props: PinGuardProps) {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>}
|
</Row>}
|
||||||
<PinGuardMCUInputGroup
|
<PinGuardMCUInputGroup
|
||||||
label={t("Pin Guard {{ num }}", { num: 1 })}
|
label={DeviceSetting.pinGuard1}
|
||||||
pinNumKey={"pin_guard_1_pin_nr"}
|
pinNumKey={"pin_guard_1_pin_nr"}
|
||||||
timeoutKey={"pin_guard_1_time_out"}
|
timeoutKey={"pin_guard_1_time_out"}
|
||||||
activeStateKey={"pin_guard_1_active_state"}
|
activeStateKey={"pin_guard_1_active_state"}
|
||||||
|
@ -52,7 +52,7 @@ export function PinGuard(props: PinGuardProps) {
|
||||||
resources={resources}
|
resources={resources}
|
||||||
sourceFwConfig={sourceFwConfig} />
|
sourceFwConfig={sourceFwConfig} />
|
||||||
<PinGuardMCUInputGroup
|
<PinGuardMCUInputGroup
|
||||||
label={t("Pin Guard {{ num }}", { num: 2 })}
|
label={DeviceSetting.pinGuard2}
|
||||||
pinNumKey={"pin_guard_2_pin_nr"}
|
pinNumKey={"pin_guard_2_pin_nr"}
|
||||||
timeoutKey={"pin_guard_2_time_out"}
|
timeoutKey={"pin_guard_2_time_out"}
|
||||||
activeStateKey={"pin_guard_2_active_state"}
|
activeStateKey={"pin_guard_2_active_state"}
|
||||||
|
@ -60,7 +60,7 @@ export function PinGuard(props: PinGuardProps) {
|
||||||
resources={resources}
|
resources={resources}
|
||||||
sourceFwConfig={sourceFwConfig} />
|
sourceFwConfig={sourceFwConfig} />
|
||||||
<PinGuardMCUInputGroup
|
<PinGuardMCUInputGroup
|
||||||
label={t("Pin Guard {{ num }}", { num: 3 })}
|
label={DeviceSetting.pinGuard3}
|
||||||
pinNumKey={"pin_guard_3_pin_nr"}
|
pinNumKey={"pin_guard_3_pin_nr"}
|
||||||
timeoutKey={"pin_guard_3_time_out"}
|
timeoutKey={"pin_guard_3_time_out"}
|
||||||
activeStateKey={"pin_guard_3_active_state"}
|
activeStateKey={"pin_guard_3_active_state"}
|
||||||
|
@ -68,7 +68,7 @@ export function PinGuard(props: PinGuardProps) {
|
||||||
resources={resources}
|
resources={resources}
|
||||||
sourceFwConfig={sourceFwConfig} />
|
sourceFwConfig={sourceFwConfig} />
|
||||||
<PinGuardMCUInputGroup
|
<PinGuardMCUInputGroup
|
||||||
label={t("Pin Guard {{ num }}", { num: 4 })}
|
label={DeviceSetting.pinGuard4}
|
||||||
pinNumKey={"pin_guard_4_pin_nr"}
|
pinNumKey={"pin_guard_4_pin_nr"}
|
||||||
timeoutKey={"pin_guard_4_time_out"}
|
timeoutKey={"pin_guard_4_time_out"}
|
||||||
activeStateKey={"pin_guard_4_active_state"}
|
activeStateKey={"pin_guard_4_active_state"}
|
||||||
|
@ -76,7 +76,7 @@ export function PinGuard(props: PinGuardProps) {
|
||||||
resources={resources}
|
resources={resources}
|
||||||
sourceFwConfig={sourceFwConfig} />
|
sourceFwConfig={sourceFwConfig} />
|
||||||
<PinGuardMCUInputGroup
|
<PinGuardMCUInputGroup
|
||||||
label={t("Pin Guard {{ num }}", { num: 5 })}
|
label={DeviceSetting.pinGuard5}
|
||||||
pinNumKey={"pin_guard_5_pin_nr"}
|
pinNumKey={"pin_guard_5_pin_nr"}
|
||||||
timeoutKey={"pin_guard_5_time_out"}
|
timeoutKey={"pin_guard_5_time_out"}
|
||||||
activeStateKey={"pin_guard_5_active_state"}
|
activeStateKey={"pin_guard_5_active_state"}
|
||||||
|
|
|
@ -2,28 +2,32 @@ import * as React from "react";
|
||||||
import { Row, Col } from "../../../ui/index";
|
import { Row, Col } from "../../../ui/index";
|
||||||
import { t } from "../../../i18next_wrapper";
|
import { t } from "../../../i18next_wrapper";
|
||||||
import { DevSettings } from "../../../account/dev/dev_support";
|
import { DevSettings } from "../../../account/dev/dev_support";
|
||||||
|
import { Highlight } from "../maybe_highlight";
|
||||||
|
import { DeviceSetting } from "../../../constants";
|
||||||
|
|
||||||
export function SpacePanelHeader() {
|
export function SpacePanelHeader() {
|
||||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||||
const width = newFormat ? 4 : 2;
|
const width = newFormat ? 4 : 2;
|
||||||
const offset = newFormat ? 0 : 6;
|
const offset = newFormat ? 0 : 6;
|
||||||
return <div className="label-headings">
|
return <div className="label-headings">
|
||||||
<Row>
|
<Highlight settingName={DeviceSetting.axisHeadingLabels}>
|
||||||
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
|
<Row>
|
||||||
<label>
|
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
|
||||||
{t("X AXIS")}
|
<label>
|
||||||
</label>
|
{t("X AXIS")}
|
||||||
</Col>
|
</label>
|
||||||
<Col xs={width} className={"centered-button-div"}>
|
</Col>
|
||||||
<label>
|
<Col xs={width} className={"centered-button-div"}>
|
||||||
{t("Y AXIS")}
|
<label>
|
||||||
</label>
|
{t("Y AXIS")}
|
||||||
</Col>
|
</label>
|
||||||
<Col xs={width} className={"centered-button-div"}>
|
</Col>
|
||||||
<label>
|
<Col xs={width} className={"centered-button-div"}>
|
||||||
{t("Z AXIS")}
|
<label>
|
||||||
</label>
|
{t("Z AXIS")}
|
||||||
</Col>
|
</label>
|
||||||
</Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Highlight>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ export interface NumericMCUInputGroupProps {
|
||||||
export interface PinGuardMCUInputGroupProps {
|
export interface PinGuardMCUInputGroupProps {
|
||||||
sourceFwConfig: SourceFwConfig;
|
sourceFwConfig: SourceFwConfig;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
label: string;
|
label: DeviceSetting;
|
||||||
pinNumKey: McuParamName;
|
pinNumKey: McuParamName;
|
||||||
timeoutKey: McuParamName;
|
timeoutKey: McuParamName;
|
||||||
activeStateKey: McuParamName;
|
activeStateKey: McuParamName;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { store } from "../../redux/store";
|
||||||
import { ControlPanelState } from "../interfaces";
|
import { ControlPanelState } from "../interfaces";
|
||||||
import { toggleControlPanel, bulkToggleControlPanel } from "../actions";
|
import { toggleControlPanel, bulkToggleControlPanel } from "../actions";
|
||||||
import { urlFriendly } from "../../util";
|
import { urlFriendly } from "../../util";
|
||||||
|
@ -56,6 +57,11 @@ const ERROR_HANDLING_PANEL = [
|
||||||
];
|
];
|
||||||
const PIN_GUARD_PANEL = [
|
const PIN_GUARD_PANEL = [
|
||||||
DeviceSetting.pinGuard,
|
DeviceSetting.pinGuard,
|
||||||
|
DeviceSetting.pinGuard1,
|
||||||
|
DeviceSetting.pinGuard2,
|
||||||
|
DeviceSetting.pinGuard3,
|
||||||
|
DeviceSetting.pinGuard4,
|
||||||
|
DeviceSetting.pinGuard5,
|
||||||
];
|
];
|
||||||
const DANGER_ZONE_PANEL = [
|
const DANGER_ZONE_PANEL = [
|
||||||
DeviceSetting.dangerZone,
|
DeviceSetting.dangerZone,
|
||||||
|
@ -63,6 +69,8 @@ const DANGER_ZONE_PANEL = [
|
||||||
];
|
];
|
||||||
const PIN_BINDINGS_PANEL = [
|
const PIN_BINDINGS_PANEL = [
|
||||||
DeviceSetting.pinBindings,
|
DeviceSetting.pinBindings,
|
||||||
|
DeviceSetting.savedPinBindings,
|
||||||
|
DeviceSetting.addNewPinBinding,
|
||||||
];
|
];
|
||||||
const POWER_AND_RESET_PANEL = [
|
const POWER_AND_RESET_PANEL = [
|
||||||
DeviceSetting.powerAndReset,
|
DeviceSetting.powerAndReset,
|
||||||
|
@ -183,6 +191,7 @@ export interface HighlightProps {
|
||||||
| (React.ReactChild | false)[]
|
| (React.ReactChild | false)[]
|
||||||
| (React.ReactChild | React.ReactChild[])[];
|
| (React.ReactChild | React.ReactChild[])[];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
searchTerm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HighlightState {
|
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() {
|
render() {
|
||||||
|
const show = !this.searchTerm ||
|
||||||
|
this.props.settingName.toLowerCase().includes(this.searchTerm);
|
||||||
return <div className={[
|
return <div className={[
|
||||||
this.props.className,
|
this.props.className,
|
||||||
this.state.className,
|
this.state.className,
|
||||||
].join(" ")}>
|
].join(" ")}
|
||||||
|
hidden={!show}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { PinNumberDropdown } from "./pin_number_dropdown";
|
||||||
import { DevSettings } from "../../account/dev/dev_support";
|
import { DevSettings } from "../../account/dev/dev_support";
|
||||||
import { ToolTips } from "../../constants";
|
import { ToolTips } from "../../constants";
|
||||||
import { Position } from "@blueprintjs/core";
|
import { Position } from "@blueprintjs/core";
|
||||||
|
import { Highlight } from "./maybe_highlight";
|
||||||
|
|
||||||
export class PinGuardMCUInputGroup
|
export class PinGuardMCUInputGroup
|
||||||
extends React.Component<PinGuardMCUInputGroupProps> {
|
extends React.Component<PinGuardMCUInputGroupProps> {
|
||||||
|
@ -50,7 +51,7 @@ export class PinGuardMCUInputGroup
|
||||||
? <Row>
|
? <Row>
|
||||||
<Col xs={3}>
|
<Col xs={3}>
|
||||||
<label>
|
<label>
|
||||||
{label}
|
{t(label)}
|
||||||
</label>
|
</label>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={3}>
|
<Col xs={3}>
|
||||||
|
@ -63,46 +64,48 @@ export class PinGuardMCUInputGroup
|
||||||
<this.State />
|
<this.State />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
: <div className={"pin-guard-input-row"}>
|
: <Highlight settingName={label}>
|
||||||
<Row>
|
<div className={"pin-guard-input-row"}>
|
||||||
<Col xs={12}>
|
<Row>
|
||||||
<label>
|
<Col xs={12}>
|
||||||
{label}
|
<label>
|
||||||
</label>
|
{t(label)}
|
||||||
</Col>
|
</label>
|
||||||
</Row>
|
</Col>
|
||||||
<Row>
|
</Row>
|
||||||
<Col xs={5} xsOffset={1} className="no-pad">
|
<Row>
|
||||||
<label>
|
<Col xs={5} xsOffset={1} className="no-pad">
|
||||||
{t("Pin Number")}
|
<label>
|
||||||
</label>
|
{t("Pin Number")}
|
||||||
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
|
</label>
|
||||||
position={Position.TOP_RIGHT} />
|
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
|
||||||
</Col>
|
position={Position.TOP_RIGHT} />
|
||||||
<Col xs={5} className="no-pad">
|
</Col>
|
||||||
<this.Number />
|
<Col xs={5} className="no-pad">
|
||||||
</Col>
|
<this.Number />
|
||||||
</Row>
|
</Col>
|
||||||
<Row>
|
</Row>
|
||||||
<Col xs={5} xsOffset={1} className="no-pad">
|
<Row>
|
||||||
<label>
|
<Col xs={5} xsOffset={1} className="no-pad">
|
||||||
{t("Timeout (sec)")}
|
<label>
|
||||||
</label>
|
{t("Timeout (sec)")}
|
||||||
</Col>
|
</label>
|
||||||
<Col xs={5} className="no-pad">
|
</Col>
|
||||||
<this.Timeout />
|
<Col xs={5} className="no-pad">
|
||||||
</Col>
|
<this.Timeout />
|
||||||
</Row>
|
</Col>
|
||||||
<Row>
|
</Row>
|
||||||
<Col xs={5} xsOffset={1} className="no-pad">
|
<Row>
|
||||||
<label>
|
<Col xs={5} xsOffset={1} className="no-pad">
|
||||||
{t("To State")}
|
<label>
|
||||||
</label>
|
{t("To State")}
|
||||||
</Col>
|
</label>
|
||||||
<Col xs={5} className="no-pad">
|
</Col>
|
||||||
<this.State />
|
<Col xs={5} className="no-pad">
|
||||||
</Col>
|
<this.State />
|
||||||
</Row>
|
</Col>
|
||||||
</div>;
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Highlight>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,13 @@ export function Diagnosis(props: DiagnosisProps) {
|
||||||
<div className={"saucer-connector last " + diagnosisColor} />
|
<div className={"saucer-connector last " + diagnosisColor} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={10} className={"connectivity-diagnosis"}>
|
<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>
|
<p>
|
||||||
{diagnose(props)}
|
{diagnose(props)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -81,6 +81,7 @@ export enum Feature {
|
||||||
ota_update_hour = "ota_update_hour",
|
ota_update_hour = "ota_update_hour",
|
||||||
rpi_led_control = "rpi_led_control",
|
rpi_led_control = "rpi_led_control",
|
||||||
sensors = "sensors",
|
sensors = "sensors",
|
||||||
|
update_resource = "update_resource",
|
||||||
use_update_channel = "use_update_channel",
|
use_update_channel = "use_update_channel",
|
||||||
variables = "variables",
|
variables = "variables",
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ export const specialActionLabelLookup: { [x: string]: string } = {
|
||||||
|
|
||||||
export const specialActionList: DropDownItem[] =
|
export const specialActionList: DropDownItem[] =
|
||||||
Object.values(PinBindingSpecialAction)
|
Object.values(PinBindingSpecialAction)
|
||||||
.filter(action => action != PinBindingSpecialAction.dump_info)
|
|
||||||
.map((action: PinBindingSpecialAction) =>
|
.map((action: PinBindingSpecialAction) =>
|
||||||
({ label: specialActionLabelLookup[action], value: action }));
|
({ label: specialActionLabelLookup[action], value: action }));
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
} from "farmbot/dist/resources/api_resources";
|
} from "farmbot/dist/resources/api_resources";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { DevSettings } from "../../account/dev/dev_support";
|
import { DevSettings } from "../../account/dev/dev_support";
|
||||||
|
import { DeviceSetting } from "../../constants";
|
||||||
|
|
||||||
export class PinBindingInputGroup
|
export class PinBindingInputGroup
|
||||||
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
|
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
|
||||||
|
@ -129,7 +130,7 @@ export class PinBindingInputGroup
|
||||||
render() {
|
render() {
|
||||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||||
return <div className="pin-binding-input-rows">
|
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 && <this.Number />}
|
||||||
{newFormat && <Row>
|
{newFormat && <Row>
|
||||||
<Col xs={5}>
|
<Col xs={5}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Row, Col, Help } from "../../ui";
|
import { Row, Col, Help } from "../../ui";
|
||||||
import { ToolTips } from "../../constants";
|
import { ToolTips, DeviceSetting } from "../../constants";
|
||||||
import { selectAllPinBindings } from "../../resources/selectors";
|
import { selectAllPinBindings } from "../../resources/selectors";
|
||||||
import { PinBindingsContentProps, PinBindingListItems } from "./interfaces";
|
import { PinBindingsContentProps, PinBindingListItems } from "./interfaces";
|
||||||
import { PinBindingsList } from "./pin_bindings_list";
|
import { PinBindingsList } from "./pin_bindings_list";
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
} from "farmbot/dist/resources/api_resources";
|
} from "farmbot/dist/resources/api_resources";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
import { DevSettings } from "../../account/dev/dev_support";
|
import { DevSettings } from "../../account/dev/dev_support";
|
||||||
|
import { Highlight } from "../components/maybe_highlight";
|
||||||
|
|
||||||
/** Width of UI columns in Pin Bindings widget. */
|
/** Width of UI columns in Pin Bindings widget. */
|
||||||
export enum PinBindingColWidth {
|
export enum PinBindingColWidth {
|
||||||
|
@ -73,32 +74,38 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
|
||||||
const pinBindings = apiPinBindings(resources);
|
const pinBindings = apiPinBindings(resources);
|
||||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||||
return <div className="pin-bindings">
|
return <div className="pin-bindings">
|
||||||
<Row>
|
<Highlight settingName={DeviceSetting.pinBindings}>
|
||||||
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
|
<Row>
|
||||||
position={Position.TOP_RIGHT} />}
|
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
|
||||||
<StockPinBindingsButton
|
position={Position.TOP_RIGHT} />}
|
||||||
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
<StockPinBindingsButton
|
||||||
<Popover
|
dispatch={dispatch} firmwareHardware={firmwareHardware} />
|
||||||
position={Position.TOP_RIGHT}
|
<Popover
|
||||||
interactionKind={PopoverInteractionKind.HOVER}
|
position={Position.TOP_RIGHT}
|
||||||
portalClassName={"bindings-warning-icon"}
|
interactionKind={PopoverInteractionKind.HOVER}
|
||||||
popoverClassName={"help"}>
|
portalClassName={"bindings-warning-icon"}
|
||||||
<i className="fa fa-exclamation-triangle" />
|
popoverClassName={"help"}>
|
||||||
<div className={"pin-binding-warning"}>
|
<i className="fa fa-exclamation-triangle" />
|
||||||
{t(ToolTips.PIN_BINDING_WARNING)}
|
<div className={"pin-binding-warning"}>
|
||||||
</div>
|
{t(ToolTips.PIN_BINDING_WARNING)}
|
||||||
</Popover>
|
</div>
|
||||||
</Row>
|
</Popover>
|
||||||
|
</Row>
|
||||||
|
</Highlight>
|
||||||
<div className={"pin-bindings-list-and-input"}>
|
<div className={"pin-bindings-list-and-input"}>
|
||||||
{!newFormat && <PinBindingsListHeader />}
|
{!newFormat && <PinBindingsListHeader />}
|
||||||
<PinBindingsList
|
<Highlight settingName={DeviceSetting.savedPinBindings}>
|
||||||
pinBindings={pinBindings}
|
<PinBindingsList
|
||||||
dispatch={dispatch}
|
pinBindings={pinBindings}
|
||||||
resources={resources} />
|
dispatch={dispatch}
|
||||||
<PinBindingInputGroup
|
resources={resources} />
|
||||||
pinBindings={pinBindings}
|
</Highlight>
|
||||||
dispatch={dispatch}
|
<Highlight settingName={DeviceSetting.addNewPinBinding}>
|
||||||
resources={resources} />
|
<PinBindingInputGroup
|
||||||
|
pinBindings={pinBindings}
|
||||||
|
dispatch={dispatch}
|
||||||
|
resources={resources} />
|
||||||
|
</Highlight>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { DevSettings } from "../../account/dev/dev_support";
|
||||||
import {
|
import {
|
||||||
PinBindingType, PinBindingSpecialAction,
|
PinBindingType, PinBindingSpecialAction,
|
||||||
} from "farmbot/dist/resources/api_resources";
|
} from "farmbot/dist/resources/api_resources";
|
||||||
|
import { DeviceSetting } from "../../constants";
|
||||||
|
|
||||||
export const PinBindingsList = (props: PinBindingsListProps) => {
|
export const PinBindingsList = (props: PinBindingsListProps) => {
|
||||||
const { pinBindings, resources, dispatch } = props;
|
const { pinBindings, resources, dispatch } = props;
|
||||||
|
@ -41,7 +42,7 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
|
||||||
|
|
||||||
const newFormat = DevSettings.futureFeaturesEnabled();
|
const newFormat = DevSettings.futureFeaturesEnabled();
|
||||||
return <div className={"bindings-list"}>
|
return <div className={"bindings-list"}>
|
||||||
{newFormat && <Row><label>{t("saved pin bindings")}</label></Row>}
|
{newFormat && <Row><label>{t(DeviceSetting.savedPinBindings)}</label></Row>}
|
||||||
{pinBindings
|
{pinBindings
|
||||||
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
|
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
|
||||||
.map(x => {
|
.map(x => {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import * as React from "react";
|
||||||
import { RawFarmDesigner as FarmDesigner } from "../index";
|
import { RawFarmDesigner as FarmDesigner } from "../index";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { Props } from "../interfaces";
|
import { Props } from "../interfaces";
|
||||||
import { GardenMapLegendProps } from "../map/interfaces";
|
|
||||||
import { bot } from "../../__test_support__/fake_state/bot";
|
import { bot } from "../../__test_support__/fake_state/bot";
|
||||||
import {
|
import {
|
||||||
fakeImage, fakeWebAppConfig,
|
fakeImage, fakeWebAppConfig,
|
||||||
|
@ -28,6 +27,8 @@ import {
|
||||||
import { fakeState } from "../../__test_support__/fake_state";
|
import { fakeState } from "../../__test_support__/fake_state";
|
||||||
import { edit } from "../../api/crud";
|
import { edit } from "../../api/crud";
|
||||||
import { BooleanSetting } from "../../session_keys";
|
import { BooleanSetting } from "../../session_keys";
|
||||||
|
import { GardenMapLegend } from "../map/legend/garden_map_legend";
|
||||||
|
import { GardenMap } from "../map/garden_map";
|
||||||
|
|
||||||
describe("<FarmDesigner/>", () => {
|
describe("<FarmDesigner/>", () => {
|
||||||
const fakeProps = (): Props => ({
|
const fakeProps = (): Props => ({
|
||||||
|
@ -36,6 +37,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
designer: fakeDesignerState(),
|
designer: fakeDesignerState(),
|
||||||
hoveredPlant: undefined,
|
hoveredPlant: undefined,
|
||||||
genericPoints: [],
|
genericPoints: [],
|
||||||
|
weeds: [],
|
||||||
allPoints: [],
|
allPoints: [],
|
||||||
plants: [],
|
plants: [],
|
||||||
toolSlots: [],
|
toolSlots: [],
|
||||||
|
@ -46,7 +48,10 @@ describe("<FarmDesigner/>", () => {
|
||||||
raw_encoders: { x: undefined, y: undefined, z: undefined },
|
raw_encoders: { x: undefined, y: undefined, z: undefined },
|
||||||
},
|
},
|
||||||
botMcuParams: bot.hardware.mcu_params,
|
botMcuParams: bot.hardware.mcu_params,
|
||||||
stepsPerMmXY: { x: undefined, y: undefined },
|
botSize: {
|
||||||
|
x: { value: 3000, isDefault: true },
|
||||||
|
y: { value: 1500, isDefault: true },
|
||||||
|
},
|
||||||
peripherals: [],
|
peripherals: [],
|
||||||
eStopStatus: false,
|
eStopStatus: false,
|
||||||
latestImages: [],
|
latestImages: [],
|
||||||
|
@ -67,8 +72,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
|
|
||||||
it("loads default map settings", () => {
|
it("loads default map settings", () => {
|
||||||
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
|
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
|
||||||
const legendProps =
|
const legendProps = wrapper.find(GardenMapLegend).props();
|
||||||
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
|
|
||||||
expect(legendProps.legendMenuOpen).toBeFalsy();
|
expect(legendProps.legendMenuOpen).toBeFalsy();
|
||||||
expect(legendProps.showPlants).toBeTruthy();
|
expect(legendProps.showPlants).toBeTruthy();
|
||||||
expect(legendProps.showPoints).toBeTruthy();
|
expect(legendProps.showPoints).toBeTruthy();
|
||||||
|
@ -76,8 +80,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
expect(legendProps.showFarmbot).toBeTruthy();
|
expect(legendProps.showFarmbot).toBeTruthy();
|
||||||
expect(legendProps.showImages).toBeFalsy();
|
expect(legendProps.showImages).toBeFalsy();
|
||||||
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
|
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
|
||||||
// tslint:disable-next-line:no-any
|
const gardenMapProps = wrapper.find(GardenMap).props();
|
||||||
const gardenMapProps = wrapper.find("GardenMap").props() as any;
|
|
||||||
expect(gardenMapProps.gridSize.x).toEqual(2900);
|
expect(gardenMapProps.gridSize.x).toEqual(2900);
|
||||||
expect(gardenMapProps.gridSize.y).toEqual(1400);
|
expect(gardenMapProps.gridSize.y).toEqual(1400);
|
||||||
});
|
});
|
||||||
|
@ -90,8 +93,7 @@ describe("<FarmDesigner/>", () => {
|
||||||
image2.body.created_at = "2001-01-01T00:00:00.000Z";
|
image2.body.created_at = "2001-01-01T00:00:00.000Z";
|
||||||
p.latestImages = [image1, image2];
|
p.latestImages = [image1, image2];
|
||||||
const wrapper = mount(<FarmDesigner {...p} />);
|
const wrapper = mount(<FarmDesigner {...p} />);
|
||||||
const legendProps =
|
const legendProps = wrapper.find(GardenMapLegend).props();
|
||||||
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
|
|
||||||
expect(legendProps.imageAgeInfo)
|
expect(legendProps.imageAgeInfo)
|
||||||
.toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 });
|
.toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 });
|
||||||
});
|
});
|
||||||
|
@ -137,4 +139,18 @@ describe("<FarmDesigner/>", () => {
|
||||||
bot_origin_quadrant: 2
|
bot_origin_quadrant: 2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("initializes setting", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.getConfigValue = () => false;
|
||||||
|
const i = new FarmDesigner(p);
|
||||||
|
expect(i.initializeSetting(BooleanSetting.show_farmbot, true)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets bot origin quadrant", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.getConfigValue = () => 1;
|
||||||
|
const i = new FarmDesigner(p);
|
||||||
|
expect(i.getBotOriginQuadrant()).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe("<MoveTo />", () => {
|
||||||
it("moves to location: bot's current z value", () => {
|
it("moves to location: bot's current z value", () => {
|
||||||
const wrapper = mount(<MoveTo {...fakeProps()} />);
|
const wrapper = mount(<MoveTo {...fakeProps()} />);
|
||||||
wrapper.find("button").simulate("click");
|
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", () => {
|
it("goes back", () => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { designer } from "../reducer";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { ReduxAction } from "../../redux/interfaces";
|
import { ReduxAction } from "../../redux/interfaces";
|
||||||
import {
|
import {
|
||||||
HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult,
|
HoveredPlantPayl, DrawnPointPayl, CropLiveSearchResult, DrawnWeedPayl,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import { BotPosition } from "../../devices/interfaces";
|
import { BotPosition } from "../../devices/interfaces";
|
||||||
import {
|
import {
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
} from "../../__test_support__/fake_crop_search_result";
|
} from "../../__test_support__/fake_crop_search_result";
|
||||||
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
||||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
|
import { PointType } from "farmbot";
|
||||||
|
|
||||||
describe("designer reducer", () => {
|
describe("designer reducer", () => {
|
||||||
const oldState = fakeDesignerState;
|
const oldState = fakeDesignerState;
|
||||||
|
@ -24,13 +25,22 @@ describe("designer reducer", () => {
|
||||||
expect(newState.cropSearchInProgress).toEqual(true);
|
expect(newState.cropSearchInProgress).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects plants", () => {
|
it("selects points", () => {
|
||||||
const action: ReduxAction<string[]> = {
|
const action: ReduxAction<string[]> = {
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: ["plantUuid"]
|
payload: ["pointUuid"]
|
||||||
};
|
};
|
||||||
const newState = designer(oldState(), action);
|
const newState = designer(oldState(), action);
|
||||||
expect(newState.selectedPlants).toEqual(["plantUuid"]);
|
expect(newState.selectedPoints).toEqual(["pointUuid"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets selection point type", () => {
|
||||||
|
const action: ReduxAction<PointType[] | undefined> = {
|
||||||
|
type: Actions.SET_SELECTION_POINT_TYPE,
|
||||||
|
payload: ["Plant"],
|
||||||
|
};
|
||||||
|
const newState = designer(oldState(), action);
|
||||||
|
expect(newState.selectionPointType).toEqual(["Plant"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets hovered plant", () => {
|
it("sets hovered plant", () => {
|
||||||
|
@ -84,25 +94,49 @@ describe("designer reducer", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets current point data", () => {
|
it("sets current point data", () => {
|
||||||
const action: ReduxAction<CurrentPointPayl> = {
|
const action: ReduxAction<DrawnPointPayl> = {
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: { cx: 10, cy: 20, r: 30, color: "red" }
|
payload: { cx: 10, cy: 20, r: 30, color: "red" }
|
||||||
};
|
};
|
||||||
const newState = designer(oldState(), action);
|
const newState = designer(oldState(), action);
|
||||||
expect(newState.currentPoint).toEqual({
|
expect(newState.drawnPoint).toEqual({
|
||||||
cx: 10, cy: 20, r: 30, color: "red"
|
cx: 10, cy: 20, r: 30, color: "red"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses current point color", () => {
|
it("uses current point color", () => {
|
||||||
const action: ReduxAction<CurrentPointPayl> = {
|
const action: ReduxAction<DrawnPointPayl> = {
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: { cx: 10, cy: 20, r: 30 }
|
payload: { cx: 10, cy: 20, r: 30 }
|
||||||
};
|
};
|
||||||
const state = oldState();
|
const state = oldState();
|
||||||
state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" };
|
state.drawnPoint = { cx: 0, cy: 0, r: 0, color: "red" };
|
||||||
const newState = designer(state, action);
|
const newState = designer(state, action);
|
||||||
expect(newState.currentPoint).toEqual({
|
expect(newState.drawnPoint).toEqual({
|
||||||
|
cx: 10, cy: 20, r: 30, color: "red"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets current weed data", () => {
|
||||||
|
const action: ReduxAction<DrawnWeedPayl> = {
|
||||||
|
type: Actions.SET_DRAWN_WEED_DATA,
|
||||||
|
payload: { cx: 10, cy: 20, r: 30, color: "red" }
|
||||||
|
};
|
||||||
|
const newState = designer(oldState(), action);
|
||||||
|
expect(newState.drawnWeed).toEqual({
|
||||||
|
cx: 10, cy: 20, r: 30, color: "red"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses current weed color", () => {
|
||||||
|
const action: ReduxAction<DrawnWeedPayl> = {
|
||||||
|
type: Actions.SET_DRAWN_WEED_DATA,
|
||||||
|
payload: { cx: 10, cy: 20, r: 30 }
|
||||||
|
};
|
||||||
|
const state = oldState();
|
||||||
|
state.drawnWeed = { cx: 0, cy: 0, r: 0, color: "red" };
|
||||||
|
const newState = designer(state, action);
|
||||||
|
expect(newState.drawnWeed).toEqual({
|
||||||
cx: 10, cy: 20, r: 30, color: "red"
|
cx: 10, cy: 20, r: 30, color: "red"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -156,4 +190,24 @@ describe("designer reducer", () => {
|
||||||
const newState = designer(state, action);
|
const newState = designer(state, action);
|
||||||
expect(newState.tryGroupSortType).toEqual("random");
|
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;
|
||||||
|
const action: ReduxAction<boolean> = {
|
||||||
|
type: Actions.EDIT_GROUP_AREA_IN_MAP, payload: true
|
||||||
|
};
|
||||||
|
const newState = designer(state, action);
|
||||||
|
expect(newState.editGroupAreaInMap).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { mapStateToProps, getPlants } from "../state_to_props";
|
import { mapStateToProps, getPlants, botSize } from "../state_to_props";
|
||||||
import { fakeState } from "../../__test_support__/fake_state";
|
import { fakeState } from "../../__test_support__/fake_state";
|
||||||
import {
|
import {
|
||||||
buildResourceIndex, fakeDevice,
|
buildResourceIndex, fakeDevice,
|
||||||
|
@ -11,14 +11,13 @@ import {
|
||||||
fakeWebAppConfig,
|
fakeWebAppConfig,
|
||||||
fakeFarmwareEnv,
|
fakeFarmwareEnv,
|
||||||
fakeSensorReading,
|
fakeSensorReading,
|
||||||
|
fakeFirmwareConfig,
|
||||||
} from "../../__test_support__/fake_state/resources";
|
} from "../../__test_support__/fake_state/resources";
|
||||||
import { WebAppConfig } from "farmbot/dist/resources/configs/web_app";
|
import { WebAppConfig } from "farmbot/dist/resources/configs/web_app";
|
||||||
import { generateUuid } from "../../resources/util";
|
import { generateUuid } from "../../resources/util";
|
||||||
import { DevSettings } from "../../account/dev/dev_support";
|
import { DevSettings } from "../../account/dev/dev_support";
|
||||||
|
|
||||||
describe("mapStateToProps()", () => {
|
describe("mapStateToProps()", () => {
|
||||||
const DISCARDED_AT = "2018-01-01T00:00:00.000Z";
|
|
||||||
|
|
||||||
it("hovered plantUUID is undefined", () => {
|
it("hovered plantUUID is undefined", () => {
|
||||||
const state = fakeState();
|
const state = fakeState();
|
||||||
state.resources.consumers.farm_designer.hoveredPlant = {
|
state.resources.consumers.farm_designer.hoveredPlant = {
|
||||||
|
@ -40,18 +39,11 @@ describe("mapStateToProps()", () => {
|
||||||
checkValue(2, true);
|
checkValue(2, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stepsPerMm is defined", () => {
|
|
||||||
const state = fakeState();
|
|
||||||
state.bot.hardware.mcu_params.movement_step_per_mm_x = 3;
|
|
||||||
state.bot.hardware.mcu_params.movement_step_per_mm_y = 4;
|
|
||||||
expect(mapStateToProps(state).stepsPerMmXY).toEqual({ x: 3, y: 4 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns selected plant", () => {
|
it("returns selected plant", () => {
|
||||||
const state = fakeState();
|
const state = fakeState();
|
||||||
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
|
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
|
||||||
const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0];
|
const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0];
|
||||||
state.resources.consumers.farm_designer.selectedPlants = [plantUuid];
|
state.resources.consumers.farm_designer.selectedPoints = [plantUuid];
|
||||||
expect(mapStateToProps(state).selectedPlant).toEqual(
|
expect(mapStateToProps(state).selectedPlant).toEqual(
|
||||||
expect.objectContaining({ uuid: plantUuid }));
|
expect.objectContaining({ uuid: plantUuid }));
|
||||||
});
|
});
|
||||||
|
@ -61,11 +53,8 @@ describe("mapStateToProps()", () => {
|
||||||
const webAppConfig = fakeWebAppConfig();
|
const webAppConfig = fakeWebAppConfig();
|
||||||
(webAppConfig.body as WebAppConfig).show_historic_points = true;
|
(webAppConfig.body as WebAppConfig).show_historic_points = true;
|
||||||
const point1 = fakePoint();
|
const point1 = fakePoint();
|
||||||
point1.body.discarded_at = undefined;
|
|
||||||
const point2 = fakePoint();
|
const point2 = fakePoint();
|
||||||
point2.body.discarded_at = DISCARDED_AT;
|
|
||||||
const point3 = fakePoint();
|
const point3 = fakePoint();
|
||||||
point3.body.discarded_at = DISCARDED_AT;
|
|
||||||
state.resources = buildResourceIndex([
|
state.resources = buildResourceIndex([
|
||||||
webAppConfig, point1, point2, point3, fakeDevice(),
|
webAppConfig, point1, point2, point3, fakeDevice(),
|
||||||
]);
|
]);
|
||||||
|
@ -77,15 +66,12 @@ describe("mapStateToProps()", () => {
|
||||||
const webAppConfig = fakeWebAppConfig();
|
const webAppConfig = fakeWebAppConfig();
|
||||||
(webAppConfig.body as WebAppConfig).show_historic_points = false;
|
(webAppConfig.body as WebAppConfig).show_historic_points = false;
|
||||||
const point1 = fakePoint();
|
const point1 = fakePoint();
|
||||||
point1.body.discarded_at = undefined;
|
|
||||||
const point2 = fakePoint();
|
const point2 = fakePoint();
|
||||||
point2.body.discarded_at = DISCARDED_AT;
|
|
||||||
const point3 = fakePoint();
|
const point3 = fakePoint();
|
||||||
point3.body.discarded_at = DISCARDED_AT;
|
|
||||||
state.resources = buildResourceIndex([
|
state.resources = buildResourceIndex([
|
||||||
webAppConfig, point1, point2, point3, fakeDevice(),
|
webAppConfig, point1, point2, point3, fakeDevice(),
|
||||||
]);
|
]);
|
||||||
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
|
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns sensor readings", () => {
|
it("returns sensor readings", () => {
|
||||||
|
@ -144,3 +130,45 @@ describe("getPlants()", () => {
|
||||||
expect.objectContaining({ rotation: "15" }));
|
expect.objectContaining({ rotation: "15" }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("botSize()", () => {
|
||||||
|
it("returns default bot size", () => {
|
||||||
|
const state = fakeState();
|
||||||
|
expect(botSize(state)).toEqual({
|
||||||
|
x: { value: 2900, isDefault: true },
|
||||||
|
y: { value: 1400, isDefault: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns map setting bot size", () => {
|
||||||
|
const state = fakeState();
|
||||||
|
const webAppConfig = fakeWebAppConfig();
|
||||||
|
webAppConfig.body.map_size_x = 1000;
|
||||||
|
webAppConfig.body.map_size_y = 1000;
|
||||||
|
state.resources = buildResourceIndex([fakeDevice(), webAppConfig]);
|
||||||
|
expect(botSize(state)).toEqual({
|
||||||
|
x: { value: 1000, isDefault: true },
|
||||||
|
y: { value: 1000, isDefault: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns axis length setting bot size", () => {
|
||||||
|
const state = fakeState();
|
||||||
|
const firmwareConfig = fakeFirmwareConfig();
|
||||||
|
firmwareConfig.body.movement_step_per_mm_x = 2;
|
||||||
|
firmwareConfig.body.movement_step_per_mm_y = 4;
|
||||||
|
firmwareConfig.body.movement_stop_at_max_x = 1;
|
||||||
|
firmwareConfig.body.movement_stop_at_max_y = 1;
|
||||||
|
firmwareConfig.body.movement_axis_nr_steps_x = 100;
|
||||||
|
firmwareConfig.body.movement_axis_nr_steps_y = 100;
|
||||||
|
const webAppConfig = fakeWebAppConfig();
|
||||||
|
webAppConfig.body.map_size_x = 1000;
|
||||||
|
webAppConfig.body.map_size_y = 1000;
|
||||||
|
state.resources = buildResourceIndex([
|
||||||
|
fakeDevice(), firmwareConfig, webAppConfig]);
|
||||||
|
expect(mapStateToProps(state).botSize).toEqual({
|
||||||
|
x: { value: 50, isDefault: false },
|
||||||
|
y: { value: 25, isDefault: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -81,21 +81,12 @@ interface DesignerPanelTopProps {
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
title?: string;
|
title?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
noIcon?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
||||||
const withBtn = !!props.linkTo || !!props.onClick;
|
const withBtn = !!props.linkTo || !!props.onClick;
|
||||||
return <div className={`panel-top ${withBtn ? "with-button" : ""}`}>
|
return <div className={`panel-top ${withBtn ? "with-button" : ""}`}>
|
||||||
<div className="thin-search-wrapper">
|
{props.children}
|
||||||
<div className="text-input-wrapper">
|
|
||||||
{!props.noIcon &&
|
|
||||||
<i className="fa fa-search" />}
|
|
||||||
<ErrorBoundary>
|
|
||||||
{props.children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{props.onClick &&
|
{props.onClick &&
|
||||||
<a>
|
<a>
|
||||||
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}
|
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}
|
||||||
|
|
|
@ -223,7 +223,7 @@ describe("<EditFEForm />", () => {
|
||||||
]}
|
]}
|
||||||
findExecutable={jest.fn(() => seq)}
|
findExecutable={jest.fn(() => seq)}
|
||||||
dispatch={jest.fn()}
|
dispatch={jest.fn()}
|
||||||
repeatOptions={repeatOptions}
|
repeatOptions={repeatOptions()}
|
||||||
timeSettings={fakeTimeSettings()}
|
timeSettings={fakeTimeSettings()}
|
||||||
autoSyncEnabled={false}
|
autoSyncEnabled={false}
|
||||||
resources={buildResourceIndex([]).index}
|
resources={buildResourceIndex([]).index}
|
||||||
|
@ -545,7 +545,10 @@ describe("<RepeatForm />", () => {
|
||||||
const fakeProps = (): RepeatFormProps => ({
|
const fakeProps = (): RepeatFormProps => ({
|
||||||
isRegimen: false,
|
isRegimen: false,
|
||||||
fieldGet: jest.fn(key =>
|
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(),
|
fieldSet: jest.fn(),
|
||||||
timeSettings: fakeTimeSettings(),
|
timeSettings: fakeTimeSettings(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,7 +26,7 @@ export interface FarmEventRepeatFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexKey: keyof DropDownItem = "value";
|
const indexKey: keyof DropDownItem = "value";
|
||||||
const OPTN_LOOKUP = keyBy(repeatOptions, indexKey);
|
const OPTN_LOOKUP = () => keyBy(repeatOptions(), indexKey);
|
||||||
|
|
||||||
export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
|
export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
|
||||||
const { disabled, fieldSet, repeat, endDate, endTime, timeUnit } = props;
|
const { disabled, fieldSet, repeat, endDate, endTime, timeUnit } = props;
|
||||||
|
@ -50,9 +50,9 @@ export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={8}>
|
<Col xs={8}>
|
||||||
<FBSelect
|
<FBSelect
|
||||||
list={repeatOptions}
|
list={repeatOptions()}
|
||||||
onChange={ddi => fieldSet("timeUnit", "" + ddi.value)}
|
onChange={ddi => fieldSet("timeUnit", "" + ddi.value)}
|
||||||
selectedItem={OPTN_LOOKUP[timeUnit] || OPTN_LOOKUP["daily"]} />
|
selectedItem={OPTN_LOOKUP()[timeUnit] || OPTN_LOOKUP()["daily"]} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<label>
|
<label>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from "../../ui/empty_state_wrapper";
|
} from "../../ui/empty_state_wrapper";
|
||||||
import { some, uniq, map, sortBy } from "lodash";
|
import { some, uniq, map, sortBy } from "lodash";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
|
import { SearchField } from "../../ui/search_field";
|
||||||
|
|
||||||
const filterSearch = (term: string) => (item: CalendarOccurrence) =>
|
const filterSearch = (term: string) => (item: CalendarOccurrence) =>
|
||||||
item.heading.toLowerCase().includes(term)
|
item.heading.toLowerCase().includes(term)
|
||||||
|
@ -105,14 +106,12 @@ export class PureFarmEvents
|
||||||
<DesignerPanelTop
|
<DesignerPanelTop
|
||||||
panel={Panel.FarmEvents}
|
panel={Panel.FarmEvents}
|
||||||
linkTo={"/app/designer/events/add"}
|
linkTo={"/app/designer/events/add"}
|
||||||
title={t("Add event")}
|
title={t("Add event")}>
|
||||||
noIcon={true}>
|
<SearchField searchTerm={this.state.searchTerm}
|
||||||
<i className="fa fa-calendar" onClick={this.resetCalendar} />
|
customLeftIcon={
|
||||||
<input
|
<i className="fa fa-calendar" onClick={this.resetCalendar} />}
|
||||||
name="searchTerm"
|
placeholder={t("Search your events...")}
|
||||||
value={this.state.searchTerm}
|
onChange={searchTerm => this.setState({ searchTerm })} />
|
||||||
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
|
|
||||||
placeholder={t("Search your events...")} />
|
|
||||||
</DesignerPanelTop>
|
</DesignerPanelTop>
|
||||||
<DesignerPanelContent panelName={"farm-event"}>
|
<DesignerPanelContent panelName={"farm-event"}>
|
||||||
<div className="farm-events">
|
<div className="farm-events">
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const formatDate = (input: string, timeSettings: TimeSettings) => {
|
||||||
return moment(iso).utcOffset(timeSettings.utcOffset).format("YYYY-MM-DD");
|
return moment(iso).utcOffset(timeSettings.utcOffset).format("YYYY-MM-DD");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const repeatOptions = [
|
export const repeatOptions = () => [
|
||||||
{ label: t("Minutes"), value: "minutely", name: "time_unit" },
|
{ label: t("Minutes"), value: "minutely", name: "time_unit" },
|
||||||
{ label: t("Hours"), value: "hourly", name: "time_unit" },
|
{ label: t("Hours"), value: "hourly", name: "time_unit" },
|
||||||
{ label: t("Days"), value: "daily", name: "time_unit" },
|
{ label: t("Days"), value: "daily", name: "time_unit" },
|
||||||
|
@ -147,7 +147,7 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
|
||||||
sequencesById,
|
sequencesById,
|
||||||
farmEventsById,
|
farmEventsById,
|
||||||
executableOptions: executableList,
|
executableOptions: executableList,
|
||||||
repeatOptions,
|
repeatOptions: repeatOptions(),
|
||||||
handleTime,
|
handleTime,
|
||||||
farmEvents,
|
farmEvents,
|
||||||
getFarmEvent,
|
getFarmEvent,
|
||||||
|
|
|
@ -11,8 +11,7 @@ import { NumericSetting, BooleanSetting } from "../session_keys";
|
||||||
import { isUndefined, last, isFinite } from "lodash";
|
import { isUndefined, last, isFinite } from "lodash";
|
||||||
import { AxisNumberProperty, BotSize } from "./map/interfaces";
|
import { AxisNumberProperty, BotSize } from "./map/interfaces";
|
||||||
import {
|
import {
|
||||||
getBotSize, round, getPanelStatus, MapPanelStatus, mapPanelClassName,
|
round, getPanelStatus, MapPanelStatus, mapPanelClassName, getMapPadding,
|
||||||
getMapPadding,
|
|
||||||
} from "./map/util";
|
} from "./map/util";
|
||||||
import {
|
import {
|
||||||
calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex,
|
calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex,
|
||||||
|
@ -70,6 +69,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
legend_menu_open: init(BooleanSetting.legend_menu_open, false),
|
legend_menu_open: init(BooleanSetting.legend_menu_open, false),
|
||||||
show_plants: init(BooleanSetting.show_plants, true),
|
show_plants: init(BooleanSetting.show_plants, true),
|
||||||
show_points: init(BooleanSetting.show_points, true),
|
show_points: init(BooleanSetting.show_points, true),
|
||||||
|
show_weeds: init(BooleanSetting.show_weeds, true),
|
||||||
show_spread: init(BooleanSetting.show_spread, false),
|
show_spread: init(BooleanSetting.show_spread, false),
|
||||||
show_farmbot: init(BooleanSetting.show_farmbot, true),
|
show_farmbot: init(BooleanSetting.show_farmbot, true),
|
||||||
show_images: init(BooleanSetting.show_images, false),
|
show_images: init(BooleanSetting.show_images, false),
|
||||||
|
@ -116,6 +116,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
legend_menu_open,
|
legend_menu_open,
|
||||||
show_plants,
|
show_plants,
|
||||||
show_points,
|
show_points,
|
||||||
|
show_weeds,
|
||||||
show_spread,
|
show_spread,
|
||||||
show_farmbot,
|
show_farmbot,
|
||||||
show_images,
|
show_images,
|
||||||
|
@ -124,11 +125,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
zoom_level
|
zoom_level
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const botSize = getBotSize(
|
|
||||||
this.props.botMcuParams,
|
|
||||||
this.props.stepsPerMmXY,
|
|
||||||
getDefaultAxisLength(this.props.getConfigValue));
|
|
||||||
|
|
||||||
const stopAtHome = {
|
const stopAtHome = {
|
||||||
x: !!this.props.botMcuParams.movement_stop_at_home_x,
|
x: !!this.props.botMcuParams.movement_stop_at_home_x,
|
||||||
y: !!this.props.botMcuParams.movement_stop_at_home_y
|
y: !!this.props.botMcuParams.movement_stop_at_home_y
|
||||||
|
@ -155,6 +151,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
legendMenuOpen={legend_menu_open}
|
legendMenuOpen={legend_menu_open}
|
||||||
showPlants={show_plants}
|
showPlants={show_plants}
|
||||||
showPoints={show_points}
|
showPoints={show_points}
|
||||||
|
showWeeds={show_weeds}
|
||||||
showSpread={show_spread}
|
showSpread={show_spread}
|
||||||
showFarmbot={show_farmbot}
|
showFarmbot={show_farmbot}
|
||||||
showImages={show_images}
|
showImages={show_images}
|
||||||
|
@ -164,6 +161,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
timeSettings={this.props.timeSettings}
|
timeSettings={this.props.timeSettings}
|
||||||
getConfigValue={this.props.getConfigValue}
|
getConfigValue={this.props.getConfigValue}
|
||||||
|
shouldDisplay={this.props.shouldDisplay}
|
||||||
imageAgeInfo={imageAgeInfo} />
|
imageAgeInfo={imageAgeInfo} />
|
||||||
|
|
||||||
<DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
|
<DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
|
||||||
|
@ -181,6 +179,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
<GardenMap
|
<GardenMap
|
||||||
showPoints={show_points}
|
showPoints={show_points}
|
||||||
showPlants={show_plants}
|
showPlants={show_plants}
|
||||||
|
showWeeds={show_weeds}
|
||||||
showSpread={show_spread}
|
showSpread={show_spread}
|
||||||
showFarmbot={show_farmbot}
|
showFarmbot={show_farmbot}
|
||||||
showImages={show_images}
|
showImages={show_images}
|
||||||
|
@ -192,15 +191,16 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
|
||||||
designer={this.props.designer}
|
designer={this.props.designer}
|
||||||
plants={this.props.plants}
|
plants={this.props.plants}
|
||||||
genericPoints={this.props.genericPoints}
|
genericPoints={this.props.genericPoints}
|
||||||
|
weeds={this.props.weeds}
|
||||||
allPoints={this.props.allPoints}
|
allPoints={this.props.allPoints}
|
||||||
toolSlots={this.props.toolSlots}
|
toolSlots={this.props.toolSlots}
|
||||||
botLocationData={this.props.botLocationData}
|
botLocationData={this.props.botLocationData}
|
||||||
botSize={botSize}
|
botSize={this.props.botSize}
|
||||||
stopAtHome={stopAtHome}
|
stopAtHome={stopAtHome}
|
||||||
hoveredPlant={this.props.hoveredPlant}
|
hoveredPlant={this.props.hoveredPlant}
|
||||||
zoomLvl={zoom_level}
|
zoomLvl={zoom_level}
|
||||||
botOriginQuadrant={this.getBotOriginQuadrant()}
|
botOriginQuadrant={this.getBotOriginQuadrant()}
|
||||||
gridSize={getGridSize(this.props.getConfigValue, botSize)}
|
gridSize={getGridSize(this.props.getConfigValue, this.props.botSize)}
|
||||||
gridOffset={gridOffset}
|
gridOffset={gridOffset}
|
||||||
peripherals={this.props.peripherals}
|
peripherals={this.props.peripherals}
|
||||||
eStopStatus={this.props.eStopStatus}
|
eStopStatus={this.props.eStopStatus}
|
||||||
|
|
|
@ -11,10 +11,12 @@ import {
|
||||||
TaggedSensor,
|
TaggedSensor,
|
||||||
TaggedPoint,
|
TaggedPoint,
|
||||||
TaggedPointGroup,
|
TaggedPointGroup,
|
||||||
|
TaggedWeedPointer,
|
||||||
|
PointType,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { SlotWithTool, ResourceIndex } from "../resources/interfaces";
|
import { SlotWithTool, ResourceIndex, UUID } from "../resources/interfaces";
|
||||||
import {
|
import {
|
||||||
BotPosition, StepsPerMmXY, BotLocationData, ShouldDisplay,
|
BotPosition, BotLocationData, ShouldDisplay,
|
||||||
} from "../devices/interfaces";
|
} from "../devices/interfaces";
|
||||||
import { isNumber } from "lodash";
|
import { isNumber } from "lodash";
|
||||||
import { McuParams, TaggedCrop } from "farmbot";
|
import { McuParams, TaggedCrop } from "farmbot";
|
||||||
|
@ -48,6 +50,7 @@ export interface State extends TypeCheckerHint {
|
||||||
legend_menu_open: boolean;
|
legend_menu_open: boolean;
|
||||||
show_plants: boolean;
|
show_plants: boolean;
|
||||||
show_points: boolean;
|
show_points: boolean;
|
||||||
|
show_weeds: boolean;
|
||||||
show_spread: boolean;
|
show_spread: boolean;
|
||||||
show_farmbot: boolean;
|
show_farmbot: boolean;
|
||||||
show_images: boolean;
|
show_images: boolean;
|
||||||
|
@ -63,13 +66,14 @@ export interface Props {
|
||||||
designer: DesignerState;
|
designer: DesignerState;
|
||||||
hoveredPlant: TaggedPlant | undefined;
|
hoveredPlant: TaggedPlant | undefined;
|
||||||
genericPoints: TaggedGenericPointer[];
|
genericPoints: TaggedGenericPointer[];
|
||||||
|
weeds: TaggedWeedPointer[];
|
||||||
allPoints: TaggedPoint[];
|
allPoints: TaggedPoint[];
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
toolSlots: SlotWithTool[];
|
toolSlots: SlotWithTool[];
|
||||||
crops: TaggedCrop[];
|
crops: TaggedCrop[];
|
||||||
botLocationData: BotLocationData;
|
botLocationData: BotLocationData;
|
||||||
botMcuParams: McuParams;
|
botMcuParams: McuParams;
|
||||||
stepsPerMmXY: StepsPerMmXY;
|
botSize: BotSize;
|
||||||
peripherals: { label: string, value: boolean }[];
|
peripherals: { label: string, value: boolean }[];
|
||||||
eStopStatus: boolean;
|
eStopStatus: boolean;
|
||||||
latestImages: TaggedImage[];
|
latestImages: TaggedImage[];
|
||||||
|
@ -106,7 +110,8 @@ export interface Crop {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DesignerState {
|
export interface DesignerState {
|
||||||
selectedPlants: string[] | undefined;
|
selectedPoints: UUID[] | undefined;
|
||||||
|
selectionPointType: PointType[] | undefined;
|
||||||
hoveredPlant: HoveredPlantPayl;
|
hoveredPlant: HoveredPlantPayl;
|
||||||
hoveredPoint: string | undefined;
|
hoveredPoint: string | undefined;
|
||||||
hoveredPlantListItem: string | undefined;
|
hoveredPlantListItem: string | undefined;
|
||||||
|
@ -115,10 +120,12 @@ export interface DesignerState {
|
||||||
cropSearchResults: CropLiveSearchResult[];
|
cropSearchResults: CropLiveSearchResult[];
|
||||||
cropSearchInProgress: boolean;
|
cropSearchInProgress: boolean;
|
||||||
chosenLocation: BotPosition;
|
chosenLocation: BotPosition;
|
||||||
currentPoint: CurrentPointPayl | undefined;
|
drawnPoint: DrawnPointPayl | undefined;
|
||||||
|
drawnWeed: DrawnWeedPayl | undefined;
|
||||||
openedSavedGarden: string | undefined;
|
openedSavedGarden: string | undefined;
|
||||||
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
||||||
editGroupAreaInMap: boolean;
|
editGroupAreaInMap: boolean;
|
||||||
|
settingsSearchTerm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
|
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
|
||||||
|
@ -181,6 +188,7 @@ export interface FarmEventState {
|
||||||
export interface GardenMapProps {
|
export interface GardenMapProps {
|
||||||
showPlants: boolean | undefined;
|
showPlants: boolean | undefined;
|
||||||
showPoints: boolean | undefined;
|
showPoints: boolean | undefined;
|
||||||
|
showWeeds: boolean | undefined;
|
||||||
showSpread: boolean | undefined;
|
showSpread: boolean | undefined;
|
||||||
showFarmbot: boolean | undefined;
|
showFarmbot: boolean | undefined;
|
||||||
showImages: boolean | undefined;
|
showImages: boolean | undefined;
|
||||||
|
@ -189,6 +197,7 @@ export interface GardenMapProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
designer: DesignerState;
|
designer: DesignerState;
|
||||||
genericPoints: TaggedGenericPointer[];
|
genericPoints: TaggedGenericPointer[];
|
||||||
|
weeds: TaggedWeedPointer[];
|
||||||
allPoints: TaggedPoint[];
|
allPoints: TaggedPoint[];
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
toolSlots: SlotWithTool[];
|
toolSlots: SlotWithTool[];
|
||||||
|
@ -279,7 +288,15 @@ export interface CameraCalibrationData {
|
||||||
calibrationZ: string | undefined;
|
calibrationZ: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentPointPayl {
|
export interface DrawnPointPayl {
|
||||||
|
name?: string;
|
||||||
|
cx: number;
|
||||||
|
cy: number;
|
||||||
|
r: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrawnWeedPayl {
|
||||||
name?: string;
|
name?: string;
|
||||||
cx: number;
|
cx: number;
|
||||||
cy: number;
|
cy: number;
|
||||||
|
|
|
@ -4,10 +4,9 @@ jest.mock("../../../history", () => ({
|
||||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../../../api/crud", () => ({
|
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
|
||||||
edit: jest.fn(),
|
|
||||||
overwrite: jest.fn(),
|
jest.mock("../../point_groups/actions", () => ({ overwriteGroup: jest.fn() }));
|
||||||
}));
|
|
||||||
|
|
||||||
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
|
||||||
const mockGroup = fakePointGroup();
|
const mockGroup = fakePointGroup();
|
||||||
|
@ -16,12 +15,13 @@ jest.mock("../../point_groups/group_detail", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPlant,
|
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPoint,
|
||||||
setHoveredPlant,
|
setHoveredPlant,
|
||||||
|
mapPointClickAction,
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { MovePlantProps } from "../../interfaces";
|
import { MovePlantProps } from "../../interfaces";
|
||||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||||
import { edit, overwrite } from "../../../api/crud";
|
import { edit } from "../../../api/crud";
|
||||||
import { Actions } from "../../../constants";
|
import { Actions } from "../../../constants";
|
||||||
import { DEFAULT_ICON, svgToUrl } from "../../../open_farm/icons";
|
import { DEFAULT_ICON, svgToUrl } from "../../../open_farm/icons";
|
||||||
import { history } from "../../../history";
|
import { history } from "../../../history";
|
||||||
|
@ -30,6 +30,8 @@ import { GetState } from "../../../redux/interfaces";
|
||||||
import {
|
import {
|
||||||
buildResourceIndex,
|
buildResourceIndex,
|
||||||
} from "../../../__test_support__/resource_index_builder";
|
} from "../../../__test_support__/resource_index_builder";
|
||||||
|
import { overwriteGroup } from "../../point_groups/actions";
|
||||||
|
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
|
||||||
|
|
||||||
describe("movePlant", () => {
|
describe("movePlant", () => {
|
||||||
it.each<[string, Record<"x" | "y", number>, Record<"x" | "y", number>]>([
|
it.each<[string, Record<"x" | "y", number>, Record<"x" | "y", number>]>([
|
||||||
|
@ -74,7 +76,7 @@ describe("closePlantInfo()", () => {
|
||||||
closePlantInfo(dispatch)();
|
closePlantInfo(dispatch)();
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
payload: undefined, type: Actions.SELECT_PLANT
|
payload: undefined, type: Actions.SELECT_POINT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,7 +86,7 @@ describe("closePlantInfo()", () => {
|
||||||
closePlantInfo(dispatch)();
|
closePlantInfo(dispatch)();
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
payload: undefined, type: Actions.SELECT_PLANT
|
payload: undefined, type: Actions.SELECT_POINT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -115,7 +117,7 @@ describe("clickMapPlant", () => {
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
|
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
|
||||||
expect(dispatch).toHaveBeenCalledWith(selectPlant(["fakeUuid"]));
|
expect(dispatch).toHaveBeenCalledWith(selectPoint(["fakeUuid"]));
|
||||||
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
|
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
@ -127,12 +129,25 @@ describe("clickMapPlant", () => {
|
||||||
const plant = fakePlant();
|
const plant = fakePlant();
|
||||||
plant.body.id = 23;
|
plant.body.id = 23;
|
||||||
state.resources = buildResourceIndex([plant]);
|
state.resources = buildResourceIndex([plant]);
|
||||||
const dispatch = jest.fn();
|
const dispatch = mockDispatch();
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
||||||
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
|
expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
|
||||||
name: "Fake", point_ids: [1, 23]
|
expect.objectContaining({
|
||||||
}));
|
name: "Fake", point_ids: [1, 23]
|
||||||
|
}));
|
||||||
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't add a point to current group", () => {
|
||||||
|
mockPath = "/app/designer/groups/1";
|
||||||
|
mockGroup.body.point_ids = [1];
|
||||||
|
const state = fakeState();
|
||||||
|
state.resources = buildResourceIndex([]);
|
||||||
|
const dispatch = mockDispatch();
|
||||||
|
const getState: GetState = jest.fn(() => state);
|
||||||
|
clickMapPlant("missing plant uuid", "fakeIcon")(dispatch, getState);
|
||||||
|
expect(overwriteGroup).not.toHaveBeenCalled();
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -143,12 +158,13 @@ describe("clickMapPlant", () => {
|
||||||
const plant = fakePlant();
|
const plant = fakePlant();
|
||||||
plant.body.id = 2;
|
plant.body.id = 2;
|
||||||
state.resources = buildResourceIndex([plant]);
|
state.resources = buildResourceIndex([plant]);
|
||||||
const dispatch = jest.fn();
|
const dispatch = mockDispatch();
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
||||||
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
|
expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
|
||||||
name: "Fake", point_ids: [1]
|
expect.objectContaining({
|
||||||
}));
|
name: "Fake", point_ids: [1]
|
||||||
|
}));
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -162,7 +178,7 @@ describe("clickMapPlant", () => {
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT, payload: [plant.uuid]
|
type: Actions.SELECT_POINT, payload: [plant.uuid]
|
||||||
});
|
});
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -173,13 +189,39 @@ describe("clickMapPlant", () => {
|
||||||
const plant = fakePlant();
|
const plant = fakePlant();
|
||||||
plant.uuid = "fakePlantUuid";
|
plant.uuid = "fakePlantUuid";
|
||||||
state.resources = buildResourceIndex([plant]);
|
state.resources = buildResourceIndex([plant]);
|
||||||
state.resources.consumers.farm_designer.selectedPlants = [plant.uuid];
|
state.resources.consumers.farm_designer.selectedPoints = [plant.uuid];
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const getState: GetState = jest.fn(() => state);
|
const getState: GetState = jest.fn(() => state);
|
||||||
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
|
||||||
expect(dispatch).toHaveBeenCalledWith({
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT, payload: []
|
type: Actions.SELECT_POINT, payload: []
|
||||||
});
|
});
|
||||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mapPointClickAction()", () => {
|
||||||
|
it("navigates", () => {
|
||||||
|
mockPath = "/app/designer/plants";
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
mapPointClickAction(dispatch, "uuid", "fake path")();
|
||||||
|
expect(history.push).toHaveBeenCalledWith("fake path");
|
||||||
|
expect(dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't navigate: box select", () => {
|
||||||
|
mockPath = "/app/designer/plants/select";
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
mapPointClickAction(dispatch, "uuid", "fake path")();
|
||||||
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
|
expect(dispatch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't navigate: group edit", () => {
|
||||||
|
mockPath = "/app/designer/groups/edit/1";
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
mapPointClickAction(dispatch, "uuid", "fake path")();
|
||||||
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
|
expect(dispatch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -6,15 +6,17 @@ jest.mock("../actions", () => ({
|
||||||
import { Mode } from "../interfaces";
|
import { Mode } from "../interfaces";
|
||||||
let mockMode = Mode.none;
|
let mockMode = Mode.none;
|
||||||
let mockAtPlant = true;
|
let mockAtPlant = true;
|
||||||
|
let mockInteractionAllow = true;
|
||||||
jest.mock("../util", () => ({
|
jest.mock("../util", () => ({
|
||||||
getMode: () => mockMode,
|
getMode: () => mockMode,
|
||||||
getMapSize: () => ({ h: 100, w: 100 }),
|
getMapSize: () => ({ h: 100, w: 100 }),
|
||||||
getGardenCoordinates: jest.fn(),
|
getGardenCoordinates: jest.fn(),
|
||||||
transformXY: jest.fn(() => ({ qx: 0, qy: 0 })),
|
transformXY: jest.fn(() => ({ qx: 0, qy: 0 })),
|
||||||
transformForQuadrant: jest.fn(),
|
transformForQuadrant: jest.fn(),
|
||||||
maybeNoPointer: jest.fn(),
|
|
||||||
round: jest.fn(),
|
round: jest.fn(),
|
||||||
cursorAtPlant: () => mockAtPlant,
|
cursorAtPlant: () => mockAtPlant,
|
||||||
|
allowInteraction: () => mockInteractionAllow,
|
||||||
|
allowGroupAreaInteraction: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../layers/plants/plant_actions", () => ({
|
jest.mock("../layers/plants/plant_actions", () => ({
|
||||||
|
@ -81,6 +83,7 @@ const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
|
||||||
const fakeProps = (): GardenMapProps => ({
|
const fakeProps = (): GardenMapProps => ({
|
||||||
showPoints: true,
|
showPoints: true,
|
||||||
showPlants: true,
|
showPlants: true,
|
||||||
|
showWeeds: true,
|
||||||
showSpread: false,
|
showSpread: false,
|
||||||
showFarmbot: false,
|
showFarmbot: false,
|
||||||
showImages: false,
|
showImages: false,
|
||||||
|
@ -92,6 +95,7 @@ const fakeProps = (): GardenMapProps => ({
|
||||||
designer: fakeDesignerState(),
|
designer: fakeDesignerState(),
|
||||||
plants: [],
|
plants: [],
|
||||||
genericPoints: [],
|
genericPoints: [],
|
||||||
|
weeds: [],
|
||||||
allPoints: [],
|
allPoints: [],
|
||||||
toolSlots: [],
|
toolSlots: [],
|
||||||
botLocationData: {
|
botLocationData: {
|
||||||
|
@ -286,7 +290,22 @@ describe("<GardenMap/>", () => {
|
||||||
wrapper.find(".drop-area-svg").simulate("mouseDown", {
|
wrapper.find(".drop-area-svg").simulate("mouseDown", {
|
||||||
pageX: 1, pageY: 2
|
pageX: 1, pageY: 2
|
||||||
});
|
});
|
||||||
expect(startNewPoint).toHaveBeenCalled();
|
expect(startNewPoint).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: "point"
|
||||||
|
}));
|
||||||
|
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ pageX: 1, pageY: 2 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts drawing weed", () => {
|
||||||
|
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||||
|
mockMode = Mode.createWeed;
|
||||||
|
wrapper.find(".drop-area-svg").simulate("mouseDown", {
|
||||||
|
pageX: 1, pageY: 2
|
||||||
|
});
|
||||||
|
expect(startNewPoint).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: "weed"
|
||||||
|
}));
|
||||||
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
expect(getGardenCoordinates).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ pageX: 1, pageY: 2 }));
|
expect.objectContaining({ pageX: 1, pageY: 2 }));
|
||||||
});
|
});
|
||||||
|
@ -297,7 +316,20 @@ describe("<GardenMap/>", () => {
|
||||||
wrapper.find(".drop-area-svg").simulate("mouseMove", {
|
wrapper.find(".drop-area-svg").simulate("mouseMove", {
|
||||||
pageX: 10, pageY: 20
|
pageX: 10, pageY: 20
|
||||||
});
|
});
|
||||||
expect(resizePoint).toHaveBeenCalled();
|
expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: "point"
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets drawn weed radius", () => {
|
||||||
|
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||||
|
mockMode = Mode.createWeed;
|
||||||
|
wrapper.find(".drop-area-svg").simulate("mouseMove", {
|
||||||
|
pageX: 10, pageY: 20
|
||||||
|
});
|
||||||
|
expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: "weed"
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lays eggs", () => {
|
it("lays eggs", () => {
|
||||||
|
@ -350,7 +382,7 @@ describe("<GardenMap/>", () => {
|
||||||
it("closes panel", () => {
|
it("closes panel", () => {
|
||||||
mockMode = Mode.boxSelect;
|
mockMode = Mode.boxSelect;
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.designer.selectedPlants = undefined;
|
p.designer.selectedPoints = undefined;
|
||||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
wrapper.instance().closePanel()();
|
wrapper.instance().closePanel()();
|
||||||
expect(closePlantInfo).toHaveBeenCalled();
|
expect(closePlantInfo).toHaveBeenCalled();
|
||||||
|
@ -366,7 +398,7 @@ describe("<GardenMap/>", () => {
|
||||||
it("doesn't close panel: box select", () => {
|
it("doesn't close panel: box select", () => {
|
||||||
mockMode = Mode.boxSelect;
|
mockMode = Mode.boxSelect;
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.designer.selectedPlants = [fakePlant().uuid];
|
p.designer.selectedPoints = [fakePlant().uuid];
|
||||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
wrapper.instance().closePanel()();
|
wrapper.instance().closePanel()();
|
||||||
expect(closePlantInfo).not.toHaveBeenCalled();
|
expect(closePlantInfo).not.toHaveBeenCalled();
|
||||||
|
@ -375,7 +407,7 @@ describe("<GardenMap/>", () => {
|
||||||
it("doesn't close panel: move mode", () => {
|
it("doesn't close panel: move mode", () => {
|
||||||
mockMode = Mode.moveTo;
|
mockMode = Mode.moveTo;
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.designer.selectedPlants = [fakePlant().uuid];
|
p.designer.selectedPoints = [fakePlant().uuid];
|
||||||
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
wrapper.instance().closePanel()();
|
wrapper.instance().closePanel()();
|
||||||
expect(closePlantInfo).not.toHaveBeenCalled();
|
expect(closePlantInfo).not.toHaveBeenCalled();
|
||||||
|
@ -404,6 +436,56 @@ describe("<GardenMap/>", () => {
|
||||||
expect(wrapper.instance().state.isDragging).toBe(true);
|
expect(wrapper.instance().state.isDragging).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows interactions: default", () => {
|
||||||
|
mockMode = Mode.none;
|
||||||
|
mockInteractionAllow = true;
|
||||||
|
const p = fakeProps();
|
||||||
|
p.designer.selectionPointType = undefined;
|
||||||
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
|
const allowed = wrapper.instance().interactions("Plant");
|
||||||
|
expect(allowed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows interactions: box select", () => {
|
||||||
|
mockMode = Mode.boxSelect;
|
||||||
|
mockInteractionAllow = true;
|
||||||
|
const p = fakeProps();
|
||||||
|
p.designer.selectionPointType = undefined;
|
||||||
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
|
const allowed = wrapper.instance().interactions("Plant");
|
||||||
|
expect(allowed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows interactions: group edit", () => {
|
||||||
|
mockMode = Mode.editGroup;
|
||||||
|
mockInteractionAllow = true;
|
||||||
|
const p = fakeProps();
|
||||||
|
p.designer.selectionPointType = undefined;
|
||||||
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
|
const allowed = wrapper.instance().interactions("Plant");
|
||||||
|
expect(allowed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disallows interactions: default", () => {
|
||||||
|
mockMode = Mode.none;
|
||||||
|
mockInteractionAllow = false;
|
||||||
|
const p = fakeProps();
|
||||||
|
p.designer.selectionPointType = undefined;
|
||||||
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
|
const allowed = wrapper.instance().interactions("Plant");
|
||||||
|
expect(allowed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disallows interactions: box select", () => {
|
||||||
|
mockMode = Mode.boxSelect;
|
||||||
|
mockInteractionAllow = true;
|
||||||
|
const p = fakeProps();
|
||||||
|
p.designer.selectionPointType = ["Plant"];
|
||||||
|
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
|
||||||
|
const allowed = wrapper.instance().interactions("Weed");
|
||||||
|
expect(allowed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it("unswapped height and width", () => {
|
it("unswapped height and width", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.getConfigValue = () => false;
|
p.getConfigValue = () => false;
|
||||||
|
|
|
@ -4,11 +4,6 @@ jest.mock("../../../history", () => ({
|
||||||
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
|
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mockGardenOpen = true;
|
|
||||||
jest.mock("../../saved_gardens/saved_gardens", () => ({
|
|
||||||
savedGardenOpen: () => mockGardenOpen,
|
|
||||||
}));
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
round,
|
round,
|
||||||
translateScreenToGarden,
|
translateScreenToGarden,
|
||||||
|
@ -21,6 +16,9 @@ import {
|
||||||
mapPanelClassName,
|
mapPanelClassName,
|
||||||
getMode,
|
getMode,
|
||||||
cursorAtPlant,
|
cursorAtPlant,
|
||||||
|
allowInteraction,
|
||||||
|
allowGroupAreaInteraction,
|
||||||
|
savedGardenOpen,
|
||||||
} from "../util";
|
} from "../util";
|
||||||
import { McuParams } from "farmbot";
|
import { McuParams } from "farmbot";
|
||||||
import {
|
import {
|
||||||
|
@ -32,13 +30,37 @@ import {
|
||||||
} from "../../../__test_support__/map_transform_props";
|
} from "../../../__test_support__/map_transform_props";
|
||||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||||
|
|
||||||
describe("Utils", () => {
|
describe("round()", () => {
|
||||||
it("rounds a number", () => {
|
it("rounds a number", () => {
|
||||||
expect(round(44)).toEqual(40);
|
expect(round(44)).toEqual(40);
|
||||||
expect(round(98)).toEqual(100);
|
expect(round(98)).toEqual(100);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mapPanelClassName()", () => {
|
||||||
|
it("returns correct panel status: short panel", () => {
|
||||||
|
Object.defineProperty(window, "innerWidth", {
|
||||||
|
value: 400,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
mockPath = "/app/designer/move_to";
|
||||||
|
expect(mapPanelClassName()).toEqual("short-panel");
|
||||||
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
|
expect(mapPanelClassName()).toEqual("short-panel");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct panel status: panel open", () => {
|
||||||
|
Object.defineProperty(window, "innerWidth", {
|
||||||
|
value: 500,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
mockPath = "/app/designer/move_to";
|
||||||
|
expect(mapPanelClassName()).toEqual("panel-open");
|
||||||
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
|
expect(mapPanelClassName()).toEqual("panel-open");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("translateScreenToGarden()", () => {
|
describe("translateScreenToGarden()", () => {
|
||||||
it("translates screen coords to garden coords: zoomLvl = 1", () => {
|
it("translates screen coords to garden coords: zoomLvl = 1", () => {
|
||||||
const result = translateScreenToGarden({
|
const result = translateScreenToGarden({
|
||||||
|
@ -344,17 +366,26 @@ describe("getMode()", () => {
|
||||||
expect(getMode()).toEqual(Mode.points);
|
expect(getMode()).toEqual(Mode.points);
|
||||||
mockPath = "/app/designer/points/add";
|
mockPath = "/app/designer/points/add";
|
||||||
expect(getMode()).toEqual(Mode.createPoint);
|
expect(getMode()).toEqual(Mode.createPoint);
|
||||||
mockPath = "/app/designer/gardens";
|
mockPath = "/app/designer/weeds";
|
||||||
mockGardenOpen = true;
|
expect(getMode()).toEqual(Mode.weeds);
|
||||||
|
mockPath = "/app/designer/weeds/add";
|
||||||
|
expect(getMode()).toEqual(Mode.createWeed);
|
||||||
|
mockPath = "/app/designer/gardens/1";
|
||||||
expect(getMode()).toEqual(Mode.templateView);
|
expect(getMode()).toEqual(Mode.templateView);
|
||||||
mockPath = "/app/designer/groups/1";
|
mockPath = "/app/designer/groups/1";
|
||||||
expect(getMode()).toEqual(Mode.editGroup);
|
expect(getMode()).toEqual(Mode.editGroup);
|
||||||
mockPath = "";
|
mockPath = "";
|
||||||
mockGardenOpen = false;
|
|
||||||
expect(getMode()).toEqual(Mode.none);
|
expect(getMode()).toEqual(Mode.none);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("savedGardenOpen", () => {
|
||||||
|
it("is open", () => {
|
||||||
|
const result = savedGardenOpen(["", "", "", "gardens", "4", ""]);
|
||||||
|
expect(result).toEqual(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getGardenCoordinates()", () => {
|
describe("getGardenCoordinates()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Object.defineProperty(document, "querySelector", {
|
Object.defineProperty(document, "querySelector", {
|
||||||
|
@ -396,27 +427,37 @@ describe("getGardenCoordinates()", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("mapPanelClassName()", () => {
|
describe("allowInteraction()", () => {
|
||||||
it("returns correct panel status: short panel", () => {
|
it("allows interaction", () => {
|
||||||
Object.defineProperty(window, "innerWidth", {
|
mockPath = "/app/designer/plants";
|
||||||
value: 400,
|
expect(allowInteraction()).toBeTruthy();
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
mockPath = "/app/designer/move_to";
|
|
||||||
expect(mapPanelClassName()).toEqual("short-panel");
|
|
||||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
|
||||||
expect(mapPanelClassName()).toEqual("short-panel");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns correct panel status: panel open", () => {
|
it("disallows interaction", () => {
|
||||||
Object.defineProperty(window, "innerWidth", {
|
|
||||||
value: 500,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
mockPath = "/app/designer/move_to";
|
|
||||||
expect(mapPanelClassName()).toEqual("panel-open");
|
|
||||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
expect(mapPanelClassName()).toEqual("panel-open");
|
expect(allowInteraction()).toBeFalsy();
|
||||||
|
mockPath = "/app/designer/move_to";
|
||||||
|
expect(allowInteraction()).toBeFalsy();
|
||||||
|
mockPath = "/app/designer/points/add";
|
||||||
|
expect(allowInteraction()).toBeFalsy();
|
||||||
|
mockPath = "/app/designer/weeds/add";
|
||||||
|
expect(allowInteraction()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("allowGroupAreaInteraction()", () => {
|
||||||
|
it("allows interaction", () => {
|
||||||
|
mockPath = "/app/designer/plants";
|
||||||
|
expect(allowGroupAreaInteraction()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disallows interaction", () => {
|
||||||
|
mockPath = "/app/designer/plants/select";
|
||||||
|
expect(allowGroupAreaInteraction()).toBeFalsy();
|
||||||
|
mockPath = "/app/designer/move_to";
|
||||||
|
expect(allowGroupAreaInteraction()).toBeFalsy();
|
||||||
|
mockPath = "/app/designer/groups/1";
|
||||||
|
expect(allowGroupAreaInteraction()).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MovePlantProps, DraggableEvent } from "../interfaces";
|
import { MovePlantProps, DraggableEvent } from "../interfaces";
|
||||||
import { defensiveClone } from "../../util";
|
import { defensiveClone } from "../../util";
|
||||||
import { edit, overwrite } from "../../api/crud";
|
import { edit } from "../../api/crud";
|
||||||
import { history } from "../../history";
|
import { history } from "../../history";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
|
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
|
||||||
|
@ -12,6 +12,7 @@ import { TaggedPoint } from "farmbot";
|
||||||
import { getMode } from "../map/util";
|
import { getMode } from "../map/util";
|
||||||
import { ResourceIndex, UUID } from "../../resources/interfaces";
|
import { ResourceIndex, UUID } from "../../resources/interfaces";
|
||||||
import { selectAllPointGroups } from "../../resources/selectors";
|
import { selectAllPointGroups } from "../../resources/selectors";
|
||||||
|
import { overwriteGroup } from "../point_groups/actions";
|
||||||
|
|
||||||
export function movePlant(payload: MovePlantProps) {
|
export function movePlant(payload: MovePlantProps) {
|
||||||
const tr = payload.plant;
|
const tr = payload.plant;
|
||||||
|
@ -23,8 +24,8 @@ export function movePlant(payload: MovePlantProps) {
|
||||||
return edit(tr, update);
|
return edit(tr, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectPlant = (payload: string[] | undefined) => {
|
export const selectPoint = (payload: string[] | undefined) => {
|
||||||
return { type: Actions.SELECT_PLANT, payload };
|
return { type: Actions.SELECT_POINT, payload };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
||||||
|
@ -33,35 +34,36 @@ export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const addOrRemoveFromGroup =
|
const addOrRemoveFromGroup =
|
||||||
(clickedPlantUuid: UUID, resources: ResourceIndex) => {
|
(clickedPlantUuid: UUID, resources: ResourceIndex) =>
|
||||||
const group = findGroupFromUrl(selectAllPointGroups(resources));
|
(dispatch: Function) => {
|
||||||
const point =
|
const group = findGroupFromUrl(selectAllPointGroups(resources));
|
||||||
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
|
const point =
|
||||||
if (group && point?.body.id) {
|
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
|
||||||
type Body = (typeof group)["body"];
|
if (group && point?.body.id) {
|
||||||
const nextGroup: Body = ({
|
type Body = (typeof group)["body"];
|
||||||
...group.body,
|
const nextGroup: Body = ({
|
||||||
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
|
...group.body,
|
||||||
});
|
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
|
||||||
if (!group.body.point_ids.includes(point.body.id)) {
|
});
|
||||||
nextGroup.point_ids.push(point.body.id);
|
if (!group.body.point_ids.includes(point.body.id)) {
|
||||||
|
nextGroup.point_ids.push(point.body.id);
|
||||||
|
}
|
||||||
|
nextGroup.point_ids = uniq(nextGroup.point_ids);
|
||||||
|
dispatch(overwriteGroup(group, nextGroup));
|
||||||
}
|
}
|
||||||
nextGroup.point_ids = uniq(nextGroup.point_ids);
|
};
|
||||||
return overwrite(group, nextGroup);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addOrRemoveFromSelection =
|
const addOrRemoveFromSelection =
|
||||||
(clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => {
|
(clickedPointUuid: UUID, selectedPoints: UUID[] | undefined) => {
|
||||||
const nextSelected =
|
const nextSelected =
|
||||||
(selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid);
|
(selectedPoints || []).filter(uuid => uuid !== clickedPointUuid);
|
||||||
if (!(selectedPlants?.includes(clickedPlantUuid))) {
|
if (!(selectedPoints?.includes(clickedPointUuid))) {
|
||||||
nextSelected.push(clickedPlantUuid);
|
nextSelected.push(clickedPointUuid);
|
||||||
}
|
}
|
||||||
return selectPlant(nextSelected);
|
return selectPoint(nextSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
|
export const clickMapPlant = (clickedPlantUuid: UUID, icon: string) => {
|
||||||
return (dispatch: Function, getState: GetState) => {
|
return (dispatch: Function, getState: GetState) => {
|
||||||
switch (getMode()) {
|
switch (getMode()) {
|
||||||
case Mode.editGroup:
|
case Mode.editGroup:
|
||||||
|
@ -69,11 +71,11 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
|
||||||
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
|
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
|
||||||
break;
|
break;
|
||||||
case Mode.boxSelect:
|
case Mode.boxSelect:
|
||||||
const { selectedPlants } = getState().resources.consumers.farm_designer;
|
const { selectedPoints } = getState().resources.consumers.farm_designer;
|
||||||
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPlants));
|
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPoints));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
dispatch(selectPlant([clickedPlantUuid]));
|
dispatch(selectPoint([clickedPlantUuid]));
|
||||||
dispatch(setHoveredPlant(clickedPlantUuid, icon));
|
dispatch(setHoveredPlant(clickedPlantUuid, icon));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -81,7 +83,7 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unselectPlant = (dispatch: Function) => () => {
|
export const unselectPlant = (dispatch: Function) => () => {
|
||||||
dispatch(selectPlant(undefined));
|
dispatch(selectPoint(undefined));
|
||||||
dispatch(setHoveredPlant(undefined));
|
dispatch(setHoveredPlant(undefined));
|
||||||
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
|
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
|
||||||
};
|
};
|
||||||
|
@ -104,3 +106,14 @@ export const setDragIcon =
|
||||||
e.dataTransfer.setDragImage
|
e.dataTransfer.setDragImage
|
||||||
&& e.dataTransfer.setDragImage(dragImg, width / 2, height / 2);
|
&& e.dataTransfer.setDragImage(dragImg, width / 2, height / 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mapPointClickAction =
|
||||||
|
(dispatch: Function, uuid: UUID, path?: string) => () => {
|
||||||
|
switch (getMode()) {
|
||||||
|
case Mode.editGroup:
|
||||||
|
case Mode.boxSelect:
|
||||||
|
return dispatch(clickMapPlant(uuid, ""));
|
||||||
|
default:
|
||||||
|
return path && history.push(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -8,9 +8,8 @@ jest.mock("../../../point_groups/criteria", () => ({
|
||||||
editGtLtCriteria: jest.fn(),
|
editGtLtCriteria: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../../../../api/crud", () => ({
|
jest.mock("../../../point_groups/actions", () => ({
|
||||||
overwrite: jest.fn(),
|
overwriteGroup: jest.fn(),
|
||||||
save: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -25,8 +24,8 @@ import {
|
||||||
import { Actions } from "../../../../constants";
|
import { Actions } from "../../../../constants";
|
||||||
import { history } from "../../../../history";
|
import { history } from "../../../../history";
|
||||||
import { editGtLtCriteria } from "../../../point_groups/criteria";
|
import { editGtLtCriteria } from "../../../point_groups/criteria";
|
||||||
import { overwrite, save } from "../../../../api/crud";
|
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
import { overwriteGroup } from "../../../point_groups/actions";
|
||||||
|
|
||||||
describe("getSelected", () => {
|
describe("getSelected", () => {
|
||||||
it("returns some", () => {
|
it("returns some", () => {
|
||||||
|
@ -55,6 +54,9 @@ describe("resizeBox", () => {
|
||||||
const fakeProps = (): ResizeSelectionBoxProps => ({
|
const fakeProps = (): ResizeSelectionBoxProps => ({
|
||||||
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
|
||||||
plants: [],
|
plants: [],
|
||||||
|
allPoints: [],
|
||||||
|
selectionPointType: undefined,
|
||||||
|
getConfigValue: () => true,
|
||||||
gardenCoords: { x: 100, y: 200 },
|
gardenCoords: { x: 100, y: 200 },
|
||||||
setMapState: jest.fn(),
|
setMapState: jest.fn(),
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
|
@ -68,7 +70,7 @@ describe("resizeBox", () => {
|
||||||
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -113,7 +115,7 @@ describe("resizeBox", () => {
|
||||||
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: [plant.uuid]
|
payload: [plant.uuid]
|
||||||
});
|
});
|
||||||
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
|
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
|
||||||
|
@ -135,7 +137,7 @@ describe("startNewSelectionBox", () => {
|
||||||
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
|
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
|
||||||
});
|
});
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -157,7 +159,7 @@ describe("startNewSelectionBox", () => {
|
||||||
startNewSelectionBox(p);
|
startNewSelectionBox(p);
|
||||||
expect(p.setMapState).not.toHaveBeenCalled();
|
expect(p.setMapState).not.toHaveBeenCalled();
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SELECT_PLANT,
|
type: Actions.SELECT_POINT,
|
||||||
payload: undefined
|
payload: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -186,8 +188,7 @@ describe("maybeUpdateGroup()", () => {
|
||||||
expectedBody && (expectedBody.point_ids = [
|
expectedBody && (expectedBody.point_ids = [
|
||||||
plant1.body.id || 0, plant2.body.id || 0,
|
plant1.body.id || 0, plant2.body.id || 0,
|
||||||
]);
|
]);
|
||||||
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
|
expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedBody);
|
||||||
expect(save).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates criteria", () => {
|
it("updates criteria", () => {
|
||||||
|
@ -211,7 +212,6 @@ describe("maybeUpdateGroup()", () => {
|
||||||
maybeUpdateGroup(p);
|
maybeUpdateGroup(p);
|
||||||
expect(p.dispatch).not.toHaveBeenCalled();
|
expect(p.dispatch).not.toHaveBeenCalled();
|
||||||
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
expect(editGtLtCriteria).not.toHaveBeenCalled();
|
||||||
expect(overwrite).not.toHaveBeenCalled();
|
expect(overwriteGroup).not.toHaveBeenCalled();
|
||||||
expect(save).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,18 +3,20 @@ import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
|
||||||
import { SelectionBoxData } from "./selection_box";
|
import { SelectionBoxData } from "./selection_box";
|
||||||
import { GardenMapState } from "../../interfaces";
|
import { GardenMapState } from "../../interfaces";
|
||||||
import { history } from "../../../history";
|
import { history } from "../../../history";
|
||||||
import { selectPlant } from "../actions";
|
import { selectPoint } from "../actions";
|
||||||
import { getMode } from "../util";
|
import { getMode } from "../util";
|
||||||
import { editGtLtCriteria } from "../../point_groups/criteria";
|
import { editGtLtCriteria } from "../../point_groups/criteria";
|
||||||
import { TaggedPointGroup } from "farmbot";
|
import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot";
|
||||||
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
||||||
import { overwrite } from "../../../api/crud";
|
|
||||||
import { unpackUUID } from "../../../util";
|
import { unpackUUID } from "../../../util";
|
||||||
import { UUID } from "../../../resources/interfaces";
|
import { UUID } from "../../../resources/interfaces";
|
||||||
|
import { getFilteredPoints } from "../../plants/select_plants";
|
||||||
|
import { GetWebAppConfigValue } from "../../../config_storage/actions";
|
||||||
|
import { overwriteGroup } from "../../point_groups/actions";
|
||||||
|
|
||||||
/** Return all plants within the selection box. */
|
/** Return all plants within the selection box. */
|
||||||
export const getSelected = (
|
export const getSelected = (
|
||||||
plants: TaggedPlant[],
|
plants: (TaggedPlant | TaggedPoint)[],
|
||||||
box: SelectionBoxData | undefined,
|
box: SelectionBoxData | undefined,
|
||||||
): string[] | undefined => {
|
): string[] | undefined => {
|
||||||
const arraySelected = plants.filter(p => {
|
const arraySelected = plants.filter(p => {
|
||||||
|
@ -35,6 +37,9 @@ export const getSelected = (
|
||||||
export interface ResizeSelectionBoxProps {
|
export interface ResizeSelectionBoxProps {
|
||||||
selectionBox: SelectionBoxData | undefined;
|
selectionBox: SelectionBoxData | undefined;
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
|
allPoints: TaggedPoint[];
|
||||||
|
selectionPointType: PointType[] | undefined;
|
||||||
|
getConfigValue: GetWebAppConfigValue;
|
||||||
gardenCoords: AxisNumberProperty | undefined;
|
gardenCoords: AxisNumberProperty | undefined;
|
||||||
setMapState: (x: Partial<GardenMapState>) => void;
|
setMapState: (x: Partial<GardenMapState>) => void;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
@ -54,11 +59,16 @@ export const resizeBox = (props: ResizeSelectionBoxProps) => {
|
||||||
props.setMapState({ selectionBox: newSelectionBox });
|
props.setMapState({ selectionBox: newSelectionBox });
|
||||||
if (props.plantActions) {
|
if (props.plantActions) {
|
||||||
// Select all plants within the updated selection box
|
// Select all plants within the updated selection box
|
||||||
const payload = getSelected(props.plants, newSelectionBox);
|
const { plants, allPoints, selectionPointType, getConfigValue } = props;
|
||||||
|
const points =
|
||||||
|
getFilteredPoints({
|
||||||
|
plants, allPoints, selectionPointType, getConfigValue
|
||||||
|
});
|
||||||
|
const payload = getSelected(points, newSelectionBox);
|
||||||
if (payload && getMode() === Mode.none) {
|
if (payload && getMode() === Mode.none) {
|
||||||
history.push("/app/designer/plants/select");
|
history.push("/app/designer/plants/select");
|
||||||
}
|
}
|
||||||
props.dispatch(selectPlant(payload));
|
props.dispatch(selectPoint(payload));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +94,7 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
|
||||||
}
|
}
|
||||||
if (props.plantActions) {
|
if (props.plantActions) {
|
||||||
// Clear the previous plant selection when starting a new selection box
|
// Clear the previous plant selection when starting a new selection box
|
||||||
props.dispatch(selectPlant(undefined));
|
props.dispatch(selectPoint(undefined));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,20 +109,21 @@ export interface MaybeUpdateGroupProps {
|
||||||
|
|
||||||
export const maybeUpdateGroup =
|
export const maybeUpdateGroup =
|
||||||
(props: MaybeUpdateGroupProps) => {
|
(props: MaybeUpdateGroupProps) => {
|
||||||
if (props.selectionBox && props.group) {
|
const { group } = props;
|
||||||
|
if (props.selectionBox && group) {
|
||||||
if (props.editGroupAreaInMap
|
if (props.editGroupAreaInMap
|
||||||
&& props.shouldDisplay(Feature.criteria_groups)) {
|
&& props.shouldDisplay(Feature.criteria_groups)) {
|
||||||
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
|
props.dispatch(editGtLtCriteria(group, props.selectionBox));
|
||||||
} else {
|
} else {
|
||||||
const nextGroupBody = cloneDeep(props.group.body);
|
const nextGroupBody = cloneDeep(group.body);
|
||||||
props.boxSelected?.map(uuid => {
|
props.boxSelected?.map(uuid => {
|
||||||
const { kind, remoteId } = unpackUUID(uuid);
|
const { kind, remoteId } = unpackUUID(uuid);
|
||||||
remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId);
|
remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId);
|
||||||
});
|
});
|
||||||
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
|
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
|
||||||
if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
|
if (!isEqual(group.body.point_ids, nextGroupBody.point_ids)) {
|
||||||
props.dispatch(overwrite(props.group, nextGroupBody));
|
props.dispatch(overwriteGroup(group, nextGroupBody));
|
||||||
props.dispatch(selectPlant(undefined));
|
props.dispatch(selectPoint(undefined));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { startNewPoint, resizePoint } from "../drawn_point_actions";
|
import {
|
||||||
|
startNewPoint, resizePoint, StartNewPointProps, ResizePointProps,
|
||||||
|
} from "../drawn_point_actions";
|
||||||
import { Actions } from "../../../../constants";
|
import { Actions } from "../../../../constants";
|
||||||
|
|
||||||
describe("startNewPoint", () => {
|
describe("startNewPoint", () => {
|
||||||
const fakeProps = () => ({
|
const fakeProps = (): StartNewPointProps => ({
|
||||||
gardenCoords: { x: 100, y: 200 },
|
gardenCoords: { x: 100, y: 200 },
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
setMapState: jest.fn(),
|
setMapState: jest.fn(),
|
||||||
|
type: "point",
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts point", () => {
|
it("starts point", () => {
|
||||||
|
@ -13,15 +16,25 @@ describe("startNewPoint", () => {
|
||||||
startNewPoint(p);
|
startNewPoint(p);
|
||||||
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
|
payload: { cx: 100, cy: 200, r: 0 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.type = "weed";
|
||||||
|
startNewPoint(p);
|
||||||
|
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.SET_DRAWN_WEED_DATA,
|
||||||
payload: { cx: 100, cy: 200, r: 0 }
|
payload: { cx: 100, cy: 200, r: 0 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't start point", () => {
|
it("doesn't start point", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
// tslint:disable-next-line:no-any
|
p.gardenCoords = undefined;
|
||||||
p.gardenCoords = undefined as any;
|
|
||||||
startNewPoint(p);
|
startNewPoint(p);
|
||||||
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
|
||||||
expect(p.dispatch).not.toHaveBeenCalled();
|
expect(p.dispatch).not.toHaveBeenCalled();
|
||||||
|
@ -29,18 +42,29 @@ describe("startNewPoint", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resizePoint", () => {
|
describe("resizePoint", () => {
|
||||||
const fakeProps = () => ({
|
const fakeProps = (): ResizePointProps => ({
|
||||||
gardenCoords: { x: 100, y: 200 },
|
gardenCoords: { x: 100, y: 200 },
|
||||||
currentPoint: { cx: 100, cy: 200, r: 0 },
|
drawnPoint: { cx: 100, cy: 200, r: 0 },
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
|
type: "point",
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes point", () => {
|
it("resizes point", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
resizePoint(p);
|
resizePoint(p);
|
||||||
expect(p.dispatch).toHaveBeenCalledWith({
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: Actions.SET_DRAWN_POINT_DATA,
|
||||||
|
payload: { cx: 100, cy: 200, r: 0 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resizes weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.type = "weed";
|
||||||
|
resizePoint(p);
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.SET_DRAWN_WEED_DATA,
|
||||||
payload: { cx: 100, cy: 200, r: 0 }
|
payload: { cx: 100, cy: 200, r: 0 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,13 +12,12 @@ describe("<DrawnPoint/>", () => {
|
||||||
cx: 10,
|
cx: 10,
|
||||||
cy: 20,
|
cy: 20,
|
||||||
r: 30,
|
r: 30,
|
||||||
color: "red"
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders point", () => {
|
it("renders point", () => {
|
||||||
const wrapper = svgMount(<DrawnPoint {...fakeProps()} />);
|
const wrapper = svgMount(<DrawnPoint {...fakeProps()} />);
|
||||||
expect(wrapper.find("g").props().stroke).toEqual("red");
|
expect(wrapper.find("g").props().stroke).toEqual("green");
|
||||||
expect(wrapper.find("circle").first().props()).toEqual({
|
expect(wrapper.find("circle").first().props()).toEqual({
|
||||||
id: "point-radius", strokeDasharray: "4 5",
|
id: "point-radius", strokeDasharray: "4 5",
|
||||||
cx: 10, cy: 20, r: 30,
|
cx: 10, cy: 20, r: 30,
|
||||||
|
@ -28,4 +27,11 @@ describe("<DrawnPoint/>", () => {
|
||||||
cx: 10, cy: 20, r: 2,
|
cx: 10, cy: 20, r: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders point with chosen color", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.data = { cx: 0, cy: 0, r: 1, color: "red" };
|
||||||
|
const wrapper = svgMount(<DrawnPoint {...p} />);
|
||||||
|
expect(wrapper.find("g").props().stroke).toEqual("red");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { DrawnWeed, DrawnWeedProps } from "../drawn_weed";
|
||||||
|
import {
|
||||||
|
fakeMapTransformProps,
|
||||||
|
} from "../../../../__test_support__/map_transform_props";
|
||||||
|
import { svgMount } from "../../../../__test_support__/svg_mount";
|
||||||
|
|
||||||
|
describe("<DrawnWeed />", () => {
|
||||||
|
const fakeProps = (): DrawnWeedProps => ({
|
||||||
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
data: {
|
||||||
|
cx: 10,
|
||||||
|
cy: 20,
|
||||||
|
r: 30,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders weed", () => {
|
||||||
|
const wrapper = svgMount(<DrawnWeed {...fakeProps()} />);
|
||||||
|
const stop = wrapper.find("stop").first().props();
|
||||||
|
expect(stop.stopColor).toEqual("red");
|
||||||
|
expect(stop.stopOpacity).toEqual(0.25);
|
||||||
|
expect(wrapper.find("circle").first().props()).toEqual({
|
||||||
|
id: "weed-radius", cx: 10, cy: 20, r: 30, fill: "url(#DrawnWeedGradient)",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders point with chosen color", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.data = { cx: 0, cy: 0, r: 1, color: "orange" };
|
||||||
|
const wrapper = svgMount(<DrawnWeed {...p} />);
|
||||||
|
const stop = wrapper.find("stop").first().props();
|
||||||
|
expect(stop.stopColor).toEqual("orange");
|
||||||
|
expect(stop.stopOpacity).toEqual(0.5);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { MapTransformProps } from "../interfaces";
|
import { MapTransformProps } from "../interfaces";
|
||||||
import { transformXY } from "../util";
|
import { transformXY } from "../util";
|
||||||
import { CurrentPointPayl } from "../../interfaces";
|
import { DrawnPointPayl } from "../../interfaces";
|
||||||
|
|
||||||
export interface DrawnPointProps {
|
export interface DrawnPointProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
data: CurrentPointPayl | undefined;
|
data: DrawnPointPayl | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DrawnPoint(props: DrawnPointProps) {
|
export function DrawnPoint(props: DrawnPointProps) {
|
||||||
|
|
|
@ -1,37 +1,47 @@
|
||||||
import { Actions } from "../../../constants";
|
import { Actions } from "../../../constants";
|
||||||
import { AxisNumberProperty } from "../interfaces";
|
import { AxisNumberProperty } from "../interfaces";
|
||||||
import { CurrentPointPayl } from "../../interfaces";
|
import { DrawnPointPayl } from "../../interfaces";
|
||||||
|
|
||||||
|
export interface StartNewPointProps {
|
||||||
|
gardenCoords: AxisNumberProperty | undefined;
|
||||||
|
dispatch: Function;
|
||||||
|
setMapState: Function;
|
||||||
|
type: "point" | "weed";
|
||||||
|
}
|
||||||
|
|
||||||
/** Create a new point. */
|
/** Create a new point. */
|
||||||
export const startNewPoint = (props: {
|
export const startNewPoint = (props: StartNewPointProps) => {
|
||||||
gardenCoords: AxisNumberProperty | undefined,
|
|
||||||
dispatch: Function,
|
|
||||||
setMapState: Function,
|
|
||||||
}) => {
|
|
||||||
props.setMapState({ isDragging: true });
|
props.setMapState({ isDragging: true });
|
||||||
const center = props.gardenCoords;
|
const center = props.gardenCoords;
|
||||||
if (center) {
|
if (center) {
|
||||||
// Set the center of a new point
|
// Set the center of a new point
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: props.type == "weed"
|
||||||
|
? Actions.SET_DRAWN_WEED_DATA
|
||||||
|
: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: { cx: center.x, cy: center.y, r: 0 }
|
payload: { cx: center.x, cy: center.y, r: 0 }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ResizePointProps {
|
||||||
|
gardenCoords: AxisNumberProperty | undefined;
|
||||||
|
drawnPoint: DrawnPointPayl | undefined;
|
||||||
|
dispatch: Function;
|
||||||
|
isDragging: boolean | undefined;
|
||||||
|
type: "point" | "weed";
|
||||||
|
}
|
||||||
|
|
||||||
/** Resize a point. */
|
/** Resize a point. */
|
||||||
export const resizePoint = (props: {
|
export const resizePoint = (props: ResizePointProps) => {
|
||||||
gardenCoords: AxisNumberProperty | undefined,
|
|
||||||
currentPoint: CurrentPointPayl | undefined,
|
|
||||||
dispatch: Function,
|
|
||||||
isDragging: boolean | undefined,
|
|
||||||
}) => {
|
|
||||||
const edge = props.gardenCoords;
|
const edge = props.gardenCoords;
|
||||||
if (edge && props.currentPoint && !!props.isDragging) {
|
if (edge && props.drawnPoint && !!props.isDragging) {
|
||||||
const { cx, cy } = props.currentPoint;
|
const { cx, cy } = props.drawnPoint;
|
||||||
// Adjust the radius of the point being created
|
// Adjust the radius of the point being created
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: Actions.SET_CURRENT_POINT_DATA,
|
type: props.type == "weed"
|
||||||
|
? Actions.SET_DRAWN_WEED_DATA
|
||||||
|
: Actions.SET_DRAWN_POINT_DATA,
|
||||||
payload: {
|
payload: {
|
||||||
cx, cy, // Center was set by click, radius is adjusted by drag
|
cx, cy, // Center was set by click, radius is adjusted by drag
|
||||||
r: Math.round(Math.sqrt(
|
r: Math.round(Math.sqrt(
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { MapTransformProps } from "../interfaces";
|
||||||
|
import { transformXY } from "../util";
|
||||||
|
import { DrawnWeedPayl } from "../../interfaces";
|
||||||
|
|
||||||
|
export interface DrawnWeedProps {
|
||||||
|
mapTransformProps: MapTransformProps;
|
||||||
|
data: DrawnWeedPayl | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawnWeed(props: DrawnWeedProps) {
|
||||||
|
const ID = "current-weed";
|
||||||
|
const { data, mapTransformProps } = props;
|
||||||
|
if (!data) { return <g id={ID} />; }
|
||||||
|
const { cx, cy, r } = data;
|
||||||
|
const color = data.color || "red";
|
||||||
|
const { qx, qy } = transformXY(cx, cy, mapTransformProps);
|
||||||
|
const stopOpacity = ["gray", "pink", "orange"].includes(color) ? 0.5 : 0.25;
|
||||||
|
return <g id={ID}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id={"DrawnWeedGradient"}>
|
||||||
|
<stop offset="90%" stopColor={color} stopOpacity={stopOpacity} />
|
||||||
|
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<circle
|
||||||
|
id={"weed-radius"}
|
||||||
|
cx={qx}
|
||||||
|
cy={qy}
|
||||||
|
r={r}
|
||||||
|
fill={"url(#DrawnWeedGradient)"} />
|
||||||
|
</g>;
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from "./interfaces";
|
} from "./interfaces";
|
||||||
import { GardenMapProps, GardenMapState } from "../interfaces";
|
import { GardenMapProps, GardenMapState } from "../interfaces";
|
||||||
import {
|
import {
|
||||||
getMapSize, getGardenCoordinates, getMode, cursorAtPlant,
|
getMapSize, getGardenCoordinates, getMode, cursorAtPlant, allowInteraction,
|
||||||
} from "./util";
|
} from "./util";
|
||||||
import {
|
import {
|
||||||
Grid, MapBackground,
|
Grid, MapBackground,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
PlantLayer,
|
PlantLayer,
|
||||||
SpreadLayer,
|
SpreadLayer,
|
||||||
PointLayer,
|
PointLayer,
|
||||||
|
WeedLayer,
|
||||||
ToolSlotLayer,
|
ToolSlotLayer,
|
||||||
FarmBotLayer,
|
FarmBotLayer,
|
||||||
ImageLayer,
|
ImageLayer,
|
||||||
|
@ -34,9 +35,12 @@ import { NNPath } from "../point_groups/paths";
|
||||||
import { history } from "../../history";
|
import { history } from "../../history";
|
||||||
import { ZonesLayer } from "./layers/zones/zones_layer";
|
import { ZonesLayer } from "./layers/zones/zones_layer";
|
||||||
import { ErrorBoundary } from "../../error_boundary";
|
import { ErrorBoundary } from "../../error_boundary";
|
||||||
import { TaggedPoint, TaggedPointGroup } from "farmbot";
|
import { TaggedPoint, TaggedPointGroup, PointType } from "farmbot";
|
||||||
import { findGroupFromUrl } from "../point_groups/group_detail";
|
import { findGroupFromUrl } from "../point_groups/group_detail";
|
||||||
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
import { pointsSelectedByGroup } from "../point_groups/criteria";
|
||||||
|
import { DrawnWeed } from "./drawn_point/drawn_weed";
|
||||||
|
import { UUID } from "../../resources/interfaces";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
|
||||||
export class GardenMap extends
|
export class GardenMap extends
|
||||||
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
||||||
|
@ -81,8 +85,12 @@ export class GardenMap extends
|
||||||
pointsSelectedByGroup(this.group, this.props.allPoints) : [];
|
pointsSelectedByGroup(this.group, this.props.allPoints) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get groupSelected(): UUID[] {
|
||||||
|
return this.pointsSelectedByGroup.map(point => point.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
/** Save the current plant (if needed) and reset drag state. */
|
/** Save the current plant (if needed) and reset drag state. */
|
||||||
endDrag = () => {
|
endDrag = throttle(() => {
|
||||||
maybeSavePlantLocation({
|
maybeSavePlantLocation({
|
||||||
plant: this.getPlant(),
|
plant: this.getPlant(),
|
||||||
isDragging: this.state.isDragging,
|
isDragging: this.state.isDragging,
|
||||||
|
@ -94,7 +102,7 @@ export class GardenMap extends
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
shouldDisplay: this.props.shouldDisplay,
|
shouldDisplay: this.props.shouldDisplay,
|
||||||
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
|
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
|
||||||
boxSelected: this.props.designer.selectedPlants,
|
boxSelected: this.props.designer.selectedPoints,
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
isDragging: false, qPageX: 0, qPageY: 0,
|
isDragging: false, qPageX: 0, qPageY: 0,
|
||||||
|
@ -102,7 +110,7 @@ export class GardenMap extends
|
||||||
activeDragSpread: undefined,
|
activeDragSpread: undefined,
|
||||||
selectionBox: undefined
|
selectionBox: undefined
|
||||||
});
|
});
|
||||||
}
|
}, 400);
|
||||||
|
|
||||||
getGardenCoordinates =
|
getGardenCoordinates =
|
||||||
(e: React.DragEvent<HTMLElement> | React.MouseEvent<SVGElement>):
|
(e: React.DragEvent<HTMLElement> | React.MouseEvent<SVGElement>):
|
||||||
|
@ -152,6 +160,15 @@ export class GardenMap extends
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
setMapState: this.setMapState,
|
setMapState: this.setMapState,
|
||||||
|
type: "point",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Mode.createWeed:
|
||||||
|
startNewPoint({
|
||||||
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
|
dispatch: this.props.dispatch,
|
||||||
|
setMapState: this.setMapState,
|
||||||
|
type: "weed",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Mode.clickToAdd:
|
case Mode.clickToAdd:
|
||||||
|
@ -163,8 +180,8 @@ export class GardenMap extends
|
||||||
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
|
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
|
||||||
switch (getMode()) {
|
switch (getMode()) {
|
||||||
case Mode.moveTo:
|
case Mode.moveTo:
|
||||||
break;
|
|
||||||
case Mode.createPoint:
|
case Mode.createPoint:
|
||||||
|
case Mode.createWeed:
|
||||||
case Mode.clickToAdd:
|
case Mode.clickToAdd:
|
||||||
case Mode.editPlant:
|
case Mode.editPlant:
|
||||||
break;
|
break;
|
||||||
|
@ -196,17 +213,27 @@ export class GardenMap extends
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interactions = (pointerType: PointType): boolean => {
|
||||||
|
if (allowInteraction()) {
|
||||||
|
switch (getMode()) {
|
||||||
|
case Mode.editGroup:
|
||||||
|
case Mode.boxSelect:
|
||||||
|
return (this.props.designer.selectionPointType || ["Plant"])
|
||||||
|
.includes(pointerType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowInteraction();
|
||||||
|
};
|
||||||
|
|
||||||
/** Return the selected plant, mode-allowing. */
|
/** Return the selected plant, mode-allowing. */
|
||||||
getPlant = (): TaggedPlant | undefined => {
|
getPlant = (): TaggedPlant | undefined => {
|
||||||
switch (getMode()) {
|
return allowInteraction()
|
||||||
case Mode.boxSelect:
|
? this.props.selectedPlant
|
||||||
case Mode.moveTo:
|
: undefined;
|
||||||
case Mode.points:
|
}
|
||||||
case Mode.createPoint:
|
|
||||||
return undefined; // For modes without plant interaction
|
get currentPoint(): UUID | undefined {
|
||||||
default:
|
return this.props.designer.selectedPoints?.[0];
|
||||||
return this.props.selectedPlant;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
|
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
|
||||||
|
@ -273,15 +300,28 @@ export class GardenMap extends
|
||||||
case Mode.createPoint:
|
case Mode.createPoint:
|
||||||
resizePoint({
|
resizePoint({
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
currentPoint: this.props.designer.currentPoint,
|
drawnPoint: this.props.designer.drawnPoint,
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
isDragging: this.state.isDragging,
|
isDragging: this.state.isDragging,
|
||||||
|
type: "point",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Mode.createWeed:
|
||||||
|
resizePoint({
|
||||||
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
|
drawnPoint: this.props.designer.drawnWeed,
|
||||||
|
dispatch: this.props.dispatch,
|
||||||
|
isDragging: this.state.isDragging,
|
||||||
|
type: "weed",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Mode.editGroup:
|
case Mode.editGroup:
|
||||||
resizeBox({
|
resizeBox({
|
||||||
selectionBox: this.state.selectionBox,
|
selectionBox: this.state.selectionBox,
|
||||||
plants: this.props.plants,
|
plants: this.props.plants,
|
||||||
|
allPoints: this.props.allPoints,
|
||||||
|
selectionPointType: this.props.designer.selectionPointType,
|
||||||
|
getConfigValue: this.props.getConfigValue,
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
setMapState: this.setMapState,
|
setMapState: this.setMapState,
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
|
@ -293,6 +333,9 @@ export class GardenMap extends
|
||||||
resizeBox({
|
resizeBox({
|
||||||
selectionBox: this.state.selectionBox,
|
selectionBox: this.state.selectionBox,
|
||||||
plants: this.props.plants,
|
plants: this.props.plants,
|
||||||
|
allPoints: this.props.allPoints,
|
||||||
|
selectionPointType: this.props.designer.selectionPointType,
|
||||||
|
getConfigValue: this.props.getConfigValue,
|
||||||
gardenCoords: this.getGardenCoordinates(e),
|
gardenCoords: this.getGardenCoordinates(e),
|
||||||
setMapState: this.setMapState,
|
setMapState: this.setMapState,
|
||||||
dispatch: this.props.dispatch,
|
dispatch: this.props.dispatch,
|
||||||
|
@ -308,7 +351,7 @@ export class GardenMap extends
|
||||||
case Mode.moveTo:
|
case Mode.moveTo:
|
||||||
return () => { };
|
return () => { };
|
||||||
case Mode.boxSelect:
|
case Mode.boxSelect:
|
||||||
return this.props.designer.selectedPlants
|
return this.props.designer.selectedPoints
|
||||||
? () => { }
|
? () => { }
|
||||||
: closePlantInfo(this.props.dispatch);
|
: closePlantInfo(this.props.dispatch);
|
||||||
default:
|
default:
|
||||||
|
@ -362,6 +405,7 @@ export class GardenMap extends
|
||||||
botSize={this.props.botSize}
|
botSize={this.props.botSize}
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
groups={this.props.groups}
|
groups={this.props.groups}
|
||||||
|
startDrag={this.startDragOnBackground}
|
||||||
currentGroup={this.group?.uuid} />
|
currentGroup={this.group?.uuid} />
|
||||||
SensorReadingsLayer = () => <SensorReadingsLayer
|
SensorReadingsLayer = () => <SensorReadingsLayer
|
||||||
visible={!!this.props.showSensorReadings}
|
visible={!!this.props.showSensorReadings}
|
||||||
|
@ -385,7 +429,20 @@ export class GardenMap extends
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
hoveredPoint={this.props.designer.hoveredPoint}
|
hoveredPoint={this.props.designer.hoveredPoint}
|
||||||
visible={!!this.props.showPoints}
|
visible={!!this.props.showPoints}
|
||||||
|
interactions={this.interactions("GenericPointer")}
|
||||||
genericPoints={this.props.genericPoints} />
|
genericPoints={this.props.genericPoints} />
|
||||||
|
WeedLayer = () => <WeedLayer
|
||||||
|
mapTransformProps={this.mapTransformProps}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
hoveredPoint={this.props.designer.hoveredPoint}
|
||||||
|
visible={!!this.props.showWeeds}
|
||||||
|
spreadVisible={!!this.props.showSpread}
|
||||||
|
currentPoint={this.currentPoint}
|
||||||
|
boxSelected={this.props.designer.selectedPoints}
|
||||||
|
groupSelected={this.groupSelected}
|
||||||
|
interactions={this.interactions("Weed")}
|
||||||
|
weeds={this.props.weeds}
|
||||||
|
animate={this.animate} />
|
||||||
PlantLayer = () => <PlantLayer
|
PlantLayer = () => <PlantLayer
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
|
@ -395,10 +452,11 @@ export class GardenMap extends
|
||||||
hoveredPlant={this.props.hoveredPlant}
|
hoveredPlant={this.props.hoveredPlant}
|
||||||
dragging={!!this.state.isDragging}
|
dragging={!!this.state.isDragging}
|
||||||
editing={this.isEditing}
|
editing={this.isEditing}
|
||||||
boxSelected={this.props.designer.selectedPlants}
|
boxSelected={this.props.designer.selectedPoints}
|
||||||
groupSelected={this.pointsSelectedByGroup.map(point => point.uuid)}
|
groupSelected={this.groupSelected}
|
||||||
zoomLvl={this.props.zoomLvl}
|
zoomLvl={this.props.zoomLvl}
|
||||||
activeDragXY={this.state.activeDragXY}
|
activeDragXY={this.state.activeDragXY}
|
||||||
|
interactions={this.interactions("Plant")}
|
||||||
animate={this.animate} />
|
animate={this.animate} />
|
||||||
ToolSlotLayer = () => <ToolSlotLayer
|
ToolSlotLayer = () => <ToolSlotLayer
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
|
@ -406,6 +464,7 @@ export class GardenMap extends
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
hoveredToolSlot={this.props.designer.hoveredToolSlot}
|
hoveredToolSlot={this.props.designer.hoveredToolSlot}
|
||||||
botPositionX={this.props.botLocationData.position.x}
|
botPositionX={this.props.botLocationData.position.x}
|
||||||
|
interactions={this.interactions("ToolSlot")}
|
||||||
slots={this.props.toolSlots} />
|
slots={this.props.toolSlots} />
|
||||||
FarmBotLayer = () => <FarmBotLayer
|
FarmBotLayer = () => <FarmBotLayer
|
||||||
mapTransformProps={this.mapTransformProps}
|
mapTransformProps={this.mapTransformProps}
|
||||||
|
@ -443,8 +502,10 @@ export class GardenMap extends
|
||||||
chosenLocation={this.props.designer.chosenLocation}
|
chosenLocation={this.props.designer.chosenLocation}
|
||||||
mapTransformProps={this.mapTransformProps} />
|
mapTransformProps={this.mapTransformProps} />
|
||||||
DrawnPoint = () => <DrawnPoint
|
DrawnPoint = () => <DrawnPoint
|
||||||
data={this.props.designer.currentPoint}
|
data={this.props.designer.drawnPoint}
|
||||||
key={"currentPoint"}
|
mapTransformProps={this.mapTransformProps} />
|
||||||
|
DrawnWeed = () => <DrawnWeed
|
||||||
|
data={this.props.designer.drawnWeed}
|
||||||
mapTransformProps={this.mapTransformProps} />
|
mapTransformProps={this.mapTransformProps} />
|
||||||
GroupOrder = () => <GroupOrder
|
GroupOrder = () => <GroupOrder
|
||||||
group={this.group}
|
group={this.group}
|
||||||
|
@ -468,6 +529,7 @@ export class GardenMap extends
|
||||||
<this.SensorReadingsLayer />
|
<this.SensorReadingsLayer />
|
||||||
<this.SpreadLayer />
|
<this.SpreadLayer />
|
||||||
<this.PointLayer />
|
<this.PointLayer />
|
||||||
|
<this.WeedLayer />
|
||||||
<this.PlantLayer />
|
<this.PlantLayer />
|
||||||
<this.ToolSlotLayer />
|
<this.ToolSlotLayer />
|
||||||
<this.FarmBotLayer />
|
<this.FarmBotLayer />
|
||||||
|
@ -476,6 +538,7 @@ export class GardenMap extends
|
||||||
<this.SelectionBox />
|
<this.SelectionBox />
|
||||||
<this.TargetCoordinate />
|
<this.TargetCoordinate />
|
||||||
<this.DrawnPoint />
|
<this.DrawnPoint />
|
||||||
|
<this.DrawnWeed />
|
||||||
<this.GroupOrder />
|
<this.GroupOrder />
|
||||||
<this.NNPath />
|
<this.NNPath />
|
||||||
<this.Bugs />
|
<this.Bugs />
|
||||||
|
|
|
@ -2,9 +2,12 @@ import {
|
||||||
TaggedPlantPointer,
|
TaggedPlantPointer,
|
||||||
TaggedGenericPointer,
|
TaggedGenericPointer,
|
||||||
TaggedPlantTemplate,
|
TaggedPlantTemplate,
|
||||||
|
TaggedWeedPointer,
|
||||||
} from "farmbot";
|
} from "farmbot";
|
||||||
import { State, BotOriginQuadrant } from "../interfaces";
|
import { State, BotOriginQuadrant } from "../interfaces";
|
||||||
import { BotPosition, BotLocationData } from "../../devices/interfaces";
|
import {
|
||||||
|
BotPosition, BotLocationData, ShouldDisplay,
|
||||||
|
} from "../../devices/interfaces";
|
||||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||||
import { TimeSettings } from "../../interfaces";
|
import { TimeSettings } from "../../interfaces";
|
||||||
import { UUID } from "../../resources/interfaces";
|
import { UUID } from "../../resources/interfaces";
|
||||||
|
@ -22,9 +25,10 @@ export interface PlantLayerProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
zoomLvl: number;
|
zoomLvl: number;
|
||||||
activeDragXY: BotPosition | undefined;
|
activeDragXY: BotPosition | undefined;
|
||||||
boxSelected: string[] | undefined;
|
boxSelected: UUID[] | undefined;
|
||||||
groupSelected: UUID[];
|
groupSelected: UUID[];
|
||||||
animate: boolean;
|
animate: boolean;
|
||||||
|
interactions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GardenMapLegendProps {
|
export interface GardenMapLegendProps {
|
||||||
|
@ -33,6 +37,7 @@ export interface GardenMapLegendProps {
|
||||||
legendMenuOpen: boolean;
|
legendMenuOpen: boolean;
|
||||||
showPlants: boolean;
|
showPlants: boolean;
|
||||||
showPoints: boolean;
|
showPoints: boolean;
|
||||||
|
showWeeds: boolean;
|
||||||
showSpread: boolean;
|
showSpread: boolean;
|
||||||
showFarmbot: boolean;
|
showFarmbot: boolean;
|
||||||
showImages: boolean;
|
showImages: boolean;
|
||||||
|
@ -45,6 +50,7 @@ export interface GardenMapLegendProps {
|
||||||
imageAgeInfo: { newestDate: string, toOldest: number };
|
imageAgeInfo: { newestDate: string, toOldest: number };
|
||||||
gardenId?: number;
|
gardenId?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
shouldDisplay: ShouldDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapTransformProps = {
|
export type MapTransformProps = {
|
||||||
|
@ -80,6 +86,17 @@ export interface GardenPointProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GardenWeedProps {
|
||||||
|
mapTransformProps: MapTransformProps;
|
||||||
|
weed: TaggedWeedPointer;
|
||||||
|
hovered: boolean;
|
||||||
|
current: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
animate: boolean;
|
||||||
|
spreadVisible: boolean;
|
||||||
|
dispatch: Function;
|
||||||
|
}
|
||||||
|
|
||||||
interface DragHelpersBaseProps {
|
interface DragHelpersBaseProps {
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
|
@ -152,7 +169,9 @@ export enum Mode {
|
||||||
addPlant = "addPlant",
|
addPlant = "addPlant",
|
||||||
moveTo = "moveTo",
|
moveTo = "moveTo",
|
||||||
points = "points",
|
points = "points",
|
||||||
|
weeds = "weeds",
|
||||||
createPoint = "createPoint",
|
createPoint = "createPoint",
|
||||||
|
createWeed = "createWeed",
|
||||||
templateView = "templateView",
|
templateView = "templateView",
|
||||||
editGroup = "editGroup",
|
editGroup = "editGroup",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
import * as React from "react";
|
jest.mock("../../../../../api/crud", () => ({
|
||||||
import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu";
|
edit: jest.fn(),
|
||||||
import { shallow, mount } from "enzyme";
|
save: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fakeWebAppConfig,
|
fakeWebAppConfig,
|
||||||
} from "../../../../../__test_support__/fake_state/resources";
|
} 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();
|
const mockConfig = fakeWebAppConfig();
|
||||||
jest.mock("../../../../../resources/selectors", () => ({
|
jest.mock("../../../../../resources/selectors", () => ({
|
||||||
getWebAppConfig: () => mockConfig,
|
getWebAppConfig: () => mockConfig,
|
||||||
assertUuid: jest.fn(),
|
assertUuid: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../../../../../config_storage/actions", () => ({
|
import * as React from "react";
|
||||||
setWebAppConfigValue: jest.fn(),
|
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 />", () => {
|
describe("<ImageFilterMenu />", () => {
|
||||||
mockConfig.body.photo_filter_begin = "";
|
mockConfig.body.photo_filter_begin = "";
|
||||||
|
@ -45,13 +51,19 @@ describe("<ImageFilterMenu />", () => {
|
||||||
["endDate", "photo_filter_end", 2],
|
["endDate", "photo_filter_end", 2],
|
||||||
])("sets filter: %s", (filter, key, i) => {
|
])("sets filter: %s", (filter, key, i) => {
|
||||||
const p = fakeProps();
|
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} />);
|
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||||
currentTarget: { value: "2001-01-03" }
|
currentTarget: { value: "2001-01-03" }
|
||||||
});
|
});
|
||||||
expect(wrapper.instance().state[filter]).toEqual("2001-01-03");
|
expect(wrapper.instance().state[filter]).toEqual("2001-01-03");
|
||||||
expect(setWebAppConfigValue)
|
expect(edit).toHaveBeenCalledWith(config, {
|
||||||
.toHaveBeenCalledWith(key, "2001-01-03T00:00:00.000Z");
|
[key]: "2001-01-03T00:00:00.000Z"
|
||||||
|
});
|
||||||
|
expect(save).toHaveBeenCalledWith(config.uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each<[
|
it.each<[
|
||||||
|
@ -61,14 +73,64 @@ describe("<ImageFilterMenu />", () => {
|
||||||
["endTime", "photo_filter_end", 3],
|
["endTime", "photo_filter_end", 3],
|
||||||
])("sets filter: %s", (filter, key, i) => {
|
])("sets filter: %s", (filter, key, i) => {
|
||||||
const p = fakeProps();
|
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} />);
|
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||||
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
|
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
|
||||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||||
currentTarget: { value: "05:00" }
|
currentTarget: { value: "05:00" }
|
||||||
});
|
});
|
||||||
expect(wrapper.instance().state[filter]).toEqual("05:00");
|
expect(wrapper.instance().state[filter]).toEqual("05:00");
|
||||||
expect(setWebAppConfigValue)
|
expect(edit).toHaveBeenCalledWith(config, {
|
||||||
.toHaveBeenCalledWith(key, "2001-01-03T05:00:00.000Z");
|
[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", () => {
|
it("loads values from config", () => {
|
||||||
|
@ -83,14 +145,34 @@ describe("<ImageFilterMenu />", () => {
|
||||||
|
|
||||||
it("changes slider", () => {
|
it("changes slider", () => {
|
||||||
const p = fakeProps();
|
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";
|
p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z";
|
||||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||||
wrapper.instance().sliderChange(1);
|
wrapper.instance().sliderChange(1);
|
||||||
expect(wrapper.instance().state.slider).toEqual(1);
|
expect(wrapper.instance().state.slider).toEqual(1);
|
||||||
expect(setWebAppConfigValue)
|
expect(edit).toHaveBeenCalledWith(config, {
|
||||||
.toHaveBeenCalledWith("photo_filter_begin", "2001-01-02T00:00:00.000Z");
|
photo_filter_begin: "2001-01-02T00:00:00.000Z",
|
||||||
expect(setWebAppConfigValue)
|
photo_filter_end: "2001-01-03T00:00:00.000Z",
|
||||||
.toHaveBeenCalledWith("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", () => {
|
it("displays slider labels", () => {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { BlurableInput } from "../../../../ui/index";
|
import { BlurableInput } from "../../../../ui/index";
|
||||||
import { offsetTime } from "../../../farm_events/edit_fe_form";
|
import { offsetTime } from "../../../farm_events/edit_fe_form";
|
||||||
import {
|
import { GetWebAppConfigValue } from "../../../../config_storage/actions";
|
||||||
setWebAppConfigValue, GetWebAppConfigValue,
|
|
||||||
} from "../../../../config_storage/actions";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {
|
import {
|
||||||
formatDate, formatTime,
|
formatDate, formatTime,
|
||||||
|
@ -11,8 +9,13 @@ import {
|
||||||
import { Slider } from "@blueprintjs/core";
|
import { Slider } from "@blueprintjs/core";
|
||||||
import { t } from "../../../../i18next_wrapper";
|
import { t } from "../../../../i18next_wrapper";
|
||||||
import { TimeSettings } from "../../../../interfaces";
|
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;
|
beginDate: string | undefined;
|
||||||
beginTime: string | undefined;
|
beginTime: string | undefined;
|
||||||
endDate: string | undefined;
|
endDate: string | undefined;
|
||||||
|
@ -20,6 +23,8 @@ interface ImageFilterMenuState {
|
||||||
slider: number;
|
slider: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageFilterMenuState = Partial<FullImageFilterMenuState>;
|
||||||
|
|
||||||
export interface ImageFilterMenuProps {
|
export interface ImageFilterMenuProps {
|
||||||
timeSettings: TimeSettings;
|
timeSettings: TimeSettings;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
@ -28,26 +33,48 @@ export interface ImageFilterMenuProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageFilterMenu
|
export class ImageFilterMenu
|
||||||
extends React.Component<ImageFilterMenuProps, Partial<ImageFilterMenuState>> {
|
extends React.Component<ImageFilterMenuProps, ImageFilterMenuState> {
|
||||||
constructor(props: ImageFilterMenuProps) {
|
state: ImageFilterMenuState = {};
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillMount() {
|
componentDidMount() {
|
||||||
const { newestDate, toOldest } = this.props.imageAgeInfo;
|
|
||||||
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
|
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
|
||||||
this.setState({
|
if (isString(beginDatetime) || isUndefined(beginDatetime)) {
|
||||||
slider: toOldest + 1 - (beginDatetime
|
this.updateSliderState(beginDatetime);
|
||||||
? Math.abs(moment(beginDatetime.toString())
|
}
|
||||||
.diff(moment(newestDate).clone(), "days")) : 0)
|
|
||||||
});
|
|
||||||
this.updateState();
|
this.updateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps() {
|
updateSliderState = (begin: string | undefined) => {
|
||||||
this.updateState();
|
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 = () => {
|
updateState = () => {
|
||||||
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
|
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
|
||||||
|
@ -70,27 +97,27 @@ export class ImageFilterMenu
|
||||||
const input = e.currentTarget.value;
|
const input = e.currentTarget.value;
|
||||||
this.setState({ [datetime]: input });
|
this.setState({ [datetime]: input });
|
||||||
const { beginDate, beginTime, endDate, endTime } = this.state;
|
const { beginDate, beginTime, endDate, endTime } = this.state;
|
||||||
const { dispatch, timeSettings } = this.props;
|
const { timeSettings } = this.props;
|
||||||
let value = undefined;
|
let value = undefined;
|
||||||
switch (datetime) {
|
switch (datetime) {
|
||||||
case "beginDate":
|
case "beginDate":
|
||||||
value = offsetTime(input, beginTime || "00:00", timeSettings);
|
value = offsetTime(input, beginTime || "00:00", timeSettings);
|
||||||
dispatch(setWebAppConfigValue("photo_filter_begin", value));
|
this.setValues({ photo_filter_begin: value });
|
||||||
break;
|
break;
|
||||||
case "beginTime":
|
case "beginTime":
|
||||||
if (beginDate) {
|
if (beginDate) {
|
||||||
value = offsetTime(beginDate, input, timeSettings);
|
value = offsetTime(beginDate, input, timeSettings);
|
||||||
dispatch(setWebAppConfigValue("photo_filter_begin", value));
|
this.setValues({ photo_filter_begin: value });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "endDate":
|
case "endDate":
|
||||||
value = offsetTime(input, endTime || "00:00", timeSettings);
|
value = offsetTime(input, endTime || "00:00", timeSettings);
|
||||||
dispatch(setWebAppConfigValue("photo_filter_end", value));
|
this.setValues({ photo_filter_end: value });
|
||||||
break;
|
break;
|
||||||
case "endTime":
|
case "endTime":
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
value = offsetTime(endDate, input, timeSettings);
|
value = offsetTime(endDate, input, timeSettings);
|
||||||
dispatch(setWebAppConfigValue("photo_filter_end", value));
|
this.setValues({ photo_filter_end: value });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -100,13 +127,12 @@ export class ImageFilterMenu
|
||||||
sliderChange = (slider: number) => {
|
sliderChange = (slider: number) => {
|
||||||
const { newestDate, toOldest } = this.props.imageAgeInfo;
|
const { newestDate, toOldest } = this.props.imageAgeInfo;
|
||||||
this.setState({ slider });
|
this.setState({ slider });
|
||||||
const { dispatch, timeSettings } = this.props;
|
const { timeSettings } = this.props;
|
||||||
const calcDate = (day: number) =>
|
const calcDate = (day: number) =>
|
||||||
moment(newestDate).subtract(toOldest - day, "days").toISOString();
|
moment(newestDate).subtract(toOldest - day, "days").toISOString();
|
||||||
const begin = offsetTime(calcDate(slider - 1), "00:00", timeSettings);
|
const begin = offsetTime(calcDate(slider - 1), "00:00", timeSettings);
|
||||||
const end = offsetTime(calcDate(slider), "00:00", timeSettings);
|
const end = offsetTime(calcDate(slider), "00:00", timeSettings);
|
||||||
dispatch(setWebAppConfigValue("photo_filter_begin", begin));
|
this.setValues({ photo_filter_begin: begin, photo_filter_end: end });
|
||||||
dispatch(setWebAppConfigValue("photo_filter_end", end));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLabel = (day: number) => {
|
renderLabel = (day: number) => {
|
||||||
|
@ -191,3 +217,14 @@ export class ImageFilterMenu
|
||||||
</div>;
|
</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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from "./farmbot/farmbot_layer";
|
export * from "./farmbot/farmbot_layer";
|
||||||
export * from "./plants/plant_layer";
|
export * from "./plants/plant_layer";
|
||||||
export * from "./points/point_layer";
|
export * from "./points/point_layer";
|
||||||
|
export * from "./weeds/weed_layer";
|
||||||
export * from "./spread/spread_layer";
|
export * from "./spread/spread_layer";
|
||||||
export * from "./tool_slots/tool_slot_layer";
|
export * from "./tool_slots/tool_slot_layer";
|
||||||
export * from "./images/image_layer";
|
export * from "./images/image_layer";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
let mockPath = "/app/designer/plants";
|
let mockPath = "/app/designer/plants";
|
||||||
jest.mock("../../../../../history", () => ({
|
jest.mock("../../../../../history", () => ({
|
||||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
getPathArray: jest.fn(() => mockPath.split("/"))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -31,6 +31,7 @@ describe("<PlantLayer/>", () => {
|
||||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||||
animate: true,
|
animate: true,
|
||||||
hoveredPlant: undefined,
|
hoveredPlant: undefined,
|
||||||
|
interactions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows plants", () => {
|
it("shows plants", () => {
|
||||||
|
@ -59,14 +60,19 @@ describe("<PlantLayer/>", () => {
|
||||||
it("is in clickable mode", () => {
|
it("is in clickable mode", () => {
|
||||||
mockPath = "/app/designer/plants";
|
mockPath = "/app/designer/plants";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = true;
|
||||||
|
p.plants[0].body.id = 1;
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||||
expect(wrapper.find("Link").props().style).toEqual({});
|
expect(wrapper.find("Link").props().style).toEqual({
|
||||||
|
cursor: "pointer"
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is in non-clickable mode", () => {
|
it("is in non-clickable mode", () => {
|
||||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
|
p.plants[0].body.id = 1;
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||||
expect(wrapper.find("Link").props().style)
|
expect(wrapper.find("Link").props().style)
|
||||||
.toEqual({ pointerEvents: "none" });
|
.toEqual({ pointerEvents: "none" });
|
||||||
|
@ -111,22 +117,12 @@ describe("<PlantLayer/>", () => {
|
||||||
expect(wrapper.find("GardenPlant").props().selected).toEqual(true);
|
expect(wrapper.find("GardenPlant").props().selected).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows clicking of unsaved plants", () => {
|
|
||||||
const p = fakeProps();
|
|
||||||
const plant = fakePlant();
|
|
||||||
plant.body.id = 1;
|
|
||||||
p.plants = [plant];
|
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
|
||||||
expect((wrapper.find("Link").props()).style).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't allow clicking of unsaved plants", () => {
|
it("doesn't allow clicking of unsaved plants", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const plant = fakePlant();
|
p.interactions = false;
|
||||||
plant.body.id = 0;
|
p.plants[0].body.id = 0;
|
||||||
p.plants = [plant];
|
|
||||||
const wrapper = svgMount(<PlantLayer {...p} />);
|
const wrapper = svgMount(<PlantLayer {...p} />);
|
||||||
expect((wrapper.find("Link").props()).style)
|
expect(wrapper.find("Link").props().style)
|
||||||
.toEqual({ pointerEvents: "none" });
|
.toEqual({ pointerEvents: "none" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||||
import { GardenPlant } from "./garden_plant";
|
import { GardenPlant } from "./garden_plant";
|
||||||
import { PlantLayerProps, Mode } from "../../interfaces";
|
import { PlantLayerProps, Mode } from "../../interfaces";
|
||||||
import { unpackUUID } from "../../../../util";
|
import { unpackUUID } from "../../../../util";
|
||||||
import { maybeNoPointer, getMode } from "../../util";
|
import { getMode } from "../../util";
|
||||||
import { Link } from "../../../../link";
|
import { Link } from "../../../../link";
|
||||||
|
|
||||||
export function PlantLayer(props: PlantLayerProps) {
|
export function PlantLayer(props: PlantLayerProps) {
|
||||||
|
@ -44,9 +44,12 @@ export function PlantLayer(props: PlantLayerProps) {
|
||||||
activeDragXY={activeDragXY}
|
activeDragXY={activeDragXY}
|
||||||
hovered={hovered}
|
hovered={hovered}
|
||||||
animate={animate} />;
|
animate={animate} />;
|
||||||
|
const style: React.SVGProps<SVGGElement>["style"] =
|
||||||
|
(props.interactions && p.body.id)
|
||||||
|
? { cursor: "pointer" } : { pointerEvents: "none" };
|
||||||
const wrapperProps = {
|
const wrapperProps = {
|
||||||
className: "plant-link-wrapper",
|
className: "plant-link-wrapper",
|
||||||
style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
|
style,
|
||||||
key: p.uuid,
|
key: p.uuid,
|
||||||
};
|
};
|
||||||
return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect)
|
return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect)
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
|
jest.mock("../../../../../history", () => ({
|
||||||
|
history: { push: jest.fn() },
|
||||||
|
getPathArray: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { GardenPoint } from "../garden_point";
|
import { GardenPoint } from "../garden_point";
|
||||||
|
@ -55,10 +58,9 @@ describe("<GardenPoint/>", () => {
|
||||||
|
|
||||||
it("opens point info", () => {
|
it("opens point info", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.point.body.name = "weed";
|
|
||||||
const wrapper = svgMount(<GardenPoint {...p} />);
|
const wrapper = svgMount(<GardenPoint {...p} />);
|
||||||
wrapper.find("g").simulate("click");
|
wrapper.find("g").simulate("click");
|
||||||
expect(history.push).toHaveBeenCalledWith(
|
expect(history.push).toHaveBeenCalledWith(
|
||||||
`/app/designer/weeds/${p.point.body.id}`);
|
`/app/designer/points/${p.point.body.id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,10 +19,12 @@ describe("<PointLayer/>", () => {
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
hoveredPoint: undefined,
|
hoveredPoint: undefined,
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
|
interactions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows points", () => {
|
it("shows points", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
const wrapper = svgMount(<PointLayer {...p} />);
|
const wrapper = svgMount(<PointLayer {...p} />);
|
||||||
const layer = wrapper.find("#point-layer");
|
const layer = wrapper.find("#point-layer");
|
||||||
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
|
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
|
||||||
|
@ -40,6 +42,7 @@ describe("<PointLayer/>", () => {
|
||||||
it("allows point mode interaction", () => {
|
it("allows point mode interaction", () => {
|
||||||
mockPath = "/app/designer/points";
|
mockPath = "/app/designer/points";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = true;
|
||||||
const wrapper = svgMount(<PointLayer {...p} />);
|
const wrapper = svgMount(<PointLayer {...p} />);
|
||||||
const layer = wrapper.find("#point-layer");
|
const layer = wrapper.find("#point-layer");
|
||||||
expect(layer.props().style).toEqual({});
|
expect(layer.props().style).toEqual({});
|
||||||
|
|
|
@ -2,8 +2,7 @@ import * as React from "react";
|
||||||
import { GardenPointProps } from "../../interfaces";
|
import { GardenPointProps } from "../../interfaces";
|
||||||
import { transformXY } from "../../util";
|
import { transformXY } from "../../util";
|
||||||
import { Actions } from "../../../../constants";
|
import { Actions } from "../../../../constants";
|
||||||
import { history } from "../../../../history";
|
import { mapPointClickAction } from "../../actions";
|
||||||
import { isAWeed } from "../../../points/weeds_inventory";
|
|
||||||
|
|
||||||
export const GardenPoint = (props: GardenPointProps) => {
|
export const GardenPoint = (props: GardenPointProps) => {
|
||||||
|
|
||||||
|
@ -19,11 +18,11 @@ export const GardenPoint = (props: GardenPointProps) => {
|
||||||
const { id, x, y, meta } = point.body;
|
const { id, x, y, meta } = point.body;
|
||||||
const { qx, qy } = transformXY(x, y, mapTransformProps);
|
const { qx, qy } = transformXY(x, y, mapTransformProps);
|
||||||
const color = meta.color || "green";
|
const color = meta.color || "green";
|
||||||
const panel = isAWeed(point.body.name, meta.type) ? "weeds" : "points";
|
return <g id={`point-${id}`} className={"map-point"} stroke={color}
|
||||||
return <g id={"point-" + id} className={"map-point"} stroke={color}
|
|
||||||
onMouseEnter={iconHover("start")}
|
onMouseEnter={iconHover("start")}
|
||||||
onMouseLeave={iconHover("end")}
|
onMouseLeave={iconHover("end")}
|
||||||
onClick={() => history.push(`/app/designer/${panel}/${id}`)}>
|
onClick={mapPointClickAction(props.dispatch, point.uuid,
|
||||||
|
`/app/designer/points/${id}`)}>
|
||||||
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius}
|
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius}
|
||||||
fill={hovered ? color : "transparent"} />
|
fill={hovered ? color : "transparent"} />
|
||||||
<circle id="point-center" cx={qx} cy={qy} r={2} />
|
<circle id="point-center" cx={qx} cy={qy} r={2} />
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { TaggedGenericPointer } from "farmbot";
|
import { TaggedGenericPointer } from "farmbot";
|
||||||
import { GardenPoint } from "./garden_point";
|
import { GardenPoint } from "./garden_point";
|
||||||
import { MapTransformProps, Mode } from "../../interfaces";
|
import { MapTransformProps } from "../../interfaces";
|
||||||
import { getMode } from "../../util";
|
|
||||||
|
|
||||||
export interface PointLayerProps {
|
export interface PointLayerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -10,13 +9,14 @@ export interface PointLayerProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
hoveredPoint: string | undefined;
|
hoveredPoint: string | undefined;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
interactions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PointLayer(props: PointLayerProps) {
|
export function PointLayer(props: PointLayerProps) {
|
||||||
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
|
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
|
||||||
const style: React.CSSProperties =
|
const style: React.CSSProperties =
|
||||||
getMode() === Mode.points ? {} : { pointerEvents: "none" };
|
props.interactions ? {} : { pointerEvents: "none" };
|
||||||
return <g id="point-layer" style={style}>
|
return <g id={"point-layer"} style={style}>
|
||||||
{visible &&
|
{visible &&
|
||||||
genericPoints.map(p =>
|
genericPoints.map(p =>
|
||||||
<GardenPoint
|
<GardenPoint
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class SpreadCircle extends
|
||||||
React.Component<SpreadCircleProps, SpreadCircleState> {
|
React.Component<SpreadCircleProps, SpreadCircleState> {
|
||||||
state: SpreadCircleState = { spread: undefined };
|
state: SpreadCircleState = { spread: undefined };
|
||||||
|
|
||||||
UNSAFE_componentWillMount = () => {
|
componentDidMount = () => {
|
||||||
cachedCrop(this.props.plant.body.openfarm_slug)
|
cachedCrop(this.props.plant.body.openfarm_slug)
|
||||||
.then(({ spread }) => this.setState({ spread }));
|
.then(({ spread }) => this.setState({ spread }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ describe("<ToolSlotLayer/>", () => {
|
||||||
pointer_type: "ToolSlot",
|
pointer_type: "ToolSlot",
|
||||||
tool_id: undefined,
|
tool_id: undefined,
|
||||||
name: "Name",
|
name: "Name",
|
||||||
radius: 50,
|
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 2,
|
y: 2,
|
||||||
z: 3,
|
z: 3,
|
||||||
|
@ -38,6 +37,7 @@ describe("<ToolSlotLayer/>", () => {
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
hoveredToolSlot: undefined,
|
hoveredToolSlot: undefined,
|
||||||
|
interactions: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
it("toggles visibility off", () => {
|
it("toggles visibility off", () => {
|
||||||
|
@ -61,9 +61,19 @@ describe("<ToolSlotLayer/>", () => {
|
||||||
expect(history.push).not.toHaveBeenCalled();
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("is in clickable mode", () => {
|
||||||
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
|
const p = fakeProps();
|
||||||
|
p.interactions = true;
|
||||||
|
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||||
|
expect(wrapper.find("g").props().style)
|
||||||
|
.toEqual({ cursor: "pointer" });
|
||||||
|
});
|
||||||
|
|
||||||
it("is in non-clickable mode", () => {
|
it("is in non-clickable mode", () => {
|
||||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||||
expect(wrapper.find("g").props().style)
|
expect(wrapper.find("g").props().style)
|
||||||
.toEqual({ pointerEvents: "none" });
|
.toEqual({ pointerEvents: "none" });
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
|
jest.mock("../../../../../history", () => ({
|
||||||
|
history: { push: jest.fn() },
|
||||||
|
getPathArray: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
|
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
|
||||||
|
|
|
@ -2,7 +2,6 @@ import * as React from "react";
|
||||||
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
|
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
|
||||||
import { ToolSlotPoint } from "./tool_slot_point";
|
import { ToolSlotPoint } from "./tool_slot_point";
|
||||||
import { MapTransformProps } from "../../interfaces";
|
import { MapTransformProps } from "../../interfaces";
|
||||||
import { maybeNoPointer } from "../../util";
|
|
||||||
|
|
||||||
export interface ToolSlotLayerProps {
|
export interface ToolSlotLayerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -11,6 +10,7 @@ export interface ToolSlotLayerProps {
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
hoveredToolSlot: UUID | undefined;
|
hoveredToolSlot: UUID | undefined;
|
||||||
|
interactions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
||||||
|
@ -18,7 +18,9 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
||||||
|
|
||||||
return <g
|
return <g
|
||||||
id="toolslot-layer"
|
id="toolslot-layer"
|
||||||
style={maybeNoPointer({ cursor: "pointer" })}>
|
style={props.interactions
|
||||||
|
? { cursor: "pointer" }
|
||||||
|
: { pointerEvents: "none" }}>
|
||||||
{visible &&
|
{visible &&
|
||||||
slots.map(slot =>
|
slots.map(slot =>
|
||||||
<ToolSlotPoint
|
<ToolSlotPoint
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { MapTransformProps } from "../../interfaces";
|
||||||
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
|
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
|
||||||
import { ToolLabel } from "./tool_label";
|
import { ToolLabel } from "./tool_label";
|
||||||
import { includes } from "lodash";
|
import { includes } from "lodash";
|
||||||
import { history } from "../../../../history";
|
|
||||||
import { t } from "../../../../i18next_wrapper";
|
import { t } from "../../../../i18next_wrapper";
|
||||||
|
import { mapPointClickAction } from "../../actions";
|
||||||
|
|
||||||
export interface TSPProps {
|
export interface TSPProps {
|
||||||
slot: SlotWithTool;
|
slot: SlotWithTool;
|
||||||
|
@ -30,25 +30,27 @@ export const reduceToolName = (raw: string | undefined) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolSlotPoint = (props: TSPProps) => {
|
export const ToolSlotPoint = (props: TSPProps) => {
|
||||||
|
const { tool, toolSlot } = props.slot;
|
||||||
const {
|
const {
|
||||||
id, x, y, pullout_direction, gantry_mounted
|
id, x, y, pullout_direction, gantry_mounted
|
||||||
} = props.slot.toolSlot.body;
|
} = toolSlot.body;
|
||||||
const { mapTransformProps, botPositionX } = props;
|
const { mapTransformProps, botPositionX } = props;
|
||||||
const { quadrant, xySwap } = mapTransformProps;
|
const { quadrant, xySwap } = mapTransformProps;
|
||||||
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
|
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
|
||||||
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
|
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
|
||||||
const toolName = props.slot.tool ? props.slot.tool.body.name : t("Empty");
|
const toolName = tool ? tool.body.name : t("Empty");
|
||||||
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
|
const hovered = toolSlot.uuid === props.hoveredToolSlot;
|
||||||
const toolProps = {
|
const toolProps = {
|
||||||
x: qx,
|
x: qx,
|
||||||
y: qy,
|
y: qy,
|
||||||
hovered,
|
hovered,
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
uuid: props.slot.toolSlot.uuid,
|
uuid: toolSlot.uuid,
|
||||||
xySwap,
|
xySwap,
|
||||||
};
|
};
|
||||||
return <g id={"toolslot-" + id}
|
return <g id={"toolslot-" + id}
|
||||||
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}>
|
onClick={mapPointClickAction(props.dispatch, toolSlot.uuid,
|
||||||
|
`/app/designer/tool-slots/${id}`)}>
|
||||||
{pullout_direction && !gantry_mounted &&
|
{pullout_direction && !gantry_mounted &&
|
||||||
<ToolbaySlot
|
<ToolbaySlot
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
jest.mock("../../../../../history", () => ({
|
||||||
|
history: { push: jest.fn() },
|
||||||
|
getPathArray: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { GardenWeed } from "../garden_weed";
|
||||||
|
import { GardenWeedProps } from "../../../interfaces";
|
||||||
|
import { fakeWeed } from "../../../../../__test_support__/fake_state/resources";
|
||||||
|
import {
|
||||||
|
fakeMapTransformProps,
|
||||||
|
} from "../../../../../__test_support__/map_transform_props";
|
||||||
|
import { Actions } from "../../../../../constants";
|
||||||
|
import { history } from "../../../../../history";
|
||||||
|
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||||
|
|
||||||
|
describe("<GardenWeed />", () => {
|
||||||
|
const fakeProps = (): GardenWeedProps => ({
|
||||||
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
weed: fakeWeed(),
|
||||||
|
hovered: false,
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
current: false,
|
||||||
|
selected: false,
|
||||||
|
animate: false,
|
||||||
|
spreadVisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.weed.body.meta.color = undefined;
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
expect(wrapper.find("#weed-radius").props().r).toEqual(100);
|
||||||
|
expect(wrapper.find("#weed-radius").props().opacity).toEqual(0.5);
|
||||||
|
expect(wrapper.find("stop").first().props().stopColor).toEqual("red");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders weed color", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.weed.body.meta.color = "orange";
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
expect(wrapper.find("#weed-radius").props().r).toEqual(100);
|
||||||
|
expect(wrapper.find("#weed-radius").props().opacity).toEqual(0.5);
|
||||||
|
expect(wrapper.find("stop").first().props().stopColor).toEqual("orange");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("animates", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.animate = true;
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
expect(wrapper.find(".soil-cloud").length).toEqual(1);
|
||||||
|
expect(wrapper.find("image").hasClass("animate")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovers weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
wrapper.find("g").first().simulate("mouseEnter");
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.TOGGLE_HOVERED_POINT,
|
||||||
|
payload: p.weed.uuid
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is hovered", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.hovered = true;
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
expect(wrapper.find("#weed-radius").props().opacity).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("un-hovers weed", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
wrapper.find("g").first().simulate("mouseLeave");
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.TOGGLE_HOVERED_POINT,
|
||||||
|
payload: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens weed info", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = svgMount(<GardenWeed {...p} />);
|
||||||
|
wrapper.find("g").first().simulate("click");
|
||||||
|
expect(history.push).toHaveBeenCalledWith(
|
||||||
|
`/app/designer/weeds/${p.weed.body.id}`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,66 @@
|
||||||
|
let mockPath = "/app/designer/plants";
|
||||||
|
jest.mock("../../../../../history", () => ({
|
||||||
|
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { WeedLayer, WeedLayerProps } from "../weed_layer";
|
||||||
|
import { fakeWeed } from "../../../../../__test_support__/fake_state/resources";
|
||||||
|
import {
|
||||||
|
fakeMapTransformProps,
|
||||||
|
} from "../../../../../__test_support__/map_transform_props";
|
||||||
|
import { GardenWeed } from "../garden_weed";
|
||||||
|
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||||
|
|
||||||
|
describe("<WeedLayer/>", () => {
|
||||||
|
const fakeProps = (): WeedLayerProps => ({
|
||||||
|
visible: true,
|
||||||
|
spreadVisible: true,
|
||||||
|
weeds: [fakeWeed()],
|
||||||
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
hoveredPoint: undefined,
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
currentPoint: undefined,
|
||||||
|
boxSelected: undefined,
|
||||||
|
groupSelected: [],
|
||||||
|
animate: false,
|
||||||
|
interactions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows weeds", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.interactions = false;
|
||||||
|
const wrapper = svgMount(<WeedLayer {...p} />);
|
||||||
|
const layer = wrapper.find("#weeds-layer");
|
||||||
|
expect(layer.find(GardenWeed).html()).toContain("r=\"100\"");
|
||||||
|
expect(layer.props().style).toEqual({ pointerEvents: "none" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles visibility off", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.visible = false;
|
||||||
|
const wrapper = svgMount(<WeedLayer {...p} />);
|
||||||
|
const layer = wrapper.find("#weeds-layer");
|
||||||
|
expect(layer.find(GardenWeed).length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows weed mode interaction", () => {
|
||||||
|
mockPath = "/app/designer/weeds";
|
||||||
|
const p = fakeProps();
|
||||||
|
p.interactions = true;
|
||||||
|
const wrapper = svgMount(<WeedLayer {...p} />);
|
||||||
|
const layer = wrapper.find("#weeds-layer");
|
||||||
|
expect(layer.props().style).toEqual({ cursor: "pointer" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is selected", () => {
|
||||||
|
mockPath = "/app/designer/weeds";
|
||||||
|
const p = fakeProps();
|
||||||
|
const weed = fakeWeed();
|
||||||
|
p.weeds = [weed];
|
||||||
|
p.boxSelected = [weed.uuid];
|
||||||
|
const wrapper = svgMount(<WeedLayer {...p} />);
|
||||||
|
const layer = wrapper.find("#weeds-layer");
|
||||||
|
expect(layer.find(GardenWeed).props().selected).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { GardenWeedProps } from "../../interfaces";
|
||||||
|
import { transformXY } from "../../util";
|
||||||
|
import { Actions } from "../../../../constants";
|
||||||
|
import { Color } from "../../../../ui";
|
||||||
|
import { mapPointClickAction } from "../../actions";
|
||||||
|
|
||||||
|
export const DEFAULT_WEED_ICON = "/app-resources/img/generic-weed.svg";
|
||||||
|
|
||||||
|
export const GardenWeed = (props: GardenWeedProps) => {
|
||||||
|
|
||||||
|
const iconHover = (action: "start" | "end") => () => {
|
||||||
|
const hover = action === "start";
|
||||||
|
props.dispatch({
|
||||||
|
type: Actions.TOGGLE_HOVERED_POINT,
|
||||||
|
payload: hover ? props.weed.uuid : undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { weed, mapTransformProps, hovered, current, selected, animate } = props;
|
||||||
|
const { id, x, y, meta, radius } = weed.body;
|
||||||
|
const { qx, qy } = transformXY(x, y, mapTransformProps);
|
||||||
|
const color = meta.color || "red";
|
||||||
|
const stopOpacity = ["gray", "pink", "orange"].includes(color) ? 0.5 : 0.25;
|
||||||
|
const className = [
|
||||||
|
"weed-image", `is-chosen-${current || selected}`, animate ? "animate" : "",
|
||||||
|
].join(" ");
|
||||||
|
const iconRadius = hovered ? radius * 0.88 : radius * 0.8;
|
||||||
|
return <g id={`weed-${id}`} className={`map-weed ${color}`}
|
||||||
|
onMouseEnter={iconHover("start")}
|
||||||
|
onMouseLeave={iconHover("end")}
|
||||||
|
onClick={mapPointClickAction(props.dispatch, weed.uuid,
|
||||||
|
`/app/designer/weeds/${id}`)}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id={`Weed${id}Gradient`}>
|
||||||
|
<stop offset="90%" stopColor={color} stopOpacity={stopOpacity} />
|
||||||
|
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{animate &&
|
||||||
|
<circle
|
||||||
|
className="soil-cloud"
|
||||||
|
cx={qx}
|
||||||
|
cy={qy}
|
||||||
|
r={radius}
|
||||||
|
fill={Color.soilCloud}
|
||||||
|
fillOpacity={0} />}
|
||||||
|
|
||||||
|
{props.spreadVisible &&
|
||||||
|
<circle
|
||||||
|
id={"weed-radius"}
|
||||||
|
cx={qx}
|
||||||
|
cy={qy}
|
||||||
|
r={radius}
|
||||||
|
fill={`url(#Weed${id}Gradient)`}
|
||||||
|
opacity={hovered ? 1 : 0.5} />}
|
||||||
|
|
||||||
|
<g id="weed-icon">
|
||||||
|
<image
|
||||||
|
className={className}
|
||||||
|
xlinkHref={DEFAULT_WEED_ICON}
|
||||||
|
height={iconRadius * 2}
|
||||||
|
width={iconRadius * 2}
|
||||||
|
x={qx - iconRadius}
|
||||||
|
y={qy - iconRadius} />
|
||||||
|
</g>
|
||||||
|
</g>;
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { TaggedWeedPointer } from "farmbot";
|
||||||
|
import { GardenWeed } from "./garden_weed";
|
||||||
|
import { MapTransformProps } from "../../interfaces";
|
||||||
|
import { UUID } from "../../../../resources/interfaces";
|
||||||
|
|
||||||
|
export interface WeedLayerProps {
|
||||||
|
visible: boolean;
|
||||||
|
spreadVisible: boolean;
|
||||||
|
weeds: TaggedWeedPointer[];
|
||||||
|
mapTransformProps: MapTransformProps;
|
||||||
|
hoveredPoint: UUID | undefined;
|
||||||
|
currentPoint: UUID | undefined;
|
||||||
|
boxSelected: UUID[] | undefined;
|
||||||
|
groupSelected: UUID[];
|
||||||
|
dispatch: Function;
|
||||||
|
animate: boolean;
|
||||||
|
interactions: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeedLayer(props: WeedLayerProps) {
|
||||||
|
const { visible, weeds, mapTransformProps } = props;
|
||||||
|
return <g id={"weeds-layer"} style={props.interactions
|
||||||
|
? { cursor: "pointer" } : { pointerEvents: "none" }}>
|
||||||
|
{visible &&
|
||||||
|
weeds.map(p => {
|
||||||
|
const current = p.uuid === props.currentPoint;
|
||||||
|
const hovered = p.uuid === props.hoveredPoint;
|
||||||
|
const selectedByBox = !!props.boxSelected?.includes(p.uuid);
|
||||||
|
const selectedByGroup = props.groupSelected.includes(p.uuid);
|
||||||
|
return <GardenWeed
|
||||||
|
weed={p}
|
||||||
|
key={p.uuid}
|
||||||
|
hovered={hovered}
|
||||||
|
current={current}
|
||||||
|
selected={selectedByBox || selectedByGroup}
|
||||||
|
animate={props.animate}
|
||||||
|
spreadVisible={props.spreadVisible}
|
||||||
|
dispatch={props.dispatch}
|
||||||
|
mapTransformProps={mapTransformProps} />;
|
||||||
|
})}
|
||||||
|
</g>;
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import {
|
import {
|
||||||
fakeMapTransformProps,
|
fakeMapTransformProps,
|
||||||
} from "../../../../../__test_support__/map_transform_props";
|
} from "../../../../../__test_support__/map_transform_props";
|
||||||
|
import { ReactWrapper } from "enzyme";
|
||||||
|
|
||||||
describe("<ZonesLayer />", () => {
|
describe("<ZonesLayer />", () => {
|
||||||
const fakeProps = (): ZonesLayerProps => ({
|
const fakeProps = (): ZonesLayerProps => ({
|
||||||
|
@ -18,6 +19,7 @@ describe("<ZonesLayer />", () => {
|
||||||
y: { value: 1500, isDefault: true }
|
y: { value: 1500, isDefault: true }
|
||||||
},
|
},
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
startDrag: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
@ -25,6 +27,27 @@ describe("<ZonesLayer />", () => {
|
||||||
expect(wrapper.find(".zones-layer").length).toEqual(1);
|
expect(wrapper.find(".zones-layer").length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expectSolid = (zone2D: ReactWrapper) => {
|
||||||
|
const zoneProps = zone2D.find("rect").props();
|
||||||
|
expect(zoneProps.fill).toEqual(undefined);
|
||||||
|
expect(zoneProps.stroke).toEqual(undefined);
|
||||||
|
expect(zoneProps.strokeDasharray).toEqual(undefined);
|
||||||
|
expect(zoneProps.strokeWidth).toEqual(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectOutline = (zone2D: ReactWrapper) => {
|
||||||
|
const zoneProps = zone2D.find("rect").props();
|
||||||
|
expect(zoneProps.fill).toEqual("none");
|
||||||
|
expect(zoneProps.stroke).toEqual("white");
|
||||||
|
expect(zoneProps.strokeDasharray).toEqual(15);
|
||||||
|
expect(zoneProps.strokeWidth).toEqual(4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectNone = (zone2D: ReactWrapper) => {
|
||||||
|
expect(zone2D.html()).toEqual(
|
||||||
|
"<g id=\"zones-2D-1\" class=\"current\"></g>");
|
||||||
|
};
|
||||||
|
|
||||||
it("renders current group's zones: 2D", () => {
|
it("renders current group's zones: 2D", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.visible = false;
|
p.visible = false;
|
||||||
|
@ -37,6 +60,7 @@ describe("<ZonesLayer />", () => {
|
||||||
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
||||||
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
||||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||||
|
expectSolid(wrapper.find("#zones-2D-1"));
|
||||||
expect(wrapper.find("#zones-2D-2").length).toEqual(0);
|
expect(wrapper.find("#zones-2D-2").length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,19 +73,22 @@ describe("<ZonesLayer />", () => {
|
||||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||||
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
|
||||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||||
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
|
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||||
|
expectNone(wrapper.find("#zones-2D-1"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders current group's zones: 0D", () => {
|
it("renders current group's zones: 0D", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.visible = false;
|
p.visible = false;
|
||||||
p.groups[0].body.id = 1;
|
p.groups[0].body.id = 1;
|
||||||
|
p.groups[0].body.criteria.number_gt = { x: 10 };
|
||||||
p.groups[0].body.criteria.number_eq = { x: [100], y: [100] };
|
p.groups[0].body.criteria.number_eq = { x: [100], y: [100] };
|
||||||
p.currentGroup = p.groups[0].uuid;
|
p.currentGroup = p.groups[0].uuid;
|
||||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||||
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
|
||||||
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
|
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||||
|
expectOutline(wrapper.find("#zones-2D-1"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders current group's zones: none", () => {
|
it("renders current group's zones: none", () => {
|
||||||
|
@ -70,15 +97,20 @@ describe("<ZonesLayer />", () => {
|
||||||
p.groups[0].body.id = 1;
|
p.groups[0].body.id = 1;
|
||||||
p.currentGroup = p.groups[0].uuid;
|
p.currentGroup = p.groups[0].uuid;
|
||||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||||
expect(wrapper.html())
|
expect(wrapper.html()).toEqual(
|
||||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
`<svg>
|
||||||
|
<g class=\"zones-layer\" style=\"cursor: pointer;\">
|
||||||
|
<g id=\"zones-2D-1\" class=\"current\">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>`.replace(/[ ]{2,}/g, "").replace(/[^\S ]/g, ""));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't render current group's zones", () => {
|
it("doesn't render current group's zones", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.visible = false;
|
p.visible = false;
|
||||||
const wrapper = svgMount(<ZonesLayer {...p} />);
|
const wrapper = svgMount(<ZonesLayer {...p} />);
|
||||||
expect(wrapper.html())
|
expect(wrapper.html()).toEqual(
|
||||||
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
|
"<svg><g class=\"zones-layer\" style=\"cursor: pointer;\"></g></svg>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
import { svgMount } from "../../../../../__test_support__/svg_mount";
|
||||||
import {
|
import {
|
||||||
Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType,
|
Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType, spaceSelected,
|
||||||
} from "../zones";
|
} from "../zones";
|
||||||
import {
|
import {
|
||||||
fakePointGroup,
|
fakePointGroup,
|
||||||
|
@ -10,6 +12,7 @@ import {
|
||||||
fakeMapTransformProps,
|
fakeMapTransformProps,
|
||||||
} from "../../../../../__test_support__/map_transform_props";
|
} from "../../../../../__test_support__/map_transform_props";
|
||||||
import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces";
|
import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces";
|
||||||
|
import { history } from "../../../../../history";
|
||||||
|
|
||||||
const fakeProps = (): ZonesProps => ({
|
const fakeProps = (): ZonesProps => ({
|
||||||
group: fakePointGroup(),
|
group: fakePointGroup(),
|
||||||
|
@ -57,6 +60,15 @@ describe("<Zones0D />", () => {
|
||||||
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
|
||||||
expect(wrapper.find("circle").length).toEqual(2);
|
expect(wrapper.find("circle").length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens group", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.group.body.id = 1;
|
||||||
|
p.group.body.criteria.number_eq = { x: [100], y: [200, 300] };
|
||||||
|
const wrapper = svgMount(<Zones0D {...p} />);
|
||||||
|
wrapper.find("#zones-0D-1").simulate("click");
|
||||||
|
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("<Zones1D />", () => {
|
describe("<Zones1D />", () => {
|
||||||
|
@ -104,6 +116,15 @@ describe("<Zones1D />", () => {
|
||||||
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
|
||||||
expect(wrapper.find("line").length).toEqual(2);
|
expect(wrapper.find("line").length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens group", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.group.body.id = 1;
|
||||||
|
p.group.body.criteria.number_eq = { x: [], y: [200, 300] };
|
||||||
|
const wrapper = svgMount(<Zones1D {...p} />);
|
||||||
|
wrapper.find("#zones-1D-1").simulate("click");
|
||||||
|
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("<Zones2D />", () => {
|
describe("<Zones2D />", () => {
|
||||||
|
@ -137,6 +158,16 @@ describe("<Zones2D />", () => {
|
||||||
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
|
||||||
expect(wrapper.find("rect").length).toEqual(1);
|
expect(wrapper.find("rect").length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens group", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.group.body.id = 1;
|
||||||
|
p.group.body.criteria.number_gt = { x: 100, y: 200 };
|
||||||
|
p.group.body.criteria.number_lt = { x: 300, y: 400 };
|
||||||
|
const wrapper = svgMount(<Zones2D {...p} />);
|
||||||
|
wrapper.find("#zones-2D-1").simulate("click");
|
||||||
|
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getZoneType()", () => {
|
describe("getZoneType()", () => {
|
||||||
|
@ -163,3 +194,58 @@ describe("getZoneType()", () => {
|
||||||
expect(getZoneType(group)).toEqual(ZoneType.points);
|
expect(getZoneType(group)).toEqual(ZoneType.points);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("spaceSelected()", () => {
|
||||||
|
const botSize = {
|
||||||
|
x: { value: 3000, isDefault: true },
|
||||||
|
y: { value: 1500, isDefault: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
it("is selected: area", () => {
|
||||||
|
const group = fakePointGroup();
|
||||||
|
group.body.criteria.number_eq = {};
|
||||||
|
group.body.criteria.number_lt = {};
|
||||||
|
group.body.criteria.number_gt = {};
|
||||||
|
expect(spaceSelected(group, botSize)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isn't selected: area", () => {
|
||||||
|
const group = fakePointGroup();
|
||||||
|
group.body.criteria.number_eq = {};
|
||||||
|
group.body.criteria.number_lt = { x: 100 };
|
||||||
|
group.body.criteria.number_gt = { x: 200 };
|
||||||
|
expect(spaceSelected(group, botSize)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is selected: lines", () => {
|
||||||
|
const group = fakePointGroup();
|
||||||
|
group.body.criteria.number_eq = { x: [0] };
|
||||||
|
group.body.criteria.number_lt = {};
|
||||||
|
group.body.criteria.number_gt = {};
|
||||||
|
expect(spaceSelected(group, botSize)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isn't selected: lines", () => {
|
||||||
|
const group = fakePointGroup();
|
||||||
|
group.body.criteria.number_eq = { x: [0] };
|
||||||
|
group.body.criteria.number_lt = {};
|
||||||
|
group.body.criteria.number_gt = { x: 100 };
|
||||||
|
expect(spaceSelected(group, botSize)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is selected: points", () => {
|
||||||
|
const group = fakePointGroup();
|
||||||
|
group.body.criteria.number_eq = { x: [0], y: [0] };
|
||||||
|
group.body.criteria.number_lt = {};
|
||||||
|
group.body.criteria.number_gt = {};
|
||||||
|
expect(spaceSelected(group, botSize)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isn't selected: points", () => {
|
||||||
|
const group = fakePointGroup();
|
||||||
|
group.body.criteria.number_eq = { x: [0], y: [0] };
|
||||||
|
group.body.criteria.number_lt = { x: 0 };
|
||||||
|
group.body.criteria.number_gt = {};
|
||||||
|
expect(spaceSelected(group, botSize)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { MapTransformProps, BotSize } from "../../interfaces";
|
||||||
import { transformXY } from "../../util";
|
import { transformXY } from "../../util";
|
||||||
import { isUndefined } from "lodash";
|
import { isUndefined } from "lodash";
|
||||||
import { UUID } from "../../../../resources/interfaces";
|
import { UUID } from "../../../../resources/interfaces";
|
||||||
|
import { history } from "../../../../history";
|
||||||
|
|
||||||
export interface ZonesProps {
|
export interface ZonesProps {
|
||||||
currentGroup: UUID | undefined;
|
currentGroup: UUID | undefined;
|
||||||
|
@ -43,15 +44,18 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => {
|
||||||
return ZoneType.none;
|
return ZoneType.none;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openGroup = (id: number | undefined) =>
|
||||||
|
() => history.push(`/app/designer/groups/${id}`);
|
||||||
|
|
||||||
/** Bounds for area selected by criteria or bot extents. */
|
/** Bounds for area selected by criteria or bot extents. */
|
||||||
const getBoundary = (props: GetBoundaryProps): Boundary => {
|
const getBoundary = (props: GetBoundaryProps): Boundary => {
|
||||||
const { criteria } = props.group.body;
|
const { criteria } = props.group.body;
|
||||||
const gt = criteria.number_gt;
|
const gt = criteria.number_gt;
|
||||||
const lt = criteria.number_lt;
|
const lt = criteria.number_lt;
|
||||||
const x1 = gt.x || 0;
|
const x1 = gt.x ?? (0 - 0.01);
|
||||||
const x2 = lt.x || props.botSize.x.value;
|
const x2 = lt.x ?? (props.botSize.x.value + 0.01);
|
||||||
const y1 = gt.y || 0;
|
const y1 = gt.y ?? (0 - 0.01);
|
||||||
const y2 = lt.y || props.botSize.y.value;
|
const y2 = lt.y ?? (props.botSize.y.value + 0.01);
|
||||||
const selectsAll = !(gt.x || lt.x || gt.y || lt.y);
|
const selectsAll = !(gt.x || lt.x || gt.y || lt.y);
|
||||||
return { x1, x2, y1, y2, selectsAll };
|
return { x1, x2, y1, y2, selectsAll };
|
||||||
};
|
};
|
||||||
|
@ -85,7 +89,8 @@ const zone0D = (props: ZonesProps) =>
|
||||||
/** Coordinates selected by both x and y number equal values. */
|
/** Coordinates selected by both x and y number equal values. */
|
||||||
export const Zones0D = (props: ZonesProps) => {
|
export const Zones0D = (props: ZonesProps) => {
|
||||||
const current = props.group.uuid == props.currentGroup;
|
const current = props.group.uuid == props.currentGroup;
|
||||||
return <g id={`zones-0D-${props.group.body.id}`}
|
const { id } = props.group.body;
|
||||||
|
return <g id={`zones-0D-${id}`} onClick={openGroup(id)}
|
||||||
className={current ? "current" : ""}>
|
className={current ? "current" : ""}>
|
||||||
{zone0D(props).map((point, i) =>
|
{zone0D(props).map((point, i) =>
|
||||||
<circle key={i} cx={point.x} cy={point.y} r={5} />)}
|
<circle key={i} cx={point.x} cy={point.y} r={5} />)}
|
||||||
|
@ -109,11 +114,11 @@ const zone1D = (props: ZonesProps) => {
|
||||||
const boundary = getBoundary(props);
|
const boundary = getBoundary(props);
|
||||||
return getLines(boundary, props.group).map(line => {
|
return getLines(boundary, props.group).map(line => {
|
||||||
const min = transformXY(
|
const min = transformXY(
|
||||||
line.x || boundary.x1,
|
line.x ?? boundary.x1,
|
||||||
line.y || boundary.y1, props.mapTransformProps);
|
line.y ?? boundary.y1, props.mapTransformProps);
|
||||||
const max = transformXY(
|
const max = transformXY(
|
||||||
line.x || boundary.x2,
|
line.x ?? boundary.x2,
|
||||||
line.y || boundary.y2, props.mapTransformProps);
|
line.y ?? boundary.y2, props.mapTransformProps);
|
||||||
return {
|
return {
|
||||||
x1: min.qx,
|
x1: min.qx,
|
||||||
y1: min.qy,
|
y1: min.qy,
|
||||||
|
@ -126,7 +131,8 @@ const zone1D = (props: ZonesProps) => {
|
||||||
/** Lines selected by an x or y number equal value. */
|
/** Lines selected by an x or y number equal value. */
|
||||||
export const Zones1D = (props: ZonesProps) => {
|
export const Zones1D = (props: ZonesProps) => {
|
||||||
const current = props.group.uuid == props.currentGroup;
|
const current = props.group.uuid == props.currentGroup;
|
||||||
return <g id={`zones-1D-${props.group.body.id}`}
|
const { id } = props.group.body;
|
||||||
|
return <g id={`zones-1D-${id}`} onClick={openGroup(id)}
|
||||||
className={current ? "current" : ""}>
|
className={current ? "current" : ""}>
|
||||||
{zone1D(props).map((line, i) =>
|
{zone1D(props).map((line, i) =>
|
||||||
<line key={i} x1={line.x1} y1={line.y1}
|
<line key={i} x1={line.x1} y1={line.y1}
|
||||||
|
@ -138,24 +144,59 @@ export const Zones1D = (props: ZonesProps) => {
|
||||||
const zone2D = (boundary: Boundary, mapTransformProps: MapTransformProps) => {
|
const zone2D = (boundary: Boundary, mapTransformProps: MapTransformProps) => {
|
||||||
const position = transformXY(boundary.x1, boundary.y1, mapTransformProps);
|
const position = transformXY(boundary.x1, boundary.y1, mapTransformProps);
|
||||||
const { xySwap, quadrant } = mapTransformProps;
|
const { xySwap, quadrant } = mapTransformProps;
|
||||||
const xLength = boundary.x2 - boundary.x1;
|
const xLength = Math.max(0, boundary.x2 - boundary.x1);
|
||||||
const yLength = boundary.y2 - boundary.y1;
|
const yLength = Math.max(0, boundary.y2 - boundary.y1);
|
||||||
|
const width = xySwap ? yLength : xLength;
|
||||||
|
const height = xySwap ? xLength : yLength;
|
||||||
return {
|
return {
|
||||||
x: [1, 4].includes(quadrant) ? position.qx - xLength : position.qx,
|
x: [1, 4].includes(quadrant) ? position.qx - width : position.qx,
|
||||||
y: [3, 4].includes(quadrant) ? position.qy - yLength : position.qy,
|
y: [3, 4].includes(quadrant) ? position.qy - height : position.qy,
|
||||||
width: xySwap ? yLength : xLength,
|
width,
|
||||||
height: xySwap ? xLength : yLength,
|
height,
|
||||||
selectsAll: boundary.selectsAll,
|
selectsAll: boundary.selectsAll,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Area selected by x and y number gt/lt values. */
|
/** Area selected by x and y number gt/lt values. */
|
||||||
export const Zones2D = (props: ZonesProps) => {
|
export const Zones2D = (props: ZonesProps) => {
|
||||||
const zone = zone2D(getBoundary(props), props.mapTransformProps);
|
|
||||||
const current = props.group.uuid == props.currentGroup;
|
const current = props.group.uuid == props.currentGroup;
|
||||||
return <g id={`zones-2D-${props.group.body.id}`}
|
const zone = zone2D(getBoundary(props), props.mapTransformProps);
|
||||||
|
const not2D = getZoneType(props.group) !== ZoneType.area;
|
||||||
|
const rectProps: React.SVGProps<SVGElement> = not2D ? {
|
||||||
|
stroke: current ? "white" : "black",
|
||||||
|
strokeWidth: 4,
|
||||||
|
strokeDasharray: 15,
|
||||||
|
fill: "none",
|
||||||
|
} : {};
|
||||||
|
const { id } = props.group.body;
|
||||||
|
return <g id={`zones-2D-${id}`} onClick={openGroup(id)}
|
||||||
className={current ? "current" : ""}>
|
className={current ? "current" : ""}>
|
||||||
{!zone.selectsAll &&
|
{!zone.selectsAll &&
|
||||||
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height} />}
|
<rect x={zone.x} y={zone.y} width={zone.width} height={zone.height}
|
||||||
|
stroke={rectProps.stroke}
|
||||||
|
strokeWidth={rectProps.strokeWidth}
|
||||||
|
strokeDasharray={rectProps.strokeDasharray}
|
||||||
|
fill={rectProps.fill} />}
|
||||||
</g>;
|
</g>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Determine if location criteria selects some space. */
|
||||||
|
export const spaceSelected =
|
||||||
|
(group: TaggedPointGroup, botSize: BotSize) => {
|
||||||
|
const boundary = getBoundary({ group, botSize });
|
||||||
|
const area = {
|
||||||
|
width: Math.max(0, boundary.x2 - boundary.x1),
|
||||||
|
height: Math.max(0, boundary.y2 - boundary.y1),
|
||||||
|
};
|
||||||
|
const lines = getLines(boundary, group);
|
||||||
|
const points = getPoints(boundary, group);
|
||||||
|
switch (getZoneType(group)) {
|
||||||
|
case ZoneType.none:
|
||||||
|
case ZoneType.area:
|
||||||
|
return (area.width > 0) && (area.height > 0);
|
||||||
|
case ZoneType.lines:
|
||||||
|
return lines.length > 0;
|
||||||
|
case ZoneType.points:
|
||||||
|
return points.length > 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { TaggedPointGroup } from "farmbot";
|
||||||
import { MapTransformProps, BotSize } from "../../interfaces";
|
import { MapTransformProps, BotSize } from "../../interfaces";
|
||||||
import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones";
|
import { Zones0D, Zones1D, Zones2D, getZoneType, ZoneType } from "./zones";
|
||||||
import { UUID } from "../../../../resources/interfaces";
|
import { UUID } from "../../../../resources/interfaces";
|
||||||
|
import { allowGroupAreaInteraction } from "../../util";
|
||||||
|
|
||||||
export interface ZonesLayerProps {
|
export interface ZonesLayerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -10,6 +11,7 @@ export interface ZonesLayerProps {
|
||||||
groups: TaggedPointGroup[];
|
groups: TaggedPointGroup[];
|
||||||
botSize: BotSize;
|
botSize: BotSize;
|
||||||
mapTransformProps: MapTransformProps;
|
mapTransformProps: MapTransformProps;
|
||||||
|
startDrag(e: React.MouseEvent<SVGElement>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ZonesLayer(props: ZonesLayerProps) {
|
export function ZonesLayer(props: ZonesLayerProps) {
|
||||||
|
@ -17,9 +19,10 @@ export function ZonesLayer(props: ZonesLayerProps) {
|
||||||
const commonProps = { botSize, mapTransformProps, currentGroup };
|
const commonProps = { botSize, mapTransformProps, currentGroup };
|
||||||
const visible = (group: TaggedPointGroup) =>
|
const visible = (group: TaggedPointGroup) =>
|
||||||
props.visible || (group.uuid == currentGroup);
|
props.visible || (group.uuid == currentGroup);
|
||||||
return <g className="zones-layer">
|
return <g className="zones-layer" style={allowGroupAreaInteraction()
|
||||||
|
? { cursor: "pointer" }
|
||||||
|
: { pointerEvents: "none" }} onMouseDown={props.startDrag}>
|
||||||
{groups.map(group => visible(group) &&
|
{groups.map(group => visible(group) &&
|
||||||
getZoneType(group) === ZoneType.area &&
|
|
||||||
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
<Zones2D {...commonProps} key={group.uuid} group={group} />)}
|
||||||
{groups.map(group => visible(group) &&
|
{groups.map(group => visible(group) &&
|
||||||
getZoneType(group) === ZoneType.lines &&
|
getZoneType(group) === ZoneType.lines &&
|
||||||
|
|
|
@ -35,6 +35,7 @@ describe("<GardenMapLegend />", () => {
|
||||||
legendMenuOpen: true,
|
legendMenuOpen: true,
|
||||||
showPlants: false,
|
showPlants: false,
|
||||||
showPoints: false,
|
showPoints: false,
|
||||||
|
showWeeds: false,
|
||||||
showSpread: false,
|
showSpread: false,
|
||||||
showFarmbot: false,
|
showFarmbot: false,
|
||||||
showImages: false,
|
showImages: false,
|
||||||
|
@ -45,6 +46,7 @@ describe("<GardenMapLegend />", () => {
|
||||||
timeSettings: fakeTimeSettings(),
|
timeSettings: fakeTimeSettings(),
|
||||||
getConfigValue: jest.fn(),
|
getConfigValue: jest.fn(),
|
||||||
imageAgeInfo: { newestDate: "", toOldest: 1 },
|
imageAgeInfo: { newestDate: "", toOldest: 1 },
|
||||||
|
shouldDisplay: () => true,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { GetWebAppConfigValue } from "../../../config_storage/actions";
|
||||||
import { BooleanSetting } from "../../../session_keys";
|
import { BooleanSetting } from "../../../session_keys";
|
||||||
import { DevSettings } from "../../../account/dev/dev_support";
|
import { DevSettings } from "../../../account/dev/dev_support";
|
||||||
import { t } from "../../../i18next_wrapper";
|
import { t } from "../../../i18next_wrapper";
|
||||||
|
import { Feature } from "../../../devices/interfaces";
|
||||||
|
import { SelectModeLink } from "../../plants/select_plants";
|
||||||
|
|
||||||
export const ZoomControls = ({ zoom, getConfigValue }: {
|
export const ZoomControls = ({ zoom, getConfigValue }: {
|
||||||
zoom: (value: number) => () => void,
|
zoom: (value: number) => () => void,
|
||||||
|
@ -59,6 +61,10 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
||||||
popover={DevSettings.futureFeaturesEnabled()
|
popover={DevSettings.futureFeaturesEnabled()
|
||||||
? <PointsSubMenu toggle={toggle} getConfigValue={getConfigValue} />
|
? <PointsSubMenu toggle={toggle} getConfigValue={getConfigValue} />
|
||||||
: undefined} />
|
: undefined} />
|
||||||
|
<LayerToggle
|
||||||
|
value={props.showWeeds}
|
||||||
|
label={t("Weeds?")}
|
||||||
|
onClick={toggle(BooleanSetting.show_weeds)} />
|
||||||
<LayerToggle
|
<LayerToggle
|
||||||
value={props.showSpread}
|
value={props.showSpread}
|
||||||
label={t("Spread?")}
|
label={t("Spread?")}
|
||||||
|
@ -77,7 +83,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
||||||
dispatch={props.dispatch}
|
dispatch={props.dispatch}
|
||||||
getConfigValue={getConfigValue}
|
getConfigValue={getConfigValue}
|
||||||
imageAgeInfo={props.imageAgeInfo} />} />
|
imageAgeInfo={props.imageAgeInfo} />} />
|
||||||
{DevSettings.futureFeaturesEnabled() &&
|
{props.shouldDisplay(Feature.criteria_groups) &&
|
||||||
<LayerToggle
|
<LayerToggle
|
||||||
value={props.showZones}
|
value={props.showZones}
|
||||||
label={t("areas?")}
|
label={t("areas?")}
|
||||||
|
@ -104,6 +110,7 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
|
||||||
<ZoomControls zoom={props.zoom} getConfigValue={props.getConfigValue} />
|
<ZoomControls zoom={props.zoom} getConfigValue={props.getConfigValue} />
|
||||||
<LayerToggles {...props} />
|
<LayerToggles {...props} />
|
||||||
<MoveModeLink />
|
<MoveModeLink />
|
||||||
|
<SelectModeLink />
|
||||||
<BugsControls />
|
<BugsControls />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from "./interfaces";
|
} from "./interfaces";
|
||||||
import { trim } from "../../util";
|
import { trim } from "../../util";
|
||||||
import { history, getPathArray } from "../../history";
|
import { history, getPathArray } from "../../history";
|
||||||
import { savedGardenOpen } from "../saved_gardens/saved_gardens";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Farm Designer Map Utilities
|
* Farm Designer Map Utilities
|
||||||
|
@ -293,21 +292,30 @@ export const getMode = (): Mode => {
|
||||||
if ((pathArray[3] === "groups" || pathArray[3] === "zones")
|
if ((pathArray[3] === "groups" || pathArray[3] === "zones")
|
||||||
&& pathArray[4]) { return Mode.editGroup; }
|
&& pathArray[4]) { return Mode.editGroup; }
|
||||||
if (pathArray[6] === "add") { return Mode.clickToAdd; }
|
if (pathArray[6] === "add") { return Mode.clickToAdd; }
|
||||||
|
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
|
||||||
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
|
if (!isNaN(parseInt(pathArray.slice(-1)[0]))) { return Mode.editPlant; }
|
||||||
if (pathArray[5] === "edit") { return Mode.editPlant; }
|
if (pathArray[5] === "edit") { return Mode.editPlant; }
|
||||||
if (pathArray[6] === "edit") { return Mode.editPlant; }
|
if (pathArray[6] === "edit") { return Mode.editPlant; }
|
||||||
if (pathArray[4] === "select") { return Mode.boxSelect; }
|
if (pathArray[4] === "select") { return Mode.boxSelect; }
|
||||||
if (pathArray[4] === "crop_search") { return Mode.addPlant; }
|
if (pathArray[4] === "crop_search") { return Mode.addPlant; }
|
||||||
if (pathArray[3] === "move_to") { return Mode.moveTo; }
|
if (pathArray[3] === "move_to") { return Mode.moveTo; }
|
||||||
if (pathArray[3] === "points" || pathArray[3] === "weeds") {
|
if (pathArray[3] === "points") {
|
||||||
if (pathArray[4] === "add") { return Mode.createPoint; }
|
if (pathArray[4] === "add") { return Mode.createPoint; }
|
||||||
return Mode.points;
|
return Mode.points;
|
||||||
}
|
}
|
||||||
if (savedGardenOpen(pathArray)) { return Mode.templateView; }
|
if (pathArray[3] === "weeds") {
|
||||||
|
if (pathArray[4] === "add") { return Mode.createWeed; }
|
||||||
|
return Mode.weeds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Mode.none;
|
return Mode.none;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Check if a SavedGarden is currently open (URL approach). */
|
||||||
|
export const savedGardenOpen = (pathArray: string[]) =>
|
||||||
|
pathArray[3] === "gardens" && parseInt(pathArray[4]) > 0
|
||||||
|
? parseInt(pathArray[4]) : false;
|
||||||
|
|
||||||
export const getZoomLevelFromMap = (map: Element) =>
|
export const getZoomLevelFromMap = (map: Element) =>
|
||||||
parseFloat((window.getComputedStyle(map).transform || "(1").split("(")[1]);
|
parseFloat((window.getComputedStyle(map).transform || "(1").split("(")[1]);
|
||||||
|
|
||||||
|
@ -337,18 +345,28 @@ export const getGardenCoordinates = (props: {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maybeNoPointer =
|
export const allowInteraction = () => {
|
||||||
(defaultStyle: React.CSSProperties): React.SVGProps<SVGGElement>["style"] => {
|
switch (getMode()) {
|
||||||
switch (getMode()) {
|
case Mode.clickToAdd:
|
||||||
case Mode.clickToAdd:
|
case Mode.moveTo:
|
||||||
case Mode.moveTo:
|
case Mode.createPoint:
|
||||||
case Mode.points:
|
case Mode.createWeed:
|
||||||
case Mode.createPoint:
|
return false;
|
||||||
return { pointerEvents: "none" };
|
default:
|
||||||
default:
|
return true;
|
||||||
return defaultStyle;
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
export const allowGroupAreaInteraction = () => {
|
||||||
|
if (!allowInteraction()) { return false; }
|
||||||
|
switch (getMode()) {
|
||||||
|
case Mode.boxSelect:
|
||||||
|
case Mode.editGroup:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** Check if the cursor is within the selected plant indicator area. */
|
/** Check if the cursor is within the selected plant indicator area. */
|
||||||
export const cursorAtPlant =
|
export const cursorAtPlant =
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue