Compare commits

...

94 Commits

Author SHA1 Message Date
server 53bc33fc3e Example Debian Buster install script 2020-05-08 15:34:21 -06:00
Rick Carlino 1a7ee04d0b
Merge pull request #1778 from FarmBot/dep_updates
FBJS updates
2020-05-08 16:15:39 -05:00
gabrielburnworth 4a7a683ba7 dep updates (fe) 2020-05-08 13:27:15 -07:00
Rick Carlino 8700d50c81
Merge pull request #1777 from FarmBot/plant_z
Add plant z input
2020-05-07 15:39:35 -05:00
gabrielburnworth 3bab5694b8 add plant z input 2020-05-07 12:15:49 -07:00
Rick Carlino e205214ba4
Merge pull request #1776 from FarmBot/dep_updates
Dependency upgrades
2020-05-07 08:24:47 -05:00
gabrielburnworth 73e9daed05 dep updates (fe) 2020-05-06 15:56:04 -07:00
gabrielburnworth 426f97ddc2 minor step changes 2020-05-06 15:55:59 -07:00
Rick Carlino 88e526cce3
Merge pull request #1775 from FarmBot/point_meta_updates
Merge meta attrs, dont overwrite
2020-05-06 16:40:08 -05:00
Rick Carlino 9e14c2125d
Merge branch 'staging' into point_meta_updates 2020-05-06 15:10:17 -05:00
Rick Carlino 889c78c77a Merge meta attrs, dont overwrite 2020-05-06 15:07:27 -05:00
Rick Carlino 3b1dbe2209
Merge pull request #1774 from FarmBot/mark_as
New Mark As UI
2020-05-01 14:29:18 -05:00
gabrielburnworth 980d39f70d new update_resource ui 2020-05-01 09:00:21 -07:00
Rick Carlino 461f4c2509
Merge pull request #1772 from FarmBot/shutdown_step
Add shutdown sequence step
2020-04-28 10:28:39 -05:00
gabrielburnworth d2176fd6ea dep updates (fe) 2020-04-28 07:20:21 -07:00
gabrielburnworth c9511593a3 add shutdown sequence command 2020-04-28 07:18:04 -07:00
Rick Carlino 66553d143d
Merge pull request #1771 from FarmBot/planted_at_updates
Override `created_at` value with `planted_at` value when available
2020-04-27 21:50:03 -05:00
Rick Carlino 696350343b Override `created_at` value with `planted_at` value when available 2020-04-27 16:09:12 -05:00
Rick Carlino 1f773c44fc
Merge pull request #1770 from FarmBot/planted_at_updates
Set default `planted_at` value
2020-04-27 15:45:50 -05:00
Rick Carlino 87c22d4a96 Set default `planted_at` value 2020-04-27 15:05:53 -05:00
Rick Carlino 9f35dd9992
Merge pull request #1769 from FarmBot/dep_updates
Dependency upgrades
2020-04-24 11:10:19 -05:00
gabrielburnworth 14bf5216e0 dep updates (fe) 2020-04-24 08:22:24 -07:00
gabrielburnworth 1d196d633a Merge branch 'master' of https://github.com/FarmBot/Farmbot-Web-App into staging 2020-04-24 06:49:58 -07:00
Rick Carlino de607e3e3a
Merge pull request #1768 from FarmBot/master-hotfix/update-fallback
Minor hotfix
2020-04-23 20:07:29 -05:00
gabrielburnworth d931cd1b84 update coverage task 2020-04-23 16:55:52 -07:00
gabrielburnworth b3f93dd678 update fallback 2020-04-23 15:58:33 -07:00
Rick Carlino 7f9ecd450d
Merge pull request #1767 from FarmBot/fe_updates
Misc updates
2020-04-23 15:04:05 -05:00
gabrielburnworth 69462e4b60 weeks refactor 2020-04-23 12:12:04 -07:00
gabrielburnworth d3732aed20 version updates 2020-04-23 12:11:25 -07:00
Rick Carlino 6213028f0f
Merge pull request #1765 from FarmBot/mark_as
Mark As step updates
2020-04-21 20:38:10 -05:00
gabrielburnworth 281813369e resource_update -> update_resource (fe) 2020-04-21 14:39:55 -07:00
Rick Carlino 1014eece5f
Merge pull request #1764 from FarmBot/minor-fixes
Minor fixes
2020-04-20 18:06:23 -05:00
gabrielburnworth 6f484ab2e3 minor fixes 2020-04-20 14:07:40 -07:00
Rick Carlino 0bd6d9a967
Merge pull request #1763 from FarmBot/mark_as
Add weed status for Mark As step
2020-04-20 09:26:28 -05:00
gabrielburnworth 25b2f18c4c add weed status for mark as 2020-04-17 16:13:26 -07:00
Rick Carlino 1556084dbd
Merge branch 'staging' into always_upgrade 2020-04-17 13:22:18 -05:00
Rick Carlino 0571100229 Remind custoemrs to upgrade FBOS before troubleshooting 2020-04-17 13:13:32 -05:00
Rick Carlino d6909f439c
Merge pull request #1760 from FarmBot/mark_as
Delete dump_info node
2020-04-17 11:54:09 -05:00
Rick Carlino 36b5c90b65 Merge remote-tracking branch 'origin/mark_as' into mark_as 2020-04-17 11:09:29 -05:00
Rick Carlino f3ac957485 Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into mark_as 2020-04-17 11:08:31 -05:00
Rick Carlino 6f834517ca More dump_info removal 2020-04-17 11:08:17 -05:00
Rick Carlino 44c3f7dc4e
Merge branch 'staging' into mark_as 2020-04-17 11:01:53 -05:00
Rick Carlino 5bb77c1c14 Delete dump_info node 2020-04-17 10:58:36 -05:00
Rick Carlino 3ee1478a58
Merge pull request #1759 from FarmBot/mark_as
Plant stage updates.
2020-04-17 10:58:02 -05:00
Rick Carlino df9e0ef26b Update PLANT_STAGES 2020-04-17 09:56:53 -05:00
Rick Carlino 0e02ca06ee Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into mark_as 2020-04-16 15:57:29 -05:00
Rick Carlino 643bcb1a37
Merge pull request #1758 from FarmBot/mark_as
Phase 0: Ability to pass variables to MARK AS step
2020-04-16 13:27:52 -05:00
Rick Carlino 88b20a73ea Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into mark_as 2020-04-16 13:26:29 -05:00
Rick Carlino e8a8165635
Merge branch 'staging' into mark_as 2020-04-16 13:04:15 -05:00
Rick Carlino 588d4eb36e
Merge pull request #1757 from FarmBot/settings_updates
Settings and dependency updates
2020-04-16 13:04:03 -05:00
Rick Carlino efea80b593 Deprecate `resource_update`. Add `update_resource`. 2020-04-15 13:46:46 -05:00
gabrielburnworth c75d93f3c4 dep updates (fe) 2020-04-14 16:03:48 -07:00
gabrielburnworth bee1e0e074 settings panel updates 2020-04-14 16:03:38 -07:00
Rick Carlino 4375a935f0
Merge pull request #1756 from FarmBot/recovery_release
v9.3.0 - Jolly Juniper
2020-04-14 09:33:24 -05:00
Rick Carlino 7d5fe7c9f6 Deploy fixes 2020-04-14 09:09:07 -05:00
Rick Carlino e801d53d51 Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into staging 2020-04-14 08:47:43 -05:00
Rick Carlino bf0a03d11d
Merge pull request #1754 from FarmBot/search_refactor
Search Refactor
2020-04-14 08:46:51 -05:00
gabrielburnworth ec757b1b29 misc bug fixes 2020-04-13 23:38:49 -07:00
gabrielburnworth 3c3b120b9b refactor search fields 2020-04-13 18:15:11 -07:00
Rick Carlino 046035ab9e
Merge pull request #1753 from FarmBot/staging
v9.2.6 - Jolly Juniper
2020-04-13 17:27:06 -05:00
Rick Carlino 52b481e831 Merge branch 'staging' of github.com:FarmBot/Farmbot-Web-App into staging 2020-04-13 16:43:16 -05:00
Rick Carlino 73422eb8ea
Merge pull request #1752 from FarmBot/criteria_ui_updates
Criteria UI updates
2020-04-13 16:42:26 -05:00
gabrielburnworth b087e08f13 criteria ui updates 2020-04-13 12:24:38 -07:00
Rick Carlino 1e1b405c32
Merge pull request #1751 from FarmBot/staging
v9.2.6 release
2020-04-12 11:49:39 -05:00
Rick Carlino dccea4e474
Merge pull request #1750 from FarmBot/image_updates
v9.2.6 - Jolly Juniper
2020-04-12 11:22:26 -05:00
Rick Carlino 91d86bad0c Update structure.sql 2020-04-12 10:55:08 -05:00
Rick Carlino bfe4df68a8 Fix typo in migration 2020-04-12 10:41:53 -05:00
Rick Carlino dd6a43d901 Dep updates 2020-04-12 10:33:49 -05:00
Rick Carlino 726cd6d4e7 Merge branch 'master' into image_updates 2020-04-12 10:24:48 -05:00
Rick Carlino d730cd9260 Bump image limit from 100 to 450 2020-04-12 10:24:17 -05:00
Rick Carlino eb8cfd3c91
Merge pull request #1749 from FarmBot/staging
v9.2.5 - Jolly Juniper
2020-04-08 14:00:45 -05:00
Rick Carlino 8f9bd4a5e7
Merge pull request #1746 from FarmBot/weeds
Add new weed pointer type
2020-04-02 13:51:59 -05:00
gabrielburnworth 3ebf434945 use Weed instead of GenericPointer for weeds 2020-04-02 10:24:33 -07:00
Rick Carlino 92a7194c6e
Merge pull request #1744 from FarmBot/bugfix/add-tool
Fix add tool bug
2020-03-31 09:32:33 -05:00
gabrielburnworth 24ad841d7f add and edit tool improvements 2020-03-30 17:57:32 -07:00
Rick Carlino b45f806309
Merge pull request #1740 from FarmBot/staging
v9.2.4
2020-03-24 12:43:37 -05:00
Rick Carlino cf7ec86106
Merge pull request #1738 from FarmBot/weeds
Part I: Fork "Weed" point type from "GenericPointer"
2020-03-22 20:53:42 -05:00
Rick Carlino 37f7517c51 Temporary changes to pass type checking. May not be correct. 2020-03-22 19:26:19 -05:00
Rick Carlino 2a04803dc6 Fork `Weed` pointer_type from GenericPointer, not Plant 2020-03-22 18:57:47 -05:00
Rick Carlino 3700406687 Add new `Weed` PointerType 2020-03-22 18:32:00 -05:00
Rick Carlino 1ba0ff7871
Merge pull request #1737 from FarmBot/deps
Routine Dep Upgrades
2020-03-22 14:07:55 -05:00
Rick Carlino be23cd44b5 Routine dep upgrades: JS 2020-03-20 11:09:54 -05:00
Rick Carlino 43d1b9da33 Routine dep upgrades: Ruby 2020-03-20 10:31:18 -05:00
Rick Carlino bda30bae09
Merge pull request #1736 from FarmBot/staging
v9.2.3 - Jolly Juniper
2020-03-18 16:23:53 -05:00
Rick Carlino de21cb16da Merge branch 'master' of https://git.heroku.com/farmbot-production into staging 2020-03-18 15:56:33 -05:00
Rick Carlino 0a153bc656 Merge branch 'master' of github.com:FarmBot/Farmbot-Web-App into staging 2020-03-18 15:56:14 -05:00
Rick Carlino 149451c270
Merge pull request #1735 from FarmBot/bot-online-bugfix
Minor bug fix
2020-03-18 15:46:50 -05:00
Gabriel Burnworth d7de315c20
flip bot online boolean 2020-03-18 13:09:11 -07:00
Rick Carlino 61c09b69b9 Merge branch 'master' of github.com:FarmBot/Farmbot-Web-App 2020-03-17 15:29:47 -05:00
Rick Carlino 863071824b
Merge pull request #1733 from FarmBot/staging
v9.2.2 - Jolly Juniper
2020-03-17 15:27:37 -05:00
Rick Carlino 3a9ea9af79
Merge pull request #1731 from FarmBot/staging
v9.2.1 - Jolly Juniper
2020-03-16 19:19:41 -05:00
Rick Carlino bc68f3e79f Merge branch 'staging' 2020-03-16 19:08:55 -05:00
Rick Carlino bce0700cd9
Merge pull request #1723 from FarmBot/staging
v9.2.0 - Jolly Juniper
2020-02-27 15:37:03 -06:00
Rick Carlino a5b1d5631e
Merge pull request #1701 from FarmBot/staging
v9.1.3 - Jolly Juniper
2020-02-20 12:57:33 -06:00
302 changed files with 8959 additions and 3714 deletions

