Compare commits

..

5 Commits
jebba ... hmm

Author SHA1 Message Date
Rick Carlino be38ad830a Merge branch 'master' of https://git.heroku.com/farmbot-staging into staging 2020-03-17 14:44:54 -05:00
Rick Carlino 4f03626d5f Merge branch 'master' into staging 2020-03-17 14:43:25 -05:00
Rick Carlino 9f1cf4eedd Loosen restriction on `package` attr of `flash_firmware`. 2020-02-20 18:13:46 -06:00
Rick Carlino afceaf25e0 Add test for flash_firmware node 2020-02-20 18:03:23 -06:00
Rick Carlino e5389b747e Begin work on 'flash firmware' button 2020-02-20 17:32:58 -06:00
302 changed files with 3617 additions and 8837 deletions

View File

@ -35,7 +35,7 @@ group :development, :test do
gem "hashdiff"
gem "pry-rails"
gem "pry"
gem "rspec-rails"
gem "rspec-rails", "4.0.0.beta3"
gem "rspec"
gem 'rspec_junit_formatter'
gem "simplecov"

View File

@ -7,38 +7,38 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.0.2.2)
actionpack (= 6.0.2.2)
actioncable (6.0.2.1)
actionpack (= 6.0.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
actionmailbox (6.0.2.1)
actionpack (= 6.0.2.1)
activejob (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
mail (>= 2.7.1)
actionmailer (6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
actionmailer (6.0.2.1)
actionpack (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.2.2)
actionview (= 6.0.2.2)
activesupport (= 6.0.2.2)
actionpack (6.0.2.1)
actionview (= 6.0.2.1)
activesupport (= 6.0.2.1)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.2.2)
actionpack (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
actiontext (6.0.2.1)
actionpack (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
nokogiri (>= 1.8.5)
actionview (6.0.2.2)
activesupport (= 6.0.2.2)
actionview (6.0.2.1)
activesupport (= 6.0.2.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -48,20 +48,20 @@ GEM
activemodel (>= 4.1, < 6.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.0.2.2)
activesupport (= 6.0.2.2)
activejob (6.0.2.1)
activesupport (= 6.0.2.1)
globalid (>= 0.3.6)
activemodel (6.0.2.2)
activesupport (= 6.0.2.2)
activerecord (6.0.2.2)
activemodel (= 6.0.2.2)
activesupport (= 6.0.2.2)
activestorage (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
activemodel (6.0.2.1)
activesupport (= 6.0.2.1)
activerecord (6.0.2.1)
activemodel (= 6.0.2.1)
activesupport (= 6.0.2.1)
activestorage (6.0.2.1)
actionpack (= 6.0.2.1)
activejob (= 6.0.2.1)
activerecord (= 6.0.2.1)
marcel (~> 0.3.1)
activesupport (6.0.2.2)
activesupport (6.0.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -69,11 +69,11 @@ GEM
zeitwerk (~> 2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
amq-protocol (2.3.1)
amq-protocol (2.3.0)
bcrypt (3.1.13)
builder (3.2.4)
bunny (2.15.0)
amq-protocol (~> 2.3, >= 2.3.1)
bunny (2.14.4)
amq-protocol (~> 2.3, >= 2.3.0)
case_transform (0.2)
activesupport
climate_control (0.2.0)
@ -99,27 +99,27 @@ GEM
responders
warden (~> 1.2.3)
diff-lcs (1.3)
digest-crc (0.5.1)
digest-crc (0.4.1)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
erubi (1.9.0)
factory_bot (5.1.2)
factory_bot (5.1.1)
activesupport (>= 4.2.0)
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.11.0)
faker (2.10.2)
i18n (>= 1.6, < 2)
faraday (0.17.3)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.14.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
font-awesome-rails (4.7.0.5)
railties (>= 3.2, < 6.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.37.2)
google-api-client (0.37.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@ -130,8 +130,8 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.1)
faraday (>= 0.17.3, < 2.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
@ -140,14 +140,14 @@ GEM
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.11.0)
faraday (>= 0.17.3, < 2.0)
googleauth (0.10.0)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
hashdiff (1.0.1)
hashdiff (1.0.0)
hashie (3.6.0)
httpclient (2.8.3)
i18n (1.8.2)
@ -167,7 +167,7 @@ GEM
activerecord
kaminari-core (= 1.2.0)
kaminari-core (1.2.0)
loofah (2.5.0)
loofah (2.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -175,33 +175,33 @@ GEM
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoist (0.16.2)
method_source (1.0.0)
method_source (0.9.2)
mimemagic (0.3.4)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
multi_json (1.13.1)
multipart-post (2.1.1)
mutations (0.9.1)
mutations (0.9.0)
activesupport
nio4r (2.5.2)
nokogiri (1.10.9)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0)
os (1.0.1)
passenger (6.0.4)
rack
rake (>= 0.8.1)
pg (1.2.3)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pg (1.2.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
rabbitmq_http_api_client (1.13.0)
faraday (>= 0.15, < 1)
faraday_middleware (>= 0.13.0, < 1)
rabbitmq_http_api_client (1.12.0)
faraday (~> 0.15.4)
faraday_middleware (~> 0.13.0)
hashie (~> 3.6)
multi_json (~> 1.13.1)
rack (2.2.2)
@ -211,20 +211,20 @@ GEM
rack (>= 2.0.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.2.2)
actioncable (= 6.0.2.2)
actionmailbox (= 6.0.2.2)
actionmailer (= 6.0.2.2)
actionpack (= 6.0.2.2)
actiontext (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
activemodel (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
rails (6.0.2.1)
actioncable (= 6.0.2.1)
actionmailbox (= 6.0.2.1)
actionmailer (= 6.0.2.1)
actionpack (= 6.0.2.1)
actiontext (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
activemodel (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
bundler (>= 1.3.0)
railties (= 6.0.2.2)
railties (= 6.0.2.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
@ -236,9 +236,9 @@ GEM
rails_stdout_logging
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (6.0.2.2)
actionpack (= 6.0.2.2)
activesupport (= 6.0.2.2)
railties (6.0.2.1)
actionpack (= 6.0.2.1)
activesupport (= 6.0.2.1)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -261,20 +261,20 @@ GEM
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.1)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0)
rspec-rails (4.0.0.beta3)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.9.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
@ -282,15 +282,15 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
secure_headers (6.3.0)
signet (0.13.0)
signet (0.12.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.18.5)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-html (0.12.2)
simplecov-html (0.12.1)
sprockets (4.0.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -300,7 +300,7 @@ GEM
sprockets (>= 3.0.0)
thor (1.0.1)
thread_safe (0.3.6)
tzinfo (1.2.7)
tzinfo (1.2.6)
thread_safe (~> 0.1)
uber (0.1.0)
url (0.3.2)
@ -312,7 +312,7 @@ GEM
websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4)
zeitwerk (2.3.0)
zeitwerk (2.2.2)
PLATFORMS
ruby
@ -348,7 +348,7 @@ DEPENDENCIES
request_store
rollbar
rspec
rspec-rails
rspec-rails (= 4.0.0.beta3)
rspec_junit_formatter
scenic
secure_headers

View File

@ -46,6 +46,5 @@ module Resources
Plant => Points,
Point => Points,
ToolSlot => Points,
Weed => Points,
}
end # Resources

View File

@ -30,19 +30,19 @@ module CeleryScriptSettingsBag
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
ALLOWED_PIN_MODES = [DIGITAL = 0, ANALOG = 1]
ALLOWED_PIN_TYPES = PIN_TYPE_MAP.keys
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant Weed)
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot Weed GenericPointer)
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant)
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot GenericPointer)
ALLOWED_RPC_NODES = %w(assertion calibrate change_ownership
check_updates emergency_lock
check_updates dump_info emergency_lock
emergency_unlock execute execute_script
factory_reset find_home flash_firmware home
install_farmware install_first_party_farmware _if
move_absolute move_relative power_off read_pin
read_status reboot remove_farmware update_resource
read_status reboot remove_farmware resource_update
send_message set_servo_angle set_user_env sync
take_photo toggle_pin update_farmware wait
write_pin zero)
ALLOWED_SPEC_ACTION = %w(emergency_lock emergency_unlock power_off
ALLOWED_SPEC_ACTION = %w(dump_info emergency_lock emergency_unlock power_off
read_status reboot sync take_photo)
ANY_VARIABLE = %i(tool coordinate point identifier)
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
@ -73,7 +73,8 @@ module CeleryScriptSettingsBag
ONLY_ONE_COORD = "Move Absolute does not accept a group of locations " \
"as input. Please change your selection to a single" \
" location."
PLANT_STAGES = %w(planned planted harvested sprouted removed)
PLANT_STAGES = %w(planned planted harvested sprouted)
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
MISC_ENUM_ERR = '"%s" is not valid. Allowed values: %s'
MAX_WAIT_MS = 1000 * 60 * 3 # Three Minutes
@ -81,13 +82,6 @@ module CeleryScriptSettingsBag
"A single wait node cannot exceed #{MAX_WAIT_MS / 1000 / 60} minutes. " +
"Consider lowering the wait time or using multiple WAIT blocks."
Corpus = CeleryScript::Corpus.new
THIS_IS_DEPRECATED = {
args: [:resource_type, :resource_id, :label, :value],
tags: [:function, :api_writer, :network_user],
blk: ->(n) do
n.invalidate!("Deprecated `mark_as` detected. Delete it and re-add")
end,
}
CORPUS_VALUES = {
boolean: [TrueClass, FalseClass],
@ -257,7 +251,9 @@ module CeleryScriptSettingsBag
# outside of the API. If `package` _was_ declared as a native enum (rather
# than a string), it would cause false type errors in FE/FBJS.
blk: ->(node) do
manual_enum(ALLOWED_PACKAGES, node, BAD_PACKAGE)
unless node.parent.kind.to_s == "flash_firmware"
manual_enum(ALLOWED_PACKAGES, node, BAD_PACKAGE)
end
end,
},
axis: {
@ -284,9 +280,6 @@ module CeleryScriptSettingsBag
lua: {
defn: [v(:string)],
},
resource: {
defn: [n(:identifier), n(:resource)],
},
}.map do |(name, conf)|
blk = conf[:blk]
defn = conf.fetch(:defn)
@ -326,6 +319,10 @@ module CeleryScriptSettingsBag
args: [:x, :y, :z],
tags: [:data, :location_like],
},
dump_info: {
tags: [:function, :network_user, :disk_user, :api_writer],
docs: "Sends an info dump to server administrators for troubleshooting.",
},
emergency_lock: {
tags: [:function, :firmware_user, :control_flow],
},
@ -518,22 +515,15 @@ module CeleryScriptSettingsBag
tags: [:function, :firmware_user, :rpi_user],
blk: ->(n) { no_rpi_analog(n) },
},
# DEPRECATED- Get rid of this node ASAP -RC 15 APR 2020
resource_update: THIS_IS_DEPRECATED,
resource: {
args: [:resource_type, :resource_id],
tags: [:network_user],
resource_update: {
args: RESOURCE_UPDATE_ARGS,
tags: [:function, :api_writer, :network_user],
blk: ->(n) do
resource_type = n.args.fetch(:resource_type).value
resource_id = n.args.fetch(:resource_id).value
check_resource_type(n, resource_type, resource_id, Device.current)
end,
},
update_resource: {
args: [:resource],
body: [:pair],
tags: [:function, :api_writer, :network_user],
},
point_group: {
args: [:point_group_id],
tags: [:data, :list_like],

View File

@ -8,7 +8,6 @@ class InUsePoint < ApplicationRecord
GenericPointer.name => DEFAULT_NAME,
ToolSlot.name => "slot",
Plant.name => "plant",
Weed.name => "weed"
}
def readonly?

View File

@ -1,18 +1,20 @@
class PinBinding < ApplicationRecord
OFF_LIMITS = [
2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 24, 25, 27,
OFF_LIMITS = [
2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 24, 25, 27
]
BAD_PIN_NUM = "The following pin numbers cannot be used: %s" % OFF_LIMITS.join(", ")
BAD_PIN_NUM = \
"The following pin numbers cannot be used: %s" % OFF_LIMITS.join(", ")
belongs_to :device
belongs_to :sequence
enum special_action: { emergency_lock: "emergency_lock",
enum special_action: { dump_info: "dump_info",
emergency_lock: "emergency_lock",
emergency_unlock: "emergency_unlock",
power_off: "power_off",
read_status: "read_status",
reboot: "reboot",
sync: "sync",
take_photo: "take_photo" }
power_off: "power_off",
read_status: "read_status",
reboot: "reboot",
sync: "sync",
take_photo: "take_photo" }
validates :pin_num, uniqueness: { scope: :device }
def fancy_name

View File

@ -4,7 +4,7 @@ class Point < ApplicationRecord
# axis value > 21k right now - RC
# Using real constants instead of strings results
# in circular dep. errors.
POINTER_KINDS = ["GenericPointer", "Plant", "ToolSlot", "Weed"]
POINTER_KINDS = ["GenericPointer", "Plant", "ToolSlot"]
self.inheritance_column = "pointer_type"
belongs_to :device

View File

@ -1,2 +0,0 @@
class Weed < Point
end

View File

@ -27,19 +27,11 @@ module Points
end
def execute
Point.transaction { point.update!(update_params) && point }
Point.transaction { point.update!(inputs.except(:point)) && point }
end
private
def merged_meta_fields
@merged_meta_fields ||= (point.meta || {}).merge(meta || {})
end
def update_params
@update_params ||= inputs.except(:point).merge(meta: merged_meta_fields)
end
def new_tool_id?
raw_inputs.key?("tool_id")
end

View File

@ -1,24 +1,6 @@
class BasePointSerializer < ApplicationSerializer
attributes :device_id, :name, :pointer_type, :meta, :x, :y, :z
# PROBLEM:
# * Users need a mutable way to mark a plant's creation time => `planted_at`
# * DB Admin needs to know the _real_ created_at time.
# * We can't change field names (or destroy data) that is in use by legacy devices
#
# SOLUTION:
# * Don't allow users to modify `created_at`
# * Provide `planted_at` if possible.
# * Always provide `planted_at` if it is available
# * Provide a read-only view of `created_at` if `planted_at` is `nil`
def planted_at
object.planted_at || object.created_at
end
def created_at
planted_at
end
def meta
object.meta || {}
end

View File

@ -1,3 +0,0 @@
class WeedSerializer < BasePointSerializer
attributes :radius, :discarded_at, :plant_stage
end

View File

@ -1,4 +1,5 @@
class MakeDefaulDeviceNameFarmbot < ActiveRecord::Migration[5.1]
def change
change_column_default(:devices, :name, "Farmbot")
end

View File

@ -1,8 +0,0 @@
class AddShowWeedsToWebAppConfig < ActiveRecord::Migration[6.0]
def change
add_column :web_app_configs,
:show_weeds,
:boolean,
default: false
end
end

View File

@ -1,5 +0,0 @@
class UpdateMaxImageCount < ActiveRecord::Migration[6.0]
def change
change_column_default(:devices, :max_images_count, 450)
end
end

View File

@ -150,8 +150,8 @@ ALTER SEQUENCE public.alerts_id_seq OWNED BY public.alerts.id;
CREATE TABLE public.ar_internal_metadata (
key character varying NOT NULL,
value character varying,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
@ -262,7 +262,7 @@ CREATE TABLE public.devices (
id integer NOT NULL,
name character varying DEFAULT 'FarmBot'::character varying,
max_log_count integer DEFAULT 1000,
max_images_count integer DEFAULT 450,
max_images_count integer DEFAULT 100,
timezone character varying(280),
last_saw_api 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.value
FROM public.edge_nodes
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[])))
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])))
), resource_id AS (
SELECT edge_nodes.primary_node_id,
edge_nodes.kind,
@ -1644,7 +1644,8 @@ CREATE TABLE public.users (
agreed_to_terms_at timestamp without time zone,
confirmation_sent_at timestamp without time zone,
unconfirmed_email character varying,
inactivity_warning_sent_at timestamp without time zone
inactivity_warning_sent_at timestamp without time zone,
inactivity_warning_count integer
);
@ -1728,9 +1729,7 @@ CREATE TABLE public.web_app_configs (
confirm_sequence_deletion boolean DEFAULT true,
discard_unsaved_sequences boolean DEFAULT false,
user_interface_read_only_mode boolean DEFAULT false,
assertion_log integer DEFAULT 1,
show_zones boolean DEFAULT false,
show_weeds boolean DEFAULT false
assertion_log integer DEFAULT 1
);
@ -3377,9 +3376,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20191219212755'),
('20191220010646'),
('20200116140201'),
('20200204192005'),
('20200204230135'),
('20200323235926'),
('20200412152208');
('20200204192005');

View File

@ -1,90 +0,0 @@
# 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 ^

View File

@ -1,8 +1,7 @@
import { DesignerState } from "../farm_designer/interfaces";
export const fakeDesignerState = (): DesignerState => ({
selectedPoints: undefined,
selectionPointType: undefined,
selectedPlants: undefined,
hoveredPlant: {
plantUUID: undefined,
icon: ""
@ -14,10 +13,8 @@ export const fakeDesignerState = (): DesignerState => ({
cropSearchResults: [],
cropSearchInProgress: false,
chosenLocation: { x: undefined, y: undefined, z: undefined },
drawnPoint: undefined,
drawnWeed: undefined,
currentPoint: undefined,
openedSavedGarden: undefined,
tryGroupSortType: undefined,
editGroupAreaInMap: false,
settingsSearchTerm: "",
});

View File

@ -1,2 +0,0 @@
export const mockDispatch = (innerDispatch = jest.fn()) =>
jest.fn(x => typeof x === "function" && x(innerDispatch));

View File

@ -26,7 +26,6 @@ import {
TaggedAlert,
TaggedPointGroup,
TaggedFolder,
TaggedWeedPointer,
} from "farmbot";
import { fakeResource } from "../fake_resource";
import {
@ -134,6 +133,7 @@ export function fakeToolSlot(): TaggedToolSlotPointer {
x: 0,
y: 0,
z: 0,
radius: 25,
pointer_type: "ToolSlot",
meta: {},
tool_id: undefined,
@ -171,20 +171,6 @@ 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 {
return fakeResource("SavedGarden", {
id: idCounter++,
@ -303,7 +289,6 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
show_sensor_readings: false,
show_plants: true,
show_points: true,
show_weeds: true,
x_axis_inverted: false,
y_axis_inverted: false,
z_axis_inverted: true,

View File

@ -7,5 +7,4 @@ jest.mock("../toast/toast", () => ({
error: jest.fn(),
warning: jest.fn(),
busy: jest.fn(),
removeToast: jest.fn(),
}));

View File

@ -265,6 +265,7 @@ const tr11: TaggedPoint = {
"pointer_type": "ToolSlot",
"pullout_direction": 0,
"gantry_mounted": false,
"radius": 25,
"x": 10,
"y": 10,
"z": 10,
@ -315,28 +316,6 @@ const tr15: TaggedResource = {
"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 = {
kind: "Log",
specialStatus: SpecialStatus.SAVED,
@ -366,7 +345,6 @@ export const FAKE_RESOURCES: TaggedResource[] = [
tr0,
tr14,
tr15,
tr16,
log,
];
const KIND: keyof TaggedResource = "kind"; // Safety first, kids.

View File

@ -334,7 +334,6 @@ const MUST_CONFIRM_LIST: ResourceName[] = [
"Regimen",
"Image",
"SavedGarden",
"PointGroup",
];
const confirmationChecker = (resourceName: ResourceName, force = false) =>

View File

@ -37,9 +37,7 @@ import { getDevice } from "../../../device";
import { talk } from "browser-speech";
import { MessageType } from "../../../sequences/interfaces";
import { FbjsEventName } from "farmbot/dist/constants";
import {
info, error, success, warning, fun, busy, removeToast,
} from "../../../toast/toast";
import { info, error, success, warning, fun, busy } from "../../../toast/toast";
import { onLogs } from "../../log_handlers";
import { fakeState } from "../../../__test_support__/fake_state";
import { globalQueue } from "../../batch_queue";
@ -179,8 +177,7 @@ describe("onOffline", () => {
jest.resetAllMocks();
onOffline();
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
expect(error).toHaveBeenCalledWith(
Content.MQTT_DISCONNECTED, "Error", "red", "offline");
expect(error).toHaveBeenCalledWith(Content.MQTT_DISCONNECTED);
});
});
@ -189,17 +186,13 @@ describe("onOnline", () => {
jest.resetAllMocks();
onOnline();
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", ANY_NUMBER);
expect(removeToast).toHaveBeenCalledWith("offline");
});
});
describe("onReconnect()", () => {
it("sends reconnect toast", () => {
onReconnect();
expect(warning).toHaveBeenCalledWith(
"Attempting to reconnect to the message broker",
"Offline", "yellow", "offline");
});
describe("onReconnect", () => {
onReconnect();
expect(warning).toHaveBeenCalledWith(
"Attempting to reconnect to the message broker", "Offline", "yellow");
});
describe("changeLastClientConnected", () => {
@ -275,8 +268,7 @@ describe("onPublicBroadcast", () => {
console.log = jest.fn();
onPublicBroadcast({});
expectBroadcastLog();
expect(window.alert).toHaveBeenCalledWith(
Content.FORCE_REFRESH_CANCEL_WARNING);
expect(window.alert).toHaveBeenCalledWith(Content.FORCE_REFRESH_CANCEL_WARNING);
expect(location.assign).not.toHaveBeenCalled();
});
});

View File

@ -4,9 +4,7 @@ import { Log } from "farmbot/dist/resources/api_resources";
import { Farmbot, BotStateTree, TaggedResource } from "farmbot";
import { FbjsEventName } from "farmbot/dist/constants";
import { noop } from "lodash";
import {
success, error, info, warning, fun, busy, removeToast,
} from "../toast/toast";
import { success, error, info, warning, fun, busy } from "../toast/toast";
import { HardwareState } from "../devices/interfaces";
import { GetState, ReduxAction } from "../redux/interfaces";
import { Content, Actions } from "../constants";
@ -104,6 +102,11 @@ export function readStatus() {
.then(() => { commandOK(noun); }, commandErr(noun));
}
export const onOffline = () => {
dispatchNetworkDown("user.mqtt", now());
error(t(Content.MQTT_DISCONNECTED));
};
export const changeLastClientConnected = (bot: Farmbot) => () => {
bot.setUserEnv({
"LAST_CLIENT_CONNECTED": JSON.stringify(new Date())
@ -154,20 +157,14 @@ export function onMalformed() {
}
}
export const onOnline = () => {
removeToast("offline");
success(t("Reconnected to the message broker."), t("Online"));
dispatchNetworkUp("user.mqtt", now());
};
export const onReconnect = () =>
warning(t("Attempting to reconnect to the message broker"),
t("Offline"), "yellow", "offline");
export const onOffline = () => {
dispatchNetworkDown("user.mqtt", now());
error(t(Content.MQTT_DISCONNECTED), t("Error"), "red", "offline");
};
export const onOnline =
() => {
success(t("Reconnected to the message broker."), t("Online"));
dispatchNetworkUp("user.mqtt", now());
};
export const onReconnect =
() => warning(t("Attempting to reconnect to the message broker"),
t("Offline"), "yellow");
export function onPublicBroadcast(payl: unknown) {
console.log(FbjsEventName.publicBroadcast, payl);

View File

@ -2,27 +2,6 @@ import { trim } from "./util";
export namespace ToolTips {
// Farm Designer: Groups
export const SORT_DESCRIPTION =
trim(`When executing a sequence over a Group of locations, FarmBot will
travel to each group member in the order of the chosen sort method.
If the random option is chosen, FarmBot will travel in a random order
every time, so the ordering shown below will only be representative.`);
export const CRITERIA_SELECTION_COUNT =
trim(`Filter additions can only be removed by changing filters.
Click and drag in the map to modify selection filters.
Filters will be applied at the time of sequence execution. The final
selection at that time may differ from the selection currently
displayed.`);
export const CRITERIA_ALPHA_FEATURE =
trim(`Group filters is a new feature under active development.
Use with caution.`);
export const DOT_NOTATION_TIP =
trim(`Tip: Use dot notation (i.e., 'meta.color') to access meta fields.`);
// Controls
export const MOVE =
trim(`Use these manual control buttons to move FarmBot in realtime.
@ -33,12 +12,7 @@ export namespace ToolTips {
export const WEBCAM =
trim(`If you have a webcam, you can view the video stream in this widget.
Press the edit button to update and save your webcam URL.
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.`);
Press the edit button to update and save your webcam URL.`);
export const PERIPHERALS =
trim(`Use these toggle switches to control FarmBot's peripherals in
@ -52,19 +26,10 @@ export namespace ToolTips {
export const SENSOR_HISTORY =
trim(`View and filter historical sensor reading data.`);
// FarmBot OS Settings: Firmware
export const FIRMWARE_VALUE_API =
trim(`Firmware value from your choice in the dropdown to the left, as
understood by the Web App.`);
// Device
export const OS_SETTINGS =
trim(`View and change device settings.`);
export const FIRMWARE_VALUE_FBOS =
trim(`Firmware value reported from the firmware, as understood by
FarmBot OS.`);
export const FIRMWARE_VALUE_MCU =
trim(`Firmware value reported from the firmware.`);
// Hardware Settings
export const HW_SETTINGS =
trim(`Change settings of your FarmBot hardware with the fields below.
Caution: Changing these settings to extreme values can cause hardware
@ -73,6 +38,18 @@ export namespace ToolTips {
Tip: Recalibrate FarmBot after changing settings and test a
few sequences to verify that everything works as expected.`);
export const PIN_BINDINGS =
trim(`Assign an action or sequence to execute when a Raspberry Pi
GPIO pin is activated.`);
export const PIN_BINDING_WARNING =
trim(`Warning: Binding to a pin without a physical button and
pull-down resistor connected may put FarmBot into an unstable state.`);
// Connectivity
export const CONNECTIVITY =
trim(`Diagnose connectivity issues with FarmBot and the browser.`);
// Hardware Settings: Homing and Calibration
export const HOMING_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
@ -225,16 +202,13 @@ export namespace ToolTips {
trim(`The number of the pin to guard. This pin will be set to the specified
state after the duration specified by TIMEOUT.`);
// Hardware Settings: Pin Bindings
export const PIN_BINDINGS =
trim(`Assign an action or sequence to execute when a Raspberry Pi
GPIO pin is activated.`);
export const PIN_BINDING_WARNING =
trim(`Warning: Binding to a pin without a physical button and
pull-down resistor connected may put FarmBot into an unstable state.`);
// Farmware
export const FARMWARE =
trim(`Manage Farmware (plugins).`);
export const FARMWARE_LIST =
trim(`View, select, and install new Farmware.`);
export const PHOTOS =
trim(`Take and view photos with your FarmBot's camera.`);
@ -258,6 +232,9 @@ export namespace ToolTips {
You can also edit, copy, and delete existing sequences;
assign a color; and give your commands custom names.`);
export const SEQUENCE_LIST =
trim(`Here is the list of all of your sequences. Click one to edit.`);
export const DEFAULT_VALUE =
trim(`Select a location to be used as the default value for this variable.
If the sequence is ever run without the variable explicitly set to
@ -335,7 +312,6 @@ export namespace ToolTips {
export const TAKE_PHOTO =
trim(`Snaps a photo using the device camera. Select the camera type
on the Device page.`);
export const EMERGENCY_LOCK =
trim(`Stops a device from moving until it is unlocked by a user.`);
@ -346,7 +322,10 @@ export namespace ToolTips {
trim(`The Mark As step allows FarmBot to programmatically edit the
properties of the UTM, plants, and weeds from within a sequence.
For example, you can mark a plant as "planted" during a seeding
sequence or mark a weed as "removed" after removing it.`);
sequence or delete a weed after removing it.`);
export const REBOOT =
trim(`Power cycle FarmBot's onboard computer.`);
export const SET_SERVO_ANGLE =
trim(`Move a servo to the provided angle. An angle of 90 degrees
@ -359,9 +338,6 @@ export namespace ToolTips {
export const MOVE_TO_HOME =
trim(`Move FarmBot to home for the provided axis.`);
export const ASSERTION =
trim(`Evaluate Lua commands. For power users and software developers.`);
export const FIRMWARE_ACTION =
trim(`FarmBot OS or micro-controller firmware action.`);
@ -387,6 +363,20 @@ export namespace ToolTips {
growing at the same or different times. Multiple regimens can be
applied to any one plant.`);
export const REGIMEN_LIST =
trim(`This is a list of all of your regimens. Click one to begin
editing it.`);
// Tools
export const TOOL_LIST =
trim(`This is a list of all your FarmBot tools and seed containers.
Click the Edit button to add, edit, or delete tools or seed containers.`);
export const TOOLBAY_LIST =
trim(`Tool slots are where you store your FarmBot tools and seed
containers, which should be reflective of your real FarmBot hardware
configuration.`);
// Logs
export const LOGS =
trim(`View and filter log messages.`);
@ -409,6 +399,16 @@ export namespace ToolTips {
export const FIRMWARE_DEBUG_MESSAGES =
trim(`Log all debug received from firmware (clears after refresh).`);
export const MESSAGES =
trim(`View messages.`);
// App
export const LABS =
trim(`Customize your web app experience.`);
export const TOURS =
trim(`Take a guided tour of the Web App.`);
}
export namespace Content {
@ -512,9 +512,11 @@ export namespace Content {
real account at`);
// App Settings
export const TIME_FORMAT_24_HOUR =
trim(`Display time using the 24-hour notation,
i.e., 23:00 instead of 11:00pm`);
export const CONFIRM_STEP_DELETION =
trim(`Show a confirmation dialog when deleting a sequence step.`);
export const CONFIRM_SEQUENCE_DELETION =
trim(`Show a confirmation dialog when deleting a sequence.`);
export const HIDE_WEBCAM_WIDGET =
trim(`If not using a webcam, use this setting to remove the
@ -524,6 +526,14 @@ export namespace Content {
trim(`If not using sensors, use this setting to remove the
widget from the Controls page.`);
export const DYNAMIC_MAP_SIZE =
trim(`Change the garden map size based on axis length.
A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
the HARDWARE widget. Overrides MAP SIZE values.`);
export const PLANT_ANIMATIONS =
trim(`Enable plant animations in the garden map.`);
export const BROWSER_SPEAK_LOGS =
trim(`Have the browser also read aloud log messages on the
"Speak" channel that are spoken by FarmBot.`);
@ -536,33 +546,6 @@ export namespace Content {
trim(`Warning! When enabled, any unsaved changes
will be discarded when refreshing or closing the page. Are you sure?`);
export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
trim(`Confirm when unlocking FarmBot after an emergency stop.`);
export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
trim(`Warning! When disabled, clicking the UNLOCK button will immediately
unlock FarmBot instead of confirming that it is safe to do so.
As a result, double-clicking the E-STOP button may not stop FarmBot.
Are you sure you want to disable this feature?`);
export const USER_INTERFACE_READ_ONLY_MODE =
trim(`Disallow account data changes. This does
not prevent Farmwares or FarmBot OS from changing settings.`);
// Sequence Settings
export const CONFIRM_STEP_DELETION =
trim(`Show a confirmation dialog when deleting a sequence step.`);
export const CONFIRM_SEQUENCE_DELETION =
trim(`Show a confirmation dialog when deleting a sequence.`);
export const SHOW_PINS =
trim(`Show raw pin lists in Read Sensor, Control Peripheral, and
If Statement steps.`);
export const EXPAND_STEP_OPTIONS =
trim(`Choose whether advanced step options are open or closed by default.`);
export const DISCARD_UNSAVED_SEQUENCE_CHANGES =
trim(`Don't ask about saving sequence work before
closing browser tab. Warning: may cause loss of data.`);
@ -571,19 +554,34 @@ export namespace Content {
trim(`Warning! When enabled, any unsaved changes to sequences
will be discarded when refreshing or closing the page. Are you sure?`);
// Farm Designer Settings
export const PLANT_ANIMATIONS =
trim(`Enable plant animations in the garden map.`);
export const VIRTUAL_TRAIL =
trim(`Display a virtual trail for FarmBot in the garden map to show
movement and watering history while the map is open. Toggling this setting
will clear data for the current trail.`);
export const DYNAMIC_MAP_SIZE =
trim(`Change the garden map size based on axis length.
A value must be input in AXIS LENGTH and STOP AT MAX must be enabled in
the HARDWARE widget. Overrides MAP SIZE values.`);
export const TIME_FORMAT_24_HOUR =
trim(`Display time using the 24-hour notation,
i.e., 23:00 instead of 11:00pm`);
export const SHOW_PINS =
trim(`Show raw pin lists in Read Sensor, Control Peripheral, and
If Statement steps.`);
export const EXPAND_STEP_OPTIONS =
trim(`Choose whether advanced step options are open or closed by default.`);
export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
trim(`Confirm when unlocking FarmBot after an emergency stop.`);
export const USER_INTERFACE_READ_ONLY_MODE =
trim(`Disallow account data changes. This does
not prevent Farmwares or FarmBot OS from changing settings.`);
export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
trim(`Warning! When disabled, clicking the UNLOCK button will immediately
unlock FarmBot instead of confirming that it is safe to do so.
As a result, double-clicking the E-STOP button may not stop FarmBot.
Are you sure you want to disable this feature?`);
export const MAP_SIZE =
trim(`Specify custom map dimensions (in millimeters).
@ -602,41 +600,13 @@ export namespace Content {
export const CONFIRM_PLANT_DELETION =
trim(`Show a confirmation dialog when deleting a plant.`);
// FarmBot OS Settings
export const DIFFERENT_TZ_WARNING =
trim(`Note: The selected timezone for your FarmBot is different than
your local browser time.`);
// Device
export const NOT_HTTPS =
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
export const OS_BETA_RELEASES =
trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
FarmBot system stability. Are you sure?`);
export const DEVICE_NEVER_SEEN =
trim(`The device has never been seen. Most likely,
there is a network connectivity issue on the device's end.`);
export const TOO_OLD_TO_UPDATE =
trim(`Please re-flash your FarmBot's SD card.`);
export const OS_AUTO_UPDATE =
trim(`When enabled, FarmBot OS will automatically download and install
software updates at the chosen time.`);
export const AUTO_SYNC =
trim(`When enabled, device resources such as sequences and regimens
will be sent to the device automatically. This removes the need to push
"SYNC" after making changes in the web app. Changes to running
sequences and regimens while auto sync is enabled will result in
instantaneous change.`);
// FarmBot OS Settings: Power and Reset
export const RESTART_FARMBOT =
trim(`This will restart FarmBot's Raspberry Pi and controller
software.`);
export const SHUTDOWN_FARMBOT =
trim(`This will shutdown FarmBot's Raspberry Pi. To turn it
back on, unplug FarmBot and plug it back in.`);
export const CONTACT_SYSADMIN =
trim(`Please contact the system(s) administrator(s) and ask them to enable
HTTPS://`);
export const FACTORY_RESET_WARNING =
trim(`Factory resetting your FarmBot will destroy all data on the device,
@ -654,6 +624,10 @@ export namespace Content {
not delete data stored in your web app account. Are you sure you wish
to continue?`);
export const MCU_RESET_ALERT =
trim(`Warning: This will reset all hardware settings to the default values.
Are you sure you wish to continue?`);
export const AUTO_FACTORY_RESET =
trim(`Automatically factory reset when the WiFi network cannot be
detected. Useful for network changes.`);
@ -662,26 +636,54 @@ export namespace Content {
trim(`Time in minutes to attempt connecting to WiFi before a factory
reset.`);
export const NOT_HTTPS =
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
export const DIFFERENT_TZ_WARNING =
trim(`Note: The selected timezone for your FarmBot is different than
your local browser time.`);
export const CONTACT_SYSADMIN =
trim(`Please contact the system(s) administrator(s) and ask them to enable
HTTPS://`);
export const RESTART_FARMBOT =
trim(`This will restart FarmBot's Raspberry Pi and controller
software.`);
// FarmBot OS Settings: Firmware
export const RESTART_FIRMWARE =
trim(`Restart the Farmduino or Arduino firmware.`);
// Hardware Settings: Danger Zone
export const OS_AUTO_UPDATE =
trim(`When enabled, FarmBot OS will automatically download and install
software updates at the chosen time.`);
export const AUTO_SYNC =
trim(`When enabled, device resources such as sequences and regimens
will be sent to the device automatically. This removes the need to push
"SYNC" after making changes in the web app. Changes to running
sequences and regimens while auto sync is enabled will result in
instantaneous change.`);
export const SHUTDOWN_FARMBOT =
trim(`This will shutdown FarmBot's Raspberry Pi. To turn it
back on, unplug FarmBot and plug it back in.`);
export const OS_BETA_RELEASES =
trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
FarmBot system stability. Are you sure?`);
export const DIAGNOSTIC_CHECK =
trim(`Save snapshot of FarmBot OS system information, including
user and device identity, to the database. A code will be returned
that you can provide in support requests to allow FarmBot to look up
data relevant to the issue to help us identify the problem.`);
export const DEVICE_NEVER_SEEN =
trim(`The device has never been seen. Most likely,
there is a network connectivity issue on the device's end.`);
export const TOO_OLD_TO_UPDATE =
trim(`Please re-flash your FarmBot's SD card.`);
// Hardware Settings
export const RESTORE_DEFAULT_HARDWARE_SETTINGS =
trim(`Restoring hardware parameter defaults will destroy the
current settings, resetting them to default values.`);
export const MCU_RESET_ALERT =
trim(`Warning: This will reset all hardware settings to the default values.
Are you sure you wish to continue?`);
// App
export const APP_LOAD_TIMEOUT_MESSAGE =
trim(`App could not be fully loaded, we recommend you try
@ -709,6 +711,10 @@ export namespace Content {
broken and may break or otherwise hinder your usage of the rest of the
app. This feature may disappear or break at any time.`);
export const NEW_TOS =
trim(`Before logging in, you must agree to our latest Terms of Service and
Privacy Policy`);
export const FORCE_REFRESH_CONFIRM =
trim(`A new version of the FarmBot web app has been released.
Refresh page?`);
@ -740,15 +746,6 @@ export namespace Content {
encoders, stall detection, or endstops enabled for the chosen axis.
Enable endstops, encoders, or stall detection from the Device page for: `);
export const REBOOT_STEP =
trim(`Power cycle FarmBot's onboard computer.`);
export const SHUTDOWN_STEP =
trim(`Power down FarmBot's onboard computer.`);
export const ESTOP_STEP =
trim(`Unlocking a device requires user intervention.`);
export const IN_USE =
trim(`Used in another resource. Protected from deletion.`);
@ -776,7 +773,7 @@ export namespace Content {
trim(`Click and drag or use the inputs to draw a weed.`);
export const BOX_SELECT_DESCRIPTION =
trim(`Drag a box around the items you would like to select.
trim(`Drag a box around the plants you would like to select.
Press the back arrow to exit.`);
export const SAVED_GARDENS =
@ -822,8 +819,7 @@ export namespace Content {
export const MOUNTED_TOOL =
trim(`The tool currently mounted to the UTM can be set here or by using
a MARK AS step in a sequence. Use the verify button or read the tool
verification pin in a sequence to verify that a tool is attached.`);
a MARK AS step in a sequence.`);
// Farm Events
export const NOTHING_SCHEDULED =
@ -835,6 +831,10 @@ export namespace Content {
regimen tasks. Consider rescheduling this event to tomorrow if
this is a concern.`);
export const INVALID_RUN_TIME =
trim(`This event does not appear to have a valid run time.
Perhaps you entered bad dates?`);
export const FARM_EVENT_TZ_WARNING =
trim(`Note: Times displayed according to FarmBot's local time, which
is currently different from your browser's time. Timezone data is
@ -849,14 +849,27 @@ export namespace Content {
trim(`You haven't made any sequences or regimens yet. To add an event,
first create a sequence or regimen.`);
// Groups
export const SORT_DESCRIPTION =
trim(`When executing a sequence over a Group of locations, FarmBot will
travel to each group member in the order of the chosen sort method.
If the random option is chosen, FarmBot will travel in a random order
every time, so the ordering shown below will only be representative.`);
export const CRITERIA_SELECTION_COUNT =
trim(`Criteria additions can only be removed by changing criteria.
Click and drag in the map to modify selection criteria.
Criteria will be applied at the time of sequence execution. The final
selection at that time may differ from the selection currently
displayed.`);
// Farmware
export const NO_IMAGES_YET =
trim(`You haven't yet taken any photos with your FarmBot.
Once you do, they will show up here.`);
export const PROCESSING_PHOTO =
trim(`Processing now. Results usually available in one minute.
Check log messages for result status.`);
trim(`Processing now. Results usually available in one minute.`);
export const NOT_AVAILABLE_WHEN_OFFLINE =
trim(`Not available when device is offline.`);
@ -932,8 +945,6 @@ export namespace TourContent {
}
export enum DeviceSetting {
axisHeadingLabels = ``,
// Homing and calibration
homingAndCalibration = `Homing and Calibration`,
homing = `Homing`,
@ -985,11 +996,6 @@ export enum DeviceSetting {
// Pin Guard
pinGuard = `Pin Guard`,
pinGuard1 = `Pin Guard 1`,
pinGuard2 = `Pin Guard 2`,
pinGuard3 = `Pin Guard 3`,
pinGuard4 = `Pin Guard 4`,
pinGuard5 = `Pin Guard 5`,
// Danger Zone
dangerZone = `Danger Zone`,
@ -997,8 +1003,6 @@ export enum DeviceSetting {
// Pin Bindings
pinBindings = `Pin Bindings`,
savedPinBindings = `Saved pin bindings`,
addNewPinBinding = `Add new pin binding`,
// FarmBot OS
farmbot = `FarmBot`,
@ -1016,6 +1020,7 @@ export enum DeviceSetting {
powerAndReset = `Power and Reset`,
restartFarmbot = `Restart Farmbot`,
shutdownFarmbot = `Shutdown Farmbot`,
restartFirmware = `Restart Firmware`,
factoryReset = `Factory Reset`,
autoFactoryReset = `Automatic Factory Reset`,
connectionAttemptPeriod = `Connection Attempt Period`,
@ -1033,7 +1038,6 @@ export enum DeviceSetting {
// Firmware
firmwareSection = `Firmware`,
restartFirmware = `Restart Firmware`,
flashFirmware = `Flash firmware`,
}
@ -1135,8 +1139,7 @@ export enum Actions {
// Designer
SEARCH_QUERY_CHANGE = "SEARCH_QUERY_CHANGE",
SELECT_POINT = "SELECT_POINT",
SET_SELECTION_POINT_TYPE = "SET_SELECTION_POINT_TYPE",
SELECT_PLANT = "SELECT_PLANT",
TOGGLE_HOVERED_PLANT = "TOGGLE_HOVERED_PLANT",
TOGGLE_HOVERED_POINT = "TOGGLE_HOVERED_POINT",
HOVER_PLANT_LIST_ITEM = "HOVER_PLANT_LIST_ITEM",
@ -1145,11 +1148,9 @@ export enum Actions {
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",
OF_SEARCH_RESULTS_NO = "OF_SEARCH_RESULTS_NO",
CHOOSE_LOCATION = "CHOOSE_LOCATION",
SET_DRAWN_POINT_DATA = "SET_DRAWN_POINT_DATA",
SET_DRAWN_WEED_DATA = "SET_DRAWN_WEED_DATA",
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
TRY_SORT_TYPE = "TRY_SORT_TYPE",
SET_SETTINGS_SEARCH_TERM = "SET_SETTINGS_SEARCH_TERM",
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
// Regimens

View File

@ -38,7 +38,6 @@ $pink: #ebb;
$light_red: #e99;
$red: #e66;
$dark_red: #f00;
$medium_dark_red: #c00;
$darkest_red: #900;
$panel_green: #35761b;
$panel_light_green: #f3f9f1;

View File

@ -120,68 +120,46 @@
.thin-search-wrapper {
width: 100%;
.thin-search {
.spinner-container {
.text-input-wrapper {
position: relative;
margin: 1rem;
border-bottom: 1px solid $dark_gray;
&:before,
&:after {
content: "";
position: absolute;
top: 0;
bottom: 0;
background: $dark_gray;
width: 1px;
height: 3px;
}
&:before {
left: 0;
}
&:after {
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
margin-right: 1rem;
}
.text-input-wrapper {
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;
}
}
i {
font-size: 1.5rem;
}
input {
background: transparent;
box-shadow: none !important;
padding-left: 3rem !important;
font-size: 1.4rem !important;
&:active,
&:focus {
background: transparent !important;
}
&::-webkit-input-placeholder {
color: $placeholder_gray;
}
.fa-search {
position: absolute;
top: 0.8rem;
left: 1rem;
cursor: default !important;
}
}
input {
background: transparent;
box-shadow: none !important;
padding-left: 3rem !important;
font-size: 1.4rem !important;
&:active,
&:focus {
background: transparent !important;
}
&::-webkit-input-placeholder {
color: $placeholder_gray;
}
}
}
@ -207,41 +185,30 @@
}
}
%panel-item-base {
text-align: right;
font-size: 1rem;
padding-right: 1rem;
line-height: 3rem;
float: right;
}
.plant-search-item,
.group-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
img {
margin-right: 0.5rem;
height: 3rem;
width: 3rem;
}
&.quick-del {
&:hover {
background: lighten($red, 10%) !important;
&:after {
content: "x";
margin-left: 1rem;
color: $darkest_red;
font-weight: bold;
}
}
margin: 0 1rem 0 0;
height: 4rem;
width: 4rem;
}
}
%panel-item-base {
text-align: right;
font-size: 1rem;
padding-top: 1.4rem;
padding-right: 1rem;
float: right;
}
.plant-search-item-age {
@extend %panel-item-base;
}
.group-item-count {
@extend %panel-item-base;
padding-top: 0.6rem;
line-height: 1rem;
}
.plant-search-item-name {
display: inline-block;
@ -252,27 +219,24 @@
text-overflow: ellipsis;
margin-left: 1rem;
}
.weed-search-item,
.point-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
.saucer {
display: inline-block;
height: 3rem;
width: 3rem;
margin: 0 1rem 0 0;
height: 2rem;
width: 2rem;
vertical-align: middle;
margin-right: 0.25rem;
}
}
.weed-search-item-info,
.point-search-item-info {
text-align: right;
font-size: 1rem;
padding-top: 0.6rem;
padding-right: 1rem;
line-height: 3rem;
float: right;
}
.weed-search-item-name,
.point-search-item-name {
display: inline-block;
vertical-align: middle;
@ -280,34 +244,19 @@
width: 40%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 1.25rem;
margin-left: 1rem;
}
.tool-search-item,
.tool-slot-search-item {
line-height: 4rem;
cursor: pointer;
.row {
margin-left: 0;
margin-right: 0;
}
.tool-slot-search-item-name {
margin-left: -1rem;
}
p {
font-size: 1rem;
line-height: 4rem;
&.tool-status,
&.tool-slot-position {
float: right;
}
}
svg {
vertical-align: middle;
}
.tool-slot-position-info {
padding: 0;
padding-right: 1.75rem;
}
}
.thin-search {
.spinner-container {
position: absolute;
top: 0;
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
margin-right: 1rem;
}
}
@ -335,17 +284,11 @@
}
.map-point {
cursor: pointer !important;
stroke-width: 2;
stroke-opacity: 0.3;
fill-opacity: 0.1;
}
.map-weed {
cursor: pointer !important;
}
.weed-image,
.plant-image {
transform-origin: bottom;
transform-box: fill-box;
@ -394,9 +337,6 @@
fill: $white;
stroke: $white;
}
&:hover {
opacity: 0.15;
}
}
}
@ -564,10 +504,10 @@
cursor: pointer;
}
.more-bugs,
.select-mode,
.move-to-mode {
margin: auto;
margin-top: 1rem;
button {
margin-right: 1rem;
}
p {
text-align: center;
padding-top: 2rem;

View File

@ -291,19 +291,9 @@
.panel-action-buttons {
position: absolute;
z-index: 9;
height: 16rem;
width: 100%;
background: $panel_medium_light_gray;
padding: 0.5rem;
&.status {
height: 20rem;
}
&.more {
height: 23rem;
}
&.more.status {
height: 26rem;
}
button {
margin: 0.5rem;
float: left;
@ -312,15 +302,10 @@
min-width: -webkit-fill-available;
margin-bottom: 0px;
margin-left: .5rem;
margin-top: 0;
}
.button-row {
float: left;
width: 100%;
margin-bottom: 1rem;
}
.filter-search {
padding-right: 1rem;
}
.plant-status-bulk-update {
display: inline-flex;
@ -334,35 +319,17 @@
line-height: 4.1rem;
}
}
.more {
float: right;
cursor: pointer;
margin-right: 1rem;
line-height: 2.5rem;
p {
display: inline;
font-size: 1.4rem;
margin-right: 1rem;
}
}
}
.panel-content {
padding-top: 16rem;
padding-top: 15rem;
padding-right: 0;
padding-left: 0;
padding-bottom: 5rem;
max-height: calc(100vh - 13rem);
overflow-y: auto;
overflow-x: hidden;
&.status {
padding-top: 20rem;
}
&.more {
padding-top: 23rem;
}
&.more.status {
padding-top: 26rem;
}
.plant-search-item,
.group-search-item { pointer-events: none; }
}
}
@ -398,13 +365,9 @@
margin-top: 1rem;
}
}
.point-color-input {
div[class*=col-] {
padding-left: 0.5rem;
}
.saucer {
margin-top: 2.75rem;
}
.saucer {
margin: 1rem;
margin-left: 2rem;
}
.delete-row {
margin: 1.5rem;
@ -414,39 +377,16 @@
.weed-info-panel-content,
.point-info-panel-content {
.point-color-input {
div[class*=col-] {
padding-left: 0.5rem;
}
.saucer {
margin-top: 4.5rem;
}
.saucer {
margin: 1rem;
margin-left: 2rem;
}
.fb-button & .red {
display: block;
margin-top: 3rem;
}
font-size: 1.4rem;
p {
margin-top: 1rem;
margin-bottom: 0.5rem !important;
font-size: 1.2rem;
}
.weed-removal-method-section {
.weed-removal-method {
display: flex;
input {
margin: 0;
width: 10%;
box-shadow: none;
}
label {
margin: 0;
margin-top: auto;
font-size: 1.25rem;
font-weight: normal;
}
}
}
}
@ -571,7 +511,6 @@
margin-top: 1rem;
p {
font-size: 1.25rem;
margin-bottom: 0.5rem !important;
}
}
input {
@ -618,8 +557,22 @@
overflow-x: hidden;
.tool-search-item,
.tool-slot-search-item {
line-height: 4rem;
cursor: pointer;
margin-left: -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 {
.bp3-button {
min-height: 2.5rem;
@ -632,6 +585,13 @@
line-height: 2rem;
}
}
svg {
vertical-align: middle;
}
.tool-slot-position-info {
padding: 0;
padding-right: 1rem;
}
}
.mounted-tool-header {
display: flex;
@ -690,7 +650,6 @@
margin-top: 1rem;
&.red {
float: left;
margin-bottom: 1rem;
}
}
svg {
@ -700,19 +659,6 @@
height: 10rem;
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 {
.filter-search {
margin-bottom: 1rem;
@ -804,101 +750,19 @@
}
}
.no-pad {
padding: 0;
}
.settings-panel-content {
padding: 0;
margin-top: 6rem;
max-height: calc(100vh - 15rem);
overflow-y: auto;
overflow-x: hidden;
margin-top: 5rem;
padding-bottom: 5rem;
.section {
margin-bottom: 2rem;
button {
margin-top: 1.75rem;
}
.bulk-expand-controls {
p {
padding: 0.5rem;
margin-left: 1rem;
}
.row:first-child {
margin-right: 0;
margin-top: 1rem;
}
.row:nth-child(2) {
padding-left: 1.5rem;
padding-right: 3rem;
}
.label-headings {
margin-right: 2rem;
label {
line-height: 1rem;
}
}
.release-notes-wrapper {
float: right !important;
}
.network-not-found-timer {
margin-top: 1rem;
}
.pin-guard-input-row {
.row {
margin-left: -15px;
margin-right: -15px;
padding-left: 0;
padding-right: 1rem;
margin-bottom: 1rem;
}
}
.pin-bindings {
margin-right: 1rem;
.row {
padding-left: 0;
padding-right: 0;
margin-left: 1rem;
margin-right: 0;
margin-top: 1rem;
}
div[class*=col-] {
padding: 0;
padding-right: 1rem;
}
.bindings-list {
margin-left: -5px;
.binding-action {
font-weight: bold;
font-size: 1.2rem;
}
}
.pin-binding-input-rows {
margin-right: 1rem;
margin-left: -15px;
label {
margin-left: 1rem !important;
}
.green {
float: left;
margin-left: 1rem;
}
.row:last-child {
margin-top: 0;
}
}
.stock-pin-bindings-button {
display: inline;
button {
margin: 0;
margin-top: 0.5rem;
}
}
}
.fb-button {
margin-top: 0.5rem;
}
label {
margin: 0 !important;
line-height: 3rem;
}
.bp3-popover-wrapper {
display: inline;
float: none;
}
.map-size-inputs {
.row {
@ -908,31 +772,6 @@
margin-top: 0.5rem;
}
}
.help-icon {
margin-left: 1rem;
}
.all-settings-content {
max-height: calc(100vh - 22rem);
overflow-y: auto;
overflow-x: hidden;
margin-top: 1rem;
padding-left: 1rem;
.expandable-header {
margin-top: 1.5rem;
margin-bottom: 0;
}
.section {
margin-bottom: 0;
}
}
.designer-settings {
max-height: calc(100vh - 14rem);
overflow-y: auto;
overflow-x: hidden;
margin-right: -10px;
padding-right: 1rem;
padding-left: 1rem;
}
.designer-setting {
&.disabled {
input {
@ -968,19 +807,6 @@
}
}
.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,
.zones-inventory-panel,
.groups-panel {
@ -998,11 +824,6 @@
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 5rem;
.clear-day-criteria,
.clear-point-ids,
.clear-criteria {
margin-top: 0.2rem;
}
.group-member-display {
i[class*=fa-caret-] {
float: right;
@ -1029,47 +850,20 @@
.criteria-heading {
margin-top: 0;
}
.alpha-icon {
display: inline;
float: none !important;
margin-left: 1rem;
color: $orange;
font-size: 1.4rem;
}
p {
&.category {
display: block;
padding-top: 1rem;
padding-bottom: 1rem;
text-transform: none;
font-size: 1.2rem;
font-weight: bold;
}
}
.bp3-popover-wrapper {
float: right;
}
.fb-button {
margin-top: 0.5rem;
}
.point-type-section,
.criteria-checkbox-list-item {
.fb-checkbox {
display: inline;
margin-right: 1rem;
vertical-align: top;
}
p {
display: inline;
text-transform: uppercase;
}
input[type="text"] {
width: 50%;
height: 2rem;
}
}
.point-type-checkboxes {
.point-type-section {
.fb-checkbox {
display: inline;
margin-right: 1rem;
vertical-align: top;
}
p {
display: inline;
text-transform: uppercase;
}
.point-type-checkbox {
position: relative;
height: 2rem;
@ -1088,9 +882,19 @@
}
}
.plant-criteria-options,
.weed-criteria-options,
.point-criteria-options,
.tool-criteria-options {
margin-left: 3rem;
p {
&.category {
display: block;
padding-top: 1rem;
padding-bottom: 1rem;
text-transform: none;
font-size: 1.2rem;
font-weight: bold;
}
}
hr {
margin: 0.5rem;
}
@ -1120,13 +924,7 @@
margin-top: 1rem;
}
.day-criteria {
.criteria-checkbox-list-item {
margin-bottom: 1rem;
p {
vertical-align: middle;
}
}
.days-old-text {
p {
display: inline;
vertical-align: bottom;
}
@ -1134,7 +932,6 @@
line-height: 1.75rem;
}
}
.number-eq-criteria,
.string-eq-criteria {
margin-top: 1rem;
.row {
@ -1161,13 +958,19 @@
font-size: 1.2rem;
}
}
.fb-toggle-button {
width: 85px;
margin-top: 0;
&.red {
background: $dark_gray !important;
}
}
.clear-criteria {
margin-top: 2rem;
}
.basic,
.advanced {
margin-left: 1rem;
.filter-search {
height: 3rem;
margin-bottom: 1rem;
}
.day-criteria {
.row {
margin-left: 0;
@ -1179,17 +982,6 @@
}
}
.advanced {
.bp3-popover-wrapper {
display: inline;
float: none;
margin-left: 1rem;
font-size: 1.4rem;
}
.filter-search {
.bp3-popover-wrapper {
margin-left: 0;
}
}
.row {
margin-left: 0;
}
@ -1207,28 +999,29 @@
}
}
}
}
}
.group-member-count-breakdown {
margin-bottom: 1rem;
.manual-group-member-count,
.criteria-group-member-count {
div {
display: inline;
padding: 0.25rem;
font-size: 1.2rem;
.criteria-point-count-breakdown {
margin-bottom: 1rem;
.manual-group-member-count,
.criteria-group-member-count {
margin-left: 2rem;
div {
display: inline;
padding: 0.25rem;
font-size: 1.2rem;
border: 1px solid $panel_light_blue;
}
p {
display: inline;
margin-left: 1rem;
}
}
.criteria-group-member-count {
div {
border: 1px solid gray;
border-radius: 5px;
}
}
}
p {
display: inline;
margin-left: 0.5rem;
}
}
}
.criteria-options-menu {
label {
margin-right: 1rem;
}
}
@ -1237,7 +1030,6 @@
display: inline-block;
.row {
margin-left: 0;
margin-right: -2.5rem;
div[class*=col-] {
padding: 0;
text-align: center;
@ -1253,28 +1045,16 @@
margin-top: 0.5rem;
}
}
button {
margin-top: 2rem !important;
}
.edit-in-map {
float: right;
button {
margin: 1rem !important;
width: 5rem !important;
margin-right: 0 !important;
}
label {
margin-top: 1.1rem !important;
}
}
.location-selection-warning {
i,
p {
display: inline;
margin-right: 1rem;
color: $darkest_red;
}
}
}
.weeds-inventory-panel,

View File

@ -1314,50 +1314,6 @@ ul {
}
}
.update-resource-step {
.update-resource-step-resource {
margin-bottom: 1rem;
}
.update-resource-pair {
margin-top: 0;
margin-right: -2rem;
div[class*=col-] {
padding: 0;
padding-right: 2rem;
}
.custom-meta-field {
position: relative;
input {
height: 3rem;
}
.fa-undo {
position: absolute;
top: 0.65rem;
right: 0.5rem;
color: $medium_light_gray;
&:hover {
color: $dark_gray;
}
}
}
.custom-field-warning {
display: inline-block;
margin-top: 0.5rem;
i,
p {
display: inline;
cursor: default !important;
margin-right: 0.5rem;
color: $darkest_red;
}
.did-you-mean {
cursor: pointer !important;
font-weight: bold;
}
}
}
}
.farmware-name-manual-input {
margin-top: 1rem;
}
@ -1468,11 +1424,6 @@ ul {
button {
float: none !important;
}
.bp3-popover-wrapper {
display: inline;
margin-left: 0.5rem;
font-size: 1.3rem;
}
}
.problem-alert {

View File

@ -127,16 +127,6 @@ select {
background: $white;
margin-top: 0;
cursor: pointer;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $white;
opacity: 0.5;
}
&:checked:after {
content: "";
position: absolute;

View File

@ -106,7 +106,7 @@
&.take-photo-step {
background: $brown;
}
&.update-resource-step {
&.resource-update-step {
background: $brown;
}
&.set-servo-angle-step {
@ -136,9 +136,6 @@
&.reboot-step {
background: $brown;
}
&.shutdown-step {
background: $brown;
}
&.unknown-step {
background: $gray;
}
@ -229,7 +226,7 @@
&.take-photo-step a {
color: $dark_brown;
}
&.update-resource-step {
&.resource-update-step {
background: $light_brown;
}
&.set-servo-angle-step {
@ -256,9 +253,6 @@
&.emergency-stop-step {
background: $light_red;
}
&.shutdown-step {
background: $light_brown;
}
&.reboot-step {
background: $light_brown;
}

View File

@ -1,5 +1,3 @@
jest.mock("../../redux/store", () => ({ store: jest.fn() }));
import { botReducer, initialState } from "../reducer";
import { Actions } from "../../constants";
import { ControlPanelState, BotState } from "../interfaces";

View File

@ -1,12 +1,5 @@
jest.mock("../../actions", () => ({
toggleControlPanel: jest.fn(),
bulkToggleControlPanel: jest.fn(),
}));
import { fakeState } from "../../../__test_support__/fake_state";
const mockState = fakeState();
jest.mock("../../../redux/store", () => ({
store: { getState: () => mockState },
}));
import * as React from "react";
@ -16,7 +9,7 @@ import {
} from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { panelState } from "../../../__test_support__/control_panel_state";
import { toggleControlPanel, bulkToggleControlPanel } from "../../actions";
import { toggleControlPanel } from "../../actions";
describe("<Highlight />", () => {
const fakeProps = (): HighlightProps => ({
@ -32,24 +25,6 @@ describe("<Highlight />", () => {
wrapper.instance().componentDidMount();
expect(wrapper.state().className).toEqual("unhighlight");
});
it("doesn't hide: no search term", () => {
mockState.resources.consumers.farm_designer.settingsSearchTerm = "";
const wrapper = mount(<Highlight {...fakeProps()} />);
expect(wrapper.find("div").first().props().hidden).toEqual(false);
});
it("doesn't hide: matches search term", () => {
mockState.resources.consumers.farm_designer.settingsSearchTerm = "motor";
const wrapper = mount(<Highlight {...fakeProps()} />);
expect(wrapper.find("div").first().props().hidden).toEqual(false);
});
it("hides", () => {
mockState.resources.consumers.farm_designer.settingsSearchTerm = "encoder";
const wrapper = mount(<Highlight {...fakeProps()} />);
expect(wrapper.find("div").first().props().hidden).toEqual(true);
});
});
describe("maybeHighlight()", () => {
@ -103,11 +78,4 @@ describe("maybeOpenPanel()", () => {
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("closes other panels", () => {
location.search = "?highlight=motors";
maybeOpenPanel(panelState(), true)(jest.fn());
expect(toggleControlPanel).toHaveBeenCalledWith("motors");
expect(bulkToggleControlPanel).toHaveBeenCalledWith(false, true);
});
});

View File

@ -9,12 +9,11 @@ import { settingToggle } from "../../actions";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { DeviceSetting } from "../../../constants";
describe("<PinGuardMCUInputGroup/>", () => {
const fakeProps = (): PinGuardMCUInputGroupProps => {
return {
label: DeviceSetting.pinGuard1,
label: "Pin Guard 1",
pinNumKey: "pin_guard_1_pin_nr",
timeoutKey: "pin_guard_1_time_out",
activeStateKey: "pin_guard_1_active_state",

View File

@ -13,12 +13,11 @@ import {
import { TaggedFirmwareConfig } from "farmbot";
import { FBSelect } from "../../../ui";
import { updateMCU } from "../../actions";
import { DeviceSetting } from "../../../constants";
describe("<PinNumberDropdown />", () => {
const fakeProps =
(firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({
label: DeviceSetting.pinGuard1,
label: "Pin Guard 1",
pinNumKey: "pin_guard_1_pin_nr",
timeoutKey: "pin_guard_1_time_out",
activeStateKey: "pin_guard_1_active_state",

View File

@ -43,6 +43,16 @@ describe("<BoardType/>", () => {
expect(wrapper.text()).toContain("Farmduino");
});
it("sets sending status", () => {
const wrapper = mount<BoardType>(<BoardType {...fakeProps()} />);
expect(wrapper.state().sending).toBeFalsy();
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: true, consistent: false });
wrapper.setProps(p);
wrapper.mount();
expect(wrapper.state().sending).toBeTruthy();
});
it("calls updateConfig", () => {
const p = fakeProps();
const wrapper = mount<BoardType>(<BoardType {...p} />);

View File

@ -11,15 +11,12 @@ import { OsUpdateButton } from "../os_update_button";
import { OsUpdateButtonProps } from "../interfaces";
import { ShouldDisplay } from "../../../interfaces";
import { Content } from "../../../../constants";
import { ConfigurationName } from "farmbot";
const UPDATE_CHANNEL = "update_channel" as ConfigurationName;
describe("<OsUpdateButton/>", () => {
beforeEach(() => {
bot.currentOSVersion = "6.1.6";
bot.hardware.informational_settings.controller_version = "6.1.6";
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = "stable";
bot.hardware.configuration.beta_opt_in = false;
});
const fakeProps = (): OsUpdateButtonProps => ({
@ -36,6 +33,7 @@ describe("<OsUpdateButton/>", () => {
availableVersion: string | undefined;
availableBetaVersion: string | undefined;
availableBetaCommit: string | undefined;
betaOptIn: boolean | undefined;
onBeta: boolean | undefined;
update_available?: boolean | undefined;
shouldDisplay: ShouldDisplay;
@ -48,6 +46,7 @@ describe("<OsUpdateButton/>", () => {
availableVersion: "6.1.6",
availableBetaVersion: undefined,
availableBetaCommit: undefined,
betaOptIn: false,
onBeta: false,
shouldDisplay: () => false,
update_channel: "stable",
@ -105,7 +104,7 @@ describe("<OsUpdateButton/>", () => {
expected: Results) => {
const {
installedVersion, installedCommit, onBeta, update_available,
availableVersion, availableBetaVersion, availableBetaCommit,
availableVersion, availableBetaVersion, availableBetaCommit, betaOptIn,
shouldDisplay, update_channel,
} = testProps;
bot.hardware.informational_settings.controller_version = installedVersion;
@ -116,7 +115,9 @@ describe("<OsUpdateButton/>", () => {
bot.currentOSVersion = availableVersion;
bot.currentBetaOSVersion = availableBetaVersion;
bot.currentBetaOSCommit = availableBetaCommit;
(bot.hardware.configuration[UPDATE_CHANNEL] as string) = update_channel;
bot.hardware.configuration.beta_opt_in = betaOptIn;
// tslint:disable-next-line:no-any
(bot.hardware.configuration as any).update_channel = update_channel;
const p = fakeProps();
p.shouldDisplay = shouldDisplay;
@ -155,7 +156,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableVersion = undefined;
testProps.update_channel = "beta";
testProps.betaOptIn = true;
const expectedResults = cantConnect("release server");
testButtonState(testProps, expectedResults);
});
@ -165,7 +166,7 @@ describe("<OsUpdateButton/>", () => {
testProps.installedVersion = "6.1.6";
testProps.availableVersion = undefined;
testProps.availableBetaVersion = "6.1.7-beta";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
const expectedResults = updateNeeded("6.1.7-beta");
testButtonState(testProps, expectedResults);
});
@ -174,7 +175,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = undefined;
testProps.update_channel = "beta";
testProps.betaOptIn = true;
const expectedResults = upToDate("6.1.6");
testButtonState(testProps, expectedResults);
});
@ -204,7 +205,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.5";
testProps.availableBetaVersion = "7.0.0-beta";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
const expectedResults = updateNeeded("7.0.0-beta");
testButtonState(testProps, expectedResults);
});
@ -213,7 +214,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = "6.1.6-beta";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
const expectedResults = upToDate("6.1.6");
testButtonState(testProps, expectedResults);
});
@ -222,7 +223,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = "6.1.6-beta";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
testProps.onBeta = true;
const expectedResults = updateNeeded("6.1.6");
testButtonState(testProps, expectedResults);
@ -232,7 +233,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.availableBetaVersion = "6.1.6-beta";
testProps.update_channel = "stable";
testProps.betaOptIn = false;
testProps.onBeta = true;
const expectedResults = updateNeeded("6.1.6");
testButtonState(testProps, expectedResults);
@ -242,7 +243,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.7";
testProps.availableBetaVersion = "6.1.7-beta";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
testProps.onBeta = true;
const expectedResults = upToDate("6.1.7-beta");
testButtonState(testProps, expectedResults);
@ -252,7 +253,7 @@ describe("<OsUpdateButton/>", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.7-beta";
testProps.availableBetaVersion = "6.1.7-beta";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
const expectedResults = upToDate("6.1.7-beta");
testButtonState(testProps, expectedResults);
});
@ -263,7 +264,7 @@ describe("<OsUpdateButton/>", () => {
testProps.installedCommit = "old commit";
testProps.availableBetaVersion = "7.0.0-beta";
testProps.availableBetaCommit = "new commit";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
testProps.onBeta = true;
const expectedResults = updateNeeded("7.0.0-beta");
testButtonState(testProps, expectedResults);
@ -272,7 +273,7 @@ describe("<OsUpdateButton/>", () => {
it("handles installed version newer than available (beta enabled)", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.7";
testProps.update_channel = "beta";
testProps.betaOptIn = true;
testProps.onBeta = false;
testProps.availableBetaVersion = "6.1.7-beta";
const expectedResults = upToDate("6.1.7-beta");
@ -307,6 +308,16 @@ describe("<OsUpdateButton/>", () => {
testButtonState(testProps, expectedResults);
});
it("doesn't use update_channel value", () => {
const testProps = defaultTestProps();
testProps.installedVersion = "6.1.6";
testProps.shouldDisplay = () => false;
testProps.update_channel = "beta";
testProps.availableBetaVersion = "6.1.7-beta";
const expectedResults = upToDate("6.1.6");
testButtonState(testProps, expectedResults);
});
it("compares release candidates: newer", () => {
const testProps = defaultTestProps();
testProps.availableVersion = "6.1.5";

View File

@ -13,7 +13,17 @@ import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
export class BoardType extends React.Component<BoardTypeProps, {}> {
interface BoardTypeState { sending: boolean }
export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
state = {
sending: this.sending
};
UNSAFE_componentWillReceiveProps() {
this.setState({ sending: this.sending });
}
get sending() {
return !this.props.sourceFbosConfig("firmware_hardware").consistent;
}
@ -29,14 +39,15 @@ export class BoardType extends React.Component<BoardTypeProps, {}> {
if (selectedItem && isFwHardwareValue(firmware_hardware)) {
info(t("Sending firmware configuration..."), t("Sending"));
this.props.dispatch(updateConfig({ firmware_hardware }));
this.setState({ sending: true });
this.forceUpdate();
}
}
FirmwareSelection = () =>
<FBSelect
key={this.props.firmwareHardware + "" + this.sending}
extraClass={this.sending ? "dim" : ""}
key={this.props.firmwareHardware}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />

View File

@ -263,7 +263,7 @@ export function FbosDetails(props: FbosDetailsProps) {
soc_temp, wifi_level, uptime, memory_usage, disk_usage, throttled,
wifi_level_percent, cpu_usage, private_ip,
} = props.botInfoSettings;
const { last_ota, last_ota_checkup, fbos_version } = props.deviceAccount.body;
const { last_ota, last_ota_checkup } = props.deviceAccount.body;
const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---";
const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit;
@ -273,7 +273,6 @@ export function FbosDetails(props: FbosDetailsProps) {
botToMqttLastSeen={props.botToMqttLastSeen}
timeSettings={props.timeSettings}
device={props.deviceAccount} />
<p><b>{t("Version last seen")}: </b>{fbos_version}</p>
<p><b>{t("Environment")}: </b>{env}</p>
<CommitDisplay title={t("Commit")}
repo={FarmBotRepo.FarmBotOS} commit={commit} />

View File

@ -8,8 +8,6 @@ import { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces";
import { Alert } from "farmbot";
import { isFwHardwareValue, boardType } from "../firmware_hardware_support";
import { Help } from "../../../ui";
import { ToolTips } from "../../../constants";
export interface FirmwareHardwareStatusIconProps {
firmwareHardware: string | undefined;
@ -61,13 +59,10 @@ export const FirmwareHardwareStatusDetails =
(props: FirmwareHardwareStatusDetailsProps) => {
return <div className="firmware-hardware-status-details">
<label>{t("Web App")}</label>
<Help text={ToolTips.FIRMWARE_VALUE_API} />
<p>{lookup(props.apiFirmwareValue) || t("unknown")}</p>
<label>{t("FarmBot OS")}</label>
<Help text={ToolTips.FIRMWARE_VALUE_FBOS} />
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
<label>{t("Arduino/Farmduino")}</label>
<Help text={ToolTips.FIRMWARE_VALUE_MCU} />
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
<FirmwareAlerts
alerts={props.alerts}

View File

@ -4,7 +4,7 @@ import { SemverResult, semverCompare } from "../../../util";
import { OsUpdateButtonProps } from "./interfaces";
import { checkControllerUpdates } from "../../actions";
import { isString } from "lodash";
import { BotState } from "../../interfaces";
import { BotState, Feature } from "../../interfaces";
import { Content } from "../../../constants";
import { t } from "../../../i18next_wrapper";
@ -154,8 +154,9 @@ export const OsUpdateButton = (props: OsUpdateButtonProps) => {
const { controller_version } = bot.hardware.informational_settings;
/** FBOS beta release opt-in setting. */
const betaOptIn =
sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable";
const betaOptIn = props.shouldDisplay(Feature.use_update_channel)
? sourceFbosConfig("update_channel" as ConfigurationName).value !== "stable"
: !!sourceFbosConfig("beta_opt_in").value;
/** FBOS update availability. */
const buttonStatusProps = buttonVersionStatus({ bot, betaOptIn });

View File

@ -28,7 +28,7 @@ export class HardwareSettings extends
bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig,
firmwareHardware, resources
} = this.props;
const botOnline = isBotOnlineFromState(bot);
const botOnline = !isBotOnlineFromState(bot);
const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>

View File

@ -24,7 +24,7 @@ export function DangerZone(props: DangerZoneProps) {
<Highlight settingName={DeviceSetting.resetHardwareParams}>
<Row>
<Col xs={newFormat ? 8 : 4}>
<label style={{ lineHeight: "1.5rem" }}>
<label>
{t(DeviceSetting.resetHardwareParams)}
</label>
</Col>

View File

@ -44,7 +44,7 @@ export function PinGuard(props: PinGuardProps) {
</Col>
</Row>}
<PinGuardMCUInputGroup
label={DeviceSetting.pinGuard1}
label={t("Pin Guard {{ num }}", { num: 1 })}
pinNumKey={"pin_guard_1_pin_nr"}
timeoutKey={"pin_guard_1_time_out"}
activeStateKey={"pin_guard_1_active_state"}
@ -52,7 +52,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={DeviceSetting.pinGuard2}
label={t("Pin Guard {{ num }}", { num: 2 })}
pinNumKey={"pin_guard_2_pin_nr"}
timeoutKey={"pin_guard_2_time_out"}
activeStateKey={"pin_guard_2_active_state"}
@ -60,7 +60,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={DeviceSetting.pinGuard3}
label={t("Pin Guard {{ num }}", { num: 3 })}
pinNumKey={"pin_guard_3_pin_nr"}
timeoutKey={"pin_guard_3_time_out"}
activeStateKey={"pin_guard_3_active_state"}
@ -68,7 +68,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={DeviceSetting.pinGuard4}
label={t("Pin Guard {{ num }}", { num: 4 })}
pinNumKey={"pin_guard_4_pin_nr"}
timeoutKey={"pin_guard_4_time_out"}
activeStateKey={"pin_guard_4_active_state"}
@ -76,7 +76,7 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
<PinGuardMCUInputGroup
label={DeviceSetting.pinGuard5}
label={t("Pin Guard {{ num }}", { num: 5 })}
pinNumKey={"pin_guard_5_pin_nr"}
timeoutKey={"pin_guard_5_time_out"}
activeStateKey={"pin_guard_5_active_state"}

View File

@ -2,32 +2,28 @@ import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { t } from "../../../i18next_wrapper";
import { DevSettings } from "../../../account/dev/dev_support";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
export function SpacePanelHeader() {
const newFormat = DevSettings.futureFeaturesEnabled();
const width = newFormat ? 4 : 2;
const offset = newFormat ? 0 : 6;
return <div className="label-headings">
<Highlight settingName={DeviceSetting.axisHeadingLabels}>
<Row>
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
<label>
{t("X AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Y AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Z AXIS")}
</label>
</Col>
</Row>
</Highlight>
<Row>
<Col xs={width} xsOffset={offset} className={"centered-button-div"}>
<label>
{t("X AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Y AXIS")}
</label>
</Col>
<Col xs={width} className={"centered-button-div"}>
<label>
{t("Z AXIS")}
</label>
</Col>
</Row>
</div>;
}

View File

@ -65,7 +65,7 @@ export interface NumericMCUInputGroupProps {
export interface PinGuardMCUInputGroupProps {
sourceFwConfig: SourceFwConfig;
dispatch: Function;
label: DeviceSetting;
label: string;
pinNumKey: McuParamName;
timeoutKey: McuParamName;
activeStateKey: McuParamName;

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { store } from "../../redux/store";
import { ControlPanelState } from "../interfaces";
import { toggleControlPanel, bulkToggleControlPanel } from "../actions";
import { urlFriendly } from "../../util";
@ -57,11 +56,6 @@ const ERROR_HANDLING_PANEL = [
];
const PIN_GUARD_PANEL = [
DeviceSetting.pinGuard,
DeviceSetting.pinGuard1,
DeviceSetting.pinGuard2,
DeviceSetting.pinGuard3,
DeviceSetting.pinGuard4,
DeviceSetting.pinGuard5,
];
const DANGER_ZONE_PANEL = [
DeviceSetting.dangerZone,
@ -69,8 +63,6 @@ const DANGER_ZONE_PANEL = [
];
const PIN_BINDINGS_PANEL = [
DeviceSetting.pinBindings,
DeviceSetting.savedPinBindings,
DeviceSetting.addNewPinBinding,
];
const POWER_AND_RESET_PANEL = [
DeviceSetting.powerAndReset,
@ -191,7 +183,6 @@ export interface HighlightProps {
| (React.ReactChild | false)[]
| (React.ReactChild | React.ReactChild[])[];
className?: string;
searchTerm?: string;
}
interface HighlightState {
@ -209,19 +200,11 @@ export class Highlight extends React.Component<HighlightProps, HighlightState> {
}
}
get searchTerm() {
const { resources } = store.getState();
return resources.consumers.farm_designer.settingsSearchTerm;
}
render() {
const show = !this.searchTerm ||
this.props.settingName.toLowerCase().includes(this.searchTerm);
return <div className={[
this.props.className,
this.state.className,
].join(" ")}
hidden={!show}>
].join(" ")}>
{this.props.children}
</div>;
}

View File

@ -10,7 +10,6 @@ import { PinNumberDropdown } from "./pin_number_dropdown";
import { DevSettings } from "../../account/dev/dev_support";
import { ToolTips } from "../../constants";
import { Position } from "@blueprintjs/core";
import { Highlight } from "./maybe_highlight";
export class PinGuardMCUInputGroup
extends React.Component<PinGuardMCUInputGroupProps> {
@ -51,7 +50,7 @@ export class PinGuardMCUInputGroup
? <Row>
<Col xs={3}>
<label>
{t(label)}
{label}
</label>
</Col>
<Col xs={3}>
@ -64,48 +63,46 @@ export class PinGuardMCUInputGroup
<this.State />
</Col>
</Row>
: <Highlight settingName={label}>
<div className={"pin-guard-input-row"}>
<Row>
<Col xs={12}>
<label>
{t(label)}
</label>
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Pin Number")}
</label>
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
position={Position.TOP_RIGHT} />
</Col>
<Col xs={5} className="no-pad">
<this.Number />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Timeout (sec)")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.Timeout />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("To State")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.State />
</Col>
</Row>
</div>
</Highlight>;
: <div className={"pin-guard-input-row"}>
<Row>
<Col xs={12}>
<label>
{label}
</label>
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Pin Number")}
</label>
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
position={Position.TOP_RIGHT} />
</Col>
<Col xs={5} className="no-pad">
<this.Number />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Timeout (sec)")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.Timeout />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("To State")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.State />
</Col>
</Row>
</div>;
}
}

View File

@ -41,13 +41,6 @@ export function Diagnosis(props: DiagnosisProps) {
<div className={"saucer-connector last " + diagnosisColor} />
</Col>
<Col xs={10} className={"connectivity-diagnosis"}>
<p className="blinking">
{t("Always")}&nbsp;
<a className="blinking" href="/app/device?highlight=farmbot_os">
<u>{t("upgrade FarmBot OS")}</u>
</a>
&nbsp;{t("before troubleshooting.")}
</p>
<p>
{diagnose(props)}
</p>

View File

@ -81,7 +81,6 @@ export enum Feature {
ota_update_hour = "ota_update_hour",
rpi_led_control = "rpi_led_control",
sensors = "sensors",
update_resource = "update_resource",
use_update_channel = "use_update_channel",
variables = "variables",
}

View File

@ -32,6 +32,7 @@ export const specialActionLabelLookup: { [x: string]: string } = {
export const specialActionList: DropDownItem[] =
Object.values(PinBindingSpecialAction)
.filter(action => action != PinBindingSpecialAction.dump_info)
.map((action: PinBindingSpecialAction) =>
({ label: specialActionLabelLookup[action], value: action }));

View File

@ -25,7 +25,6 @@ import {
} from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
import { DeviceSetting } from "../../constants";
export class PinBindingInputGroup
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
@ -130,7 +129,7 @@ export class PinBindingInputGroup
render() {
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className="pin-binding-input-rows">
{newFormat && <Row><label>{t(DeviceSetting.addNewPinBinding)}</label></Row>}
{newFormat && <Row><label>{t("add new pin binding")}</label></Row>}
{newFormat && <this.Number />}
{newFormat && <Row>
<Col xs={5}>

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { Row, Col, Help } from "../../ui";
import { ToolTips, DeviceSetting } from "../../constants";
import { ToolTips } from "../../constants";
import { selectAllPinBindings } from "../../resources/selectors";
import { PinBindingsContentProps, PinBindingListItems } from "./interfaces";
import { PinBindingsList } from "./pin_bindings_list";
@ -17,7 +17,6 @@ import {
} from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
import { Highlight } from "../components/maybe_highlight";
/** Width of UI columns in Pin Bindings widget. */
export enum PinBindingColWidth {
@ -74,38 +73,32 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
const pinBindings = apiPinBindings(resources);
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className="pin-bindings">
<Highlight settingName={DeviceSetting.pinBindings}>
<Row>
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
position={Position.TOP_RIGHT} />}
<StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover
position={Position.TOP_RIGHT}
interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" />
<div className={"pin-binding-warning"}>
{t(ToolTips.PIN_BINDING_WARNING)}
</div>
</Popover>
</Row>
</Highlight>
<Row>
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
position={Position.TOP_RIGHT} />}
<StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover
position={Position.TOP_RIGHT}
interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" />
<div className={"pin-binding-warning"}>
{t(ToolTips.PIN_BINDING_WARNING)}
</div>
</Popover>
</Row>
<div className={"pin-bindings-list-and-input"}>
{!newFormat && <PinBindingsListHeader />}
<Highlight settingName={DeviceSetting.savedPinBindings}>
<PinBindingsList
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
</Highlight>
<Highlight settingName={DeviceSetting.addNewPinBinding}>
<PinBindingInputGroup
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
</Highlight>
<PinBindingsList
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
<PinBindingInputGroup
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
</div>
</div>;
};

View File

@ -15,7 +15,6 @@ import { DevSettings } from "../../account/dev/dev_support";
import {
PinBindingType, PinBindingSpecialAction,
} from "farmbot/dist/resources/api_resources";
import { DeviceSetting } from "../../constants";
export const PinBindingsList = (props: PinBindingsListProps) => {
const { pinBindings, resources, dispatch } = props;
@ -42,7 +41,7 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className={"bindings-list"}>
{newFormat && <Row><label>{t(DeviceSetting.savedPinBindings)}</label></Row>}
{newFormat && <Row><label>{t("saved pin bindings")}</label></Row>}
{pinBindings
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
.map(x => {

View File

@ -15,6 +15,7 @@ import * as React from "react";
import { RawFarmDesigner as FarmDesigner } from "../index";
import { mount } from "enzyme";
import { Props } from "../interfaces";
import { GardenMapLegendProps } from "../map/interfaces";
import { bot } from "../../__test_support__/fake_state/bot";
import {
fakeImage, fakeWebAppConfig,
@ -27,8 +28,6 @@ import {
import { fakeState } from "../../__test_support__/fake_state";
import { edit } from "../../api/crud";
import { BooleanSetting } from "../../session_keys";
import { GardenMapLegend } from "../map/legend/garden_map_legend";
import { GardenMap } from "../map/garden_map";
describe("<FarmDesigner/>", () => {
const fakeProps = (): Props => ({
@ -37,7 +36,6 @@ describe("<FarmDesigner/>", () => {
designer: fakeDesignerState(),
hoveredPlant: undefined,
genericPoints: [],
weeds: [],
allPoints: [],
plants: [],
toolSlots: [],
@ -48,10 +46,7 @@ describe("<FarmDesigner/>", () => {
raw_encoders: { x: undefined, y: undefined, z: undefined },
},
botMcuParams: bot.hardware.mcu_params,
botSize: {
x: { value: 3000, isDefault: true },
y: { value: 1500, isDefault: true },
},
stepsPerMmXY: { x: undefined, y: undefined },
peripherals: [],
eStopStatus: false,
latestImages: [],
@ -72,7 +67,8 @@ describe("<FarmDesigner/>", () => {
it("loads default map settings", () => {
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
const legendProps = wrapper.find(GardenMapLegend).props();
const legendProps =
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
expect(legendProps.legendMenuOpen).toBeFalsy();
expect(legendProps.showPlants).toBeTruthy();
expect(legendProps.showPoints).toBeTruthy();
@ -80,7 +76,8 @@ describe("<FarmDesigner/>", () => {
expect(legendProps.showFarmbot).toBeTruthy();
expect(legendProps.showImages).toBeFalsy();
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
const gardenMapProps = wrapper.find(GardenMap).props();
// tslint:disable-next-line:no-any
const gardenMapProps = wrapper.find("GardenMap").props() as any;
expect(gardenMapProps.gridSize.x).toEqual(2900);
expect(gardenMapProps.gridSize.y).toEqual(1400);
});
@ -93,7 +90,8 @@ describe("<FarmDesigner/>", () => {
image2.body.created_at = "2001-01-01T00:00:00.000Z";
p.latestImages = [image1, image2];
const wrapper = mount(<FarmDesigner {...p} />);
const legendProps = wrapper.find(GardenMapLegend).props();
const legendProps =
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
expect(legendProps.imageAgeInfo)
.toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 });
});
@ -139,18 +137,4 @@ describe("<FarmDesigner/>", () => {
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);
});
});

View File

@ -33,7 +33,7 @@ describe("<MoveTo />", () => {
it("moves to location: bot's current z value", () => {
const wrapper = mount(<MoveTo {...fakeProps()} />);
wrapper.find("button").simulate("click");
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 30 });
});
it("goes back", () => {

View File

@ -2,7 +2,7 @@ import { designer } from "../reducer";
import { Actions } from "../../constants";
import { ReduxAction } from "../../redux/interfaces";
import {
HoveredPlantPayl, DrawnPointPayl, CropLiveSearchResult, DrawnWeedPayl,
HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult,
} from "../interfaces";
import { BotPosition } from "../../devices/interfaces";
import {
@ -10,7 +10,6 @@ import {
} from "../../__test_support__/fake_crop_search_result";
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { PointType } from "farmbot";
describe("designer reducer", () => {
const oldState = fakeDesignerState;
@ -25,22 +24,13 @@ describe("designer reducer", () => {
expect(newState.cropSearchInProgress).toEqual(true);
});
it("selects points", () => {
it("selects plants", () => {
const action: ReduxAction<string[]> = {
type: Actions.SELECT_POINT,
payload: ["pointUuid"]
type: Actions.SELECT_PLANT,
payload: ["plantUuid"]
};
const newState = designer(oldState(), action);
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"]);
expect(newState.selectedPlants).toEqual(["plantUuid"]);
});
it("sets hovered plant", () => {
@ -94,49 +84,25 @@ describe("designer reducer", () => {
});
it("sets current point data", () => {
const action: ReduxAction<DrawnPointPayl> = {
type: Actions.SET_DRAWN_POINT_DATA,
const action: ReduxAction<CurrentPointPayl> = {
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx: 10, cy: 20, r: 30, color: "red" }
};
const newState = designer(oldState(), action);
expect(newState.drawnPoint).toEqual({
expect(newState.currentPoint).toEqual({
cx: 10, cy: 20, r: 30, color: "red"
});
});
it("uses current point color", () => {
const action: ReduxAction<DrawnPointPayl> = {
type: Actions.SET_DRAWN_POINT_DATA,
const action: ReduxAction<CurrentPointPayl> = {
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx: 10, cy: 20, r: 30 }
};
const state = oldState();
state.drawnPoint = { cx: 0, cy: 0, r: 0, color: "red" };
state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" };
const newState = designer(state, action);
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({
expect(newState.currentPoint).toEqual({
cx: 10, cy: 20, r: 30, color: "red"
});
});
@ -190,24 +156,4 @@ describe("designer reducer", () => {
const newState = designer(state, action);
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);
});
});

View File

@ -0,0 +1,70 @@
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);
});
});

View File

@ -1,4 +1,4 @@
import { mapStateToProps, getPlants, botSize } from "../state_to_props";
import { mapStateToProps, getPlants } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import {
buildResourceIndex, fakeDevice,
@ -11,13 +11,14 @@ import {
fakeWebAppConfig,
fakeFarmwareEnv,
fakeSensorReading,
fakeFirmwareConfig,
} from "../../__test_support__/fake_state/resources";
import { WebAppConfig } from "farmbot/dist/resources/configs/web_app";
import { generateUuid } from "../../resources/util";
import { DevSettings } from "../../account/dev/dev_support";
describe("mapStateToProps()", () => {
const DISCARDED_AT = "2018-01-01T00:00:00.000Z";
it("hovered plantUUID is undefined", () => {
const state = fakeState();
state.resources.consumers.farm_designer.hoveredPlant = {
@ -39,11 +40,18 @@ describe("mapStateToProps()", () => {
checkValue(2, true);
});
it("stepsPerMm is defined", () => {
const state = fakeState();
state.bot.hardware.mcu_params.movement_step_per_mm_x = 3;
state.bot.hardware.mcu_params.movement_step_per_mm_y = 4;
expect(mapStateToProps(state).stepsPerMmXY).toEqual({ x: 3, y: 4 });
});
it("returns selected plant", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakePlant(), fakeDevice()]);
const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0];
state.resources.consumers.farm_designer.selectedPoints = [plantUuid];
state.resources.consumers.farm_designer.selectedPlants = [plantUuid];
expect(mapStateToProps(state).selectedPlant).toEqual(
expect.objectContaining({ uuid: plantUuid }));
});
@ -53,8 +61,11 @@ describe("mapStateToProps()", () => {
const webAppConfig = fakeWebAppConfig();
(webAppConfig.body as WebAppConfig).show_historic_points = true;
const point1 = fakePoint();
point1.body.discarded_at = undefined;
const point2 = fakePoint();
point2.body.discarded_at = DISCARDED_AT;
const point3 = fakePoint();
point3.body.discarded_at = DISCARDED_AT;
state.resources = buildResourceIndex([
webAppConfig, point1, point2, point3, fakeDevice(),
]);
@ -66,12 +77,15 @@ describe("mapStateToProps()", () => {
const webAppConfig = fakeWebAppConfig();
(webAppConfig.body as WebAppConfig).show_historic_points = false;
const point1 = fakePoint();
point1.body.discarded_at = undefined;
const point2 = fakePoint();
point2.body.discarded_at = DISCARDED_AT;
const point3 = fakePoint();
point3.body.discarded_at = DISCARDED_AT;
state.resources = buildResourceIndex([
webAppConfig, point1, point2, point3, fakeDevice(),
]);
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
});
it("returns sensor readings", () => {
@ -130,45 +144,3 @@ describe("getPlants()", () => {
expect.objectContaining({ rotation: "15" }));
});
});
describe("botSize()", () => {
it("returns default bot size", () => {
const state = fakeState();
expect(botSize(state)).toEqual({
x: { value: 2900, isDefault: true },
y: { value: 1400, isDefault: true },
});
});
it("returns map setting bot size", () => {
const state = fakeState();
const webAppConfig = fakeWebAppConfig();
webAppConfig.body.map_size_x = 1000;
webAppConfig.body.map_size_y = 1000;
state.resources = buildResourceIndex([fakeDevice(), webAppConfig]);
expect(botSize(state)).toEqual({
x: { value: 1000, isDefault: true },
y: { value: 1000, isDefault: true },
});
});
it("returns axis length setting bot size", () => {
const state = fakeState();
const firmwareConfig = fakeFirmwareConfig();
firmwareConfig.body.movement_step_per_mm_x = 2;
firmwareConfig.body.movement_step_per_mm_y = 4;
firmwareConfig.body.movement_stop_at_max_x = 1;
firmwareConfig.body.movement_stop_at_max_y = 1;
firmwareConfig.body.movement_axis_nr_steps_x = 100;
firmwareConfig.body.movement_axis_nr_steps_y = 100;
const webAppConfig = fakeWebAppConfig();
webAppConfig.body.map_size_x = 1000;
webAppConfig.body.map_size_y = 1000;
state.resources = buildResourceIndex([
fakeDevice(), firmwareConfig, webAppConfig]);
expect(mapStateToProps(state).botSize).toEqual({
x: { value: 50, isDefault: false },
y: { value: 25, isDefault: false },
});
});
});

View File

@ -81,12 +81,21 @@ interface DesignerPanelTopProps {
onClick?(): void;
title?: string;
children?: React.ReactNode;
noIcon?: boolean;
}
export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
const withBtn = !!props.linkTo || !!props.onClick;
return <div className={`panel-top ${withBtn ? "with-button" : ""}`}>
{props.children}
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
{!props.noIcon &&
<i className="fa fa-search" />}
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
</div>
{props.onClick &&
<a>
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}

View File

@ -223,7 +223,7 @@ describe("<EditFEForm />", () => {
]}
findExecutable={jest.fn(() => seq)}
dispatch={jest.fn()}
repeatOptions={repeatOptions()}
repeatOptions={repeatOptions}
timeSettings={fakeTimeSettings()}
autoSyncEnabled={false}
resources={buildResourceIndex([]).index}
@ -545,10 +545,7 @@ describe("<RepeatForm />", () => {
const fakeProps = (): RepeatFormProps => ({
isRegimen: false,
fieldGet: jest.fn(key =>
"" + ({
endDate: "2017-07-26", endTime: "08:57",
startDate: "2017-07-25", startTime: "08:57"
} as FarmEventViewModel)[key]),
"" + ({ endDate: "2017-07-26" } as FarmEventViewModel)[key]),
fieldSet: jest.fn(),
timeSettings: fakeTimeSettings(),
});

View File

@ -26,7 +26,7 @@ export interface FarmEventRepeatFormProps {
}
const indexKey: keyof DropDownItem = "value";
const OPTN_LOOKUP = () => keyBy(repeatOptions(), indexKey);
const OPTN_LOOKUP = keyBy(repeatOptions, indexKey);
export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
const { disabled, fieldSet, repeat, endDate, endTime, timeUnit } = props;
@ -50,9 +50,9 @@ export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
</Col>
<Col xs={8}>
<FBSelect
list={repeatOptions()}
list={repeatOptions}
onChange={ddi => fieldSet("timeUnit", "" + ddi.value)}
selectedItem={OPTN_LOOKUP()[timeUnit] || OPTN_LOOKUP()["daily"]} />
selectedItem={OPTN_LOOKUP[timeUnit] || OPTN_LOOKUP["daily"]} />
</Col>
</Row>
<label>

View File

@ -16,7 +16,6 @@ import {
} from "../../ui/empty_state_wrapper";
import { some, uniq, map, sortBy } from "lodash";
import { t } from "../../i18next_wrapper";
import { SearchField } from "../../ui/search_field";
const filterSearch = (term: string) => (item: CalendarOccurrence) =>
item.heading.toLowerCase().includes(term)
@ -106,12 +105,14 @@ export class PureFarmEvents
<DesignerPanelTop
panel={Panel.FarmEvents}
linkTo={"/app/designer/events/add"}
title={t("Add event")}>
<SearchField searchTerm={this.state.searchTerm}
customLeftIcon={
<i className="fa fa-calendar" onClick={this.resetCalendar} />}
placeholder={t("Search your events...")}
onChange={searchTerm => this.setState({ searchTerm })} />
title={t("Add event")}
noIcon={true}>
<i className="fa fa-calendar" onClick={this.resetCalendar} />
<input
name="searchTerm"
value={this.state.searchTerm}
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search your events...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"farm-event"}>
<div className="farm-events">

View File

@ -41,7 +41,7 @@ export const formatDate = (input: string, timeSettings: TimeSettings) => {
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("Hours"), value: "hourly", name: "time_unit" },
{ label: t("Days"), value: "daily", name: "time_unit" },
@ -147,7 +147,7 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
sequencesById,
farmEventsById,
executableOptions: executableList,
repeatOptions: repeatOptions(),
repeatOptions,
handleTime,
farmEvents,
getFarmEvent,

View File

@ -11,7 +11,8 @@ import { NumericSetting, BooleanSetting } from "../session_keys";
import { isUndefined, last, isFinite } from "lodash";
import { AxisNumberProperty, BotSize } from "./map/interfaces";
import {
round, getPanelStatus, MapPanelStatus, mapPanelClassName, getMapPadding,
getBotSize, round, getPanelStatus, MapPanelStatus, mapPanelClassName,
getMapPadding,
} from "./map/util";
import {
calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex,
@ -69,7 +70,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
legend_menu_open: init(BooleanSetting.legend_menu_open, false),
show_plants: init(BooleanSetting.show_plants, true),
show_points: init(BooleanSetting.show_points, true),
show_weeds: init(BooleanSetting.show_weeds, true),
show_spread: init(BooleanSetting.show_spread, false),
show_farmbot: init(BooleanSetting.show_farmbot, true),
show_images: init(BooleanSetting.show_images, false),
@ -116,7 +116,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
legend_menu_open,
show_plants,
show_points,
show_weeds,
show_spread,
show_farmbot,
show_images,
@ -125,6 +124,11 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
zoom_level
} = this.state;
const botSize = getBotSize(
this.props.botMcuParams,
this.props.stepsPerMmXY,
getDefaultAxisLength(this.props.getConfigValue));
const stopAtHome = {
x: !!this.props.botMcuParams.movement_stop_at_home_x,
y: !!this.props.botMcuParams.movement_stop_at_home_y
@ -151,7 +155,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
legendMenuOpen={legend_menu_open}
showPlants={show_plants}
showPoints={show_points}
showWeeds={show_weeds}
showSpread={show_spread}
showFarmbot={show_farmbot}
showImages={show_images}
@ -161,7 +164,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
getConfigValue={this.props.getConfigValue}
shouldDisplay={this.props.shouldDisplay}
imageAgeInfo={imageAgeInfo} />
<DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
@ -179,7 +181,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
<GardenMap
showPoints={show_points}
showPlants={show_plants}
showWeeds={show_weeds}
showSpread={show_spread}
showFarmbot={show_farmbot}
showImages={show_images}
@ -191,16 +192,15 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
designer={this.props.designer}
plants={this.props.plants}
genericPoints={this.props.genericPoints}
weeds={this.props.weeds}
allPoints={this.props.allPoints}
toolSlots={this.props.toolSlots}
botLocationData={this.props.botLocationData}
botSize={this.props.botSize}
botSize={botSize}
stopAtHome={stopAtHome}
hoveredPlant={this.props.hoveredPlant}
zoomLvl={zoom_level}
botOriginQuadrant={this.getBotOriginQuadrant()}
gridSize={getGridSize(this.props.getConfigValue, this.props.botSize)}
gridSize={getGridSize(this.props.getConfigValue, botSize)}
gridOffset={gridOffset}
peripherals={this.props.peripherals}
eStopStatus={this.props.eStopStatus}

View File

@ -11,12 +11,10 @@ import {
TaggedSensor,
TaggedPoint,
TaggedPointGroup,
TaggedWeedPointer,
PointType,
} from "farmbot";
import { SlotWithTool, ResourceIndex, UUID } from "../resources/interfaces";
import { SlotWithTool, ResourceIndex } from "../resources/interfaces";
import {
BotPosition, BotLocationData, ShouldDisplay,
BotPosition, StepsPerMmXY, BotLocationData, ShouldDisplay,
} from "../devices/interfaces";
import { isNumber } from "lodash";
import { McuParams, TaggedCrop } from "farmbot";
@ -50,7 +48,6 @@ export interface State extends TypeCheckerHint {
legend_menu_open: boolean;
show_plants: boolean;
show_points: boolean;
show_weeds: boolean;
show_spread: boolean;
show_farmbot: boolean;
show_images: boolean;
@ -66,14 +63,13 @@ export interface Props {
designer: DesignerState;
hoveredPlant: TaggedPlant | undefined;
genericPoints: TaggedGenericPointer[];
weeds: TaggedWeedPointer[];
allPoints: TaggedPoint[];
plants: TaggedPlant[];
toolSlots: SlotWithTool[];
crops: TaggedCrop[];
botLocationData: BotLocationData;
botMcuParams: McuParams;
botSize: BotSize;
stepsPerMmXY: StepsPerMmXY;
peripherals: { label: string, value: boolean }[];
eStopStatus: boolean;
latestImages: TaggedImage[];
@ -110,8 +106,7 @@ export interface Crop {
}
export interface DesignerState {
selectedPoints: UUID[] | undefined;
selectionPointType: PointType[] | undefined;
selectedPlants: string[] | undefined;
hoveredPlant: HoveredPlantPayl;
hoveredPoint: string | undefined;
hoveredPlantListItem: string | undefined;
@ -120,12 +115,10 @@ export interface DesignerState {
cropSearchResults: CropLiveSearchResult[];
cropSearchInProgress: boolean;
chosenLocation: BotPosition;
drawnPoint: DrawnPointPayl | undefined;
drawnWeed: DrawnWeedPayl | undefined;
currentPoint: CurrentPointPayl | undefined;
openedSavedGarden: string | undefined;
tryGroupSortType: PointGroupSortType | "nn" | undefined;
editGroupAreaInMap: boolean;
settingsSearchTerm: string;
}
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
@ -188,7 +181,6 @@ export interface FarmEventState {
export interface GardenMapProps {
showPlants: boolean | undefined;
showPoints: boolean | undefined;
showWeeds: boolean | undefined;
showSpread: boolean | undefined;
showFarmbot: boolean | undefined;
showImages: boolean | undefined;
@ -197,7 +189,6 @@ export interface GardenMapProps {
dispatch: Function;
designer: DesignerState;
genericPoints: TaggedGenericPointer[];
weeds: TaggedWeedPointer[];
allPoints: TaggedPoint[];
plants: TaggedPlant[];
toolSlots: SlotWithTool[];
@ -288,15 +279,7 @@ export interface CameraCalibrationData {
calibrationZ: string | undefined;
}
export interface DrawnPointPayl {
name?: string;
cx: number;
cy: number;
r: number;
color?: string;
}
export interface DrawnWeedPayl {
export interface CurrentPointPayl {
name?: string;
cx: number;
cy: number;

View File

@ -4,9 +4,10 @@ jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
}));
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
jest.mock("../../point_groups/actions", () => ({ overwriteGroup: jest.fn() }));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
overwrite: jest.fn(),
}));
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
const mockGroup = fakePointGroup();
@ -15,13 +16,12 @@ jest.mock("../../point_groups/group_detail", () => ({
}));
import {
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPoint,
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPlant,
setHoveredPlant,
mapPointClickAction,
} from "../actions";
import { MovePlantProps } from "../../interfaces";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { edit } from "../../../api/crud";
import { edit, overwrite } from "../../../api/crud";
import { Actions } from "../../../constants";
import { DEFAULT_ICON, svgToUrl } from "../../../open_farm/icons";
import { history } from "../../../history";
@ -30,8 +30,6 @@ import { GetState } from "../../../redux/interfaces";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { overwriteGroup } from "../../point_groups/actions";
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
describe("movePlant", () => {
it.each<[string, Record<"x" | "y", number>, Record<"x" | "y", number>]>([
@ -76,7 +74,7 @@ describe("closePlantInfo()", () => {
closePlantInfo(dispatch)();
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
expect(dispatch).toHaveBeenCalledWith({
payload: undefined, type: Actions.SELECT_POINT
payload: undefined, type: Actions.SELECT_PLANT
});
});
@ -86,7 +84,7 @@ describe("closePlantInfo()", () => {
closePlantInfo(dispatch)();
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
expect(dispatch).toHaveBeenCalledWith({
payload: undefined, type: Actions.SELECT_POINT
payload: undefined, type: Actions.SELECT_PLANT
});
});
});
@ -117,7 +115,7 @@ describe("clickMapPlant", () => {
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith(selectPoint(["fakeUuid"]));
expect(dispatch).toHaveBeenCalledWith(selectPlant(["fakeUuid"]));
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
expect(dispatch).toHaveBeenCalledTimes(2);
});
@ -129,25 +127,12 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.body.id = 23;
state.resources = buildResourceIndex([plant]);
const dispatch = mockDispatch();
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
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(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
name: "Fake", point_ids: [1, 23]
}));
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -158,13 +143,12 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.body.id = 2;
state.resources = buildResourceIndex([plant]);
const dispatch = mockDispatch();
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
expect.objectContaining({
name: "Fake", point_ids: [1]
}));
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
name: "Fake", point_ids: [1]
}));
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -178,7 +162,7 @@ describe("clickMapPlant", () => {
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_POINT, payload: [plant.uuid]
type: Actions.SELECT_PLANT, payload: [plant.uuid]
});
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -189,39 +173,13 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.uuid = "fakePlantUuid";
state.resources = buildResourceIndex([plant]);
state.resources.consumers.farm_designer.selectedPoints = [plant.uuid];
state.resources.consumers.farm_designer.selectedPlants = [plant.uuid];
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_POINT, payload: []
type: Actions.SELECT_PLANT, payload: []
});
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();
});
});

View File

@ -6,17 +6,15 @@ jest.mock("../actions", () => ({
import { Mode } from "../interfaces";
let mockMode = Mode.none;
let mockAtPlant = true;
let mockInteractionAllow = true;
jest.mock("../util", () => ({
getMode: () => mockMode,
getMapSize: () => ({ h: 100, w: 100 }),
getGardenCoordinates: jest.fn(),
transformXY: jest.fn(() => ({ qx: 0, qy: 0 })),
transformForQuadrant: jest.fn(),
maybeNoPointer: jest.fn(),
round: jest.fn(),
cursorAtPlant: () => mockAtPlant,
allowInteraction: () => mockInteractionAllow,
allowGroupAreaInteraction: jest.fn(),
}));
jest.mock("../layers/plants/plant_actions", () => ({
@ -83,7 +81,6 @@ const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
const fakeProps = (): GardenMapProps => ({
showPoints: true,
showPlants: true,
showWeeds: true,
showSpread: false,
showFarmbot: false,
showImages: false,
@ -95,7 +92,6 @@ const fakeProps = (): GardenMapProps => ({
designer: fakeDesignerState(),
plants: [],
genericPoints: [],
weeds: [],
allPoints: [],
toolSlots: [],
botLocationData: {
@ -290,22 +286,7 @@ describe("<GardenMap/>", () => {
wrapper.find(".drop-area-svg").simulate("mouseDown", {
pageX: 1, pageY: 2
});
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(startNewPoint).toHaveBeenCalled();
expect(getGardenCoordinates).toHaveBeenCalledWith(
expect.objectContaining({ pageX: 1, pageY: 2 }));
});
@ -316,20 +297,7 @@ describe("<GardenMap/>", () => {
wrapper.find(".drop-area-svg").simulate("mouseMove", {
pageX: 10, pageY: 20
});
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"
}));
expect(resizePoint).toHaveBeenCalled();
});
it("lays eggs", () => {
@ -382,7 +350,7 @@ describe("<GardenMap/>", () => {
it("closes panel", () => {
mockMode = Mode.boxSelect;
const p = fakeProps();
p.designer.selectedPoints = undefined;
p.designer.selectedPlants = undefined;
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).toHaveBeenCalled();
@ -398,7 +366,7 @@ describe("<GardenMap/>", () => {
it("doesn't close panel: box select", () => {
mockMode = Mode.boxSelect;
const p = fakeProps();
p.designer.selectedPoints = [fakePlant().uuid];
p.designer.selectedPlants = [fakePlant().uuid];
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).not.toHaveBeenCalled();
@ -407,7 +375,7 @@ describe("<GardenMap/>", () => {
it("doesn't close panel: move mode", () => {
mockMode = Mode.moveTo;
const p = fakeProps();
p.designer.selectedPoints = [fakePlant().uuid];
p.designer.selectedPlants = [fakePlant().uuid];
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).not.toHaveBeenCalled();
@ -436,56 +404,6 @@ describe("<GardenMap/>", () => {
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", () => {
const p = fakeProps();
p.getConfigValue = () => false;

View File

@ -4,6 +4,11 @@ jest.mock("../../../history", () => ({
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
}));
let mockGardenOpen = true;
jest.mock("../../saved_gardens/saved_gardens", () => ({
savedGardenOpen: () => mockGardenOpen,
}));
import {
round,
translateScreenToGarden,
@ -16,9 +21,6 @@ import {
mapPanelClassName,
getMode,
cursorAtPlant,
allowInteraction,
allowGroupAreaInteraction,
savedGardenOpen,
} from "../util";
import { McuParams } from "farmbot";
import {
@ -30,37 +32,13 @@ import {
} from "../../../__test_support__/map_transform_props";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
describe("round()", () => {
describe("Utils", () => {
it("rounds a number", () => {
expect(round(44)).toEqual(40);
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()", () => {
it("translates screen coords to garden coords: zoomLvl = 1", () => {
const result = translateScreenToGarden({
@ -366,26 +344,17 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.points);
mockPath = "/app/designer/points/add";
expect(getMode()).toEqual(Mode.createPoint);
mockPath = "/app/designer/weeds";
expect(getMode()).toEqual(Mode.weeds);
mockPath = "/app/designer/weeds/add";
expect(getMode()).toEqual(Mode.createWeed);
mockPath = "/app/designer/gardens/1";
mockPath = "/app/designer/gardens";
mockGardenOpen = true;
expect(getMode()).toEqual(Mode.templateView);
mockPath = "/app/designer/groups/1";
expect(getMode()).toEqual(Mode.editGroup);
mockPath = "";
mockGardenOpen = false;
expect(getMode()).toEqual(Mode.none);
});
});
describe("savedGardenOpen", () => {
it("is open", () => {
const result = savedGardenOpen(["", "", "", "gardens", "4", ""]);
expect(result).toEqual(4);
});
});
describe("getGardenCoordinates()", () => {
beforeEach(() => {
Object.defineProperty(document, "querySelector", {
@ -427,37 +396,27 @@ describe("getGardenCoordinates()", () => {
});
});
describe("allowInteraction()", () => {
it("allows interaction", () => {
mockPath = "/app/designer/plants";
expect(allowInteraction()).toBeTruthy();
});
it("disallows interaction", () => {
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(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();
expect(mapPanelClassName()).toEqual("short-panel");
});
it("disallows interaction", () => {
mockPath = "/app/designer/plants/select";
expect(allowGroupAreaInteraction()).toBeFalsy();
it("returns correct panel status: panel open", () => {
Object.defineProperty(window, "innerWidth", {
value: 500,
configurable: true
});
mockPath = "/app/designer/move_to";
expect(allowGroupAreaInteraction()).toBeFalsy();
mockPath = "/app/designer/groups/1";
expect(allowGroupAreaInteraction()).toBeFalsy();
expect(mapPanelClassName()).toEqual("panel-open");
mockPath = "/app/designer/plants/crop_search/mint/add";
expect(mapPanelClassName()).toEqual("panel-open");
});
});

View File

@ -1,6 +1,6 @@
import { MovePlantProps, DraggableEvent } from "../interfaces";
import { defensiveClone } from "../../util";
import { edit } from "../../api/crud";
import { edit, overwrite } from "../../api/crud";
import { history } from "../../history";
import { Actions } from "../../constants";
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
@ -12,7 +12,6 @@ import { TaggedPoint } from "farmbot";
import { getMode } from "../map/util";
import { ResourceIndex, UUID } from "../../resources/interfaces";
import { selectAllPointGroups } from "../../resources/selectors";
import { overwriteGroup } from "../point_groups/actions";
export function movePlant(payload: MovePlantProps) {
const tr = payload.plant;
@ -24,8 +23,8 @@ export function movePlant(payload: MovePlantProps) {
return edit(tr, update);
}
export const selectPoint = (payload: string[] | undefined) => {
return { type: Actions.SELECT_POINT, payload };
export const selectPlant = (payload: string[] | undefined) => {
return { type: Actions.SELECT_PLANT, payload };
};
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
@ -34,36 +33,35 @@ export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
});
const addOrRemoveFromGroup =
(clickedPlantUuid: UUID, resources: ResourceIndex) =>
(dispatch: Function) => {
const group = findGroupFromUrl(selectAllPointGroups(resources));
const point =
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
if (group && point?.body.id) {
type Body = (typeof group)["body"];
const nextGroup: Body = ({
...group.body,
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
});
if (!group.body.point_ids.includes(point.body.id)) {
nextGroup.point_ids.push(point.body.id);
}
nextGroup.point_ids = uniq(nextGroup.point_ids);
dispatch(overwriteGroup(group, nextGroup));
(clickedPlantUuid: UUID, resources: ResourceIndex) => {
const group = findGroupFromUrl(selectAllPointGroups(resources));
const point =
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
if (group && point?.body.id) {
type Body = (typeof group)["body"];
const nextGroup: Body = ({
...group.body,
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
});
if (!group.body.point_ids.includes(point.body.id)) {
nextGroup.point_ids.push(point.body.id);
}
};
const addOrRemoveFromSelection =
(clickedPointUuid: UUID, selectedPoints: UUID[] | undefined) => {
const nextSelected =
(selectedPoints || []).filter(uuid => uuid !== clickedPointUuid);
if (!(selectedPoints?.includes(clickedPointUuid))) {
nextSelected.push(clickedPointUuid);
nextGroup.point_ids = uniq(nextGroup.point_ids);
return overwrite(group, nextGroup);
}
return selectPoint(nextSelected);
};
export const clickMapPlant = (clickedPlantUuid: UUID, icon: string) => {
const addOrRemoveFromSelection =
(clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => {
const nextSelected =
(selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid);
if (!(selectedPlants?.includes(clickedPlantUuid))) {
nextSelected.push(clickedPlantUuid);
}
return selectPlant(nextSelected);
};
export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
return (dispatch: Function, getState: GetState) => {
switch (getMode()) {
case Mode.editGroup:
@ -71,11 +69,11 @@ export const clickMapPlant = (clickedPlantUuid: UUID, icon: string) => {
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
break;
case Mode.boxSelect:
const { selectedPoints } = getState().resources.consumers.farm_designer;
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPoints));
const { selectedPlants } = getState().resources.consumers.farm_designer;
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPlants));
break;
default:
dispatch(selectPoint([clickedPlantUuid]));
dispatch(selectPlant([clickedPlantUuid]));
dispatch(setHoveredPlant(clickedPlantUuid, icon));
break;
}
@ -83,7 +81,7 @@ export const clickMapPlant = (clickedPlantUuid: UUID, icon: string) => {
};
export const unselectPlant = (dispatch: Function) => () => {
dispatch(selectPoint(undefined));
dispatch(selectPlant(undefined));
dispatch(setHoveredPlant(undefined));
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
};
@ -106,14 +104,3 @@ export const setDragIcon =
e.dataTransfer.setDragImage
&& 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);
}
};

View File

@ -8,8 +8,9 @@ jest.mock("../../../point_groups/criteria", () => ({
editGtLtCriteria: jest.fn(),
}));
jest.mock("../../../point_groups/actions", () => ({
overwriteGroup: jest.fn(),
jest.mock("../../../../api/crud", () => ({
overwrite: jest.fn(),
save: jest.fn(),
}));
import {
@ -24,8 +25,8 @@ import {
import { Actions } from "../../../../constants";
import { history } from "../../../../history";
import { editGtLtCriteria } from "../../../point_groups/criteria";
import { overwrite, save } from "../../../../api/crud";
import { cloneDeep } from "lodash";
import { overwriteGroup } from "../../../point_groups/actions";
describe("getSelected", () => {
it("returns some", () => {
@ -54,9 +55,6 @@ describe("resizeBox", () => {
const fakeProps = (): ResizeSelectionBoxProps => ({
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
plants: [],
allPoints: [],
selectionPointType: undefined,
getConfigValue: () => true,
gardenCoords: { x: 100, y: 200 },
setMapState: jest.fn(),
dispatch: jest.fn(),
@ -70,7 +68,7 @@ describe("resizeBox", () => {
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_POINT,
type: Actions.SELECT_PLANT,
payload: undefined
});
});
@ -115,7 +113,7 @@ describe("resizeBox", () => {
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_POINT,
type: Actions.SELECT_PLANT,
payload: [plant.uuid]
});
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
@ -137,7 +135,7 @@ describe("startNewSelectionBox", () => {
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_POINT,
type: Actions.SELECT_PLANT,
payload: undefined
});
});
@ -159,7 +157,7 @@ describe("startNewSelectionBox", () => {
startNewSelectionBox(p);
expect(p.setMapState).not.toHaveBeenCalled();
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_POINT,
type: Actions.SELECT_PLANT,
payload: undefined
});
});
@ -188,7 +186,8 @@ describe("maybeUpdateGroup()", () => {
expectedBody && (expectedBody.point_ids = [
plant1.body.id || 0, plant2.body.id || 0,
]);
expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedBody);
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
expect(save).not.toHaveBeenCalled();
});
it("updates criteria", () => {
@ -212,6 +211,7 @@ describe("maybeUpdateGroup()", () => {
maybeUpdateGroup(p);
expect(p.dispatch).not.toHaveBeenCalled();
expect(editGtLtCriteria).not.toHaveBeenCalled();
expect(overwriteGroup).not.toHaveBeenCalled();
expect(overwrite).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
});
});

View File

@ -3,20 +3,18 @@ import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
import { SelectionBoxData } from "./selection_box";
import { GardenMapState } from "../../interfaces";
import { history } from "../../../history";
import { selectPoint } from "../actions";
import { selectPlant } from "../actions";
import { getMode } from "../util";
import { editGtLtCriteria } from "../../point_groups/criteria";
import { TaggedPointGroup, TaggedPoint, PointType } from "farmbot";
import { TaggedPointGroup } from "farmbot";
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
import { overwrite } from "../../../api/crud";
import { unpackUUID } from "../../../util";
import { UUID } from "../../../resources/interfaces";
import { getFilteredPoints } from "../../plants/select_plants";
import { GetWebAppConfigValue } from "../../../config_storage/actions";
import { overwriteGroup } from "../../point_groups/actions";
/** Return all plants within the selection box. */
export const getSelected = (
plants: (TaggedPlant | TaggedPoint)[],
plants: TaggedPlant[],
box: SelectionBoxData | undefined,
): string[] | undefined => {
const arraySelected = plants.filter(p => {
@ -37,9 +35,6 @@ export const getSelected = (
export interface ResizeSelectionBoxProps {
selectionBox: SelectionBoxData | undefined;
plants: TaggedPlant[];
allPoints: TaggedPoint[];
selectionPointType: PointType[] | undefined;
getConfigValue: GetWebAppConfigValue;
gardenCoords: AxisNumberProperty | undefined;
setMapState: (x: Partial<GardenMapState>) => void;
dispatch: Function;
@ -59,16 +54,11 @@ export const resizeBox = (props: ResizeSelectionBoxProps) => {
props.setMapState({ selectionBox: newSelectionBox });
if (props.plantActions) {
// Select all plants within the updated selection box
const { plants, allPoints, selectionPointType, getConfigValue } = props;
const points =
getFilteredPoints({
plants, allPoints, selectionPointType, getConfigValue
});
const payload = getSelected(points, newSelectionBox);
const payload = getSelected(props.plants, newSelectionBox);
if (payload && getMode() === Mode.none) {
history.push("/app/designer/plants/select");
}
props.dispatch(selectPoint(payload));
props.dispatch(selectPlant(payload));
}
}
}
@ -94,7 +84,7 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
}
if (props.plantActions) {
// Clear the previous plant selection when starting a new selection box
props.dispatch(selectPoint(undefined));
props.dispatch(selectPlant(undefined));
}
};
@ -109,21 +99,20 @@ export interface MaybeUpdateGroupProps {
export const maybeUpdateGroup =
(props: MaybeUpdateGroupProps) => {
const { group } = props;
if (props.selectionBox && group) {
if (props.selectionBox && props.group) {
if (props.editGroupAreaInMap
&& props.shouldDisplay(Feature.criteria_groups)) {
props.dispatch(editGtLtCriteria(group, props.selectionBox));
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
} else {
const nextGroupBody = cloneDeep(group.body);
const nextGroupBody = cloneDeep(props.group.body);
props.boxSelected?.map(uuid => {
const { kind, remoteId } = unpackUUID(uuid);
remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId);
});
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
if (!isEqual(group.body.point_ids, nextGroupBody.point_ids)) {
props.dispatch(overwriteGroup(group, nextGroupBody));
props.dispatch(selectPoint(undefined));
if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
props.dispatch(overwrite(props.group, nextGroupBody));
props.dispatch(selectPlant(undefined));
}
}
}

View File

@ -1,14 +1,11 @@
import {
startNewPoint, resizePoint, StartNewPointProps, ResizePointProps,
} from "../drawn_point_actions";
import { startNewPoint, resizePoint } from "../drawn_point_actions";
import { Actions } from "../../../../constants";
describe("startNewPoint", () => {
const fakeProps = (): StartNewPointProps => ({
const fakeProps = () => ({
gardenCoords: { x: 100, y: 200 },
dispatch: jest.fn(),
setMapState: jest.fn(),
type: "point",
});
it("starts point", () => {
@ -16,25 +13,15 @@ describe("startNewPoint", () => {
startNewPoint(p);
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
expect(p.dispatch).toHaveBeenCalledWith({
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,
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx: 100, cy: 200, r: 0 }
});
});
it("doesn't start point", () => {
const p = fakeProps();
p.gardenCoords = undefined;
// tslint:disable-next-line:no-any
p.gardenCoords = undefined as any;
startNewPoint(p);
expect(p.setMapState).toHaveBeenCalledWith({ isDragging: true });
expect(p.dispatch).not.toHaveBeenCalled();
@ -42,29 +29,18 @@ describe("startNewPoint", () => {
});
describe("resizePoint", () => {
const fakeProps = (): ResizePointProps => ({
const fakeProps = () => ({
gardenCoords: { x: 100, y: 200 },
drawnPoint: { cx: 100, cy: 200, r: 0 },
currentPoint: { cx: 100, cy: 200, r: 0 },
dispatch: jest.fn(),
isDragging: true,
type: "point",
});
it("resizes point", () => {
const p = fakeProps();
resizePoint(p);
expect(p.dispatch).toHaveBeenCalledWith({
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,
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx: 100, cy: 200, r: 0 }
});
});

View File

@ -12,12 +12,13 @@ describe("<DrawnPoint/>", () => {
cx: 10,
cy: 20,
r: 30,
color: "red"
}
});
it("renders point", () => {
const wrapper = svgMount(<DrawnPoint {...fakeProps()} />);
expect(wrapper.find("g").props().stroke).toEqual("green");
expect(wrapper.find("g").props().stroke).toEqual("red");
expect(wrapper.find("circle").first().props()).toEqual({
id: "point-radius", strokeDasharray: "4 5",
cx: 10, cy: 20, r: 30,
@ -27,11 +28,4 @@ describe("<DrawnPoint/>", () => {
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");
});
});

View File

@ -1,36 +0,0 @@
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);
});
});

View File

@ -1,11 +1,11 @@
import * as React from "react";
import { MapTransformProps } from "../interfaces";
import { transformXY } from "../util";
import { DrawnPointPayl } from "../../interfaces";
import { CurrentPointPayl } from "../../interfaces";
export interface DrawnPointProps {
mapTransformProps: MapTransformProps;
data: DrawnPointPayl | undefined;
data: CurrentPointPayl | undefined;
}
export function DrawnPoint(props: DrawnPointProps) {

View File

@ -1,47 +1,37 @@
import { Actions } from "../../../constants";
import { AxisNumberProperty } from "../interfaces";
import { DrawnPointPayl } from "../../interfaces";
export interface StartNewPointProps {
gardenCoords: AxisNumberProperty | undefined;
dispatch: Function;
setMapState: Function;
type: "point" | "weed";
}
import { CurrentPointPayl } from "../../interfaces";
/** Create a new point. */
export const startNewPoint = (props: StartNewPointProps) => {
export const startNewPoint = (props: {
gardenCoords: AxisNumberProperty | undefined,
dispatch: Function,
setMapState: Function,
}) => {
props.setMapState({ isDragging: true });
const center = props.gardenCoords;
if (center) {
// Set the center of a new point
props.dispatch({
type: props.type == "weed"
? Actions.SET_DRAWN_WEED_DATA
: Actions.SET_DRAWN_POINT_DATA,
type: Actions.SET_CURRENT_POINT_DATA,
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. */
export const resizePoint = (props: ResizePointProps) => {
export const resizePoint = (props: {
gardenCoords: AxisNumberProperty | undefined,
currentPoint: CurrentPointPayl | undefined,
dispatch: Function,
isDragging: boolean | undefined,
}) => {
const edge = props.gardenCoords;
if (edge && props.drawnPoint && !!props.isDragging) {
const { cx, cy } = props.drawnPoint;
if (edge && props.currentPoint && !!props.isDragging) {
const { cx, cy } = props.currentPoint;
// Adjust the radius of the point being created
props.dispatch({
type: props.type == "weed"
? Actions.SET_DRAWN_WEED_DATA
: Actions.SET_DRAWN_POINT_DATA,
type: Actions.SET_CURRENT_POINT_DATA,
payload: {
cx, cy, // Center was set by click, radius is adjusted by drag
r: Math.round(Math.sqrt(

View File

@ -1,34 +0,0 @@
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>;
}

View File

@ -6,7 +6,7 @@ import {
} from "./interfaces";
import { GardenMapProps, GardenMapState } from "../interfaces";
import {
getMapSize, getGardenCoordinates, getMode, cursorAtPlant, allowInteraction,
getMapSize, getGardenCoordinates, getMode, cursorAtPlant,
} from "./util";
import {
Grid, MapBackground,
@ -17,7 +17,6 @@ import {
PlantLayer,
SpreadLayer,
PointLayer,
WeedLayer,
ToolSlotLayer,
FarmBotLayer,
ImageLayer,
@ -35,12 +34,9 @@ import { NNPath } from "../point_groups/paths";
import { history } from "../../history";
import { ZonesLayer } from "./layers/zones/zones_layer";
import { ErrorBoundary } from "../../error_boundary";
import { TaggedPoint, TaggedPointGroup, PointType } from "farmbot";
import { TaggedPoint, TaggedPointGroup } from "farmbot";
import { findGroupFromUrl } from "../point_groups/group_detail";
import { pointsSelectedByGroup } from "../point_groups/criteria";
import { DrawnWeed } from "./drawn_point/drawn_weed";
import { UUID } from "../../resources/interfaces";
import { throttle } from "lodash";
export class GardenMap extends
React.Component<GardenMapProps, Partial<GardenMapState>> {
@ -85,12 +81,8 @@ export class GardenMap extends
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. */
endDrag = throttle(() => {
endDrag = () => {
maybeSavePlantLocation({
plant: this.getPlant(),
isDragging: this.state.isDragging,
@ -102,7 +94,7 @@ export class GardenMap extends
dispatch: this.props.dispatch,
shouldDisplay: this.props.shouldDisplay,
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
boxSelected: this.props.designer.selectedPoints,
boxSelected: this.props.designer.selectedPlants,
});
this.setState({
isDragging: false, qPageX: 0, qPageY: 0,
@ -110,7 +102,7 @@ export class GardenMap extends
activeDragSpread: undefined,
selectionBox: undefined
});
}, 400);
}
getGardenCoordinates =
(e: React.DragEvent<HTMLElement> | React.MouseEvent<SVGElement>):
@ -160,15 +152,6 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e),
dispatch: this.props.dispatch,
setMapState: this.setMapState,
type: "point",
});
break;
case Mode.createWeed:
startNewPoint({
gardenCoords: this.getGardenCoordinates(e),
dispatch: this.props.dispatch,
setMapState: this.setMapState,
type: "weed",
});
break;
case Mode.clickToAdd:
@ -180,8 +163,8 @@ export class GardenMap extends
startDragOnBackground = (e: React.MouseEvent<SVGElement>): void => {
switch (getMode()) {
case Mode.moveTo:
break;
case Mode.createPoint:
case Mode.createWeed:
case Mode.clickToAdd:
case Mode.editPlant:
break;
@ -213,27 +196,17 @@ 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. */
getPlant = (): TaggedPlant | undefined => {
return allowInteraction()
? this.props.selectedPlant
: undefined;
}
get currentPoint(): UUID | undefined {
return this.props.designer.selectedPoints?.[0];
switch (getMode()) {
case Mode.boxSelect:
case Mode.moveTo:
case Mode.points:
case Mode.createPoint:
return undefined; // For modes without plant interaction
default:
return this.props.selectedPlant;
}
}
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
@ -300,28 +273,15 @@ export class GardenMap extends
case Mode.createPoint:
resizePoint({
gardenCoords: this.getGardenCoordinates(e),
drawnPoint: this.props.designer.drawnPoint,
currentPoint: this.props.designer.currentPoint,
dispatch: this.props.dispatch,
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;
case Mode.editGroup:
resizeBox({
selectionBox: this.state.selectionBox,
plants: this.props.plants,
allPoints: this.props.allPoints,
selectionPointType: this.props.designer.selectionPointType,
getConfigValue: this.props.getConfigValue,
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
dispatch: this.props.dispatch,
@ -333,9 +293,6 @@ export class GardenMap extends
resizeBox({
selectionBox: this.state.selectionBox,
plants: this.props.plants,
allPoints: this.props.allPoints,
selectionPointType: this.props.designer.selectionPointType,
getConfigValue: this.props.getConfigValue,
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
dispatch: this.props.dispatch,
@ -351,7 +308,7 @@ export class GardenMap extends
case Mode.moveTo:
return () => { };
case Mode.boxSelect:
return this.props.designer.selectedPoints
return this.props.designer.selectedPlants
? () => { }
: closePlantInfo(this.props.dispatch);
default:
@ -405,7 +362,6 @@ export class GardenMap extends
botSize={this.props.botSize}
mapTransformProps={this.mapTransformProps}
groups={this.props.groups}
startDrag={this.startDragOnBackground}
currentGroup={this.group?.uuid} />
SensorReadingsLayer = () => <SensorReadingsLayer
visible={!!this.props.showSensorReadings}
@ -429,20 +385,7 @@ export class GardenMap extends
dispatch={this.props.dispatch}
hoveredPoint={this.props.designer.hoveredPoint}
visible={!!this.props.showPoints}
interactions={this.interactions("GenericPointer")}
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
mapTransformProps={this.mapTransformProps}
dispatch={this.props.dispatch}
@ -452,11 +395,10 @@ export class GardenMap extends
hoveredPlant={this.props.hoveredPlant}
dragging={!!this.state.isDragging}
editing={this.isEditing}
boxSelected={this.props.designer.selectedPoints}
groupSelected={this.groupSelected}
boxSelected={this.props.designer.selectedPlants}
groupSelected={this.pointsSelectedByGroup.map(point => point.uuid)}
zoomLvl={this.props.zoomLvl}
activeDragXY={this.state.activeDragXY}
interactions={this.interactions("Plant")}
animate={this.animate} />
ToolSlotLayer = () => <ToolSlotLayer
mapTransformProps={this.mapTransformProps}
@ -464,7 +406,6 @@ export class GardenMap extends
dispatch={this.props.dispatch}
hoveredToolSlot={this.props.designer.hoveredToolSlot}
botPositionX={this.props.botLocationData.position.x}
interactions={this.interactions("ToolSlot")}
slots={this.props.toolSlots} />
FarmBotLayer = () => <FarmBotLayer
mapTransformProps={this.mapTransformProps}
@ -502,10 +443,8 @@ export class GardenMap extends
chosenLocation={this.props.designer.chosenLocation}
mapTransformProps={this.mapTransformProps} />
DrawnPoint = () => <DrawnPoint
data={this.props.designer.drawnPoint}
mapTransformProps={this.mapTransformProps} />
DrawnWeed = () => <DrawnWeed
data={this.props.designer.drawnWeed}
data={this.props.designer.currentPoint}
key={"currentPoint"}
mapTransformProps={this.mapTransformProps} />
GroupOrder = () => <GroupOrder
group={this.group}
@ -529,7 +468,6 @@ export class GardenMap extends
<this.SensorReadingsLayer />
<this.SpreadLayer />
<this.PointLayer />
<this.WeedLayer />
<this.PlantLayer />
<this.ToolSlotLayer />
<this.FarmBotLayer />
@ -538,7 +476,6 @@ export class GardenMap extends
<this.SelectionBox />
<this.TargetCoordinate />
<this.DrawnPoint />
<this.DrawnWeed />
<this.GroupOrder />
<this.NNPath />
<this.Bugs />

View File

@ -2,12 +2,9 @@ import {
TaggedPlantPointer,
TaggedGenericPointer,
TaggedPlantTemplate,
TaggedWeedPointer,
} from "farmbot";
import { State, BotOriginQuadrant } from "../interfaces";
import {
BotPosition, BotLocationData, ShouldDisplay,
} from "../../devices/interfaces";
import { BotPosition, BotLocationData } from "../../devices/interfaces";
import { GetWebAppConfigValue } from "../../config_storage/actions";
import { TimeSettings } from "../../interfaces";
import { UUID } from "../../resources/interfaces";
@ -25,10 +22,9 @@ export interface PlantLayerProps {
mapTransformProps: MapTransformProps;
zoomLvl: number;
activeDragXY: BotPosition | undefined;
boxSelected: UUID[] | undefined;
boxSelected: string[] | undefined;
groupSelected: UUID[];
animate: boolean;
interactions: boolean;
}
export interface GardenMapLegendProps {
@ -37,7 +33,6 @@ export interface GardenMapLegendProps {
legendMenuOpen: boolean;
showPlants: boolean;
showPoints: boolean;
showWeeds: boolean;
showSpread: boolean;
showFarmbot: boolean;
showImages: boolean;
@ -50,7 +45,6 @@ export interface GardenMapLegendProps {
imageAgeInfo: { newestDate: string, toOldest: number };
gardenId?: number;
className?: string;
shouldDisplay: ShouldDisplay;
}
export type MapTransformProps = {
@ -86,17 +80,6 @@ export interface GardenPointProps {
dispatch: Function;
}
export interface GardenWeedProps {
mapTransformProps: MapTransformProps;
weed: TaggedWeedPointer;
hovered: boolean;
current: boolean;
selected: boolean;
animate: boolean;
spreadVisible: boolean;
dispatch: Function;
}
interface DragHelpersBaseProps {
dragging: boolean;
mapTransformProps: MapTransformProps;
@ -169,9 +152,7 @@ export enum Mode {
addPlant = "addPlant",
moveTo = "moveTo",
points = "points",
weeds = "weeds",
createPoint = "createPoint",
createWeed = "createWeed",
templateView = "templateView",
editGroup = "editGroup",
}

View File

@ -1,30 +1,24 @@
jest.mock("../../../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu";
import { shallow, mount } from "enzyme";
import {
fakeWebAppConfig,
} from "../../../../../__test_support__/fake_state/resources";
import { StringConfigKey } from "farmbot/dist/resources/configs/web_app";
import { setWebAppConfigValue } from "../../../../../config_storage/actions";
import {
fakeTimeSettings,
} from "../../../../../__test_support__/fake_time_settings";
const mockConfig = fakeWebAppConfig();
jest.mock("../../../../../resources/selectors", () => ({
getWebAppConfig: () => mockConfig,
assertUuid: jest.fn(),
}));
import * as React from "react";
import { ImageFilterMenu, ImageFilterMenuProps } from "../image_filter_menu";
import { shallow, mount } from "enzyme";
import { StringConfigKey } from "farmbot/dist/resources/configs/web_app";
import {
fakeTimeSettings,
} from "../../../../../__test_support__/fake_time_settings";
import { edit, save } from "../../../../../api/crud";
import { fakeState } from "../../../../../__test_support__/fake_state";
import {
buildResourceIndex,
} from "../../../../../__test_support__/resource_index_builder";
jest.mock("../../../../../config_storage/actions", () => ({
setWebAppConfigValue: jest.fn(),
}));
describe("<ImageFilterMenu />", () => {
mockConfig.body.photo_filter_begin = "";
@ -51,19 +45,13 @@ describe("<ImageFilterMenu />", () => {
["endDate", "photo_filter_end", 2],
])("sets filter: %s", (filter, key, i) => {
const p = fakeProps();
const state = fakeState();
const config = fakeWebAppConfig();
state.resources = buildResourceIndex([config]);
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
wrapper.find("BlurableInput").at(i).simulate("commit", {
currentTarget: { value: "2001-01-03" }
});
expect(wrapper.instance().state[filter]).toEqual("2001-01-03");
expect(edit).toHaveBeenCalledWith(config, {
[key]: "2001-01-03T00:00:00.000Z"
});
expect(save).toHaveBeenCalledWith(config.uuid);
expect(setWebAppConfigValue)
.toHaveBeenCalledWith(key, "2001-01-03T00:00:00.000Z");
});
it.each<[
@ -73,64 +61,14 @@ describe("<ImageFilterMenu />", () => {
["endTime", "photo_filter_end", 3],
])("sets filter: %s", (filter, key, i) => {
const p = fakeProps();
const state = fakeState();
const config = fakeWebAppConfig();
state.resources = buildResourceIndex([config]);
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
wrapper.find("BlurableInput").at(i).simulate("commit", {
currentTarget: { value: "05:00" }
});
expect(wrapper.instance().state[filter]).toEqual("05:00");
expect(edit).toHaveBeenCalledWith(config, {
[key]: "2001-01-03T05:00:00.000Z"
});
expect(save).toHaveBeenCalledWith(config.uuid);
});
it.each<[
"beginDate" | "endDate",
"photo_filter_begin" | "photo_filter_end",
number
]>([
["beginDate", "photo_filter_begin", 0],
["endDate", "photo_filter_end", 2],
])("unsets filter: %s", (filter, key, i) => {
const p = fakeProps();
const state = fakeState();
const config = fakeWebAppConfig();
state.resources = buildResourceIndex([config]);
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
wrapper.find("BlurableInput").at(i).simulate("commit", {
currentTarget: { value: "" }
});
expect(wrapper.instance().state[filter]).toEqual(undefined);
// tslint:disable-next-line:no-null-keyword
expect(edit).toHaveBeenCalledWith(config, { [key]: null });
expect(save).toHaveBeenCalledWith(config.uuid);
});
it.each<[
"beginTime" | "endTime", number
]>([
["beginTime", 1],
["endTime", 3],
])("doesn't set filter: %s", (filter, i) => {
const p = fakeProps();
const state = fakeState();
const config = fakeWebAppConfig();
state.resources = buildResourceIndex([config]);
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
wrapper.find("BlurableInput").at(i).simulate("commit", {
currentTarget: { value: "05:00" }
});
expect(wrapper.instance().state[filter]).toEqual("05:00");
expect(edit).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
expect(setWebAppConfigValue)
.toHaveBeenCalledWith(key, "2001-01-03T05:00:00.000Z");
});
it("loads values from config", () => {
@ -145,34 +83,14 @@ describe("<ImageFilterMenu />", () => {
it("changes slider", () => {
const p = fakeProps();
const state = fakeState();
const config = fakeWebAppConfig();
state.resources = buildResourceIndex([config]);
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
p.getConfigValue = () => undefined;
p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z";
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
wrapper.instance().sliderChange(1);
expect(wrapper.instance().state.slider).toEqual(1);
expect(edit).toHaveBeenCalledWith(config, {
photo_filter_begin: "2001-01-02T00:00:00.000Z",
photo_filter_end: "2001-01-03T00:00:00.000Z",
});
expect(save).toHaveBeenCalledWith(config.uuid);
});
it("doesn't update config", () => {
const p = fakeProps();
const state = fakeState();
state.resources = buildResourceIndex([]);
p.dispatch = jest.fn(x => x(jest.fn(), () => state));
p.getConfigValue = () => 1;
p.imageAgeInfo.newestDate = "2001-01-03T05:00:00.000Z";
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
wrapper.instance().sliderChange(1);
expect(wrapper.instance().state.slider).toEqual(1);
expect(edit).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
expect(setWebAppConfigValue)
.toHaveBeenCalledWith("photo_filter_begin", "2001-01-02T00:00:00.000Z");
expect(setWebAppConfigValue)
.toHaveBeenCalledWith("photo_filter_end", "2001-01-03T00:00:00.000Z");
});
it("displays slider labels", () => {

View File

@ -1,7 +1,9 @@
import * as React from "react";
import { BlurableInput } from "../../../../ui/index";
import { offsetTime } from "../../../farm_events/edit_fe_form";
import { GetWebAppConfigValue } from "../../../../config_storage/actions";
import {
setWebAppConfigValue, GetWebAppConfigValue,
} from "../../../../config_storage/actions";
import moment from "moment";
import {
formatDate, formatTime,
@ -9,13 +11,8 @@ import {
import { Slider } from "@blueprintjs/core";
import { t } from "../../../../i18next_wrapper";
import { TimeSettings } from "../../../../interfaces";
import { StringConfigKey } from "farmbot/dist/resources/configs/web_app";
import { GetState } from "../../../../redux/interfaces";
import { getWebAppConfig } from "../../../../resources/getters";
import { edit, save } from "../../../../api/crud";
import { isString, isUndefined } from "lodash";
interface FullImageFilterMenuState {
interface ImageFilterMenuState {
beginDate: string | undefined;
beginTime: string | undefined;
endDate: string | undefined;
@ -23,8 +20,6 @@ interface FullImageFilterMenuState {
slider: number;
}
type ImageFilterMenuState = Partial<FullImageFilterMenuState>;
export interface ImageFilterMenuProps {
timeSettings: TimeSettings;
dispatch: Function;
@ -33,48 +28,26 @@ export interface ImageFilterMenuProps {
}
export class ImageFilterMenu
extends React.Component<ImageFilterMenuProps, ImageFilterMenuState> {
state: ImageFilterMenuState = {};
extends React.Component<ImageFilterMenuProps, Partial<ImageFilterMenuState>> {
constructor(props: ImageFilterMenuProps) {
super(props);
this.state = {};
}
componentDidMount() {
UNSAFE_componentWillMount() {
const { newestDate, toOldest } = this.props.imageAgeInfo;
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
if (isString(beginDatetime) || isUndefined(beginDatetime)) {
this.updateSliderState(beginDatetime);
}
this.setState({
slider: toOldest + 1 - (beginDatetime
? Math.abs(moment(beginDatetime.toString())
.diff(moment(newestDate).clone(), "days")) : 0)
});
this.updateState();
}
updateSliderState = (begin: string | undefined) => {
const { newestDate, toOldest } = this.props.imageAgeInfo;
const offset = begin ? Math.abs(moment(begin.toString())
.diff(moment(newestDate).clone(), "days")) : 0;
this.setState({ slider: toOldest + 1 - offset });
};
setValues = (update: StringValueUpdate) => {
Object.entries(update).map(([key, value]) => {
switch (key) {
case "photo_filter_begin":
this.updateSliderState(value);
value
? this.setState({
beginDate: formatDate(value.toString(), this.props.timeSettings),
beginTime: formatTime(value.toString(), this.props.timeSettings),
})
: this.setState({ beginDate: undefined, beginTime: undefined });
break;
case "photo_filter_end":
value
? this.setState({
endDate: formatDate(value.toString(), this.props.timeSettings),
endTime: formatTime(value.toString(), this.props.timeSettings),
})
: this.setState({ endDate: undefined, endTime: undefined });
break;
}
});
this.props.dispatch(setWebAppConfigValues(update));
};
UNSAFE_componentWillReceiveProps() {
this.updateState();
}
updateState = () => {
const beginDatetime = this.props.getConfigValue("photo_filter_begin");
@ -97,27 +70,27 @@ export class ImageFilterMenu
const input = e.currentTarget.value;
this.setState({ [datetime]: input });
const { beginDate, beginTime, endDate, endTime } = this.state;
const { timeSettings } = this.props;
const { dispatch, timeSettings } = this.props;
let value = undefined;
switch (datetime) {
case "beginDate":
value = offsetTime(input, beginTime || "00:00", timeSettings);
this.setValues({ photo_filter_begin: value });
dispatch(setWebAppConfigValue("photo_filter_begin", value));
break;
case "beginTime":
if (beginDate) {
value = offsetTime(beginDate, input, timeSettings);
this.setValues({ photo_filter_begin: value });
dispatch(setWebAppConfigValue("photo_filter_begin", value));
}
break;
case "endDate":
value = offsetTime(input, endTime || "00:00", timeSettings);
this.setValues({ photo_filter_end: value });
dispatch(setWebAppConfigValue("photo_filter_end", value));
break;
case "endTime":
if (endDate) {
value = offsetTime(endDate, input, timeSettings);
this.setValues({ photo_filter_end: value });
dispatch(setWebAppConfigValue("photo_filter_end", value));
}
break;
}
@ -127,12 +100,13 @@ export class ImageFilterMenu
sliderChange = (slider: number) => {
const { newestDate, toOldest } = this.props.imageAgeInfo;
this.setState({ slider });
const { timeSettings } = this.props;
const { dispatch, timeSettings } = this.props;
const calcDate = (day: number) =>
moment(newestDate).subtract(toOldest - day, "days").toISOString();
const begin = offsetTime(calcDate(slider - 1), "00:00", timeSettings);
const end = offsetTime(calcDate(slider), "00:00", timeSettings);
this.setValues({ photo_filter_begin: begin, photo_filter_end: end });
dispatch(setWebAppConfigValue("photo_filter_begin", begin));
dispatch(setWebAppConfigValue("photo_filter_end", end));
}
renderLabel = (day: number) => {
@ -217,14 +191,3 @@ export class ImageFilterMenu
</div>;
}
}
type StringValueUpdate = Partial<Record<StringConfigKey, string | undefined>>;
const setWebAppConfigValues = (update: StringValueUpdate) =>
(dispatch: Function, getState: GetState) => {
const webAppConfig = getWebAppConfig(getState().resources.index);
if (webAppConfig) {
dispatch(edit(webAppConfig, update));
dispatch(save(webAppConfig.uuid));
}
};

View File

@ -1,7 +1,6 @@
export * from "./farmbot/farmbot_layer";
export * from "./plants/plant_layer";
export * from "./points/point_layer";
export * from "./weeds/weed_layer";
export * from "./spread/spread_layer";
export * from "./tool_slots/tool_slot_layer";
export * from "./images/image_layer";

View File

@ -1,6 +1,6 @@
let mockPath = "/app/designer/plants";
jest.mock("../../../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/"))
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
import * as React from "react";
@ -31,7 +31,6 @@ describe("<PlantLayer/>", () => {
activeDragXY: { x: undefined, y: undefined, z: undefined },
animate: true,
hoveredPlant: undefined,
interactions: true,
});
it("shows plants", () => {
@ -60,19 +59,14 @@ describe("<PlantLayer/>", () => {
it("is in clickable mode", () => {
mockPath = "/app/designer/plants";
const p = fakeProps();
p.interactions = true;
p.plants[0].body.id = 1;
const wrapper = svgMount(<PlantLayer {...p} />);
expect(wrapper.find("Link").props().style).toEqual({
cursor: "pointer"
});
expect(wrapper.find("Link").props().style).toEqual({});
});
it("is in non-clickable mode", () => {
mockPath = "/app/designer/plants/crop_search/mint/add";
const p = fakeProps();
p.interactions = false;
p.plants[0].body.id = 1;
const wrapper = svgMount(<PlantLayer {...p} />);
expect(wrapper.find("Link").props().style)
.toEqual({ pointerEvents: "none" });
@ -117,12 +111,22 @@ describe("<PlantLayer/>", () => {
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", () => {
const p = fakeProps();
p.interactions = false;
p.plants[0].body.id = 0;
const plant = fakePlant();
plant.body.id = 0;
p.plants = [plant];
const wrapper = svgMount(<PlantLayer {...p} />);
expect(wrapper.find("Link").props().style)
expect((wrapper.find("Link").props()).style)
.toEqual({ pointerEvents: "none" });
});

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { GardenPlant } from "./garden_plant";
import { PlantLayerProps, Mode } from "../../interfaces";
import { unpackUUID } from "../../../../util";
import { getMode } from "../../util";
import { maybeNoPointer, getMode } from "../../util";
import { Link } from "../../../../link";
export function PlantLayer(props: PlantLayerProps) {
@ -44,12 +44,9 @@ export function PlantLayer(props: PlantLayerProps) {
activeDragXY={activeDragXY}
hovered={hovered}
animate={animate} />;
const style: React.SVGProps<SVGGElement>["style"] =
(props.interactions && p.body.id)
? { cursor: "pointer" } : { pointerEvents: "none" };
const wrapperProps = {
className: "plant-link-wrapper",
style,
style: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
key: p.uuid,
};
return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect)

View File

@ -1,7 +1,4 @@
jest.mock("../../../../../history", () => ({
history: { push: jest.fn() },
getPathArray: jest.fn(),
}));
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
import { GardenPoint } from "../garden_point";
@ -58,9 +55,10 @@ describe("<GardenPoint/>", () => {
it("opens point info", () => {
const p = fakeProps();
p.point.body.name = "weed";
const wrapper = svgMount(<GardenPoint {...p} />);
wrapper.find("g").simulate("click");
expect(history.push).toHaveBeenCalledWith(
`/app/designer/points/${p.point.body.id}`);
`/app/designer/weeds/${p.point.body.id}`);
});
});

View File

@ -19,12 +19,10 @@ describe("<PointLayer/>", () => {
mapTransformProps: fakeMapTransformProps(),
hoveredPoint: undefined,
dispatch: jest.fn(),
interactions: true,
});
it("shows points", () => {
const p = fakeProps();
p.interactions = false;
const wrapper = svgMount(<PointLayer {...p} />);
const layer = wrapper.find("#point-layer");
expect(layer.find(GardenPoint).html()).toContain("r=\"100\"");
@ -42,7 +40,6 @@ describe("<PointLayer/>", () => {
it("allows point mode interaction", () => {
mockPath = "/app/designer/points";
const p = fakeProps();
p.interactions = true;
const wrapper = svgMount(<PointLayer {...p} />);
const layer = wrapper.find("#point-layer");
expect(layer.props().style).toEqual({});

View File

@ -2,7 +2,8 @@ import * as React from "react";
import { GardenPointProps } from "../../interfaces";
import { transformXY } from "../../util";
import { Actions } from "../../../../constants";
import { mapPointClickAction } from "../../actions";
import { history } from "../../../../history";
import { isAWeed } from "../../../points/weeds_inventory";
export const GardenPoint = (props: GardenPointProps) => {
@ -18,11 +19,11 @@ export const GardenPoint = (props: GardenPointProps) => {
const { id, x, y, meta } = point.body;
const { qx, qy } = transformXY(x, y, mapTransformProps);
const color = meta.color || "green";
return <g id={`point-${id}`} className={"map-point"} stroke={color}
const panel = isAWeed(point.body.name, meta.type) ? "weeds" : "points";
return <g id={"point-" + id} className={"map-point"} stroke={color}
onMouseEnter={iconHover("start")}
onMouseLeave={iconHover("end")}
onClick={mapPointClickAction(props.dispatch, point.uuid,
`/app/designer/points/${id}`)}>
onClick={() => history.push(`/app/designer/${panel}/${id}`)}>
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius}
fill={hovered ? color : "transparent"} />
<circle id="point-center" cx={qx} cy={qy} r={2} />

View File

@ -1,7 +1,8 @@
import * as React from "react";
import { TaggedGenericPointer } from "farmbot";
import { GardenPoint } from "./garden_point";
import { MapTransformProps } from "../../interfaces";
import { MapTransformProps, Mode } from "../../interfaces";
import { getMode } from "../../util";
export interface PointLayerProps {
visible: boolean;
@ -9,14 +10,13 @@ export interface PointLayerProps {
mapTransformProps: MapTransformProps;
hoveredPoint: string | undefined;
dispatch: Function;
interactions: boolean;
}
export function PointLayer(props: PointLayerProps) {
const { visible, genericPoints, mapTransformProps, hoveredPoint } = props;
const style: React.CSSProperties =
props.interactions ? {} : { pointerEvents: "none" };
return <g id={"point-layer"} style={style}>
getMode() === Mode.points ? {} : { pointerEvents: "none" };
return <g id="point-layer" style={style}>
{visible &&
genericPoints.map(p =>
<GardenPoint

View File

@ -73,7 +73,7 @@ export class SpreadCircle extends
React.Component<SpreadCircleProps, SpreadCircleState> {
state: SpreadCircleState = { spread: undefined };
componentDidMount = () => {
UNSAFE_componentWillMount = () => {
cachedCrop(this.props.plant.body.openfarm_slug)
.then(({ spread }) => this.setState({ spread }));
}

View File

@ -22,6 +22,7 @@ describe("<ToolSlotLayer/>", () => {
pointer_type: "ToolSlot",
tool_id: undefined,
name: "Name",
radius: 50,
x: 1,
y: 2,
z: 3,
@ -37,7 +38,6 @@ describe("<ToolSlotLayer/>", () => {
mapTransformProps: fakeMapTransformProps(),
dispatch: jest.fn(),
hoveredToolSlot: undefined,
interactions: true,
};
}
it("toggles visibility off", () => {
@ -61,19 +61,9 @@ describe("<ToolSlotLayer/>", () => {
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", () => {
mockPath = "/app/designer/plants/crop_search/mint/add";
const p = fakeProps();
p.interactions = false;
const wrapper = shallow(<ToolSlotLayer {...p} />);
expect(wrapper.find("g").props().style)
.toEqual({ pointerEvents: "none" });

View File

@ -1,7 +1,4 @@
jest.mock("../../../../../history", () => ({
history: { push: jest.fn() },
getPathArray: jest.fn(),
}));
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";

View File

@ -2,6 +2,7 @@ import * as React from "react";
import { SlotWithTool, UUID } from "../../../../resources/interfaces";
import { ToolSlotPoint } from "./tool_slot_point";
import { MapTransformProps } from "../../interfaces";
import { maybeNoPointer } from "../../util";
export interface ToolSlotLayerProps {
visible: boolean;
@ -10,7 +11,6 @@ export interface ToolSlotLayerProps {
mapTransformProps: MapTransformProps;
dispatch: Function;
hoveredToolSlot: UUID | undefined;
interactions: boolean;
}
export function ToolSlotLayer(props: ToolSlotLayerProps) {
@ -18,9 +18,7 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
return <g
id="toolslot-layer"
style={props.interactions
? { cursor: "pointer" }
: { pointerEvents: "none" }}>
style={maybeNoPointer({ cursor: "pointer" })}>
{visible &&
slots.map(slot =>
<ToolSlotPoint

View File

@ -5,8 +5,8 @@ import { MapTransformProps } from "../../interfaces";
import { ToolbaySlot, ToolNames, Tool, GantryToolSlot } from "./tool_graphics";
import { ToolLabel } from "./tool_label";
import { includes } from "lodash";
import { history } from "../../../../history";
import { t } from "../../../../i18next_wrapper";
import { mapPointClickAction } from "../../actions";
export interface TSPProps {
slot: SlotWithTool;
@ -30,27 +30,25 @@ export const reduceToolName = (raw: string | undefined) => {
};
export const ToolSlotPoint = (props: TSPProps) => {
const { tool, toolSlot } = props.slot;
const {
id, x, y, pullout_direction, gantry_mounted
} = toolSlot.body;
} = props.slot.toolSlot.body;
const { mapTransformProps, botPositionX } = props;
const { quadrant, xySwap } = mapTransformProps;
const xPosition = gantry_mounted ? (botPositionX || 0) : x;
const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps);
const toolName = tool ? tool.body.name : t("Empty");
const hovered = toolSlot.uuid === props.hoveredToolSlot;
const toolName = props.slot.tool ? props.slot.tool.body.name : t("Empty");
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
const toolProps = {
x: qx,
y: qy,
hovered,
dispatch: props.dispatch,
uuid: toolSlot.uuid,
uuid: props.slot.toolSlot.uuid,
xySwap,
};
return <g id={"toolslot-" + id}
onClick={mapPointClickAction(props.dispatch, toolSlot.uuid,
`/app/designer/tool-slots/${id}`)}>
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}>
{pullout_direction && !gantry_mounted &&
<ToolbaySlot
id={id}

View File

@ -1,89 +0,0 @@
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}`);
});
});

View File

@ -1,66 +0,0 @@
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();
});
});

View File

@ -1,69 +0,0 @@
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>;
};

View File

@ -1,43 +0,0 @@
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>;
}

View File

@ -7,7 +7,6 @@ import {
import {
fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props";
import { ReactWrapper } from "enzyme";
describe("<ZonesLayer />", () => {
const fakeProps = (): ZonesLayerProps => ({
@ -19,7 +18,6 @@ describe("<ZonesLayer />", () => {
y: { value: 1500, isDefault: true }
},
mapTransformProps: fakeMapTransformProps(),
startDrag: jest.fn(),
});
it("renders", () => {
@ -27,27 +25,6 @@ describe("<ZonesLayer />", () => {
expect(wrapper.find(".zones-layer").length).toEqual(1);
});
const expectSolid = (zone2D: ReactWrapper) => {
const zoneProps = zone2D.find("rect").props();
expect(zoneProps.fill).toEqual(undefined);
expect(zoneProps.stroke).toEqual(undefined);
expect(zoneProps.strokeDasharray).toEqual(undefined);
expect(zoneProps.strokeWidth).toEqual(undefined);
};
const expectOutline = (zone2D: ReactWrapper) => {
const zoneProps = zone2D.find("rect").props();
expect(zoneProps.fill).toEqual("none");
expect(zoneProps.stroke).toEqual("white");
expect(zoneProps.strokeDasharray).toEqual(15);
expect(zoneProps.strokeWidth).toEqual(4);
};
const expectNone = (zone2D: ReactWrapper) => {
expect(zone2D.html()).toEqual(
"<g id=\"zones-2D-1\" class=\"current\"></g>");
};
it("renders current group's zones: 2D", () => {
const p = fakeProps();
p.visible = false;
@ -60,7 +37,6 @@ describe("<ZonesLayer />", () => {
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
expectSolid(wrapper.find("#zones-2D-1"));
expect(wrapper.find("#zones-2D-2").length).toEqual(0);
});
@ -73,22 +49,19 @@ describe("<ZonesLayer />", () => {
const wrapper = svgMount(<ZonesLayer {...p} />);
expect(wrapper.find("#zones-0D-1").length).toEqual(0);
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
expectNone(wrapper.find("#zones-2D-1"));
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
});
it("renders current group's zones: 0D", () => {
const p = fakeProps();
p.visible = false;
p.groups[0].body.id = 1;
p.groups[0].body.criteria.number_gt = { x: 10 };
p.groups[0].body.criteria.number_eq = { x: [100], y: [100] };
p.currentGroup = p.groups[0].uuid;
const wrapper = svgMount(<ZonesLayer {...p} />);
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
expect(wrapper.find("#zones-1D-1").length).toEqual(0);
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
expectOutline(wrapper.find("#zones-2D-1"));
expect(wrapper.find("#zones-2D-1").length).toEqual(0);
});
it("renders current group's zones: none", () => {
@ -97,20 +70,15 @@ describe("<ZonesLayer />", () => {
p.groups[0].body.id = 1;
p.currentGroup = p.groups[0].uuid;
const wrapper = svgMount(<ZonesLayer {...p} />);
expect(wrapper.html()).toEqual(
`<svg>
<g class=\"zones-layer\" style=\"cursor: pointer;\">
<g id=\"zones-2D-1\" class=\"current\">
</g>
</g>
</svg>`.replace(/[ ]{2,}/g, "").replace(/[^\S ]/g, ""));
expect(wrapper.html())
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
});
it("doesn't render current group's zones", () => {
const p = fakeProps();
p.visible = false;
const wrapper = svgMount(<ZonesLayer {...p} />);
expect(wrapper.html()).toEqual(
"<svg><g class=\"zones-layer\" style=\"cursor: pointer;\"></g></svg>");
expect(wrapper.html())
.toEqual("<svg><g class=\"zones-layer\"></g></svg>");
});
});

View File

@ -1,9 +1,7 @@
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
import * as React from "react";
import { svgMount } from "../../../../../__test_support__/svg_mount";
import {
Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType, spaceSelected,
Zones0D, ZonesProps, Zones1D, Zones2D, getZoneType, ZoneType,
} from "../zones";
import {
fakePointGroup,
@ -12,7 +10,6 @@ import {
fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props";
import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces";
import { history } from "../../../../../history";
const fakeProps = (): ZonesProps => ({
group: fakePointGroup(),
@ -60,15 +57,6 @@ describe("<Zones0D />", () => {
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
expect(wrapper.find("circle").length).toEqual(2);
});
it("opens group", () => {
const p = fakeProps();
p.group.body.id = 1;
p.group.body.criteria.number_eq = { x: [100], y: [200, 300] };
const wrapper = svgMount(<Zones0D {...p} />);
wrapper.find("#zones-0D-1").simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
});
});
describe("<Zones1D />", () => {
@ -116,15 +104,6 @@ describe("<Zones1D />", () => {
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
expect(wrapper.find("line").length).toEqual(2);
});
it("opens group", () => {
const p = fakeProps();
p.group.body.id = 1;
p.group.body.criteria.number_eq = { x: [], y: [200, 300] };
const wrapper = svgMount(<Zones1D {...p} />);
wrapper.find("#zones-1D-1").simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
});
});
describe("<Zones2D />", () => {
@ -158,16 +137,6 @@ describe("<Zones2D />", () => {
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
expect(wrapper.find("rect").length).toEqual(1);
});
it("opens group", () => {
const p = fakeProps();
p.group.body.id = 1;
p.group.body.criteria.number_gt = { x: 100, y: 200 };
p.group.body.criteria.number_lt = { x: 300, y: 400 };
const wrapper = svgMount(<Zones2D {...p} />);
wrapper.find("#zones-2D-1").simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/1");
});
});
describe("getZoneType()", () => {
@ -194,58 +163,3 @@ describe("getZoneType()", () => {
expect(getZoneType(group)).toEqual(ZoneType.points);
});
});
describe("spaceSelected()", () => {
const botSize = {
x: { value: 3000, isDefault: true },
y: { value: 1500, isDefault: true }
};
it("is selected: area", () => {
const group = fakePointGroup();
group.body.criteria.number_eq = {};
group.body.criteria.number_lt = {};
group.body.criteria.number_gt = {};
expect(spaceSelected(group, botSize)).toBeTruthy();
});
it("isn't selected: area", () => {
const group = fakePointGroup();
group.body.criteria.number_eq = {};
group.body.criteria.number_lt = { x: 100 };
group.body.criteria.number_gt = { x: 200 };
expect(spaceSelected(group, botSize)).toBeFalsy();
});
it("is selected: lines", () => {
const group = fakePointGroup();
group.body.criteria.number_eq = { x: [0] };
group.body.criteria.number_lt = {};
group.body.criteria.number_gt = {};
expect(spaceSelected(group, botSize)).toBeTruthy();
});
it("isn't selected: lines", () => {
const group = fakePointGroup();
group.body.criteria.number_eq = { x: [0] };
group.body.criteria.number_lt = {};
group.body.criteria.number_gt = { x: 100 };
expect(spaceSelected(group, botSize)).toBeFalsy();
});
it("is selected: points", () => {
const group = fakePointGroup();
group.body.criteria.number_eq = { x: [0], y: [0] };
group.body.criteria.number_lt = {};
group.body.criteria.number_gt = {};
expect(spaceSelected(group, botSize)).toBeTruthy();
});
it("isn't selected: points", () => {
const group = fakePointGroup();
group.body.criteria.number_eq = { x: [0], y: [0] };
group.body.criteria.number_lt = { x: 0 };
group.body.criteria.number_gt = {};
expect(spaceSelected(group, botSize)).toBeFalsy();
});
});

Some files were not shown because too many files have changed in this diff Show More