diff --git a/Gemfile b/Gemfile index 969c90c65..22a2c8929 100755 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem "scenic" gem "secure_headers" gem "tzinfo" # For validation of user selected timezone names gem "valid_url" -# gem "farady", "~> 1.0.0" +gem "kaminari" group :development, :test do gem "climate_control" diff --git a/Gemfile.lock b/Gemfile.lock index 7f22fde26..6b1c2886e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -72,7 +72,7 @@ GEM amq-protocol (2.3.0) bcrypt (3.1.13) builder (3.2.4) - bunny (2.14.3) + bunny (2.14.4) amq-protocol (~> 2.3, >= 2.3.0) case_transform (0.2) activesupport @@ -82,9 +82,9 @@ GEM simplecov url coderay (1.1.2) - concurrent-ruby (1.1.5) + concurrent-ruby (1.1.6) crass (1.0.6) - database_cleaner (1.7.0) + database_cleaner (1.8.3) declarative (0.0.10) declarative-option (0.1.0) delayed_job (4.1.8) @@ -100,7 +100,7 @@ GEM warden (~> 1.2.3) diff-lcs (1.3) digest-crc (0.4.1) - discard (1.1.0) + discard (1.2.0) activerecord (>= 4.2, < 7) docile (1.3.2) erubi (1.9.0) @@ -109,7 +109,7 @@ GEM factory_bot_rails (5.1.1) factory_bot (~> 5.1.0) railties (>= 4.2.0) - faker (2.10.1) + faker (2.10.2) i18n (>= 1.6, < 2) faraday (0.15.4) multipart-post (>= 1.2, < 3) @@ -119,7 +119,7 @@ GEM railties (>= 3.2, < 6.1) globalid (0.4.2) activesupport (>= 4.2.0) - google-api-client (0.36.4) + google-api-client (0.37.1) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -127,10 +127,12 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.12) - google-cloud-core (1.4.1) + 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-errors (1.0.0) google-cloud-storage (1.25.1) addressable (~> 2.5) digest-crc (~> 0.4) @@ -153,6 +155,18 @@ GEM json (2.3.0) jsonapi-renderer (0.2.2) jwt (2.2.1) + kaminari (1.2.0) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.0) + kaminari-activerecord (= 1.2.0) + kaminari-core (= 1.2.0) + kaminari-actionview (1.2.0) + actionview + kaminari-core (= 1.2.0) + kaminari-activerecord (1.2.0) + activerecord + kaminari-core (= 1.2.0) + kaminari-core (1.2.0) loofah (2.4.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -162,7 +176,7 @@ GEM mimemagic (~> 0.3.2) memoist (0.16.2) method_source (0.9.2) - mimemagic (0.3.3) + mimemagic (0.3.4) mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.0) @@ -171,7 +185,7 @@ GEM mutations (0.9.0) activesupport nio4r (2.5.2) - nokogiri (1.10.7) + nokogiri (1.10.8) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) os (1.0.1) @@ -190,7 +204,7 @@ GEM faraday_middleware (~> 0.13.0) hashie (~> 3.6) multi_json (~> 1.13.1) - rack (2.1.1) + rack (2.2.2) rack-attack (6.2.2) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -240,7 +254,7 @@ GEM actionpack (>= 5.0) railties (>= 5.0) retriable (3.1.2) - rollbar (2.23.2) + rollbar (2.24.0) rspec (3.9.0) rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) @@ -264,7 +278,7 @@ GEM rspec-support (3.9.2) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - scenic (1.5.1) + scenic (1.5.2) activerecord (>= 4.0.0) railties (>= 4.0.0) secure_headers (6.3.0) @@ -273,11 +287,10 @@ GEM faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simplecov (0.17.1) + simplecov (0.18.5) docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) + simplecov-html (~> 0.11) + simplecov-html (0.12.1) sprockets (4.0.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -320,6 +333,7 @@ DEPENDENCIES google-cloud-storage (~> 1.11) hashdiff jwt + kaminari mutations passenger pg diff --git a/app/controllers/api/abstract_controller.rb b/app/controllers/api/abstract_controller.rb index a3fdaa4b5..4b27ef6b3 100644 --- a/app/controllers/api/abstract_controller.rb +++ b/app/controllers/api/abstract_controller.rb @@ -80,6 +80,16 @@ module Api { root: false, user: current_user } end + def maybe_paginate(collection) + page = params[:page] + per = params[:per] + + if page && per + render json: collection.page(page).per(per) + else + render json: collection + end + end private def clean_expired_farm_events diff --git a/app/controllers/api/alerts_controller.rb b/app/controllers/api/alerts_controller.rb index e1c9c2cb4..41298b2a2 100644 --- a/app/controllers/api/alerts_controller.rb +++ b/app/controllers/api/alerts_controller.rb @@ -1,7 +1,7 @@ module Api class AlertsController < Api::AbstractController def index - render json: current_device.alerts + maybe_paginate current_device.alerts end def destroy diff --git a/app/controllers/api/farm_events_controller.rb b/app/controllers/api/farm_events_controller.rb index a2913cdee..f974cd613 100644 --- a/app/controllers/api/farm_events_controller.rb +++ b/app/controllers/api/farm_events_controller.rb @@ -3,7 +3,7 @@ module Api before_action :clean_expired_farm_events, only: [:index] def index - render json: current_device.farm_events + maybe_paginate current_device.farm_events end def show diff --git a/app/controllers/api/farmware_envs_controller.rb b/app/controllers/api/farmware_envs_controller.rb index 630edf7f1..9340eb72e 100644 --- a/app/controllers/api/farmware_envs_controller.rb +++ b/app/controllers/api/farmware_envs_controller.rb @@ -10,7 +10,7 @@ module Api end def index - render json: farmware_envs + maybe_paginate farmware_envs end def show diff --git a/app/controllers/api/farmware_installations_controller.rb b/app/controllers/api/farmware_installations_controller.rb index 21a67dd3a..6b59eeaf4 100644 --- a/app/controllers/api/farmware_installations_controller.rb +++ b/app/controllers/api/farmware_installations_controller.rb @@ -1,7 +1,7 @@ module Api class FarmwareInstallationsController < Api::AbstractController def index - render json: farmware_installations + maybe_paginate farmware_installations end def show diff --git a/app/controllers/api/peripherals_controller.rb b/app/controllers/api/peripherals_controller.rb index f9fb20030..d9ed7e98e 100644 --- a/app/controllers/api/peripherals_controller.rb +++ b/app/controllers/api/peripherals_controller.rb @@ -1,7 +1,7 @@ module Api class PeripheralsController < Api::AbstractController def index - render json: current_device.peripherals + maybe_paginate current_device.peripherals end def show diff --git a/app/controllers/api/pin_bindings_controller.rb b/app/controllers/api/pin_bindings_controller.rb index a50c8821d..6809b1627 100644 --- a/app/controllers/api/pin_bindings_controller.rb +++ b/app/controllers/api/pin_bindings_controller.rb @@ -1,7 +1,7 @@ module Api class PinBindingsController < Api::AbstractController def index - render json: pin_bindings + maybe_paginate pin_bindings end def show diff --git a/app/controllers/api/plant_templates_controller.rb b/app/controllers/api/plant_templates_controller.rb index 9e398e56b..0618edcc9 100644 --- a/app/controllers/api/plant_templates_controller.rb +++ b/app/controllers/api/plant_templates_controller.rb @@ -1,7 +1,7 @@ module Api class PlantTemplatesController < Api::AbstractController def index - render json: current_device.plant_templates + maybe_paginate current_device.plant_templates end def create diff --git a/app/controllers/api/point_groups_controller.rb b/app/controllers/api/point_groups_controller.rb index a6bbef56d..bd45be617 100644 --- a/app/controllers/api/point_groups_controller.rb +++ b/app/controllers/api/point_groups_controller.rb @@ -3,7 +3,7 @@ module Api before_action :clean_expired_farm_events, only: [:destroy] def index - render json: your_point_groups + maybe_paginate your_point_groups end def show diff --git a/app/controllers/api/points_controller.rb b/app/controllers/api/points_controller.rb index 99c098c7d..18d27db68 100644 --- a/app/controllers/api/points_controller.rb +++ b/app/controllers/api/points_controller.rb @@ -20,7 +20,7 @@ module Api .where("discarded_at < ?", Time.now - HARD_DELETE_AFTER) .destroy_all - render json: points(params.fetch(:filter) { "kept" }) + maybe_paginate points(params.fetch(:filter) { "kept" }) end def show diff --git a/app/controllers/api/regimens_controller.rb b/app/controllers/api/regimens_controller.rb index 9c9eea1fd..dd168cf11 100644 --- a/app/controllers/api/regimens_controller.rb +++ b/app/controllers/api/regimens_controller.rb @@ -3,7 +3,7 @@ module Api before_action :clean_expired_farm_events, only: [:destroy] def index - render json: your_regimens + maybe_paginate your_regimens end def show diff --git a/app/controllers/api/saved_gardens_controller.rb b/app/controllers/api/saved_gardens_controller.rb index f8bcbc8e1..bd689d8b8 100644 --- a/app/controllers/api/saved_gardens_controller.rb +++ b/app/controllers/api/saved_gardens_controller.rb @@ -1,7 +1,7 @@ module Api class SavedGardensController < Api::AbstractController def index - render json: current_device.saved_gardens + maybe_paginate current_device.saved_gardens end def create diff --git a/app/controllers/api/sensor_readings_controller.rb b/app/controllers/api/sensor_readings_controller.rb index 1954a04c1..65feb4c28 100644 --- a/app/controllers/api/sensor_readings_controller.rb +++ b/app/controllers/api/sensor_readings_controller.rb @@ -1,11 +1,14 @@ module Api class SensorReadingsController < Api::AbstractController + LIMIT = 5000 + before_action :clean_old + def create - mutate SensorReadings::Create.run(raw_json, device: current_device) + mutate SensorReadings::Create.run(raw_json, device: current_device) end def index - render json: readings + maybe_paginate(readings) end def show @@ -17,10 +20,23 @@ module Api render json: "" end - private + private + + def clean_old + if current_device.sensor_readings.count > LIMIT + current_device + .sensor_readings + .where + .not(id: readings.pluck(:id)) + .delete_all + end + end def readings - SensorReading.where(device: current_device) + @readings ||= SensorReading + .where(device: current_device) + .order(created_at: :desc) + .limit(LIMIT) end def reading diff --git a/app/controllers/api/sensors_controller.rb b/app/controllers/api/sensors_controller.rb index 2e259dc74..aff19605e 100644 --- a/app/controllers/api/sensors_controller.rb +++ b/app/controllers/api/sensors_controller.rb @@ -1,7 +1,7 @@ module Api class SensorsController < Api::AbstractController def index - render json: current_device.sensors + maybe_paginate current_device.sensors end def show diff --git a/app/controllers/api/tools_controller.rb b/app/controllers/api/tools_controller.rb index 706d6398d..28c70c4a1 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -2,7 +2,7 @@ module Api class ToolsController < Api::AbstractController def index - render json: tools + maybe_paginate tools end def show diff --git a/app/controllers/api/webcam_feeds_controller.rb b/app/controllers/api/webcam_feeds_controller.rb index a22d99ebe..82ae43f80 100644 --- a/app/controllers/api/webcam_feeds_controller.rb +++ b/app/controllers/api/webcam_feeds_controller.rb @@ -7,7 +7,7 @@ module Api end def index - render json: webcams + maybe_paginate webcams end def show diff --git a/app/models/device.rb b/app/models/device.rb index f2ecf6521..750244f22 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -171,9 +171,10 @@ class Device < ApplicationRecord end TOO_MANY_CONNECTIONS = - "Your device is " + - "reconnecting to the server too often. Please " + - "see https://developer.farm.bot/docs/connectivity-issues" + "Your device is reconnecting to the server too often. " + + "This may be a sign of local network issues. " + + "Please review the documentation provided at " + + "https://software.farm.bot/docs/connecting-farmbot-to-the-internet" def self.connection_warning(username) device_id = username.split("_").last.to_i || 0 device = self.find_by(id: device_id) diff --git a/app/models/in_use_point.rb b/app/models/in_use_point.rb index 820684516..4c29f3894 100644 --- a/app/models/in_use_point.rb +++ b/app/models/in_use_point.rb @@ -6,7 +6,7 @@ class InUsePoint < ApplicationRecord DEFAULT_NAME = "point" FANCY_NAMES = { GenericPointer.name => DEFAULT_NAME, - ToolSlot.name => "tool slot", + ToolSlot.name => "slot", Plant.name => "plant", } diff --git a/app/models/point_group.rb b/app/models/point_group.rb index eef492243..36c56ffd4 100644 --- a/app/models/point_group.rb +++ b/app/models/point_group.rb @@ -4,7 +4,7 @@ class PointGroup < ApplicationRecord BAD_SORT = "%{value} is not valid. Valid options are: " + SORT_TYPES.map(&:inspect).join(", ") DEFAULT_CRITERIA = { - day: { op: "<", days: 0 }, + day: { op: "<", days_ago: 0 }, string_eq: {}, number_eq: {}, number_lt: {}, diff --git a/app/models/tool_slot.rb b/app/models/tool_slot.rb index 22a0abd90..e416dadbd 100644 --- a/app/models/tool_slot.rb +++ b/app/models/tool_slot.rb @@ -11,7 +11,7 @@ class ToolSlot < Point MIN_PULLOUT = PULLOUT_DIRECTIONS.min PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\ "%{value} is not valid." - IN_USE = "already in use by another tool slot. "\ + IN_USE = "already in use by another slot. "\ "Please un-assign the tool from its current slot"\ " before reassigning." diff --git a/app/mutations/devices/create_seed_data.rb b/app/mutations/devices/create_seed_data.rb index 8cc336244..fb0899b39 100644 --- a/app/mutations/devices/create_seed_data.rb +++ b/app/mutations/devices/create_seed_data.rb @@ -7,7 +7,9 @@ module Devices "genesis_1.2" => Devices::Seeders::GenesisOneTwo, "genesis_1.3" => Devices::Seeders::GenesisOneThree, "genesis_1.4" => Devices::Seeders::GenesisOneFour, + "genesis_1.5" => Devices::Seeders::GenesisOneFive, "genesis_xl_1.4" => Devices::Seeders::GenesisXlOneFour, + "genesis_xl_1.5" => Devices::Seeders::GenesisXlOneFive, "demo_account" => Devices::Seeders::DemoAccountSeeder, "none" => Devices::Seeders::None, diff --git a/app/mutations/devices/seeders/abstract_express.rb b/app/mutations/devices/seeders/abstract_express.rb index 0ab4feb85..557d04ccb 100644 --- a/app/mutations/devices/seeders/abstract_express.rb +++ b/app/mutations/devices/seeders/abstract_express.rb @@ -27,7 +27,7 @@ module Devices add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, y: 25, - z: -200, + z: 0, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -37,25 +37,18 @@ module Devices add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, y: 50, - z: -200, + z: 0, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) end - def tool_slots_slot_3 - add_tool_slot(name: ToolNames::SEED_TROUGH_3, - x: 0, - y: 75, - z: -200, - tool: tools_seed_trough_3, - pullout_direction: ToolSlot::NONE, - gantry_mounted: true) - end - + def tool_slots_slot_3; end def tool_slots_slot_4; end def tool_slots_slot_5; end def tool_slots_slot_6; end + def tool_slots_slot_7; end + def tool_slots_slot_8; end def tools_seed_bin; end def tools_seed_tray; end @@ -69,11 +62,6 @@ module Devices add_tool(ToolNames::SEED_TROUGH_2) end - def tools_seed_trough_3 - @tools_seed_trough_3 ||= - add_tool(ToolNames::SEED_TROUGH_3) - end - def tools_seeder; end def tools_soil_sensor; end def tools_watering_nozzle; end diff --git a/app/mutations/devices/seeders/abstract_genesis.rb b/app/mutations/devices/seeders/abstract_genesis.rb index b52665768..36c48398c 100644 --- a/app/mutations/devices/seeders/abstract_genesis.rb +++ b/app/mutations/devices/seeders/abstract_genesis.rb @@ -75,6 +75,9 @@ module Devices tool: tools_weeder) end + def tool_slots_slot_7; end + def tool_slots_slot_8; end + def tools_seed_bin @tools_seed_bin ||= add_tool(ToolNames::SEED_BIN) @@ -87,7 +90,6 @@ module Devices def tools_seed_trough_1; end def tools_seed_trough_2; end - def tools_seed_trough_3; end def tools_seeder @tools_seeder ||= diff --git a/app/mutations/devices/seeders/abstract_seeder.rb b/app/mutations/devices/seeders/abstract_seeder.rb index fd6bc4ba1..7d5a41003 100644 --- a/app/mutations/devices/seeders/abstract_seeder.rb +++ b/app/mutations/devices/seeders/abstract_seeder.rb @@ -37,7 +37,6 @@ module Devices :tools_seed_tray, :tools_seed_trough_1, :tools_seed_trough_2, - :tools_seed_trough_3, :tools_seeder, :tools_soil_sensor, :tools_watering_nozzle, @@ -50,6 +49,8 @@ module Devices :tool_slots_slot_4, :tool_slots_slot_5, :tool_slots_slot_6, + :tool_slots_slot_7, + :tool_slots_slot_8, # WEBCAM FEEDS =========================== :webcam_feeds, @@ -152,11 +153,12 @@ module Devices def tool_slots_slot_4; end def tool_slots_slot_5; end def tool_slots_slot_6; end + def tool_slots_slot_7; end + def tool_slots_slot_8; end def tools_seed_bin; end def tools_seed_tray; end def tools_seed_trough_1; end def tools_seed_trough_2; end - def tools_seed_trough_3; end def tools_seeder; end def tools_soil_sensor; end def tools_watering_nozzle; end diff --git a/app/mutations/devices/seeders/constants.rb b/app/mutations/devices/seeders/constants.rb index c6f56b654..4962dd36c 100644 --- a/app/mutations/devices/seeders/constants.rb +++ b/app/mutations/devices/seeders/constants.rb @@ -31,7 +31,6 @@ module Devices LIGHTING = "Lighting" SEED_TROUGH_1 = "Seed Trough 1" SEED_TROUGH_2 = "Seed Trough 2" - SEED_TROUGH_3 = "Seed Trough 3" end # Stub plants ============================== diff --git a/app/mutations/devices/seeders/genesis_one_five.rb b/app/mutations/devices/seeders/genesis_one_five.rb new file mode 100644 index 000000000..2731dd21a --- /dev/null +++ b/app/mutations/devices/seeders/genesis_one_five.rb @@ -0,0 +1,41 @@ +module Devices + module Seeders + class GenesisOneFive < AbstractGenesis + def settings_firmware + device + .fbos_config + .update!(firmware_hardware: FbosConfig::FARMDUINO_K15) + end + + def tool_slots_slot_7 + add_tool_slot(name: ToolNames::SEED_TROUGH_1, + x: 0, + y: 25, + z: 0, + tool: tools_seed_trough_1, + pullout_direction: ToolSlot::NONE, + gantry_mounted: true) + end + + def tool_slots_slot_8 + add_tool_slot(name: ToolNames::SEED_TROUGH_2, + x: 0, + y: 50, + z: 0, + tool: tools_seed_trough_2, + pullout_direction: ToolSlot::NONE, + gantry_mounted: true) + end + + def tools_seed_trough_1 + @tools_seed_trough_1 ||= + add_tool(ToolNames::SEED_TROUGH_1) + end + + def tools_seed_trough_2 + @tools_seed_trough_2 ||= + add_tool(ToolNames::SEED_TROUGH_2) + end + end + end +end diff --git a/app/mutations/devices/seeders/genesis_xl_one_five.rb b/app/mutations/devices/seeders/genesis_xl_one_five.rb new file mode 100644 index 000000000..7333947ae --- /dev/null +++ b/app/mutations/devices/seeders/genesis_xl_one_five.rb @@ -0,0 +1,53 @@ +module Devices + module Seeders + class GenesisXlOneFive < AbstractGenesis + def settings_firmware + device + .fbos_config + .update!(firmware_hardware: FbosConfig::FARMDUINO_K15) + end + + def settings_device_name + device.update!(name: "FarmBot Genesis XL") + end + + def settings_default_map_size_x + device.web_app_config.update!(map_size_x: 5_900) + end + + def settings_default_map_size_y + device.web_app_config.update!(map_size_y: 2_900) + end + + def tool_slots_slot_7 + add_tool_slot(name: ToolNames::SEED_TROUGH_1, + x: 0, + y: 25, + z: 0, + tool: tools_seed_trough_1, + pullout_direction: ToolSlot::NONE, + gantry_mounted: true) + end + + def tool_slots_slot_8 + add_tool_slot(name: ToolNames::SEED_TROUGH_2, + x: 0, + y: 50, + z: 0, + tool: tools_seed_trough_2, + pullout_direction: ToolSlot::NONE, + gantry_mounted: true) + end + + def tools_seed_trough_1 + @tools_seed_trough_1 ||= + add_tool(ToolNames::SEED_TROUGH_1) + end + + def tools_seed_trough_2 + @tools_seed_trough_2 ||= + add_tool(ToolNames::SEED_TROUGH_2) + end + end + end +end diff --git a/app/mutations/devices/seeders/none.rb b/app/mutations/devices/seeders/none.rb index ea3a49bb1..714c2050c 100644 --- a/app/mutations/devices/seeders/none.rb +++ b/app/mutations/devices/seeders/none.rb @@ -28,11 +28,12 @@ module Devices def tool_slots_slot_4; end def tool_slots_slot_5; end def tool_slots_slot_6; end + def tool_slots_slot_7; end + def tool_slots_slot_8; end def tools_seed_bin; end def tools_seed_tray; end def tools_seed_trough_1; end def tools_seed_trough_2; end - def tools_seed_trough_3; end def tools_seeder; end def tools_soil_sensor; end def tools_watering_nozzle; end diff --git a/app/mutations/point_groups/helpers.rb b/app/mutations/point_groups/helpers.rb index 0f5a3d7f5..7d6094e45 100644 --- a/app/mutations/point_groups/helpers.rb +++ b/app/mutations/point_groups/helpers.rb @@ -5,7 +5,7 @@ module PointGroups hash :criteria do hash(:day) do string :op, in: [">", "<"] - integer :days + integer :days_ago end hash(:string_eq) { array :*, class: String } hash(:number_eq) { array :*, class: Integer } diff --git a/app/mutations/tools/destroy.rb b/app/mutations/tools/destroy.rb index e504606b7..3618a5d98 100644 --- a/app/mutations/tools/destroy.rb +++ b/app/mutations/tools/destroy.rb @@ -1,9 +1,9 @@ module Tools class Destroy < Mutations::Command - STILL_IN_USE = "Can't delete tool because the following sequences are "\ - "still using it: %s" - STILL_IN_SLOT = "Can't delete tool because it is still in a tool slot. "\ - "Please remove it from the tool slot first." + STILL_IN_USE = "Can't delete tool or seed container because the " \ + "following sequences are still using it: %s" + STILL_IN_SLOT = "Can't delete tool or seed container because it is " \ + "still in a slot. Please remove it from the slot first." required do model :tool, class: Tool @@ -15,10 +15,11 @@ module Tools end def execute + maybe_unmount_tool tool.destroy! end -private + private def slot @slot ||= tool.tool_slot @@ -33,8 +34,14 @@ private end def names - @names ||= \ + @names ||= InUseTool.where(tool_id: tool.id).pluck(:sequence_name).join(", ") end + + def maybe_unmount_tool + if tool.device.mounted_tool_id == tool.id + tool.device.update!(mounted_tool_id: nil) + end + end end end diff --git a/app/serializers/point_group_serializer.rb b/app/serializers/point_group_serializer.rb index 66e128aa4..f762c7e3c 100644 --- a/app/serializers/point_group_serializer.rb +++ b/app/serializers/point_group_serializer.rb @@ -4,4 +4,8 @@ class PointGroupSerializer < ApplicationSerializer def point_ids object.point_group_items.pluck(:point_id) end + + def criteria + object.criteria || PointGroup::DEFAULT_CRITERIA + end end diff --git a/frontend/__test_support__/control_panel_state.ts b/frontend/__test_support__/control_panel_state.ts index 13eddf61a..c90ff8580 100644 --- a/frontend/__test_support__/control_panel_state.ts +++ b/frontend/__test_support__/control_panel_state.ts @@ -4,9 +4,15 @@ export const panelState = (): ControlPanelState => { return { homing_and_calibration: false, motors: false, - encoders_and_endstops: false, + encoders: false, + endstops: false, + error_handling: false, + pin_bindings: false, danger_zone: false, power_and_reset: false, - pin_guard: false + pin_guard: false, + farm_designer: false, + firmware: false, + farmbot_os: false, }; }; diff --git a/frontend/__test_support__/fake_state/bot.ts b/frontend/__test_support__/fake_state/bot.ts index ec6c82df5..d236854cf 100644 --- a/frontend/__test_support__/fake_state/bot.ts +++ b/frontend/__test_support__/fake_state/bot.ts @@ -1,16 +1,10 @@ import { Everything } from "../../interfaces"; +import { panelState } from "../control_panel_state"; export const bot: Everything["bot"] = { "consistent": true, "stepSize": 100, - "controlPanelState": { - "homing_and_calibration": false, - "motors": false, - "encoders_and_endstops": false, - "danger_zone": false, - "power_and_reset": false, - "pin_guard": false, - }, + "controlPanelState": panelState(), "hardware": { "gpio_registry": {}, "mcu_params": { diff --git a/frontend/__test_support__/fake_state/images.ts b/frontend/__test_support__/fake_state/images.ts index c032dd147..d09ad6de4 100644 --- a/frontend/__test_support__/fake_state/images.ts +++ b/frontend/__test_support__/fake_state/images.ts @@ -54,5 +54,5 @@ export const fakeImages: TaggedImage[] = [ } }, "uuid": "Image.7.5" - } + }, ]; diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index 641cba1bb..015b8432d 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -29,7 +29,7 @@ import { } from "farmbot"; import { fakeResource } from "../fake_resource"; import { - ExecutableType, PinBindingType, Folder + ExecutableType, PinBindingType, Folder, } from "farmbot/dist/resources/api_resources"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { MessageType } from "../../sequences/interfaces"; @@ -460,7 +460,7 @@ export function fakePointGroup(): TaggedPointGroup { sort_type: "xy_ascending", point_ids: [], criteria: { - day: { op: "<", days: 0 }, + day: { op: "<", days_ago: 0 }, number_eq: {}, number_gt: {}, number_lt: {}, diff --git a/frontend/__test_support__/fake_variables.ts b/frontend/__test_support__/fake_variables.ts index 449779f1c..084285ff0 100644 --- a/frontend/__test_support__/fake_variables.ts +++ b/frontend/__test_support__/fake_variables.ts @@ -2,7 +2,7 @@ import { Coordinate } from "farmbot"; import { VariableNameSet } from "../resources/interfaces"; export const fakeVariableNameSet = ( - label = "parent", vector = { x: 0, y: 0, z: 0 } + label = "parent", vector = { x: 0, y: 0, z: 0 }, ): VariableNameSet => { const data_value: Coordinate = { kind: "coordinate", args: vector diff --git a/frontend/__test_support__/farm_event_calendar_support.ts b/frontend/__test_support__/farm_event_calendar_support.ts index 9d4d84b26..7bde8e97b 100644 --- a/frontend/__test_support__/farm_event_calendar_support.ts +++ b/frontend/__test_support__/farm_event_calendar_support.ts @@ -1,6 +1,6 @@ import moment from "moment"; import { - FarmEventWithExecutable + FarmEventWithExecutable, } from "../farm_designer/farm_events/calendar/interfaces"; export const TIME = { @@ -24,7 +24,7 @@ export const fakeFarmEventWithExecutable = (): FarmEventWithExecutable => { color: "red", name: "faker", kind: "sequence", - args: { version: 0, locals: { kind: "scope_declaration", args: {} }, } + args: { version: 0, locals: { kind: "scope_declaration", args: {} } } } }; }; @@ -84,7 +84,7 @@ export const calendarRows = [ "subheading": "25", "id": 79, "childExecutableName": "Goto 0, 0, 0 123" - } + }, ] }, { @@ -171,7 +171,7 @@ export const calendarRows = [ "subheading": "25", "id": 79, "childExecutableName": "Goto 0, 0, 0 123" - } + }, ] }, { @@ -258,7 +258,7 @@ export const calendarRows = [ "subheading": "25", "id": 79, "childExecutableName": "Goto 0, 0, 0 123" - } + }, ] - } + }, ]; diff --git a/frontend/__test_support__/resource_index_builder.ts b/frontend/__test_support__/resource_index_builder.ts index 1272005d0..ff4ce3bb8 100644 --- a/frontend/__test_support__/resource_index_builder.ts +++ b/frontend/__test_support__/resource_index_builder.ts @@ -62,7 +62,7 @@ const tr0: TaggedResource = { }, "speed": 100 } - } + }, ], "args": { "version": 4, @@ -287,7 +287,7 @@ const tr12: TaggedResource = { "regimen_id": 11, "sequence_id": 23, "time_offset": 345900000 - } + }, ], body: [], }, @@ -345,7 +345,7 @@ export const FAKE_RESOURCES: TaggedResource[] = [ tr0, tr14, tr15, - log + log, ]; const KIND: keyof TaggedResource = "kind"; // Safety first, kids. type ResourceGroupNumber = 0 | 1 | 2 | 3 | 4; diff --git a/frontend/__tests__/app_test.tsx b/frontend/__tests__/app_test.tsx index 70e8e305f..bdd6ff6d9 100644 --- a/frontend/__tests__/app_test.tsx +++ b/frontend/__tests__/app_test.tsx @@ -9,11 +9,11 @@ import { RawApp as App, AppProps, mapStateToProps } from "../app"; import { mount } from "enzyme"; import { bot } from "../__test_support__/fake_state/bot"; import { - fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv + fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv, } from "../__test_support__/fake_state/resources"; import { fakeState } from "../__test_support__/fake_state"; import { - buildResourceIndex + buildResourceIndex, } from "../__test_support__/resource_index_builder"; import { ResourceName } from "farmbot"; import { fakeTimeSettings } from "../__test_support__/fake_time_settings"; @@ -125,7 +125,7 @@ describe(": NavBar", () => { "Device", "Sequences", "Regimens", - "Farmware" + "Farmware", ]; strings.map(string => expect(t).toContain(string)); wrapper.unmount(); @@ -157,7 +157,6 @@ describe("mapStateToProps()", () => { const state = fakeState(); const config = fakeFbosConfig(); config.body.auto_sync = true; - config.body.api_migrated = true; const fakeEnv = fakeFarmwareEnv(); state.resources = buildResourceIndex([config, fakeEnv]); state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" }; diff --git a/frontend/__tests__/attach_app_to_dom_test.ts b/frontend/__tests__/attach_app_to_dom_test.ts index 1a2d69daa..3daabdbf2 100644 --- a/frontend/__tests__/attach_app_to_dom_test.ts +++ b/frontend/__tests__/attach_app_to_dom_test.ts @@ -1,17 +1,16 @@ -jest.mock("../util", () => { - return { - attachToRoot: jest.fn(), - // Incidental mock. Can be removed if errors go away. - trim: jest.fn(x => x) - }; -}); +jest.mock("../util", () => ({ + attachToRoot: jest.fn(), + // Incidental mock. Can be removed if errors go away. + trim: jest.fn(x => x), + urlFriendly: jest.fn(), +})); jest.mock("../redux/store", () => { return { store: { dispatch: jest.fn() } }; }); jest.mock("../account/dev/dev_support", () => ({ - DevSettings: { futureFeaturesEnabled: () => false, } + DevSettings: { futureFeaturesEnabled: () => false } })); jest.mock("../config/actions", () => { diff --git a/frontend/__tests__/external_urls_test.ts b/frontend/__tests__/external_urls_test.ts new file mode 100644 index 000000000..99f23cea5 --- /dev/null +++ b/frontend/__tests__/external_urls_test.ts @@ -0,0 +1,33 @@ +jest.unmock("../external_urls"); +import { ExternalUrl } from "../external_urls"; + +/* tslint:disable:max-line-length */ + +describe("ExternalUrl", () => { + it("returns urls", () => { + expect(ExternalUrl.featureMinVersions) + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/FEATURE_MIN_VERSIONS.json"); + expect(ExternalUrl.osReleaseNotes) + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md"); + expect(ExternalUrl.latestRelease) + .toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest"); + expect(ExternalUrl.webAppRepo) + .toEqual("https://github.com/FarmBot/Farmbot-Web-App"); + expect(ExternalUrl.gitHubFarmBot) + .toEqual("https://github.com/FarmBot"); + expect(ExternalUrl.softwareDocs) + .toEqual("https://software.farm.bot/docs"); + expect(ExternalUrl.softwareForum) + .toEqual("https://forum.farmbot.org/c/software"); + expect(ExternalUrl.OpenFarm.cropApi) + .toEqual("https://openfarm.cc/api/v1/crops/"); + expect(ExternalUrl.OpenFarm.cropBrowse) + .toEqual("https://openfarm.cc/crops/"); + expect(ExternalUrl.OpenFarm.newCrop) + .toEqual("https://openfarm.cc/en/crops/new"); + expect(ExternalUrl.Video.desktop) + .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"); + expect(ExternalUrl.Video.mobile) + .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"); + }); +}); diff --git a/frontend/__tests__/interceptors_test.ts b/frontend/__tests__/interceptors_test.ts index 7316ac461..fbc179327 100644 --- a/frontend/__tests__/interceptors_test.ts +++ b/frontend/__tests__/interceptors_test.ts @@ -19,7 +19,7 @@ jest.mock("../session", () => ({ })); import { - responseFulfilled, isLocalRequest, requestFulfilled, responseRejected + responseFulfilled, isLocalRequest, requestFulfilled, responseRejected, } from "../interceptors"; import { AxiosResponse, Method } from "axios"; import { uuid } from "farmbot"; diff --git a/frontend/__tests__/interface_test.ts b/frontend/__tests__/interface_test.ts index bd461387f..92ee4a7b0 100644 --- a/frontend/__tests__/interface_test.ts +++ b/frontend/__tests__/interface_test.ts @@ -30,7 +30,6 @@ import "../regimens/editor/interfaces"; import "../regimens/interfaces"; import "../resources/interfaces"; import "../sequences/interfaces"; -import "../tools/interfaces"; describe("interfaces", () => { it("cant explain why coverage is 0 for interface files", () => { diff --git a/frontend/__tests__/refresh_token_no_test.ts b/frontend/__tests__/refresh_token_no_test.ts index aeb1c09c2..888f42596 100644 --- a/frontend/__tests__/refresh_token_no_test.ts +++ b/frontend/__tests__/refresh_token_no_test.ts @@ -9,7 +9,7 @@ jest.mock("axios", () => ({ })); -jest.mock("../session", () => ({ Session: { clear: jest.fn(), } })); +jest.mock("../session", () => ({ Session: { clear: jest.fn() } })); import { maybeRefreshToken } from "../refresh_token"; import { API } from "../api/index"; diff --git a/frontend/__tests__/resource_index_builder_test.ts b/frontend/__tests__/resource_index_builder_test.ts index e953c7296..05ec30418 100644 --- a/frontend/__tests__/resource_index_builder_test.ts +++ b/frontend/__tests__/resource_index_builder_test.ts @@ -1,6 +1,6 @@ import { buildResourceIndex, - FAKE_RESOURCES + FAKE_RESOURCES, } from "../__test_support__/resource_index_builder"; import { TaggedFarmEvent, SpecialStatus } from "farmbot"; diff --git a/frontend/__tests__/route_config_test.tsx b/frontend/__tests__/route_config_test.tsx index 20bded0a9..85a879908 100644 --- a/frontend/__tests__/route_config_test.tsx +++ b/frontend/__tests__/route_config_test.tsx @@ -12,7 +12,7 @@ type Info = UnboundRouteConfig<{}, {}>; const fakeCallback = ( component: ConnectedComponent, child: ConnectedComponent | undefined, - info: Info + info: Info, ) => { if (info.$ == "*") { expect(component.name).toEqual("FourOhFour"); diff --git a/frontend/account/__tests__/request_account_exports_test.ts b/frontend/account/__tests__/request_account_exports_test.ts index 4cb97b3cf..873abbef1 100644 --- a/frontend/account/__tests__/request_account_exports_test.ts +++ b/frontend/account/__tests__/request_account_exports_test.ts @@ -11,7 +11,7 @@ jest.mock("axios", () => ({ import { API } from "../../api"; import { Content } from "../../constants"; import { - requestAccountExport, generateFilename + requestAccountExport, generateFilename, } from "../request_account_export"; import { success } from "../../toast/toast"; import axios from "axios"; diff --git a/frontend/account/components/change_password.tsx b/frontend/account/components/change_password.tsx index 8be5d0c7e..0288b3c43 100644 --- a/frontend/account/components/change_password.tsx +++ b/frontend/account/components/change_password.tsx @@ -3,7 +3,7 @@ import { Widget, WidgetHeader, WidgetBody, - SaveBtn + SaveBtn, } from "../../ui/index"; import { SpecialStatus } from "farmbot"; import Axios from "axios"; diff --git a/frontend/account/components/dangerous_delete_widget.tsx b/frontend/account/components/dangerous_delete_widget.tsx index 635c050e9..09e304fe1 100644 --- a/frontend/account/components/dangerous_delete_widget.tsx +++ b/frontend/account/components/dangerous_delete_widget.tsx @@ -20,7 +20,7 @@ export class DangerousDeleteWidget extends return -
+
{t(this.props.warning)}