View File

@ -35,7 +35,7 @@ group :development, :test do
gem "hashdiff"
gem "pry-rails"
gem "pry"
gem "rspec-rails", "4.0.0.beta3"
gem "rspec-rails"
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.1)
actionpack (= 6.0.2.1)
actioncable (6.0.2.2)
actionpack (= 6.0.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
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)
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)
mail (>= 2.7.1)
actionmailer (6.0.2.1)
actionpack (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
actionmailer (6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.2.1)
actionview (= 6.0.2.1)
activesupport (= 6.0.2.1)
actionpack (6.0.2.2)
actionview (= 6.0.2.2)
activesupport (= 6.0.2.2)
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.1)
actionpack (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
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)
nokogiri (>= 1.8.5)
actionview (6.0.2.1)
activesupport (= 6.0.2.1)
actionview (6.0.2.2)
activesupport (= 6.0.2.2)
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.1)
activesupport (= 6.0.2.1)
activejob (6.0.2.2)
activesupport (= 6.0.2.2)
globalid (>= 0.3.6)
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)
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)
marcel (~> 0.3.1)
activesupport (6.0.2.1)
activesupport (6.0.2.2)
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.0)
amq-protocol (2.3.1)
bcrypt (3.1.13)
builder (3.2.4)
bunny (2.14.4)
amq-protocol (~> 2.3, >= 2.3.0)
bunny (2.15.0)
amq-protocol (~> 2.3, >= 2.3.1)
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.4.1)
digest-crc (0.5.1)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
erubi (1.9.0)
factory_bot (5.1.1)
factory_bot (5.1.2)
activesupport (>= 4.2.0)
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.10.2)
faker (2.11.0)
i18n (>= 1.6, < 2)
faraday (0.15.4)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1)
faraday_middleware (0.14.0)
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.1)
google-api-client (0.37.2)
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.0)
faraday (~> 0.11)
google-cloud-env (1.3.1)
faraday (>= 0.17.3, < 2.0)
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.10.0)
faraday (~> 0.12)
googleauth (0.11.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
hashdiff (1.0.0)
hashdiff (1.0.1)
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.4.0)
loofah (2.5.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 (0.9.2)
method_source (1.0.0)
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.0)
mutations (0.9.1)
activesupport
nio4r (2.5.2)
nokogiri (1.10.8)
nokogiri (1.10.9)
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.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pg (1.2.3)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
rabbitmq_http_api_client (1.12.0)
faraday (~> 0.15.4)
faraday_middleware (~> 0.13.0)
rabbitmq_http_api_client (1.13.0)
faraday (>= 0.15, < 1)
faraday_middleware (>= 0.13.0, < 1)
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.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)
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)
bundler (>= 1.3.0)
railties (= 6.0.2.1)
railties (= 6.0.2.2)
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.1)
actionpack (= 6.0.2.1)
activesupport (= 6.0.2.1)
railties (6.0.2.2)
actionpack (= 6.0.2.2)
activesupport (= 6.0.2.2)
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.0)
rspec-expectations (3.9.1)
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.beta3)
rspec-rails (4.0.0)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
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.12.0)
signet (0.13.0)
addressable (~> 2.3)
faraday (~> 0.9)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.18.5)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-html (0.12.1)
simplecov-html (0.12.2)
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.6)
tzinfo (1.2.7)
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.2.2)
zeitwerk (2.3.0)
PLATFORMS
ruby
@ -348,7 +348,7 @@ DEPENDENCIES
request_store
rollbar
rspec
rspec-rails (= 4.0.0.beta3)
rspec-rails
rspec_junit_formatter
scenic
secure_headers

View File

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

View File

@ -16,9 +16,9 @@ module CeleryScriptSettingsBag
end
PIN_TYPE_MAP = { "Peripheral" => Peripheral,
"Sensor" => Sensor,
"BoxLed3" => BoxLed,
"BoxLed4" => BoxLed }
"Sensor" => Sensor,
"BoxLed3" => BoxLed,
"BoxLed4" => BoxLed }
ALLOWED_AXIS = %w(x y z all)
ALLOWED_ASSERTION_TYPES = %w(abort recover abort_recover continue)
ALLOWED_CHANGES = %w(add remove update)
@ -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)
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot GenericPointer)
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant Weed)
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot Weed GenericPointer)
ALLOWED_RPC_NODES = %w(assertion calibrate change_ownership
check_updates dump_info emergency_lock
check_updates emergency_lock
emergency_unlock execute execute_script
factory_reset find_home flash_firmware home
install_farmware install_first_party_farmware _if
move_absolute move_relative power_off read_pin
read_status reboot remove_farmware resource_update
read_status reboot remove_farmware update_resource
send_message set_servo_angle set_user_env sync
take_photo toggle_pin update_farmware wait
write_pin zero)
ALLOWED_SPEC_ACTION = %w(dump_info emergency_lock emergency_unlock power_off
ALLOWED_SPEC_ACTION = %w(emergency_lock emergency_unlock power_off
read_status reboot sync take_photo)
ANY_VARIABLE = %i(tool coordinate point identifier)
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
@ -73,8 +73,7 @@ module CeleryScriptSettingsBag
ONLY_ONE_COORD = "Move Absolute does not accept a group of locations " \
"as input. Please change your selection to a single" \
" location."
PLANT_STAGES = %w(planned planted harvested sprouted)
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
PLANT_STAGES = %w(planned planted harvested sprouted removed)
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
MISC_ENUM_ERR = '"%s" is not valid. Allowed values: %s'
MAX_WAIT_MS = 1000 * 60 * 3 # Three Minutes
@ -82,6 +81,13 @@ module CeleryScriptSettingsBag
"A single wait node cannot exceed #{MAX_WAIT_MS / 1000 / 60} minutes. " +
"Consider lowering the wait time or using multiple WAIT blocks."
Corpus = CeleryScript::Corpus.new
THIS_IS_DEPRECATED = {
args: [:resource_type, :resource_id, :label, :value],
tags: [:function, :api_writer, :network_user],
blk: ->(n) do
n.invalidate!("Deprecated `mark_as` detected. Delete it and re-add")
end,
}
CORPUS_VALUES = {
boolean: [TrueClass, FalseClass],
@ -278,6 +284,9 @@ module CeleryScriptSettingsBag
lua: {
defn: [v(:string)],
},
resource: {
defn: [n(:identifier), n(:resource)],
},
}.map do |(name, conf)|
blk = conf[:blk]
defn = conf.fetch(:defn)
@ -317,10 +326,6 @@ module CeleryScriptSettingsBag
args: [:x, :y, :z],
tags: [:data, :location_like],
},
dump_info: {
tags: [:function, :network_user, :disk_user, :api_writer],
docs: "Sends an info dump to server administrators for troubleshooting.",
},
emergency_lock: {
tags: [:function, :firmware_user, :control_flow],
},
@ -513,15 +518,22 @@ module CeleryScriptSettingsBag
tags: [:function, :firmware_user, :rpi_user],
blk: ->(n) { no_rpi_analog(n) },
},
resource_update: {
args: RESOURCE_UPDATE_ARGS,
tags: [:function, :api_writer, :network_user],
# DEPRECATED- Get rid of this node ASAP -RC 15 APR 2020
resource_update: THIS_IS_DEPRECATED,
resource: {
args: [:resource_type, :resource_id],
tags: [:network_user],
blk: ->(n) do
resource_type = n.args.fetch(:resource_type).value
resource_id = n.args.fetch(:resource_id).value
check_resource_type(n, resource_type, resource_id, Device.current)
end,
},
update_resource: {
args: [:resource],
body: [:pair],
tags: [:function, :api_writer, :network_user],
},
point_group: {
args: [:point_group_id],
tags: [:data, :list_like],
@ -529,7 +541,7 @@ module CeleryScriptSettingsBag
resource_id = n.args.fetch(:point_group_id).value
check_resource_type(n, "PointGroup", resource_id, Device.current)
end,
}
},
}.map { |(name, list)| Corpus.node(name, **list) }
HASH = Corpus.as_json

View File

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

View File

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

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"]
POINTER_KINDS = ["GenericPointer", "Plant", "ToolSlot", "Weed"]
self.inheritance_column = "pointer_type"
belongs_to :device

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
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 without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
@ -262,7 +262,7 @@ CREATE TABLE public.devices (
id integer NOT NULL,
name character varying DEFAULT 'FarmBot'::character varying,
max_log_count integer DEFAULT 1000,
max_images_count integer DEFAULT 100,
max_images_count integer DEFAULT 450,
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)::text, ('"ToolSlot"'::character varying)::text, ('"Plant"'::character varying)::text])))
WHERE (((edge_nodes.kind)::text = 'resource_type'::text) AND ((edge_nodes.value)::text = ANY ((ARRAY['"GenericPointer"'::character varying, '"ToolSlot"'::character varying, '"Plant"'::character varying])::text[])))
), resource_id AS (
SELECT edge_nodes.primary_node_id,
edge_nodes.kind,
@ -1644,8 +1644,7 @@ 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_count integer
inactivity_warning_sent_at timestamp without time zone
);
@ -1729,7 +1728,9 @@ 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
assertion_log integer DEFAULT 1,
show_zones boolean DEFAULT false,
show_weeds boolean DEFAULT false
);
@ -3376,6 +3377,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20191219212755'),
('20191220010646'),
('20200116140201'),
('20200204192005');
('20200204192005'),
('20200204230135'),
('20200323235926'),
('20200412152208');

90
debian_example.sh 100644
View File