{t(this.props.confirmation)} @@ -42,6 +42,7 @@ export class DangerousDeleteWidget extends diff --git a/frontend/account/components/export_account_panel.tsx b/frontend/account/components/export_account_panel.tsx index d431f0b8c..111e278f2 100644 --- a/frontend/account/components/export_account_panel.tsx +++ b/frontend/account/components/export_account_panel.tsx @@ -7,7 +7,7 @@ export function ExportAccountPanel(props: { onClick: () => void }) { return -
+
{t(Content.EXPORT_DATA_DESC)}
@@ -19,6 +19,7 @@ export function ExportAccountPanel(props: { onClick: () => void }) { diff --git a/frontend/account/components/settings.tsx b/frontend/account/components/settings.tsx index ff59123d9..bfb51407b 100644 --- a/frontend/account/components/settings.tsx +++ b/frontend/account/components/settings.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { - BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn + BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn, } from "../../ui/index"; import { SettingsPropTypes } from "../interfaces"; import { t } from "../../i18next_wrapper"; diff --git a/frontend/account/dev/__tests__/dev_mode_test.tsx b/frontend/account/dev/__tests__/dev_mode_test.tsx index 66abd2d7f..39dcd3d56 100644 --- a/frontend/account/dev/__tests__/dev_mode_test.tsx +++ b/frontend/account/dev/__tests__/dev_mode_test.tsx @@ -8,7 +8,7 @@ import { DevMode } from "../dev_mode"; import * as React from "react"; import { range } from "lodash"; import { - setWebAppConfigValue + setWebAppConfigValue, } from "../../../config_storage/actions"; import { warning } from "../../../toast/toast"; diff --git a/frontend/account/dev/__tests__/dev_widget_test.tsx b/frontend/account/dev/__tests__/dev_widget_test.tsx index cbd670afa..058ee8d3a 100644 --- a/frontend/account/dev/__tests__/dev_widget_test.tsx +++ b/frontend/account/dev/__tests__/dev_widget_test.tsx @@ -7,7 +7,7 @@ jest.mock("../../../config_storage/actions", () => ({ import * as React from "react"; import { mount, shallow } from "enzyme"; import { - DevWidget, DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow + DevWidget, DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow, } from "../dev_widget"; import { DevSettings } from "../dev_support"; import { setWebAppConfigValue } from "../../../config_storage/actions"; diff --git a/frontend/account/dev/dev_support.ts b/frontend/account/dev/dev_support.ts index 0c6a0e414..a604c7570 100644 --- a/frontend/account/dev/dev_support.ts +++ b/frontend/account/dev/dev_support.ts @@ -1,6 +1,6 @@ import { store } from "../../redux/store"; import { - getWebAppConfigValue, setWebAppConfigValue + getWebAppConfigValue, setWebAppConfigValue, } from "../../config_storage/actions"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; diff --git a/frontend/account/dev/dev_widget.tsx b/frontend/account/dev/dev_widget.tsx index 2a6ad1ea8..3837c9010 100644 --- a/frontend/account/dev/dev_widget.tsx +++ b/frontend/account/dev/dev_widget.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { - Widget, WidgetHeader, WidgetBody, Row, Col, BlurableInput + Widget, WidgetHeader, WidgetBody, Row, Col, BlurableInput, } from "../../ui"; import { ToggleButton } from "../../controls/toggle_button"; import { setWebAppConfigValue } from "../../config_storage/actions"; diff --git a/frontend/account/index.tsx b/frontend/account/index.tsx index 658704fae..c222cb409 100644 --- a/frontend/account/index.tsx +++ b/frontend/account/index.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { connect } from "react-redux"; import { - Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget + Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget, } from "./components"; import { Props } from "./interfaces"; import { Page, Row, Col } from "../ui"; @@ -47,12 +47,13 @@ export class RawAccount extends React.Component { (key: keyof User) => (key === "email") && this.setState({ warnThem: true }); onChange = (e: React.FormEvent) => { - const { name, value } = e.currentTarget; - if (isKey(name)) { - this.tempHack(name); - this.props.dispatch(edit(this.props.user, { [name]: value })); + const { value } = e.currentTarget; + const field = e.currentTarget.name; + if (isKey(field)) { + this.tempHack(field); + this.props.dispatch(edit(this.props.user, { [field]: value })); } else { - throw new Error("Bad key: " + name); + throw new Error("Bad key: " + field); } }; diff --git a/frontend/account/labs/__tests__/labs_features_test.tsx b/frontend/account/labs/__tests__/labs_features_test.tsx index 9d518772e..05ac2e7ab 100644 --- a/frontend/account/labs/__tests__/labs_features_test.tsx +++ b/frontend/account/labs/__tests__/labs_features_test.tsx @@ -5,7 +5,7 @@ const mockFeatures = [ storageKey: "weedDetector", callback: jest.fn(), value: false - } + }, ]; const mocks = { diff --git a/frontend/account/labs/labs_features_list_data.ts b/frontend/account/labs/labs_features_list_data.ts index 2e1b58207..2acc81fd4 100644 --- a/frontend/account/labs/labs_features_list_data.ts +++ b/frontend/account/labs/labs_features_list_data.ts @@ -1,7 +1,7 @@ import { BooleanSetting } from "../../session_keys"; import { Content } from "../../constants"; import { - GetWebAppConfigValue, setWebAppConfigValue + GetWebAppConfigValue, setWebAppConfigValue, } from "../../config_storage/actions"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { t } from "../../i18next_wrapper"; @@ -78,7 +78,7 @@ export const fetchLabFeatures = storageKey: BooleanSetting.user_interface_read_only_mode, value: false, displayInvert: false, - } + }, ].map(fetchSettingValue(getConfigValue))); /** Always allow toggling from true => false (deactivate). diff --git a/frontend/account/labs/labs_features_list_ui.tsx b/frontend/account/labs/labs_features_list_ui.tsx index f9dfafb84..1640ddda2 100644 --- a/frontend/account/labs/labs_features_list_ui.tsx +++ b/frontend/account/labs/labs_features_list_ui.tsx @@ -11,7 +11,7 @@ interface LabsFeaturesListProps { } export function LabsFeaturesList(props: LabsFeaturesListProps) { - return
+ return
{fetchLabFeatures(props.getConfigValue).map((feature, i) => { const displayValue = feature.displayInvert ? !feature.value : feature.value; return @@ -23,6 +23,7 @@ export function LabsFeaturesList(props: LabsFeaturesListProps) { props.onToggle(feature) .then(() => feature.callback && feature.callback())} diff --git a/frontend/account/request_account_export.ts b/frontend/account/request_account_export.ts index 95bc0d568..703e41fc8 100644 --- a/frontend/account/request_account_export.ts +++ b/frontend/account/request_account_export.ts @@ -9,9 +9,8 @@ interface DataDumpExport { device?: DeviceAccountSettings; } type Response = AxiosResponse; export function generateFilename({ device }: DataDumpExport): string { - let name: string; - name = device ? (device.name + "_" + device.id) : "farmbot"; - return `export_${name}.json`.toLowerCase(); + const nameAndId = device ? (device.name + "_" + device.id) : "farmbot"; + return `export_${nameAndId}.json`.toLowerCase(); } // Thanks, @KOL - https://stackoverflow.com/a/19328891/1064917 diff --git a/frontend/api/api.ts b/frontend/api/api.ts index 03d5a55d0..264242e62 100644 --- a/frontend/api/api.ts +++ b/frontend/api/api.ts @@ -158,6 +158,10 @@ export class API { get farmwareInstallationPath() { return `${this.baseUrl}/api/farmware_installations/`; } + /** /api/first_party_farmwares */ + get firstPartyFarmwarePath() { + return `${this.baseUrl}/api/first_party_farmwares`; + } /** /api/alerts/:id */ get alertPath() { return `${this.baseUrl}/api/alerts/`; } /** /api/global_bulletins/:id */ diff --git a/frontend/apology.tsx b/frontend/apology.tsx index a00871358..bd2ac6328 100644 --- a/frontend/apology.tsx +++ b/frontend/apology.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { Session } from "./session"; +import { ExternalUrl } from "./external_urls"; const OUTER_STYLE: React.CSSProperties = { borderRadius: "10px", @@ -47,7 +48,7 @@ export function Apology(_: {}) {
  • Send a report to our developer team via the  - FarmBot software + FarmBot software forum. Including additional information (such as steps leading up to the error) helps us identify solutions more quickly. diff --git a/frontend/app.tsx b/frontend/app.tsx index c22a3fb5e..cdf2c1519 100644 --- a/frontend/app.tsx +++ b/frontend/app.tsx @@ -18,7 +18,7 @@ import { validBotLocationData, validFwConfig, validFbosConfig } from "./util"; import { BooleanSetting } from "./session_keys"; import { getPathArray } from "./history"; import { - getWebAppConfigValue, GetWebAppConfigValue + getWebAppConfigValue, GetWebAppConfigValue, } from "./config_storage/actions"; import { takeSortedLogs } from "./logs/state_to_props"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; @@ -99,7 +99,7 @@ const MUST_LOAD: ResourceName[] = [ "FarmEvent", "Point", "Device", - "Tool" // Sequence editor needs this for rendering. + "Tool", // Sequence editor needs this for rendering. ]; export class RawApp extends React.Component { diff --git a/frontend/auth/actions.ts b/frontend/auth/actions.ts index 76126927c..f8d417137 100644 --- a/frontend/auth/actions.ts +++ b/frontend/auth/actions.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { - fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL, - fetchLatestGHBetaRelease + fetchReleases, fetchMinOsFeatureData, + fetchLatestGHBetaRelease, } from "../devices/actions"; import { AuthState } from "./interfaces"; import { ReduxAction } from "../redux/interfaces"; @@ -10,12 +10,13 @@ import { API } from "../api"; import { responseFulfilled, responseRejected, - requestFulfilled + requestFulfilled, } from "../interceptors"; import { Actions } from "../constants"; import { connectDevice } from "../connectivity/connect_device"; import { getFirstPartyFarmwareList } from "../farmware/actions"; import { readOnlyInterceptor } from "../read_only_mode"; +import { ExternalUrl } from "../external_urls"; export function didLogin(authState: AuthState, dispatch: Function) { API.setBaseUrl(authState.token.unencoded.iss); @@ -24,7 +25,7 @@ export function didLogin(authState: AuthState, dispatch: Function) { beta_os_update_server && beta_os_update_server != "NOT_SET" && dispatch(fetchLatestGHBetaRelease(beta_os_update_server)); dispatch(getFirstPartyFarmwareList()); - dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL)); + dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions)); dispatch(setToken(authState)); Sync.fetchSyncData(dispatch); dispatch(connectDevice(authState)); diff --git a/frontend/config/__tests__/actions_test.ts b/frontend/config/__tests__/actions_test.ts index aee2412a9..baf50dae9 100644 --- a/frontend/config/__tests__/actions_test.ts +++ b/frontend/config/__tests__/actions_test.ts @@ -1,58 +1,72 @@ -const mockState = { - auth: { - token: { - unencoded: { iss: "http://geocities.com" } - } - } -}; - -jest.mock("axios", () => ({ - interceptors: { - response: { use: jest.fn() }, - request: { use: jest.fn() } - }, - get() { return Promise.resolve({ data: mockState }); } -})); - jest.mock("../../session", () => ({ Session: { fetchStoredToken: jest.fn(), getAll: () => undefined, - clear: jest.fn() + clear: jest.fn(), } })); jest.mock("../../auth/actions", () => ({ didLogin: jest.fn(), - setToken: jest.fn() + setToken: jest.fn(), })); +jest.mock("../../refresh_token", () => ({ maybeRefreshToken: jest.fn() })); + +let mockTimeout = Promise.resolve({ token: "fake token data" }); +jest.mock("promise-timeout", () => ({ timeout: () => mockTimeout })); + import { ready, storeToken } from "../actions"; import { setToken, didLogin } from "../../auth/actions"; import { Session } from "../../session"; import { auth } from "../../__test_support__/fake_state/token"; import { fakeState } from "../../__test_support__/fake_state"; -describe("Actions", () => { - it("calls didLogin()", () => { - jest.resetAllMocks(); +describe("ready()", () => { + it("uses new token", async () => { + const fakeAuth = { token: "fake token data" }; + mockTimeout = Promise.resolve(fakeAuth); const dispatch = jest.fn(); const thunk = ready(); - thunk(dispatch, fakeState); - expect(setToken).toHaveBeenCalled(); + const state = fakeState(); + console.warn = jest.fn(); + await thunk(dispatch, () => state); + expect(setToken).toHaveBeenCalledWith(fakeAuth); + expect(didLogin).toHaveBeenCalledWith(fakeAuth, dispatch); + expect(console.warn).not.toHaveBeenCalled(); + expect(Session.clear).not.toHaveBeenCalled(); }); - it("Calls Session.clear() when missing auth", () => { - jest.resetAllMocks(); + it("uses old token", async () => { + mockTimeout = Promise.reject({ token: "not used" }); + const dispatch = jest.fn(); + const thunk = ready(); + const state = fakeState(); + console.warn = jest.fn(); + await thunk(dispatch, () => state); + expect(setToken).toHaveBeenLastCalledWith(state.auth); + expect(didLogin).toHaveBeenCalledWith(state.auth, dispatch); + expect(console.warn) + .toHaveBeenCalledWith(expect.stringContaining("Can't refresh token.")); + expect(Session.clear).not.toHaveBeenCalled(); + }); + + it("calls Session.clear() when missing auth", () => { const dispatch = jest.fn(); const state = fakeState(); delete state.auth; const getState = () => state; const thunk = ready(); + console.warn = jest.fn(); thunk(dispatch, getState); + expect(setToken).not.toHaveBeenCalled(); + expect(didLogin).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); expect(Session.clear).toHaveBeenCalled(); }); +}); +describe("storeToken()", () => { it("stores token", () => { const old = auth; old.token.unencoded.jti = "old"; diff --git a/frontend/config_storage/__tests__/actions_test.ts b/frontend/config_storage/__tests__/actions_test.ts index 9794fbec1..e1b9474a0 100644 --- a/frontend/config_storage/__tests__/actions_test.ts +++ b/frontend/config_storage/__tests__/actions_test.ts @@ -1,5 +1,5 @@ import { - toggleWebAppBool, getWebAppConfigValue, setWebAppConfigValue + toggleWebAppBool, getWebAppConfigValue, setWebAppConfigValue, } from "../actions"; import { BooleanSetting, NumericSetting } from "../../session_keys"; import { edit, save } from "../../api/crud"; diff --git a/frontend/config_storage/actions.ts b/frontend/config_storage/actions.ts index 54688ebb5..c4b023dc6 100644 --- a/frontend/config_storage/actions.ts +++ b/frontend/config_storage/actions.ts @@ -4,7 +4,7 @@ import { BooleanConfigKey, WebAppConfig, NumberConfigKey, - StringConfigKey + StringConfigKey, } from "farmbot/dist/resources/configs/web_app"; import { getWebAppConfig } from "../resources/getters"; diff --git a/frontend/connectivity/__tests__/auto_sync_handle_inbound_test.ts b/frontend/connectivity/__tests__/auto_sync_handle_inbound_test.ts index d4aa6ca43..ac905d4a0 100644 --- a/frontend/connectivity/__tests__/auto_sync_handle_inbound_test.ts +++ b/frontend/connectivity/__tests__/auto_sync_handle_inbound_test.ts @@ -10,7 +10,7 @@ import { fakeState } from "../../__test_support__/fake_state"; import { GetState } from "../../redux/interfaces"; import { handleInbound } from "../auto_sync_handle_inbound"; import { - handleCreateOrUpdate + handleCreateOrUpdate, } from "../auto_sync"; import { destroyOK } from "../../resources/actions"; import { SkipMqttData, BadMqttData, UpdateMqttData, DeleteMqttData } from "../interfaces"; diff --git a/frontend/connectivity/__tests__/auto_sync_test.ts b/frontend/connectivity/__tests__/auto_sync_test.ts index 84c1129af..c0fcf47fc 100644 --- a/frontend/connectivity/__tests__/auto_sync_test.ts +++ b/frontend/connectivity/__tests__/auto_sync_test.ts @@ -4,7 +4,7 @@ import { asTaggedResource, handleCreate, handleUpdate, - handleCreateOrUpdate + handleCreateOrUpdate, } from "../auto_sync"; import { SpecialStatus, TaggedSequence } from "farmbot"; import { Actions } from "../../constants"; diff --git a/frontend/connectivity/__tests__/connect_device/event_listeners_test.ts b/frontend/connectivity/__tests__/connect_device/event_listeners_test.ts index f8bb6453f..223ae101b 100644 --- a/frontend/connectivity/__tests__/connect_device/event_listeners_test.ts +++ b/frontend/connectivity/__tests__/connect_device/event_listeners_test.ts @@ -35,7 +35,7 @@ describe("attachEventListeners", () => { ].map(e => expect(dev.on).toHaveBeenCalledWith(e, expect.any(Function))); [ "message", - "reconnect" + "reconnect", ].map(e => { if (dev.client) { expect(dev.client.on).toHaveBeenCalledWith(e, expect.any(Function)); diff --git a/frontend/connectivity/__tests__/connect_device/status_checks_test.ts b/frontend/connectivity/__tests__/connect_device/status_checks_test.ts index fca02b4d8..1b0fbda1a 100644 --- a/frontend/connectivity/__tests__/connect_device/status_checks_test.ts +++ b/frontend/connectivity/__tests__/connect_device/status_checks_test.ts @@ -1,21 +1,15 @@ -jest.mock("../../slow_down", () => { - return { - slowDown: jest.fn((fn: Function) => fn), - }; -}); - -jest.mock("../../../devices/actions", () => ({ - badVersion: jest.fn(), - EXPECTED_MAJOR: 1, - EXPECTED_MINOR: 0, +jest.mock("../../slow_down", () => ({ + slowDown: jest.fn((fn: Function) => fn) })); +jest.mock("../../../devices/actions", () => ({ badVersion: jest.fn() })); + import { onStatus, incomingStatus, incomingLegacyStatus, onLegacyStatus, - HACKY_FLAGS + HACKY_FLAGS, } from "../../connect_device"; import { slowDown } from "../../slow_down"; import { fakeState } from "../../../__test_support__/fake_state"; @@ -49,8 +43,10 @@ describe("onStatus()", () => { }); it("version ok", () => { + globalConfig.MINIMUM_FBOS_VERSION = "1.0.0"; callOnStatus("1.0.0"); expect(badVersion).not.toHaveBeenCalled(); + delete globalConfig.MINIMUM_FBOS_VERSION; }); }); diff --git a/frontend/connectivity/__tests__/data_consistency_test.ts b/frontend/connectivity/__tests__/data_consistency_test.ts index 82161a88d..3023a8b2f 100644 --- a/frontend/connectivity/__tests__/data_consistency_test.ts +++ b/frontend/connectivity/__tests__/data_consistency_test.ts @@ -20,7 +20,7 @@ import { getDevice } from "../../device"; import { store } from "../../redux/store"; import { Actions } from "../../constants"; import { - startTracking, outstandingRequests, stopTracking, cleanUUID + startTracking, outstandingRequests, stopTracking, cleanUUID, } from "../data_consistency"; const unprocessedUuid = "~UU.ID~"; diff --git a/frontend/connectivity/__tests__/ping_mqtt_test.ts b/frontend/connectivity/__tests__/ping_mqtt_test.ts index ea00a23cb..bd2b154a5 100644 --- a/frontend/connectivity/__tests__/ping_mqtt_test.ts +++ b/frontend/connectivity/__tests__/ping_mqtt_test.ts @@ -8,7 +8,7 @@ jest.mock("../index", () => ({ import { readPing, startPinging, - PING_INTERVAL + PING_INTERVAL, } from "../ping_mqtt"; import { Farmbot, RpcRequest, RpcRequestBodyItem } from "farmbot"; import { FarmBotInternalConfig } from "farmbot/dist/config"; diff --git a/frontend/connectivity/__tests__/reducer_qos_test.ts b/frontend/connectivity/__tests__/reducer_qos_test.ts index ec41072fa..dd06996fc 100644 --- a/frontend/connectivity/__tests__/reducer_qos_test.ts +++ b/frontend/connectivity/__tests__/reducer_qos_test.ts @@ -41,7 +41,7 @@ describe("connectivity reducer", () => { it("broadcasts PING_OK", () => { pingOK("yep", 123); expect(store.dispatch).toHaveBeenCalledWith({ - payload: { at: 123, id: "yep", }, + payload: { at: 123, id: "yep" }, type: "PING_OK", }); }); diff --git a/frontend/connectivity/auto_sync.ts b/frontend/connectivity/auto_sync.ts index 94972a8d4..d7b01fb6a 100644 --- a/frontend/connectivity/auto_sync.ts +++ b/frontend/connectivity/auto_sync.ts @@ -4,7 +4,7 @@ import { TaggedResource, SpecialStatus } from "farmbot"; import { overwrite, init } from "../api/crud"; import { handleInbound } from "./auto_sync_handle_inbound"; import { - SyncPayload, MqttDataResult, Reason, UpdateMqttData + SyncPayload, MqttDataResult, Reason, UpdateMqttData, } from "./interfaces"; import { outstandingRequests } from "./data_consistency"; import { newTaggedResource } from "../sync/actions"; diff --git a/frontend/connectivity/connect_device.ts b/frontend/connectivity/connect_device.ts index 3922c62a5..c0ce91e2a 100644 --- a/frontend/connectivity/connect_device.ts +++ b/frontend/connectivity/connect_device.ts @@ -8,13 +8,7 @@ import { success, error, info, warning, fun, busy } from "../toast/toast"; import { HardwareState } from "../devices/interfaces"; import { GetState, ReduxAction } from "../redux/interfaces"; import { Content, Actions } from "../constants"; -import { - EXPECTED_MAJOR, - EXPECTED_MINOR, - commandOK, - badVersion, - commandErr -} from "../devices/actions"; +import { commandOK, badVersion, commandErr } from "../devices/actions"; import { init } from "../api/crud"; import { AuthState } from "../auth/interfaces"; import { autoSync } from "./auto_sync"; @@ -123,7 +117,7 @@ const setBothUp = () => bothUp(); const legacyChecks = (getState: GetState) => { const { controller_version } = getState().bot.hardware.informational_settings; if (HACKY_FLAGS.needVersionCheck && controller_version) { - const IS_OK = versionOK(controller_version, EXPECTED_MAJOR, EXPECTED_MINOR); + const IS_OK = versionOK(controller_version); if (!IS_OK) { badVersion(); } HACKY_FLAGS.needVersionCheck = false; } diff --git a/frontend/connectivity/log_handlers.ts b/frontend/connectivity/log_handlers.ts index 598c52afc..4c4fad2d2 100644 --- a/frontend/connectivity/log_handlers.ts +++ b/frontend/connectivity/log_handlers.ts @@ -3,7 +3,7 @@ import { actOnChannelName, showLogOnScreen, speakLogAloud, - initLog + initLog, } from "./connect_device"; import { GetState } from "../redux/interfaces"; import { Log } from "farmbot/dist/resources/api_resources"; diff --git a/frontend/connectivity/ping_mqtt.tsx b/frontend/connectivity/ping_mqtt.tsx index 0fc71f052..df2c381fb 100644 --- a/frontend/connectivity/ping_mqtt.tsx +++ b/frontend/connectivity/ping_mqtt.tsx @@ -4,7 +4,7 @@ import { dispatchNetworkUp, dispatchQosStart, pingOK, - pingNO + pingNO, } from "./index"; import { isNumber } from "lodash"; import axios from "axios"; diff --git a/frontend/connectivity/reducer.ts b/frontend/connectivity/reducer.ts index 90e322107..81597929a 100644 --- a/frontend/connectivity/reducer.ts +++ b/frontend/connectivity/reducer.ts @@ -2,7 +2,7 @@ import { generateReducer } from "../redux/generate_reducer"; import { Actions } from "../constants"; import { ConnectionState, - EdgeStatus + EdgeStatus, } from "./interfaces"; import { startPing, completePing, failPing } from "../devices/connectivity/qos"; diff --git a/frontend/constants.ts b/frontend/constants.ts index 71388071f..15b21f9a1 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -39,8 +39,8 @@ export namespace ToolTips { few sequences to verify that everything works as expected.`); export const PIN_BINDINGS = - trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is - activated.`); + 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 @@ -51,24 +51,38 @@ export namespace ToolTips { trim(`Diagnose connectivity issues with FarmBot and the browser.`); // Hardware Settings: Homing and Calibration - export const HOMING = + export const HOMING_ENCODERS = trim(`If encoders or end-stops are enabled, home axis (find zero).`); - export const CALIBRATION = + export const HOMING_STALL_DETECTION = + trim(`If stall detection or end-stops are enabled, home axis + (find zero).`); + + export const CALIBRATION_ENCODERS = trim(`If encoders or end-stops are enabled, home axis and determine maximum.`); + export const CALIBRATION_STALL_DETECTION = + trim(`If stall detection or end-stops are enabled, home axis and + determine maximum.`); + export const SET_ZERO_POSITION = trim(`Set the current location as zero.`); - export const FIND_HOME_ON_BOOT = + export const FIND_HOME_ON_BOOT_ENCODERS = trim(`If encoders or end-stops are enabled, find the home position - when the device powers on. - Warning! This will perform homing on all axes when the - device powers on. Encoders or endstops must be enabled. + when the device powers on. Warning! This will perform homing on all + axes when the device powers on. Encoders or endstops must be enabled. It is recommended to make sure homing works properly before enabling this feature. (default: disabled)`); + export const FIND_HOME_ON_BOOT_STALL_DETECTION = + trim(`If stall detection or end-stops are enabled, find the home + position when the device powers on. Warning! This will perform homing + on all axes when the device powers on. Stall detection or endstops + must be enabled. It is recommended to make sure homing works properly + before enabling this feature. (default: disabled)`); + export const STOP_AT_HOME = trim(`Stop at the home location of the axis. (default: disabled)`); @@ -85,18 +99,7 @@ export namespace ToolTips { trim(`Set the length of each axis to provide software limits. Used only if STOP AT MAX is enabled. (default: 0 (disabled))`); - export const TIMEOUT_AFTER = - trim(`Amount of time to wait for a command to execute before stopping. - (default: 120s)`); - // Hardware Settings: Motors - export const MAX_MOVEMENT_RETRIES = - trim(`Number of times to retry a movement before stopping. (default: 3)`); - - export const E_STOP_ON_MOV_ERR = - trim(`Emergency stop if movement is not complete after the maximum - number of retries. (default: disabled)`); - export const MAX_SPEED = trim(`Maximum travel speed after acceleration in millimeters per second. (default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`); @@ -132,18 +135,22 @@ export namespace ToolTips { export const MOTOR_CURRENT = trim(`Motor current in milliamps. (default: 600)`); - export const STALL_SENSITIVITY = - trim(`Motor stall sensitivity. (default: 30)`); - export const ENABLE_X2_MOTOR = trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS. (default: enabled)`); - // Hardware Settings: Encoders and Endstops + // Hardware Settings: Encoders / Stall Detection export const ENABLE_ENCODERS = trim(`Enable use of rotary encoders for stall detection, calibration and homing. (default: enabled)`); + export const ENABLE_STALL_DETECTION = + trim(`Enable use of motor stall detection for detecting missed steps, + calibration and homing. (default: enabled)`); + + export const STALL_SENSITIVITY = + trim(`Motor stall sensitivity. (default: 30)`); + export const ENCODER_POSITIONING = trim(`Use encoders for positioning. (default: disabled)`); @@ -151,17 +158,22 @@ export namespace ToolTips { trim(`Reverse the direction of encoder position reading. (default: disabled)`); - export const MAX_MISSED_STEPS = + export const MAX_MISSED_STEPS_ENCODERS = trim(`Number of steps missed (determined by encoder) before motor is considered to have stalled. (default: 5)`); - export const ENCODER_MISSED_STEP_DECAY = + export const MAX_MISSED_STEPS_STALL_DETECTION = + trim(`Number of steps missed (determined by motor stall detection) before + motor is considered to have stalled. (default: 5)`); + + export const MISSED_STEP_DECAY = trim(`Reduction to missed step total for every good step. (default: 5)`); export const ENCODER_SCALING = trim(`encoder scaling factor = 10000 * (motor resolution * microsteps) / (encoder resolution). (default: 5556 (10000*200/360))`); + // Hardware Settings: Endstops export const ENABLE_ENDSTOPS = trim(`Enable use of electronic end-stops for end detection, calibration and homing. (default: disabled)`); @@ -173,6 +185,18 @@ export namespace ToolTips { trim(`Invert axis end-stops. Enable for normally closed (NC), disable for normally open (NO). (default: disabled)`); + // Hardware Settings: Error Handling + export const TIMEOUT_AFTER = + trim(`Amount of time to wait for a command to execute before stopping. + (default: 120s)`); + + export const MAX_MOVEMENT_RETRIES = + trim(`Number of times to retry a movement before stopping. (default: 3)`); + + export const E_STOP_ON_MOV_ERR = + trim(`Emergency stop if movement is not complete after the maximum + number of retries. (default: disabled)`); + // Hardware Settings: Pin Guard export const PIN_GUARD_PIN_NUMBER = trim(`The number of the pin to guard. This pin will be set to the specified @@ -263,8 +287,12 @@ export namespace ToolTips { export const FIND_HOME = trim(`The Find Home step instructs the device to perform a homing - command (using encoders or endstops) to find and set zero for - the chosen axis or axes.`); + command (using encoders, stall detection, or endstops) to find and set + zero for the chosen axis or axes.`); + + export const CALIBRATE = + trim(`If encoders, stall detection, or end-stops are enabled, + home axis and determine maximum.`); export const IF = trim(`Execute a sequence if a condition is satisfied. If the condition @@ -620,8 +648,8 @@ export namespace Content { trim(`Restart the Farmduino or Arduino firmware.`); export const OS_AUTO_UPDATE = - trim(`When enabled, FarmBot OS will periodically check for, download, - and install updates automatically.`); + 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 @@ -635,7 +663,7 @@ export namespace Content { back on, unplug FarmBot and plug it back in.`); export const OS_BETA_RELEASES = - trim(`Warning! Opting in to FarmBot OS beta releases may reduce + trim(`Warning! Leaving the stable FarmBot OS release channel may reduce FarmBot system stability. Are you sure?`); export const DIAGNOSTIC_CHECK = @@ -674,9 +702,9 @@ export namespace Content { trim(`FarmBot sent a malformed message. You may need to upgrade FarmBot OS. Please upgrade FarmBot OS and log back in.`); - export const OLD_FBOS_REC_UPGRADE = trim(`Your version of FarmBot OS is - outdated and will soon no longer be supported. Please update your device as - soon as possible.`); + export const OLD_FBOS_REC_UPGRADE = + trim(`Your version of FarmBot OS is outdated and will soon no longer + be supported. Please update your device as soon as possible.`); export const EXPERIMENTAL_WARNING = trim(`Warning! This is an EXPERIMENTAL feature. This feature may be @@ -715,8 +743,8 @@ export namespace Content { export const END_DETECTION_DISABLED = trim(`This command will not execute correctly because you do not have - encoders or endstops enabled for the chosen axis. Enable endstops or - encoders from the Device page for: `); + encoders, stall detection, or endstops enabled for the chosen axis. + Enable endstops, encoders, or stall detection from the Device page for: `); export const IN_USE = trim(`Used in another resource. Protected from deletion.`); @@ -784,7 +812,10 @@ export namespace Content { trim(`add this crop on OpenFarm?`); export const NO_TOOLS = - trim(`Press "+" to add a new tool.`); + trim(`Press "+" to add a new tool or seed container.`); + + export const NO_SEED_CONTAINERS = + trim(`Press "+" to add a seed container.`); export const MOUNTED_TOOL = trim(`The tool currently mounted to the UTM can be set here or by using @@ -859,12 +890,23 @@ export namespace TourContent { selecting one, and dragging it into the garden.`); export const ADD_TOOLS = - trim(`Press edit and then the + button to add tools and seed containers.`); + trim(`Press the + button to add tools and seed containers.`); + + export const ADD_SEED_CONTAINERS = + trim(`Press the + button to add seed containers.`); + + export const ADD_TOOLS_AND_SLOTS = + trim(`Press the + button to add tools and seed containers. Then create + slots for them to by pressing the slot + button.`); + + export const ADD_SEED_CONTAINERS_AND_SLOTS = + trim(`Press the + button to add seed containers. Then create + slots for them to by pressing the slot + button.`); export const ADD_TOOLS_SLOTS = trim(`Add the newly created tools and seed containers to the - corresponding tool slots on FarmBot: - press edit and then + to create a tool slot.`); + corresponding slots on FarmBot: + press the + button to create a slot.`); export const ADD_PERIPHERALS = trim(`Press edit and then the + button to add peripherals.`); @@ -902,6 +944,103 @@ export namespace TourContent { trim(`Toggle various settings to customize your web app experience.`); } +export enum DeviceSetting { + // Homing and calibration + homingAndCalibration = `Homing and Calibration`, + homing = `Homing`, + calibration = `Calibration`, + setZeroPosition = `Set Zero Position`, + findHomeOnBoot = `Find Home on Boot`, + stopAtHome = `Stop at Home`, + stopAtMax = `Stop at Max`, + negativeCoordinatesOnly = `Negative Coordinates Only`, + axisLength = `Axis Length (mm)`, + + // Motors + motors = `Motors`, + maxSpeed = `Max Speed (mm/s)`, + homingSpeed = `Homing Speed (mm/s)`, + minimumSpeed = `Minimum Speed (mm/s)`, + accelerateFor = `Accelerate for (mm)`, + stepsPerMm = `Steps per MM`, + microstepsPerStep = `Microsteps per step`, + alwaysPowerMotors = `Always Power Motors`, + invertMotors = `Invert Motors`, + motorCurrent = `Motor Current`, + enable2ndXMotor = `Enable 2nd X Motor`, + invert2ndXMotor = `Invert 2nd X Motor`, + + // Encoders / Stall Detection + encoders = `Encoders`, + stallDetection = `Stall Detection`, + enableEncoders = `Enable Encoders`, + enableStallDetection = `Enable Stall Detection`, + stallSensitivity = `Stall Sensitivity`, + useEncodersForPositioning = `Use Encoders for Positioning`, + invertEncoders = `Invert Encoders`, + maxMissedSteps = `Max Missed Steps`, + missedStepDecay = `Missed Step Decay`, + encoderScaling = `Encoder Scaling`, + + // Endstops + endstops = `Endstops`, + enableEndstops = `Enable Endstops`, + swapEndstops = `Swap Endstops`, + invertEndstops = `Invert Endstops`, + + // Error handling + errorHandling = `Error Handling`, + timeoutAfter = `Timeout after (seconds)`, + maxRetries = `Max Retries`, + estopOnMovementError = `E-Stop on Movement Error`, + + // Pin Guard + pinGuard = `Pin Guard`, + + // Danger Zone + dangerZone = `Danger Zone`, + resetHardwareParams = `Reset hardware parameter defaults`, + + // Pin Bindings + pinBindings = `Pin Bindings`, + + // FarmBot OS + farmbot = `FarmBot`, + name = `name`, + timezone = `timezone`, + camera = `camera`, + firmware = `Firmware`, + applySoftwareUpdates = `update time`, + farmbotOSAutoUpdate = `auto update`, + farmbotOS = `Farmbot OS`, + autoSync = `Auto Sync`, + bootSequence = `Boot Sequence`, + + // Power and Reset + powerAndReset = `Power and Reset`, + restartFarmbot = `Restart Farmbot`, + shutdownFarmbot = `Shutdown Farmbot`, + restartFirmware = `Restart Firmware`, + factoryReset = `Factory Reset`, + autoFactoryReset = `Automatic Factory Reset`, + connectionAttemptPeriod = `Connection Attempt Period`, + changeOwnership = `Change Ownership`, + + // Farm Designer + farmDesigner = `Farm Designer`, + animations = `Plant animations`, + trail = `Virtual FarmBot trail`, + dynamicMap = `Dynamic map size`, + mapSize = `Map size`, + rotateMap = `Rotate map`, + mapOrigin = `Map origin`, + confirmPlantDeletion = `Confirm plant deletion`, + + // Firmware + firmwareSection = `Firmware`, + flashFirmware = `Flash firmware`, +} + export namespace DiagnosticMessages { export const OK = trim(`All systems nominal.`); @@ -924,8 +1063,7 @@ export namespace DiagnosticMessages { but we have no recent record of FarmBot connecting to the internet. This usually happens because of poor WiFi connectivity in the garden, a bad password during configuration, a very long power outage, or - blocked ports on FarmBot's local network. Please refer IT staff to - https://software.farm.bot/docs/for-it-security-professionals`); + blocked ports on FarmBot's local network. Please refer IT staff to:`); export const NO_WS_AVAILABLE = trim(`You are either offline, using a web browser that does not support WebSockets, or are behind a firewall that diff --git a/frontend/controls/__tests__/controls_test.tsx b/frontend/controls/__tests__/controls_test.tsx index 15462ac06..aba980723 100644 --- a/frontend/controls/__tests__/controls_test.tsx +++ b/frontend/controls/__tests__/controls_test.tsx @@ -3,7 +3,7 @@ import { mount } from "enzyme"; import { RawControls as Controls } from "../controls"; import { bot } from "../../__test_support__/fake_state/bot"; import { - fakePeripheral, fakeWebcamFeed, fakeSensor + fakePeripheral, fakeWebcamFeed, fakeSensor, } from "../../__test_support__/fake_state/resources"; import { Dictionary } from "farmbot"; import { Props } from "../interfaces"; diff --git a/frontend/controls/axis_display_group.tsx b/frontend/controls/axis_display_group.tsx index 601e8b812..13583749c 100644 --- a/frontend/controls/axis_display_group.tsx +++ b/frontend/controls/axis_display_group.tsx @@ -3,17 +3,19 @@ import { Row, Col } from "../ui/index"; import { AxisDisplayGroupProps } from "./interfaces"; import { isNumber } from "lodash"; import { t } from "../i18next_wrapper"; +import { Xyz } from "farmbot"; -const Axis = ({ val }: { val: number | undefined }) => - -; +const Axis = ({ axis, val }: { val: number | undefined, axis: Xyz }) => + + + ; export const AxisDisplayGroup = ({ position, label }: AxisDisplayGroupProps) => { const { x, y, z } = position; return - - - + + +
  • Perform a "hard refresh" (CTRL + SHIFT + R on most machines).
  • Session.clear()}>Log out by clicking here.
  • Send the error information (below) to our developer team via the - FarmBot software + FarmBot software forum. Including additional information (such as steps leading up to the error) help us identify solutions more quickly.
  • diff --git a/frontend/css/_blueprint_overrides.scss b/frontend/css/_blueprint_overrides.scss index 391d11aa9..039e5add0 100644 --- a/frontend/css/_blueprint_overrides.scss +++ b/frontend/css/_blueprint_overrides.scss @@ -1,5 +1,6 @@ // Padding for the popups. .bp3-popover-content { + z-index: 999; padding: 1rem; } diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index 74d11b8af..bb34b097a 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -30,6 +30,9 @@ padding: 35rem 2rem 2rem 2rem; // at zoom = 1.0: 350px 20px 20px 20px } transition: 0.2s ease; + &::-webkit-scrollbar { + display: none; + } } .drop-area { diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index a1867cc96..affd0f7d0 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -552,8 +552,12 @@ } .tool-slots-panel-content, .tools-panel-content { + max-height: calc(100vh - 19rem); + overflow-y: auto; + overflow-x: hidden; .tool-search-item, .tool-slot-search-item { + line-height: 4rem; cursor: pointer; margin-left: -15px; margin-right: -15px; @@ -562,11 +566,32 @@ margin-right: 0; } p { - line-height: 3rem; + font-size: 1.2rem; + line-height: 4rem; + &.tool-status, &.tool-slot-position { float: right; } } + .filter-search { + .bp3-button { + min-height: 2.5rem; + max-height: 2.5rem; + span { + line-height: 1.5rem; + } + } + i { + line-height: 2rem; + } + } + svg { + vertical-align: middle; + } + .tool-slot-position-info { + padding: 0; + padding-right: 1rem; + } } .mounted-tool-header { display: flex; @@ -580,6 +605,7 @@ font-size: 1.4rem; } } + .tools-header, .tool-slots-header { display: flex; margin-top: 1rem; @@ -623,10 +649,42 @@ float: left; } } + svg { + display: block; + margin: auto; + width: 10rem; + height: 10rem; + margin-top: 2rem; + } .add-stock-tools { + .filter-search { + margin-bottom: 1rem; + button { + margin-top: 0.2rem; + } + } ul { font-size: 1.2rem; padding-left: 1rem; + li { + margin-top: 0.5rem; + line-height: 2rem; + cursor: pointer; + width: 50%; + &:hover { + font-weight: bold; + } + .fb-checkbox { + display: inline; + } + p { + display: inline; + line-height: 2.25rem; + font-size: 1.2rem; + vertical-align: top; + margin-left: 1rem; + } + } } button { .fa-plus { @@ -638,6 +696,13 @@ .add-tool-slot-panel-content, .edit-tool-slot-panel-content { + svg { + display: block; + margin: auto; + width: 10rem; + height: 10rem; + margin-top: 2rem; + } label { margin-top: 0 !important; } @@ -650,12 +715,24 @@ .direction-icon { margin-left: 1rem; } - .use-current-location-input { + .help-icon { + color: $dark_gray; + } + .tool-slot-location-input { + .axis-inputs { + padding-left: 0; + } + .use-current-location { + padding: 0; + margin-left: -1rem; + } button { - margin: 0; - float: none; - margin-left: 1rem; - vertical-align: middle; + margin-top: 0.5rem; + margin-right: 0.5rem; + height: 2.5rem; + .fa { + font-size: 1.5rem; + } } } .gantry-mounted-input { @@ -868,7 +945,9 @@ margin: 0; } } -.clear-button { +.preview-button, +.cancel-button, +.save-button { text-transform: uppercase; font-size: 1rem; border: 1px solid; @@ -881,3 +960,10 @@ margin-right: 1.5rem; &:hover { color: $white; } } + +.desktop-hide { + display: none !important; + @media screen and (max-width: 1075px) { + display: block !important; + } +} diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 8e320263b..9b63e725d 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -226,7 +226,7 @@ fieldset { .percent-bar { position: absolute; top: 2px; - left: 12rem; + right: 0; height: 1rem; width: 25%; clip-path: polygon(0 85%, 100% 0, 100% 100%, 0% 100%); @@ -407,6 +407,18 @@ a { } } +.load-progress-bar-wrapper { + position: absolute; + top: 3.2rem; + bottom: 0; + right: 0; + width: 100%; + height: 1px; + .load-progress-bar { + height: 100%; + } +} + .firmware-setting-export-menu { button { margin-bottom: 1rem; @@ -433,14 +445,17 @@ a { } } -.pin-bindings-widget { +.pin-bindings { .fa-exclamation-triangle { color: $orange; + margin-left: 1rem; + margin-top: 0.75rem; } .fa-th-large { + position: absolute; + top: 0.75rem; + left: 0.5rem; color: $dark_gray; - margin-top: 0.5rem; - margin-left: 0.5rem; } .fb-button { &.green { @@ -449,16 +464,27 @@ a { } .bindings-list { margin-bottom: 1rem; + margin-left: 1rem; font-size: 1.2rem; } + .binding-type-dropdown { + margin-bottom: 1.5rem; + } .stock-pin-bindings-button { button { - margin: 0 !important; + margin: 1rem; + float: left; + margin-left: 2rem; } i { margin-right: 0.5rem; } } + .bp3-popover-wrapper { + display: inline; + float: none !important; + margin-left: 1rem; + } } .sensor-history-widget { @@ -1308,6 +1334,12 @@ ul { } } +.boolean-camera-calibration-config { + input[type=checkbox] { + display: block; + } +} + .tour-list { margin: auto; max-width: 300px; @@ -1519,16 +1551,29 @@ textarea:focus { box-shadow: 0 0 10px rgba(0,0,0,.2); } -.sort-path-info-bar { - background: lightgray; +.sort-option-bar { cursor: pointer; - font-size: 1.1rem; margin-top: 0.25rem; margin-bottom: 0.25rem; - white-space: nowrap; - line-height: 1.75rem; + border: 2px solid darken($panel_light_blue, 30%); + border-radius: 5px; + &:hover, &.selected { + .sort-path-info-bar { + background: darken($panel_light_blue, 40%); + } + } &:hover { - background: darken(lightgray, 10%); + border: 2px solid darken($panel_light_blue, 40%); + } + &.selected { + border: 2px solid $medium_gray; + } + .sort-path-info-bar { + background: darken($panel_light_blue, 30%); + font-size: 1.2rem; + padding-left: 0.5rem; + white-space: nowrap; + line-height: 2.5rem; } } @@ -1601,3 +1646,29 @@ textarea:focus { } } } + +.section { + display: block !important; +} + +.highlight, +.unhighlight { + display: flex; +} + +.highlight { + background-color: $light_yellow; + box-shadow: 0px 0px 7px 4px $light_yellow; +} + +.unhighlight { + transition: background-color 10s linear, box-shadow 10s linear; + background-color: transparent; + box-shadow: none; +} + +.read-only-icon { + margin: 9px 0px 0px 9px; + float: right; + box-sizing: inherit; +} diff --git a/frontend/css/image_flipper.scss b/frontend/css/image_flipper.scss index 48573d577..99c9f8747 100644 --- a/frontend/css/image_flipper.scss +++ b/frontend/css/image_flipper.scss @@ -97,6 +97,7 @@ } } +.camera-calibration, .weed-detector{ .farmware-button{ position: relative; diff --git a/frontend/css/inputs.scss b/frontend/css/inputs.scss index e31b27443..9326c1a44 100644 --- a/frontend/css/inputs.scss +++ b/frontend/css/inputs.scss @@ -154,4 +154,12 @@ select { } } } + &.disabled { + input[type="checkbox"] { + cursor: not-allowed; + &:checked:after { + border-color: $gray; + } + } + } } diff --git a/frontend/css/sequences.scss b/frontend/css/sequences.scss index be3cdff30..83472c0e4 100644 --- a/frontend/css/sequences.scss +++ b/frontend/css/sequences.scss @@ -322,6 +322,9 @@ border-left: 4px solid transparent; &.active { border-left: 4px solid $dark_gray; + p { + font-weight: bold; + } } .fa-chevron-down, .fa-chevron-right { position: absolute; @@ -330,11 +333,11 @@ font-size: 1.1rem; } .folder-settings-icon, - .fa-bars { + .fa-arrows-v { position: absolute; right: 0; } - .fa-bars, .fa-ellipsis-v { + .fa-arrows-v, .fa-ellipsis-v { display: none; } .fa-ellipsis-v { @@ -342,8 +345,14 @@ display: block; } } + @media screen and (max-width: 450px) { + .fa-arrows-v, .fa-ellipsis-v { + display: block; + margin-right: 0.5rem; + } + } &:hover { - .fa-bars, .fa-ellipsis-v { + .fa-arrows-v, .fa-ellipsis-v { display: block; } } @@ -367,7 +376,7 @@ white-space: nowrap; text-overflow: ellipsis; font-size: 1.2rem; - font-weight: bold; + font-weight: normal; width: 75%; padding: 0.5rem; padding-left: 0; diff --git a/frontend/demo/demo_iframe.tsx b/frontend/demo/demo_iframe.tsx index e0f5ba72c..54824f796 100644 --- a/frontend/demo/demo_iframe.tsx +++ b/frontend/demo/demo_iframe.tsx @@ -2,16 +2,14 @@ import { connect, MqttClient } from "mqtt"; import React from "react"; import { uuid } from "farmbot"; import axios from "axios"; +import { ExternalUrl } from "../external_urls"; +import { t } from "../i18next_wrapper"; interface State { error: Error | undefined; stage: string; } -const VIDEO_URL = - "https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"; -const PHONE_URL = - "https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"; const WS_CONFIG = { username: "farmbot_demo", password: "required, but not used.", @@ -26,7 +24,7 @@ export const WAITING_ON_API = "Planting your demo garden..."; // APPLICATION CODE ============================== export class DemoIframe extends React.Component<{}, State> { state: State = - { error: undefined, stage: "DEMO THE APP" }; + { error: undefined, stage: t("DEMO THE APP") }; setError = (error?: Error) => this.setState({ error }); @@ -63,10 +61,12 @@ export class DemoIframe extends React.Component<{}, State> { return
    - -
    ; diff --git a/frontend/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts index ad82f57be..74cf244be 100644 --- a/frontend/devices/__tests__/actions_test.ts +++ b/frontend/devices/__tests__/actions_test.ts @@ -32,11 +32,11 @@ jest.mock("axios", () => ({ get: jest.fn(() => mockGetRelease) })); import * as actions from "../actions"; import { - fakeFirmwareConfig, fakeFbosConfig + fakeFirmwareConfig, fakeFbosConfig, } from "../../__test_support__/fake_state/resources"; import { fakeState } from "../../__test_support__/fake_state"; import { - changeStepSize, commandErr + changeStepSize, commandErr, } from "../actions"; import { Actions } from "../../constants"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; @@ -307,7 +307,7 @@ describe("commandErr()", () => { }); }); -describe("toggleControlPanel()", function () { +describe("toggleControlPanel()", () => { it("toggles", () => { const action = actions.toggleControlPanel("homing_and_calibration"); expect(action.payload).toEqual("homing_and_calibration"); @@ -353,9 +353,10 @@ describe("fetchReleases()", () => { it("fails to fetches latest OS release version", async () => { mockGetRelease = Promise.reject("error"); const dispatch = jest.fn(); + console.error = jest.fn(); await actions.fetchReleases("url")(dispatch); await expect(axios.get).toHaveBeenCalledWith("url"); - expect(error).toHaveBeenCalledWith( + expect(console.error).toHaveBeenCalledWith( "Could not download FarmBot OS update information."); expect(dispatch).toHaveBeenCalledWith({ payload: "error", diff --git a/frontend/devices/__tests__/devices_test.tsx b/frontend/devices/__tests__/devices_test.tsx index e4ae06027..2ab700000 100644 --- a/frontend/devices/__tests__/devices_test.tsx +++ b/frontend/devices/__tests__/devices_test.tsx @@ -7,7 +7,7 @@ import { Props } from "../interfaces"; import { auth } from "../../__test_support__/fake_state/token"; import { bot } from "../../__test_support__/fake_state/bot"; import { - fakeDevice, buildResourceIndex, FAKE_RESOURCES + fakeDevice, buildResourceIndex, FAKE_RESOURCES, } from "../../__test_support__/resource_index_builder"; import { FarmbotOsSettings } from "../components/farmbot_os_settings"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; diff --git a/frontend/devices/__tests__/state_to_props_test.tsx b/frontend/devices/__tests__/state_to_props_test.tsx index 130b5ab5c..1fb47e404 100644 --- a/frontend/devices/__tests__/state_to_props_test.tsx +++ b/frontend/devices/__tests__/state_to_props_test.tsx @@ -2,7 +2,7 @@ import { fakeFbosConfig, fakeImage, fakeFarmwareEnv, - fakeWebAppConfig + fakeWebAppConfig, } from "../../__test_support__/fake_state/resources"; let mockFbosConfig: TaggedFbosConfig | undefined = fakeFbosConfig(); @@ -35,7 +35,6 @@ describe("mapStateToProps()", () => { it("uses the API as the source of FBOS settings", () => { const fakeApiConfig = fakeFbosConfig(); fakeApiConfig.body.auto_sync = true; - fakeApiConfig.body.api_migrated = true; mockFbosConfig = fakeApiConfig; const props = mapStateToProps(fakeState()); expect(props.sourceFbosConfig("auto_sync")).toEqual({ @@ -53,19 +52,6 @@ describe("mapStateToProps()", () => { }); }); - it("uses the bot as the source of FBOS settings: ignore API defaults", () => { - const state = fakeState(); - state.bot.hardware.configuration.auto_sync = false; - const fakeApiConfig = fakeFbosConfig(); - fakeApiConfig.body.auto_sync = true; - fakeApiConfig.body.api_migrated = false; - mockFbosConfig = fakeApiConfig; - const props = mapStateToProps(state); - expect(props.sourceFbosConfig("auto_sync")).toEqual({ - value: false, consistent: true - }); - }); - it("returns API Farmware env vars", () => { const state = fakeState(); state.bot.hardware.user_env = {}; diff --git a/frontend/devices/__tests__/update_interceptor_test.ts b/frontend/devices/__tests__/update_interceptor_test.ts index 9a939c5ab..034f92133 100644 --- a/frontend/devices/__tests__/update_interceptor_test.ts +++ b/frontend/devices/__tests__/update_interceptor_test.ts @@ -3,7 +3,7 @@ import { lessThan, mcuParamValidator, OK, - McuErrors + McuErrors, } from "../update_interceptor"; describe("greaterThan() and lessThan()", () => { diff --git a/frontend/devices/actions.ts b/frontend/devices/actions.ts index 30cbcaab6..ebec5bf00 100644 --- a/frontend/devices/actions.ts +++ b/frontend/devices/actions.ts @@ -3,13 +3,13 @@ import { success, warning, info, error } from "../toast/toast"; import { getDevice } from "../device"; import { Everything } from "../interfaces"; import { - GithubRelease, MoveRelProps, MinOsFeatureLookup, SourceFwConfig, Axis + GithubRelease, MoveRelProps, MinOsFeatureLookup, SourceFwConfig, Axis, } from "./interfaces"; import { Thunk } from "../redux/interfaces"; import { McuParams, TaggedFirmwareConfig, ParameterApplication, ALLOWED_PIN_MODES, - FirmwareHardware + FirmwareHardware, } from "farmbot"; import { ControlPanelState } from "../devices/interfaces"; import { oneOf, versionOK, trim } from "../util"; @@ -26,11 +26,6 @@ import { t } from "../i18next_wrapper"; const ON = 1, OFF = 0; export type ConfigKey = keyof McuParams; -export const EXPECTED_MAJOR = 6; -export const EXPECTED_MINOR = 0; -export const FEATURE_MIN_VERSIONS_URL = - "https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" + - "FEATURE_MIN_VERSIONS.json"; // Already filtering messages in FarmBot OS and the API- this is just for // an additional layer of safety. const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"]; @@ -132,7 +127,7 @@ export function sync(): Thunk { return function (_dispatch, getState) { const currentFBOSversion = getState().bot.hardware.informational_settings.controller_version; - const IS_OK = versionOK(currentFBOSversion, EXPECTED_MAJOR, EXPECTED_MINOR); + const IS_OK = versionOK(currentFBOSversion); if (IS_OK) { getDevice() .sync() @@ -149,7 +144,7 @@ export function sync(): Thunk { export function execSequence( sequenceId: number | undefined, - bodyVariables?: ParameterApplication[] + bodyVariables?: ParameterApplication[], ) { const noun = t("Sequence execution"); if (sequenceId) { @@ -217,7 +212,7 @@ export const fetchReleases = }) .catch((ferror) => { !options.beta && - error(t("Could not download FarmBot OS update information.")); + console.error(t("Could not download FarmBot OS update information.")); dispatch({ type: options.beta ? "FETCH_BETA_OS_UPDATE_INFO_ERROR" @@ -290,13 +285,13 @@ export function MCUFactoryReset() { /** Toggle a firmware setting. */ export function settingToggle( - name: ConfigKey, + key: ConfigKey, sourceFwConfig: SourceFwConfig, - displayAlert?: string | undefined + displayAlert?: string | undefined, ) { return function (dispatch: Function, getState: () => Everything) { if (displayAlert) { alert(trim(displayAlert)); } - const update = { [name]: (sourceFwConfig(name).value === 0) ? ON : OFF }; + const update = { [key]: (sourceFwConfig(key).value === 0) ? ON : OFF }; const firmwareConfig = getFirmwareConfig(getState().resources.index); const toggleFirmwareConfig = (fwConfig: TaggedFirmwareConfig) => { dispatch(edit(fwConfig, update)); @@ -330,7 +325,7 @@ export function pinToggle(pin_number: number) { } export function readPin( - pin_number: number, label: string, pin_mode: ALLOWED_PIN_MODES + pin_number: number, label: string, pin_mode: ALLOWED_PIN_MODES, ) { const noun = t("Read pin"); return getDevice() diff --git a/frontend/devices/components/__tests__/axis_tracking_status_test.tsx b/frontend/devices/components/__tests__/axis_tracking_status_test.tsx index b33c9896f..d1489c858 100644 --- a/frontend/devices/components/__tests__/axis_tracking_status_test.tsx +++ b/frontend/devices/components/__tests__/axis_tracking_status_test.tsx @@ -1,5 +1,5 @@ import { - axisTrackingStatus, disabledAxisMap, enabledAxisMap + axisTrackingStatus, disabledAxisMap, enabledAxisMap, } from "../axis_tracking_status"; import { bot } from "../../../__test_support__/fake_state/bot"; @@ -16,7 +16,7 @@ const expected = { "axis": "z", "disabled": true - } + }, ]; describe("axisTrackingStatus()", () => { diff --git a/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx b/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx index eaf610a0b..505650d38 100644 --- a/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx +++ b/frontend/devices/components/__tests__/boolean_mcu_input_group_test.tsx @@ -7,13 +7,14 @@ import { ToggleButton } from "../../../controls/toggle_button"; import { settingToggle } from "../../actions"; import { bot } from "../../../__test_support__/fake_state/bot"; import { BooleanMCUInputGroupProps } from "../interfaces"; +import { DeviceSetting } from "../../../constants"; describe("BooleanMCUInputGroup", () => { const fakeProps = (): BooleanMCUInputGroupProps => ({ sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }), dispatch: jest.fn(), tooltip: "Tooltip", - name: "Name", + label: DeviceSetting.invertEncoders, x: "encoder_invert_x", y: "encoder_invert_y", z: "encoder_invert_z", diff --git a/frontend/devices/components/__tests__/bot_config_input_box_test.tsx b/frontend/devices/components/__tests__/bot_config_input_box_test.tsx index 5fc0e766a..5b6e3c1ed 100644 --- a/frontend/devices/components/__tests__/bot_config_input_box_test.tsx +++ b/frontend/devices/components/__tests__/bot_config_input_box_test.tsx @@ -9,7 +9,7 @@ import { BotConfigInputBox, BotConfigInputBoxProps } from "../bot_config_input_b import { fakeState } from "../../../__test_support__/fake_state"; import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { edit, save } from "../../../api/crud"; diff --git a/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx b/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx index 917caaead..757cc3b56 100644 --- a/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx +++ b/frontend/devices/components/__tests__/farmbot_os_settings_test.tsx @@ -22,6 +22,8 @@ import axios from "axios"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { edit } from "../../../api/crud"; import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources"; +import { formEvent } from "../../../__test_support__/fake_html_events"; +import { Content } from "../../../constants"; describe("", () => { beforeEach(() => { @@ -54,8 +56,8 @@ describe("", () => { const osSettings = mount(); expect(osSettings.find("input").length).toBe(1); expect(osSettings.find("button").length).toBe(7); - ["NAME", "TIME ZONE", "FARMBOT OS", "CAMERA", "FIRMWARE"] - .map(string => expect(osSettings.text()).toContain(string)); + ["name", "time zone", "farmbot os", "camera", "firmware"] + .map(string => expect(osSettings.text().toLowerCase()).toContain(string)); }); it("fetches OS release notes", async () => { @@ -115,4 +117,18 @@ describe("", () => { const osSettings = shallow(); expect(osSettings.find("BootSequenceSelector").length).toEqual(1); }); + + it("prevents default form submit action", () => { + const osSettings = shallow(); + const e = formEvent(); + osSettings.find("form").simulate("submit", e); + expect(e.preventDefault).toHaveBeenCalled(); + }); + + it("warns about timezone mismatch", () => { + const p = fakeProps(); + p.deviceAccount.body.timezone = "different"; + const osSettings = mount(); + expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING); + }); }); diff --git a/frontend/devices/components/__tests__/firmware_hardware_support_test.ts b/frontend/devices/components/__tests__/firmware_hardware_support_test.ts index e7f5e2a4a..46ddb33f8 100644 --- a/frontend/devices/components/__tests__/firmware_hardware_support_test.ts +++ b/frontend/devices/components/__tests__/firmware_hardware_support_test.ts @@ -1,4 +1,5 @@ -import { boardType } from "../firmware_hardware_support"; +import { boardType, getFwHardwareValue } from "../firmware_hardware_support"; +import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources"; describe("boardType()", () => { it("returns Farmduino", () => { @@ -32,3 +33,18 @@ describe("boardType()", () => { expect(boardType("none")).toEqual("none"); }); }); + +describe("getFwHardwareValue()", () => { + it("returns undefined", () => { + const fbosConfig = fakeFbosConfig(); + fbosConfig.body.firmware_hardware = "wrong"; + expect(getFwHardwareValue(fbosConfig)).toEqual(undefined); + expect(getFwHardwareValue(undefined)).toEqual(undefined); + }); + + it("returns real value", () => { + const fbosConfig = fakeFbosConfig(); + fbosConfig.body.firmware_hardware = "express_k10"; + expect(getFwHardwareValue(fbosConfig)).toEqual("express_k10"); + }); +}); diff --git a/frontend/devices/components/__tests__/hardware_settings_test.tsx b/frontend/devices/components/__tests__/hardware_settings_test.tsx index 02e7a3001..90120882f 100644 --- a/frontend/devices/components/__tests__/hardware_settings_test.tsx +++ b/frontend/devices/components/__tests__/hardware_settings_test.tsx @@ -6,12 +6,14 @@ import { Actions } from "../../../constants"; import { bot } from "../../../__test_support__/fake_state/bot"; import { panelState } from "../../../__test_support__/control_panel_state"; import { - fakeFirmwareConfig + fakeFirmwareConfig, } from "../../../__test_support__/fake_state/resources"; import { clickButton } from "../../../__test_support__/helpers"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; +import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; +import { Color } from "../../../ui"; describe("", () => { const fakeProps = (): HardwareSettingsProps => ({ @@ -29,7 +31,7 @@ describe("", () => { it("renders", () => { const wrapper = mount(); - ["expand all", "x axis", "motors"].map(string => + ["expand all", "motors"].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); @@ -65,13 +67,44 @@ describe("", () => { it("shows param export menu", () => { const p = fakeProps(); p.firmwareConfig = fakeFirmwareConfig().body; - p.firmwareConfig.api_migrated = true; const wrapper = shallow(); expect(wrapper.html()).toContain("fa-download"); }); - it("doesn't show param export menu", () => { - const wrapper = shallow(); - expect(wrapper.html()).not.toContain("fa-download"); + it("shows setting load progress", () => { + type ConsistencyLookup = Record; + const consistent: Partial = + ({ id: false, encoder_invert_x: true, encoder_enabled_y: false }); + const consistencyLookup = consistent as ConsistencyLookup; + const p = fakeProps(); + const fakeConfig: Partial = + ({ id: 0, encoder_invert_x: 1, encoder_enabled_y: 0 }); + p.firmwareConfig = fakeConfig as FirmwareConfig; + p.sourceFwConfig = x => + ({ value: p.firmwareConfig?.[x], consistent: consistencyLookup[x] }); + const wrapper = mount(); + const barStyle = wrapper.find(".load-progress-bar").props().style; + expect(barStyle?.background).toEqual(Color.white); + expect(barStyle?.width).toEqual("50%"); + }); + + it("shows setting load progress: 0%", () => { + const p = fakeProps(); + p.firmwareConfig = fakeFirmwareConfig().body; + p.sourceFwConfig = () => ({ value: 0, consistent: false }); + const wrapper = mount(); + const barStyle = wrapper.find(".load-progress-bar").props().style; + expect(barStyle?.width).toEqual("0%"); + expect(barStyle?.background).toEqual(Color.darkGray); + }); + + it("shows setting load progress: 100%", () => { + const p = fakeProps(); + p.firmwareConfig = fakeFirmwareConfig().body; + p.sourceFwConfig = () => ({ value: 0, consistent: true }); + const wrapper = mount(); + const barStyle = wrapper.find(".load-progress-bar").props().style; + expect(barStyle?.width).toEqual("100%"); + expect(barStyle?.background).toEqual(Color.darkGray); }); }); diff --git a/frontend/devices/components/__tests__/maybe_highlight_test.tsx b/frontend/devices/components/__tests__/maybe_highlight_test.tsx new file mode 100644 index 000000000..2bf800e6a --- /dev/null +++ b/frontend/devices/components/__tests__/maybe_highlight_test.tsx @@ -0,0 +1,81 @@ +jest.mock("../../actions", () => ({ + toggleControlPanel: jest.fn(), +})); + +import * as React from "react"; +import { mount } from "enzyme"; +import { + Highlight, HighlightProps, maybeHighlight, maybeOpenPanel, highlight, +} from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; +import { panelState } from "../../../__test_support__/control_panel_state"; +import { toggleControlPanel } from "../../actions"; + +describe("", () => { + const fakeProps = (): HighlightProps => ({ + settingName: DeviceSetting.motors, + children:
    , + className: "section", + }); + + it("fades highlight", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.setState({ className: "highlight" }); + wrapper.instance().componentDidMount(); + expect(wrapper.state().className).toEqual("unhighlight"); + }); +}); + +describe("maybeHighlight()", () => { + beforeEach(() => { + highlight.opened = false; + highlight.highlighted = false; + }); + + it("highlights only once", () => { + location.search = "?highlight=motors"; + expect(maybeHighlight(DeviceSetting.motors)).toEqual("highlight"); + expect(maybeHighlight(DeviceSetting.motors)).toEqual(""); + }); + + it("doesn't highlight: different setting", () => { + location.search = "?highlight=name"; + expect(maybeHighlight(DeviceSetting.motors)).toEqual(""); + }); + + it("doesn't highlight: no matches", () => { + location.search = "?highlight=na"; + expect(maybeHighlight(DeviceSetting.motors)).toEqual(""); + }); +}); + +describe("maybeOpenPanel()", () => { + beforeEach(() => { + highlight.opened = false; + highlight.highlighted = false; + }); + + it("opens panel only once", () => { + location.search = "?highlight=motors"; + maybeOpenPanel(panelState())(jest.fn()); + expect(toggleControlPanel).toHaveBeenCalledWith("motors"); + jest.resetAllMocks(); + maybeOpenPanel(panelState())(jest.fn()); + expect(toggleControlPanel).not.toHaveBeenCalled(); + }); + + it("doesn't open panel: already open", () => { + location.search = "?highlight=motors"; + const panels = panelState(); + panels.motors = true; + maybeOpenPanel(panels)(jest.fn()); + expect(toggleControlPanel).not.toHaveBeenCalled(); + }); + + it("doesn't open panel: no search term", () => { + location.search = ""; + maybeOpenPanel(panelState())(jest.fn()); + expect(toggleControlPanel).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx b/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx index 1cc9d7d80..bfdce94ea 100644 --- a/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx +++ b/frontend/devices/components/__tests__/pin_guard_input_group_test.tsx @@ -7,13 +7,13 @@ import { PinGuardMCUInputGroupProps } from "../interfaces"; import { bot } from "../../../__test_support__/fake_state/bot"; import { settingToggle } from "../../actions"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; describe("", () => { const fakeProps = (): PinGuardMCUInputGroupProps => { return { - name: "Pin Guard 1", + label: "Pin Guard 1", pinNumKey: "pin_guard_1_pin_nr", timeoutKey: "pin_guard_1_time_out", activeStateKey: "pin_guard_1_active_state", diff --git a/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx b/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx index d8f8a8647..d4bdc6bda 100644 --- a/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx +++ b/frontend/devices/components/__tests__/pin_number_dropdown_test.tsx @@ -5,10 +5,10 @@ import { mount, shallow } from "enzyme"; import { PinNumberDropdown } from "../pin_number_dropdown"; import { PinGuardMCUInputGroupProps } from "../interfaces"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { - fakeFirmwareConfig, fakePeripheral + fakeFirmwareConfig, fakePeripheral, } from "../../../__test_support__/fake_state/resources"; import { TaggedFirmwareConfig } from "farmbot"; import { FBSelect } from "../../../ui"; @@ -17,7 +17,7 @@ import { updateMCU } from "../../actions"; describe("", () => { const fakeProps = (firmwareConfig?: TaggedFirmwareConfig): PinGuardMCUInputGroupProps => ({ - name: "Pin Guard 1", + label: "Pin Guard 1", pinNumKey: "pin_guard_1_pin_nr", timeoutKey: "pin_guard_1_time_out", activeStateKey: "pin_guard_1_active_state", diff --git a/frontend/devices/components/__tests__/source_config_value_test.ts b/frontend/devices/components/__tests__/source_config_value_test.ts index 17f543729..4ea998d6a 100644 --- a/frontend/devices/components/__tests__/source_config_value_test.ts +++ b/frontend/devices/components/__tests__/source_config_value_test.ts @@ -2,7 +2,7 @@ import { sourceFbosConfigValue, sourceFwConfigValue } from "../source_config_val import { bot } from "../../../__test_support__/fake_state/bot"; import { fakeFbosConfig, - fakeFirmwareConfig + fakeFirmwareConfig, } from "../../../__test_support__/fake_state/resources"; describe("sourceFbosConfigValue()", () => { diff --git a/frontend/devices/components/boolean_mcu_input_group.tsx b/frontend/devices/components/boolean_mcu_input_group.tsx index cd223864c..710fafa31 100644 --- a/frontend/devices/components/boolean_mcu_input_group.tsx +++ b/frontend/devices/components/boolean_mcu_input_group.tsx @@ -4,12 +4,14 @@ import { settingToggle } from "../actions"; import { Row, Col, Help } from "../../ui/index"; import { BooleanMCUInputGroupProps } from "./interfaces"; import { Position } from "@blueprintjs/core"; +import { t } from "../../i18next_wrapper"; +import { Highlight } from "./maybe_highlight"; export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { const { tooltip, - name, + label, x, y, z, @@ -26,40 +28,42 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { const zParam = sourceFwConfig(z); return - - - - - - - dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> - - - - dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> - - - - dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> - + + + + + + + + dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> + + + + dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> + + + + dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> + + ; } diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index 9fceb9231..d31d066dc 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -4,8 +4,8 @@ import { t } from "../../i18next_wrapper"; import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces"; import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui"; import { save, edit } from "../../api/crud"; -import { MustBeOnline, isBotOnline } from "../must_be_online"; -import { Content } from "../../constants"; +import { isBotOnline } from "../must_be_online"; +import { Content, DeviceSetting } from "../../constants"; import { TimezoneSelector } from "../timezones/timezone_selector"; import { timezoneMismatch } from "../timezones/guess_timezone"; import { CameraSelection } from "./fbos_settings/camera_selection"; @@ -15,6 +15,9 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row"; import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector"; +import { ExternalUrl } from "../../external_urls"; +import { Highlight } from "./maybe_highlight"; +import { OtaTimeSelectorRow } from "./fbos_settings/ota_time_selector"; export enum ColWidth { label = 3, @@ -22,15 +25,12 @@ export enum ColWidth { button = 2 } -const OS_RELEASE_NOTES_URL = - "https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md"; - export class FarmbotOsSettings extends React.Component { state: FarmbotOsState = { allOsReleaseNotes: "" }; componentDidMount() { - this.fetchReleaseNotes(OS_RELEASE_NOTES_URL); + this.fetchReleaseNotes(ExternalUrl.osReleaseNotes); } get osMajorVersion() { @@ -79,91 +79,86 @@ export class FarmbotOsSettings const { bot, sourceFbosConfig, botToMqttStatus } = this.props; const { sync_status } = bot.hardware.informational_settings; const botOnline = isBotOnline(sync_status, botToMqttStatus); - const timeFormat = this.props.webAppConfig.body.time_format_24_hour ? - "24h" : "12h"; return e.preventDefault()}> - - - - - - + + + + + + + + - - - - -
    - {this.maybeWarnTz()} -
    -
    + + + + + +
    + {this.maybeWarnTz()} +
    -
    - + +
    - - - - - - - {this.props.shouldDisplay(Feature.boot_sequence) && - } - - + + + + + + + {this.props.shouldDisplay(Feature.boot_sequence) && + } + ; diff --git a/frontend/devices/components/fbos_settings/__tests__/auto_sync_row_test.tsx b/frontend/devices/components/fbos_settings/__tests__/auto_sync_row_test.tsx index 6a5afdb62..56b113f73 100644 --- a/frontend/devices/components/fbos_settings/__tests__/auto_sync_row_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/auto_sync_row_test.tsx @@ -15,7 +15,7 @@ import { fakeState } from "../../../../__test_support__/fake_state"; import { edit, save } from "../../../../api/crud"; import { fakeFbosConfig } from "../../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; describe("", () => { diff --git a/frontend/devices/components/fbos_settings/__tests__/auto_update_row_test.tsx b/frontend/devices/components/fbos_settings/__tests__/auto_update_row_test.tsx index 09b5e0d91..7fa9c6c9d 100644 --- a/frontend/devices/components/fbos_settings/__tests__/auto_update_row_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/auto_update_row_test.tsx @@ -11,7 +11,7 @@ import { fakeState } from "../../../../__test_support__/fake_state"; import { edit, save } from "../../../../api/crud"; import { fakeFbosConfig } from "../../../../__test_support__/fake_state/resources"; import { - buildResourceIndex, fakeDevice + buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; describe("", () => { @@ -20,11 +20,8 @@ describe("", () => { state.resources = buildResourceIndex([fakeConfig]); const fakeProps = (): AutoUpdateRowProps => ({ - timeFormat: "12h", - shouldDisplay: jest.fn(() => true), - device: fakeDevice(), dispatch: jest.fn(x => x(jest.fn(), () => state)), - sourceFbosConfig: () => ({ value: 1, consistent: true }) + sourceFbosConfig: () => ({ value: 1, consistent: true }), }); it("renders", () => { @@ -36,7 +33,7 @@ describe("", () => { const p = fakeProps(); p.sourceFbosConfig = () => ({ value: 0, consistent: true }); const wrapper = mount(); - wrapper.find("button").at(1).simulate("click"); + wrapper.find("button").first().simulate("click"); expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: true }); expect(save).toHaveBeenCalledWith(fakeConfig.uuid); }); @@ -45,7 +42,7 @@ describe("", () => { const p = fakeProps(); p.sourceFbosConfig = () => ({ value: 1, consistent: true }); const wrapper = mount(); - wrapper.find("button").at(1).simulate("click"); + wrapper.find("button").first().simulate("click"); expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: false }); expect(save).toHaveBeenCalledWith(fakeConfig.uuid); }); diff --git a/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx b/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx index 2532f9022..9d550378f 100644 --- a/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/board_type_test.tsx @@ -9,15 +9,15 @@ import { BoardType } from "../board_type"; import { BoardTypeProps } from "../interfaces"; import { fakeState } from "../../../../__test_support__/fake_state"; import { - fakeFbosConfig + fakeFbosConfig, } from "../../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; import { edit, save } from "../../../../api/crud"; import { bot } from "../../../../__test_support__/fake_state/bot"; import { - fakeTimeSettings + fakeTimeSettings, } from "../../../../__test_support__/fake_time_settings"; describe("", () => { @@ -69,6 +69,9 @@ describe("", () => { { label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" }, { label: "Farmduino (Genesis v1.3)", value: "farmduino" }, { label: "Farmduino (Genesis v1.4)", value: "farmduino_k14" }, + { label: "Farmduino (Genesis v1.5)", value: "farmduino_k15" }, + { label: "Farmduino (Express v1.0)", value: "express_k10" }, + { label: "None", value: "none" }, ]); }); diff --git a/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx b/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx index 410b12931..150cf9d6e 100644 --- a/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx @@ -1,12 +1,12 @@ import { - sequence2ddi, mapStateToProps, RawBootSequenceSelector + sequence2ddi, mapStateToProps, RawBootSequenceSelector, } from "../boot_sequence_selector"; import { - fakeSequence, fakeFbosConfig + fakeSequence, fakeFbosConfig, } from "../../../../__test_support__/fake_state/resources"; import { fakeState } from "../../../../__test_support__/fake_state"; import { - buildResourceIndex + buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; import React from "react"; import { mount } from "enzyme"; diff --git a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx index f7cd59033..f529db921 100644 --- a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx @@ -11,7 +11,7 @@ import { FbosDetailsProps } from "../interfaces"; import { fakeFbosConfig } from "../../../../__test_support__/fake_state/resources"; import { fakeState } from "../../../../__test_support__/fake_state"; import { - buildResourceIndex, fakeDevice + buildResourceIndex, fakeDevice, } from "../../../../__test_support__/resource_index_builder"; import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings"; import { updateConfig } from "../../../actions"; @@ -85,6 +85,27 @@ describe("", () => { expect(wrapper.text()).toContain("0.0.0"); }); + it("displays firmware commit link from firmware_commit", () => { + const p = fakeProps(); + const commit = "abcdefgh"; + p.botInfoSettings.firmware_commit = commit; + p.botInfoSettings.firmware_version = "1.0.0"; + const wrapper = mount(); + expect(wrapper.find("a").last().text()).toEqual(commit); + expect(wrapper.find("a").last().props().href?.split("/").slice(-1)[0]) + .toEqual(commit); + }); + + it("displays firmware commit link from version", () => { + const p = fakeProps(); + const commit = "abcdefgh"; + p.botInfoSettings.firmware_version = `1.2.3.R.x-${commit}+`; + const wrapper = mount(); + expect(wrapper.find("a").last().text()).toEqual(commit); + expect(wrapper.find("a").last().props().href?.split("/").slice(-1)[0]) + .toEqual(commit); + }); + it("displays commit link", () => { const p = fakeProps(); p.botInfoSettings.commit = "abcdefgh"; @@ -95,6 +116,7 @@ describe("", () => { it("doesn't display link without commit", () => { const p = fakeProps(); + p.botInfoSettings.firmware_version = undefined; p.botInfoSettings.commit = "---"; p.botInfoSettings.firmware_commit = "---"; const wrapper = mount(); diff --git a/frontend/devices/components/fbos_settings/__tests__/firmware_hardware_status_test.tsx b/frontend/devices/components/fbos_settings/__tests__/firmware_hardware_status_test.tsx index f63470754..d1c9cc6b7 100644 --- a/frontend/devices/components/fbos_settings/__tests__/firmware_hardware_status_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/firmware_hardware_status_test.tsx @@ -8,7 +8,7 @@ import { FirmwareHardwareStatusDetailsProps, FirmwareHardwareStatusDetails, FirmwareHardwareStatusIconProps, FirmwareHardwareStatusIcon, FirmwareHardwareStatusProps, FirmwareHardwareStatus, - FirmwareActions, FirmwareActionsProps + FirmwareActions, FirmwareActionsProps, } from "../firmware_hardware_status"; import { bot } from "../../../../__test_support__/fake_state/bot"; import { clickButton } from "../../../../__test_support__/helpers"; @@ -22,7 +22,6 @@ describe("", () => { apiFirmwareValue: undefined, botFirmwareValue: undefined, mcuFirmwareValue: undefined, - shouldDisplay: () => true, timeSettings: fakeTimeSettings(), dispatch: jest.fn(), }); @@ -79,7 +78,6 @@ describe("", () => { alerts: [], botOnline: true, apiFirmwareValue: undefined, - shouldDisplay: () => true, timeSettings: fakeTimeSettings(), dispatch: jest.fn(), }); diff --git a/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx b/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx index d993e3dc0..a88c28944 100644 --- a/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/power_and_reset_test.tsx @@ -6,6 +6,13 @@ jest.mock("../../../../api/crud", () => ({ save: jest.fn(), })); +let mockDev = false; +jest.mock("../../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + import * as React from "react"; import { PowerAndReset } from "../power_and_reset"; import { mount } from "enzyme"; @@ -16,39 +23,49 @@ import { fakeState } from "../../../../__test_support__/fake_state"; import { clickButton } from "../../../../__test_support__/helpers"; import { fakeFbosConfig } from "../../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; import { edit, save } from "../../../../api/crud"; describe("", () => { + beforeEach(() => { + mockDev = false; + }); + const fakeConfig = fakeFbosConfig(); const state = fakeState(); state.resources = buildResourceIndex([fakeConfig]); - const fakeProps = (): PowerAndResetProps => { - return { - controlPanelState: panelState(), - dispatch: jest.fn(x => x(jest.fn(), () => state)), - sourceFbosConfig: () => ({ value: true, consistent: true }), - shouldDisplay: jest.fn(), - botOnline: true, - }; - }; + const fakeProps = (): PowerAndResetProps => ({ + controlPanelState: panelState(), + dispatch: jest.fn(x => x(jest.fn(), () => state)), + sourceFbosConfig: () => ({ value: true, consistent: true }), + botOnline: true, + }); - it("open", () => { + it("renders in open state", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = true; const wrapper = mount(); - ["Power and Reset", "Restart", "Shutdown", "Factory Reset", - "Automatic Factory Reset", "Connection Attempt Period", "Change Ownership"] + ["Power and Reset", "Restart", "Shutdown", + "Factory Reset", "Automatic Factory Reset", + "Connection Attempt Period", "Change Ownership"] .map(string => expect(wrapper.text().toLowerCase()) .toContain(string.toLowerCase())); - ["Restart Firmware"] - .map(string => expect(wrapper.text().toLowerCase()) - .not.toContain(string.toLowerCase())); + expect(wrapper.text().toLowerCase()) + .toContain("Restart Firmware".toLowerCase()); }); - it("closed", () => { + it("doesn't render restart firmware", () => { + mockDev = true; + const p = fakeProps(); + p.controlPanelState.power_and_reset = true; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()) + .not.toContain("Restart Firmware".toLowerCase()); + }); + + it("renders as closed", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = false; const wrapper = mount(); @@ -73,7 +90,7 @@ describe("", () => { p.sourceFbosConfig = () => ({ value: false, consistent: true }); p.controlPanelState.power_and_reset = true; const wrapper = mount(); - clickButton(wrapper, 3, "yes"); + clickButton(wrapper, 4, "yes"); expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true }); expect(save).toHaveBeenCalledWith(fakeConfig.uuid); }); @@ -81,7 +98,6 @@ describe("", () => { it("restarts firmware", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = true; - p.shouldDisplay = () => true; const wrapper = mount(); expect(wrapper.text().toLowerCase()) .toContain("Restart Firmware".toLowerCase()); diff --git a/frontend/devices/components/fbos_settings/auto_sync_row.tsx b/frontend/devices/components/fbos_settings/auto_sync_row.tsx index 2816da661..0813212d5 100644 --- a/frontend/devices/components/fbos_settings/auto_sync_row.tsx +++ b/frontend/devices/components/fbos_settings/auto_sync_row.tsx @@ -1,32 +1,35 @@ import * as React from "react"; import { Row, Col } from "../../../ui/index"; import { ToggleButton } from "../../../controls/toggle_button"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { updateConfig } from "../../actions"; import { ColWidth } from "../farmbot_os_settings"; import { AutoSyncRowProps } from "./interfaces"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function AutoSyncRow(props: AutoSyncRowProps) { const autoSync = props.sourceFbosConfig("auto_sync"); return - - - - -

    - {t(Content.AUTO_SYNC)} -

    - - - { - props.dispatch(updateConfig({ auto_sync: !autoSync.value })); - }} /> - + + + + + +

    + {t(Content.AUTO_SYNC)} +

    + + + { + props.dispatch(updateConfig({ auto_sync: !autoSync.value })); + }} /> + +
    ; } diff --git a/frontend/devices/components/fbos_settings/auto_update_row.tsx b/frontend/devices/components/fbos_settings/auto_update_row.tsx index b83ca5c32..173ed5ddf 100644 --- a/frontend/devices/components/fbos_settings/auto_update_row.tsx +++ b/frontend/devices/components/fbos_settings/auto_update_row.tsx @@ -3,25 +3,19 @@ import { Row, Col } from "../../../ui/index"; import { ColWidth } from "../farmbot_os_settings"; import { ToggleButton } from "../../../controls/toggle_button"; import { updateConfig } from "../../actions"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { AutoUpdateRowProps } from "./interfaces"; import { t } from "../../../i18next_wrapper"; -import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector"; -import { Feature } from "../../interfaces"; +import { Highlight } from "../maybe_highlight"; export function AutoUpdateRow(props: AutoUpdateRowProps) { const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); - return
    - {props.shouldDisplay(Feature.ota_update_hour) && } - + return + @@ -36,6 +30,6 @@ export function AutoUpdateRow(props: AutoUpdateRowProps) { os_auto_update: !osAutoUpdate.value }))} /> - -
    ; +
    + ; } diff --git a/frontend/devices/components/fbos_settings/board_type.tsx b/frontend/devices/components/fbos_settings/board_type.tsx index 1f9c309f4..59ef35567 100644 --- a/frontend/devices/components/fbos_settings/board_type.tsx +++ b/frontend/devices/components/fbos_settings/board_type.tsx @@ -8,8 +8,10 @@ import { BoardTypeProps } from "./interfaces"; import { t } from "../../../i18next_wrapper"; import { FirmwareHardwareStatus } from "./firmware_hardware_status"; import { - isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI + isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI, } from "../firmware_hardware_support"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; interface BoardTypeState { sending: boolean } @@ -47,31 +49,30 @@ export class BoardType extends React.Component { render() { return - - - - -
    + + + + + -
    - - - - + + + + +
    ; } } diff --git a/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx b/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx index 6c3c9342b..1a0f4d460 100644 --- a/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx +++ b/frontend/devices/components/fbos_settings/boot_sequence_selector.tsx @@ -9,6 +9,8 @@ import { selectAllSequences, findSequenceById } from "../../../resources/selecto import { betterCompact } from "../../../util"; import { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; interface Props { list: DropDownItem[]; @@ -56,18 +58,20 @@ export class RawBootSequenceSelector extends React.Component { render() { return - - - - - - + + + + + + + + ; } } diff --git a/frontend/devices/components/fbos_settings/camera_selection.tsx b/frontend/devices/components/fbos_settings/camera_selection.tsx index 46f3d895b..44329497d 100644 --- a/frontend/devices/components/fbos_settings/camera_selection.tsx +++ b/frontend/devices/components/fbos_settings/camera_selection.tsx @@ -1,14 +1,15 @@ import * as React from "react"; import { DropDownItem, Row, Col, FBSelect } from "../../../ui/index"; import { - CameraSelectionProps, CameraSelectionState + CameraSelectionProps, CameraSelectionState, } from "./interfaces"; import { info, success, error } from "../../../toast/toast"; import { getDevice } from "../../../device"; import { ColWidth } from "../farmbot_os_settings"; import { Feature, UserEnv } from "../../interfaces"; import { t } from "../../../i18next_wrapper"; -import { Content, ToolTips } from "../../../constants"; +import { Content, ToolTips, DeviceSetting } from "../../../constants"; +import { Highlight } from "../maybe_highlight"; /** Check if the camera has been disabled. */ export const cameraDisabled = (env: UserEnv): boolean => @@ -84,21 +85,21 @@ export class CameraSelection render() { return - - - - -
    + + + + + -
    - + +
    ; } } diff --git a/frontend/devices/components/fbos_settings/change_ownership_form.tsx b/frontend/devices/components/fbos_settings/change_ownership_form.tsx index 2effbfaf4..45d65a198 100644 --- a/frontend/devices/components/fbos_settings/change_ownership_form.tsx +++ b/frontend/devices/components/fbos_settings/change_ownership_form.tsx @@ -90,6 +90,7 @@ export class ChangeOwnershipForm diff --git a/frontend/devices/components/fbos_settings/factory_reset_row.tsx b/frontend/devices/components/fbos_settings/factory_reset_row.tsx index 0a42a8f3a..8cacd7a0f 100644 --- a/frontend/devices/components/fbos_settings/factory_reset_row.tsx +++ b/frontend/devices/components/fbos_settings/factory_reset_row.tsx @@ -1,79 +1,87 @@ import * as React from "react"; import { Row, Col } from "../../../ui/index"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { factoryReset, updateConfig } from "../../actions"; import { ToggleButton } from "../../../controls/toggle_button"; import { BotConfigInputBox } from "../bot_config_input_box"; -import { FactoryResetRowProps } from "./interfaces"; +import { FactoryResetRowsProps } from "./interfaces"; import { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; -export function FactoryResetRow(props: FactoryResetRowProps) { +export function FactoryResetRows(props: FactoryResetRowsProps) { const { dispatch, sourceFbosConfig, botOnline } = props; const disableFactoryReset = sourceFbosConfig("disable_factory_reset"); const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {}; - return
    + return
    - - - - -

    - {t(Content.FACTORY_RESET_WARNING)} -

    - - - - + + + + + +

    + {t(Content.FACTORY_RESET_WARNING)} +

    + + + + +
    - - - - -

    - {t(Content.AUTO_FACTORY_RESET)} -

    - - - { - dispatch(updateConfig({ - disable_factory_reset: !disableFactoryReset.value - })); - }} /> - + + + + + +

    + {t(Content.AUTO_FACTORY_RESET)} +

    + + + { + dispatch(updateConfig({ + disable_factory_reset: !disableFactoryReset.value + })); + }} /> + +
    - - - - -

    - {t(Content.AUTO_FACTORY_RESET_PERIOD)} -

    - - - - + + + + + +

    + {t(Content.AUTO_FACTORY_RESET_PERIOD)} +

    + + + + +
    ; } diff --git a/frontend/devices/components/fbos_settings/farmbot_os_row.tsx b/frontend/devices/components/fbos_settings/farmbot_os_row.tsx index 2f1cc8e04..2bcc6fd15 100644 --- a/frontend/devices/components/fbos_settings/farmbot_os_row.tsx +++ b/frontend/devices/components/fbos_settings/farmbot_os_row.tsx @@ -7,6 +7,8 @@ import { FarmbotOsRowProps } from "./interfaces"; import { FbosDetails } from "./fbos_details"; import { t } from "../../../i18next_wrapper"; import { ErrorBoundary } from "../../../error_boundary"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; const getVersionString = (fbosVersion: string | undefined, onBeta: boolean | undefined): string => { @@ -21,48 +23,50 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) { } = bot.hardware.informational_settings; const version = getVersionString(controller_version, currently_on_beta); return - - - - - -

    - {t("Version {{ version }}", { version })} -

    - - - -
    - - - -

    - {t("Release Notes")}  + + + + + + +

    + {t("Version {{ version }}", { version })} +

    + + + +
    + + + +

    + {t("Release Notes")}  -

    -
    -

    {props.osReleaseNotesHeading}

    - - {osReleaseNotes} - -
    -
    - - - - +

    +
    +

    {props.osReleaseNotesHeading}

    + + {osReleaseNotes} + +
    + + + + + +
    ; } diff --git a/frontend/devices/components/fbos_settings/fbos_button_row.tsx b/frontend/devices/components/fbos_settings/fbos_button_row.tsx index 1cf1d3a58..97f97b497 100644 --- a/frontend/devices/components/fbos_settings/fbos_button_row.tsx +++ b/frontend/devices/components/fbos_settings/fbos_button_row.tsx @@ -2,10 +2,12 @@ import * as React from "react"; import { Row, Col } from "../../../ui"; import { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; export interface FbosButtonRowProps { botOnline: boolean; - label: string; + label: DeviceSetting; description: string; buttonText: string; color: string; @@ -14,24 +16,27 @@ export interface FbosButtonRowProps { export const FbosButtonRow = (props: FbosButtonRowProps) => { return - - - - -

    - {t(props.description)} -

    - - - - + + + + + +

    + {t(props.description)} +

    + + + + +
    ; }; diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index 9070b57cf..9ddc7de29 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -14,6 +14,7 @@ import { timeFormatString } from "../../../util"; import { TimeSettings } from "../../../interfaces"; import { StringConfigKey } from "farmbot/dist/resources/configs/fbos"; import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; +import { ExternalUrl, FarmBotRepo } from "../../../external_urls"; /** Return an indicator color for the given temperature (C). */ export const colorFromTemp = (temp: number | undefined): string => { @@ -40,7 +41,7 @@ interface ChipTemperatureDisplayProps { /** RPI CPU temperature display row: label, temperature, indicator. */ export function ChipTemperatureDisplay( - { chip, temperature }: ChipTemperatureDisplayProps + { chip, temperature }: ChipTemperatureDisplayProps, ): JSX.Element { return

    @@ -54,21 +55,24 @@ export function ChipTemperatureDisplay( interface WiFiStrengthDisplayProps { wifiStrength: number | undefined; wifiStrengthPercent?: number | undefined; + extraInfo?: boolean; } /** WiFi signal strength display row: label, strength, indicator. */ export function WiFiStrengthDisplay( - { wifiStrength, wifiStrengthPercent }: WiFiStrengthDisplayProps + { wifiStrength, wifiStrengthPercent, extraInfo }: WiFiStrengthDisplayProps, ): JSX.Element { const percent = wifiStrength ? Math.round(-0.0154 * wifiStrength ** 2 - 0.4 * wifiStrength + 98) : 0; const dbString = `${wifiStrength || 0}dBm`; const percentString = `${wifiStrengthPercent || percent}%`; + const numberDisplay = + extraInfo ? `${percentString} (${dbString})` : percentString; return

    {t("WiFi strength")}: - {wifiStrength ? dbString : "N/A"} + {wifiStrength ? numberDisplay : "N/A"}

    {wifiStrength &&
    @@ -170,13 +174,13 @@ const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8); interface CommitDisplayProps { title: string; - repo: string; + repo: FarmBotRepo; commit: string; } /** GitHub commit display row: label, commit link. */ const CommitDisplay = ( - { title, repo, commit }: CommitDisplayProps + { title, repo, commit }: CommitDisplayProps, ): JSX.Element => { const shortCommit = shortenCommit(commit); return

    @@ -184,7 +188,7 @@ const CommitDisplay = ( {shortCommit === "---" ? shortCommit : {shortCommit} } @@ -218,7 +222,7 @@ export interface BetaReleaseOptInButtonProps { /** Label and toggle button for opting in to FBOS beta releases. */ export const BetaReleaseOptIn = ( - { dispatch, sourceFbosConfig }: BetaReleaseOptInButtonProps + { dispatch, sourceFbosConfig }: BetaReleaseOptInButtonProps, ): JSX.Element => { const betaOptIn = sourceFbosConfig("update_channel" as ConfigurationName).value; return

    @@ -260,22 +264,25 @@ export function FbosDetails(props: FbosDetailsProps) { wifi_level_percent, cpu_usage, private_ip, } = props.botInfoSettings; const { last_ota, last_ota_checkup } = props.deviceAccount.body; + const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---"; + const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit; - return
    + return

    {t("Environment")}: {env}

    - +

    {t("Target")}: {target}

    {t("Node name")}: {last((node_name || "").split("@"))}

    {t("Device ID")}: {props.deviceAccount.body.id}

    {isString(private_ip) &&

    {t("Local IP address")}: {private_ip}

    }

    {t("Firmware")}: {reformatFwVersion(firmware_version)}

    + repo={FarmBotRepo.FarmBotArduinoFirmware} commit={firmwareCommit} />

    {t("Firmware code")}: {firmware_version}

    {isNumber(uptime) && } {isNumber(memory_usage) && @@ -283,7 +290,7 @@ export function FbosDetails(props: FbosDetailsProps) { {isNumber(disk_usage) &&

    {t("Disk usage")}: {disk_usage}%

    } {isNumber(cpu_usage) &&

    {t("CPU usage")}: {cpu_usage}%

    } - ; }; -const lookup = (value: string | undefined) => +export const lookup = (value: string | undefined) => value && Object.keys(FIRMWARE_CHOICES_DDI).includes(value) ? FIRMWARE_CHOICES_DDI[value].label : undefined; @@ -36,7 +36,6 @@ export interface FirmwareHardwareStatusDetailsProps { apiFirmwareValue: string | undefined; botFirmwareValue: string | undefined; mcuFirmwareValue: string | undefined; - shouldDisplay: ShouldDisplay; timeSettings: TimeSettings; dispatch: Function; } @@ -50,6 +49,7 @@ export const FlashFirmwareBtn = (props: FlashFirmwareBtnProps) => { const { apiFirmwareValue } = props; return - {firmwareConfig && - - - - } - -
    - -
    - - - - - -
    + + + + + + + + + + + + ; } } + +interface SettingLoadProgressProps { + sourceFwConfig: SourceFwConfig; + firmwareConfig: FirmwareConfig | undefined; +} + +const UNTRACKED_KEYS: (keyof FirmwareConfig)[] = [ + "id", "created_at", "updated_at", "device_id", "api_migrated", + "param_config_ok", "param_test", "param_use_eeprom", "param_version", +]; + +/** Track firmware configuration adoption by FarmBot OS. */ +const SettingLoadProgress = (props: SettingLoadProgressProps) => { + const keys = Object.keys(props.firmwareConfig || {}) + .filter((k: keyof FirmwareConfig) => !UNTRACKED_KEYS.includes(k)); + const loadedKeys = keys.filter((key: McuParamName) => + props.sourceFwConfig(key).consistent); + const progress = loadedKeys.length / keys.length * 100; + const color = [0, 100].includes(progress) ? Color.darkGray : Color.white; + return
    +
    +
    ; +}; diff --git a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx index 9d384af5c..e7c47b59b 100644 --- a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx @@ -1,22 +1,41 @@ -const mockDevice = { - calibrate: jest.fn(() => Promise.resolve({})) -}; -jest.mock("../../../../device", () => ({ - getDevice: () => (mockDevice) -})); import * as React from "react"; import { mount } from "enzyme"; import { CalibrationRow } from "../calibration_row"; import { bot } from "../../../../__test_support__/fake_state/bot"; +import { CalibrationRowProps } from "../../interfaces"; +import { DeviceSetting } from "../../../../constants"; + +describe("", () => { + const fakeProps = (): CalibrationRowProps => ({ + type: "calibrate", + hardware: bot.hardware.mcu_params, + botDisconnected: false, + action: jest.fn(), + toolTip: "calibrate", + title: DeviceSetting.calibration, + axisTitle: "calibrate", + }); -describe("", () => { it("calls device", () => { - const result = mount(); + const p = fakeProps(); + const result = mount(); + p.hardware.encoder_enabled_x = 1; + p.hardware.encoder_enabled_y = 1; + p.hardware.encoder_enabled_z = 0; [0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click")); - expect(mockDevice.calibrate).toHaveBeenCalledTimes(2); - [{ axis: "y" }, { axis: "x" }].map(x => - expect(mockDevice.calibrate).toHaveBeenCalledWith(x)); + expect(p.action).toHaveBeenCalledTimes(2); + ["y", "x"].map(x => expect(p.action).toHaveBeenCalledWith(x)); + }); + + it("is not disabled", () => { + const p = fakeProps(); + p.type = "zero"; + const result = mount(); + p.hardware.encoder_enabled_x = 0; + p.hardware.encoder_enabled_y = 1; + p.hardware.encoder_enabled_z = 0; + [0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click")); + expect(p.action).toHaveBeenCalledTimes(3); + ["x", "y", "z"].map(x => expect(p.action).toHaveBeenCalledWith(x)); }); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/encoder_type_test.tsx b/frontend/devices/components/hardware_settings/__tests__/encoder_type_test.tsx index 8881584e3..216472f92 100644 --- a/frontend/devices/components/hardware_settings/__tests__/encoder_type_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/encoder_type_test.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { - EncoderType, EncoderTypeProps, LOOKUP, findByType, isEncoderValue + EncoderType, EncoderTypeProps, LOOKUP, findByType, isEncoderValue, } from "../encoder_type"; import { shallow } from "enzyme"; import { FBSelect } from "../../../../ui/index"; diff --git a/frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx b/frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx deleted file mode 100644 index 6c70916d5..000000000 --- a/frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react"; -import { mount, shallow } from "enzyme"; -import { EncodersAndEndStops } from "../encoders_and_endstops"; -import { EncodersProps, NumericMCUInputGroupProps } from "../../interfaces"; -import { panelState } from "../../../../__test_support__/control_panel_state"; -import { bot } from "../../../../__test_support__/fake_state/bot"; -import { Dictionary } from "farmbot"; - -describe("", () => { - const mockFeatures: Dictionary = {}; - const fakeProps = (): EncodersProps => ({ - dispatch: jest.fn(), - controlPanelState: panelState(), - sourceFwConfig: x => - ({ value: bot.hardware.mcu_params[x], consistent: true }), - shouldDisplay: jest.fn(key => mockFeatures[key]), - firmwareHardware: undefined, - }); - - it("shows encoder labels", () => { - const p = fakeProps(); - p.firmwareHardware = undefined; - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("encoder"); - expect(wrapper.text().toLowerCase()).not.toContain("stall"); - }); - - it("shows stall labels", () => { - const p = fakeProps(); - p.firmwareHardware = "express_k10"; - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("encoder"); - expect(wrapper.text().toLowerCase()).toContain("stall"); - }); - - it.each<["short" | "long"]>([ - ["short"], - ["long"], - ])("uses %s int scaling factor", (size) => { - mockFeatures.long_scaling_factor = size === "short" ? false : true; - const wrapper = shallow(); - const sfProps = wrapper.find("NumericMCUInputGroup").at(2) - .props() as NumericMCUInputGroupProps; - expect(sfProps.name).toEqual("Encoder Scaling"); - expect(sfProps.intSize).toEqual(size); - }); -}); diff --git a/frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx b/frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx new file mode 100644 index 000000000..d06e176d6 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { Encoders } from "../encoders"; +import { EncodersProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { bot } from "../../../../__test_support__/fake_state/bot"; + +describe("", () => { + const fakeProps = (): EncodersProps => ({ + dispatch: jest.fn(), + controlPanelState: panelState(), + sourceFwConfig: x => + ({ value: bot.hardware.mcu_params[x], consistent: true }), + firmwareHardware: undefined, + }); + + it("shows encoder labels", () => { + const p = fakeProps(); + p.firmwareHardware = undefined; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("encoder"); + expect(wrapper.text().toLowerCase()).not.toContain("stall"); + }); + + it("shows stall labels", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("encoder"); + expect(wrapper.text().toLowerCase()).toContain("stall"); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx b/frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx new file mode 100644 index 000000000..96a2a5b69 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { EndStops } from "../endstops"; +import { EndStopsProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { bot } from "../../../../__test_support__/fake_state/bot"; + +describe("", () => { + const fakeProps = (): EndStopsProps => ({ + dispatch: jest.fn(), + controlPanelState: panelState(), + sourceFwConfig: x => + ({ value: bot.hardware.mcu_params[x], consistent: true }), + }); + + it("shows endstop labels", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("endstop"); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx b/frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx new file mode 100644 index 000000000..3f1c76049 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx @@ -0,0 +1,47 @@ +jest.mock("../../../../api/crud", () => ({ + edit: jest.fn(), + save: jest.fn(), +})); + +import * as React from "react"; +import { mount } from "enzyme"; +import { ErrorHandling } from "../error_handling"; +import { ErrorHandlingProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { bot } from "../../../../__test_support__/fake_state/bot"; +import { edit, save } from "../../../../api/crud"; +import { fakeState } from "../../../../__test_support__/fake_state"; +import { + fakeFirmwareConfig, +} from "../../../../__test_support__/fake_state/resources"; +import { + buildResourceIndex, +} from "../../../../__test_support__/resource_index_builder"; + +describe("", () => { + const fakeConfig = fakeFirmwareConfig(); + const state = fakeState(); + state.resources = buildResourceIndex([fakeConfig]); + const fakeProps = (): ErrorHandlingProps => ({ + dispatch: jest.fn(x => x(jest.fn(), () => state)), + controlPanelState: panelState(), + sourceFwConfig: x => + ({ value: bot.hardware.mcu_params[x], consistent: true }), + }); + + it("shows error handling labels", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("error handling"); + }); + + it("toggles retries e-stop parameter", () => { + const p = fakeProps(); + p.controlPanelState.error_handling = true; + p.sourceFwConfig = () => ({ value: 1, consistent: true }); + const wrapper = mount(); + wrapper.find("button").at(0).simulate("click"); + expect(edit).toHaveBeenCalledWith(fakeConfig, { param_e_stop_on_mov_err: 0 }); + expect(save).toHaveBeenCalledWith(fakeConfig.uuid); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/export_menu_test.tsx b/frontend/devices/components/hardware_settings/__tests__/export_menu_test.tsx index 33071a515..1a58e7332 100644 --- a/frontend/devices/components/hardware_settings/__tests__/export_menu_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/export_menu_test.tsx @@ -1,10 +1,10 @@ import * as React from "react"; import { mount } from "enzyme"; import { - FwParamExportMenu, condenseFwConfig, uncondenseFwConfig + FwParamExportMenu, condenseFwConfig, uncondenseFwConfig, } from "../export_menu"; import { - fakeFirmwareConfig + fakeFirmwareConfig, } from "../../../../__test_support__/fake_state/resources"; describe("", () => { diff --git a/frontend/devices/components/hardware_settings/__tests__/header_test.tsx b/frontend/devices/components/hardware_settings/__tests__/header_test.tsx index 902fee1b6..8b63d6d11 100644 --- a/frontend/devices/components/hardware_settings/__tests__/header_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/header_test.tsx @@ -1,16 +1,17 @@ import * as React from "react"; import { Header } from "../header"; import { mount } from "enzyme"; +import { DeviceSetting } from "../../../../constants"; describe("
    ", () => { it("renders", () => { const fn = jest.fn(); const el = mount(
    ); - expect(el.text()).toContain("FOO"); + expect(el.text().toLowerCase()).toContain("motors"); expect(el.find(".fa-minus").length).toBe(1); }); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx index 6fd42b781..2b951aacf 100644 --- a/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx @@ -1,29 +1,47 @@ -jest.mock("../../../actions", () => ({ updateMCU: jest.fn() })); +jest.mock("../../../actions", () => ({ + updateMCU: jest.fn(), + commandErr: jest.fn(), +})); + +const mockDevice = { + calibrate: jest.fn(() => Promise.resolve({})), + findHome: jest.fn(() => Promise.resolve({})), + setZero: jest.fn(() => Promise.resolve({})), +}; +jest.mock("../../../../device", () => ({ getDevice: () => mockDevice })); import * as React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { HomingAndCalibration } from "../homing_and_calibration"; import { bot } from "../../../../__test_support__/fake_state/bot"; import { updateMCU } from "../../../actions"; import { - fakeFirmwareConfig + fakeFirmwareConfig, } from "../../../../__test_support__/fake_state/resources"; import { error, warning } from "../../../../toast/toast"; import { inputEvent } from "../../../../__test_support__/fake_html_events"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { HomingAndCalibrationProps } from "../../interfaces"; +import { CalibrationRow } from "../calibration_row"; describe("", () => { + const fakeProps = (): HomingAndCalibrationProps => ({ + dispatch: jest.fn(), + bot, + controlPanelState: panelState(), + sourceFwConfig: x => ({ + value: bot.hardware.mcu_params[x], consistent: true + }), + firmwareConfig: fakeFirmwareConfig().body, + botDisconnected: false, + firmwareHardware: undefined, + }); + function testAxisLengthInput( provided: string, expected: string | undefined) { - const dispatch = jest.fn(); - bot.controlPanelState.homing_and_calibration = true; - const result = mount( ({ - value: bot.hardware.mcu_params[x], consistent: true - })} - botDisconnected={false} />); + const p = fakeProps(); + p.bot.controlPanelState.homing_and_calibration = true; + const result = mount(); const e = inputEvent(provided); const input = result.find("input").first().props(); input.onChange && input.onChange(e); @@ -45,4 +63,33 @@ describe("", () => { expect(warning).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); }); + + it("finds home", () => { + const wrapper = shallow(); + wrapper.find(CalibrationRow).first().props().action("x"); + expect(mockDevice.findHome).toHaveBeenCalledWith({ + axis: "x", speed: 100 + }); + }); + + it("calibrates", () => { + const wrapper = shallow(); + wrapper.find(CalibrationRow).at(1).props().action("all"); + expect(mockDevice.calibrate).toHaveBeenCalledWith({ axis: "all" }); + }); + + it("sets zero", () => { + const wrapper = shallow(); + wrapper.find(CalibrationRow).last().props().action("all"); + expect(mockDevice.setZero).toHaveBeenCalledWith("all"); + }); + + it("shows express board related labels", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + p.controlPanelState.homing_and_calibration = true; + const wrapper = shallow(); + expect(wrapper.find(CalibrationRow).first().props().toolTip) + .toContain("stall detection"); + }); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx deleted file mode 100644 index 4287bc45f..000000000 --- a/frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -const mockDevice = { - findHome: jest.fn(() => Promise.resolve({})) -}; - -jest.mock("../../../../device", () => ({ - getDevice: () => (mockDevice) -})); -import * as React from "react"; -import { mount } from "enzyme"; -import { HomingRow } from "../homing_row"; -import { bot } from "../../../../__test_support__/fake_state/bot"; - -describe("", () => { - it("renders three buttons", () => { - const wrapper = mount(); - const txt = wrapper.text().toUpperCase(); - ["X", "Y", "Z"].map(function (axis) { - expect(txt).toContain(`HOME ${axis}`); - }); - }); - - it("calls device", () => { - const result = mount(); - [0, 1, 2].map(i => - result.find("LockableButton").at(i).simulate("click")); - [{ axis: "x", speed: 100 }, { axis: "y", speed: 100 }].map(x => - expect(mockDevice.findHome).toHaveBeenCalledWith(x)); - }); -}); diff --git a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx index fb2df2174..116c1f068 100644 --- a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx @@ -11,10 +11,10 @@ import { McuParamName } from "farmbot"; import { panelState } from "../../../../__test_support__/control_panel_state"; import { fakeState } from "../../../../__test_support__/fake_state"; import { - fakeFirmwareConfig + fakeFirmwareConfig, } from "../../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../../__test_support__/resource_index_builder"; import { edit, save } from "../../../../api/crud"; @@ -37,9 +37,7 @@ describe("", () => { it("renders the base case", () => { const wrapper = render(); ["Enable 2nd X Motor", - "Max Retries", - "E-Stop on Movement Error", - "Max Speed (mm/s)" + "Max Speed (mm/s)", ].map(string => expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); }); @@ -48,16 +46,14 @@ describe("", () => { const p = fakeProps(); p.firmwareHardware = "express_k10"; const wrapper = render(); - expect(wrapper.text()).toContain("Stall"); - expect(wrapper.text()).toContain("Current"); + expect(wrapper.text()).toContain("Motor Current"); }); it("doesn't show TMC parameters", () => { const p = fakeProps(); p.firmwareHardware = "farmduino"; const wrapper = render(); - expect(wrapper.text()).not.toContain("Stall"); - expect(wrapper.text()).not.toContain("Current"); + expect(wrapper.text()).not.toContain("Motor Current"); }); const testParamToggle = ( @@ -72,15 +68,6 @@ describe("", () => { expect(save).toHaveBeenCalledWith(fakeConfig.uuid); }); }; - testParamToggle("toggles retries e-stop parameter", "param_e_stop_on_mov_err", 0); - testParamToggle("toggles enable X2", "movement_secondary_motor_x", 7); - testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 8); - - it("renders TMC params", () => { - const p = fakeProps(); - p.firmwareHardware = "express_k10"; - const wrapper = render(); - expect(wrapper.text()).toContain("Motor Current"); - expect(wrapper.text()).toContain("Stall Sensitivity"); - }); + testParamToggle("toggles enable X2", "movement_secondary_motor_x", 6); + testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 7); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx b/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx new file mode 100644 index 000000000..5f98b0198 --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { PinBindings } from "../pin_bindings"; +import { PinBindingsProps } from "../../interfaces"; +import { panelState } from "../../../../__test_support__/control_panel_state"; +import { + buildResourceIndex, +} from "../../../../__test_support__/resource_index_builder"; + +describe("", () => { + const fakeProps = (): PinBindingsProps => ({ + dispatch: jest.fn(), + controlPanelState: panelState(), + resources: buildResourceIndex([]).index, + firmwareHardware: undefined, + }); + + it("shows pin binding labels", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("pin bindings"); + }); +}); diff --git a/frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx b/frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx deleted file mode 100644 index 280578bc4..000000000 --- a/frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -const mockDevice = { - setZero: jest.fn(() => Promise.resolve()) -}; -jest.mock("../../../../device", () => ({ - getDevice: () => (mockDevice) -})); -import * as React from "react"; -import { mount } from "enzyme"; -import { ZeroRow } from "../zero_row"; - -describe("", () => { - it("calls device", () => { - const result = mount(); - [0, 1, 2].map(i => result.find("ZeroButton").at(i).simulate("click")); - ["x", "y", "z"].map(x => - expect(mockDevice.setZero).toHaveBeenCalledWith(x)); - expect(mockDevice.setZero).toHaveBeenCalledTimes(3); - }); -}); diff --git a/frontend/devices/components/hardware_settings/calibration_row.tsx b/frontend/devices/components/hardware_settings/calibration_row.tsx index 62c7cb31c..145020de3 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -1,40 +1,38 @@ import * as React from "react"; -import { getDevice } from "../../../device"; -import { Axis } from "../../interfaces"; import { LockableButton } from "../lockable_button"; import { axisTrackingStatus } from "../axis_tracking_status"; -import { ToolTips } from "../../../constants"; import { Row, Col, Help } from "../../../ui/index"; import { CalibrationRowProps } from "../interfaces"; -import { commandErr } from "../../actions"; import { t } from "../../../i18next_wrapper"; import { Position } from "@blueprintjs/core"; - -const calibrate = (axis: Axis) => getDevice() - .calibrate({ axis }) - .catch(commandErr("Calibration")); +import { Highlight } from "../maybe_highlight"; export function CalibrationRow(props: CalibrationRowProps) { const { hardware, botDisconnected } = props; return - - - - - {axisTrackingStatus(hardware) - .map(row => { - const { axis, disabled } = row; - return - calibrate(axis)}> - {t("CALIBRATE {{axis}}", { axis })} - - ; - })} + + + + + + {axisTrackingStatus(hardware) + .map(row => { + const { axis } = row; + const hardwareDisabled = props.type == "zero" ? false : row.disabled; + return + props.action(axis)}> + {`${t(props.axisTitle)} ${axis}`} + + ; + })} + ; } diff --git a/frontend/devices/components/hardware_settings/danger_zone.tsx b/frontend/devices/components/hardware_settings/danger_zone.tsx index cd78e7f23..0d0d941eb 100644 --- a/frontend/devices/components/hardware_settings/danger_zone.tsx +++ b/frontend/devices/components/hardware_settings/danger_zone.tsx @@ -3,41 +3,46 @@ import { DangerZoneProps } from "../interfaces"; import { Row, Col } from "../../../ui/index"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function DangerZone(props: DangerZoneProps) { const { dispatch, onReset, botDisconnected } = props; const { danger_zone } = props.controlPanelState; - return
    + return
    - - - - -

    - {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} -

    - - - - + + + + + +

    + {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} +

    + + + + +
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/encoder_type.tsx b/frontend/devices/components/hardware_settings/encoder_type.tsx index 8bd800efb..237ac70f8 100644 --- a/frontend/devices/components/hardware_settings/encoder_type.tsx +++ b/frontend/devices/components/hardware_settings/encoder_type.tsx @@ -19,7 +19,7 @@ const OPTIONS = [LOOKUP[Encoder.differential], LOOKUP[Encoder.quadrature]]; const KEYS: McuParamName[] = [ "encoder_type_x", "encoder_type_y", - "encoder_type_z" + "encoder_type_z", ]; export function isEncoderValue(x: unknown): x is Encoder { diff --git a/frontend/devices/components/hardware_settings/encoders.tsx b/frontend/devices/components/hardware_settings/encoders.tsx new file mode 100644 index 000000000..40a7fa219 --- /dev/null +++ b/frontend/devices/components/hardware_settings/encoders.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; +import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; +import { ToolTips, DeviceSetting } from "../../../constants"; +import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; +import { EncodersProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { hasEncoders } from "../firmware_hardware_support"; +import { Highlight } from "../maybe_highlight"; +import { SpacePanelHeader } from "./space_panel_header"; + +export function Encoders(props: EncodersProps) { + + const { encoders } = props.controlPanelState; + const { dispatch, sourceFwConfig, firmwareHardware } = props; + + const encodersDisabled = { + x: !sourceFwConfig("encoder_enabled_x").value, + y: !sourceFwConfig("encoder_enabled_y").value, + z: !sourceFwConfig("encoder_enabled_z").value + }; + const showEncoders = hasEncoders(firmwareHardware); + + return +
    + +
    + +
    + + {!showEncoders && + } + {showEncoders && + } + {showEncoders && + } + + + {showEncoders && + } +
    + ; +} diff --git a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx b/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx deleted file mode 100644 index 72bdce981..000000000 --- a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from "react"; -import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; -import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; -import { EncodersProps } from "../interfaces"; -import { Header } from "./header"; -import { Collapse } from "@blueprintjs/core"; -import { Feature } from "../../interfaces"; -import { t } from "../../../i18next_wrapper"; -import { isExpressBoard } from "../firmware_hardware_support"; - -export function EncodersAndEndStops(props: EncodersProps) { - - const { encoders_and_endstops } = props.controlPanelState; - const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props; - - const encodersDisabled = { - x: !sourceFwConfig("encoder_enabled_x").value, - y: !sourceFwConfig("encoder_enabled_y").value, - z: !sourceFwConfig("encoder_enabled_z").value - }; - - return
    -
    - - - {!isExpressBoard(firmwareHardware) && - } - {!isExpressBoard(firmwareHardware) && - } - - - {!isExpressBoard(firmwareHardware) && - } - - - - -
    ; -} diff --git a/frontend/devices/components/hardware_settings/endstops.tsx b/frontend/devices/components/hardware_settings/endstops.tsx new file mode 100644 index 000000000..00c82442b --- /dev/null +++ b/frontend/devices/components/hardware_settings/endstops.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; +import { ToolTips, DeviceSetting } from "../../../constants"; +import { EndStopsProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { Highlight } from "../maybe_highlight"; +import { SpacePanelHeader } from "./space_panel_header"; + +export function EndStops(props: EndStopsProps) { + + const { endstops } = props.controlPanelState; + const { dispatch, sourceFwConfig } = props; + + return +
    + +
    + +
    + + + +
    + ; +} diff --git a/frontend/devices/components/hardware_settings/error_handling.tsx b/frontend/devices/components/hardware_settings/error_handling.tsx new file mode 100644 index 000000000..64681cef5 --- /dev/null +++ b/frontend/devices/components/hardware_settings/error_handling.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; +import { ToolTips, DeviceSetting } from "../../../constants"; +import { ErrorHandlingProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { McuInputBox } from "../mcu_input_box"; +import { settingToggle } from "../../actions"; +import { SingleSettingRow } from "./single_setting_row"; +import { ToggleButton } from "../../../controls/toggle_button"; +import { Highlight } from "../maybe_highlight"; +import { SpacePanelHeader } from "./space_panel_header"; + +export function ErrorHandling(props: ErrorHandlingProps) { + + const { error_handling } = props.controlPanelState; + const { dispatch, sourceFwConfig } = props; + const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); + + return +
    + +
    + +
    + + + + + + dispatch( + settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} /> + +
    + ; +} diff --git a/frontend/devices/components/hardware_settings/export_menu.tsx b/frontend/devices/components/hardware_settings/export_menu.tsx index 67546b166..bfd4b0ffd 100644 --- a/frontend/devices/components/hardware_settings/export_menu.tsx +++ b/frontend/devices/components/hardware_settings/export_menu.tsx @@ -25,7 +25,7 @@ const getSubKeyName = (key: string) => { }; export const FwParamExportMenu = - ({ firmwareConfig }: { firmwareConfig: FirmwareConfig }) => { + ({ firmwareConfig }: { firmwareConfig: FirmwareConfig | undefined }) => { /** Filter out unnecessary parameters. */ const filteredConfig = pickBy(firmwareConfig, (_, key) => !["id", "device_id", "api_migrated", "created_at", "updated_at", @@ -90,7 +90,6 @@ export const uncondenseFwConfig = Object.entries(obj).map(([subKey, value]) => { const fwConfigKey = subKey != "" ? `${key}_${subKey}` : key; uncondensedFwConfig[fwConfigKey] = value; - } - )); + })); return uncondensedFwConfig; }; diff --git a/frontend/devices/components/hardware_settings/header.tsx b/frontend/devices/components/hardware_settings/header.tsx index 3536d115a..f072cad13 100644 --- a/frontend/devices/components/hardware_settings/header.tsx +++ b/frontend/devices/components/hardware_settings/header.tsx @@ -2,18 +2,20 @@ import * as React from "react"; import { ControlPanelState } from "../../interfaces"; import { toggleControlPanel } from "../../actions"; import { ExpandableHeader } from "../../../ui/expandable_header"; +import { t } from "../../../i18next_wrapper"; +import { DeviceSetting } from "../../../constants"; interface Props { dispatch: Function; - name: keyof ControlPanelState; - title: string; + panel: keyof ControlPanelState; + title: DeviceSetting; expanded: boolean; } export const Header = (props: Props) => { - const { dispatch, name, title, expanded } = props; + const { dispatch, panel, title, expanded } = props; return dispatch(toggleControlPanel(name))} />; + title={t(title)} + onClick={() => dispatch(toggleControlPanel(panel))} />; }; diff --git a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx index 7775539a8..21fb3ed7f 100644 --- a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx +++ b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx @@ -1,20 +1,26 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; -import { HomingRow } from "./homing_row"; import { CalibrationRow } from "./calibration_row"; -import { ZeroRow } from "./zero_row"; import { disabledAxisMap } from "../axis_tracking_status"; import { HomingAndCalibrationProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; import { t } from "../../../i18next_wrapper"; import { calculateScale } from "./motors"; +import { hasEncoders } from "../firmware_hardware_support"; +import { getDevice } from "../../../device"; +import { commandErr } from "../../actions"; +import { CONFIG_DEFAULTS } from "farmbot/dist/config"; +import { Highlight } from "../maybe_highlight"; +import { SpacePanelHeader } from "./space_panel_header"; export function HomingAndCalibration(props: HomingAndCalibrationProps) { - const { dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected + const { + dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected, + firmwareHardware } = props; const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params; const { homing_and_calibration } = props.bot.controlPanelState; @@ -27,19 +33,54 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { const scale = calculateScale(sourceFwConfig); - return
    + return
    - - - +
    + +
    + getDevice() + .findHome({ speed: CONFIG_DEFAULTS.speed, axis }) + .catch(commandErr("'Find Home' request"))} + hardware={hardware} + botDisconnected={botDisconnected} /> + getDevice().calibrate({ axis }) + .catch(commandErr("Calibration"))} + hardware={hardware} + botDisconnected={botDisconnected} /> + getDevice().setZero(axis) + .catch(commandErr("Zeroing"))} + hardware={hardware} + botDisconnected={botDisconnected} /> -
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/homing_row.tsx b/frontend/devices/components/hardware_settings/homing_row.tsx deleted file mode 100644 index bac27807c..000000000 --- a/frontend/devices/components/hardware_settings/homing_row.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; -import { HomingRowProps } from "../interfaces"; -import { LockableButton } from "../lockable_button"; -import { axisTrackingStatus } from "../axis_tracking_status"; -import { ToolTips } from "../../../constants"; -import { Row, Col, Help } from "../../../ui/index"; -import { CONFIG_DEFAULTS } from "farmbot/dist/config"; -import { commandErr } from "../../actions"; -import { Axis } from "../../interfaces"; -import { getDevice } from "../../../device"; -import { t } from "../../../i18next_wrapper"; -import { Position } from "@blueprintjs/core"; - -const speed = CONFIG_DEFAULTS.speed; -const findHome = (axis: Axis) => getDevice() - .findHome({ speed, axis }) - .catch(commandErr("'Find Home' request")); - -export function HomingRow(props: HomingRowProps) { - const { hardware, botDisconnected } = props; - - return - - - - - {axisTrackingStatus(hardware) - .map((row) => { - const { axis, disabled } = row; - return - findHome(axis)}> - {t("FIND HOME {{axis}}", { axis })} - - ; - })} - ; -} diff --git a/frontend/devices/components/hardware_settings/motors.tsx b/frontend/devices/components/hardware_settings/motors.tsx index d76d6987e..4464b32bc 100644 --- a/frontend/devices/components/hardware_settings/motors.tsx +++ b/frontend/devices/components/hardware_settings/motors.tsx @@ -1,40 +1,23 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { ToggleButton } from "../../../controls/toggle_button"; import { settingToggle } from "../../actions"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { MotorsProps } from "../interfaces"; -import { Row, Col, Help } from "../../../ui/index"; import { Header } from "./header"; -import { Collapse, Position } from "@blueprintjs/core"; -import { McuInputBox } from "../mcu_input_box"; -import { t } from "../../../i18next_wrapper"; +import { Collapse } from "@blueprintjs/core"; import { Xyz, McuParamName } from "farmbot"; import { SourceFwConfig } from "../../interfaces"; import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props"; -import { isTMCBoard, isExpressBoard } from "../firmware_hardware_support"; - -const SingleSettingRow = - ({ label, tooltip, settingType, children }: { - label: string, - tooltip: string, - children: React.ReactChild, - settingType: "button" | "input", - }) => - - - - - - {settingType === "button" - ? {children} - : {children}} - ; +import { isTMCBoard } from "../firmware_hardware_support"; +import { SingleSettingRow } from "./single_setting_row"; +import { Highlight } from "../maybe_highlight"; +import { SpacePanelHeader } from "./space_panel_header"; export const calculateScale = (sourceFwConfig: SourceFwConfig): Record => { - const getV = (name: McuParamName) => sourceFwConfig(name).value; + const getV = (key: McuParamName) => sourceFwConfig(key).value; return { x: calcMicrostepsPerMm(getV("movement_step_per_mm_x"), getV("movement_microsteps_x")), @@ -51,39 +34,21 @@ export function Motors(props: MotorsProps) { } = props; const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x"); const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); - const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); const scale = calculateScale(sourceFwConfig); - const encodersDisabled = { - x: !sourceFwConfig("encoder_enabled_x").value, - y: !sourceFwConfig("encoder_enabled_y").value, - z: !sourceFwConfig("encoder_enabled_z").value, - }; - return
    + + return
    - - - - - dispatch( - settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} /> - +
    + +
    {isTMCBoard(firmwareHardware) && } - {isExpressBoard(firmwareHardware) && - }
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/pin_bindings.tsx b/frontend/devices/components/hardware_settings/pin_bindings.tsx new file mode 100644 index 000000000..04be71214 --- /dev/null +++ b/frontend/devices/components/hardware_settings/pin_bindings.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { PinBindingsProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { PinBindingsContent } from "../../pin_bindings/pin_bindings"; +import { DeviceSetting } from "../../../constants"; +import { Highlight } from "../maybe_highlight"; + +export function PinBindings(props: PinBindingsProps) { + + const { pin_bindings } = props.controlPanelState; + const { dispatch, resources, firmwareHardware } = props; + + return +
    + + + + ; +} diff --git a/frontend/devices/components/hardware_settings/pin_guard.tsx b/frontend/devices/components/hardware_settings/pin_guard.tsx index 9bd49c83b..1a40611b9 100644 --- a/frontend/devices/components/hardware_settings/pin_guard.tsx +++ b/frontend/devices/components/hardware_settings/pin_guard.tsx @@ -4,19 +4,21 @@ import { PinGuardProps } from "../interfaces"; import { Header } from "./header"; import { Collapse, Position } from "@blueprintjs/core"; import { Row, Col, Help } from "../../../ui/index"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function PinGuard(props: PinGuardProps) { const { pin_guard } = props.controlPanelState; const { dispatch, sourceFwConfig, resources } = props; - return
    + return
    @@ -39,7 +41,7 @@ export function PinGuard(props: PinGuardProps) { -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/single_setting_row.tsx b/frontend/devices/components/hardware_settings/single_setting_row.tsx new file mode 100644 index 000000000..0ab1c4bba --- /dev/null +++ b/frontend/devices/components/hardware_settings/single_setting_row.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { Row, Col, Help } from "../../../ui/index"; +import { Position } from "@blueprintjs/core"; +import { DeviceSetting } from "../../../constants"; +import { Highlight } from "../maybe_highlight"; +import { t } from "../../../i18next_wrapper"; + +export interface SingleSettingRowProps { + label: DeviceSetting; + tooltip: string; + children: React.ReactChild; + settingType: "button" | "input"; +} + +export const SingleSettingRow = + ({ label, tooltip, settingType, children }: SingleSettingRowProps) => + + + + + + + {settingType === "button" + ? {children} + : {children}} + + ; diff --git a/frontend/devices/components/hardware_settings/zero_row.tsx b/frontend/devices/components/hardware_settings/zero_row.tsx deleted file mode 100644 index 809fffed5..000000000 --- a/frontend/devices/components/hardware_settings/zero_row.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { getDevice } from "../../../device"; -import { Axis } from "../../interfaces"; -import { ToolTips } from "../../../constants"; -import { Row, Col, Help } from "../../../ui/index"; -import { ZeroRowProps } from "../interfaces"; -import { commandErr } from "../../actions"; -import { t } from "../../../i18next_wrapper"; -import { Position } from "@blueprintjs/core"; - -const zero = - (axis: Axis) => getDevice().setZero(axis).catch(commandErr("Zeroing")); -const AXES: Axis[] = ["x", "y", "z"]; - -export function ZeroButton(props: { axis: Axis; disabled: boolean; }) { - const { axis, disabled } = props; - return ; -} - -export function ZeroRow({ botDisconnected }: ZeroRowProps) { - return - - - - - {AXES.map((axis) => { - return - - ; - })} - ; -} diff --git a/frontend/devices/components/interfaces.ts b/frontend/devices/components/interfaces.ts index 75fd5141e..cfb683fd7 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -1,16 +1,12 @@ import { BotState, Xyz, SourceFwConfig, - ControlPanelState, ShouldDisplay + ControlPanelState, Axis, } from "../interfaces"; import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist"; import { IntegerSize } from "../../util"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { ResourceIndex } from "../../resources/interfaces"; - -export interface HomingRowProps { - hardware: McuParams; - botDisconnected: boolean; -} +import { DeviceSetting } from "../../constants"; export interface ZeroRowProps { botDisconnected: boolean; @@ -19,16 +15,18 @@ export interface ZeroRowProps { export interface HomingAndCalibrationProps { dispatch: Function; bot: BotState; + controlPanelState: ControlPanelState; sourceFwConfig: SourceFwConfig; firmwareConfig: FirmwareConfig | undefined; botDisconnected: boolean; + firmwareHardware: FirmwareHardware | undefined; } export interface BooleanMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; tooltip: string; - name: string; + label: DeviceSetting; x: McuParamName; y: McuParamName; z: McuParamName; @@ -39,15 +37,20 @@ export interface BooleanMCUInputGroupProps { } export interface CalibrationRowProps { + type: "find_home" | "calibrate" | "zero"; hardware: McuParams; botDisconnected: boolean; + action(axis: Axis): void; + toolTip: string; + title: DeviceSetting; + axisTitle: string; } export interface NumericMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; tooltip: string; - name: string; + label: DeviceSetting; x: McuParamName; xScale?: number; y: McuParamName; @@ -62,7 +65,7 @@ export interface NumericMCUInputGroupProps { export interface PinGuardMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; - name: string; + label: string; pinNumKey: McuParamName; timeoutKey: McuParamName; activeStateKey: McuParamName; @@ -85,12 +88,30 @@ export interface MotorsProps { export interface EncodersProps { dispatch: Function; - shouldDisplay: ShouldDisplay; controlPanelState: ControlPanelState; sourceFwConfig: SourceFwConfig; firmwareHardware: FirmwareHardware | undefined; } +export interface EndStopsProps { + dispatch: Function; + controlPanelState: ControlPanelState; + sourceFwConfig: SourceFwConfig; +} + +export interface ErrorHandlingProps { + dispatch: Function; + controlPanelState: ControlPanelState; + sourceFwConfig: SourceFwConfig; +} + +export interface PinBindingsProps { + dispatch: Function; + controlPanelState: ControlPanelState; + resources: ResourceIndex; + firmwareHardware: FirmwareHardware | undefined; +} + export interface DangerZoneProps { dispatch: Function; controlPanelState: ControlPanelState; diff --git a/frontend/devices/components/lockable_button.tsx b/frontend/devices/components/lockable_button.tsx index 65ace1da2..1bcd74f5c 100644 --- a/frontend/devices/components/lockable_button.tsx +++ b/frontend/devices/components/lockable_button.tsx @@ -4,13 +4,15 @@ interface Props { onClick: Function; disabled: boolean; children?: React.ReactNode; + title?: string; } -export function LockableButton({ onClick, disabled, children }: Props) { +export function LockableButton({ onClick, disabled, children, title }: Props) { const className = disabled ? "gray" : "yellow"; return ; diff --git a/frontend/devices/components/maybe_highlight.tsx b/frontend/devices/components/maybe_highlight.tsx new file mode 100644 index 000000000..6fefacf33 --- /dev/null +++ b/frontend/devices/components/maybe_highlight.tsx @@ -0,0 +1,203 @@ +import * as React from "react"; +import { ControlPanelState } from "../interfaces"; +import { toggleControlPanel } from "../actions"; +import { urlFriendly } from "../../util"; +import { DeviceSetting } from "../../constants"; +import { trim } from "lodash"; + +const HOMING_PANEL = [ + DeviceSetting.homingAndCalibration, + DeviceSetting.homing, + DeviceSetting.calibration, + DeviceSetting.setZeroPosition, + DeviceSetting.findHomeOnBoot, + DeviceSetting.stopAtHome, + DeviceSetting.stopAtMax, + DeviceSetting.negativeCoordinatesOnly, + DeviceSetting.axisLength, +]; +const MOTORS_PANEL = [ + DeviceSetting.motors, + DeviceSetting.maxSpeed, + DeviceSetting.homingSpeed, + DeviceSetting.minimumSpeed, + DeviceSetting.accelerateFor, + DeviceSetting.stepsPerMm, + DeviceSetting.microstepsPerStep, + DeviceSetting.alwaysPowerMotors, + DeviceSetting.invertMotors, + DeviceSetting.motorCurrent, + DeviceSetting.enable2ndXMotor, + DeviceSetting.invert2ndXMotor, +]; +const ENCODERS_PANEL = [ + DeviceSetting.encoders, + DeviceSetting.stallDetection, + DeviceSetting.enableEncoders, + DeviceSetting.enableStallDetection, + DeviceSetting.stallSensitivity, + DeviceSetting.useEncodersForPositioning, + DeviceSetting.invertEncoders, + DeviceSetting.maxMissedSteps, + DeviceSetting.missedStepDecay, + DeviceSetting.encoderScaling, +]; +const ENDSTOPS_PANEL = [ + DeviceSetting.endstops, + DeviceSetting.enableEndstops, + DeviceSetting.swapEndstops, + DeviceSetting.invertEndstops, +]; +const ERROR_HANDLING_PANEL = [ + DeviceSetting.errorHandling, + DeviceSetting.timeoutAfter, + DeviceSetting.maxRetries, + DeviceSetting.estopOnMovementError, +]; +const PIN_GUARD_PANEL = [ + DeviceSetting.pinGuard, +]; +const DANGER_ZONE_PANEL = [ + DeviceSetting.dangerZone, + DeviceSetting.resetHardwareParams, +]; +const PIN_BINDINGS_PANEL = [ + DeviceSetting.pinBindings, +]; +const POWER_AND_RESET_PANEL = [ + DeviceSetting.powerAndReset, + DeviceSetting.restartFarmbot, + DeviceSetting.shutdownFarmbot, + DeviceSetting.restartFirmware, + DeviceSetting.factoryReset, + DeviceSetting.autoFactoryReset, + DeviceSetting.connectionAttemptPeriod, + DeviceSetting.changeOwnership, +]; + +const FARM_DESIGNER_PANEL = [ + DeviceSetting.farmDesigner, + DeviceSetting.animations, + DeviceSetting.trail, + DeviceSetting.dynamicMap, + DeviceSetting.mapSize, + DeviceSetting.rotateMap, + DeviceSetting.mapOrigin, + DeviceSetting.confirmPlantDeletion, +]; + +const FIRMWARE_PANEL = [ + DeviceSetting.firmwareSection, + DeviceSetting.firmware, + DeviceSetting.flashFirmware, + DeviceSetting.restartFirmware, +]; + +const FARMBOT_PANEL = [ + DeviceSetting.farmbot, + DeviceSetting.name, + DeviceSetting.timezone, + DeviceSetting.camera, + DeviceSetting.applySoftwareUpdates, + DeviceSetting.farmbotOSAutoUpdate, + DeviceSetting.farmbotOS, + DeviceSetting.autoSync, + DeviceSetting.bootSequence, +]; + +/** Look up parent panels for settings. */ +const SETTING_PANEL_LOOKUP = {} as Record; +HOMING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "homing_and_calibration"); +MOTORS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "motors"); +ENCODERS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "encoders"); +ENDSTOPS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "endstops"); +ERROR_HANDLING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "error_handling"); +PIN_GUARD_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_guard"); +DANGER_ZONE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "danger_zone"); +PIN_BINDINGS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_bindings"); +POWER_AND_RESET_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "power_and_reset"); +FARM_DESIGNER_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "farm_designer"); +FIRMWARE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "firmware"); +FARMBOT_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "farmbot_os"); + +/** Keep string up until first `(` character (trailing whitespace removed). */ +const stripUnits = (settingName: string) => trim(settingName.split("(")[0]); + +/** Look up parent panels for settings using URL-friendly names. */ +const URL_FRIENDLY_LOOKUP: Record = {}; +Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) => { + URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel; + URL_FRIENDLY_LOOKUP[urlFriendly(stripUnits(setting))] = panel; +}); + +/** Look up all relevant names for the same setting. */ +const ALTERNATE_NAMES = + Object.values(DeviceSetting).reduce((acc, s) => { acc[s] = [s]; return acc; }, + {} as Record); +ALTERNATE_NAMES[DeviceSetting.encoders].push(DeviceSetting.stallDetection); +ALTERNATE_NAMES[DeviceSetting.stallDetection].push(DeviceSetting.encoders); + +/** Generate array of names for the same setting. Most only have one. */ +const compareValues = (settingName: DeviceSetting) => + (ALTERNATE_NAMES[settingName] as string[]) + .concat(stripUnits(settingName)) + .map(s => urlFriendly(s)); + +/** Retrieve a highlight search term. */ +const getHighlightName = () => location.search.split("?highlight=").pop(); + +/** Only open panel and highlight once per app load. Exported for tests. */ +export const highlight = { opened: false, highlighted: false }; + +/** Open a panel if a setting in that panel is highlighted. */ +export const maybeOpenPanel = (panelState: ControlPanelState) => + (dispatch: Function) => { + if (highlight.opened) { return; } + const urlFriendlySettingName = urlFriendly(getHighlightName() || ""); + if (!urlFriendlySettingName) { return; } + const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName]; + const panelIsOpen = panelState[panel]; + if (panelIsOpen) { return; } + dispatch(toggleControlPanel(panel)); + highlight.opened = true; + }; + +/** Highlight a setting if provided as a search term. */ +export const maybeHighlight = (settingName: DeviceSetting) => { + const item = getHighlightName(); + if (highlight.highlighted || !item) { return ""; } + const isCurrentSetting = compareValues(settingName).includes(item); + if (!isCurrentSetting) { return ""; } + highlight.highlighted = true; + return "highlight"; +}; + +export interface HighlightProps { + settingName: DeviceSetting; + children: React.ReactChild + | React.ReactChild[] + | (React.ReactChild | React.ReactChild[])[]; + className?: string; +} + +interface HighlightState { + className: string; +} + +/** Wrap highlight-able settings. */ +export class Highlight extends React.Component { + state: HighlightState = { className: maybeHighlight(this.props.settingName) }; + + componentDidMount = () => { + if (this.state.className == "highlight") { + /** Slowly fades highlight. */ + this.setState({ className: "unhighlight" }); + } + } + + render() { + return
    + {this.props.children} +
    ; + } +} diff --git a/frontend/devices/components/mcu_input_box.tsx b/frontend/devices/components/mcu_input_box.tsx index eef345506..dd4dac9e5 100644 --- a/frontend/devices/components/mcu_input_box.tsx +++ b/frontend/devices/components/mcu_input_box.tsx @@ -4,7 +4,7 @@ import { McuInputBoxProps } from "../interfaces"; import { updateMCU } from "../actions"; import { BlurableInput } from "../../ui/index"; import { - clampUnsignedInteger, IntegerSize, getMaxInputFromIntSize + clampUnsignedInteger, IntegerSize, getMaxInputFromIntSize, } from "../../util"; import { isUndefined } from "lodash"; diff --git a/frontend/devices/components/numeric_mcu_input_group.tsx b/frontend/devices/components/numeric_mcu_input_group.tsx index 13397af32..c76b94041 100644 --- a/frontend/devices/components/numeric_mcu_input_group.tsx +++ b/frontend/devices/components/numeric_mcu_input_group.tsx @@ -3,48 +3,52 @@ import { McuInputBox } from "./mcu_input_box"; import { NumericMCUInputGroupProps } from "./interfaces"; import { Row, Col, Help } from "../../ui/index"; import { Position } from "@blueprintjs/core"; +import { Highlight } from "./maybe_highlight"; +import { t } from "../../i18next_wrapper"; export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) { const { - sourceFwConfig, dispatch, tooltip, name, x, y, z, intSize, gray, float, + sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float, } = props; return - - - - - - - - - - - - - + + + + + + + + + + + + + + + ; } diff --git a/frontend/devices/components/pin_guard_input_group.tsx b/frontend/devices/components/pin_guard_input_group.tsx index 4b9dc6272..ac401b79d 100644 --- a/frontend/devices/components/pin_guard_input_group.tsx +++ b/frontend/devices/components/pin_guard_input_group.tsx @@ -10,7 +10,7 @@ import { PinNumberDropdown } from "./pin_number_dropdown"; export function PinGuardMCUInputGroup(props: PinGuardMCUInputGroupProps) { - const { sourceFwConfig, dispatch, name, pinNumKey, timeoutKey, activeStateKey + const { sourceFwConfig, dispatch, label, pinNumKey, timeoutKey, activeStateKey } = props; const activeStateValue = sourceFwConfig(activeStateKey).value; const inactiveState = isUndefined(activeStateValue) @@ -19,7 +19,7 @@ export function PinGuardMCUInputGroup(props: PinGuardMCUInputGroupProps) { return diff --git a/frontend/devices/components/pin_number_dropdown.tsx b/frontend/devices/components/pin_number_dropdown.tsx index a706c1f36..a1fba8ab4 100644 --- a/frontend/devices/components/pin_number_dropdown.tsx +++ b/frontend/devices/components/pin_number_dropdown.tsx @@ -4,10 +4,10 @@ import { updateMCU } from "../actions"; import { isNumber } from "lodash"; import { t } from "../../i18next_wrapper"; import { - pinDropdowns, celery2DropDown, PinGroupName, PERIPHERAL_HEADING + pinDropdowns, celery2DropDown, PinGroupName, PERIPHERAL_HEADING, } from "../../sequences/step_tiles/pin_and_peripheral_support"; import { - selectAllPeripherals, selectAllSavedPeripherals + selectAllPeripherals, selectAllSavedPeripherals, } from "../../resources/selectors"; import { Dictionary, NamedPin, McuParamName } from "farmbot"; import { ResourceIndex } from "../../resources/interfaces"; @@ -56,8 +56,12 @@ const pinNumOrNamedPin = } : pin; +const DISABLE_DDI = (): DropDownItem => ({ + label: t("None"), value: 0 +}); + const listItems = (resources: ResourceIndex): DropDownItem[] => - [...peripheralItems(resources), ...pinDropdowns(n => n)]; + [DISABLE_DDI(), ...peripheralItems(resources), ...pinDropdowns(n => n)]; const peripheralItems = (resources: ResourceIndex): DropDownItem[] => { const list = selectAllSavedPeripherals(resources) diff --git a/frontend/devices/components/source_config_value.ts b/frontend/devices/components/source_config_value.ts index 3d19bdd62..1dc3eba39 100644 --- a/frontend/devices/components/source_config_value.ts +++ b/frontend/devices/components/source_config_value.ts @@ -1,12 +1,12 @@ import { - Configuration, ConfigurationName, McuParams, McuParamName + Configuration, ConfigurationName, McuParams, McuParamName, } from "farmbot"; import { SourceFbosConfig, SourceFwConfig } from "../interfaces"; import { FbosConfig } from "farmbot/dist/resources/configs/fbos"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; export const sourceFbosConfigValue = - (apiConfig: FbosConfig | undefined, botConfig: Configuration + (apiConfig: FbosConfig | undefined, botConfig: Configuration, ): SourceFbosConfig => (setting: ConfigurationName) => { const apiValue = apiConfig && apiConfig[setting as keyof FbosConfig]; @@ -18,7 +18,7 @@ export const sourceFbosConfigValue = }; export const sourceFwConfigValue = - (apiConfig: FirmwareConfig | undefined, botConfig: McuParams + (apiConfig: FirmwareConfig | undefined, botConfig: McuParams, ): SourceFwConfig => (setting: McuParamName) => { const apiValue = apiConfig && apiConfig[setting]; diff --git a/frontend/devices/connectivity/__tests__/diagram_test.tsx b/frontend/devices/connectivity/__tests__/diagram_test.tsx index d031c7ac0..9591d103e 100644 --- a/frontend/devices/connectivity/__tests__/diagram_test.tsx +++ b/frontend/devices/connectivity/__tests__/diagram_test.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import { mount } from "enzyme"; import { ConnectivityDiagram, ConnectivityDiagramProps, @@ -9,7 +8,7 @@ import { getTextPosition, getLineProps, DiagramNodes, - getConnectionColor + getConnectionColor, } from "../diagram"; import { Color } from "../../../ui/index"; import { svgMount } from "../../../__test_support__/svg_mount"; @@ -83,9 +82,9 @@ describe("getTextPosition()", () => { describe("nodeLabel()", () => { it("renders", () => { - const label = mount(nodeLabel("Top Node", "top" as DiagramNodes)); - expect(label.text()).toEqual("Top Node"); - expect(label.props()) + const label = svgMount(nodeLabel("Top Node", "top" as DiagramNodes)); + expect(label.find("text").text()).toEqual("Top Node"); + expect(label.find("text").props()) .toEqual({ children: "Top Node", textAnchor: "middle", x: 0, y: -75 }); }); }); diff --git a/frontend/devices/connectivity/__tests__/qos_test.ts b/frontend/devices/connectivity/__tests__/qos_test.ts index 63c144426..6a8f9f7d4 100644 --- a/frontend/devices/connectivity/__tests__/qos_test.ts +++ b/frontend/devices/connectivity/__tests__/qos_test.ts @@ -4,10 +4,10 @@ import { completePing, startPing, failPing, - PingDictionary + PingDictionary, } from "../qos"; import { - fakePings + fakePings, } from "../../../__test_support__/fake_state/pings"; describe("QoS helpers", () => { diff --git a/frontend/devices/connectivity/__tests__/retry_btn_test.tsx b/frontend/devices/connectivity/__tests__/retry_btn_test.tsx deleted file mode 100644 index 5aec1fa77..000000000 --- a/frontend/devices/connectivity/__tests__/retry_btn_test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from "react"; -import { shallow } from "enzyme"; -import { RetryBtn } from "../retry_btn"; -import { SpecialStatus } from "farmbot"; - -describe("", () => { - it("is green before saving", () => { - const props = { - flags: [true], - onClick: jest.fn(), - status: SpecialStatus.SAVED - }; - const el = shallow(); - expect(el.find(".green").length).toBe(1); - expect(el.find(".yellow").length).toBe(0); - expect(el.find(".red").length).toBe(0); - }); - - it("is yellow during save", () => { - const props = { - flags: [false, true], - onClick: jest.fn(), - status: SpecialStatus.SAVING - }; - const el = shallow(); - expect(el.find(".green").length).toBe(0); - expect(el.find(".yellow").length).toBe(1); - expect(el.find(".red").length).toBe(0); - }); - - it("is red when problems arise", () => { - const props = { - flags: [true, false], - onClick: jest.fn(), - status: SpecialStatus.SAVED - }; - const el = shallow(); - expect(el.find(".green").length).toBe(0); - expect(el.find(".yellow").length).toBe(0); - expect(el.find(".red").length).toBe(1); - }); -}); diff --git a/frontend/devices/connectivity/__tests__/status_checks_test.ts b/frontend/devices/connectivity/__tests__/status_checks_test.ts index 7070678d2..d614a738c 100644 --- a/frontend/devices/connectivity/__tests__/status_checks_test.ts +++ b/frontend/devices/connectivity/__tests__/status_checks_test.ts @@ -1,5 +1,5 @@ import { - browserToMQTT, botToMQTT, botToAPI, botToFirmware, browserToAPI + browserToMQTT, botToMQTT, botToAPI, botToFirmware, browserToAPI, } from "../status_checks"; import moment from "moment"; import { ConnectionStatus } from "../../../connectivity/interfaces"; diff --git a/frontend/devices/connectivity/connectivity.tsx b/frontend/devices/connectivity/connectivity.tsx index 54acb72d3..aa4920236 100644 --- a/frontend/devices/connectivity/connectivity.tsx +++ b/frontend/devices/connectivity/connectivity.tsx @@ -5,7 +5,7 @@ import { ConnectivityRow, StatusRowProps } from "./connectivity_row"; import { Row, Col } from "../../ui"; import { ConnectivityDiagram } from "./diagram"; import { - ChipTemperatureDisplay, WiFiStrengthDisplay, VoltageDisplay + ChipTemperatureDisplay, WiFiStrengthDisplay, VoltageDisplay, } from "../components/fbos_settings/fbos_details"; import { t } from "../../i18next_wrapper"; import { QosPanel } from "./qos_panel"; @@ -26,12 +26,14 @@ export class Connectivity extends React.Component { state: ConnectivityState = { hoveredConnection: undefined }; - hover = (name: string) => - () => this.setState({ hoveredConnection: name }); + hover = (connectionName: string) => + () => this.setState({ hoveredConnection: connectionName }); render() { const { informational_settings } = this.props.bot.hardware; - const { soc_temp, wifi_level, throttled } = informational_settings; + const { + soc_temp, wifi_level, throttled, wifi_level_percent + } = informational_settings; return
    @@ -42,7 +44,8 @@ export class Connectivity
    - +
    diff --git a/frontend/devices/connectivity/diagnosis.tsx b/frontend/devices/connectivity/diagnosis.tsx index f6fb0c9a1..de6b2ece5 100644 --- a/frontend/devices/connectivity/diagnosis.tsx +++ b/frontend/devices/connectivity/diagnosis.tsx @@ -28,7 +28,7 @@ export function Diagnosis(props: DiagnosisProps) { const diagnosisBoolean = diagnosisStatus(props); const diagnosisColor = diagnosisBoolean ? "green" : "red"; const title = diagnosisBoolean ? t("Ok") : t("Error"); - return
    + return

    {t("Diagnosis")}

    diff --git a/frontend/devices/connectivity/diagram.tsx b/frontend/devices/connectivity/diagram.tsx index 3d0f61a66..99df138c1 100644 --- a/frontend/devices/connectivity/diagram.tsx +++ b/frontend/devices/connectivity/diagram.tsx @@ -49,8 +49,9 @@ const diagramPositions: CowardlyDictionary> = { subRight: { x: 40, y: 110 } }; -export function getTextPosition(name: DiagramNodes): Record<"x" | "y", number> { - const position = diagramPositions[name]; +export function getTextPosition( + positionKey: DiagramNodes): Record<"x" | "y", number> { + const position = diagramPositions[positionKey]; if (position) { return { x: position.x, diff --git a/frontend/devices/connectivity/generate_data.ts b/frontend/devices/connectivity/generate_data.ts index 6265a24b1..1ab0659c1 100644 --- a/frontend/devices/connectivity/generate_data.ts +++ b/frontend/devices/connectivity/generate_data.ts @@ -3,7 +3,7 @@ import { BotState } from "../interfaces"; import { DiagnosisName, DiagnosisProps } from "./diagnosis"; import { StatusRowProps } from "./connectivity_row"; import { - browserToMQTT, browserToAPI, botToMQTT, botToAPI, botToFirmware + browserToMQTT, browserToAPI, botToMQTT, botToAPI, botToFirmware, } from "./status_checks"; interface ConnectivityDataProps { diff --git a/frontend/devices/connectivity/retry_btn.tsx b/frontend/devices/connectivity/retry_btn.tsx deleted file mode 100644 index ac3e0bd8a..000000000 --- a/frontend/devices/connectivity/retry_btn.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from "react"; -import { SpecialStatus } from "farmbot"; -import { t } from "../../i18next_wrapper"; - -interface RetryBtnProps { - flags: boolean[]; - onClick(): void; - status: SpecialStatus; -} - -export function RetryBtn(props: RetryBtnProps) { - const failures = props.flags.includes(false); - const color = failures ? "red" : "green"; - const css = props.status === "SAVING" ? "yellow" : color; - return ; -} diff --git a/frontend/devices/connectivity/status_checks.tsx b/frontend/devices/connectivity/status_checks.tsx index e2190c99c..2cc649d19 100644 --- a/frontend/devices/connectivity/status_checks.tsx +++ b/frontend/devices/connectivity/status_checks.tsx @@ -4,7 +4,7 @@ import { StatusRowProps } from "./connectivity_row"; import { ConnectionStatus } from "../../connectivity/interfaces"; import { t } from "../../i18next_wrapper"; import { - getBoardCategory, isKnownBoard + getBoardCategory, isKnownBoard, } from "../components/firmware_hardware_support"; /** " ago" for a given ISO time string or time in milliseconds. */ diff --git a/frontend/devices/connectivity/truth_table.ts b/frontend/devices/connectivity/truth_table.ts index 2a95f5eaf..3ff3ebc08 100644 --- a/frontend/devices/connectivity/truth_table.ts +++ b/frontend/devices/connectivity/truth_table.ts @@ -1,5 +1,11 @@ import { Dictionary } from "farmbot"; import { DiagnosticMessages } from "../../constants"; +import { docLink } from "../../ui/doc_link"; +import { trim } from "../../util/util"; + +const DiagnosticMessagesWiFiOrConfig = + trim(`${DiagnosticMessages.WIFI_OR_CONFIG} + ${docLink("for-it-security-professionals")}`); // I don't like this at all. // If anyone has a cleaner solution, I'd love to hear it. @@ -16,13 +22,13 @@ export const TRUTH_TABLE: Readonly> = { // 17: No MQTT connections. [0b10001]: DiagnosticMessages.NO_WS_AVAILABLE, // 24: Browser is connected to API and MQTT. - [0b11000]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b11000]: DiagnosticMessagesWiFiOrConfig, // 9: At least the browser is connected to MQTT. - [0b01001]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b01001]: DiagnosticMessagesWiFiOrConfig, // 8: At least the browser is connected to MQTT. - [0b01000]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b01000]: DiagnosticMessagesWiFiOrConfig, // 25: Farmbot offline. - [0b11001]: DiagnosticMessages.WIFI_OR_CONFIG, + [0b11001]: DiagnosticMessagesWiFiOrConfig, // 2: Browser offline. Farmbot last seen by the API recently. [0b00010]: DiagnosticMessages.NO_WS_AVAILABLE, // 18: Farmbot last seen by the API recently. diff --git a/frontend/devices/devices.tsx b/frontend/devices/devices.tsx index 1d45a8e46..a0970b240 100644 --- a/frontend/devices/devices.tsx +++ b/frontend/devices/devices.tsx @@ -5,7 +5,6 @@ import { FarmbotOsSettings } from "./components/farmbot_os_settings"; import { Page, Col, Row } from "../ui/index"; import { mapStateToProps } from "./state_to_props"; import { Props } from "./interfaces"; -import { PinBindings } from "./pin_bindings/pin_bindings"; import { getStatus } from "../connectivity/reducer_support"; import { isFwHardwareValue } from "./components/firmware_hardware_support"; @@ -48,9 +47,6 @@ export class RawDevices extends React.Component { firmwareHardware={firmwareHardware} sourceFwConfig={this.props.sourceFwConfig} firmwareConfig={this.props.firmwareConfig} /> - ; diff --git a/frontend/devices/interfaces.ts b/frontend/devices/interfaces.ts index dd24b9a58..7335c30a1 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -15,7 +15,7 @@ import { import { ResourceIndex } from "../resources/interfaces"; import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces"; import { - ConnectionStatus, ConnectionState, NetworkState + ConnectionStatus, ConnectionState, NetworkState, } from "../connectivity/interfaces"; import { IntegerSize } from "../util"; import { Farmwares } from "../farmware/interfaces"; @@ -93,7 +93,7 @@ export enum Feature { variables = "variables", } -/** Object fetched from FEATURE_MIN_VERSIONS_URL. */ +/** Object fetched from ExternalUrl.featureMinVersions. */ export type MinOsFeatureLookup = Partial>; export interface BotState { @@ -201,6 +201,7 @@ export interface PeripheralsProps { peripherals: TaggedPeripheral[]; dispatch: Function; disabled: boolean | undefined; + firmwareHardware: FirmwareHardware | undefined; } export interface SensorsProps { @@ -208,6 +209,7 @@ export interface SensorsProps { sensors: TaggedSensor[]; dispatch: Function; disabled: boolean | undefined; + firmwareHardware: FirmwareHardware | undefined; } export interface FarmwareProps { @@ -245,8 +247,14 @@ export interface HardwareSettingsProps { export interface ControlPanelState { homing_and_calibration: boolean; motors: boolean; - encoders_and_endstops: boolean; - danger_zone: boolean; - power_and_reset: boolean; + encoders: boolean; + endstops: boolean; + error_handling: boolean; pin_guard: boolean; + danger_zone: boolean; + pin_bindings: boolean; + power_and_reset: boolean; + farm_designer: boolean; + firmware: boolean; + farmbot_os: boolean; } diff --git a/frontend/devices/must_be_online.tsx b/frontend/devices/must_be_online.tsx index 8da2d83d1..122eb715b 100644 --- a/frontend/devices/must_be_online.tsx +++ b/frontend/devices/must_be_online.tsx @@ -27,7 +27,7 @@ export function MustBeOnline(props: MBOProps) { const { children, hideBanner, lockOpen, networkState, syncStatus } = props; const banner = hideBanner ? "" : "banner"; if (isBotOnline(syncStatus, networkState) || lockOpen) { - return
    {children}
    ; + return
    {children}
    ; } else { return
    { @@ -26,3 +29,11 @@ describe("sortByNameAndPin()", () => { sortTest(1, 1, Order.equal); // GPIO 1 == GPIO 1 }); }); + +describe("getSpecialActionLabel()", () => { + it("handles undefined values", () => { + expect(getSpecialActionLabel(undefined)).toEqual("None"); + expect(getSpecialActionLabel("wrong" as PinBindingSpecialAction)) + .toEqual(""); + }); +}); diff --git a/frontend/devices/pin_bindings/__tests__/pin_binding_input_group_test.tsx b/frontend/devices/pin_bindings/__tests__/pin_binding_input_group_test.tsx index 2b7cb1fa8..bc8dc30b8 100644 --- a/frontend/devices/pin_bindings/__tests__/pin_binding_input_group_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/pin_binding_input_group_test.tsx @@ -9,23 +9,23 @@ jest.mock("../../../api/crud", () => ({ initSave: jest.fn() })); import * as React from "react"; import { mount, shallow } from "enzyme"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { TaggedSequence } from "farmbot"; import { - fakeSequence + fakeSequence, } from "../../../__test_support__/fake_state/resources"; import { initSave } from "../../../api/crud"; import { PinBindingInputGroupProps } from "../interfaces"; import { PinBindingInputGroup, PinNumberInputGroup, BindingTypeDropDown, - ActionTargetDropDown, SequenceTargetDropDown + ActionTargetDropDown, SequenceTargetDropDown, } from "../pin_binding_input_group"; import { - fakeResourceIndex + fakeResourceIndex, } from "../../../sequences/locals_list/test_helpers"; import { - PinBindingType, PinBindingSpecialAction + PinBindingType, PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; import { error, warning } from "../../../toast/toast"; @@ -58,7 +58,7 @@ describe("", () => { it("no pin selected", () => { const wrapper = mount(); const buttons = wrapper.find("button"); - expect(buttons.last().text()).toEqual("BIND"); + expect(buttons.last().props().title).toEqual("BIND"); buttons.last().simulate("click"); expect(error).toHaveBeenCalledWith("Pin number cannot be blank."); }); @@ -66,7 +66,7 @@ describe("", () => { it("no target selected", () => { const wrapper = mount(); const buttons = wrapper.find("button"); - expect(buttons.last().text()).toEqual("BIND"); + expect(buttons.last().props().title).toEqual("BIND"); wrapper.setState({ pinNumberInput: AVAILABLE_PIN }); buttons.last().simulate("click"); expect(error).toHaveBeenCalledWith("Please select a sequence or action."); @@ -77,7 +77,7 @@ describe("", () => { p.dispatch = jest.fn(); const wrapper = mount(); const buttons = wrapper.find("button"); - expect(buttons.last().text()).toEqual("BIND"); + expect(buttons.last().props().title).toEqual("BIND"); wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 }); buttons.last().simulate("click"); expect(mockDevice.registerGpio).not.toHaveBeenCalled(); @@ -94,7 +94,7 @@ describe("", () => { p.dispatch = jest.fn(); const wrapper = mount(); const buttons = wrapper.find("button"); - expect(buttons.last().text()).toEqual("BIND"); + expect(buttons.last().props().title).toEqual("BIND"); wrapper.setState({ pinNumberInput: 0, bindingType: PinBindingType.special, diff --git a/frontend/devices/pin_bindings/__tests__/pin_bindings_list_test.tsx b/frontend/devices/pin_bindings/__tests__/pin_bindings_list_test.tsx index cf8946546..485b84af6 100644 --- a/frontend/devices/pin_bindings/__tests__/pin_bindings_list_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/pin_bindings_list_test.tsx @@ -7,7 +7,7 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); jest.mock("../../../api/crud", () => ({ destroy: jest.fn() })); import { - PinBindingType, PinBindingSpecialAction + PinBindingType, PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; const mockData = [{ pin_number: 1, sequence_id: undefined, @@ -23,11 +23,11 @@ jest.mock("../tagged_pin_binding_init", () => ({ import * as React from "react"; import { mount } from "enzyme"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { TaggedSequence } from "farmbot"; import { - fakeSequence, fakePinBinding + fakeSequence, fakePinBinding, } from "../../../__test_support__/fake_state/resources"; import { destroy } from "../../../api/crud"; import { PinBindingsList } from "../pin_bindings_list"; diff --git a/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx b/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx index 4d0ac230c..46a720b82 100644 --- a/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx @@ -1,22 +1,22 @@ import * as React from "react"; -import { PinBindings } from "../pin_bindings"; +import { PinBindingsContent } from "../pin_bindings"; import { mount } from "enzyme"; import { bot } from "../../../__test_support__/fake_state/bot"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { - fakeSequence, fakePinBinding + fakeSequence, fakePinBinding, } from "../../../__test_support__/fake_state/resources"; -import { PinBindingsProps } from "../interfaces"; +import { PinBindingsContentProps } from "../interfaces"; import { SpecialPinBinding, PinBindingType, - PinBindingSpecialAction + PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; -describe("", () => { - function fakeProps(): PinBindingsProps { +describe("", () => { + function fakeProps(): PinBindingsContentProps { const fakeSequence1 = fakeSequence(); fakeSequence1.body.id = 1; fakeSequence1.body.name = "Sequence 1"; @@ -36,7 +36,7 @@ describe("", () => { (fakePinBinding2.body as SpecialPinBinding).special_action = PinBindingSpecialAction.emergency_lock; const resources = buildResourceIndex([ - fakeSequence1, fakeSequence2, fakePinBinding1, fakePinBinding2 + fakeSequence1, fakeSequence2, fakePinBinding1, fakePinBinding2, ]).index; bot.hardware.gpio_registry = { @@ -46,13 +46,14 @@ describe("", () => { return { dispatch: jest.fn(), resources: resources, + firmwareHardware: undefined, }; } it("renders", () => { const p = fakeProps(); - const wrapper = mount(); - ["pin bindings", "pin number", "none", "bind", "stock bindings"] + const wrapper = mount(); + ["pin number", "none", "bind", "stock bindings"] .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); ["26", "action"].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); diff --git a/frontend/devices/pin_bindings/__tests__/tagged_pin_binding_init_test.tsx b/frontend/devices/pin_bindings/__tests__/tagged_pin_binding_init_test.tsx index 55d4e9aa2..77ec9322a 100644 --- a/frontend/devices/pin_bindings/__tests__/tagged_pin_binding_init_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/tagged_pin_binding_init_test.tsx @@ -1,25 +1,37 @@ -jest.mock("../../../api/crud", () => ({ - initSave: jest.fn(), -})); +jest.mock("../../../api/crud", () => ({ initSave: jest.fn() })); import * as React from "react"; import { mount } from "enzyme"; -import { StockPinBindingsButton } from "../tagged_pin_binding_init"; +import { + StockPinBindingsButton, StockPinBindingsButtonProps, +} from "../tagged_pin_binding_init"; import { initSave } from "../../../api/crud"; import { stockPinBindings } from "../list_and_label_support"; describe("", () => { - const fakeProps = () => ({ - shouldDisplay: () => false, + const fakeProps = (): StockPinBindingsButtonProps => ({ dispatch: jest.fn(), + firmwareHardware: undefined, }); it("adds bindings", () => { - const p = fakeProps(); - p.shouldDisplay = () => true; - const wrapper = mount(); + const wrapper = mount(); wrapper.find("button").simulate("click"); stockPinBindings.map(body => expect(initSave).toHaveBeenCalledWith("PinBinding", body)); }); + + it("is hidden", () => { + const p = fakeProps(); + p.firmwareHardware = "arduino"; + const wrapper = mount(); + expect(wrapper.find("button").props().hidden).toBeTruthy(); + }); + + it("is not hidden", () => { + const p = fakeProps(); + p.firmwareHardware = "farmduino_k14"; + const wrapper = mount(); + expect(wrapper.find("button").props().hidden).toBeFalsy(); + }); }); diff --git a/frontend/devices/pin_bindings/interfaces.ts b/frontend/devices/pin_bindings/interfaces.ts index 1370327f2..072e30e1f 100644 --- a/frontend/devices/pin_bindings/interfaces.ts +++ b/frontend/devices/pin_bindings/interfaces.ts @@ -1,12 +1,14 @@ import { ResourceIndex } from "../../resources/interfaces"; import { PinBindingType, - PinBindingSpecialAction + PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; +import { FirmwareHardware } from "farmbot"; -export interface PinBindingsProps { +export interface PinBindingsContentProps { dispatch: Function; resources: ResourceIndex; + firmwareHardware: FirmwareHardware | undefined; } export interface PinBindingListItems { diff --git a/frontend/devices/pin_bindings/list_and_label_support.tsx b/frontend/devices/pin_bindings/list_and_label_support.tsx index 3e6e503e3..dfe80982a 100644 --- a/frontend/devices/pin_bindings/list_and_label_support.tsx +++ b/frontend/devices/pin_bindings/list_and_label_support.tsx @@ -1,6 +1,6 @@ import { PinBindingType, - PinBindingSpecialAction + PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; import { DropDownItem } from "../../ui"; import { gpio } from "./rpi_gpio_diagram"; @@ -32,9 +32,14 @@ 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 })); +export const getSpecialActionLabel = + (action: PinBindingSpecialAction | undefined) => + specialActionLabelLookup[action || ""] || ""; + /** Pin numbers for standard buttons. */ export enum ButtonPin { estop = 16, @@ -84,17 +89,17 @@ export const piSpi1Pins = [16, 17, 18, 19, 20, 21]; /** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */ export const reservedPiGPIO = piI2c0Pins; -const LabeledGpioPins: { [x: number]: string } = { - [ButtonPin.estop]: "Button 1: E-STOP", - [ButtonPin.unlock]: "Button 2: UNLOCK", - [ButtonPin.btn3]: "Button 3", - [ButtonPin.btn4]: "Button 4", - [ButtonPin.btn5]: "Button 5", -}; +const GPIO_PIN_LABELS = (): { [x: number]: string } => ({ + [ButtonPin.estop]: t("Button {{ num }}: E-STOP", { num: 1 }), + [ButtonPin.unlock]: t("Button {{ num }}: UNLOCK", { num: 2 }), + [ButtonPin.btn3]: t("Button {{ num }}", { num: 3 }), + [ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }), + [ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }), +}); export const generatePinLabel = (pin: number) => - LabeledGpioPins[pin] - ? `${LabeledGpioPins[pin]} (Pi ${pin})` + GPIO_PIN_LABELS()[pin] + ? `${t(GPIO_PIN_LABELS()[pin])} (Pi ${pin})` : `Pi GPIO ${pin}`; /** Raspberry Pi GPIO pin numbers. */ diff --git a/frontend/devices/pin_bindings/pin_binding_input_group.tsx b/frontend/devices/pin_bindings/pin_binding_input_group.tsx index 7eb87c550..32d2e4c80 100644 --- a/frontend/devices/pin_bindings/pin_binding_input_group.tsx +++ b/frontend/devices/pin_bindings/pin_binding_input_group.tsx @@ -1,11 +1,11 @@ import * as React from "react"; -import { Row, Col, FBSelect, NULL_CHOICE, DropDownItem } from "../../ui"; +import { Row, Col, FBSelect, DropDownItem } from "../../ui"; import { PinBindingColWidth } from "./pin_bindings"; import { Popover, Position } from "@blueprintjs/core"; import { RpiGpioDiagram } from "./rpi_gpio_diagram"; import { PinBindingInputGroupProps, - PinBindingInputGroupState + PinBindingInputGroupState, } from "./interfaces"; import { isNumber, includes } from "lodash"; import { initSave } from "../../api/crud"; @@ -13,14 +13,15 @@ import { pinBindingBody } from "./tagged_pin_binding_init"; import { error, warning } from "../../toast/toast"; import { validGpioPins, sysBindings, generatePinLabel, RpiPinList, - bindingTypeLabelLookup, specialActionLabelLookup, specialActionList, + bindingTypeLabelLookup, specialActionList, reservedPiGPIO, - bindingTypeList + bindingTypeList, + getSpecialActionLabel, } from "./list_and_label_support"; import { SequenceSelectBox } from "../../sequences/sequence_select_box"; import { ResourceIndex } from "../../resources/interfaces"; import { - PinBindingType, PinBindingSpecialAction + PinBindingType, PinBindingSpecialAction, } from "farmbot/dist/resources/api_resources"; import { t } from "../../i18next_wrapper"; @@ -119,8 +120,6 @@ export class PinBindingInputGroup - - {bindingType == PinBindingType.special ? - {t("BIND")} + ; @@ -152,10 +152,10 @@ export const PinNumberInputGroup = (props: { const selectedPinNumber = isNumber(pinNumberInput) ? { label: generatePinLabel(pinNumberInput), value: "" + pinNumberInput - } : NULL_CHOICE; + } : undefined; return - + void, }) => { const { bindingType, setBindingType } = props; - return ; diff --git a/frontend/devices/pin_bindings/pin_bindings.tsx b/frontend/devices/pin_bindings/pin_bindings.tsx index 76a085c77..d009211b2 100644 --- a/frontend/devices/pin_bindings/pin_bindings.tsx +++ b/frontend/devices/pin_bindings/pin_bindings.tsx @@ -1,28 +1,27 @@ import * as React from "react"; -import { Widget, WidgetBody, WidgetHeader, Row, Col } from "../../ui"; +import { Row, Col, Help } from "../../ui"; import { ToolTips } from "../../constants"; import { selectAllPinBindings } from "../../resources/selectors"; -import { PinBindingsProps, PinBindingListItems } from "./interfaces"; +import { PinBindingsContentProps, PinBindingListItems } from "./interfaces"; import { PinBindingsList } from "./pin_bindings_list"; import { PinBindingInputGroup } from "./pin_binding_input_group"; import { - StockPinBindingsButton, sysBtnBindingData + StockPinBindingsButton, sysBtnBindingData, } from "./tagged_pin_binding_init"; import { ResourceIndex } from "../../resources/interfaces"; import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core"; import { PinBindingSpecialAction, PinBindingType, - PinBinding + PinBinding, } from "farmbot/dist/resources/api_resources"; import { t } from "../../i18next_wrapper"; /** Width of UI columns in Pin Bindings widget. */ export enum PinBindingColWidth { pin = 4, - type = 3, - target = 4, - button = 1 + type = 6, + button = 2 } /** Use binding type to return a sequence ID or a special action. */ @@ -64,34 +63,30 @@ const PinBindingsListHeader = () => - - - + ; -export const PinBindings = (props: PinBindingsProps) => { - const { dispatch, resources } = props; +export const PinBindingsContent = (props: PinBindingsContentProps) => { + const { dispatch, resources, firmwareHardware } = props; const pinBindings = apiPinBindings(resources); - return - + return
    + + -
    +
    {t(ToolTips.PIN_BINDING_WARNING)}
    - - - + +
    { pinBindings={pinBindings} dispatch={dispatch} resources={resources} /> - - ; +
    +
    ; }; diff --git a/frontend/devices/pin_bindings/pin_bindings_list.tsx b/frontend/devices/pin_bindings/pin_bindings_list.tsx index bee8c844c..13f5912cc 100644 --- a/frontend/devices/pin_bindings/pin_bindings_list.tsx +++ b/frontend/devices/pin_bindings/pin_bindings_list.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { - bindingTypeLabelLookup, specialActionLabelLookup, - generatePinLabel, sortByNameAndPin + bindingTypeLabelLookup, + generatePinLabel, sortByNameAndPin, getSpecialActionLabel, } from "./list_and_label_support"; import { destroy } from "../../api/crud"; import { error } from "../../toast/toast"; @@ -36,12 +36,10 @@ export const PinBindingsList = (props: PinBindingsListProps) => { {generatePinLabel(pin_number)} - {t(bindingTypeLabelLookup[binding_type || ""])} - - + {t(bindingTypeLabelLookup[binding_type || ""])}:  {sequence_id ? findSequenceById(resources, sequence_id).body.name - : t(specialActionLabelLookup[special_action || ""])} + : t(getSpecialActionLabel(special_action))}
    ; diff --git a/frontend/devices/reducer.ts b/frontend/devices/reducer.ts index 67afb77a4..daffac9c7 100644 --- a/frontend/devices/reducer.ts +++ b/frontend/devices/reducer.ts @@ -3,7 +3,7 @@ import { ControlPanelState, HardwareState, MinOsFeatureLookup, - OsUpdateInfo + OsUpdateInfo, } from "./interfaces"; import { generateReducer } from "../redux/generate_reducer"; import { Actions } from "../constants"; @@ -11,7 +11,6 @@ import { maybeNegateStatus } from "../connectivity/maybe_negate_status"; import { ReduxAction } from "../redux/interfaces"; import { connectivityReducer, PingResultPayload } from "../connectivity/reducer"; import { versionOK } from "../util"; -import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions"; import { DeepPartial } from "redux"; import { incomingLegacyStatus } from "../connectivity/connect_device"; import { merge } from "lodash"; @@ -27,10 +26,16 @@ export const initialState = (): BotState => ({ controlPanelState: { homing_and_calibration: false, motors: false, - encoders_and_endstops: false, + encoders: false, + endstops: false, + error_handling: false, + pin_bindings: false, danger_zone: false, power_and_reset: false, - pin_guard: false + pin_guard: false, + farm_designer: false, + firmware: false, + farmbot_os: false, }, hardware: { gpio_registry: {}, @@ -116,9 +121,16 @@ export const botReducer = generateReducer(initialState()) .add(Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => { s.controlPanelState.homing_and_calibration = a.payload; s.controlPanelState.motors = a.payload; - s.controlPanelState.encoders_and_endstops = a.payload; + s.controlPanelState.encoders = a.payload; + s.controlPanelState.endstops = a.payload; + s.controlPanelState.error_handling = a.payload; + s.controlPanelState.pin_bindings = a.payload; s.controlPanelState.pin_guard = a.payload; s.controlPanelState.danger_zone = a.payload; + s.controlPanelState.power_and_reset = a.payload; + s.controlPanelState.farm_designer = a.payload; + s.controlPanelState.firmware = a.payload; + s.controlPanelState.farmbot_os = a.payload; return s; }) .add(Actions.FETCH_OS_UPDATE_INFO_OK, (s, { payload }) => { @@ -199,8 +211,7 @@ function legacyStatusHandler(state: BotState, const nextSyncStatus = maybeNegateStatus(info); - versionOK(informational_settings.controller_version, - EXPECTED_MAJOR, EXPECTED_MINOR); + versionOK(informational_settings.controller_version); state.hardware.informational_settings.sync_status = nextSyncStatus; return state; } diff --git a/frontend/devices/state_to_props.ts b/frontend/devices/state_to_props.ts index 20f224dfc..76647c602 100644 --- a/frontend/devices/state_to_props.ts +++ b/frontend/devices/state_to_props.ts @@ -6,14 +6,14 @@ import { maybeGetTimeSettings, } from "../resources/selectors"; import { - sourceFbosConfigValue, sourceFwConfigValue + sourceFbosConfigValue, sourceFwConfigValue, } from "./components/source_config_value"; import { validFwConfig, validFbosConfig } from "../util"; import { - saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn + saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn, } from "../farmware/state_to_props"; import { - getFbosConfig, getFirmwareConfig, getWebAppConfig + getFbosConfig, getFirmwareConfig, getWebAppConfig, } from "../resources/getters"; import { getAllAlerts } from "../messages/state_to_props"; diff --git a/frontend/devices/transfer_ownership/__tests__/create_transfer_cert_failure_test.ts b/frontend/devices/transfer_ownership/__tests__/create_transfer_cert_failure_test.ts index 4bffa4be1..688b3204f 100644 --- a/frontend/devices/transfer_ownership/__tests__/create_transfer_cert_failure_test.ts +++ b/frontend/devices/transfer_ownership/__tests__/create_transfer_cert_failure_test.ts @@ -13,7 +13,7 @@ jest.mock("axios", () => { import { transferOwnership } from "../transfer_ownership"; import { getDevice } from "../../../device"; import { - submitOwnershipChange + submitOwnershipChange, } from "../../components/fbos_settings/change_ownership_form"; import { API } from "../../../api"; import { error } from "../../../toast/toast"; diff --git a/frontend/error_boundary.tsx b/frontend/error_boundary.tsx index cd5096f82..342b8ab75 100644 --- a/frontend/error_boundary.tsx +++ b/frontend/error_boundary.tsx @@ -18,7 +18,7 @@ export class ErrorBoundary extends React.Component { no = () => this.props.fallback || ; - ok = () => this.props.children ||
    ; + ok = () => this.props.children ||
    ; render() { return (this.state.hasError ? this.no : this.ok)(); } } diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts new file mode 100644 index 000000000..6caa90822 --- /dev/null +++ b/frontend/external_urls.ts @@ -0,0 +1,51 @@ +enum Org { + FarmBot = "FarmBot", + FarmBotLabs = "FarmBot-Labs", +} + +export enum FarmBotRepo { + FarmBotWebApp = "Farmbot-Web-App", + FarmBotOS = "farmbot_os", + FarmBotArduinoFirmware = "farmbot-arduino-firmware", +} + +enum FbosFile { + featureMinVersions = "FEATURE_MIN_VERSIONS.json", + osReleaseNotes = "RELEASE_NOTES.md", +} + +export namespace ExternalUrl { + const GITHUB = "https://github.com"; + const GITHUB_RAW = "https://raw.githubusercontent.com"; + const GITHUB_API = "https://api.github.com"; + const OPENFARM = "https://openfarm.cc"; + const SOFTWARE_DOCS = "https://software.farm.bot"; + const FORUM = "https://forum.farmbot.org"; + const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files"; + + const FBOS_RAW = + `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/staging`; + export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`; + export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`; + + export const latestRelease = + `${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`; + + export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`; + export const webAppRepo = `${gitHubFarmBot}/${FarmBotRepo.FarmBotWebApp}`; + + export const softwareDocs = `${SOFTWARE_DOCS}/docs`; + export const softwareForum = `${FORUM}/c/software`; + + export namespace OpenFarm { + export const cropApi = `${OPENFARM}/api/v1/crops/`; + export const cropBrowse = `${OPENFARM}/crops/`; + export const newCrop = `${OPENFARM}/en/crops/new`; + } + + export namespace Video { + export const desktop = + `${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`; + export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`; + } +} diff --git a/frontend/farm_designer/__tests__/farm_designer_test.tsx b/frontend/farm_designer/__tests__/farm_designer_test.tsx index 82249de16..2190c3175 100644 --- a/frontend/farm_designer/__tests__/farm_designer_test.tsx +++ b/frontend/farm_designer/__tests__/farm_designer_test.tsx @@ -18,12 +18,12 @@ import { Props } from "../interfaces"; import { GardenMapLegendProps } from "../map/interfaces"; import { bot } from "../../__test_support__/fake_state/bot"; import { - fakeImage, fakeWebAppConfig + fakeImage, fakeWebAppConfig, } from "../../__test_support__/fake_state/resources"; import { fakeDesignerState } from "../../__test_support__/fake_designer_state"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; import { - buildResourceIndex + buildResourceIndex, } from "../../__test_support__/resource_index_builder"; import { fakeState } from "../../__test_support__/fake_state"; import { edit } from "../../api/crud"; @@ -62,6 +62,7 @@ describe("", () => { sensors: [], groups: [], shouldDisplay: () => false, + mountedToolName: undefined, }); it("loads default map settings", () => { diff --git a/frontend/farm_designer/__tests__/panel_header_test.tsx b/frontend/farm_designer/__tests__/panel_header_test.tsx index 546976771..5df39adab 100644 --- a/frontend/farm_designer/__tests__/panel_header_test.tsx +++ b/frontend/farm_designer/__tests__/panel_header_test.tsx @@ -71,6 +71,14 @@ describe("", () => { expect(wrapper.html()).toContain("active"); }); + it("renders for tools", () => { + mockPath = "/app/designer/tools"; + mockDev = false; + const wrapper = shallow(); + expect(wrapper.hasClass("gray-panel")).toBeTruthy(); + expect(wrapper.html()).toContain("active"); + }); + it("renders for zones", () => { mockPath = "/app/designer/zones"; mockDev = true; diff --git a/frontend/farm_designer/__tests__/plant_test.ts b/frontend/farm_designer/__tests__/plant_test.ts new file mode 100644 index 000000000..12bb2eac5 --- /dev/null +++ b/frontend/farm_designer/__tests__/plant_test.ts @@ -0,0 +1,19 @@ +import { Plant } from "../plant"; + +describe("Plant()", () => { + it("returns defaults", () => { + expect(Plant({})).toEqual({ + created_at: "", + id: undefined, + meta: {}, + name: "Untitled Plant", + openfarm_slug: "not-set", + plant_stage: "planned", + pointer_type: "Plant", + radius: 25, + x: 0, + y: 0, + z: 0, + }); + }); +}); diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index 375b6106c..938f4fd9d 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -2,11 +2,11 @@ import { designer } from "../reducer"; import { Actions } from "../../constants"; import { ReduxAction } from "../../redux/interfaces"; import { - HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult + HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult, } from "../interfaces"; import { BotPosition } from "../../devices/interfaces"; import { - fakeCropLiveSearchResult + fakeCropLiveSearchResult, } from "../../__test_support__/fake_crop_search_result"; import { fakeDesignerState } from "../../__test_support__/fake_designer_state"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; @@ -94,6 +94,19 @@ describe("designer reducer", () => { }); }); + it("uses current point color", () => { + const action: ReduxAction = { + type: Actions.SET_CURRENT_POINT_DATA, + payload: { cx: 10, cy: 20, r: 30 } + }; + const state = oldState(); + state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" }; + const newState = designer(state, action); + expect(newState.currentPoint).toEqual({ + cx: 10, cy: 20, r: 30, color: "red" + }); + }); + it("sets opened saved garden", () => { const payload = "savedGardenUuid"; const action: ReduxAction = { diff --git a/frontend/farm_designer/__tests__/search_selectors_test.ts b/frontend/farm_designer/__tests__/search_selectors_test.ts index 4014c4985..f60c23734 100644 --- a/frontend/farm_designer/__tests__/search_selectors_test.ts +++ b/frontend/farm_designer/__tests__/search_selectors_test.ts @@ -1,7 +1,7 @@ import { findBySlug } from "../search_selectors"; import { DEFAULT_ICON } from "../../open_farm/icons"; import { - fakeCropLiveSearchResult + fakeCropLiveSearchResult, } from "../../__test_support__/fake_crop_search_result"; describe("findBySlug()", () => { diff --git a/frontend/farm_designer/__tests__/state_to_props_test.tsx b/frontend/farm_designer/__tests__/state_to_props_test.tsx index 68dc494d8..3dfc9c517 100644 --- a/frontend/farm_designer/__tests__/state_to_props_test.tsx +++ b/frontend/farm_designer/__tests__/state_to_props_test.tsx @@ -1,7 +1,7 @@ import { mapStateToProps, getPlants } from "../state_to_props"; import { fakeState } from "../../__test_support__/fake_state"; import { - buildResourceIndex + buildResourceIndex, fakeDevice, } from "../../__test_support__/resource_index_builder"; import { fakePlant, @@ -10,7 +10,7 @@ import { fakePoint, fakeWebAppConfig, fakeFarmwareEnv, - fakeSensorReading + fakeSensorReading, } from "../../__test_support__/fake_state/resources"; import { WebAppConfig } from "farmbot/dist/resources/configs/web_app"; import { generateUuid } from "../../resources/util"; @@ -49,7 +49,7 @@ describe("mapStateToProps()", () => { it("returns selected plant", () => { const state = fakeState(); - state.resources = buildResourceIndex([fakePlant()]); + state.resources = buildResourceIndex([fakePlant(), fakeDevice()]); const plantUuid = Object.keys(state.resources.index.byKind["Point"])[0]; state.resources.consumers.farm_designer.selectedPlants = [plantUuid]; expect(mapStateToProps(state).selectedPlant).toEqual( @@ -66,7 +66,9 @@ describe("mapStateToProps()", () => { point2.body.discarded_at = DISCARDED_AT; const point3 = fakePoint(); point3.body.discarded_at = DISCARDED_AT; - state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]); + state.resources = buildResourceIndex([ + webAppConfig, point1, point2, point3, fakeDevice(), + ]); expect(mapStateToProps(state).genericPoints.length).toEqual(3); }); @@ -80,7 +82,9 @@ describe("mapStateToProps()", () => { point2.body.discarded_at = DISCARDED_AT; const point3 = fakePoint(); point3.body.discarded_at = DISCARDED_AT; - state.resources = buildResourceIndex([webAppConfig, point1, point2, point3]); + state.resources = buildResourceIndex([ + webAppConfig, point1, point2, point3, fakeDevice(), + ]); expect(mapStateToProps(state).genericPoints.length).toEqual(1); }); @@ -90,7 +94,7 @@ describe("mapStateToProps()", () => { sr1.body.created_at = "2018-01-14T20:20:38.362Z"; const sr2 = fakeSensorReading(); sr2.body.created_at = "2018-01-11T20:20:38.362Z"; - state.resources = buildResourceIndex([sr1, sr2]); + state.resources = buildResourceIndex([sr1, sr2, fakeDevice()]); const uuid1 = Object.keys(state.resources.index.byKind["SensorReading"])[0]; const uuid2 = Object.keys(state.resources.index.byKind["SensorReading"])[1]; expect(mapStateToProps(state).sensorReadings).toEqual([ @@ -112,7 +116,8 @@ describe("getPlants()", () => { const template2 = fakePlantTemplate(); template2.body.saved_garden_id = 2; return buildResourceIndex([ - savedGarden, plant1, plant2, template1, template2]); + savedGarden, plant1, plant2, template1, template2, fakeDevice(), + ]); }; it("returns plants", () => { expect(getPlants(fakeResources()).length).toEqual(2); @@ -133,7 +138,7 @@ describe("getPlants()", () => { const fwEnv = fakeFarmwareEnv(); fwEnv.body.key = "CAMERA_CALIBRATION_total_rotation_angle"; fwEnv.body.value = 15; - state.resources = buildResourceIndex([fwEnv]); + state.resources = buildResourceIndex([fwEnv, fakeDevice()]); const props = mapStateToProps(state); expect(props.cameraCalibrationData).toEqual( expect.objectContaining({ rotation: "15" })); diff --git a/frontend/farm_designer/designer_panel.tsx b/frontend/farm_designer/designer_panel.tsx index 3fc41850d..dd369abf5 100644 --- a/frontend/farm_designer/designer_panel.tsx +++ b/frontend/farm_designer/designer_panel.tsx @@ -90,7 +90,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
    {!props.noIcon && - } + } {props.children} diff --git a/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx b/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx index 04dfb8a2d..58629b307 100644 --- a/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx @@ -12,10 +12,10 @@ import { mount, shallow } from "enzyme"; import { RawAddFarmEvent as AddFarmEvent } from "../add_farm_event"; import { AddEditFarmEventProps } from "../../interfaces"; import { - fakeFarmEvent, fakeSequence, fakeRegimen + fakeFarmEvent, fakeSequence, fakeRegimen, } from "../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { destroyOK } from "../../../resources/actions"; @@ -55,7 +55,7 @@ describe("", () => { const wrapper = mount(); wrapper.setState({ uuid: "FarmEvent" }); ["Add Event", "Sequence or Regimen", "fake", "Save"].map(string => - expect(wrapper.text()).toContain(string)); + expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); const deleteBtn = wrapper.find("button").last(); expect(deleteBtn.text()).toEqual("Delete"); expect(deleteBtn.props().hidden).toBeTruthy(); diff --git a/frontend/farm_designer/farm_events/__tests__/edit_farm_event_test.tsx b/frontend/farm_designer/farm_events/__tests__/edit_farm_event_test.tsx index 757d92acd..d73b15e30 100644 --- a/frontend/farm_designer/farm_events/__tests__/edit_farm_event_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/edit_farm_event_test.tsx @@ -7,10 +7,10 @@ import { mount } from "enzyme"; import { RawEditFarmEvent as EditFarmEvent } from "../edit_farm_event"; import { AddEditFarmEventProps } from "../../interfaces"; import { - fakeFarmEvent, fakeSequence + fakeFarmEvent, fakeSequence, } from "../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; @@ -52,6 +52,6 @@ describe("", () => { const p = fakeProps(); p.getFarmEvent = jest.fn(); const wrapper = mount(); - expect(wrapper.text()).toContain("Loading"); + expect(wrapper.text()).toContain("Redirecting"); }); }); diff --git a/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx b/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx index e7baf2a78..b0adea962 100644 --- a/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/edit_fe_form_test.tsx @@ -8,7 +8,7 @@ jest.mock("../../../api/crud", () => ({ import * as React from "react"; import { - fakeFarmEvent, fakeSequence, fakeRegimen, fakePlant + fakeFarmEvent, fakeSequence, fakeRegimen, fakePlant, } from "../../../__test_support__/fake_state/resources"; import { mount, shallow } from "enzyme"; import { @@ -24,7 +24,7 @@ import { RepeatFormProps, StartTimeForm, StartTimeFormProps, - FarmEventForm + FarmEventForm, } from "../edit_fe_form"; import { isString, isFunction } from "lodash"; import { repeatOptions } from "../map_state_to_props_add_edit"; @@ -32,7 +32,7 @@ import { SpecialStatus, ParameterApplication } from "farmbot"; import moment from "moment"; import { history } from "../../../history"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { fakeVariableNameSet } from "../../../__test_support__/fake_variables"; import { save, destroy } from "../../../api/crud"; @@ -219,7 +219,7 @@ describe("", () => { label: "Sequence: Every Node", value: 11, headingId: "Sequence" - } + }, ]} findExecutable={jest.fn(() => seq)} dispatch={jest.fn()} diff --git a/frontend/farm_designer/farm_events/__tests__/farm_event_repeat_form_test.tsx b/frontend/farm_designer/farm_events/__tests__/farm_event_repeat_form_test.tsx index 240f64a57..4020e9ec6 100644 --- a/frontend/farm_designer/farm_events/__tests__/farm_event_repeat_form_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/farm_event_repeat_form_test.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { - FarmEventRepeatFormProps, FarmEventRepeatForm + FarmEventRepeatFormProps, FarmEventRepeatForm, } from "../farm_event_repeat_form"; import { shallow, ShallowWrapper, render } from "enzyme"; import { get } from "lodash"; diff --git a/frontend/farm_designer/farm_events/__tests__/farm_events_test.tsx b/frontend/farm_designer/farm_events/__tests__/farm_events_test.tsx index dfe90c87a..fb2cf70a8 100644 --- a/frontend/farm_designer/farm_events/__tests__/farm_events_test.tsx +++ b/frontend/farm_designer/farm_events/__tests__/farm_events_test.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { PureFarmEvents } from "../farm_events"; import { - calendarRows + calendarRows, } from "../../../__test_support__/farm_event_calendar_support"; import { render, shallow, mount } from "enzyme"; import { get } from "lodash"; diff --git a/frontend/farm_designer/farm_events/__tests__/map_state_to_props_add_edit_test.ts b/frontend/farm_designer/farm_events/__tests__/map_state_to_props_add_edit_test.ts index 7f1f7d2b3..930b1d51f 100644 --- a/frontend/farm_designer/farm_events/__tests__/map_state_to_props_add_edit_test.ts +++ b/frontend/farm_designer/farm_events/__tests__/map_state_to_props_add_edit_test.ts @@ -7,10 +7,10 @@ jest.mock("../../../history", () => ({ import { mapStateToPropsAddEdit } from "../map_state_to_props_add_edit"; import { fakeState } from "../../../__test_support__/fake_state"; import { - buildResourceIndex, fakeDevice + buildResourceIndex, fakeDevice, } from "../../../__test_support__/resource_index_builder"; import { - fakeSequence, fakeRegimen, fakeFarmEvent + fakeSequence, fakeRegimen, fakeFarmEvent, } from "../../../__test_support__/fake_state/resources"; import { history } from "../../../history"; import { inputEvent } from "../../../__test_support__/fake_html_events"; @@ -53,7 +53,7 @@ describe("mapStateToPropsAddEdit()", () => { it("returns executable list", () => { expect(executableOptions).toEqual(expect.arrayContaining([ { headingId: "Regimen", label: "Fake Regimen", value: 1 }, - { headingId: "Sequence", label: "Fake Sequence", value: 1 } + { headingId: "Sequence", label: "Fake Sequence", value: 1 }, ])); }); }); diff --git a/frontend/farm_designer/farm_events/__tests__/map_state_to_props_test.ts b/frontend/farm_designer/farm_events/__tests__/map_state_to_props_test.ts index 7d8af8659..b9a4ac3cd 100644 --- a/frontend/farm_designer/farm_events/__tests__/map_state_to_props_test.ts +++ b/frontend/farm_designer/farm_events/__tests__/map_state_to_props_test.ts @@ -3,10 +3,10 @@ import { fakeState } from "../../../__test_support__/fake_state"; import { fakeSequence, fakeRegimen, - fakeFarmEvent + fakeFarmEvent, } from "../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import moment from "moment"; import { countBy } from "lodash"; @@ -39,7 +39,7 @@ describe("mapStateToProps()", () => { sequence, regimen, sequenceFarmEvent, - regimenFarmEvent + regimenFarmEvent, ]; const state = fakeState(); @@ -207,7 +207,7 @@ describe("mapResourcesToCalendar(): regimen farm events", () => { mmddyy: expect.stringContaining("17"), sortKey: expect.any(Number), timeStr: expect.stringContaining("02") - } + }, ], month: "Dec", sortKey: expect.any(Number), @@ -224,12 +224,12 @@ describe("mapResourcesToCalendar(): regimen farm events", () => { mmddyy: expect.stringContaining("17"), sortKey: expect.any(Number), timeStr: expect.stringContaining("11") - } + }, ], month: "Dec", sortKey: expect.any(Number), year: 17 - } + }, ]; it("returns calendar rows", () => { diff --git a/frontend/farm_designer/farm_events/__tests__/util_test.ts b/frontend/farm_designer/farm_events/__tests__/util_test.ts index 46177d50e..18a05d3e8 100644 --- a/frontend/farm_designer/farm_events/__tests__/util_test.ts +++ b/frontend/farm_designer/farm_events/__tests__/util_test.ts @@ -5,7 +5,7 @@ import { ExecutableType } from "farmbot/dist/resources/api_resources"; describe("maybeWarnAboutMissedTasks()", () => { function testWarn( - time: string, executableType: ExecutableType = "Regimen" + time: string, executableType: ExecutableType = "Regimen", ): () => void { const callback = jest.fn(); const fe = fakeFarmEvent(executableType, 1); diff --git a/frontend/farm_designer/farm_events/add_farm_event.tsx b/frontend/farm_designer/farm_events/add_farm_event.tsx index 95e11f9af..4e1a82fc2 100644 --- a/frontend/farm_designer/farm_events/add_farm_event.tsx +++ b/frontend/farm_designer/farm_events/add_farm_event.tsx @@ -6,17 +6,17 @@ import { } from "./map_state_to_props_add_edit"; import { init, destroy } from "../../api/crud"; import { - EditFEForm, FarmEventForm, FarmEventViewModel, NEVER + EditFEForm, FarmEventForm, FarmEventViewModel, NEVER, } from "./edit_fe_form"; import { betterCompact, betterMerge } from "../../util"; import { entries } from "../../resources/util"; import { AddEditFarmEventProps, - TaggedExecutable + TaggedExecutable, } from "../interfaces"; import { ExecutableType } from "farmbot/dist/resources/api_resources"; import { - DesignerPanel, DesignerPanelHeader, DesignerPanelContent + DesignerPanel, DesignerPanelHeader, DesignerPanelContent, } from "../designer_panel"; import { variableList } from "../../sequences/locals_list/variable_support"; import { t } from "../../i18next_wrapper"; @@ -102,7 +102,7 @@ export class RawAddFarmEvent this.props.dispatch(destroyOK(farmEvent)) : undefined} /> @@ -115,7 +115,7 @@ export class RawAddFarmEvent executableOptions={this.props.executableOptions} dispatch={this.props.dispatch} findExecutable={this.props.findExecutable} - title={t("Add Event")} + title={t("Add event")} timeSettings={this.props.timeSettings} autoSyncEnabled={this.props.autoSyncEnabled} resources={this.props.resources} diff --git a/frontend/farm_designer/farm_events/calendar/__tests__/index_test.ts b/frontend/farm_designer/farm_events/calendar/__tests__/index_test.ts index d5a0e53b9..9ac430100 100644 --- a/frontend/farm_designer/farm_events/calendar/__tests__/index_test.ts +++ b/frontend/farm_designer/farm_events/calendar/__tests__/index_test.ts @@ -2,7 +2,7 @@ import { Calendar } from "../index"; import { occurrence } from "../occurrence"; import { TIME, - fakeFarmEventWithExecutable + fakeFarmEventWithExecutable, } from "../../../../__test_support__/farm_event_calendar_support"; import moment from "moment"; import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings"; diff --git a/frontend/farm_designer/farm_events/calendar/__tests__/occurrence_test.ts b/frontend/farm_designer/farm_events/calendar/__tests__/occurrence_test.ts index ab0d51b52..6c1045701 100644 --- a/frontend/farm_designer/farm_events/calendar/__tests__/occurrence_test.ts +++ b/frontend/farm_designer/farm_events/calendar/__tests__/occurrence_test.ts @@ -2,7 +2,7 @@ import { occurrence } from "../occurrence"; import moment from "moment"; import { TIME, - fakeFarmEventWithExecutable + fakeFarmEventWithExecutable, } from "../../../../__test_support__/farm_event_calendar_support"; import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings"; diff --git a/frontend/farm_designer/farm_events/calendar/__tests__/scheduler_test.ts b/frontend/farm_designer/farm_events/calendar/__tests__/scheduler_test.ts index 53782c2b0..4337df4c7 100644 --- a/frontend/farm_designer/farm_events/calendar/__tests__/scheduler_test.ts +++ b/frontend/farm_designer/farm_events/calendar/__tests__/scheduler_test.ts @@ -4,7 +4,7 @@ import { TimeLine, farmEventIntervalSeconds, maxDisplayItems, - gracePeriodSeconds + gracePeriodSeconds, } from "../scheduler"; import moment from "moment"; import { Moment } from "moment"; @@ -49,7 +49,7 @@ describe("scheduler", () => { "04:00am Th", "08:00am Th", "12:00pm Th", - "04:00pm Th" + "04:00pm Th", ]; const REALITY = result1.items.map(x => x.format("hh:mma dd")); EXPECTED.map(x => expect(REALITY).toContain(x)); @@ -97,7 +97,7 @@ describe("scheduleForFarmEvent", () => { expected: [ moment("2017-08-01T17:30:00.000Z"), moment("2017-08-03T17:30:00.000Z"), - moment("2017-08-05T17:30:00.000Z") + moment("2017-08-05T17:30:00.000Z"), ], shortenedBy: 0 }, @@ -124,7 +124,7 @@ describe("scheduleForFarmEvent", () => { timeNow: moment("2017-08-03T18:30:00.000Z"), expected: [ moment("2017-08-05T17:30:00.000Z"), - moment("2017-08-07T17:30:00.000Z") + moment("2017-08-07T17:30:00.000Z"), ], shortenedBy: 0 }, @@ -148,7 +148,7 @@ describe("scheduleForFarmEvent", () => { expected: [ moment("2017-08-01T17:30:00.000Z"), moment("2017-08-01T21:30:00.000Z"), - moment("2017-08-02T01:30:00.000Z") + moment("2017-08-02T01:30:00.000Z"), ], shortenedBy: 0 }, @@ -219,7 +219,7 @@ describe("scheduleForFarmEvent", () => { timeNow: moment("2017-08-01T16:30:00.000Z"), expected: [ moment("2017-08-01T17:30:00.000Z"), - moment("2017-08-01T21:30:00.000Z") + moment("2017-08-01T21:30:00.000Z"), ], shortenedBy: 0 }, @@ -235,7 +235,7 @@ describe("scheduleForFarmEvent", () => { .add(gracePeriodSeconds, "seconds"), expected: [ moment("2017-08-01T17:30:00.000Z"), - moment("2017-08-01T21:30:00.000Z") + moment("2017-08-01T21:30:00.000Z"), ], shortenedBy: 0 }, @@ -259,7 +259,7 @@ describe("farmEventIntervalSeconds", () => { { count: 0, unit: "yearly", result: 0 }, { count: 2, unit: "weekly", result: 1209600 }, { count: 4, unit: "minutely", result: 240 }, - { count: 3, unit: "never", result: 0 } + { count: 3, unit: "never", result: 0 }, ]; tests.forEach((T) => { diff --git a/frontend/farm_designer/farm_events/calendar/scheduler.ts b/frontend/farm_designer/farm_events/calendar/scheduler.ts index 42659dd81..471762932 100644 --- a/frontend/farm_designer/farm_events/calendar/scheduler.ts +++ b/frontend/farm_designer/farm_events/calendar/scheduler.ts @@ -93,7 +93,7 @@ export interface TimeLine { } /** Takes a subset of FarmEvent data and generates a list of dates. */ export function scheduleForFarmEvent( - { start_time, end_time, repeat, time_unit }: TimeLine, timeNow = moment() + { start_time, end_time, repeat, time_unit }: TimeLine, timeNow = moment(), ): { items: Moment[], shortenedBy: number } { const interval = repeat && farmEventIntervalSeconds(repeat, time_unit); const gracePeriod = timeNow.clone().subtract(gracePeriodSeconds, "seconds"); diff --git a/frontend/farm_designer/farm_events/calendar/selectors.ts b/frontend/farm_designer/farm_events/calendar/selectors.ts index bdb914554..eeeae8aa5 100644 --- a/frontend/farm_designer/farm_events/calendar/selectors.ts +++ b/frontend/farm_designer/farm_events/calendar/selectors.ts @@ -3,7 +3,7 @@ import { ResourceIndex } from "../../../resources/interfaces"; import { selectAllFarmEvents, indexSequenceById, - indexRegimenById + indexRegimenById, } from "../../../resources/selectors"; import { betterCompact } from "../../../util"; import { TaggedFarmEvent } from "farmbot"; diff --git a/frontend/farm_designer/farm_events/edit_farm_event.tsx b/frontend/farm_designer/farm_events/edit_farm_event.tsx index 5a86238d3..618be7c7a 100644 --- a/frontend/farm_designer/farm_events/edit_farm_event.tsx +++ b/frontend/farm_designer/farm_events/edit_farm_event.tsx @@ -3,48 +3,41 @@ import { AddEditFarmEventProps } from "../interfaces"; import { connect } from "react-redux"; import { mapStateToPropsAddEdit } from "./map_state_to_props_add_edit"; import { history } from "../../history"; -import { TaggedFarmEvent } from "farmbot"; import { EditFEForm } from "./edit_fe_form"; import { t } from "../../i18next_wrapper"; import { Panel } from "../panel_header"; import { - DesignerPanel, DesignerPanelHeader, DesignerPanelContent + DesignerPanel, DesignerPanelHeader, DesignerPanelContent, } from "../designer_panel"; export class RawEditFarmEvent extends React.Component { - redirect() { - history.push("/app/designer/events"); - return
    {t("Loading")}...
    ; - } - - renderForm(fe: TaggedFarmEvent) { + render() { + const fe = this.props.getFarmEvent(); + !fe && history.push("/app/designer/events"); const panelName = "edit-farm-event"; return + title={t("Edit event")} /> - + {fe + ? + :
    {t("Redirecting")}...
    }
    ; } - - render() { - const fe = this.props.getFarmEvent(); - return fe ? this.renderForm(fe) : this.redirect(); - } } export const EditFarmEvent = connect(mapStateToPropsAddEdit)(RawEditFarmEvent); diff --git a/frontend/farm_designer/farm_events/edit_fe_form.tsx b/frontend/farm_designer/farm_events/edit_fe_form.tsx index d2bb3d66b..85ef1dff0 100644 --- a/frontend/farm_designer/farm_events/edit_fe_form.tsx +++ b/frontend/farm_designer/farm_events/edit_fe_form.tsx @@ -28,19 +28,20 @@ import { TzWarning } from "./tz_warning"; import { nextRegItemTimes } from "./map_state_to_props"; import { first } from "lodash"; import { - TimeUnit, ExecutableType, FarmEvent + TimeUnit, ExecutableType, FarmEvent, } from "farmbot/dist/resources/api_resources"; import { LocalsList } from "../../sequences/locals_list/locals_list"; import { ResourceIndex } from "../../resources/interfaces"; import { ShouldDisplay } from "../../devices/interfaces"; import { - addOrEditParamApps, variableList, getRegimenVariableData + addOrEditParamApps, variableList, getRegimenVariableData, } from "../../sequences/locals_list/variable_support"; import { AllowedVariableNodes, } from "../../sequences/locals_list/locals_list_support"; import { t } from "../../i18next_wrapper"; import { TimeSettings } from "../../interfaces"; +import { ErrorBoundary } from "../../error_boundary"; export const NEVER: TimeUnit = "never"; /** Separate each of the form fields into their own interface. Recombined later @@ -255,17 +256,17 @@ export class EditFEForm extends React.Component { }; } - fieldSet = (name: FarmEventViewModelKey, value: string) => + fieldSet = (key: FarmEventViewModelKey, value: string) => // A merge is required to not overwrite `fe`. this.setState(betterMerge(this.state, { - fe: { [name]: value }, + fe: { [key]: value }, specialStatusLocal: SpecialStatus.DIRTY })) - fieldGet = (name: FarmEventViewModelKey): string => - (this.state.fe[name] || this.viewModel[name] || "").toString() + fieldGet = (key: FarmEventViewModelKey): string => + (this.state.fe[key] || this.viewModel[key] || "").toString() - nextItemTime = (fe: FarmEvent, now: moment.Moment + nextItemTime = (fe: FarmEvent, now: moment.Moment, ): moment.Moment | undefined => { const { timeSettings } = this.props; const kind = fe.executable_type; @@ -360,19 +361,24 @@ export class EditFEForm extends React.Component { render() { const { farmEvent } = this.props; return
    - this.commitViewModel()}> - - + + this.commitViewModel()}> + + + + +
    ; - export interface SlotDirectionInputRowProps { toolPulloutDirection: ToolPulloutDirection; onChange(update: { pullout_direction: ToolPulloutDirection }): void; @@ -51,7 +32,7 @@ export interface SlotDirectionInputRowProps { export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
    (!props.filterSelectedTool || !props.selectedTool) - || tool.body.id != props.selectedTool.body.id) + list={([NULL_CHOICE] as DropDownItem[]).concat(props.tools + .filter(tool => !props.filterSelectedTool + || tool.body.id != props.selectedTool?.body.id) + .filter(tool => !props.filterActiveTools + || !props.isActive(tool.body.id)) .map(tool => ({ label: tool.body.name || "untitled", value: tool.body.id || 0, })) - .filter(ddi => ddi.value > 0)} + .filter(ddi => ddi.value > 0))} selectedItem={props.selectedTool ? { label: props.selectedTool.body.name || "untitled", value: "" + props.selectedTool.body.id } : NULL_CHOICE} - allowEmpty={true} onChange={ddi => props.onChange({ tool_id: parseInt("" + ddi.value) })} />; @@ -97,18 +81,26 @@ export interface ToolInputRowProps { tools: TaggedTool[]; selectedTool: TaggedTool | undefined; onChange(update: { tool_id: number }): void; + isExpress: boolean; + isActive(id: number | undefined): boolean; } export const ToolInputRow = (props: ToolInputRowProps) =>
    - + + isActive={props.isActive} + filterSelectedTool={false} + filterActiveTools={true} />
    ; @@ -117,24 +109,43 @@ export interface SlotLocationInputRowProps { slotLocation: Record; gantryMounted: boolean; onChange(update: Partial>): void; + botPosition: BotPosition; } export const SlotLocationInputRow = (props: SlotLocationInputRowProps) =>
    - {["x", "y", "z"].map((axis: Xyz) => - - - {axis == "x" && props.gantryMounted - ? - : props.onChange({ - [axis]: parseFloat(e.currentTarget.value) - })} />} - )} + + {["x", "y", "z"].map((axis: Xyz) => + + + {axis == "x" && props.gantryMounted + ? + : props.onChange({ + [axis]: parseFloat(e.currentTarget.value) + })} />} + )} + + + + +
    + +

    {positionButtonTitle(props.botPosition)}

    +
    +
    + +
    ; @@ -144,25 +155,77 @@ export interface SlotEditRowsProps { tool: TaggedTool | undefined; botPosition: BotPosition; updateToolSlot(update: Partial): void; + isExpress: boolean; + xySwap: boolean; + quadrant: BotOriginQuadrant; + isActive(id: number | undefined): boolean; } export const SlotEditRows = (props: SlotEditRowsProps) =>
    + - - - - + {!props.toolSlot.body.gantry_mounted && + } + {!props.isExpress && + }
    ; + +const directionIconClass = (slotDirection: ToolPulloutDirection) => { + switch (slotDirection) { + case ToolPulloutDirection.POSITIVE_X: return "fa fa-arrow-circle-right"; + case ToolPulloutDirection.NEGATIVE_X: return "fa fa-arrow-circle-left"; + case ToolPulloutDirection.POSITIVE_Y: return "fa fa-arrow-circle-up"; + case ToolPulloutDirection.NEGATIVE_Y: return "fa fa-arrow-circle-down"; + case ToolPulloutDirection.NONE: return "fa fa-dot-circle-o"; + } +}; + +export const positionButtonTitle = (position: BotPosition): string => + positionIsDefined(position) + ? `(${position.x}, ${position.y}, ${position.z})` + : t("(unknown)"); + +export const newSlotDirection = + (old: ToolPulloutDirection | undefined): ToolPulloutDirection => + isNumber(old) && old < 4 ? old + 1 : ToolPulloutDirection.NONE; + +export const positionIsDefined = (position: BotPosition): boolean => + isNumber(position.x) && isNumber(position.y) && isNumber(position.z); + +export const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = { + [ToolPulloutDirection.NONE]: + { label: t("None"), value: ToolPulloutDirection.NONE }, + [ToolPulloutDirection.POSITIVE_X]: + { label: t("Positive X"), value: ToolPulloutDirection.POSITIVE_X }, + [ToolPulloutDirection.NEGATIVE_X]: + { label: t("Negative X"), value: ToolPulloutDirection.NEGATIVE_X }, + [ToolPulloutDirection.POSITIVE_Y]: + { label: t("Positive Y"), value: ToolPulloutDirection.POSITIVE_Y }, + [ToolPulloutDirection.NEGATIVE_Y]: + { label: t("Negative Y"), value: ToolPulloutDirection.NEGATIVE_Y }, +}; + +export const DIRECTION_CHOICES: DropDownItem[] = [ + DIRECTION_CHOICES_DDI[ToolPulloutDirection.NONE], + DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_X], + DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_X], + DIRECTION_CHOICES_DDI[ToolPulloutDirection.POSITIVE_Y], + DIRECTION_CHOICES_DDI[ToolPulloutDirection.NEGATIVE_Y], +]; diff --git a/frontend/farm_designer/util.ts b/frontend/farm_designer/util.ts index 40500c821..771f7d6bd 100644 --- a/frontend/farm_designer/util.ts +++ b/frontend/farm_designer/util.ts @@ -4,8 +4,10 @@ import { DEFAULT_ICON } from "../open_farm/icons"; import { Actions } from "../constants"; import { ExecutableType } from "farmbot/dist/resources/api_resources"; import { get } from "lodash"; +import { ExternalUrl } from "../external_urls"; -const url = (q: string) => `${OpenFarm.cropUrl}?include=pictures&filter=${q}`; +const url = (q: string) => + `${ExternalUrl.OpenFarm.cropApi}?include=pictures&filter=${q}`; const openFarmSearchQuery = (q: string): AxiosPromise => axios.get(url(q)); @@ -34,8 +36,7 @@ export const OFSearch = (searchTerm: string) => dispatch({ type: Actions.OF_SEARCH_RESULTS_OK, payload }); }) .catch(() => - dispatch({ type: Actions.OF_SEARCH_RESULTS_NO, payload: undefined }) - ); + dispatch({ type: Actions.OF_SEARCH_RESULTS_NO, payload: undefined })); }; function isExecutableType(x?: string): x is ExecutableType { diff --git a/frontend/farm_designer/zones/__tests__/add_zone_test.tsx b/frontend/farm_designer/zones/__tests__/add_zone_test.tsx index 6e4d540a8..19c45ef7a 100644 --- a/frontend/farm_designer/zones/__tests__/add_zone_test.tsx +++ b/frontend/farm_designer/zones/__tests__/add_zone_test.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { mount } from "enzyme"; import { - RawAddZone as AddZone, AddZoneProps, mapStateToProps + RawAddZone as AddZone, AddZoneProps, mapStateToProps, } from "../add_zone"; import { fakeState } from "../../../__test_support__/fake_state"; diff --git a/frontend/farm_designer/zones/__tests__/edit_zone_test.tsx b/frontend/farm_designer/zones/__tests__/edit_zone_test.tsx index 6579acb6e..0552a4bb8 100644 --- a/frontend/farm_designer/zones/__tests__/edit_zone_test.tsx +++ b/frontend/farm_designer/zones/__tests__/edit_zone_test.tsx @@ -12,12 +12,12 @@ jest.mock("../../../api/crud", () => ({ import * as React from "react"; import { mount, shallow } from "enzyme"; import { - RawEditZone as EditZone, EditZoneProps, mapStateToProps + RawEditZone as EditZone, EditZoneProps, mapStateToProps, } from "../edit_zone"; import { fakeState } from "../../../__test_support__/fake_state"; import { fakePointGroup } from "../../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../../__test_support__/resource_index_builder"; import { save, edit } from "../../../api/crud"; diff --git a/frontend/farm_designer/zones/__tests__/zones_inventory_test.tsx b/frontend/farm_designer/zones/__tests__/zones_inventory_test.tsx index 6c7f23d16..ac385be6a 100644 --- a/frontend/farm_designer/zones/__tests__/zones_inventory_test.tsx +++ b/frontend/farm_designer/zones/__tests__/zones_inventory_test.tsx @@ -8,7 +8,7 @@ jest.mock("../../../api/crud", () => ({ initSaveGetId: jest.fn() })); import * as React from "react"; import { mount, shallow } from "enzyme"; import { - RawZones as Zones, ZonesProps, mapStateToProps + RawZones as Zones, ZonesProps, mapStateToProps, } from "../zones_inventory"; import { fakeState } from "../../../__test_support__/fake_state"; import { fakePointGroup } from "../../../__test_support__/fake_state/resources"; diff --git a/frontend/farm_designer/zones/add_zone.tsx b/frontend/farm_designer/zones/add_zone.tsx index 5dc0ef6fd..d6a8b97cf 100644 --- a/frontend/farm_designer/zones/add_zone.tsx +++ b/frontend/farm_designer/zones/add_zone.tsx @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import { - DesignerPanel, DesignerPanelContent, DesignerPanelHeader + DesignerPanel, DesignerPanelContent, DesignerPanelHeader, } from "../designer_panel"; import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; diff --git a/frontend/farm_designer/zones/edit_zone.tsx b/frontend/farm_designer/zones/edit_zone.tsx index d21ee2476..3ecd18bae 100644 --- a/frontend/farm_designer/zones/edit_zone.tsx +++ b/frontend/farm_designer/zones/edit_zone.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { connect } from "react-redux"; import { - DesignerPanel, DesignerPanelHeader, DesignerPanelContent + DesignerPanel, DesignerPanelHeader, DesignerPanelContent, } from "../designer_panel"; import { t } from "../../i18next_wrapper"; import { history, getPathArray } from "../../history"; @@ -31,12 +31,9 @@ export class RawEditZone extends React.Component { } } - fallback = () => { - history.push("/app/designer/zones"); - return {t("Redirecting...")}; - } - - default = (zone: TaggedPointGroup) => { + render() { + const { zone } = this; + !zone && history.push("/app/designer/zones"); return { title={`${t("Edit")} zone`} backTo={"/app/designer/zones"} /> - - { - this.props.dispatch(edit(zone, { name: e.currentTarget.value })); - this.props.dispatch(save(zone.uuid)); - }} /> - + {zone + ?
    + + { + this.props.dispatch(edit(zone, { name: e.currentTarget.value })); + this.props.dispatch(save(zone.uuid)); + }} /> + +
    + : {t("Redirecting")}...}
    ; } - - render() { - return this.zone ? this.default(this.zone) : this.fallback(); - } } export const EditZone = connect(mapStateToProps)(RawEditZone); diff --git a/frontend/farm_designer/zones/zones_inventory.tsx b/frontend/farm_designer/zones/zones_inventory.tsx index 93d77ea7c..54d049e67 100644 --- a/frontend/farm_designer/zones/zones_inventory.tsx +++ b/frontend/farm_designer/zones/zones_inventory.tsx @@ -3,16 +3,16 @@ import { connect } from "react-redux"; import { Everything } from "../../interfaces"; import { DesignerNavTabs, Panel } from "../panel_header"; import { - EmptyStateWrapper, EmptyStateGraphic + EmptyStateWrapper, EmptyStateGraphic, } from "../../ui/empty_state_wrapper"; import { Content } from "../../constants"; import { - DesignerPanel, DesignerPanelContent, DesignerPanelTop + DesignerPanel, DesignerPanelContent, DesignerPanelTop, } from "../designer_panel"; import { t } from "../../i18next_wrapper"; import { TaggedPointGroup, TaggedPoint } from "farmbot"; import { - selectAllPointGroups, selectAllActivePoints + selectAllPointGroups, selectAllActivePoints, } from "../../resources/selectors"; import { GroupInventoryItem } from "../point_groups/group_inventory_item"; import { history } from "../../history"; @@ -53,7 +53,7 @@ export class RawZones extends React.Component { })) .then((id: number) => this.navigate(id)).catch(() => { })} title={t("Add zone")}> - diff --git a/frontend/farmware/__tests__/actions_test.ts b/frontend/farmware/__tests__/actions_test.ts index a32929d45..b7b315c76 100644 --- a/frontend/farmware/__tests__/actions_test.ts +++ b/frontend/farmware/__tests__/actions_test.ts @@ -2,8 +2,8 @@ jest.mock("axios", () => ({ get: jest.fn(() => { return Promise.resolve({ data: [ - { manifest: "url", name: "farmware0" }, - { manifest: "url", name: "farmware1" } + { package: "farmware0" }, + { package: "farmware1" }, ] }); }), diff --git a/frontend/farmware/__tests__/farmware_forms_test.tsx b/frontend/farmware/__tests__/farmware_forms_test.tsx index 39f008e98..e45286375 100644 --- a/frontend/farmware/__tests__/farmware_forms_test.tsx +++ b/frontend/farmware/__tests__/farmware_forms_test.tsx @@ -11,7 +11,7 @@ import * as React from "react"; import { mount, shallow } from "enzyme"; import { needsFarmwareForm, farmwareHelpText, getConfigEnvName, - FarmwareForm, FarmwareFormProps, ConfigFields + FarmwareForm, FarmwareFormProps, ConfigFields, } from "../farmware_forms"; import { fakeFarmware } from "../../__test_support__/fake_farmwares"; import { clickButton } from "../../__test_support__/helpers"; @@ -123,7 +123,6 @@ describe("", () => { "My Fake Farmware", [{ kind: "pair", args: { label: "my_fake_farmware_config_1", value: "4" } - }] - ); + }]); }); }); diff --git a/frontend/farmware/__tests__/farmware_info_test.tsx b/frontend/farmware/__tests__/farmware_info_test.tsx index c55d3beb5..36ec47754 100644 --- a/frontend/farmware/__tests__/farmware_info_test.tsx +++ b/frontend/farmware/__tests__/farmware_info_test.tsx @@ -15,7 +15,7 @@ import { fakeFarmware } from "../../__test_support__/fake_farmwares"; import { clickButton } from "../../__test_support__/helpers"; import { destroy } from "../../api/crud"; import { - fakeFarmwareInstallation + fakeFarmwareInstallation, } from "../../__test_support__/fake_state/resources"; import { error } from "../../toast/toast"; import { retryFetchPackageName } from "../actions"; diff --git a/frontend/farmware/__tests__/farmware_list_test.tsx b/frontend/farmware/__tests__/farmware_list_test.tsx index 66d7ada0a..f06a6d290 100644 --- a/frontend/farmware/__tests__/farmware_list_test.tsx +++ b/frontend/farmware/__tests__/farmware_list_test.tsx @@ -10,7 +10,7 @@ import * as React from "react"; import { mount, shallow } from "enzyme"; import { FarmwareList, FarmwareListProps } from "../farmware_list"; import { - fakeFarmwares, fakeFarmware + fakeFarmwares, fakeFarmware, } from "../../__test_support__/fake_farmwares"; import { clickButton } from "../../__test_support__/helpers"; import { Actions } from "../../constants"; diff --git a/frontend/farmware/__tests__/farmware_test.tsx b/frontend/farmware/__tests__/farmware_test.tsx index 9fd3fad03..6539398bb 100644 --- a/frontend/farmware/__tests__/farmware_test.tsx +++ b/frontend/farmware/__tests__/farmware_test.tsx @@ -6,7 +6,7 @@ import { mount } from "enzyme"; import { RawFarmwarePage as FarmwarePage, BasicFarmwarePage } from "../index"; import { FarmwareProps } from "../../devices/interfaces"; import { - fakeFarmware, fakeFarmwares + fakeFarmware, fakeFarmwares, } from "../../__test_support__/fake_farmwares"; import { clickButton } from "../../__test_support__/helpers"; import { Actions } from "../../constants"; @@ -90,7 +90,7 @@ describe("", () => { p.currentFarmware = "My Fake Test Farmware"; const wrapper = mount(); ["My Fake Test Farmware", "Does things", "Run", "Config 1", - "Information", "Description", "Version", "Update", "Remove" + "Information", "Description", "Version", "Update", "Remove", ].map(string => expect(wrapper.text()).toContain(string)); }); @@ -103,7 +103,7 @@ describe("", () => { p.farmwares["My Fake Test Farmware"] = farmware; p.currentFarmware = "My Fake Test Farmware"; const wrapper = mount(); - ["My Fake Farmware", "Does things", "Run", "No inputs provided." + ["My Fake Farmware", "Does things", "Run", "No inputs provided.", ].map(string => expect(wrapper.text()).toContain(string)); }); diff --git a/frontend/farmware/__tests__/generate_manifest_info_test.ts b/frontend/farmware/__tests__/generate_manifest_info_test.ts index aec356c24..4c9b36283 100644 --- a/frontend/farmware/__tests__/generate_manifest_info_test.ts +++ b/frontend/farmware/__tests__/generate_manifest_info_test.ts @@ -1,6 +1,6 @@ import { manifestInfo, manifestInfoPending } from "../generate_manifest_info"; import { - fakeFarmwareManifestV1, fakeFarmwareManifestV2 + fakeFarmwareManifestV1, fakeFarmwareManifestV2, } from "../../__test_support__/fake_farmwares"; describe("manifestInfo()", () => { diff --git a/frontend/farmware/__tests__/reducer_test.ts b/frontend/farmware/__tests__/reducer_test.ts index c763be85d..65cae270b 100644 --- a/frontend/farmware/__tests__/reducer_test.ts +++ b/frontend/farmware/__tests__/reducer_test.ts @@ -2,7 +2,7 @@ import { farmwareReducer } from "../reducer"; import { FarmwareState } from "../interfaces"; import { Actions } from "../../constants"; import { - fakeImage, fakeFarmwareInstallation + fakeImage, fakeFarmwareInstallation, } from "../../__test_support__/fake_state/resources"; describe("farmwareReducer", () => { diff --git a/frontend/farmware/__tests__/state_to_props_test.tsx b/frontend/farmware/__tests__/state_to_props_test.tsx index ec3d7c2af..8cc47c3f6 100644 --- a/frontend/farmware/__tests__/state_to_props_test.tsx +++ b/frontend/farmware/__tests__/state_to_props_test.tsx @@ -7,10 +7,10 @@ jest.mock("../../api/crud", () => ({ import { mapStateToProps, saveOrEditFarmwareEnv } from "../state_to_props"; import { fakeState } from "../../__test_support__/fake_state"; import { - buildResourceIndex + buildResourceIndex, } from "../../__test_support__/resource_index_builder"; import { - fakeFarmwareEnv, fakeFarmwareInstallation + fakeFarmwareEnv, fakeFarmwareInstallation, } from "../../__test_support__/fake_state/resources"; import { edit, initSave, save } from "../../api/crud"; import { fakeFarmwareManifestV1 } from "../../__test_support__/fake_farmwares"; diff --git a/frontend/farmware/actions.ts b/frontend/farmware/actions.ts index 0453e9d7f..c10135119 100644 --- a/frontend/farmware/actions.ts +++ b/frontend/farmware/actions.ts @@ -1,17 +1,14 @@ import axios from "axios"; -import { FarmwareManifestEntry } from "./interfaces"; import { Actions } from "../constants"; import { urlFor } from "../api/crud"; - -const farmwareManifestUrl = - "https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests" + - "/master/manifest.json"; +import { API } from "../api"; +import { FarmwareManifest } from "farmbot"; export const getFirstPartyFarmwareList = () => { return (dispatch: Function) => { - axios.get(farmwareManifestUrl) + axios.get(API.current.firstPartyFarmwarePath) .then(r => { - const names = r.data.map((fw: FarmwareManifestEntry) => fw.name); + const names = r.data.map(fw => fw.package); dispatch({ type: Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK, payload: names diff --git a/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx b/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx index 577f65133..636fc2f97 100644 --- a/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx +++ b/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx @@ -3,6 +3,13 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); jest.mock("../actions", () => ({ scanImage: jest.fn() })); jest.mock("../../images/actions", () => ({ selectImage: jest.fn() })); +let mockDev = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + import * as React from "react"; import { mount, shallow } from "enzyme"; import { CameraCalibration } from "../camera_calibration"; @@ -12,6 +19,7 @@ import { selectImage } from "../../images/actions"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { error } from "../../../toast/toast"; import { Content, ToolTips } from "../../../constants"; +import { SPECIAL_VALUES } from "../../weed_detector/remote_env/constants"; describe("", () => { const fakeProps = (): CameraCalibrationProps => ({ @@ -43,7 +51,7 @@ describe("", () => { "SATURATION025558", "VALUE025569", "Processing Parameters", - "Scan image" + "Scan image", ].map(string => expect(wrapper.text()).toContain(string)); }); @@ -116,4 +124,21 @@ describe("", () => { expect(error).toHaveBeenCalledWith( ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED); }); + + it("toggles simple version", () => { + mockDev = true; + const p = fakeProps(); + const wrapper = mount(); + wrapper.find("input").first().simulate("change"); + expect(mockDevice.setUserEnv).toHaveBeenCalledWith({ + CAMERA_CALIBRATION_easy_calibration: "\"FALSE\"" + }); + }); + + it("renders simple version", () => { + const p = fakeProps(); + p.wDEnv = { CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.TRUE }; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("blur"); + }); }); diff --git a/frontend/farmware/camera_calibration/camera_calibration.tsx b/frontend/farmware/camera_calibration/camera_calibration.tsx index 6654eac51..fd113b414 100644 --- a/frontend/farmware/camera_calibration/camera_calibration.tsx +++ b/frontend/farmware/camera_calibration/camera_calibration.tsx @@ -8,14 +8,18 @@ import { selectImage } from "../images/actions"; import { calibrate, scanImage } from "./actions"; import { envGet } from "../weed_detector/remote_env/selectors"; import { MustBeOnline, isBotOnline } from "../../devices/must_be_online"; -import { WeedDetectorConfig } from "../weed_detector/config"; +import { WeedDetectorConfig, BoolConfig } from "../weed_detector/config"; import { Feature } from "../../devices/interfaces"; import { namespace } from "../weed_detector"; import { t } from "../../i18next_wrapper"; import { formatEnvKey } from "../weed_detector/remote_env/translators"; import { - cameraBtnProps + cameraBtnProps, } from "../../devices/components/fbos_settings/camera_selection"; +import { ImageFlipper } from "../images/image_flipper"; +import { PhotoFooter } from "../images/photos"; +import { UUID } from "../../resources/interfaces"; +import { DevSettings } from "../../account/dev/dev_support"; export class CameraCalibration extends React.Component { @@ -31,9 +35,11 @@ export class CameraCalibration extends key, JSON.stringify(formatEnvKey(key, value)))) : envSave(key, value) + onFlip = (uuid: UUID) => this.props.dispatch(selectImage(uuid)); + render() { const camDisabled = cameraBtnProps(this.props.env); - return
    + return
    - - } + {!!envGet(this.namespace("easy_calibration"), this.props.wDEnv) + ?
    + + +
    + : this.props.dispatch(scanImage(id))} - onFlip={uuid => this.props.dispatch(selectImage(uuid))} + onFlip={this.onFlip} images={this.props.images} currentImage={this.props.currentImage} onChange={this.change} @@ -73,11 +91,10 @@ export class CameraCalibration extends S_HI={this.props.S_HI} V_HI={this.props.V_HI} invertHue={!!envGet(this.namespace("invert_hue_selection"), - this.props.wDEnv)} /> - -
    + this.props.wDEnv)} />} +
    ; diff --git a/frontend/farmware/camera_calibration/interfaces.ts b/frontend/farmware/camera_calibration/interfaces.ts index 98a1b7ad0..aae4a2579 100644 --- a/frontend/farmware/camera_calibration/interfaces.ts +++ b/frontend/farmware/camera_calibration/interfaces.ts @@ -2,7 +2,7 @@ import { TaggedImage, SyncStatus } from "farmbot"; import { WD_ENV } from "../weed_detector/remote_env/interfaces"; import { NetworkState } from "../../connectivity/interfaces"; import { - ShouldDisplay, SaveFarmwareEnv, UserEnv + ShouldDisplay, SaveFarmwareEnv, UserEnv, } from "../../devices/interfaces"; import { TimeSettings } from "../../interfaces"; diff --git a/frontend/farmware/farmware_config_menu.tsx b/frontend/farmware/farmware_config_menu.tsx index bcffab66e..caecf2044 100644 --- a/frontend/farmware/farmware_config_menu.tsx +++ b/frontend/farmware/farmware_config_menu.tsx @@ -22,6 +22,7 @@ export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
    @@ -44,6 +46,7 @@ export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) { diff --git a/frontend/farmware/farmware_info.tsx b/frontend/farmware/farmware_info.tsx index 6b0a3351c..c5cea4b51 100644 --- a/frontend/farmware/farmware_info.tsx +++ b/frontend/farmware/farmware_info.tsx @@ -44,11 +44,11 @@ const removeFromAPI = (props: { const FarmwareToolsVersionField = ({ version }: { version: string | undefined }) => (version && version != "latest") - ?
    + ?

    {version}

    - :
    ; + :
    ; const PendingInstallNameError = ({ url, installations }: { @@ -64,11 +64,12 @@ const PendingInstallNameError =

    {packageError}

    - :
    ; + :
    ; }; type RemoveFarmwareFunction = @@ -79,20 +80,22 @@ const FarmwareManagementSection = farmware: FarmwareManifestInfo, remove: RemoveFarmwareFunction, }) => -
    +
    {farmware.url}
    -
    +
    @@ -137,25 +140,29 @@ const uninstallFarmware = (props: RemoveFarmwareProps) => export function FarmwareInfo(props: FarmwareInfoProps) { const { farmware } = props; - return farmware ?
    - -

    {farmware.meta.description}

    - -

    {farmware.meta.version}

    - -

    {farmware.meta.fbos_version}

    - - -

    {farmware.meta.language}

    - -

    {farmware.meta.author === "Farmbot.io" - ? "FarmBot, Inc." - : farmware.meta.author}

    - - -
    :

    {t(Content.NOT_AVAILABLE_WHEN_OFFLINE)}

    ; + return farmware + ?
    + +

    {farmware.meta.description}

    + +

    {farmware.meta.version}

    + +

    {farmware.meta.fbos_version}

    + + +

    {farmware.meta.language}

    + +

    {farmware.meta.author === "Farmbot.io" + ? "FarmBot, Inc." + : farmware.meta.author}

    + + +
    + :
    +

    {t(Content.NOT_AVAILABLE_WHEN_OFFLINE)}

    +
    ; } diff --git a/frontend/farmware/farmware_list.tsx b/frontend/farmware/farmware_list.tsx index a1e1de12d..2f3250677 100644 --- a/frontend/farmware/farmware_list.tsx +++ b/frontend/farmware/farmware_list.tsx @@ -118,12 +118,13 @@ export class FarmwareList {t("Install new Farmware")}
    - this.setState({ packageUrl: e.currentTarget.value })} /> diff --git a/frontend/farmware/images/image_flipper.tsx b/frontend/farmware/images/image_flipper.tsx index 3730429a4..1f77dfe54 100644 --- a/frontend/farmware/images/image_flipper.tsx +++ b/frontend/farmware/images/image_flipper.tsx @@ -29,7 +29,7 @@ export class ImageFlipper extends const url = image.body.attachment_processed_at ? image.body.attachment_url : PLACEHOLDER_FARMBOT; - return
    + return
    {!this.state.isLoaded && } @@ -67,12 +67,14 @@ export class ImageFlipper extends diff --git a/frontend/farmware/index.tsx b/frontend/farmware/index.tsx index 29f4482b4..751d8b558 100644 --- a/frontend/farmware/index.tsx +++ b/frontend/farmware/index.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { connect } from "react-redux"; import { - Page, Row, LeftPanel, CenterPanel, RightPanel, DocSlug + Page, Row, LeftPanel, CenterPanel, RightPanel, DocSlug, } from "../ui/index"; import { mapStateToProps, isPendingInstallation } from "./state_to_props"; import { Photos } from "./images/photos"; @@ -12,7 +12,7 @@ import { envGet } from "./weed_detector/remote_env/selectors"; import { setActiveFarmwareByName } from "./set_active_farmware_by_name"; import { FarmwareList } from "./farmware_list"; import { - FarmwareForm, needsFarmwareForm, farmwareHelpText + FarmwareForm, needsFarmwareForm, farmwareHelpText, } from "./farmware_forms"; import { urlFriendly } from "../util"; import { ToolTips, Actions } from "../constants"; @@ -102,10 +102,11 @@ interface BasicFarmwarePageProps { export const BasicFarmwarePage = ({ farmwareName, farmware, botOnline }: BasicFarmwarePageProps) => -
    +
    @@ -208,6 +209,7 @@ export class RawFarmwarePage extends React.Component {
    - - this.props.dispatch(scanImage(id))} - onFlip={uuid => this.props.dispatch(selectImage(uuid))} - currentImage={this.props.currentImage} - images={this.props.images} - onChange={this.change} - timeSettings={this.props.timeSettings} - iteration={wDEnvGet(this.namespace("iteration"))} - morph={wDEnvGet(this.namespace("morph"))} - blur={wDEnvGet(this.namespace("blur"))} - H_LO={wDEnvGet(this.namespace("H_LO"))} - H_HI={wDEnvGet(this.namespace("H_HI"))} - S_LO={wDEnvGet(this.namespace("S_LO"))} - S_HI={wDEnvGet(this.namespace("S_HI"))} - V_LO={wDEnvGet(this.namespace("V_LO"))} - V_HI={wDEnvGet(this.namespace("V_HI"))} /> - + this.props.dispatch(scanImage(id))} + onFlip={uuid => this.props.dispatch(selectImage(uuid))} + currentImage={this.props.currentImage} + images={this.props.images} + onChange={this.change} + timeSettings={this.props.timeSettings} + iteration={wDEnvGet(this.namespace("iteration"))} + morph={wDEnvGet(this.namespace("morph"))} + blur={wDEnvGet(this.namespace("blur"))} + H_LO={wDEnvGet(this.namespace("H_LO"))} + H_HI={wDEnvGet(this.namespace("H_HI"))} + S_LO={wDEnvGet(this.namespace("S_LO"))} + S_HI={wDEnvGet(this.namespace("S_HI"))} + V_LO={wDEnvGet(this.namespace("V_LO"))} + V_HI={wDEnvGet(this.namespace("V_HI"))} />
    ; diff --git a/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts b/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts index 3e9696200..ec61bd735 100644 --- a/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts +++ b/frontend/farmware/weed_detector/remote_env/__tests__/translators_test.ts @@ -40,6 +40,11 @@ describe("formatEnvKey()", () => { v: SPECIAL_VALUES.FALSE, r: "FALSE" }, + { + k: "CAMERA_CALIBRATION_easy_calibration", + v: SPECIAL_VALUES.FALSE, + r: "FALSE" + }, { k: "CAMERA_CALIBRATION_calibration_along_axis", v: SPECIAL_VALUES.X, @@ -54,7 +59,7 @@ describe("formatEnvKey()", () => { k: "CAMERA_CALIBRATION_image_bot_origin_location", v: SPECIAL_VALUES.TOP_LEFT, r: "TOP_LEFT" - } + }, ].map(t => { expect(formatEnvKey(t.k as WDENVKey, t.v)).toEqual(t.r); }); diff --git a/frontend/farmware/weed_detector/remote_env/constants.ts b/frontend/farmware/weed_detector/remote_env/constants.ts index 762fbb480..1c9dced3c 100644 --- a/frontend/farmware/weed_detector/remote_env/constants.ts +++ b/frontend/farmware/weed_detector/remote_env/constants.ts @@ -25,6 +25,7 @@ export const WD_KEY_DEFAULTS = { CAMERA_CALIBRATION_calibration_along_axis: SPECIAL_VALUES.X, CAMERA_CALIBRATION_image_bot_origin_location: SPECIAL_VALUES.BOTTOM_LEFT, CAMERA_CALIBRATION_invert_hue_selection: SPECIAL_VALUES.TRUE, + CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.FALSE, CAMERA_CALIBRATION_blur: 5, CAMERA_CALIBRATION_calibration_object_separation: 100, CAMERA_CALIBRATION_camera_offset_x: 50, @@ -61,6 +62,7 @@ export const DEFAULT_FORMATTER: Translation = { case "CAMERA_CALIBRATION_calibration_along_axis": case "CAMERA_CALIBRATION_image_bot_origin_location": case "CAMERA_CALIBRATION_invert_hue_selection": + case "CAMERA_CALIBRATION_easy_calibration": return ("" + (SPECIAL_VALUES[val] || val)); default: return val; diff --git a/frontend/folders/__tests__/actions_test.ts b/frontend/folders/__tests__/actions_test.ts index 652978f9c..f39285a4c 100644 --- a/frontend/folders/__tests__/actions_test.ts +++ b/frontend/folders/__tests__/actions_test.ts @@ -77,7 +77,7 @@ const mockFolders: FolderNode[] = [ { id: 15, parent_id: 14, color: "blue", name: "Fifteen" }, { id: 16, parent_id: 14, color: "blue", name: "Sixteen" }, { id: 17, parent_id: 16, color: "blue", name: "Seventeen" }, - { id: 18, parent_id: 16, color: "blue", name: "Eighteen" } + { id: 18, parent_id: 16, color: "blue", name: "Eighteen" }, ]; const mockSequence = fakeSequence(); diff --git a/frontend/folders/__tests__/component_test.tsx b/frontend/folders/__tests__/component_test.tsx index 927fb8cfa..94d886679 100644 --- a/frontend/folders/__tests__/component_test.tsx +++ b/frontend/folders/__tests__/component_test.tsx @@ -294,14 +294,14 @@ describe("", () => { it("starts sequence move", () => { const p = fakeProps(); const wrapper = shallow(); - wrapper.find(".fa-bars").simulate("mouseDown"); + wrapper.find(".fa-arrows-v").simulate("mouseDown"); expect(p.startSequenceMove).toHaveBeenCalledWith(p.sequence.uuid); }); it("toggles sequence move", () => { const p = fakeProps(); const wrapper = shallow(); - wrapper.find(".fa-bars").simulate("mouseUp"); + wrapper.find(".fa-arrows-v").simulate("mouseUp"); expect(p.toggleSequenceMove).toHaveBeenCalledWith(p.sequence.uuid); }); }); diff --git a/frontend/folders/__tests__/data_transfer_test.ts b/frontend/folders/__tests__/data_transfer_test.ts index f44ab58b4..2f7c13d3e 100644 --- a/frontend/folders/__tests__/data_transfer_test.ts +++ b/frontend/folders/__tests__/data_transfer_test.ts @@ -7,7 +7,7 @@ const FOLDERS: FolderNode[] = [ { id: 2, color: "green", name: "Folder for growing things", parent_id: undefined }, { id: 3, color: "yellow", name: "subfolder", parent_id: 2 }, { id: 4, color: "gray", name: "tests", parent_id: undefined }, - { id: 5, color: "pink", name: "deeply nested directory", parent_id: 3 } + { id: 5, color: "pink", name: "deeply nested directory", parent_id: 3 }, ]; const TREE = ingest({ folders: FOLDERS, diff --git a/frontend/folders/__tests__/reducer_test.ts b/frontend/folders/__tests__/reducer_test.ts index 7c28380af..dedfd2c32 100644 --- a/frontend/folders/__tests__/reducer_test.ts +++ b/frontend/folders/__tests__/reducer_test.ts @@ -2,10 +2,10 @@ import { resourceReducer } from "../../resources/reducer"; import { RestResources } from "../../resources/interfaces"; import { fakeSequence, - fakeFolder + fakeFolder, } from "../../__test_support__/fake_state/resources"; import { - buildResourceIndex + buildResourceIndex, } from "../../__test_support__/resource_index_builder"; import { Actions } from "../../constants"; diff --git a/frontend/folders/__tests__/search_folder_tree_test.ts b/frontend/folders/__tests__/search_folder_tree_test.ts index a71b6a603..d5637ab8d 100644 --- a/frontend/folders/__tests__/search_folder_tree_test.ts +++ b/frontend/folders/__tests__/search_folder_tree_test.ts @@ -37,7 +37,7 @@ describe("searchFolderTree", () => { "Six", "Sixteen", // == GRANDPARENTS - "Fourteen" + "Fourteen", ].map(x => expect(results).toContain(x)); expect(results.length).toEqual(5); const results2 = searchFor("Eleven").map(x => x.name); @@ -153,13 +153,13 @@ const fakeSearchProps = (input: string): FolderSearchProps => ({ "content": ["Sequence.67.12"], "open": true, "editing": false - } + }, ], "content": ["Sequence.66.11"] - } + }, ], "content": ["Sequence.65.10"] - } + }, ] } }); diff --git a/frontend/folders/actions.ts b/frontend/folders/actions.ts index 449501dfb..68e269657 100644 --- a/frontend/folders/actions.ts +++ b/frontend/folders/actions.ts @@ -31,20 +31,20 @@ export const setFolderColor = (id: number, color: Color) => { d(save(f.uuid)); }; -export const setFolderName = (id: number, name: string) => { +export const setFolderName = (id: number, folderName: string) => { const d = store.dispatch as Function; const { index } = store.getState().resources; const folder = findFolderById(index, id); - const action = edit(folder, { name }); + const action = edit(folder, { name: folderName }); d(action); return d(save(folder.uuid)) as Promise<{}>; }; -const DEFAULTS: Folder = { - name: "New Folder", +const DEFAULTS = (): Folder => ({ + name: t("New Folder"), color: "gray", parent_id: 0, -}; +}); export const addNewSequenceToFolder = (folder_id?: number) => { const uuidMap = store.getState().resources.index.byKind["Sequence"]; @@ -67,7 +67,7 @@ export const addNewSequenceToFolder = (folder_id?: number) => { export const createFolder = (config: DeepPartial = {}) => { const d: Function = store.dispatch; - const folder: Folder = { ...DEFAULTS, ...config }; + const folder: Folder = { ...DEFAULTS(), ...config }; const action = initSave("Folder", folder); // tslint:disable-next-line:no-any const p: Promise<{}> = d(action); diff --git a/frontend/folders/climb.ts b/frontend/folders/climb.ts index 6f88092ef..9b6bc3eb7 100644 --- a/frontend/folders/climb.ts +++ b/frontend/folders/climb.ts @@ -1,5 +1,5 @@ import { - RootFolderNode, FolderUnion, FolderNodeMedial, FolderNodeInitial + RootFolderNode, FolderUnion, FolderNodeMedial, FolderNodeInitial, } from "./interfaces"; import { defensiveClone } from "../util"; diff --git a/frontend/folders/component.tsx b/frontend/folders/component.tsx index 3fb2b26f6..a5e3ec9cc 100644 --- a/frontend/folders/component.tsx +++ b/frontend/folders/component.tsx @@ -38,7 +38,7 @@ import { import { Link } from "../link"; import { urlFriendly, lastUrlChunk } from "../util"; import { - setActiveSequenceByName + setActiveSequenceByName, } from "../sequences/set_active_sequence_by_name"; import { Popover } from "@blueprintjs/core"; import { t } from "../i18next_wrapper"; @@ -75,7 +75,7 @@ export const FolderListItem = (props: FolderItemProps) => {
    {props.inUse && } - props.startSequenceMove(sequence.uuid)} onMouseUp={() => props.toggleSequenceMove(sequence.uuid)} />
    @@ -84,7 +84,9 @@ export const FolderListItem = (props: FolderItemProps) => { }; const ToggleFolderBtn = (props: ToggleFolderBtnProps) => { - return ; }; @@ -92,6 +94,7 @@ const ToggleFolderBtn = (props: ToggleFolderBtnProps) => { const AddFolderBtn = ({ folder, close }: AddFolderBtn) => { return @@ -139,6 +145,7 @@ export const FolderNameInput = ({ node }: FolderNameInputProps) => }} /> @@ -327,8 +334,8 @@ export const FolderPanelTop = (props: FolderPanelTopProps) => updateSearchTerm(e.currentTarget.value)} - type="text" - placeholder={t("Search sequences")} /> + type="text" name="searchTerm" + placeholder={t("Search sequences...")} />
    { noFolder: (localMetaAttributes[PARENTLESS] || {}).sequences || [] }; const index = folders.map(setDefaultParentId).reduce(addToIndex, emptyIndex); - const childrenOf = (i: number) => sortBy(index[i] || [], (x) => x.name.toLowerCase()); + const childrenOf = (i: number) => + sortBy(index[i] || [], (x) => x.name.toLowerCase()); const terminal = (x: FolderNode): FolderNodeTerminal => ({ ...x, kind: "terminal", content: (localMetaAttributes[x.id] || {}).sequences || [], - open: true, + open: false, editing: false, // children: [], ...(localMetaAttributes[x.id] || {}) @@ -55,7 +56,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => { const medial = (x: FolderNode): FolderNodeMedial => ({ ...x, kind: "medial", - open: true, + open: false, editing: false, children: childrenOf(x.id).map(terminal), content: (localMetaAttributes[x.id] || {}).sequences || [], @@ -67,7 +68,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => { return output.folders.push({ ...root, kind: "initial", - open: true, + open: false, editing: false, children, content: (localMetaAttributes[root.id] || {}).sequences || [], diff --git a/frontend/front_page/__tests__/create_account_test.tsx b/frontend/front_page/__tests__/create_account_test.tsx index 97722079d..54ed91436 100644 --- a/frontend/front_page/__tests__/create_account_test.tsx +++ b/frontend/front_page/__tests__/create_account_test.tsx @@ -11,7 +11,7 @@ jest.mock("../resend_verification", () => { import * as React from "react"; import { - FormField, sendEmail, DidRegister, MustRegister, CreateAccount + FormField, sendEmail, DidRegister, MustRegister, CreateAccount, } from "../create_account"; import { shallow } from "enzyme"; import { BlurableInput } from "../../ui/index"; diff --git a/frontend/front_page/create_account.tsx b/frontend/front_page/create_account.tsx index a4420daa4..f87331fb5 100644 --- a/frontend/front_page/create_account.tsx +++ b/frontend/front_page/create_account.tsx @@ -6,7 +6,7 @@ import { WidgetHeader, Row, BlurableInput, - BIProps + BIProps, } from "../ui/index"; import { resendEmail } from "./resend_verification"; @@ -44,13 +44,14 @@ interface FormFieldProps { onCommit(val: string): void; } -export const FormField = (props: FormFieldProps) =>
    - - props.onCommit(e.currentTarget.value)} /> -
    ; +export const FormField = (props: FormFieldProps) => +
    + + props.onCommit(e.currentTarget.value)} /> +
    ; interface FieldData { label: string, @@ -94,6 +95,7 @@ export function MustRegister(props: CreateAccountProps) { {props.children} diff --git a/frontend/front_page/forgot_password.tsx b/frontend/front_page/forgot_password.tsx index 705c1cbcb..dd0969c48 100644 --- a/frontend/front_page/forgot_password.tsx +++ b/frontend/front_page/forgot_password.tsx @@ -22,6 +22,7 @@ export function ForgotPassword(props: ForgotPasswordProps) { @@ -35,6 +36,7 @@ export function ForgotPassword(props: ForgotPasswordProps) { onCommit={onEmailChange} /> diff --git a/frontend/front_page/front_page.tsx b/frontend/front_page/front_page.tsx index ae1c15241..21fdf1a6b 100644 --- a/frontend/front_page/front_page.tsx +++ b/frontend/front_page/front_page.tsx @@ -38,19 +38,19 @@ export interface PartialFormEvent { /** Set value for front page state field (except for "activePanel"). */ export const setField = - (name: keyof Omit, cb: SetterCB) => + (field: keyof Omit, cb: SetterCB) => (event: PartialFormEvent) => { const state: Partial = {}; - switch (name) { + switch (field) { // Booleans case "agreeToTerms": case "registrationSent": - state[name] = event.currentTarget.checked; + state[field] = event.currentTarget.checked; break; // all others (string) default: - state[name] = event.currentTarget.value; + state[field] = event.currentTarget.value; } cb(state); }; @@ -259,5 +259,9 @@ export class FrontPage extends React.Component<{}, Partial> {
    ; } - render() { return Session.fetchStoredToken() ?
    : this.defaultContent(); } + render() { + return Session.fetchStoredToken() + ?
    + : this.defaultContent(); + } } diff --git a/frontend/front_page/laptop_splash.tsx b/frontend/front_page/laptop_splash.tsx index ec4308bfa..dc81416fa 100644 --- a/frontend/front_page/laptop_splash.tsx +++ b/frontend/front_page/laptop_splash.tsx @@ -1,6 +1,5 @@ import * as React from "react"; -const VIDEO_URL = "https://cdn.shopify.com/s/files/1/2040/0289/files/" + - "Farm_Designer_Loop.mp4?9552037556691879018"; +import { ExternalUrl } from "../external_urls"; export const LaptopSplash = ({ className }: { className: string }) =>
    @@ -8,7 +7,7 @@ export const LaptopSplash = ({ className }: { className: string }) =>
    diff --git a/frontend/front_page/login.tsx b/frontend/front_page/login.tsx index ac0b4fbe5..d803f9126 100644 --- a/frontend/front_page/login.tsx +++ b/frontend/front_page/login.tsx @@ -75,11 +75,14 @@ export class Login extends React.Component { - + {t("Forgot password?")} - diff --git a/frontend/front_page/resend_panel_body.tsx b/frontend/front_page/resend_panel_body.tsx index a227e017b..7bf6325dd 100644 --- a/frontend/front_page/resend_panel_body.tsx +++ b/frontend/front_page/resend_panel_body.tsx @@ -16,6 +16,7 @@ export function ResendPanelBody(props: { onClick(): void; }) { diff --git a/frontend/front_page/resend_verification.tsx b/frontend/front_page/resend_verification.tsx index 16cac44ca..2beaef07b 100644 --- a/frontend/front_page/resend_verification.tsx +++ b/frontend/front_page/resend_verification.tsx @@ -29,6 +29,7 @@ export class ResendVerification extends React.Component { diff --git a/frontend/front_page/terms_checkbox.tsx b/frontend/front_page/terms_checkbox.tsx index 7751040e6..cec89923e 100644 --- a/frontend/front_page/terms_checkbox.tsx +++ b/frontend/front_page/terms_checkbox.tsx @@ -13,7 +13,7 @@ export const TermsCheckbox = (props: { {t("Privacy Policy")} {` ${t("and")} `} {t("Terms of Use")} -
    ; diff --git a/frontend/help/__tests__/tour_test.tsx b/frontend/help/__tests__/tour_test.tsx index a3c5908ad..35fbb279e 100644 --- a/frontend/help/__tests__/tour_test.tsx +++ b/frontend/help/__tests__/tour_test.tsx @@ -13,7 +13,7 @@ import { history } from "../../history"; import { CallBackProps } from "react-joyride"; describe("", () => { - const EMPTY_DIV = "
    "; + const EMPTY_DIV = "
    "; it("tour is running", () => { const wrapper = mount(); @@ -56,7 +56,7 @@ describe("", () => { expect(wrapper.state()).toEqual({ run: true, index: 1, returnPath: "/app/messages" }); - expect(history.push).toHaveBeenCalledWith("/app/tools"); + expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); it("navigates through tour: other", () => { diff --git a/frontend/help/__tests__/tours_test.ts b/frontend/help/__tests__/tours_test.ts index 2ab27cf74..de9ee790f 100644 --- a/frontend/help/__tests__/tours_test.ts +++ b/frontend/help/__tests__/tours_test.ts @@ -1,7 +1,15 @@ jest.mock("../../history", () => ({ history: { push: jest.fn() } })); -import { tourPageNavigation } from "../tours"; +import { fakeState } from "../../__test_support__/fake_state"; +const mockState = fakeState(); +jest.mock("../../redux/store", () => ({ + store: { getState: () => mockState }, +})); + +import { tourPageNavigation, TOUR_STEPS, Tours } from "../tours"; import { history } from "../../history"; +import { fakeTool, fakeFbosConfig } from "../../__test_support__/fake_state/resources"; +import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; describe("tourPageNavigation()", () => { const testCase = (el: string) => { @@ -20,8 +28,36 @@ describe("tourPageNavigation()", () => { testCase(".regimen-list-panel"); testCase(".tool-list"); testCase(".toolbay-list"); + testCase(".tools"); + testCase(".tool-slots"); + testCase(".tools-panel"); testCase(".photos"); testCase(".logs-table"); testCase(".app-settings-widget"); }); + + it("includes steps based on tool count", () => { + const getTargets = () => + Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target); + mockState.resources = buildResourceIndex([]); + expect(getTargets()).not.toContain(".tool-slots"); + mockState.resources = buildResourceIndex([fakeTool()]); + expect(getTargets()).toContain(".tool-slots"); + }); + + it("has correct content based on board version", () => { + const getTitles = () => + Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.title); + mockState.resources = buildResourceIndex([]); + expect(getTitles()).toContain("Add tools and slots"); + expect(getTitles()).not.toContain("Add seed containers"); + const fbosConfig = fakeFbosConfig(); + fbosConfig.body.firmware_hardware = "express_k10"; + mockState.resources = buildResourceIndex([fbosConfig]); + expect(getTitles()).toContain("Add seed containers and slots"); + expect(getTitles()).not.toContain("Add seed containers"); + mockState.resources = buildResourceIndex([fbosConfig, fakeTool()]); + expect(getTitles()).not.toContain("Add seed containers and slots"); + expect(getTitles()).toContain("Add seed containers"); + }); }); diff --git a/frontend/help/docs.tsx b/frontend/help/docs.tsx index 8ad5a5c2d..1f1155734 100644 --- a/frontend/help/docs.tsx +++ b/frontend/help/docs.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { - Widget, WidgetBody, WidgetHeader, docLink, DOC_SLUGS, DocSlug + Widget, WidgetBody, WidgetHeader, docLink, DOC_SLUGS, DocSlug, } from "../ui"; import { t } from "../i18next_wrapper"; @@ -18,11 +18,13 @@ export const DocsWidget = () => -
    +
    -
    {documentationLink("the-farmbot-web-app", "Web App")}
    +
    + {documentationLink("the-farmbot-web-app", "Web App")} +
    -
    +
    {Object.entries(DOC_SLUGS).map(documentationLinkMapper)}
    diff --git a/frontend/help/tour.tsx b/frontend/help/tour.tsx index ece8f5064..0973e5d56 100644 --- a/frontend/help/tour.tsx +++ b/frontend/help/tour.tsx @@ -6,6 +6,7 @@ import { TOUR_STEPS, tourPageNavigation } from "./tours"; import { t } from "../i18next_wrapper"; import { Actions } from "../constants"; import { store } from "../redux/store"; +import { ErrorBoundary } from "../error_boundary"; const strings = () => ({ back: t("Back"), @@ -31,7 +32,7 @@ interface TourState { } export class Tour extends React.Component { - state: TourState = { run: false, index: 0, returnPath: "", }; + state: TourState = { run: false, index: 0, returnPath: "" }; callback = ({ action, index, step, type }: CallBackProps) => { console.log("Tour debug:", step.target, type, action); @@ -65,19 +66,23 @@ export class Tour extends React.Component { return step; }); return
    - + + +
    ; } } export const RunTour = ({ currentTour }: { currentTour: string | undefined }) => { - return currentTour ? :
    ; + return currentTour + ? + :
    ; }; diff --git a/frontend/help/tour_list.tsx b/frontend/help/tour_list.tsx index 5335cea1c..345992405 100644 --- a/frontend/help/tour_list.tsx +++ b/frontend/help/tour_list.tsx @@ -9,6 +9,7 @@ export const TourList = ({ dispatch }: { dispatch: Function }) => {tourNames().map(tour =>
    - @@ -61,6 +65,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => { > {
    + + +
    +
    + + + this.setState({ searchTerm: e.currentTarget.value })} + placeholder={t("Search logs...")} /> +
    +
    + +
    ; export interface LogsState extends Filters { autoscroll: boolean; + searchTerm: string; markdown: boolean; } diff --git a/frontend/logs/state_to_props.ts b/frontend/logs/state_to_props.ts index a8d46a8cb..69cdbcf53 100644 --- a/frontend/logs/state_to_props.ts +++ b/frontend/logs/state_to_props.ts @@ -2,7 +2,7 @@ import { Everything } from "../interfaces"; import { selectAllLogs, maybeGetTimeSettings } from "../resources/selectors"; import { LogsProps } from "./interfaces"; import { - sourceFbosConfigValue + sourceFbosConfigValue, } from "../devices/components/source_config_value"; import { validFbosConfig } from "../util"; import { ResourceIndex } from "../resources/interfaces"; diff --git a/frontend/messages/__tests__/alerts_test.tsx b/frontend/messages/__tests__/alerts_test.tsx index 08a61add7..8f85c4f3b 100644 --- a/frontend/messages/__tests__/alerts_test.tsx +++ b/frontend/messages/__tests__/alerts_test.tsx @@ -52,8 +52,7 @@ describe("", () => { const p = fakeProps(); p.alerts = [FIRMWARE_MISSING_ALERT, SEED_DATA_MISSING_ALERT]; const wrapper = mount(); - expect(wrapper.text()).toContain("2"); - expect(wrapper.text()).toContain("Your device has no firmware"); + expect(wrapper.text()).not.toContain("Your device has no firmware"); expect(wrapper.text()).toContain("Choose your FarmBot"); }); @@ -61,7 +60,6 @@ describe("", () => { const p = fakeProps(); p.alerts = [FIRMWARE_MISSING_ALERT, UNKNOWN_ALERT]; const wrapper = mount(); - expect(wrapper.text()).toContain("1"); expect(wrapper.text()).toContain("firmware: alert"); }); }); diff --git a/frontend/messages/__tests__/state_to_props_test.ts b/frontend/messages/__tests__/state_to_props_test.ts index 9c38170cf..03a04f0a4 100644 --- a/frontend/messages/__tests__/state_to_props_test.ts +++ b/frontend/messages/__tests__/state_to_props_test.ts @@ -2,7 +2,7 @@ import { fakeState } from "../../__test_support__/fake_state"; import { mapStateToProps } from "../state_to_props"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { - fakeAlert, fakeFbosConfig + fakeAlert, fakeFbosConfig, } from "../../__test_support__/fake_state/resources"; describe("mapStateToProps()", () => { @@ -18,7 +18,6 @@ describe("mapStateToProps()", () => { it("returns firmware value", () => { const state = fakeState(); const fbosConfig = fakeFbosConfig(); - fbosConfig.body.api_migrated = true; fbosConfig.body.firmware_hardware = "arduino"; state.resources = buildResourceIndex([fbosConfig]); const props = mapStateToProps(state); diff --git a/frontend/messages/alerts.tsx b/frontend/messages/alerts.tsx index 30ed30b09..5a1141819 100644 --- a/frontend/messages/alerts.tsx +++ b/frontend/messages/alerts.tsx @@ -34,6 +34,7 @@ export const Alerts = (props: AlertsProps) =>
    {sortAlerts(props.alerts) .filter(filterIncompleteAlerts) + .filter(x => x.problem_tag != "farmbot_os.firmware.missing") .map(x => { @@ -219,11 +218,11 @@ const SEED_DATA_OPTIONS = (): DropDownItem[] => [ { label: "Genesis v1.2", value: "genesis_1.2" }, { label: "Genesis v1.3", value: "genesis_1.3" }, { label: "Genesis v1.4", value: "genesis_1.4" }, + { label: "Genesis v1.5", value: "genesis_1.5" }, { label: "Genesis v1.4 XL", value: "genesis_xl_1.4" }, - ...(DevSettings.futureFeaturesEnabled() ? [ - { label: "Express v1.0", value: "express_1.0" }, - { label: "Express v1.0 XL", value: "express_xl_1.0" }, - ] : []), + { label: "Genesis v1.5 XL", value: "genesis_xl_1.5" }, + { label: "Express v1.0", value: "express_1.0" }, + { label: "Express v1.0 XL", value: "express_xl_1.0" }, { label: "Custom Bot", value: "none" }, ]; diff --git a/frontend/messages/state_to_props.ts b/frontend/messages/state_to_props.ts index 413e2746e..7632424e0 100644 --- a/frontend/messages/state_to_props.ts +++ b/frontend/messages/state_to_props.ts @@ -4,10 +4,10 @@ import { validFbosConfig, betterCompact } from "../util"; import { getFbosConfig } from "../resources/getters"; import { sourceFbosConfigValue } from "../devices/components/source_config_value"; import { - selectAllAlerts, maybeGetTimeSettings, findResourceById + selectAllAlerts, maybeGetTimeSettings, findResourceById, } from "../resources/selectors"; import { - isFwHardwareValue + isFwHardwareValue, } from "../devices/components/firmware_hardware_support"; import { ResourceIndex, UUID } from "../resources/interfaces"; import { Alert } from "farmbot"; diff --git a/frontend/nav/__tests__/compute_editor_url_from_state_test.ts b/frontend/nav/__tests__/compute_editor_url_from_state_test.ts index 8fe5ba60d..568deffe6 100644 --- a/frontend/nav/__tests__/compute_editor_url_from_state_test.ts +++ b/frontend/nav/__tests__/compute_editor_url_from_state_test.ts @@ -24,7 +24,7 @@ jest.mock("../../redux/store", () => { }); import { - computeEditorUrlFromState, computeFarmwareUrlFromState + computeEditorUrlFromState, computeFarmwareUrlFromState, } from "../compute_editor_url_from_state"; describe("computeEditorUrlFromState", () => { diff --git a/frontend/nav/__tests__/nav_links_test.tsx b/frontend/nav/__tests__/nav_links_test.tsx index 31d7eeb71..81547832a 100644 --- a/frontend/nav/__tests__/nav_links_test.tsx +++ b/frontend/nav/__tests__/nav_links_test.tsx @@ -28,11 +28,16 @@ describe("", () => { }); it("shows links", () => { - mockDev = true; const wrapper = mount(); expect(wrapper.text().toLowerCase()).not.toContain("tools"); }); + it("doesn't show link", () => { + mockDev = true; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("device"); + }); + it("shows active link", () => { mockPath = "/app/designer"; const wrapper = shallow(); diff --git a/frontend/nav/additional_menu.tsx b/frontend/nav/additional_menu.tsx index 83a15f9c8..d5d5ec10d 100644 --- a/frontend/nav/additional_menu.tsx +++ b/frontend/nav/additional_menu.tsx @@ -3,34 +3,35 @@ import { AccountMenuProps } from "./interfaces"; import { Link } from "../link"; import { shortRevision } from "../util"; import { t } from "../i18next_wrapper"; +import { ExternalUrl } from "../external_urls"; export const AdditionalMenu = (props: AccountMenuProps) => { return
    -
    +
    - + {t("Account Settings")}
    -
    +
    - + {t("Logs")}
    - + {t("Help")} -
    - - + diff --git a/frontend/nav/index.tsx b/frontend/nav/index.tsx index ba2ba0a6a..6d6645b27 100644 --- a/frontend/nav/index.tsx +++ b/frontend/nav/index.tsx @@ -35,11 +35,11 @@ export class NavBar extends React.Component> { logout = () => Session.clear(); - toggle = (name: keyof NavBarState) => () => - this.setState({ [name]: !this.state[name] }); + toggle = (key: keyof NavBarState) => () => + this.setState({ [key]: !this.state[key] }); - close = (name: keyof NavBarState) => () => - this.setState({ [name]: false }); + close = (key: keyof NavBarState) => () => + this.setState({ [key]: false }); ReadOnlyStatus = () => > { const { close } = this; const { mobileMenuOpen } = this.state; const { alertCount } = this.props; - return
    + return
    @@ -130,7 +130,7 @@ export class NavBar extends React.Component> {