@ -0,0 +1,90 @@
# How to install FarmBot Web API on a Debian Buster (10) Machine
# IMPORTANT NOTE: Resources are limited and Farmbot, inc. cannot provide
# longterm support to self-hosted users. If you have never administered a
# Ruby on Rails application, we highly advise stopping now. this presents an
# extremely high risk of data loss. Free hosting is provided at
# https://my.farm.bot and eliminates the risks and troubles of self-hosting.
#
# You are highly encouraged to use the my.farm.bot servers. Self hosted
# documentation is provided with the assumption that you have experience with
# Ruby/Javascript development.
#
# Self-hosting a Farmbot server is not a simple task.
# Remove old (possibly broke) docker versions
sudo apt-get remove docker docker-engine docker.io
# Install docker
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common gnupg2 --yes
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable" --yes
sudo apt-get update --yes
sudo apt-get install docker-ce --yes
sudo docker run hello-world # Should run!
# Install docker-compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Install FarmBot Web App
# âš  SKIP THIS STEP IF UPGRADING!
git clone https://github.com/FarmBot/Farmbot-Web-App --depth=5 --branch=master
cd Farmbot-Web-App
#snap install micro --classic # Don't like `micro`? vim, nano, etc are fine, too.
cp example.env .env # âš  SKIP THIS STEP IF UPGRADING!
# == This is a very important step!!! ==
#
# Open `.env` in a text editor and change all the values.
#
# == Nothing will work if you skip this step!!! ==
vim .env # âš  SKIP THIS STEP IF UPGRADING!
# ^ This is the most important step
# READ NOTE ABOVE. Very important!
# Install the correct version of bundler for the project
sudo docker-compose run web gem install bundler:2.1.4
# Install application specific Ruby dependencies
sudo docker-compose run web bundle install
# Install application specific Javascript deps
sudo docker-compose run web npm install
# Create a database in PostgreSQL
sudo docker-compose run web bundle exec rails db:create db:migrate
# Generate a set of *.pem files for data encryption
sudo docker-compose run web rake keys:generate # âš  SKIP THIS STEP IF UPGRADING!
# Build the UI assets via ParcelJS
sudo docker-compose run web rake assets:precompile
# Run the server! ٩(^‿^)۶
# NOTE: DONT TRY TO LOGIN until you see a message similar to this:
# "✨ Built in 44.92s"
# THIS MAY TAKE A VERY LONG TIME ON SLOW MACHINES (~3 minutes on DigitalOcean)
# You will just get an empty screen otherwise.
# This only happens during initialization
sudo docker-compose up
# At this point, setup is complete. Content should be visible at ===============
# http://YOUR_HOST:3000/.
# You can optionally verify installation by running unit tests.
# Create the database for the app to use:
sudo docker-compose run -e RAILS_ENV=test web bundle exec rails db:setup
# Run the tests in the "test" RAILS_ENV:
sudo docker-compose run -e RAILS_ENV=test web rspec spec
# Run user-interface unit tests REQUIRES AT LEAST 4 GB OF RAM:
sudo docker-compose run web npm run test
# === BEGIN OPTIONAL UPGRADES
# To update to later versions of FarmBot,
# shut down the server, create a database backup
# and run commands below.
git pull https://github.com/FarmBot/Farmbot-Web-App.git master
sudo docker-compose build
sudo docker-compose run web bundle install # <== âš  UPGRADE USERS ONLY
sudo docker-compose run web npm install # <== âš  UPGRADE USERS ONLY
sudo docker-compose run web rails db:migrate # <== âš  UPGRADE USERS ONLY
# === END OPTIONAL UPGRADES ^

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import {
TaggedAlert,
TaggedPointGroup,
TaggedFolder,
TaggedWeedPointer,
} from "farmbot";
import { fakeResource } from "../fake_resource";
import {
@ -133,7 +134,6 @@ export function fakeToolSlot(): TaggedToolSlotPointer {
x: 0,
y: 0,
z: 0,
radius: 25,
pointer_type: "ToolSlot",
meta: {},
tool_id: undefined,
@ -171,6 +171,20 @@ export function fakePoint(): TaggedGenericPointer {
});
}
export function fakeWeed(): TaggedWeedPointer {
return fakeResource("Point", {
id: idCounter++,
name: "Weed 1",
pointer_type: "Weed",
x: 200,
y: 400,
z: 0,
radius: 100,
plant_stage: "planned",
meta: { created_by: "plant-detection", color: "red" }
});
}
export function fakeSavedGarden(): TaggedSavedGarden {
return fakeResource("SavedGarden", {
id: idCounter++,
@ -289,6 +303,7 @@ 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,4 +7,5 @@ jest.mock("../toast/toast", () => ({
error: jest.fn(),
warning: jest.fn(),
busy: jest.fn(),
removeToast: jest.fn(),
}));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -120,46 +120,68 @@
.thin-search-wrapper {
width: 100%;
.text-input-wrapper {
position: relative;
margin: 1rem;
border-bottom: 1px solid $dark_gray;
&:before,
&:after {
content: "";
.thin-search {
.spinner-container {
position: absolute;
bottom: 0;
background: $dark_gray;
width: 1px;
height: 3px;
}
&:before {
left: 0;
}
&:after {
top: 0;
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
margin-right: 1rem;
}
i {
font-size: 1.5rem;
.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;
}
}
}
.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;
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;
}
}
}
}
@ -185,23 +207,33 @@
}
}
%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: 0 1rem 0 0;
height: 4rem;
width: 4rem;
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;
}
}
}
}
%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;
@ -209,6 +241,7 @@
.group-item-count {
@extend %panel-item-base;
padding-top: 0.6rem;
line-height: 1rem;
}
.plant-search-item-name {
display: inline-block;
@ -219,24 +252,27 @@
text-overflow: ellipsis;
margin-left: 1rem;
}
.weed-search-item,
.point-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
.saucer {
display: inline-block;
margin: 0 1rem 0 0;
height: 2rem;
width: 2rem;
height: 3rem;
width: 3rem;
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;
@ -244,19 +280,34 @@
width: 40%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 1rem;
margin-left: 1.25rem;
}
}
.thin-search {
.spinner-container {
position: absolute;
top: 0;
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
margin-right: 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;
}
}
}
@ -284,11 +335,17 @@
}
.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;
@ -337,6 +394,9 @@
fill: $white;
stroke: $white;
}
&:hover {
opacity: 0.15;
}
}
}
@ -504,10 +564,10 @@
cursor: pointer;
}
.more-bugs,
.select-mode,
.move-to-mode {
button {
margin-right: 1rem;
}
margin: auto;
margin-top: 1rem;
p {
text-align: center;
padding-top: 2rem;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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>
<label style={{ lineHeight: "1.5rem" }}>
{t(DeviceSetting.resetHardwareParams)}
</label>
</Col>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,13 @@ export function Diagnosis(props: DiagnosisProps) {
<div className={"saucer-connector last " + diagnosisColor} />
</Col>
<Col xs={10} className={"connectivity-diagnosis"}>
<p className="blinking">
{t("Always")}&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,6 +81,7 @@ export enum Feature {
ota_update_hour = "ota_update_hour",
rpi_led_control = "rpi_led_control",
sensors = "sensors",
update_resource = "update_resource",
use_update_channel = "use_update_channel",
variables = "variables",
}

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,6 @@ 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,
@ -28,6 +27,8 @@ 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 => ({
@ -36,6 +37,7 @@ describe("<FarmDesigner/>", () => {
designer: fakeDesignerState(),
hoveredPlant: undefined,
genericPoints: [],
weeds: [],
allPoints: [],
plants: [],
toolSlots: [],
@ -46,7 +48,10 @@ describe("<FarmDesigner/>", () => {
raw_encoders: { x: undefined, y: undefined, z: undefined },
},
botMcuParams: bot.hardware.mcu_params,
stepsPerMmXY: { x: undefined, y: undefined },
botSize: {
x: { value: 3000, isDefault: true },
y: { value: 1500, isDefault: true },
},
peripherals: [],
eStopStatus: false,
latestImages: [],
@ -67,8 +72,7 @@ describe("<FarmDesigner/>", () => {
it("loads default map settings", () => {
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
const legendProps =
wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
const legendProps = wrapper.find(GardenMapLegend).props();
expect(legendProps.legendMenuOpen).toBeFalsy();
expect(legendProps.showPlants).toBeTruthy();
expect(legendProps.showPoints).toBeTruthy();
@ -76,8 +80,7 @@ describe("<FarmDesigner/>", () => {
expect(legendProps.showFarmbot).toBeTruthy();
expect(legendProps.showImages).toBeFalsy();
expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 });
// tslint:disable-next-line:no-any
const gardenMapProps = wrapper.find("GardenMap").props() as any;
const gardenMapProps = wrapper.find(GardenMap).props();
expect(gardenMapProps.gridSize.x).toEqual(2900);
expect(gardenMapProps.gridSize.y).toEqual(1400);
});
@ -90,8 +93,7 @@ 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() as GardenMapLegendProps;
const legendProps = wrapper.find(GardenMapLegend).props();
expect(legendProps.imageAgeInfo)
.toEqual({ newestDate: "2001-01-03T00:00:00.000Z", toOldest: 2 });
});
@ -137,4 +139,18 @@ 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: 30 });
expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
});
it("goes back", () => {

View File

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

@ -1,70 +0,0 @@
jest.mock("../../config_storage/actions", () => ({
getWebAppConfigValue: jest.fn(x => { x(); return jest.fn(() => true); }),
setWebAppConfigValue: jest.fn(),
}));
import * as React from "react";
import { mount, ReactWrapper } from "enzyme";
import {
RawDesignerSettings as DesignerSettings, DesignerSettingsProps,
mapStateToProps,
} from "../settings";
import { fakeState } from "../../__test_support__/fake_state";
import { BooleanSetting, NumericSetting } from "../../session_keys";
import { setWebAppConfigValue } from "../../config_storage/actions";
const getSetting =
(wrapper: ReactWrapper, position: number, containsString: string) => {
const setting = wrapper.find(".designer-setting").at(position);
expect(setting.text().toLowerCase())
.toContain(containsString.toLowerCase());
return setting;
};
describe("<DesignerSettings />", () => {
const fakeProps = (): DesignerSettingsProps => ({
dispatch: jest.fn(),
getConfigValue: jest.fn(),
});
it("renders settings", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
expect(wrapper.text()).toContain("size");
const settings = wrapper.find(".designer-setting");
expect(settings.length).toEqual(7);
});
it("renders defaultOn setting", () => {
const p = fakeProps();
p.getConfigValue = () => undefined;
const wrapper = mount(<DesignerSettings {...p} />);
const confirmDeletion = getSetting(wrapper, 6, "confirm plant");
expect(confirmDeletion.find("button").text()).toEqual("on");
});
it("toggles setting", () => {
const wrapper = mount(<DesignerSettings {...fakeProps()} />);
const trailSetting = getSetting(wrapper, 1, "trail");
trailSetting.find("button").simulate("click");
expect(setWebAppConfigValue)
.toHaveBeenCalledWith(BooleanSetting.display_trail, true);
});
it("changes origin", () => {
const p = fakeProps();
p.getConfigValue = () => 2;
const wrapper = mount(<DesignerSettings {...p} />);
const originSetting = getSetting(wrapper, 5, "origin");
originSetting.find("div").last().simulate("click");
expect(setWebAppConfigValue).toHaveBeenCalledWith(
NumericSetting.bot_origin_quadrant, 4);
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const props = mapStateToProps(fakeState());
const value = props.getConfigValue(BooleanSetting.show_plants);
expect(value).toEqual(true);
});
});

View File

@ -1,4 +1,4 @@
import { mapStateToProps, getPlants } from "../state_to_props";
import { mapStateToProps, getPlants, botSize } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import {
buildResourceIndex, fakeDevice,
@ -11,14 +11,13 @@ 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 = {
@ -40,18 +39,11 @@ 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.selectedPlants = [plantUuid];
state.resources.consumers.farm_designer.selectedPoints = [plantUuid];
expect(mapStateToProps(state).selectedPlant).toEqual(
expect.objectContaining({ uuid: plantUuid }));
});
@ -61,11 +53,8 @@ describe("mapStateToProps()", () => {
const webAppConfig = fakeWebAppConfig();
(webAppConfig.body as WebAppConfig).show_historic_points = true;
const point1 = fakePoint();
point1.body.discarded_at = undefined;
const point2 = fakePoint();
point2.body.discarded_at = DISCARDED_AT;
const point3 = fakePoint();
point3.body.discarded_at = DISCARDED_AT;
state.resources = buildResourceIndex([
webAppConfig, point1, point2, point3, fakeDevice(),
]);
@ -77,15 +66,12 @@ describe("mapStateToProps()", () => {
const webAppConfig = fakeWebAppConfig();
(webAppConfig.body as WebAppConfig).show_historic_points = false;
const point1 = fakePoint();
point1.body.discarded_at = undefined;
const point2 = fakePoint();
point2.body.discarded_at = DISCARDED_AT;
const point3 = fakePoint();
point3.body.discarded_at = DISCARDED_AT;
state.resources = buildResourceIndex([
webAppConfig, point1, point2, point3, fakeDevice(),
]);
expect(mapStateToProps(state).genericPoints.length).toEqual(1);
expect(mapStateToProps(state).genericPoints.length).toEqual(3);
});
it("returns sensor readings", () => {
@ -144,3 +130,45 @@ describe("getPlants()", () => {
expect.objectContaining({ rotation: "15" }));
});
});
describe("botSize()", () => {
it("returns default bot size", () => {
const state = fakeState();
expect(botSize(state)).toEqual({
x: { value: 2900, isDefault: true },
y: { value: 1400, isDefault: true },
});
});
it("returns map setting bot size", () => {
const state = fakeState();
const webAppConfig = fakeWebAppConfig();
webAppConfig.body.map_size_x = 1000;
webAppConfig.body.map_size_y = 1000;
state.resources = buildResourceIndex([fakeDevice(), webAppConfig]);
expect(botSize(state)).toEqual({
x: { value: 1000, isDefault: true },
y: { value: 1000, isDefault: true },
});
});
it("returns axis length setting bot size", () => {
const state = fakeState();
const firmwareConfig = fakeFirmwareConfig();
firmwareConfig.body.movement_step_per_mm_x = 2;
firmwareConfig.body.movement_step_per_mm_y = 4;
firmwareConfig.body.movement_stop_at_max_x = 1;
firmwareConfig.body.movement_stop_at_max_y = 1;
firmwareConfig.body.movement_axis_nr_steps_x = 100;
firmwareConfig.body.movement_axis_nr_steps_y = 100;
const webAppConfig = fakeWebAppConfig();
webAppConfig.body.map_size_x = 1000;
webAppConfig.body.map_size_y = 1000;
state.resources = buildResourceIndex([
fakeDevice(), firmwareConfig, webAppConfig]);
expect(mapStateToProps(state).botSize).toEqual({
x: { value: 50, isDefault: false },
y: { value: 25, isDefault: false },
});
});
});

View File

@ -81,21 +81,12 @@ 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" : ""}`}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
{!props.noIcon &&
<i className="fa fa-search" />}
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
</div>
{props.children}
{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,7 +545,10 @@ describe("<RepeatForm />", () => {
const fakeProps = (): RepeatFormProps => ({
isRegimen: false,
fieldGet: jest.fn(key =>
"" + ({ endDate: "2017-07-26" } as FarmEventViewModel)[key]),
"" + ({
endDate: "2017-07-26", endTime: "08:57",
startDate: "2017-07-25", startTime: "08:57"
} as FarmEventViewModel)[key]),
fieldSet: jest.fn(),
timeSettings: fakeTimeSettings(),
});

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,6 +16,7 @@ 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)
@ -105,14 +106,12 @@ export class PureFarmEvents
<DesignerPanelTop
panel={Panel.FarmEvents}
linkTo={"/app/designer/events/add"}
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...")} />
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 })} />
</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,8 +11,7 @@ import { NumericSetting, BooleanSetting } from "../session_keys";
import { isUndefined, last, isFinite } from "lodash";
import { AxisNumberProperty, BotSize } from "./map/interfaces";
import {
getBotSize, round, getPanelStatus, MapPanelStatus, mapPanelClassName,
getMapPadding,
round, getPanelStatus, MapPanelStatus, mapPanelClassName, getMapPadding,
} from "./map/util";
import {
calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex,
@ -70,6 +69,7 @@ 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,6 +116,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
legend_menu_open,
show_plants,
show_points,
show_weeds,
show_spread,
show_farmbot,
show_images,
@ -124,11 +125,6 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
zoom_level
} = this.state;
const botSize = getBotSize(
this.props.botMcuParams,
this.props.stepsPerMmXY,
getDefaultAxisLength(this.props.getConfigValue));
const stopAtHome = {
x: !!this.props.botMcuParams.movement_stop_at_home_x,
y: !!this.props.botMcuParams.movement_stop_at_home_y
@ -155,6 +151,7 @@ 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}
@ -164,6 +161,7 @@ export class RawFarmDesigner extends React.Component<Props, Partial<State>> {
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
getConfigValue={this.props.getConfigValue}
shouldDisplay={this.props.shouldDisplay}
imageAgeInfo={imageAgeInfo} />
<DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
@ -181,6 +179,7 @@ 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}
@ -192,15 +191,16 @@ 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={botSize}
botSize={this.props.botSize}
stopAtHome={stopAtHome}
hoveredPlant={this.props.hoveredPlant}
zoomLvl={zoom_level}
botOriginQuadrant={this.getBotOriginQuadrant()}
gridSize={getGridSize(this.props.getConfigValue, botSize)}
gridSize={getGridSize(this.props.getConfigValue, this.props.botSize)}
gridOffset={gridOffset}
peripherals={this.props.peripherals}
eStopStatus={this.props.eStopStatus}

View File

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

View File

@ -4,10 +4,9 @@ jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
}));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
overwrite: jest.fn(),
}));
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
jest.mock("../../point_groups/actions", () => ({ overwriteGroup: jest.fn() }));
import { fakePointGroup } from "../../../__test_support__/fake_state/resources";
const mockGroup = fakePointGroup();
@ -16,12 +15,13 @@ jest.mock("../../point_groups/group_detail", () => ({
}));
import {
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPlant,
movePlant, closePlantInfo, setDragIcon, clickMapPlant, selectPoint,
setHoveredPlant,
mapPointClickAction,
} from "../actions";
import { MovePlantProps } from "../../interfaces";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { edit, overwrite } from "../../../api/crud";
import { edit } from "../../../api/crud";
import { Actions } from "../../../constants";
import { DEFAULT_ICON, svgToUrl } from "../../../open_farm/icons";
import { history } from "../../../history";
@ -30,6 +30,8 @@ import { GetState } from "../../../redux/interfaces";
import {
buildResourceIndex,
} from "../../../__test_support__/resource_index_builder";
import { overwriteGroup } from "../../point_groups/actions";
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
describe("movePlant", () => {
it.each<[string, Record<"x" | "y", number>, Record<"x" | "y", number>]>([
@ -74,7 +76,7 @@ describe("closePlantInfo()", () => {
closePlantInfo(dispatch)();
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
expect(dispatch).toHaveBeenCalledWith({
payload: undefined, type: Actions.SELECT_PLANT
payload: undefined, type: Actions.SELECT_POINT
});
});
@ -84,7 +86,7 @@ describe("closePlantInfo()", () => {
closePlantInfo(dispatch)();
expect(history.push).toHaveBeenCalledWith("/app/designer/plants");
expect(dispatch).toHaveBeenCalledWith({
payload: undefined, type: Actions.SELECT_PLANT
payload: undefined, type: Actions.SELECT_POINT
});
});
});
@ -115,7 +117,7 @@ describe("clickMapPlant", () => {
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant("fakeUuid", "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith(selectPlant(["fakeUuid"]));
expect(dispatch).toHaveBeenCalledWith(selectPoint(["fakeUuid"]));
expect(dispatch).toHaveBeenCalledWith(setHoveredPlant("fakeUuid", "fakeIcon"));
expect(dispatch).toHaveBeenCalledTimes(2);
});
@ -127,12 +129,25 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.body.id = 23;
state.resources = buildResourceIndex([plant]);
const dispatch = jest.fn();
const dispatch = mockDispatch();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
name: "Fake", point_ids: [1, 23]
}));
expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
expect.objectContaining({
name: "Fake", point_ids: [1, 23]
}));
expect(dispatch).toHaveBeenCalledTimes(1);
});
it("doesn't add a point to current group", () => {
mockPath = "/app/designer/groups/1";
mockGroup.body.point_ids = [1];
const state = fakeState();
state.resources = buildResourceIndex([]);
const dispatch = mockDispatch();
const getState: GetState = jest.fn(() => state);
clickMapPlant("missing plant uuid", "fakeIcon")(dispatch, getState);
expect(overwriteGroup).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -143,12 +158,13 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.body.id = 2;
state.resources = buildResourceIndex([plant]);
const dispatch = jest.fn();
const dispatch = mockDispatch();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(overwrite).toHaveBeenCalledWith(mockGroup, expect.objectContaining({
name: "Fake", point_ids: [1]
}));
expect(overwriteGroup).toHaveBeenCalledWith(mockGroup,
expect.objectContaining({
name: "Fake", point_ids: [1]
}));
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -162,7 +178,7 @@ describe("clickMapPlant", () => {
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT, payload: [plant.uuid]
type: Actions.SELECT_POINT, payload: [plant.uuid]
});
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -173,13 +189,39 @@ describe("clickMapPlant", () => {
const plant = fakePlant();
plant.uuid = "fakePlantUuid";
state.resources = buildResourceIndex([plant]);
state.resources.consumers.farm_designer.selectedPlants = [plant.uuid];
state.resources.consumers.farm_designer.selectedPoints = [plant.uuid];
const dispatch = jest.fn();
const getState: GetState = jest.fn(() => state);
clickMapPlant(plant.uuid, "fakeIcon")(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT, payload: []
type: Actions.SELECT_POINT, 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,15 +6,17 @@ 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", () => ({
@ -81,6 +83,7 @@ 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,
@ -92,6 +95,7 @@ const fakeProps = (): GardenMapProps => ({
designer: fakeDesignerState(),
plants: [],
genericPoints: [],
weeds: [],
allPoints: [],
toolSlots: [],
botLocationData: {
@ -286,7 +290,22 @@ describe("<GardenMap/>", () => {
wrapper.find(".drop-area-svg").simulate("mouseDown", {
pageX: 1, pageY: 2
});
expect(startNewPoint).toHaveBeenCalled();
expect(startNewPoint).toHaveBeenCalledWith(expect.objectContaining({
type: "point"
}));
expect(getGardenCoordinates).toHaveBeenCalledWith(
expect.objectContaining({ pageX: 1, pageY: 2 }));
});
it("starts drawing weed", () => {
const wrapper = shallow(<GardenMap {...fakeProps()} />);
mockMode = Mode.createWeed;
wrapper.find(".drop-area-svg").simulate("mouseDown", {
pageX: 1, pageY: 2
});
expect(startNewPoint).toHaveBeenCalledWith(expect.objectContaining({
type: "weed"
}));
expect(getGardenCoordinates).toHaveBeenCalledWith(
expect.objectContaining({ pageX: 1, pageY: 2 }));
});
@ -297,7 +316,20 @@ describe("<GardenMap/>", () => {
wrapper.find(".drop-area-svg").simulate("mouseMove", {
pageX: 10, pageY: 20
});
expect(resizePoint).toHaveBeenCalled();
expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({
type: "point"
}));
});
it("sets drawn weed radius", () => {
const wrapper = shallow(<GardenMap {...fakeProps()} />);
mockMode = Mode.createWeed;
wrapper.find(".drop-area-svg").simulate("mouseMove", {
pageX: 10, pageY: 20
});
expect(resizePoint).toHaveBeenCalledWith(expect.objectContaining({
type: "weed"
}));
});
it("lays eggs", () => {
@ -350,7 +382,7 @@ describe("<GardenMap/>", () => {
it("closes panel", () => {
mockMode = Mode.boxSelect;
const p = fakeProps();
p.designer.selectedPlants = undefined;
p.designer.selectedPoints = undefined;
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).toHaveBeenCalled();
@ -366,7 +398,7 @@ describe("<GardenMap/>", () => {
it("doesn't close panel: box select", () => {
mockMode = Mode.boxSelect;
const p = fakeProps();
p.designer.selectedPlants = [fakePlant().uuid];
p.designer.selectedPoints = [fakePlant().uuid];
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).not.toHaveBeenCalled();
@ -375,7 +407,7 @@ describe("<GardenMap/>", () => {
it("doesn't close panel: move mode", () => {
mockMode = Mode.moveTo;
const p = fakeProps();
p.designer.selectedPlants = [fakePlant().uuid];
p.designer.selectedPoints = [fakePlant().uuid];
const wrapper = mount<GardenMap>(<GardenMap {...p} />);
wrapper.instance().closePanel()();
expect(closePlantInfo).not.toHaveBeenCalled();
@ -404,6 +436,56 @@ 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,11 +4,6 @@ jest.mock("../../../history", () => ({
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
}));
let mockGardenOpen = true;
jest.mock("../../saved_gardens/saved_gardens", () => ({
savedGardenOpen: () => mockGardenOpen,
}));
import {
round,
translateScreenToGarden,
@ -21,6 +16,9 @@ import {
mapPanelClassName,
getMode,
cursorAtPlant,
allowInteraction,
allowGroupAreaInteraction,
savedGardenOpen,
} from "../util";
import { McuParams } from "farmbot";
import {
@ -32,13 +30,37 @@ import {
} from "../../../__test_support__/map_transform_props";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
describe("Utils", () => {
describe("round()", () => {
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({
@ -344,17 +366,26 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.points);
mockPath = "/app/designer/points/add";
expect(getMode()).toEqual(Mode.createPoint);
mockPath = "/app/designer/gardens";
mockGardenOpen = true;
mockPath = "/app/designer/weeds";
expect(getMode()).toEqual(Mode.weeds);
mockPath = "/app/designer/weeds/add";
expect(getMode()).toEqual(Mode.createWeed);
mockPath = "/app/designer/gardens/1";
expect(getMode()).toEqual(Mode.templateView);
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", {
@ -396,27 +427,37 @@ describe("getGardenCoordinates()", () => {
});
});
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");
describe("allowInteraction()", () => {
it("allows interaction", () => {
mockPath = "/app/designer/plants";
expect(allowInteraction()).toBeTruthy();
});
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");
it("disallows interaction", () => {
mockPath = "/app/designer/plants/crop_search/mint/add";
expect(mapPanelClassName()).toEqual("panel-open");
expect(allowInteraction()).toBeFalsy();
mockPath = "/app/designer/move_to";
expect(allowInteraction()).toBeFalsy();
mockPath = "/app/designer/points/add";
expect(allowInteraction()).toBeFalsy();
mockPath = "/app/designer/weeds/add";
expect(allowInteraction()).toBeFalsy();
});
});
describe("allowGroupAreaInteraction()", () => {
it("allows interaction", () => {
mockPath = "/app/designer/plants";
expect(allowGroupAreaInteraction()).toBeTruthy();
});
it("disallows interaction", () => {
mockPath = "/app/designer/plants/select";
expect(allowGroupAreaInteraction()).toBeFalsy();
mockPath = "/app/designer/move_to";
expect(allowGroupAreaInteraction()).toBeFalsy();
mockPath = "/app/designer/groups/1";
expect(allowGroupAreaInteraction()).toBeFalsy();
});
});

View File

@ -1,6 +1,6 @@
import { MovePlantProps, DraggableEvent } from "../interfaces";
import { defensiveClone } from "../../util";
import { edit, overwrite } from "../../api/crud";
import { edit } from "../../api/crud";
import { history } from "../../history";
import { Actions } from "../../constants";
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
@ -12,6 +12,7 @@ import { TaggedPoint } from "farmbot";
import { getMode } from "../map/util";
import { ResourceIndex, UUID } from "../../resources/interfaces";
import { selectAllPointGroups } from "../../resources/selectors";
import { overwriteGroup } from "../point_groups/actions";
export function movePlant(payload: MovePlantProps) {
const tr = payload.plant;
@ -23,8 +24,8 @@ export function movePlant(payload: MovePlantProps) {
return edit(tr, update);
}
export const selectPlant = (payload: string[] | undefined) => {
return { type: Actions.SELECT_PLANT, payload };
export const selectPoint = (payload: string[] | undefined) => {
return { type: Actions.SELECT_POINT, payload };
};
export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
@ -33,35 +34,36 @@ export const setHoveredPlant = (plantUUID: string | undefined, icon = "") => ({
});
const addOrRemoveFromGroup =
(clickedPlantUuid: UUID, resources: ResourceIndex) => {
const group = findGroupFromUrl(selectAllPointGroups(resources));
const point =
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
if (group && point?.body.id) {
type Body = (typeof group)["body"];
const nextGroup: Body = ({
...group.body,
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
});
if (!group.body.point_ids.includes(point.body.id)) {
nextGroup.point_ids.push(point.body.id);
(clickedPlantUuid: UUID, resources: ResourceIndex) =>
(dispatch: Function) => {
const group = findGroupFromUrl(selectAllPointGroups(resources));
const point =
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
if (group && point?.body.id) {
type Body = (typeof group)["body"];
const nextGroup: Body = ({
...group.body,
point_ids: [...group.body.point_ids.filter(p => p != point.body.id)]
});
if (!group.body.point_ids.includes(point.body.id)) {
nextGroup.point_ids.push(point.body.id);
}
nextGroup.point_ids = uniq(nextGroup.point_ids);
dispatch(overwriteGroup(group, nextGroup));
}
nextGroup.point_ids = uniq(nextGroup.point_ids);
return overwrite(group, nextGroup);
}
};
};
const addOrRemoveFromSelection =
(clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => {
(clickedPointUuid: UUID, selectedPoints: UUID[] | undefined) => {
const nextSelected =
(selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid);
if (!(selectedPlants?.includes(clickedPlantUuid))) {
nextSelected.push(clickedPlantUuid);
(selectedPoints || []).filter(uuid => uuid !== clickedPointUuid);
if (!(selectedPoints?.includes(clickedPointUuid))) {
nextSelected.push(clickedPointUuid);
}
return selectPlant(nextSelected);
return selectPoint(nextSelected);
};
export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
export const clickMapPlant = (clickedPlantUuid: UUID, icon: string) => {
return (dispatch: Function, getState: GetState) => {
switch (getMode()) {
case Mode.editGroup:
@ -69,11 +71,11 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
dispatch(addOrRemoveFromGroup(clickedPlantUuid, resources.index));
break;
case Mode.boxSelect:
const { selectedPlants } = getState().resources.consumers.farm_designer;
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPlants));
const { selectedPoints } = getState().resources.consumers.farm_designer;
dispatch(addOrRemoveFromSelection(clickedPlantUuid, selectedPoints));
break;
default:
dispatch(selectPlant([clickedPlantUuid]));
dispatch(selectPoint([clickedPlantUuid]));
dispatch(setHoveredPlant(clickedPlantUuid, icon));
break;
}
@ -81,7 +83,7 @@ export const clickMapPlant = (clickedPlantUuid: string, icon: string) => {
};
export const unselectPlant = (dispatch: Function) => () => {
dispatch(selectPlant(undefined));
dispatch(selectPoint(undefined));
dispatch(setHoveredPlant(undefined));
dispatch({ type: Actions.HOVER_PLANT_LIST_ITEM, payload: undefined });
};
@ -104,3 +106,14 @@ 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,9 +8,8 @@ jest.mock("../../../point_groups/criteria", () => ({
editGtLtCriteria: jest.fn(),
}));
jest.mock("../../../../api/crud", () => ({
overwrite: jest.fn(),
save: jest.fn(),
jest.mock("../../../point_groups/actions", () => ({
overwriteGroup: jest.fn(),
}));
import {
@ -25,8 +24,8 @@ import {
import { Actions } from "../../../../constants";
import { history } from "../../../../history";
import { editGtLtCriteria } from "../../../point_groups/criteria";
import { overwrite, save } from "../../../../api/crud";
import { cloneDeep } from "lodash";
import { overwriteGroup } from "../../../point_groups/actions";
describe("getSelected", () => {
it("returns some", () => {
@ -55,6 +54,9 @@ 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(),
@ -68,7 +70,7 @@ describe("resizeBox", () => {
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT,
type: Actions.SELECT_POINT,
payload: undefined
});
});
@ -113,7 +115,7 @@ describe("resizeBox", () => {
selectionBox: { x0: 0, y0: 0, x1: 100, y1: 200 }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT,
type: Actions.SELECT_POINT,
payload: [plant.uuid]
});
expect(history.push).toHaveBeenCalledWith("/app/designer/plants/select");
@ -135,7 +137,7 @@ describe("startNewSelectionBox", () => {
selectionBox: { x0: 100, y0: 200, x1: undefined, y1: undefined }
});
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT,
type: Actions.SELECT_POINT,
payload: undefined
});
});
@ -157,7 +159,7 @@ describe("startNewSelectionBox", () => {
startNewSelectionBox(p);
expect(p.setMapState).not.toHaveBeenCalled();
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_PLANT,
type: Actions.SELECT_POINT,
payload: undefined
});
});
@ -186,8 +188,7 @@ describe("maybeUpdateGroup()", () => {
expectedBody && (expectedBody.point_ids = [
plant1.body.id || 0, plant2.body.id || 0,
]);
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
expect(save).not.toHaveBeenCalled();
expect(overwriteGroup).toHaveBeenCalledWith(p.group, expectedBody);
});
it("updates criteria", () => {
@ -211,7 +212,6 @@ describe("maybeUpdateGroup()", () => {
maybeUpdateGroup(p);
expect(p.dispatch).not.toHaveBeenCalled();
expect(editGtLtCriteria).not.toHaveBeenCalled();
expect(overwrite).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
expect(overwriteGroup).not.toHaveBeenCalled();
});
});

View File

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

View File

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

View File

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

@ -0,0 +1,36 @@
import * as React from "react";
import { DrawnWeed, DrawnWeedProps } from "../drawn_weed";
import {
fakeMapTransformProps,
} from "../../../../__test_support__/map_transform_props";
import { svgMount } from "../../../../__test_support__/svg_mount";
describe("<DrawnWeed />", () => {
const fakeProps = (): DrawnWeedProps => ({
mapTransformProps: fakeMapTransformProps(),
data: {
cx: 10,
cy: 20,
r: 30,
}
});
it("renders weed", () => {
const wrapper = svgMount(<DrawnWeed {...fakeProps()} />);
const stop = wrapper.find("stop").first().props();
expect(stop.stopColor).toEqual("red");
expect(stop.stopOpacity).toEqual(0.25);
expect(wrapper.find("circle").first().props()).toEqual({
id: "weed-radius", cx: 10, cy: 20, r: 30, fill: "url(#DrawnWeedGradient)",
});
});
it("renders point with chosen color", () => {
const p = fakeProps();
p.data = { cx: 0, cy: 0, r: 1, color: "orange" };
const wrapper = svgMount(<DrawnWeed {...p} />);
const stop = wrapper.find("stop").first().props();
expect(stop.stopColor).toEqual("orange");
expect(stop.stopOpacity).toEqual(0.5);
});
});

View File

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

View File

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

View File

@ -0,0 +1,34 @@
import * as React from "react";
import { MapTransformProps } from "../interfaces";
import { transformXY } from "../util";
import { DrawnWeedPayl } from "../../interfaces";
export interface DrawnWeedProps {
mapTransformProps: MapTransformProps;
data: DrawnWeedPayl | undefined;
}
export function DrawnWeed(props: DrawnWeedProps) {
const ID = "current-weed";
const { data, mapTransformProps } = props;
if (!data) { return <g id={ID} />; }
const { cx, cy, r } = data;
const color = data.color || "red";
const { qx, qy } = transformXY(cx, cy, mapTransformProps);
const stopOpacity = ["gray", "pink", "orange"].includes(color) ? 0.5 : 0.25;
return <g id={ID}>
<defs>
<radialGradient id={"DrawnWeedGradient"}>
<stop offset="90%" stopColor={color} stopOpacity={stopOpacity} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</radialGradient>
</defs>
<circle
id={"weed-radius"}
cx={qx}
cy={qy}
r={r}
fill={"url(#DrawnWeedGradient)"} />
</g>;
}

View File

@ -6,7 +6,7 @@ import {
} from "./interfaces";
import { GardenMapProps, GardenMapState } from "../interfaces";
import {
getMapSize, getGardenCoordinates, getMode, cursorAtPlant,
getMapSize, getGardenCoordinates, getMode, cursorAtPlant, allowInteraction,
} from "./util";
import {
Grid, MapBackground,
@ -17,6 +17,7 @@ import {
PlantLayer,
SpreadLayer,
PointLayer,
WeedLayer,
ToolSlotLayer,
FarmBotLayer,
ImageLayer,
@ -34,9 +35,12 @@ 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 } from "farmbot";
import { TaggedPoint, TaggedPointGroup, PointType } 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>> {
@ -81,8 +85,12 @@ 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 = () => {
endDrag = throttle(() => {
maybeSavePlantLocation({
plant: this.getPlant(),
isDragging: this.state.isDragging,
@ -94,7 +102,7 @@ export class GardenMap extends
dispatch: this.props.dispatch,
shouldDisplay: this.props.shouldDisplay,
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
boxSelected: this.props.designer.selectedPlants,
boxSelected: this.props.designer.selectedPoints,
});
this.setState({
isDragging: false, qPageX: 0, qPageY: 0,
@ -102,7 +110,7 @@ export class GardenMap extends
activeDragSpread: undefined,
selectionBox: undefined
});
}
}, 400);
getGardenCoordinates =
(e: React.DragEvent<HTMLElement> | React.MouseEvent<SVGElement>):
@ -152,6 +160,15 @@ 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:
@ -163,8 +180,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;
@ -196,17 +213,27 @@ export class GardenMap extends
}
}
interactions = (pointerType: PointType): boolean => {
if (allowInteraction()) {
switch (getMode()) {
case Mode.editGroup:
case Mode.boxSelect:
return (this.props.designer.selectionPointType || ["Plant"])
.includes(pointerType);
}
}
return allowInteraction();
};
/** Return the selected plant, mode-allowing. */
getPlant = (): TaggedPlant | undefined => {
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;
}
return allowInteraction()
? this.props.selectedPlant
: undefined;
}
get currentPoint(): UUID | undefined {
return this.props.designer.selectedPoints?.[0];
}
handleDragOver = (e: React.DragEvent<HTMLElement>) => {
@ -273,15 +300,28 @@ export class GardenMap extends
case Mode.createPoint:
resizePoint({
gardenCoords: this.getGardenCoordinates(e),
currentPoint: this.props.designer.currentPoint,
drawnPoint: this.props.designer.drawnPoint,
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,
@ -293,6 +333,9 @@ 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,
@ -308,7 +351,7 @@ export class GardenMap extends
case Mode.moveTo:
return () => { };
case Mode.boxSelect:
return this.props.designer.selectedPlants
return this.props.designer.selectedPoints
? () => { }
: closePlantInfo(this.props.dispatch);
default:
@ -362,6 +405,7 @@ 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}
@ -385,7 +429,20 @@ 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}
@ -395,10 +452,11 @@ export class GardenMap extends
hoveredPlant={this.props.hoveredPlant}
dragging={!!this.state.isDragging}
editing={this.isEditing}
boxSelected={this.props.designer.selectedPlants}
groupSelected={this.pointsSelectedByGroup.map(point => point.uuid)}
boxSelected={this.props.designer.selectedPoints}
groupSelected={this.groupSelected}
zoomLvl={this.props.zoomLvl}
activeDragXY={this.state.activeDragXY}
interactions={this.interactions("Plant")}
animate={this.animate} />
ToolSlotLayer = () => <ToolSlotLayer
mapTransformProps={this.mapTransformProps}
@ -406,6 +464,7 @@ 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}
@ -443,8 +502,10 @@ export class GardenMap extends
chosenLocation={this.props.designer.chosenLocation}
mapTransformProps={this.mapTransformProps} />
DrawnPoint = () => <DrawnPoint
data={this.props.designer.currentPoint}
key={"currentPoint"}
data={this.props.designer.drawnPoint}
mapTransformProps={this.mapTransformProps} />
DrawnWeed = () => <DrawnWeed
data={this.props.designer.drawnWeed}
mapTransformProps={this.mapTransformProps} />
GroupOrder = () => <GroupOrder
group={this.group}
@ -468,6 +529,7 @@ export class GardenMap extends
<this.SensorReadingsLayer />
<this.SpreadLayer />
<this.PointLayer />
<this.WeedLayer />
<this.PlantLayer />
<this.ToolSlotLayer />
<this.FarmBotLayer />
@ -476,6 +538,7 @@ export class GardenMap extends
<this.SelectionBox />
<this.TargetCoordinate />
<this.DrawnPoint />
<this.DrawnWeed />
<this.GroupOrder />
<this.NNPath />
<this.Bugs />

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
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(() => { return mockPath.split("/"); })
getPathArray: jest.fn(() => mockPath.split("/"))
}));
import * as React from "react";
@ -31,6 +31,7 @@ describe("<PlantLayer/>", () => {
activeDragXY: { x: undefined, y: undefined, z: undefined },
animate: true,
hoveredPlant: undefined,
interactions: true,
});
it("shows plants", () => {
@ -59,14 +60,19 @@ 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({});
expect(wrapper.find("Link").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;
p.plants[0].body.id = 1;
const wrapper = svgMount(<PlantLayer {...p} />);
expect(wrapper.find("Link").props().style)
.toEqual({ pointerEvents: "none" });
@ -111,22 +117,12 @@ 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();
const plant = fakePlant();
plant.body.id = 0;
p.plants = [plant];
p.interactions = false;
p.plants[0].body.id = 0;
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 { maybeNoPointer, getMode } from "../../util";
import { getMode } from "../../util";
import { Link } from "../../../../link";
export function PlantLayer(props: PlantLayerProps) {
@ -44,9 +44,12 @@ 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: maybeNoPointer(p.body.id ? {} : { pointerEvents: "none" }),
style,
key: p.uuid,
};
return (getMode() === Mode.editGroup || getMode() === Mode.boxSelect)

View File

@ -1,4 +1,7 @@
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
jest.mock("../../../../../history", () => ({
history: { push: jest.fn() },
getPathArray: jest.fn(),
}));
import * as React from "react";
import { GardenPoint } from "../garden_point";
@ -55,10 +58,9 @@ 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/weeds/${p.point.body.id}`);
`/app/designer/points/${p.point.body.id}`);
});
});

View File

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

View File

@ -1,8 +1,7 @@
import * as React from "react";
import { TaggedGenericPointer } from "farmbot";
import { GardenPoint } from "./garden_point";
import { MapTransformProps, Mode } from "../../interfaces";
import { getMode } from "../../util";
import { MapTransformProps } from "../../interfaces";
export interface PointLayerProps {
visible: boolean;
@ -10,13 +9,14 @@ 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 =
getMode() === Mode.points ? {} : { pointerEvents: "none" };
return <g id="point-layer" style={style}>
props.interactions ? {} : { 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 };
UNSAFE_componentWillMount = () => {
componentDidMount = () => {
cachedCrop(this.props.plant.body.openfarm_slug)
.then(({ spread }) => this.setState({ spread }));
}

View File

@ -22,7 +22,6 @@ describe("<ToolSlotLayer/>", () => {
pointer_type: "ToolSlot",
tool_id: undefined,
name: "Name",
radius: 50,
x: 1,
y: 2,
z: 3,
@ -38,6 +37,7 @@ describe("<ToolSlotLayer/>", () => {
mapTransformProps: fakeMapTransformProps(),
dispatch: jest.fn(),
hoveredToolSlot: undefined,
interactions: true,
};
}
it("toggles visibility off", () => {
@ -61,9 +61,19 @@ 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,4 +1,7 @@
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
jest.mock("../../../../../history", () => ({
history: { push: jest.fn() },
getPathArray: jest.fn(),
}));
import * as React from "react";
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";

View File

@ -2,7 +2,6 @@ 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;
@ -11,6 +10,7 @@ export interface ToolSlotLayerProps {
mapTransformProps: MapTransformProps;
dispatch: Function;
hoveredToolSlot: UUID | undefined;
interactions: boolean;
}
export function ToolSlotLayer(props: ToolSlotLayerProps) {
@ -18,7 +18,9 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
return <g
id="toolslot-layer"
style={maybeNoPointer({ cursor: "pointer" })}>
style={props.interactions
? { cursor: "pointer" }
: { pointerEvents: "none" }}>
{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,25 +30,27 @@ export const reduceToolName = (raw: string | undefined) => {
};
export const ToolSlotPoint = (props: TSPProps) => {
const { tool, toolSlot } = props.slot;
const {
id, x, y, pullout_direction, gantry_mounted
} = props.slot.toolSlot.body;
} = 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 = props.slot.tool ? props.slot.tool.body.name : t("Empty");
const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot;
const toolName = tool ? tool.body.name : t("Empty");
const hovered = toolSlot.uuid === props.hoveredToolSlot;
const toolProps = {
x: qx,
y: qy,
hovered,
dispatch: props.dispatch,
uuid: props.slot.toolSlot.uuid,
uuid: toolSlot.uuid,
xySwap,
};
return <g id={"toolslot-" + id}
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}>
onClick={mapPointClickAction(props.dispatch, toolSlot.uuid,
`/app/designer/tool-slots/${id}`)}>
{pullout_direction && !gantry_mounted &&
<ToolbaySlot
id={id}

View File

@ -0,0 +1,89 @@
jest.mock("../../../../../history", () => ({
history: { push: jest.fn() },
getPathArray: jest.fn(),
}));
import * as React from "react";
import { GardenWeed } from "../garden_weed";
import { GardenWeedProps } from "../../../interfaces";
import { fakeWeed } from "../../../../../__test_support__/fake_state/resources";
import {
fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props";
import { Actions } from "../../../../../constants";
import { history } from "../../../../../history";
import { svgMount } from "../../../../../__test_support__/svg_mount";
describe("<GardenWeed />", () => {
const fakeProps = (): GardenWeedProps => ({
mapTransformProps: fakeMapTransformProps(),
weed: fakeWeed(),
hovered: false,
dispatch: jest.fn(),
current: false,
selected: false,
animate: false,
spreadVisible: true,
});
it("renders weed", () => {
const p = fakeProps();
p.weed.body.meta.color = undefined;
const wrapper = svgMount(<GardenWeed {...p} />);
expect(wrapper.find("#weed-radius").props().r).toEqual(100);
expect(wrapper.find("#weed-radius").props().opacity).toEqual(0.5);
expect(wrapper.find("stop").first().props().stopColor).toEqual("red");
});
it("renders weed color", () => {
const p = fakeProps();
p.weed.body.meta.color = "orange";
const wrapper = svgMount(<GardenWeed {...p} />);
expect(wrapper.find("#weed-radius").props().r).toEqual(100);
expect(wrapper.find("#weed-radius").props().opacity).toEqual(0.5);
expect(wrapper.find("stop").first().props().stopColor).toEqual("orange");
});
it("animates", () => {
const p = fakeProps();
p.animate = true;
const wrapper = svgMount(<GardenWeed {...p} />);
expect(wrapper.find(".soil-cloud").length).toEqual(1);
expect(wrapper.find("image").hasClass("animate")).toBeTruthy();
});
it("hovers weed", () => {
const p = fakeProps();
const wrapper = svgMount(<GardenWeed {...p} />);
wrapper.find("g").first().simulate("mouseEnter");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.TOGGLE_HOVERED_POINT,
payload: p.weed.uuid
});
});
it("is hovered", () => {
const p = fakeProps();
p.hovered = true;
const wrapper = svgMount(<GardenWeed {...p} />);
expect(wrapper.find("#weed-radius").props().opacity).toEqual(1);
});
it("un-hovers weed", () => {
const p = fakeProps();
const wrapper = svgMount(<GardenWeed {...p} />);
wrapper.find("g").first().simulate("mouseLeave");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.TOGGLE_HOVERED_POINT,
payload: undefined
});
});
it("opens weed info", () => {
const p = fakeProps();
const wrapper = svgMount(<GardenWeed {...p} />);
wrapper.find("g").first().simulate("click");
expect(history.push).toHaveBeenCalledWith(
`/app/designer/weeds/${p.weed.body.id}`);
});
});

View File

@ -0,0 +1,66 @@
let mockPath = "/app/designer/plants";
jest.mock("../../../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
}));
import * as React from "react";
import { WeedLayer, WeedLayerProps } from "../weed_layer";
import { fakeWeed } from "../../../../../__test_support__/fake_state/resources";
import {
fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props";
import { GardenWeed } from "../garden_weed";
import { svgMount } from "../../../../../__test_support__/svg_mount";
describe("<WeedLayer/>", () => {
const fakeProps = (): WeedLayerProps => ({
visible: true,
spreadVisible: true,
weeds: [fakeWeed()],
mapTransformProps: fakeMapTransformProps(),
hoveredPoint: undefined,
dispatch: jest.fn(),
currentPoint: undefined,
boxSelected: undefined,
groupSelected: [],
animate: false,
interactions: true,
});
it("shows weeds", () => {
const p = fakeProps();
p.interactions = false;
const wrapper = svgMount(<WeedLayer {...p} />);
const layer = wrapper.find("#weeds-layer");
expect(layer.find(GardenWeed).html()).toContain("r=\"100\"");
expect(layer.props().style).toEqual({ pointerEvents: "none" });
});
it("toggles visibility off", () => {
const p = fakeProps();
p.visible = false;
const wrapper = svgMount(<WeedLayer {...p} />);
const layer = wrapper.find("#weeds-layer");
expect(layer.find(GardenWeed).length).toEqual(0);
});
it("allows weed mode interaction", () => {
mockPath = "/app/designer/weeds";
const p = fakeProps();
p.interactions = true;
const wrapper = svgMount(<WeedLayer {...p} />);
const layer = wrapper.find("#weeds-layer");
expect(layer.props().style).toEqual({ cursor: "pointer" });
});
it("is selected", () => {
mockPath = "/app/designer/weeds";
const p = fakeProps();
const weed = fakeWeed();
p.weeds = [weed];
p.boxSelected = [weed.uuid];
const wrapper = svgMount(<WeedLayer {...p} />);
const layer = wrapper.find("#weeds-layer");
expect(layer.find(GardenWeed).props().selected).toBeTruthy();
});
});

View File

@ -0,0 +1,69 @@
import * as React from "react";
import { GardenWeedProps } from "../../interfaces";
import { transformXY } from "../../util";
import { Actions } from "../../../../constants";
import { Color } from "../../../../ui";
import { mapPointClickAction } from "../../actions";
export const DEFAULT_WEED_ICON = "/app-resources/img/generic-weed.svg";
export const GardenWeed = (props: GardenWeedProps) => {
const iconHover = (action: "start" | "end") => () => {
const hover = action === "start";
props.dispatch({
type: Actions.TOGGLE_HOVERED_POINT,
payload: hover ? props.weed.uuid : undefined
});
};
const { weed, mapTransformProps, hovered, current, selected, animate } = props;
const { id, x, y, meta, radius } = weed.body;
const { qx, qy } = transformXY(x, y, mapTransformProps);
const color = meta.color || "red";
const stopOpacity = ["gray", "pink", "orange"].includes(color) ? 0.5 : 0.25;
const className = [
"weed-image", `is-chosen-${current || selected}`, animate ? "animate" : "",
].join(" ");
const iconRadius = hovered ? radius * 0.88 : radius * 0.8;
return <g id={`weed-${id}`} className={`map-weed ${color}`}
onMouseEnter={iconHover("start")}
onMouseLeave={iconHover("end")}
onClick={mapPointClickAction(props.dispatch, weed.uuid,
`/app/designer/weeds/${id}`)}>
<defs>
<radialGradient id={`Weed${id}Gradient`}>
<stop offset="90%" stopColor={color} stopOpacity={stopOpacity} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</radialGradient>
</defs>
{animate &&
<circle
className="soil-cloud"
cx={qx}
cy={qy}
r={radius}
fill={Color.soilCloud}
fillOpacity={0} />}
{props.spreadVisible &&
<circle
id={"weed-radius"}
cx={qx}
cy={qy}
r={radius}
fill={`url(#Weed${id}Gradient)`}
opacity={hovered ? 1 : 0.5} />}
<g id="weed-icon">
<image
className={className}
xlinkHref={DEFAULT_WEED_ICON}
height={iconRadius * 2}
width={iconRadius * 2}
x={qx - iconRadius}
y={qy - iconRadius} />
</g>
</g>;
};

View File

@ -0,0 +1,43 @@
import * as React from "react";
import { TaggedWeedPointer } from "farmbot";
import { GardenWeed } from "./garden_weed";
import { MapTransformProps } from "../../interfaces";
import { UUID } from "../../../../resources/interfaces";
export interface WeedLayerProps {
visible: boolean;
spreadVisible: boolean;
weeds: TaggedWeedPointer[];
mapTransformProps: MapTransformProps;
hoveredPoint: UUID | undefined;
currentPoint: UUID | undefined;
boxSelected: UUID[] | undefined;
groupSelected: UUID[];
dispatch: Function;
animate: boolean;
interactions: boolean;
}
export function WeedLayer(props: WeedLayerProps) {
const { visible, weeds, mapTransformProps } = props;
return <g id={"weeds-layer"} style={props.interactions
? { cursor: "pointer" } : { pointerEvents: "none" }}>
{visible &&
weeds.map(p => {
const current = p.uuid === props.currentPoint;
const hovered = p.uuid === props.hoveredPoint;
const selectedByBox = !!props.boxSelected?.includes(p.uuid);
const selectedByGroup = props.groupSelected.includes(p.uuid);
return <GardenWeed
weed={p}
key={p.uuid}
hovered={hovered}
current={current}
selected={selectedByBox || selectedByGroup}
animate={props.animate}
spreadVisible={props.spreadVisible}
dispatch={props.dispatch}
mapTransformProps={mapTransformProps} />;
})}
</g>;
}

View File

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

View File

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

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