From c0fab4a38cb1b34a7978a9858d0e69f686aa2002 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Mon, 10 Feb 2020 08:34:05 -0600 Subject: [PATCH 01/26] fill `nil` criteria attributes with PointGroup::DEFAULT_CRITERIA --- app/serializers/point_group_serializer.rb | 4 ++++ spec/controllers/api/point_groups/show_spec.rb | 8 ++++++++ 2 files changed, 12 insertions(+) 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/spec/controllers/api/point_groups/show_spec.rb b/spec/controllers/api/point_groups/show_spec.rb index b6f83adef..1b6eeadc7 100644 --- a/spec/controllers/api/point_groups/show_spec.rb +++ b/spec/controllers/api/point_groups/show_spec.rb @@ -17,4 +17,12 @@ describe Api::PointGroupsController do expect(response.status).to eq(200) expect(json.fetch(:name)).to eq pg.name end + + it "fills `nil` criteria with PointGroup::DEFAULT_CRITERIA" do + pg = PointGroup.create!(name: "x", device: device, criteria: nil ) + sign_in user + get :show, params: { id: pg.id } + expect(response.status).to eq(200) + expect(json.fetch(:criteria)).to eq PointGroup::DEFAULT_CRITERIA + end end From de140d02a5a0a95b5a16cea619d2cd4b70494a26 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Mon, 10 Feb 2020 09:05:42 -0600 Subject: [PATCH 02/26] Add pagination to numerous API endpoints --- Gemfile | 2 +- Gemfile.lock | 13 +++++++++++++ app/controllers/api/abstract_controller.rb | 10 ++++++++++ app/controllers/api/alerts_controller.rb | 2 +- app/controllers/api/farm_events_controller.rb | 2 +- app/controllers/api/farmware_envs_controller.rb | 2 +- .../api/farmware_installations_controller.rb | 2 +- app/controllers/api/peripherals_controller.rb | 2 +- app/controllers/api/pin_bindings_controller.rb | 2 +- app/controllers/api/plant_templates_controller.rb | 2 +- app/controllers/api/point_groups_controller.rb | 2 +- app/controllers/api/points_controller.rb | 2 +- app/controllers/api/regimens_controller.rb | 2 +- app/controllers/api/saved_gardens_controller.rb | 2 +- app/controllers/api/sensor_readings_controller.rb | 2 +- app/controllers/api/sensors_controller.rb | 2 +- app/controllers/api/tools_controller.rb | 2 +- app/controllers/api/webcam_feeds_controller.rb | 2 +- .../api/sensor_readings/controller_spec.rb | 8 ++++++++ 19 files changed, 47 insertions(+), 16 deletions(-) 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..f0f340215 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,6 +153,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) @@ -320,6 +332,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..a0e945873 100644 --- a/app/controllers/api/sensor_readings_controller.rb +++ b/app/controllers/api/sensor_readings_controller.rb @@ -5,7 +5,7 @@ module Api end def index - render json: readings + maybe_paginate(readings) end def show 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/spec/controllers/api/sensor_readings/controller_spec.rb b/spec/controllers/api/sensor_readings/controller_spec.rb index 44af35a01..19a1e8612 100644 --- a/spec/controllers/api/sensor_readings/controller_spec.rb +++ b/spec/controllers/api/sensor_readings/controller_spec.rb @@ -82,6 +82,14 @@ describe Api::SensorReadingsController do expect(keys).to include(:x, :y, :z, :value, :pin) end + it "paginates sensor readings" do + sign_in user + SensorReading.destroy_all + FactoryBot.create_list(:sensor_reading, 30, device: user.device) + get :index, params: { format: :json, page: 2, per: 5 } + expect(json.length).to eq(5) + end + it "destroys a reading" do sign_in user SensorReading.destroy_all From 10a025369d4e84ecd549a6ee658381dbe80990e4 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Wed, 12 Feb 2020 18:35:02 -0600 Subject: [PATCH 03/26] Verbiage updates --- app/models/device.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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) From 2a6fe06ba3cffe7a018508145832052e2cefdb79 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Thu, 13 Feb 2020 13:10:48 -0600 Subject: [PATCH 04/26] Cap sensor readings to 5,000 count --- .../api/sensor_readings_controller.rb | 22 ++++++++++++++++--- .../api/sensor_readings/controller_spec.rb | 19 ++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/sensor_readings_controller.rb b/app/controllers/api/sensor_readings_controller.rb index a0e945873..65feb4c28 100644 --- a/app/controllers/api/sensor_readings_controller.rb +++ b/app/controllers/api/sensor_readings_controller.rb @@ -1,7 +1,10 @@ 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 @@ -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/spec/controllers/api/sensor_readings/controller_spec.rb b/spec/controllers/api/sensor_readings/controller_spec.rb index 19a1e8612..9f0aea4fb 100644 --- a/spec/controllers/api/sensor_readings/controller_spec.rb +++ b/spec/controllers/api/sensor_readings/controller_spec.rb @@ -18,7 +18,7 @@ describe Api::SensorReadingsController do x: nil, y: 1, z: 2, - mode: 1 + mode: 1, }.to_json, params: { format: :json } @@ -40,7 +40,7 @@ describe Api::SensorReadingsController do y: 1, z: 2, mode: 1, - read_at: read_at + read_at: read_at, }.to_json, params: { format: :json } @@ -105,5 +105,20 @@ describe Api::SensorReadingsController do post :create, body: {}.to_json expect(response.status).to eq(401) end + + it "cleans up excess logs" do + const_reassign(Api::SensorReadingsController, :LIMIT, 5) + sign_in user + 10.times do |n| + FactoryBot.create(:sensor_reading, + device: user.device, + created_at: n.minutes.ago) + end + expect(user.device.sensor_readings.count).to eq(10) + get :index, params: { format: :json } + expect(json.count).to eq(5) + expect(user.device.sensor_readings.count).to eq(5) + const_reassign(Api::SensorReadingsController, :LIMIT, 5000) + end end end From c1a9bc63bf0b97ed4228a3ebe6937ce41056b3d8 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Thu, 13 Feb 2020 13:15:37 -0600 Subject: [PATCH 05/26] SensorReadingController test improvement --- spec/controllers/api/sensor_readings/controller_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/controllers/api/sensor_readings/controller_spec.rb b/spec/controllers/api/sensor_readings/controller_spec.rb index 9f0aea4fb..7f544917c 100644 --- a/spec/controllers/api/sensor_readings/controller_spec.rb +++ b/spec/controllers/api/sensor_readings/controller_spec.rb @@ -119,6 +119,9 @@ describe Api::SensorReadingsController do expect(json.count).to eq(5) expect(user.device.sensor_readings.count).to eq(5) const_reassign(Api::SensorReadingsController, :LIMIT, 5000) + first = (json.first[:created_at]) + last = (json.last[:created_at]) + expect(first).to be > last end end end From aafd84fff7ebc0abc9efad3c7e00c3a7a4feb55f Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 13 Feb 2020 16:53:20 -0800 Subject: [PATCH 06/26] support new models --- app/mutations/devices/create_seed_data.rb | 2 + .../devices/seeders/genesis_one_five.rb | 11 +++ .../devices/seeders/genesis_xl_one_five.rb | 23 +++++ .../__tests__/fbos_details_test.tsx | 20 ++++ .../components/fbos_settings/fbos_details.tsx | 4 +- frontend/messages/cards.tsx | 9 +- .../devices/devices_controller_seed_spec.rb | 96 +++++++++++++++++++ 7 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 app/mutations/devices/seeders/genesis_one_five.rb create mode 100644 app/mutations/devices/seeders/genesis_xl_one_five.rb 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/genesis_one_five.rb b/app/mutations/devices/seeders/genesis_one_five.rb new file mode 100644 index 000000000..dde23e30d --- /dev/null +++ b/app/mutations/devices/seeders/genesis_one_five.rb @@ -0,0 +1,11 @@ +module Devices + module Seeders + class GenesisOneFive < AbstractGenesis + def settings_firmware + device + .fbos_config + .update!(firmware_hardware: FbosConfig::FARMDUINO_K15) + 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..b3e291a1f --- /dev/null +++ b/app/mutations/devices/seeders/genesis_xl_one_five.rb @@ -0,0 +1,23 @@ +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 + end + end +end 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..0c4cdb602 100644 --- a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx @@ -85,6 +85,26 @@ 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; + 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"; diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index 9070b57cf..43ad6a0c7 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -260,6 +260,8 @@ export function FbosDetails(props: FbosDetailsProps) { wifi_level_percent, cpu_usage, private_ip, } = props.botInfoSettings; const { last_ota, last_ota_checkup } = props.deviceAccount.body; + const firmwareCommit = [firmware_commit, firmware_version].includes("---") + ? firmware_commit : firmware_version?.split("-")[1] || firmware_commit; return
{t("Local IP address")}: {private_ip}

}

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

+ repo={"farmbot-arduino-firmware"} commit={firmwareCommit} />

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

{isNumber(uptime) && } {isNumber(memory_usage) && diff --git a/frontend/messages/cards.tsx b/frontend/messages/cards.tsx index 0dcf9cce9..8b03e50e5 100644 --- a/frontend/messages/cards.tsx +++ b/frontend/messages/cards.tsx @@ -23,7 +23,6 @@ import { import { updateConfig } from "../devices/actions"; import { fetchBulletinContent, seedAccount } from "./actions"; import { startCase } from "lodash"; -import { DevSettings } from "../account/dev/dev_support"; import { Session } from "../session"; export const AlertCard = (props: AlertCardProps) => { @@ -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/spec/controllers/api/devices/devices_controller_seed_spec.rb b/spec/controllers/api/devices/devices_controller_seed_spec.rb index ade9cf50d..259e07667 100644 --- a/spec/controllers/api/devices/devices_controller_seed_spec.rb +++ b/spec/controllers/api/devices/devices_controller_seed_spec.rb @@ -352,6 +352,50 @@ describe Api::DevicesController do expect(settings_default_map_size_y?(device)).to eq(1400) end + it "seeds accounts with Genesis 1.5 data" do + start_tests "genesis_1.5" + + expect(peripherals_lighting?(device).pin).to eq(7) + expect(peripherals_peripheral_4?(device).pin).to eq(10) + expect(peripherals_peripheral_5?(device).pin).to eq(12) + expect(peripherals_vacuum?(device).pin).to be(9) + expect(peripherals_water?(device).pin).to be(8) + expect(pin_bindings_button_1?(device).special_action).to eq("emergency_lock") + expect(pin_bindings_button_2?(device).special_action).to eq("emergency_unlock") + expect(plants?(device)).to be true + expect(sensors_soil_sensor?(device).pin).to eq(59) + expect(sensors_tool_verification?(device).pin).to eq(63) + expect(settings_device_name?(device)).to eq("FarmBot Genesis") + expect(settings_enable_encoders?(device)).to be(true) + expect(settings_firmware?(device)).to eq("farmduino_k15") + expect(settings_hide_sensors?(device)).to be(false) + expect(tool_slots_slot_1?(device).name).to eq("Seeder") + expect(tool_slots_slot_2?(device).name).to eq("Seed Bin") + expect(tool_slots_slot_3?(device).name).to eq("Seed Tray") + expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") + expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") + expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tools_seed_bin?(device)).to be + expect(tools_seed_tray?(device)).to be + expect(tools_seed_trough_1?(device)).to_not be + expect(tools_seed_trough_2?(device)).to_not be + expect(tools_seed_trough_3?(device)).to_not be + expect(tools_seeder?(device)).to be_kind_of(Tool) + expect(tools_soil_sensor?(device)).to be_kind_of(Tool) + expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) + expect(tools_weeder?(device)).to be_kind_of(Tool) + expect(sequences_mount_tool?(device)).to be + expect(sequences_pickup_seed_genesis?(device)).to be + expect(sequences_pickup_seed_express?(device)).to_not be + expect(sequences_plant_seed?(device)).to be_kind_of(Sequence) + expect(sequences_take_photo_of_plant?(device)).to be_kind_of(Sequence) + expect(sequences_tool_error?(device)).to be_kind_of(Sequence) + expect(sequences_unmount_tool?(device)).to be_kind_of(Sequence) + expect(sequences_water_plant?(device)).to be_kind_of(Sequence) + expect(settings_default_map_size_x?(device)).to eq(2900) + expect(settings_default_map_size_y?(device)).to eq(1400) + end + it "seeds accounts with Genesis XL 1.4 data" do start_tests "genesis_xl_1.4" @@ -404,6 +448,58 @@ describe Api::DevicesController do expect(settings_default_map_size_y?(device)).to eq(2900) end + it "seeds accounts with Genesis XL 1.5 data" do + start_tests "genesis_xl_1.5" + + expect(peripherals_lighting?(device).pin).to eq(7) + expect(peripherals_peripheral_4?(device).pin).to eq(10) + expect(peripherals_peripheral_5?(device).pin).to eq(12) + expect(peripherals_vacuum?(device).pin).to be(9) + expect(peripherals_water?(device).pin).to be(8) + expect(pin_bindings_button_1?(device).special_action).to eq("emergency_lock") + expect(pin_bindings_button_2?(device).special_action).to eq("emergency_unlock") + expect(plants?(device)).to be true + expect(sensors_soil_sensor?(device).pin).to eq(59) + expect(sensors_tool_verification?(device).pin).to eq(63) + expect(settings_device_name?(device)).to eq("FarmBot Genesis XL") + expect(settings_enable_encoders?(device)).to be(true) + expect(settings_firmware?(device)).to eq("farmduino_k15") + expect(settings_hide_sensors?(device)).to be(false) + expect(tool_slots_slot_1?(device).name).to eq("Seeder") + expect(tool_slots_slot_2?(device).name).to eq("Seed Bin") + expect(tool_slots_slot_3?(device).name).to eq("Seed Tray") + expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") + expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") + expect(tool_slots_slot_6?(device).name).to eq("Weeder") + + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") + check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") + check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") + check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") + check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") + check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + + expect(tools_seed_bin?(device)).to be + expect(tools_seed_tray?(device)).to be + expect(tools_seed_trough_1?(device)).to_not be + expect(tools_seed_trough_2?(device)).to_not be + expect(tools_seed_trough_3?(device)).to_not be + expect(tools_seeder?(device)).to be_kind_of(Tool) + expect(tools_soil_sensor?(device)).to be_kind_of(Tool) + expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) + expect(tools_weeder?(device)).to be_kind_of(Tool) + expect(sequences_mount_tool?(device)).to be + expect(sequences_pickup_seed_genesis?(device)).to be + expect(sequences_pickup_seed_express?(device)).to_not be + expect(sequences_plant_seed?(device)).to be_kind_of(Sequence) + expect(sequences_take_photo_of_plant?(device)).to be_kind_of(Sequence) + expect(sequences_tool_error?(device)).to be_kind_of(Sequence) + expect(sequences_unmount_tool?(device)).to be_kind_of(Sequence) + expect(sequences_water_plant?(device)).to be_kind_of(Sequence) + expect(settings_default_map_size_x?(device)).to eq(5900) + expect(settings_default_map_size_y?(device)).to eq(2900) + end + it "seeds accounts with Express 1.0 data" do start_tests "express_1.0" From 9dab0c4bc531600fe0e87ad108b0d885ed8779f3 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Sat, 15 Feb 2020 10:29:09 -0800 Subject: [PATCH 07/26] hardware settings ui updates --- .../__test_support__/control_panel_state.ts | 5 +- frontend/__test_support__/fake_state/bot.ts | 15 ++-- frontend/constants.ts | 86 ++++++++++++------- frontend/css/global.scss | 22 ++++- frontend/devices/__tests__/actions_test.ts | 2 +- .../components/farmbot_os_settings.tsx | 1 - .../__tests__/power_and_reset_test.tsx | 30 +++---- .../components/fbos_settings/interfaces.ts | 1 - .../fbos_settings/power_and_reset.tsx | 30 +++---- .../devices/components/hardware_settings.tsx | 35 ++++---- .../__tests__/calibration_row_test.tsx | 44 +++++++--- .../__tests__/encoders_and_endstops_test.tsx | 47 ---------- .../__tests__/encoders_test.tsx | 32 +++++++ .../__tests__/endstops_test.tsx | 21 +++++ .../__tests__/error_handling_tests.tsx | 47 ++++++++++ .../__tests__/homing_and_calibration_test.tsx | 71 ++++++++++++--- .../__tests__/homing_row_test.tsx | 33 ------- .../__tests__/motors_test.tsx | 21 +---- .../__tests__/pin_bindings_test.tsx | 22 +++++ .../__tests__/zero_row_test.tsx | 19 ---- .../hardware_settings/calibration_row.tsx | 22 ++--- ...encoders_and_endstops.tsx => encoders.tsx} | 84 +++++++----------- .../components/hardware_settings/endstops.tsx | 57 ++++++++++++ .../hardware_settings/error_handling.tsx | 53 ++++++++++++ .../homing_and_calibration.tsx | 57 ++++++++---- .../hardware_settings/homing_row.tsx | 41 --------- .../components/hardware_settings/motors.tsx | 58 +------------ .../hardware_settings/pin_bindings.tsx | 22 +++++ .../hardware_settings/single_setting_row.tsx | 20 +++++ .../components/hardware_settings/zero_row.tsx | 40 --------- frontend/devices/components/interfaces.ts | 33 +++++-- .../components/pin_number_dropdown.tsx | 6 +- frontend/devices/devices.tsx | 4 - frontend/devices/interfaces.ts | 5 +- .../__tests__/list_and_label_support_test.tsx | 11 ++- .../__tests__/pin_bindings_test.tsx | 12 +-- frontend/devices/pin_bindings/interfaces.ts | 2 +- .../pin_bindings/list_and_label_support.tsx | 5 ++ .../pin_bindings/pin_binding_input_group.tsx | 20 ++--- .../devices/pin_bindings/pin_bindings.tsx | 34 +++----- .../pin_bindings/pin_bindings_list.tsx | 10 +-- frontend/devices/reducer.ts | 10 ++- .../sequences/step_tiles/tile_calibrate.tsx | 2 +- 43 files changed, 680 insertions(+), 512 deletions(-) delete mode 100644 frontend/devices/components/hardware_settings/__tests__/encoders_and_endstops_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/encoders_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/endstops_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/error_handling_tests.tsx delete mode 100644 frontend/devices/components/hardware_settings/__tests__/homing_row_test.tsx create mode 100644 frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx delete mode 100644 frontend/devices/components/hardware_settings/__tests__/zero_row_test.tsx rename frontend/devices/components/hardware_settings/{encoders_and_endstops.tsx => encoders.tsx} (55%) create mode 100644 frontend/devices/components/hardware_settings/endstops.tsx create mode 100644 frontend/devices/components/hardware_settings/error_handling.tsx delete mode 100644 frontend/devices/components/hardware_settings/homing_row.tsx create mode 100644 frontend/devices/components/hardware_settings/pin_bindings.tsx create mode 100644 frontend/devices/components/hardware_settings/single_setting_row.tsx delete mode 100644 frontend/devices/components/hardware_settings/zero_row.tsx diff --git a/frontend/__test_support__/control_panel_state.ts b/frontend/__test_support__/control_panel_state.ts index 13eddf61a..30fdacdeb 100644 --- a/frontend/__test_support__/control_panel_state.ts +++ b/frontend/__test_support__/control_panel_state.ts @@ -4,7 +4,10 @@ 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 diff --git a/frontend/__test_support__/fake_state/bot.ts b/frontend/__test_support__/fake_state/bot.ts index ec6c82df5..98151048c 100644 --- a/frontend/__test_support__/fake_state/bot.ts +++ b/frontend/__test_support__/fake_state/bot.ts @@ -4,12 +4,15 @@ 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, + homing_and_calibration: false, + motors: false, + encoders: false, + endstops: false, + error_handling: false, + pin_bindings: false, + danger_zone: false, + power_and_reset: false, + pin_guard: false, }, "hardware": { "gpio_registry": {}, diff --git a/frontend/constants.ts b/frontend/constants.ts index 71388071f..7dd95c219 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 @@ -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.`); diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 8e320263b..59274676d 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -433,14 +433,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 +452,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 { diff --git a/frontend/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts index ad82f57be..2d0e5a1cd 100644 --- a/frontend/devices/__tests__/actions_test.ts +++ b/frontend/devices/__tests__/actions_test.ts @@ -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"); diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index 9fceb9231..9dac417d3 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -161,7 +161,6 @@ export class FarmbotOsSettings controlPanelState={this.props.bot.controlPanelState} dispatch={this.props.dispatch} sourceFbosConfig={sourceFbosConfig} - shouldDisplay={this.props.shouldDisplay} botOnline={botOnline} /> 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..147e2be34 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 @@ -25,30 +25,25 @@ describe("", () => { 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", "Restart Firmware", + "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())); }); - it("closed", () => { + it("renders as closed", () => { const p = fakeProps(); p.controlPanelState.power_and_reset = false; const wrapper = mount(); @@ -73,7 +68,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 +76,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/interfaces.ts b/frontend/devices/components/fbos_settings/interfaces.ts index b3b5047e9..ad5714335 100644 --- a/frontend/devices/components/fbos_settings/interfaces.ts +++ b/frontend/devices/components/fbos_settings/interfaces.ts @@ -53,7 +53,6 @@ export interface PowerAndResetProps { controlPanelState: ControlPanelState; dispatch: Function; sourceFbosConfig: SourceFbosConfig; - shouldDisplay: ShouldDisplay; botOnline: boolean; } diff --git a/frontend/devices/components/fbos_settings/power_and_reset.tsx b/frontend/devices/components/fbos_settings/power_and_reset.tsx index f67061e12..e6ac2d437 100644 --- a/frontend/devices/components/fbos_settings/power_and_reset.tsx +++ b/frontend/devices/components/fbos_settings/power_and_reset.tsx @@ -4,23 +4,20 @@ import { Collapse, Popover, Position } from "@blueprintjs/core"; import { FactoryResetRow } from "./factory_reset_row"; import { PowerAndResetProps } from "./interfaces"; import { ChangeOwnershipForm } from "./change_ownership_form"; -import { Feature } from "../../interfaces"; import { FbosButtonRow } from "./fbos_button_row"; import { Content } from "../../../constants"; import { reboot, powerOff, restartFirmware } from "../../actions"; import { t } from "../../../i18next_wrapper"; export function PowerAndReset(props: PowerAndResetProps) { - const { dispatch, sourceFbosConfig, shouldDisplay, botOnline } = props; + const { dispatch, sourceFbosConfig, botOnline } = props; const { power_and_reset } = props.controlPanelState; return
-
-
-
+
- {shouldDisplay(Feature.firmware_restart) && - } + { @@ -27,6 +30,7 @@ export class HardwareSettings extends const { informational_settings } = this.props.bot.hardware; const { sync_status } = informational_settings; const botDisconnected = !isBotOnline(sync_status, botToMqttStatus); + const commonProps = { dispatch, controlPanelState }; 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..f1475c44a 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,40 @@ -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"; + +describe("", () => { + const fakeProps = (): CalibrationRowProps => ({ + type: "calibrate", + hardware: bot.hardware.mcu_params, + botDisconnected: false, + action: jest.fn(), + toolTip: "calibrate", + title: "calibrate", + 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__/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..aa18b27f2 --- /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__/homing_and_calibration_test.tsx b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx index 6fd42b781..53425a84b 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,7 +1,17 @@ -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"; @@ -10,20 +20,28 @@ import { } 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..7b8032843 100644 --- a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx @@ -37,8 +37,6 @@ describe("", () => { it("renders the base case", () => { const wrapper = render(); ["Enable 2nd X Motor", - "Max Retries", - "E-Stop on Movement Error", "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..63b63d18d --- /dev/null +++ b/frontend/devices/components/hardware_settings/__tests__/pin_bindings_test.tsx @@ -0,0 +1,22 @@ +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, + }); + + 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..ab986afec 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -1,19 +1,11 @@ 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")); - export function CalibrationRow(props: CalibrationRowProps) { const { hardware, botDisconnected } = props; @@ -21,18 +13,20 @@ export function CalibrationRow(props: CalibrationRowProps) { return - + {axisTrackingStatus(hardware) .map(row => { - const { axis, disabled } = row; + const { axis } = row; + const hardwareDisabled = props.type == "zero" ? false : row.disabled; return calibrate(axis)}> - {t("CALIBRATE {{axis}}", { axis })} + disabled={hardwareDisabled || botDisconnected} + onClick={() => props.action(axis)}> + {`${t(props.axisTitle)} ${axis}`} ; })} diff --git a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx b/frontend/devices/components/hardware_settings/encoders.tsx similarity index 55% rename from frontend/devices/components/hardware_settings/encoders_and_endstops.tsx rename to frontend/devices/components/hardware_settings/encoders.tsx index 72bdce981..7addae3ae 100644 --- a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx +++ b/frontend/devices/components/hardware_settings/encoders.tsx @@ -5,41 +5,53 @@ 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) { +export function Encoders(props: EncodersProps) { - const { encoders_and_endstops } = props.controlPanelState; - const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props; + 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 isExpress = isExpressBoard(firmwareHardware); return
- + - {!isExpressBoard(firmwareHardware) && + {isExpress && + } + {!isExpress && } - {!isExpressBoard(firmwareHardware) && + {!isExpress && } - {!isExpressBoard(firmwareHardware) && + {!isExpress && } - - -
; } diff --git a/frontend/devices/components/hardware_settings/endstops.tsx b/frontend/devices/components/hardware_settings/endstops.tsx new file mode 100644 index 000000000..ffa1b5f3d --- /dev/null +++ b/frontend/devices/components/hardware_settings/endstops.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; +import { ToolTips } from "../../../constants"; +import { EndStopsProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { t } from "../../../i18next_wrapper"; + +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..e8d4142f6 --- /dev/null +++ b/frontend/devices/components/hardware_settings/error_handling.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; +import { ToolTips } from "../../../constants"; +import { ErrorHandlingProps } from "../interfaces"; +import { Header } from "./header"; +import { Collapse } from "@blueprintjs/core"; +import { t } from "../../../i18next_wrapper"; +import { McuInputBox } from "../mcu_input_box"; +import { settingToggle } from "../../actions"; +import { SingleSettingRow } from "./single_setting_row"; +import { ToggleButton } from "../../../controls/toggle_button"; + +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/homing_and_calibration.tsx b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx index 7775539a8..1602fa351 100644 --- a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx +++ b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx @@ -2,19 +2,23 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { ToolTips } 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 { isExpressBoard } from "../firmware_hardware_support"; +import { getDevice } from "../../../device"; +import { commandErr } from "../../actions"; +import { CONFIG_DEFAULTS } from "farmbot/dist/config"; 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; @@ -34,12 +38,43 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { dispatch={dispatch} expanded={homing_and_calibration} /> - - - + 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..b6281e751 100644 --- a/frontend/devices/components/hardware_settings/motors.tsx +++ b/frontend/devices/components/hardware_settings/motors.tsx @@ -5,32 +5,14 @@ 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 { Collapse } from "@blueprintjs/core"; import { t } from "../../../i18next_wrapper"; 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"; export const calculateScale = (sourceFwConfig: SourceFwConfig): Record => { @@ -51,13 +33,8 @@ 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
- - - - - dispatch( - settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} /> - } - {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..e7e716c0c --- /dev/null +++ b/frontend/devices/components/hardware_settings/pin_bindings.tsx @@ -0,0 +1,22 @@ +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"; + +export function PinBindings(props: PinBindingsProps) { + + const { pin_bindings } = props.controlPanelState; + const { dispatch, resources } = props; + + return
+
+ + + +
; +} 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..02075c813 --- /dev/null +++ b/frontend/devices/components/hardware_settings/single_setting_row.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { Row, Col, Help } from "../../../ui/index"; +import { Position } from "@blueprintjs/core"; + +export const SingleSettingRow = + ({ label, tooltip, settingType, children }: { + label: string, + tooltip: string, + children: React.ReactChild, + settingType: "button" | "input", + }) => + + + + + + {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..a76dd30ec 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -1,17 +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; -} - export interface ZeroRowProps { botDisconnected: boolean; } @@ -19,9 +14,11 @@ 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 { @@ -39,8 +36,13 @@ export interface BooleanMCUInputGroupProps { } export interface CalibrationRowProps { + type: "find_home" | "calibrate" | "zero"; hardware: McuParams; botDisconnected: boolean; + action(axis: Axis): void; + toolTip: string; + title: string; + axisTitle: string; } export interface NumericMCUInputGroupProps { @@ -85,12 +87,29 @@ 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; +} + export interface DangerZoneProps { dispatch: Function; controlPanelState: ControlPanelState; diff --git a/frontend/devices/components/pin_number_dropdown.tsx b/frontend/devices/components/pin_number_dropdown.tsx index a706c1f36..7cb4a454e 100644 --- a/frontend/devices/components/pin_number_dropdown.tsx +++ b/frontend/devices/components/pin_number_dropdown.tsx @@ -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/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..8963b9f86 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -245,7 +245,10 @@ export interface HardwareSettingsProps { export interface ControlPanelState { homing_and_calibration: boolean; motors: boolean; - encoders_and_endstops: boolean; + encoders: boolean; + endstops: boolean; + error_handling: boolean; + pin_bindings: boolean; danger_zone: boolean; power_and_reset: boolean; pin_guard: boolean; diff --git a/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx b/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx index 542871140..6742019cf 100644 --- a/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx @@ -1,4 +1,5 @@ -import { sortByNameAndPin, ButtonPin } from "../list_and_label_support"; +import { sortByNameAndPin, ButtonPin, getSpecialActionLabel } from "../list_and_label_support"; +import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources"; describe("sortByNameAndPin()", () => { @@ -26,3 +27,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_bindings_test.tsx b/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx index 4d0ac230c..70dcc2781 100644 --- a/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/pin_bindings_test.tsx @@ -1,5 +1,5 @@ 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 { @@ -8,15 +8,15 @@ import { import { fakeSequence, fakePinBinding } from "../../../__test_support__/fake_state/resources"; -import { PinBindingsProps } from "../interfaces"; +import { PinBindingsContentProps } from "../interfaces"; import { SpecialPinBinding, PinBindingType, 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"; @@ -51,8 +51,8 @@ describe("", () => { 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/interfaces.ts b/frontend/devices/pin_bindings/interfaces.ts index 1370327f2..9f8c195e4 100644 --- a/frontend/devices/pin_bindings/interfaces.ts +++ b/frontend/devices/pin_bindings/interfaces.ts @@ -4,7 +4,7 @@ import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources"; -export interface PinBindingsProps { +export interface PinBindingsContentProps { dispatch: Function; resources: ResourceIndex; } diff --git a/frontend/devices/pin_bindings/list_and_label_support.tsx b/frontend/devices/pin_bindings/list_and_label_support.tsx index 3e6e503e3..09ea49a71 100644 --- a/frontend/devices/pin_bindings/list_and_label_support.tsx +++ b/frontend/devices/pin_bindings/list_and_label_support.tsx @@ -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, diff --git a/frontend/devices/pin_bindings/pin_binding_input_group.tsx b/frontend/devices/pin_bindings/pin_binding_input_group.tsx index 7eb87c550..684318430 100644 --- a/frontend/devices/pin_bindings/pin_binding_input_group.tsx +++ b/frontend/devices/pin_bindings/pin_binding_input_group.tsx @@ -1,5 +1,5 @@ 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"; @@ -13,9 +13,10 @@ 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"; @@ -119,8 +120,6 @@ export class PinBindingInputGroup - - {bindingType == PinBindingType.special ? - + 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..f64cb3d88 100644 --- a/frontend/devices/pin_bindings/pin_bindings.tsx +++ b/frontend/devices/pin_bindings/pin_bindings.tsx @@ -1,8 +1,8 @@ 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 { @@ -20,9 +20,8 @@ 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,29 @@ const PinBindingsListHeader = () => - - - + ; -export const PinBindings = (props: PinBindingsProps) => { +export const PinBindingsContent = (props: PinBindingsContentProps) => { const { dispatch, resources } = 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..431a89388 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))} - {firmwareConfig && - - - - } - -
- -
- - - - - - - - -
+ + + + +
+ +
+ + + + + + + +
; } diff --git a/frontend/devices/components/hardware_settings/export_menu.tsx b/frontend/devices/components/hardware_settings/export_menu.tsx index 67546b166..512e9e2de 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", diff --git a/frontend/devices/reducer.ts b/frontend/devices/reducer.ts index 2da18b512..256bc1911 100644 --- a/frontend/devices/reducer.ts +++ b/frontend/devices/reducer.ts @@ -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"; @@ -205,8 +204,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/farm_designer/map/layers/farmbot/farmbot_layer.tsx b/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx index 087b919b1..96df099ea 100644 --- a/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx +++ b/frontend/farm_designer/map/layers/farmbot/farmbot_layer.tsx @@ -8,7 +8,7 @@ export function FarmBotLayer(props: FarmBotLayerProps) { visible, stopAtHome, botSize, plantAreaOffset, mapTransformProps, peripherals, eStopStatus, botLocationData, getConfigValue } = props; - return visible ? + return visible ? ", () => { const wrapper = svgMount(); expect(wrapper.find("use").props().transform).toEqual(expected); }); + + it("handles bad data", () => { + const p = fakeProps(); + p.pulloutDirection = 1.1; + p.quadrant = 1.1; + const wrapper = svgMount(); + expect(wrapper.find("use").props().transform).toEqual("rotate(0, 10, 20)"); + }); }); describe("", () => { diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts index 130c77f37..41323925d 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts @@ -49,4 +49,8 @@ describe("textAnchorPosition()", () => { expect(textAnchorPosition(4, 3, true)).toEqual(END); expect(textAnchorPosition(4, 4, true)).toEqual(START); }); + + it("handles bad data", () => { + expect(textAnchorPosition(1.1, 1.1, false)).toEqual(START); + }); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx index 4a06683a0..5788588ad 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx @@ -4,6 +4,11 @@ jest.mock("../../../../../history", () => ({ getPathArray: jest.fn(() => { return mockPath.split("/"); }) })); +let mockDev = false; +jest.mock("../../../../../account/dev/dev_support", () => ({ + DevSettings: { futureFeaturesEnabled: () => mockDev } +})); + import * as React from "react"; import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer"; import { @@ -53,6 +58,7 @@ describe("", () => { }); it("navigates to tools page", async () => { + mockDev = true; mockPath = "/app/designer/plants"; const p = fakeProps(); const wrapper = shallow(); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index 1f203c2a1..75451aefc 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -48,10 +48,10 @@ describe("", () => { const p = fakeProps(); p.slot.toolSlot.body.id = 1; const wrapper = svgMount(); - mockDev = false; + mockDev = true; wrapper.find("g").first().simulate("click"); expect(history.push).not.toHaveBeenCalled(); - mockDev = true; + mockDev = false; wrapper.find("g").first().simulate("click"); expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1"); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx index b9a6bb6de..b87288589 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx @@ -180,23 +180,30 @@ export interface GantryToolSlotGraphicProps { xySwap: boolean; } +/** dimensions */ +enum Trough { + width = 20, + length = 45, + wall = 4, +} + export const GantryToolSlot = (props: GantryToolSlotGraphicProps) => { const { x, y, xySwap } = props; - const slotLengthX = xySwap ? 24 : 49; - const slotLengthY = xySwap ? 49 : 24; + const slotLengthX = Trough.wall + (xySwap ? Trough.width : Trough.length); + const slotLengthY = Trough.wall + (xySwap ? Trough.length : Trough.width); return ; }; const SeedTrough = (props: ToolGraphicProps) => { const { x, y, hovered, dispatch, uuid, xySwap } = props; - const slotLengthX = xySwap ? 20 : 45; - const slotLengthY = xySwap ? 45 : 20; + const slotLengthX = xySwap ? Trough.width : Trough.length; + const slotLengthY = xySwap ? Trough.length : Trough.width; return dispatch(setToolHover(uuid))} onMouseLeave={() => dispatch(setToolHover(undefined))}> diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx index 9b12293c8..18d9ff713 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx @@ -39,7 +39,7 @@ export const textAnchorPosition = ( case Anchor.end: return { anchor: "end", x: -40, y: 10 }; case Anchor.middleTop: return { anchor: "middle", x: 0, y: 60 }; case Anchor.middleBottom: return { anchor: "middle", x: 0, y: -40 }; - default: return { anchor: "start", x: 40, y: 10 }; + default: throw new Error("https://xkcd.com/2200"); } }; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx index d222e4dae..d03dcef25 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx @@ -19,7 +19,7 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) { const pathArray = getPathArray(); const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4); const goToToolsPage = () => canClickTool && - !DevSettings.futureFeaturesEnabled() && history.push("/app/tools"); + DevSettings.futureFeaturesEnabled() && history.push("/app/tools"); const { slots, visible, mapTransformProps } = props; const cursor = canClickTool ? "pointer" : "default"; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx index d63554c8c..ddb889b00 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx @@ -43,7 +43,7 @@ export const ToolSlotPoint = (props: TSPProps) => { xySwap, }; return DevSettings.futureFeaturesEnabled() && + onClick={() => !DevSettings.futureFeaturesEnabled() && history.push(`/app/designer/tool-slots/${id}`)}> {pullout_direction && - {DevSettings.futureFeaturesEnabled() && + {!DevSettings.futureFeaturesEnabled() && { function fakeProps() { @@ -39,8 +39,11 @@ describe("PlantGrid", () => { it("saves a grid", async () => { const props = fakeProps(); const pg = mount().instance(); + const oldId = pg.state.gridId; await pg.saveGrid(); - expect(saveGrid).toHaveBeenCalledWith(pg.state.gridId); + expect(saveGrid).toHaveBeenCalledWith(oldId); + expect(success).toHaveBeenCalledWith("16 plants added."); + expect(pg.state.gridId).not.toEqual(oldId); }); it("stashes a grid", async () => { diff --git a/frontend/farm_designer/plants/grid/plant_grid.tsx b/frontend/farm_designer/plants/grid/plant_grid.tsx index 6878640a7..9d83b41f3 100644 --- a/frontend/farm_designer/plants/grid/plant_grid.tsx +++ b/frontend/farm_designer/plants/grid/plant_grid.tsx @@ -9,13 +9,18 @@ import { initPlantGrid } from "./generate_grid"; import { init } from "../../../api/crud"; import { uuid } from "farmbot"; import { saveGrid, stashGrid } from "./thunks"; -import { error } from "../../../toast/toast"; +import { error, success } from "../../../toast/toast"; import { t } from "../../../i18next_wrapper"; import { GridInput } from "./grid_input"; export class PlantGrid extends React.Component { state: PlantGridState = { ...EMPTY_PLANT_GRID, gridId: uuid() }; + get plantCount() { + const { numPlantsH, numPlantsV } = this.state.grid; + return numPlantsH * numPlantsV; + } + onchange = (key: PlantGridKey, val: number) => { const grid = { ...this.state.grid, [key]: val }; this.setState({ grid }); @@ -33,9 +38,7 @@ export class PlantGrid extends React.Component { } performPreview = () => { - const { numPlantsH, numPlantsV } = this.state.grid; - const total = numPlantsH * numPlantsV; - if (total > 100) { + if (this.plantCount > 100) { error(t("Please make a grid with less than 100 plants")); return; } @@ -57,7 +60,10 @@ export class PlantGrid extends React.Component { saveGrid = () => { const p: Promise<{}> = this.props.dispatch(saveGrid(this.state.gridId)); - return p.then(() => this.setState(EMPTY_PLANT_GRID)); + return p.then(() => { + success(t("{{ count }} plants added.", { count: this.plantCount })); + this.setState({ ...EMPTY_PLANT_GRID, gridId: uuid() }); + }); } inputs = () => { @@ -73,16 +79,16 @@ export class PlantGrid extends React.Component { case "clean": return ; case "dirty": return ; } diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx index 19ffd8c27..2d7909c5b 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx @@ -24,6 +24,7 @@ import { import { save, edit } from "../../../api/crud"; import { SpecialStatus } from "farmbot"; import { DEFAULT_CRITERIA } from "../criteria/interfaces"; +import { Content } from "../../../constants"; describe("", () => { const fakeProps = (): GroupDetailActiveProps => { @@ -105,16 +106,23 @@ describe("", () => { }); it("shows paths", () => { - mockDev = true; - const p = fakeProps(); - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("optimized"); - }); - - it("doesn't show paths", () => { mockDev = false; const p = fakeProps(); const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("optimized"); + expect(wrapper.text().toLowerCase()).toContain("0m"); + }); + + it("doesn't show paths", () => { + mockDev = true; + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("0m"); + }); + + it("shows random warning text", () => { + const p = fakeProps(); + p.group.body.sort_type = "random"; + const wrapper = mount(); + expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION); }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx index ffa057ad0..31cecd0ba 100644 --- a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx @@ -1,5 +1,12 @@ jest.mock("../../../api/crud", () => ({ edit: jest.fn() })); +let mockDev = false; +jest.mock("../../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + import * as React from "react"; import { shallow, mount } from "enzyme"; import { @@ -141,6 +148,7 @@ describe("", () => { p.pathPoints = cases.order.xy_ascending; const wrapper = mount(); expect(wrapper.state().pathData).toEqual(cases.distance); + expect(wrapper.text().toLowerCase()).not.toContain("optimized"); }); it.each<[PointGroupSortType]>([ @@ -154,4 +162,24 @@ describe("", () => { expect(SORT_OPTIONS[sortType](cases.order.xy_ascending)) .toEqual(cases.order[sortType]); }); + + it("renders new sort type", () => { + mockDev = true; + const p = fakeProps(); + const cases = pathTestCases(); + p.pathPoints = cases.order.xy_ascending; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("optimized"); + }); + + it("doesn't generate data twice", () => { + const p = fakeProps(); + const cases = pathTestCases(); + p.pathPoints = cases.order.xy_ascending; + const wrapper = mount(); + expect(wrapper.state().pathData).toEqual(cases.distance); + wrapper.setState({ pathData: { nn: 0 } }); + wrapper.update(); + expect(wrapper.state().pathData).toEqual({ nn: 0 }); + }); }); diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx index f87d6cb79..90eba3063 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_item_test.tsx @@ -45,7 +45,6 @@ describe("", () => { const p = fakeProps(); p.point = fakePlant(); const i = new PointGroupItem(p); - i.setState = jest.fn(); const fakeImgEvent = imgEvent(); await i.maybeGetCachedIcon(fakeImgEvent); const slug = i.props.point.body.pointer_type === "Plant" ? @@ -55,11 +54,17 @@ describe("", () => { expect(setImgSrc).not.toHaveBeenCalled(); }); + it("sets icon in state", () => { + const i = new PointGroupItem(fakeProps()); + i.setState = jest.fn(); + i.setIconState("fake icon"); + expect(i.setState).toHaveBeenCalledWith({ icon: "fake icon" }); + }); + it("fetches point icon", () => { const p = fakeProps(); p.point = fakePoint(); const i = new PointGroupItem(p); - i.setState = jest.fn(); const fakeImgEvent = imgEvent(); i.maybeGetCachedIcon(fakeImgEvent); expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled(); @@ -71,7 +76,6 @@ describe("", () => { const p = fakeProps(); p.point = fakeToolSlot(); const i = new PointGroupItem(p); - i.setState = jest.fn(); const fakeImgEvent = imgEvent(); i.maybeGetCachedIcon(fakeImgEvent); expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled(); diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx index 9c761fe55..bfd1f1d60 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx @@ -1,14 +1,10 @@ -import * as React from "react"; import { - isSortType, sortTypeChange, SORT_OPTIONS, PointGroupSortSelector, - PointGroupSortSelectorProps + isSortType, sortTypeChange, SORT_OPTIONS } from "../point_group_sort_selector"; import { DropDownItem } from "../../../ui"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { TaggedPoint } from "farmbot"; import { fakePlant } from "../../../__test_support__/fake_state/resources"; -import { mount } from "enzyme"; -import { Content } from "../../../constants"; const tests: [string, boolean][] = [ ["", false], @@ -89,15 +85,3 @@ describe("sort()", () => { expect(results).toEqual(["C", "D", "B", "A"]); }); }); - -describe("", () => { - const fakeProps = (): PointGroupSortSelectorProps => ({ - onChange: jest.fn(), - value: "random", - }); - - it("shows random warning text", () => { - const wrapper = mount(); - expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION); - }); -}); diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index b7b9251c4..b56ed3e71 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -97,9 +97,24 @@ export class GroupDetailActive defaultValue={group.body.name} onChange={this.update} onBlur={this.saveGroup} /> - +
+ + {!DevSettings.futureFeaturesEnabled() + ? p.body.id))} + pathPoints={this.pointsSelectedByGroup} + dispatch={dispatch} + group={group} /> + : } +

+ {group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)} +

+
@@ -117,11 +132,6 @@ export class GroupDetailActive {this.props.shouldDisplay(Feature.criteria_groups) && } - {DevSettings.futureFeaturesEnabled() && - } ({ x: point.body.x, y: point.body.y }); @@ -66,7 +67,8 @@ export const PathInfoBar = (props: PathInfoBarProps) => { const normalizedLength = pathLength / maxLength * 100; const sortLabel = sortTypeKey == "nn" ? "Optimized" : sortOptionsTable()[sortTypeKey]; - return
dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })} onMouseLeave={() => @@ -74,9 +76,11 @@ export const PathInfoBar = (props: PathInfoBarProps) => { onClick={() => sortTypeKey == "nn" ? error(t("Not supported yet.")) - : dispatch(edit(group, { sort_type: sortTypeKey }))} - style={{ width: `${normalizedLength}%` }}> - {`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`} + : dispatch(edit(group, { sort_type: sortTypeKey }))}> +
+ {`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`} +
; }; @@ -101,15 +105,17 @@ export class Paths extends React.Component { }; render() { - if (!this.state.pathData.nn) { this.generatePathData(this.props.pathPoints); } - return
- - {SORT_TYPES.concat("nn").map(st => - )} + if (!isNumber(this.state.pathData.nn)) { + this.generatePathData(this.props.pathPoints); + } + return
+ {SORT_TYPES.concat(DevSettings.futureFeaturesEnabled() ? "nn" : []) + .map(sortType => + )}
; } } diff --git a/frontend/farm_designer/point_groups/point_group_item.tsx b/frontend/farm_designer/point_groups/point_group_item.tsx index 8d6d7b101..cee43c163 100644 --- a/frontend/farm_designer/point_groups/point_group_item.tsx +++ b/frontend/farm_designer/point_groups/point_group_item.tsx @@ -57,6 +57,8 @@ export class PointGroupItem this.leave(); } + setIconState = (icon: string) => this.setState({ icon }); + get criteriaIcon() { return !this.props.group.body.point_ids .includes(this.props.point.body.id || 0); @@ -67,7 +69,7 @@ export class PointGroupItem switch (this.props.point.body.pointer_type) { case "Plant": const slug = this.props.point.body.openfarm_slug; - maybeGetCachedPlantIcon(slug, img, icon => this.setState({ icon })); + maybeGetCachedPlantIcon(slug, img, this.setIconState); break; case "GenericPointer": const { color } = this.props.point.body.meta; diff --git a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx b/frontend/farm_designer/point_groups/point_group_sort_selector.tsx index 5c1d6a586..e1c335dd9 100644 --- a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx +++ b/frontend/farm_designer/point_groups/point_group_sort_selector.tsx @@ -3,7 +3,6 @@ import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { FBSelect, DropDownItem } from "../../ui"; import { t } from "../../i18next_wrapper"; import { shuffle, sortBy } from "lodash"; -import { Content } from "../../constants"; import { TaggedPoint } from "farmbot"; export interface PointGroupSortSelectorProps { @@ -42,22 +41,11 @@ export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => { }; export function PointGroupSortSelector(p: PointGroupSortSelectorProps) { - - return
-
- -
- -

- {(p.value == "random") ? t(Content.SORT_DESCRIPTION) : ""} -

-
; + return ; } type Sorter = (p: TaggedPoint[]) => TaggedPoint[]; diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx index 3f6e93f7c..e982afc55 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx @@ -11,6 +11,7 @@ import { fakeState } from "../../../__test_support__/fake_state"; import { SaveBtn } from "../../../ui"; import { initSave } from "../../../api/crud"; import { history } from "../../../history"; +import { error } from "../../../toast/toast"; describe("", () => { const fakeProps = (): AddToolProps => ({ @@ -37,10 +38,19 @@ describe("", () => { expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" }); }); - it("adds stock tools", () => { + it("doesn't add stock tools", () => { const wrapper = mount(); wrapper.find("button").last().simulate("click"); - expect(initSave).toHaveBeenCalledTimes(6); + expect(error).toHaveBeenCalledWith("Please choose a FarmBot model."); + expect(initSave).not.toHaveBeenCalledTimes(6); + expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools"); + }); + + it("adds stock tools", () => { + const wrapper = mount(); + wrapper.setState({ model: "express" }); + wrapper.find("button").last().simulate("click"); + expect(initSave).toHaveBeenCalledTimes(2); expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); }); diff --git a/frontend/farm_designer/tools/add_tool.tsx b/frontend/farm_designer/tools/add_tool.tsx index b95508245..2eba033c9 100644 --- a/frontend/farm_designer/tools/add_tool.tsx +++ b/frontend/farm_designer/tools/add_tool.tsx @@ -5,11 +5,20 @@ import { } from "../designer_panel"; import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; -import { SaveBtn } from "../../ui"; +import { SaveBtn, FBSelect, DropDownItem } from "../../ui"; import { SpecialStatus } from "farmbot"; import { initSave } from "../../api/crud"; import { Panel } from "../panel_header"; import { history } from "../../history"; +import { error } from "../../toast/toast"; + +enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" } + +const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ + [Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 }, + [Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 }, + [Model.express]: { label: t("Express"), value: Model.express }, +}); export interface AddToolProps { dispatch: Function; @@ -17,6 +26,7 @@ export interface AddToolProps { export interface AddToolState { toolName: string; + model: Model | undefined; } export const mapStateToProps = (props: Everything): AddToolProps => ({ @@ -24,7 +34,7 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({ }); export class RawAddTool extends React.Component { - state: AddToolState = { toolName: "" }; + state: AddToolState = { toolName: "", model: undefined }; newTool = (name: string) => { this.props.dispatch(initSave("Tool", { name })); @@ -35,28 +45,60 @@ export class RawAddTool extends React.Component { history.push("/app/designer/tools"); } - get stockToolNames() { - return [ - t("Seeder"), - t("Watering Nozzle"), - t("Weeder"), - t("Soil Sensor"), - t("Seed Bin"), - t("Seed Tray"), - ]; + stockToolNames = (model: Model) => { + switch (model) { + case Model.genesis14: + return [ + t("Seeder"), + t("Watering Nozzle"), + t("Weeder"), + t("Soil Sensor"), + t("Seed Bin"), + t("Seed Tray"), + ]; + case Model.genesis15: + return [ + t("Seeder"), + t("Watering Nozzle"), + t("Weeder"), + t("Soil Sensor"), + t("Seed Bin"), + t("Seed Tray"), + t("Seed Trough 1"), + t("Seed Trough 2"), + ]; + case Model.express: + return [ + t("Seed Trough 1"), + t("Seed Trough 2"), + ]; + } } AddStockTools = () =>
-
    - {this.stockToolNames.map(n =>
  • {n}
  • )} -
+ this.setState({ model: ddi.value as Model })} + /> + {this.state.model && +
    + {this.stockToolNames(this.state.model).map(n =>
  • {n}
  • )} +
}
- InactiveTools = () => -
- + Tools = () => +
+
+ + +
+ +
+ +
{this.props.tools .filter(tool => !tool.body.name || tool.body.name && tool.body.name.toLowerCase() .includes(this.state.searchTerm.toLowerCase())) - .filter(tool => tool.body.status === "inactive") .map(tool => { render() { const panelName = "tools"; + const hasTools = this.props.tools.length > 0; return + linkTo={!hasTools ? "/app/designer/tools/add" : undefined} + title={!hasTools ? t("Add tool") : undefined}> 0} + notEmpty={hasTools} graphic={EmptyStateGraphic.tools} title={t("Add a tool")} text={Content.NO_TOOLS} colorScheme={"tools"}> - + ; diff --git a/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx b/frontend/farmware/camera_calibration/__tests__/camera_calibration_test.tsx index 577f65133..afdfe119e 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 => ({ @@ -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..515a7066c 100644 --- a/frontend/farmware/camera_calibration/camera_calibration.tsx +++ b/frontend/farmware/camera_calibration/camera_calibration.tsx @@ -8,7 +8,7 @@ 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"; @@ -16,6 +16,10 @@ import { formatEnvKey } from "../weed_detector/remote_env/translators"; import { 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/weed_detector/__tests__/config_test.tsx b/frontend/farmware/weed_detector/__tests__/config_test.tsx index 19495ebfb..1bd300793 100644 --- a/frontend/farmware/weed_detector/__tests__/config_test.tsx +++ b/frontend/farmware/weed_detector/__tests__/config_test.tsx @@ -31,18 +31,6 @@ describe("", () => { expect(badChange).toThrow("Weed detector got a non-numeric value"); }); - it("changes hue invert value", () => { - const p = fakeProps(); - const wrapper = shallow(); - const input = wrapper.find("input").first(); - input.simulate("change", { currentTarget: { checked: true } }); - expect(p.onChange).toHaveBeenCalledWith( - "CAMERA_CALIBRATION_invert_hue_selection", 1); - input.simulate("change", { currentTarget: { checked: false } }); - expect(p.onChange).toHaveBeenCalledWith( - "CAMERA_CALIBRATION_invert_hue_selection", 0); - }); - it("changes number value", () => { const p = fakeProps(); const wrapper = shallow(); diff --git a/frontend/farmware/weed_detector/config.tsx b/frontend/farmware/weed_detector/config.tsx index 9d775c606..3383c3b32 100644 --- a/frontend/farmware/weed_detector/config.tsx +++ b/frontend/farmware/weed_detector/config.tsx @@ -15,6 +15,9 @@ import { isNumber } from "lodash"; import { t } from "../../i18next_wrapper"; export class WeedDetectorConfig extends React.Component { + getValue(conf: keyof WD_ENV) { return envGet(conf, this.props.values); } + get simple() { return !!this.getValue("CAMERA_CALIBRATION_easy_calibration"); } + NumberBox = ({ conf, label }: { conf: keyof WD_ENV; label: string; @@ -25,7 +28,7 @@ export class WeedDetectorConfig extends React.Component { this.props.onChange(conf, parseFloat(e.currentTarget.value))} placeholder={label} /> @@ -40,57 +43,48 @@ export class WeedDetectorConfig extends React.Component { } }; - find = (needle: keyof WD_ENV): DropDownItem => { - const wow = envGet(needle, this.props.values); - const ok = SPECIAL_VALUE_DDI[wow]; - return ok || NULL_CHOICE; - }; + find = (conf: keyof WD_ENV): DropDownItem => + SPECIAL_VALUE_DDI[this.getValue(conf)] || NULL_CHOICE render() { - return
- -
- - this.props.onChange("CAMERA_CALIBRATION_invert_hue_selection", - e.currentTarget.checked ? - SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} /> -
- - - - - + return
+ {!this.simple && +
+ - - - - - - - + conf={"CAMERA_CALIBRATION_calibration_object_separation"} + label={t(`Calibration Object Separation`)} /> + + + + + + + + + + + + +
} {
; } } + +export interface BoolConfigProps { + configKey: keyof WD_ENV; + label: string; + wDEnv: Partial; + onChange(key: keyof WD_ENV, value: number): void; +} + +export const BoolConfig = (props: BoolConfigProps) => +
+ + + props.onChange(props.configKey, + e.currentTarget.checked ? + SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} /> +
; diff --git a/frontend/farmware/weed_detector/index.tsx b/frontend/farmware/weed_detector/index.tsx index 23da0c179..aa4d8c8db 100644 --- a/frontend/farmware/weed_detector/index.tsx +++ b/frontend/farmware/weed_detector/index.tsx @@ -79,29 +79,24 @@ export class WeedDetector
- - 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..825ee5f38 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, 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/logs/__tests__/state_to_props_test.ts b/frontend/logs/__tests__/state_to_props_test.ts index 96d72a6a2..646a76860 100644 --- a/frontend/logs/__tests__/state_to_props_test.ts +++ b/frontend/logs/__tests__/state_to_props_test.ts @@ -24,24 +24,10 @@ describe("mapStateToProps()", () => { state.bot.hardware.configuration.sequence_init_log = false; const fakeApiConfig = fakeFbosConfig(); fakeApiConfig.body.sequence_init_log = true; - fakeApiConfig.body.api_migrated = true; state.resources = buildResourceIndex([fakeApiConfig]); const props = mapStateToProps(state); expect(props.sourceFbosConfig("sequence_init_log")).toEqual({ value: true, consistent: false }); }); - - it("bot source of FBOS settings", () => { - const state = fakeState(); - state.bot.hardware.configuration.sequence_init_log = false; - const fakeApiConfig = fakeFbosConfig(); - fakeApiConfig.body.sequence_init_log = true; - fakeApiConfig.body.api_migrated = false; - state.resources = buildResourceIndex([fakeApiConfig]); - const props = mapStateToProps(state); - expect(props.sourceFbosConfig("sequence_init_log")).toEqual({ - value: false, consistent: true - }); - }); }); diff --git a/frontend/messages/__tests__/state_to_props_test.ts b/frontend/messages/__tests__/state_to_props_test.ts index 9c38170cf..bb509a482 100644 --- a/frontend/messages/__tests__/state_to_props_test.ts +++ b/frontend/messages/__tests__/state_to_props_test.ts @@ -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/nav/__tests__/nav_links_test.tsx b/frontend/nav/__tests__/nav_links_test.tsx index 31d7eeb71..3550df51c 100644 --- a/frontend/nav/__tests__/nav_links_test.tsx +++ b/frontend/nav/__tests__/nav_links_test.tsx @@ -28,7 +28,7 @@ describe("", () => { }); it("shows links", () => { - mockDev = true; + mockDev = false; const wrapper = mount(); expect(wrapper.text().toLowerCase()).not.toContain("tools"); }); diff --git a/frontend/nav/nav_links.tsx b/frontend/nav/nav_links.tsx index e05777dab..e4d7bc76c 100644 --- a/frontend/nav/nav_links.tsx +++ b/frontend/nav/nav_links.tsx @@ -37,7 +37,7 @@ export const getLinks = (): NavLinkParams[] => betterCompact([ name: "Regimens", icon: "calendar-check-o", slug: "regimens", computeHref: computeEditorUrlFromState("Regimen") }, - DevSettings.futureFeaturesEnabled() ? undefined : + !DevSettings.futureFeaturesEnabled() ? undefined : { name: "Tools", icon: "wrench", slug: "tools" }, { name: "Farmware", icon: "crosshairs", slug: "farmware", diff --git a/frontend/redux/upgrade_reminder.ts b/frontend/redux/upgrade_reminder.ts index f74a2d542..7f97d0d2f 100644 --- a/frontend/redux/upgrade_reminder.ts +++ b/frontend/redux/upgrade_reminder.ts @@ -1,10 +1,10 @@ import { info } from "../toast/toast"; -import { semverCompare, SemverResult, MinVersionOverride } from "../util"; +import { semverCompare, SemverResult, FbosVersionFallback } from "../util"; import { Content } from "../constants"; import { Dictionary } from "lodash"; const IDEAL_VERSION = - globalConfig.FBOS_END_OF_LIFE_VERSION || MinVersionOverride.ALWAYS; + globalConfig.FBOS_END_OF_LIFE_VERSION || FbosVersionFallback.NULL; /** Returns a function that, when given a version string, (possibly) warns the * user to upgrade FBOS versions before it hits end of life. */ @@ -12,8 +12,8 @@ export function createReminderFn() { /** FBOS Version can change during the app lifecycle. We only want one * reminder per FBOS version change. */ const alreadyChecked: Dictionary = { - // Dont bother when the user is offline. - [MinVersionOverride.ALWAYS]: true + // Don't bother when the user is offline. + [FbosVersionFallback.NULL]: true }; return function reminder(version: string) { diff --git a/frontend/redux/version_tracker_middleware.ts b/frontend/redux/version_tracker_middleware.ts index 4af0b8aed..1187b4fe5 100644 --- a/frontend/redux/version_tracker_middleware.ts +++ b/frontend/redux/version_tracker_middleware.ts @@ -1,5 +1,5 @@ import { EnvName } from "./interfaces"; -import { determineInstalledOsVersion, MinVersionOverride } from "../util/index"; +import { determineInstalledOsVersion, FbosVersionFallback } from "../util/index"; import { maybeGetDevice } from "../resources/selectors"; import { MW } from "./middlewares"; import { Everything } from "../interfaces"; @@ -11,10 +11,10 @@ const maybeRemindUserToUpdate = createReminderFn(); function getVersionFromState(state: Everything) { const device = maybeGetDevice(state.resources.index); - const v = - determineInstalledOsVersion(state.bot, device) || MinVersionOverride.ALWAYS; - maybeRemindUserToUpdate(v); - return v; + const version = determineInstalledOsVersion(state.bot, device) + || FbosVersionFallback.NULL; + maybeRemindUserToUpdate(version); + return version; } const fn: MW = diff --git a/frontend/util/__tests__/version_test.ts b/frontend/util/__tests__/version_test.ts index 28e9a1e56..e58346c44 100644 --- a/frontend/util/__tests__/version_test.ts +++ b/frontend/util/__tests__/version_test.ts @@ -5,6 +5,7 @@ import { createShouldDisplayFn, determineInstalledOsVersion, versionOK, + MinVersionOverride, } from "../version"; import { bot } from "../../__test_support__/fake_state/bot"; import { fakeDevice } from "../../__test_support__/resource_index_builder"; @@ -128,6 +129,10 @@ describe("shouldDisplay()", () => { expect(createShouldDisplayFn("10.0.0", { jest_feature: "1.0.0" }, undefined)( Feature.jest_feature)).toBeTruthy(); + globalConfig.FBOS_END_OF_LIFE_VERSION = MinVersionOverride.NEVER; + expect(createShouldDisplayFn(undefined, fakeMinOsData, undefined)( + Feature.jest_feature)).toBeTruthy(); + delete globalConfig.FBOS_END_OF_LIFE_VERSION; }); it("shouldn't display", () => { @@ -179,14 +184,20 @@ describe("determineInstalledOsVersion()", () => { describe("versionOK()", () => { it("checks if major/minor version meets min requirement", () => { - expect(versionOK("9.1.9-rc99", 3, 0)).toBeTruthy(); - expect(versionOK("3.0.9-rc99", 3, 0)).toBeTruthy(); - expect(versionOK("4.0.0", 3, 0)).toBeTruthy(); - expect(versionOK("4.0.0", 3, 1)).toBeTruthy(); - expect(versionOK("3.1.0", 3, 0)).toBeTruthy(); - expect(versionOK("2.0.-", 3, 0)).toBeFalsy(); - expect(versionOK("2.9.4", 3, 0)).toBeFalsy(); - expect(versionOK("1.9.6", 3, 0)).toBeFalsy(); - expect(versionOK("3.1.6", 4, 0)).toBeFalsy(); + globalConfig.MINIMUM_FBOS_VERSION = "3.0.0"; + expect(versionOK("9.1.9-rc99")).toBeTruthy(); + expect(versionOK("3.0.9-rc99")).toBeTruthy(); + expect(versionOK("4.0.0")).toBeTruthy(); + expect(versionOK("3.1.0")).toBeTruthy(); + expect(versionOK("2.0.-")).toBeFalsy(); + expect(versionOK("2.9.4")).toBeFalsy(); + expect(versionOK("1.9.6")).toBeFalsy(); + globalConfig.MINIMUM_FBOS_VERSION = "3.1.0"; + expect(versionOK("4.0.0")).toBeTruthy(); + globalConfig.MINIMUM_FBOS_VERSION = "4.0.0"; + expect(versionOK("3.1.6")).toBeFalsy(); + delete globalConfig.MINIMUM_FBOS_VERSION; + expect(versionOK("5.0.0")).toBeFalsy(); + expect(versionOK("7.0.0")).toBeTruthy(); }); }); diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 247a785e1..33357bb03 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -183,9 +183,7 @@ export function validBotLocationData( */ export function validFwConfig(config: TaggedFirmwareConfig | undefined): TaggedFirmwareConfig["body"] | undefined { - return (config?.body.api_migrated) - ? config.body - : undefined; + return config ? config.body : undefined; } /** @@ -193,9 +191,7 @@ export function validFwConfig(config: TaggedFirmwareConfig | undefined): */ export function validFbosConfig( config: TaggedFbosConfig | undefined): TaggedFbosConfig["body"] | undefined { - return (config?.body.api_migrated) - ? config.body - : undefined; + return config ? config.body : undefined; } interface BetterUUID { diff --git a/frontend/util/version.ts b/frontend/util/version.ts index 1a4c6082d..b6178b7d3 100644 --- a/frontend/util/version.ts +++ b/frontend/util/version.ts @@ -97,10 +97,13 @@ export function minFwVersionCheck(current: string | undefined, min: string) { * for shouldDisplay() */ export enum MinVersionOverride { - ALWAYS = "0.0.0", NEVER = "999.999.999", } +export enum FbosVersionFallback { + NULL = "0.0.0", +} + /** * Determine whether a feature should be displayed based on * the user's current FBOS version. Min FBOS version feature data is pulled @@ -114,19 +117,18 @@ export function createShouldDisplayFn( lookupData: MinOsFeatureLookup | undefined, override: string | undefined) { return function (feature: Feature): boolean { - const target = override || current; - if (isString(target)) { - const table = lookupData || {}; - const min = table[feature] || MinVersionOverride.NEVER; - switch (semverCompare(target, min)) { - case SemverResult.LEFT_IS_GREATER: - case SemverResult.EQUAL: - return true; - default: - return false; - } + const fallback = globalConfig.FBOS_END_OF_LIFE_VERSION || + FbosVersionFallback.NULL; + const target = override || current || fallback; + const table = lookupData || {}; + const min = table[feature] || MinVersionOverride.NEVER; + switch (semverCompare(target, min)) { + case SemverResult.LEFT_IS_GREATER: + case SemverResult.EQUAL: + return true; + default: + return false; } - return false; }; } @@ -147,6 +149,9 @@ export function determineInstalledOsVersion( } } +const parseVersion = (version: string) => + version.split(".").map(x => parseInt(x, 10)); + /** * Compare installed FBOS version against the lowest version compatible * with the web app to lock out incompatible FBOS versions from the App. @@ -155,20 +160,16 @@ export function determineInstalledOsVersion( * identifiers. * * @param stringyVersion version string to check ("0.0.0") - * @param _EXPECTED_MAJOR minimum required major version number - * @param _EXPECTED_MINOR minimum required minor version number */ -export function versionOK(stringyVersion = "0.0.0", - _EXPECTED_MAJOR: number, - _EXPECTED_MINOR: number) { - const [actual_major, actual_minor] = stringyVersion - .split(".") - .map(x => parseInt(x, 10)); - if (actual_major > _EXPECTED_MAJOR) { +export function versionOK(stringyVersion = "0.0.0") { + const [actual_major, actual_minor] = parseVersion(stringyVersion); + const [EXPECTED_MAJOR, EXPECTED_MINOR] = + parseVersion(globalConfig.MINIMUM_FBOS_VERSION || "6.0.0"); + if (actual_major > EXPECTED_MAJOR) { return true; } else { - const majorOK = (actual_major == _EXPECTED_MAJOR); - const minorOK = (actual_minor >= _EXPECTED_MINOR); + const majorOK = (actual_major == EXPECTED_MAJOR); + const minorOK = (actual_minor >= EXPECTED_MINOR); return (majorOK && minorOK); } } diff --git a/spec/controllers/api/devices/devices_controller_seed_spec.rb b/spec/controllers/api/devices/devices_controller_seed_spec.rb index 259e07667..f8a71103c 100644 --- a/spec/controllers/api/devices/devices_controller_seed_spec.rb +++ b/spec/controllers/api/devices/devices_controller_seed_spec.rb @@ -101,6 +101,14 @@ describe Api::DevicesController do device.tool_slots.order(id: :asc)[5] end + def tool_slots_slot_7?(device) + device.tool_slots.order(id: :asc)[6] + end + + def tool_slots_slot_8?(device) + device.tool_slots.order(id: :asc)[7] + end + def tools_seed_bin?(device) device.tools.find_by(name: "Seed Bin") end @@ -117,10 +125,6 @@ describe Api::DevicesController do device.tools.find_by(name: "Seed Trough 2") end - def tools_seed_trough_3?(device) - device.tools.find_by(name: "Seed Trough 3") - end - def tools_seeder?(device) device.tools.find_by(name: "Seeder") end @@ -230,17 +234,20 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -280,6 +287,8 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") @@ -287,11 +296,11 @@ describe Api::DevicesController do check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -331,11 +340,20 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") + check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") + check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") + check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") + check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") + check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -375,11 +393,22 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1") + expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2") + + check_slot_pairing(tool_slots_slot_1?(device), "Seeder") + check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") + check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray") + check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") + check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") + check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1") + check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2") + expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be - expect(tools_seed_trough_1?(device)).to_not be - expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be + expect(tools_seed_trough_1?(device)).to be + expect(tools_seed_trough_2?(device)).to be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -419,6 +448,8 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") @@ -431,7 +462,6 @@ describe Api::DevicesController do expect(tools_seed_tray?(device)).to be expect(tools_seed_trough_1?(device)).to_not be expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -471,6 +501,8 @@ describe Api::DevicesController do expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle") expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor") expect(tool_slots_slot_6?(device).name).to eq("Weeder") + expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1") + expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2") check_slot_pairing(tool_slots_slot_1?(device), "Seeder") check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin") @@ -478,12 +510,13 @@ describe Api::DevicesController do check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle") check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor") check_slot_pairing(tool_slots_slot_6?(device), "Weeder") + check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1") + check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2") expect(tools_seed_bin?(device)).to be expect(tools_seed_tray?(device)).to be - expect(tools_seed_trough_1?(device)).to_not be - expect(tools_seed_trough_2?(device)).to_not be - expect(tools_seed_trough_3?(device)).to_not be + expect(tools_seed_trough_1?(device)).to be + expect(tools_seed_trough_2?(device)).to be expect(tools_seeder?(device)).to be_kind_of(Tool) expect(tools_soil_sensor?(device)).to be_kind_of(Tool) expect(tools_watering_nozzle?(device)).to be_kind_of(Tool) @@ -519,18 +552,20 @@ describe Api::DevicesController do expect(settings_hide_sensors?(device)).to be(true) expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1") expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2") - expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3") + expect(tool_slots_slot_3?(device)).to_not be expect(tool_slots_slot_4?(device)).to_not be expect(tool_slots_slot_5?(device)).to_not be expect(tool_slots_slot_6?(device)).to_not be + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1") check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2") - check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3") + expect(tools_seed_bin?(device)).to_not be expect(tools_seed_tray?(device)).to_not be expect(tools_seed_trough_1?(device)).to be expect(tools_seed_trough_2?(device)).to be - expect(tools_seed_trough_3?(device)).to be expect(tools_seeder?(device)).to_not be expect(tools_soil_sensor?(device)).to_not be expect(tools_watering_nozzle?(device)).to_not be @@ -566,18 +601,20 @@ describe Api::DevicesController do expect(settings_hide_sensors?(device)).to be(true) expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1") expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2") - expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3") + expect(tool_slots_slot_3?(device)).to_not be expect(tool_slots_slot_4?(device)).to_not be expect(tool_slots_slot_5?(device)).to_not be expect(tool_slots_slot_6?(device)).to_not be + expect(tool_slots_slot_7?(device)).to_not be + expect(tool_slots_slot_8?(device)).to_not be + check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1") check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2") - check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3") + expect(tools_seed_bin?(device)).to_not be expect(tools_seed_tray?(device)).to_not be expect(tools_seed_trough_1?(device)).to be expect(tools_seed_trough_2?(device)).to be - expect(tools_seed_trough_3?(device)).to be expect(tools_seeder?(device)).to_not be expect(tools_soil_sensor?(device)).to_not be expect(tools_watering_nozzle?(device)).to_not be From 66b5e3c9620a99ee397725b4902083db1f38eb1d Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Sat, 15 Feb 2020 10:30:23 -0800 Subject: [PATCH 09/26] refactor external urls --- frontend/__tests__/external_urls_test.ts | 33 ++++++++++++ frontend/api/api.ts | 4 ++ frontend/apology.tsx | 3 +- frontend/auth/actions.ts | 5 +- frontend/constants.ts | 3 +- frontend/crash_page.tsx | 3 +- frontend/demo/demo_iframe.tsx | 9 ++-- frontend/devices/actions.ts | 3 -- .../components/farmbot_os_settings.tsx | 6 +-- .../components/fbos_settings/fbos_details.tsx | 10 ++-- frontend/devices/connectivity/truth_table.ts | 14 +++-- frontend/devices/interfaces.ts | 2 +- frontend/external_urls.ts | 51 +++++++++++++++++++ frontend/farm_designer/openfarm.ts | 3 -- frontend/farm_designer/plants/crop_info.tsx | 3 +- .../plants/openfarm_search_results.tsx | 3 +- frontend/farm_designer/util.ts | 4 +- frontend/farmware/__tests__/actions_test.ts | 4 +- frontend/farmware/actions.ts | 11 ++-- frontend/farmware/interfaces.ts | 2 - frontend/front_page/laptop_splash.tsx | 5 +- frontend/nav/additional_menu.tsx | 3 +- frontend/open_farm/__tests__/icons_test.ts | 8 +-- frontend/open_farm/cached_crop.ts | 5 +- frontend/open_farm/icons.ts | 5 -- frontend/os_download/content.tsx | 6 +-- frontend/tos_update/component.tsx | 3 +- frontend/ui/__tests__/doc_link_test.ts | 7 +-- frontend/ui/doc_link.ts | 8 +-- 29 files changed, 152 insertions(+), 74 deletions(-) create mode 100644 frontend/__tests__/external_urls_test.ts create mode 100644 frontend/external_urls.ts diff --git a/frontend/__tests__/external_urls_test.ts b/frontend/__tests__/external_urls_test.ts new file mode 100644 index 000000000..156e1932b --- /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/FEATURE_MIN_VERSIONS.json"); + expect(ExternalUrl.osReleaseNotes) + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/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("http://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.Videos.desktop) + .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"); + expect(ExternalUrl.Videos.mobile) + .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"); + }); +}); 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/auth/actions.ts b/frontend/auth/actions.ts index 76126927c..9527939f0 100644 --- a/frontend/auth/actions.ts +++ b/frontend/auth/actions.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { - fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL, + fetchReleases, fetchMinOsFeatureData, fetchLatestGHBetaRelease } from "../devices/actions"; import { AuthState } from "./interfaces"; @@ -16,6 +16,7 @@ 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/constants.ts b/frontend/constants.ts index 7dd95c219..c76339cb7 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -952,8 +952,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/crash_page.tsx b/frontend/crash_page.tsx index 031ff12ad..7f6b92dd7 100644 --- a/frontend/crash_page.tsx +++ b/frontend/crash_page.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { get } from "lodash"; import { Page } from "./ui/index"; import { Session } from "./session"; +import { ExternalUrl } from "./external_urls"; /** Use currying to pass down `error` object for now. */ export function crashPage(error: object) { @@ -24,7 +25,7 @@ export function crashPage(error: object) {
  • 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/demo/demo_iframe.tsx b/frontend/demo/demo_iframe.tsx index e0f5ba72c..16681552f 100644 --- a/frontend/demo/demo_iframe.tsx +++ b/frontend/demo/demo_iframe.tsx @@ -2,16 +2,13 @@ import { connect, MqttClient } from "mqtt"; import React from "react"; import { uuid } from "farmbot"; import axios from "axios"; +import { ExternalUrl } from "../external_urls"; 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.", @@ -63,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> { return
    - + diff --git a/frontend/devices/actions.ts b/frontend/devices/actions.ts index 67be2d969..edea6ce0f 100644 --- a/frontend/devices/actions.ts +++ b/frontend/devices/actions.ts @@ -26,9 +26,6 @@ import { t } from "../i18next_wrapper"; const ON = 1, OFF = 0; export type ConfigKey = keyof McuParams; -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"]; diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index cb4b9f0de..cbfa9d10f 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -15,6 +15,7 @@ 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"; export enum ColWidth { label = 3, @@ -22,15 +23,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() { diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index 43ad6a0c7..c06a4b627 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 => { @@ -170,7 +171,7 @@ const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8); interface CommitDisplayProps { title: string; - repo: string; + repo: FarmBotRepo; commit: string; } @@ -184,7 +185,7 @@ const CommitDisplay = ( {shortCommit === "---" ? shortCommit : {shortCommit} } @@ -270,14 +271,15 @@ export function FbosDetails(props: FbosDetailsProps) { timeSettings={props.timeSettings} device={props.deviceAccount} />

    {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) && 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/interfaces.ts b/frontend/devices/interfaces.ts index 8963b9f86..767ea8bd5 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -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 { diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts new file mode 100644 index 000000000..47e123e98 --- /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 = "http://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}`; + 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 = + `${GITHUB}/${Org.FarmBot}/${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 Videos { + export const desktop = + `${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`; + export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`; + } +} diff --git a/frontend/farm_designer/openfarm.ts b/frontend/farm_designer/openfarm.ts index 10899a263..4b07b09d9 100644 --- a/frontend/farm_designer/openfarm.ts +++ b/frontend/farm_designer/openfarm.ts @@ -67,9 +67,6 @@ export namespace OpenFarm { type: string; attributes: ImageAttrs; } - - export const cropUrl = "https://openfarm.cc/api/v1/crops"; - export const browsingCropUrl = "https://openfarm.cc/crops/"; } /** Returned by https://openfarm.cc/api/v1/crops?filter=q */ export interface CropSearchResult { diff --git a/frontend/farm_designer/plants/crop_info.tsx b/frontend/farm_designer/plants/crop_info.tsx index e6bc6f63b..e56c69f50 100644 --- a/frontend/farm_designer/plants/crop_info.tsx +++ b/frontend/farm_designer/plants/crop_info.tsx @@ -24,6 +24,7 @@ import { import { startCase, isArray, chain, isNumber } from "lodash"; import { t } from "../../i18next_wrapper"; import { Panel } from "../panel_header"; +import { ExternalUrl } from "../../external_urls"; interface InfoFieldProps { title: string; @@ -170,7 +171,7 @@ const CropDragInfoTile = const EditOnOpenFarm = ({ slug }: { slug: string }) =>
    {t("Edit on")}  - {"OpenFarm"} diff --git a/frontend/farm_designer/plants/openfarm_search_results.tsx b/frontend/farm_designer/plants/openfarm_search_results.tsx index 2efddc935..d6040a26f 100644 --- a/frontend/farm_designer/plants/openfarm_search_results.tsx +++ b/frontend/farm_designer/plants/openfarm_search_results.tsx @@ -5,6 +5,7 @@ import { } from "../../ui/empty_state_wrapper"; import { Content } from "../../constants"; import { t } from "../../i18next_wrapper"; +import { ExternalUrl } from "../../external_urls"; /** A stripped down version of OFSearchResult */ interface Result { @@ -24,7 +25,7 @@ export class OpenFarmResults extends React.Component { get text(): JSX.Element { return

    {`${t(Content.CROP_NOT_FOUND_INTRO)} `} - + {t(Content.CROP_NOT_FOUND_LINK)}

    ; diff --git a/frontend/farm_designer/util.ts b/frontend/farm_designer/util.ts index 40500c821..425fece84 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)); diff --git a/frontend/farmware/__tests__/actions_test.ts b/frontend/farmware/__tests__/actions_test.ts index a32929d45..d4bfde0b4 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/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/interfaces.ts b/frontend/farmware/interfaces.ts index 1a9cfe4d1..32d7cb132 100644 --- a/frontend/farmware/interfaces.ts +++ b/frontend/farmware/interfaces.ts @@ -24,8 +24,6 @@ export interface FarmwareState { infoOpen: boolean; } -export type FarmwareManifestEntry = Record<"name" | "manifest", string>; - export interface FarmwareConfigMenuProps { show: boolean | undefined; dispatch: Function; diff --git a/frontend/front_page/laptop_splash.tsx b/frontend/front_page/laptop_splash.tsx index ec4308bfa..bc9ddb94f 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/nav/additional_menu.tsx b/frontend/nav/additional_menu.tsx index 83a15f9c8..4e0b1290f 100644 --- a/frontend/nav/additional_menu.tsx +++ b/frontend/nav/additional_menu.tsx @@ -3,6 +3,7 @@ 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
    @@ -30,7 +31,7 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
    diff --git a/frontend/open_farm/__tests__/icons_test.ts b/frontend/open_farm/__tests__/icons_test.ts index 499c8e618..153fd1e74 100644 --- a/frontend/open_farm/__tests__/icons_test.ts +++ b/frontend/open_farm/__tests__/icons_test.ts @@ -1,10 +1,4 @@ -import { OpenFarmAPI, svgToUrl } from "../icons"; - -describe("OpenFarmAPI", () => { - it("has a base URL", () => { - expect(OpenFarmAPI.OFBaseURL).toContain("openfarm.cc"); - }); -}); +import { svgToUrl } from "../icons"; describe("svgToUrl()", () => { it("returns svg url", () => { diff --git a/frontend/open_farm/cached_crop.ts b/frontend/open_farm/cached_crop.ts index 24dc069ab..21d166b6a 100644 --- a/frontend/open_farm/cached_crop.ts +++ b/frontend/open_farm/cached_crop.ts @@ -1,7 +1,8 @@ import axios, { AxiosResponse } from "axios"; import { Dictionary } from "farmbot"; import { isObject } from "lodash"; -import { OFCropAttrs, OFCropResponse, OpenFarmAPI, svgToUrl } from "./icons"; +import { OFCropAttrs, OFCropResponse, svgToUrl } from "./icons"; +import { ExternalUrl } from "../external_urls"; export type OFIcon = Readonly; type IconDictionary = Dictionary; @@ -57,7 +58,7 @@ const cacheTheIcon = (slug: string) => }; function HTTPIconFetch(slug: string) { - const url = OpenFarmAPI.OFBaseURL + slug; + const url = ExternalUrl.OpenFarm.cropApi + slug; // Avoid duplicate requests. if (promiseCache[url]) { return promiseCache[url]; } promiseCache[url] = axios diff --git a/frontend/open_farm/icons.ts b/frontend/open_farm/icons.ts index c79f93549..b2ed9f940 100644 --- a/frontend/open_farm/icons.ts +++ b/frontend/open_farm/icons.ts @@ -1,4 +1,3 @@ -const BASE = "https://openfarm.cc/api/v1/crops/"; export const DATA_URI = "data:image/svg+xml;utf8,"; export const DEFAULT_ICON = "/app-resources/img/generic-plant.svg"; @@ -20,10 +19,6 @@ export interface OFCropResponse { }; } -export namespace OpenFarmAPI { - export const OFBaseURL = BASE; -} - export function svgToUrl(xml: string | undefined): string { return xml ? (DATA_URI + encodeURIComponent(xml)) : DEFAULT_ICON; diff --git a/frontend/os_download/content.tsx b/frontend/os_download/content.tsx index c30d5c016..f9d0e66f9 100644 --- a/frontend/os_download/content.tsx +++ b/frontend/os_download/content.tsx @@ -3,9 +3,7 @@ import axios from "axios"; import { t } from "../i18next_wrapper"; import { GithubRelease } from "../devices/interfaces"; import { Content } from "../constants"; - -const LATEST_RELEASE_URL = - "https://api.github.com/repos/farmbot/farmbot_os/releases/latest"; +import { ExternalUrl } from "../external_urls"; interface OsDownloadState { tagName: string; @@ -49,7 +47,7 @@ export class OsDownload extends React.Component<{}, OsDownloadState> { } fetchLatestRelease = () => - axios.get(LATEST_RELEASE_URL) + axios.get(ExternalUrl.latestRelease) .then(resp => this.setState({ tagName: resp.data.tag_name, diff --git a/frontend/tos_update/component.tsx b/frontend/tos_update/component.tsx index df93f2639..92d9a9336 100644 --- a/frontend/tos_update/component.tsx +++ b/frontend/tos_update/component.tsx @@ -8,6 +8,7 @@ import { API } from "../api"; import { Row, Col, Widget, WidgetHeader, WidgetBody } from "../ui"; import { TermsCheckbox } from "../front_page/terms_checkbox"; import { t } from "../i18next_wrapper"; +import { ExternalUrl } from "../external_urls"; interface Props { } interface State { @@ -86,7 +87,7 @@ export class TosUpdate extends React.Component> {

    {t("Please send us an email at contact@farm.bot or see the ")} - + {t("FarmBot forum.")}

    diff --git a/frontend/ui/__tests__/doc_link_test.ts b/frontend/ui/__tests__/doc_link_test.ts index 5a1c61d06..bcb65f595 100644 --- a/frontend/ui/__tests__/doc_link_test.ts +++ b/frontend/ui/__tests__/doc_link_test.ts @@ -1,8 +1,9 @@ -import { docLink, BASE_URL } from "../doc_link"; +import { docLink } from "../doc_link"; +import { ExternalUrl } from "../../external_urls"; describe("docLink", () => { it("creates doc links", () => { - expect(docLink()).toEqual(BASE_URL); - expect(docLink("farmware")).toEqual(BASE_URL + "farmware"); + expect(docLink()).toEqual(ExternalUrl.softwareDocs + "/"); + expect(docLink("farmware")).toEqual(ExternalUrl.softwareDocs + "/farmware"); }); }); diff --git a/frontend/ui/doc_link.ts b/frontend/ui/doc_link.ts index a65b5958c..8cbcc2edd 100644 --- a/frontend/ui/doc_link.ts +++ b/frontend/ui/doc_link.ts @@ -1,4 +1,4 @@ -export const BASE_URL = "https://software.farm.bot/docs/"; +import { ExternalUrl } from "../external_urls"; /** A centralized list of all documentation slugs in the app makes it easier to * rename / move links in the future. */ @@ -7,11 +7,13 @@ export const DOC_SLUGS = { "camera-calibration": "Camera Calibration", "the-farmbot-web-app": "Web App", "farmware": "Farmware", - "connecting-farmbot-to-the-internet": "Connecting FarmBot to the Internet" + "connecting-farmbot-to-the-internet": "Connecting FarmBot to the Internet", + "for-it-security-professionals": "For IT Security Professionals", }; export type DocSlug = keyof typeof DOC_SLUGS; /** WHY?: The function keeps things DRY. It also makes life easier when the * documentation URL / slug name changes. */ -export const docLink = (slug?: DocSlug) => BASE_URL + (slug || ""); +export const docLink = (slug?: DocSlug) => + `${ExternalUrl.softwareDocs}/${slug || ""}`; From a04ec59ba55ad8f1c3f6b745265735e40c3caa6e Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Tue, 18 Feb 2020 11:21:09 -0800 Subject: [PATCH 10/26] model and version updates part 2 --- frontend/__tests__/attach_app_to_dom_test.ts | 13 +- frontend/__tests__/external_urls_test.ts | 10 +- frontend/constants.ts | 107 +++++++++++- frontend/controls/controls.tsx | 2 + .../peripherals/__tests__/index_test.tsx | 18 +- frontend/controls/peripherals/index.tsx | 38 +++- .../controls/sensors/__tests__/index_test.tsx | 11 +- .../sensors/__tests__/sensor_list_test.tsx | 7 + frontend/controls/sensors/index.tsx | 18 +- frontend/css/_blueprint_overrides.scss | 1 + frontend/css/global.scss | 20 +++ frontend/demo/demo_iframe.tsx | 4 +- .../boolean_mcu_input_group_test.tsx | 3 +- .../__tests__/farmbot_os_settings_test.tsx | 20 ++- .../firmware_hardware_support_test.ts | 18 +- .../__tests__/maybe_highlight_test.tsx | 81 +++++++++ .../components/boolean_mcu_input_group.tsx | 76 ++++---- .../components/farmbot_os_settings.tsx | 59 ++++--- .../fbos_settings/auto_sync_row.tsx | 41 +++-- .../fbos_settings/auto_update_row.tsx | 39 +++-- .../components/fbos_settings/board_type.tsx | 52 +++--- .../fbos_settings/boot_sequence_selector.tsx | 28 +-- .../fbos_settings/camera_selection.tsx | 35 ++-- .../fbos_settings/factory_reset_row.tsx | 121 +++++++------ .../fbos_settings/farmbot_os_row.tsx | 88 +++++----- .../fbos_settings/fbos_button_row.tsx | 44 ++--- .../fbos_settings/power_and_reset.tsx | 35 ++-- .../components/firmware_hardware_support.ts | 8 +- .../devices/components/hardware_settings.tsx | 4 + .../__tests__/calibration_row_test.tsx | 3 +- .../__tests__/header_test.tsx | 7 +- .../hardware_settings/calibration_row.tsx | 41 +++-- .../hardware_settings/danger_zone.tsx | 50 +++--- .../components/hardware_settings/encoders.tsx | 33 ++-- .../components/hardware_settings/endstops.tsx | 19 +- .../hardware_settings/error_handling.tsx | 19 +- .../components/hardware_settings/header.tsx | 12 +- .../homing_and_calibration.tsx | 28 +-- .../components/hardware_settings/motors.tsx | 35 ++-- .../hardware_settings/pin_bindings.tsx | 11 +- .../hardware_settings/pin_guard.tsx | 12 +- .../hardware_settings/single_setting_row.tsx | 33 ++-- frontend/devices/components/interfaces.ts | 7 +- .../devices/components/maybe_highlight.tsx | 162 ++++++++++++++++++ .../components/numeric_mcu_input_group.tsx | 78 +++++---- frontend/devices/interfaces.ts | 8 +- .../pin_bindings/list_and_label_support.tsx | 18 +- frontend/external_urls.ts | 10 +- frontend/farm_designer/map/garden_map.tsx | 1 + frontend/farm_designer/map/interfaces.ts | 3 + .../farmbot/__tests__/bot_figure_test.tsx | 18 +- .../map/layers/farmbot/bot_figure.tsx | 2 +- .../plants/__tests__/garden_plant_test.tsx | 21 +++ .../plants/__tests__/plant_layer_test.tsx | 16 +- .../map/layers/plants/garden_plant.tsx | 12 +- .../map/layers/plants/plant_layer.tsx | 10 +- .../__tests__/tool_slot_point_test.tsx | 4 +- .../map/layers/tool_slots/tool_slot_point.tsx | 2 +- .../__tests__/group_detail_active_test.tsx | 1 + .../point_groups/group_detail.tsx | 3 + .../point_groups/group_detail_active.tsx | 4 +- .../point_groups/group_order_visual.tsx | 2 +- .../point_groups/point_group_item.tsx | 1 + .../tools/__tests__/add_tool_slot_test.tsx | 24 ++- .../tools/__tests__/add_tool_test.tsx | 30 +++- .../tools/__tests__/edit_tool_slot_test.tsx | 3 +- .../tools/__tests__/index_test.tsx | 11 +- .../tool_slot_edit_components_test.tsx | 8 + frontend/farm_designer/tools/add_tool.tsx | 75 ++++---- .../farm_designer/tools/add_tool_slot.tsx | 20 ++- frontend/farm_designer/tools/edit_tool.tsx | 2 +- .../farm_designer/tools/edit_tool_slot.tsx | 9 +- frontend/farm_designer/tools/index.tsx | 55 ++++-- .../tools/tool_slot_edit_components.tsx | 23 ++- frontend/folders/actions.ts | 8 +- frontend/front_page/laptop_splash.tsx | 2 +- frontend/help/__tests__/tour_test.tsx | 2 +- frontend/help/__tests__/tours_test.ts | 56 +++++- frontend/help/tour.tsx | 21 ++- frontend/help/tours.ts | 80 +++++++-- frontend/messages/__tests__/alerts_test.tsx | 4 +- frontend/messages/alerts.tsx | 1 + frontend/redux/upgrade_reminder.ts | 3 +- .../mark_as/__tests__/unpack_step_test.ts | 6 +- .../step_tiles/mark_as/unpack_step.ts | 27 +-- public/app-resources/languages/_helper.js | 3 +- .../languages/translation_metrics.md | 22 +-- 87 files changed, 1481 insertions(+), 701 deletions(-) create mode 100644 frontend/devices/components/__tests__/maybe_highlight_test.tsx create mode 100644 frontend/devices/components/maybe_highlight.tsx diff --git a/frontend/__tests__/attach_app_to_dom_test.ts b/frontend/__tests__/attach_app_to_dom_test.ts index 1a2d69daa..2c18907d9 100644 --- a/frontend/__tests__/attach_app_to_dom_test.ts +++ b/frontend/__tests__/attach_app_to_dom_test.ts @@ -1,10 +1,9 @@ -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() } }; diff --git a/frontend/__tests__/external_urls_test.ts b/frontend/__tests__/external_urls_test.ts index 156e1932b..99f23cea5 100644 --- a/frontend/__tests__/external_urls_test.ts +++ b/frontend/__tests__/external_urls_test.ts @@ -6,9 +6,9 @@ import { ExternalUrl } from "../external_urls"; describe("ExternalUrl", () => { it("returns urls", () => { expect(ExternalUrl.featureMinVersions) - .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/FEATURE_MIN_VERSIONS.json"); + .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/FEATURE_MIN_VERSIONS.json"); expect(ExternalUrl.osReleaseNotes) - .toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/RELEASE_NOTES.md"); + .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) @@ -18,16 +18,16 @@ describe("ExternalUrl", () => { expect(ExternalUrl.softwareDocs) .toEqual("https://software.farm.bot/docs"); expect(ExternalUrl.softwareForum) - .toEqual("http://forum.farmbot.org/c/software"); + .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.Videos.desktop) + expect(ExternalUrl.Video.desktop) .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018"); - expect(ExternalUrl.Videos.mobile) + expect(ExternalUrl.Video.mobile) .toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097"); }); }); diff --git a/frontend/constants.ts b/frontend/constants.ts index c76339cb7..7adb0a80f 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -702,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 @@ -812,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 @@ -887,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 + tool slots for them to by pressing the tool 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 seed container 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.`); + press the + button to create a tool slot.`); export const ADD_PERIPHERALS = trim(`Press edit and then the + button to add peripherals.`); @@ -930,6 +944,87 @@ 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 = `dangerZone`, + resetHardwareParams = `Reset hardware parameter defaults`, + + // Pin Bindings + pinBindings = `Pin Bindings`, + + // FarmBot OS + name = `name`, + timezone = `timezone`, + camera = `camera`, + firmware = `firmware`, + farmbotOSAutoUpdate = `Farmbot OS 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`, +} + export namespace DiagnosticMessages { export const OK = trim(`All systems nominal.`); diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index bcd98afa0..5195c58ba 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -38,6 +38,7 @@ export class RawControls extends React.Component { getWebAppConfigVal={this.props.getWebAppConfigVal} /> peripherals = () => { sensors = () => this.hideSensors ?
    : ", () => { @@ -14,7 +14,8 @@ describe("", () => { bot, peripherals: [fakePeripheral()], dispatch: jest.fn(), - disabled: false + disabled: false, + firmwareHardware: undefined, }; } @@ -73,11 +74,18 @@ describe("", () => { expect(p.dispatch).toHaveBeenCalled(); }); - it("adds farmduino peripherals", () => { + it.each<[FirmwareHardware, number]>([ + ["arduino", 2], + ["farmduino", 5], + ["farmduino_k14", 5], + ["farmduino_k15", 5], + ["express_k10", 3], + ])("adds peripherals: %s", (firmware, expectedAdds) => { const p = fakeProps(); + p.firmwareHardware = firmware; const wrapper = mount(); wrapper.setState({ isEditing: true }); - clickButton(wrapper, 3, "farmduino"); - expect(p.dispatch).toHaveBeenCalledTimes(5); + clickButton(wrapper, 3, "stock"); + expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds); }); }); diff --git a/frontend/controls/peripherals/index.tsx b/frontend/controls/peripherals/index.tsx index cad85b669..69decd905 100644 --- a/frontend/controls/peripherals/index.tsx +++ b/frontend/controls/peripherals/index.tsx @@ -56,12 +56,31 @@ export class Peripherals this.props.dispatch(init("Peripheral", { pin, label })); }; - farmduinoPeripherals = () => { - this.newPeripheral(7, t("Lighting")); - this.newPeripheral(8, t("Water")); - this.newPeripheral(9, t("Vacuum")); - this.newPeripheral(10, t("Peripheral ") + "4"); - this.newPeripheral(12, t("Peripheral ") + "5"); + get stockPeripherals() { + switch (this.props.firmwareHardware) { + case "arduino": + return [ + { pin: 8, label: t("Water") }, + { pin: 9, label: t("Vacuum") }, + ]; + case "farmduino": + case "farmduino_k14": + case "farmduino_k15": + default: + return [ + { pin: 7, label: t("Lighting") }, + { pin: 8, label: t("Water") }, + { pin: 9, label: t("Vacuum") }, + { pin: 10, label: t("Peripheral ") + "4" }, + { pin: 12, label: t("Peripheral ") + "5" }, + ]; + case "express_k10": + return [ + { pin: 7, label: t("Lighting") }, + { pin: 8, label: t("Water") }, + { pin: 9, label: t("Vacuum") }, + ]; + } } render() { @@ -92,10 +111,11 @@ export class Peripherals hidden={!isEditing} className="fb-button green" type="button" - onClick={this.farmduinoPeripherals}> + onClick={() => this.stockPeripherals.map(p => + this.newPeripheral(p.pin, p.label))}> - Farmduino - + {t("Stock")} + {this.showPins()} diff --git a/frontend/controls/sensors/__tests__/index_test.tsx b/frontend/controls/sensors/__tests__/index_test.tsx index 23d13d778..569beee99 100644 --- a/frontend/controls/sensors/__tests__/index_test.tsx +++ b/frontend/controls/sensors/__tests__/index_test.tsx @@ -18,7 +18,8 @@ describe("", () => { bot, sensors: [fakeSensor1, fakeSensor2], dispatch: jest.fn(), - disabled: false + disabled: false, + firmwareHardware: undefined, }; } @@ -68,8 +69,16 @@ describe("", () => { it("adds stock sensors", () => { const p = fakeProps(); const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("stock sensors"); wrapper.setState({ isEditing: true }); clickButton(wrapper, 3, "stock sensors"); expect(p.dispatch).toHaveBeenCalledTimes(2); }); + + it("doesn't display + stock button", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("stock sensors"); + }); }); diff --git a/frontend/controls/sensors/__tests__/sensor_list_test.tsx b/frontend/controls/sensors/__tests__/sensor_list_test.tsx index 112280990..791f9fe81 100644 --- a/frontend/controls/sensors/__tests__/sensor_list_test.tsx +++ b/frontend/controls/sensors/__tests__/sensor_list_test.tsx @@ -100,4 +100,11 @@ describe("", function () { readSensorBtn.last().simulate("click"); expect(mockDevice.readPin).not.toHaveBeenCalled(); }); + + it("renders analog reading", () => { + const p = fakeProps(); + p.pins[50] && (p.pins[50].value = 600); + const wrapper = mount(); + expect(wrapper.html()).toContain("margin-left: -3.5rem"); + }); }); diff --git a/frontend/controls/sensors/index.tsx b/frontend/controls/sensors/index.tsx index 3ed6e9c82..77bb35249 100644 --- a/frontend/controls/sensors/index.tsx +++ b/frontend/controls/sensors/index.tsx @@ -10,6 +10,7 @@ import { saveAll, init } from "../../api/crud"; import { ToolTips } from "../../constants"; import { uniq } from "lodash"; import { t } from "../../i18next_wrapper"; +import { isExpressBoard } from "../../devices/components/firmware_hardware_support"; export class Sensors extends React.Component { constructor(props: SensorsProps) { @@ -79,14 +80,15 @@ export class Sensors extends React.Component { onClick={() => this.newSensor()}> - + {!isExpressBoard(this.props.firmwareHardware) && + } {this.showPins()} 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/global.scss b/frontend/css/global.scss index b719f3d1c..02ff86b62 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -1629,3 +1629,23 @@ 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; +} diff --git a/frontend/demo/demo_iframe.tsx b/frontend/demo/demo_iframe.tsx index 16681552f..1fe9181ed 100644 --- a/frontend/demo/demo_iframe.tsx +++ b/frontend/demo/demo_iframe.tsx @@ -60,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> { return
    - + 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__/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__/maybe_highlight_test.tsx b/frontend/devices/components/__tests__/maybe_highlight_test.tsx new file mode 100644 index 000000000..e748d7574 --- /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/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 cbfa9d10f..1498f5868 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -5,7 +5,7 @@ import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces"; import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui"; import { save, edit } from "../../api/crud"; import { isBotOnline } from "../must_be_online"; -import { Content } from "../../constants"; +import { Content, DeviceSetting } from "../../constants"; import { TimezoneSelector } from "../timezones/timezone_selector"; import { timezoneMismatch } from "../timezones/guess_timezone"; import { CameraSelection } from "./fbos_settings/camera_selection"; @@ -16,6 +16,7 @@ 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"; export enum ColWidth { label = 3, @@ -85,34 +86,38 @@ export class FarmbotOsSettings - - - - - - + + + + + + + + - - - - -
    - {this.maybeWarnTz()} -
    -
    - -
    - + + + + + +
    + {this.maybeWarnTz()} +
    +
    + +
    + +
    - - - - -

    - {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 f99e553f1..2540daecf 100644 --- a/frontend/devices/components/fbos_settings/auto_update_row.tsx +++ b/frontend/devices/components/fbos_settings/auto_update_row.tsx @@ -3,10 +3,11 @@ 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 { Highlight } from "../maybe_highlight"; export function AutoUpdateRow(props: AutoUpdateRowProps) { const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); @@ -18,23 +19,25 @@ export function AutoUpdateRow(props: AutoUpdateRowProps) { value={props.device.body.ota_hour} onChange={changeOtaHour(props.dispatch, props.device)} /> - - - - -

    - {t(Content.OS_AUTO_UPDATE)} -

    - - - props.dispatch(updateConfig({ - os_auto_update: !osAutoUpdate.value - }))} /> - + + + + + +

    + {t(Content.OS_AUTO_UPDATE)} +

    + + + props.dispatch(updateConfig({ + 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 f5fc2fe23..9334f2a0d 100644 --- a/frontend/devices/components/fbos_settings/board_type.tsx +++ b/frontend/devices/components/fbos_settings/board_type.tsx @@ -10,6 +10,8 @@ import { FirmwareHardwareStatus } from "./firmware_hardware_status"; import { isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; +import { Highlight } from "../maybe_highlight"; +import { DeviceSetting } from "../../../constants"; interface BoardTypeState { sending: boolean } @@ -47,30 +49,32 @@ 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..34adca94a 100644 --- a/frontend/devices/components/fbos_settings/camera_selection.tsx +++ b/frontend/devices/components/fbos_settings/camera_selection.tsx @@ -8,7 +8,8 @@ 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,23 @@ export class CameraSelection render() { return - - - - -
    - -
    - + + + + + +
    + +
    + +
    ; } } diff --git a/frontend/devices/components/fbos_settings/factory_reset_row.tsx b/frontend/devices/components/fbos_settings/factory_reset_row.tsx index 0a42a8f3a..273313502 100644 --- a/frontend/devices/components/fbos_settings/factory_reset_row.tsx +++ b/frontend/devices/components/fbos_settings/factory_reset_row.tsx @@ -1,12 +1,13 @@ 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 { ColWidth } from "../farmbot_os_settings"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function FactoryResetRow(props: FactoryResetRowProps) { const { dispatch, sourceFbosConfig, botOnline } = props; @@ -14,66 +15,72 @@ export function FactoryResetRow(props: FactoryResetRowProps) { const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {}; 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..5ef9ec8f9 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,26 @@ export interface FbosButtonRowProps { export const FbosButtonRow = (props: FbosButtonRowProps) => { return - - - - -

    - {t(props.description)} -

    - - - - + + + + + +

    + {t(props.description)} +

    + + + + +
    ; }; diff --git a/frontend/devices/components/fbos_settings/power_and_reset.tsx b/frontend/devices/components/fbos_settings/power_and_reset.tsx index e6ac2d437..1a33fa116 100644 --- a/frontend/devices/components/fbos_settings/power_and_reset.tsx +++ b/frontend/devices/components/fbos_settings/power_and_reset.tsx @@ -5,37 +5,39 @@ import { FactoryResetRow } from "./factory_reset_row"; import { PowerAndResetProps } from "./interfaces"; import { ChangeOwnershipForm } from "./change_ownership_form"; import { FbosButtonRow } from "./fbos_button_row"; -import { Content } from "../../../constants"; +import { Content, DeviceSetting } from "../../../constants"; import { reboot, powerOff, restartFirmware } from "../../actions"; import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function PowerAndReset(props: PowerAndResetProps) { const { dispatch, sourceFbosConfig, botOnline } = props; const { power_and_reset } = props.controlPanelState; - return
    + return
    {botOnline && - -

    - {t("Change Ownership")}  - -

    - -
    - } + + +

    + {t(DeviceSetting.changeOwnership)}  + +

    + +
    +
    }
    -
    ; + ; } diff --git a/frontend/devices/components/firmware_hardware_support.ts b/frontend/devices/components/firmware_hardware_support.ts index 56e111276..71756e221 100644 --- a/frontend/devices/components/firmware_hardware_support.ts +++ b/frontend/devices/components/firmware_hardware_support.ts @@ -1,4 +1,4 @@ -import { FirmwareHardware } from "farmbot"; +import { FirmwareHardware, TaggedFbosConfig } from "farmbot"; export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { const values: FirmwareHardware[] = [ @@ -10,6 +10,12 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { return !!values.includes(x as FirmwareHardware); }; +export const getFwHardwareValue = + (fbosConfig: TaggedFbosConfig | undefined) => { + const value = fbosConfig?.body.firmware_hardware; + return isFwHardwareValue(value) ? value : undefined; + }; + const TMC_BOARDS = ["express_k10", "farmduino_k15"]; const EXPRESS_BOARDS = ["express_k10"]; diff --git a/frontend/devices/components/hardware_settings.tsx b/frontend/devices/components/hardware_settings.tsx index 0ad58b3cd..1f12f68fe 100644 --- a/frontend/devices/components/hardware_settings.tsx +++ b/frontend/devices/components/hardware_settings.tsx @@ -18,10 +18,14 @@ import { FwParamExportMenu } from "./hardware_settings/export_menu"; import { t } from "../../i18next_wrapper"; import { PinBindings } from "./hardware_settings/pin_bindings"; import { ErrorHandling } from "./hardware_settings/error_handling"; +import { maybeOpenPanel } from "./maybe_highlight"; export class HardwareSettings extends React.Component { + componentDidMount = () => + this.props.dispatch(maybeOpenPanel(this.props.controlPanelState)); + render() { const { bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig, 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 f1475c44a..e7c47b59b 100644 --- a/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/calibration_row_test.tsx @@ -3,6 +3,7 @@ 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 => ({ @@ -11,7 +12,7 @@ describe("", () => { botDisconnected: false, action: jest.fn(), toolTip: "calibrate", - title: "calibrate", + title: DeviceSetting.calibration, axisTitle: "calibrate", }); 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/calibration_row.tsx b/frontend/devices/components/hardware_settings/calibration_row.tsx index ab986afec..5be2448c2 100644 --- a/frontend/devices/components/hardware_settings/calibration_row.tsx +++ b/frontend/devices/components/hardware_settings/calibration_row.tsx @@ -5,30 +5,33 @@ import { Row, Col, Help } from "../../../ui/index"; import { CalibrationRowProps } from "../interfaces"; import { t } from "../../../i18next_wrapper"; import { Position } from "@blueprintjs/core"; +import { Highlight } from "../maybe_highlight"; export function CalibrationRow(props: CalibrationRowProps) { const { hardware, botDisconnected } = props; return - - - - - {axisTrackingStatus(hardware) - .map(row => { - const { axis } = row; - const hardwareDisabled = props.type == "zero" ? false : row.disabled; - return - props.action(axis)}> - {`${t(props.axisTitle)} ${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..a7ce7940c 100644 --- a/frontend/devices/components/hardware_settings/danger_zone.tsx +++ b/frontend/devices/components/hardware_settings/danger_zone.tsx @@ -3,41 +3,45 @@ 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/encoders.tsx b/frontend/devices/components/hardware_settings/encoders.tsx index 7addae3ae..2f5d817aa 100644 --- a/frontend/devices/components/hardware_settings/encoders.tsx +++ b/frontend/devices/components/hardware_settings/encoders.tsx @@ -1,12 +1,12 @@ 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 { EncodersProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; import { isExpressBoard } from "../firmware_hardware_support"; +import { Highlight } from "../maybe_highlight"; export function Encoders(props: EncodersProps) { @@ -20,19 +20,20 @@ export function Encoders(props: EncodersProps) { }; const isExpress = isExpressBoard(firmwareHardware); - return
    + return
    {isExpress && } {!isExpress && } {!isExpress && } {!isExpress && } -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/endstops.tsx b/frontend/devices/components/hardware_settings/endstops.tsx index ffa1b5f3d..c2b55ed65 100644 --- a/frontend/devices/components/hardware_settings/endstops.tsx +++ b/frontend/devices/components/hardware_settings/endstops.tsx @@ -1,25 +1,26 @@ import * as React from "react"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { EndStopsProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; +import { Highlight } from "../maybe_highlight"; export function EndStops(props: EndStopsProps) { const { endstops } = props.controlPanelState; const { dispatch, sourceFwConfig } = props; - return
    + return
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/error_handling.tsx b/frontend/devices/components/hardware_settings/error_handling.tsx index e8d4142f6..c0dccc7a0 100644 --- a/frontend/devices/components/hardware_settings/error_handling.tsx +++ b/frontend/devices/components/hardware_settings/error_handling.tsx @@ -1,14 +1,14 @@ import * as React from "react"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; -import { ToolTips } from "../../../constants"; +import { ToolTips, DeviceSetting } from "../../../constants"; import { ErrorHandlingProps } from "../interfaces"; import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; 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"; export function ErrorHandling(props: ErrorHandlingProps) { @@ -16,15 +16,16 @@ export function ErrorHandling(props: ErrorHandlingProps) { const { dispatch, sourceFwConfig } = props; const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); - return
    + return
    -
    ; + ; } 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 1602fa351..0fa2c4374 100644 --- a/frontend/devices/components/hardware_settings/homing_and_calibration.tsx +++ b/frontend/devices/components/hardware_settings/homing_and_calibration.tsx @@ -1,6 +1,6 @@ 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 { CalibrationRow } from "./calibration_row"; import { disabledAxisMap } from "../axis_tracking_status"; @@ -13,6 +13,7 @@ import { isExpressBoard } from "../firmware_hardware_support"; import { getDevice } from "../../../device"; import { commandErr } from "../../actions"; import { CONFIG_DEFAULTS } from "farmbot/dist/config"; +import { Highlight } from "../maybe_highlight"; export function HomingAndCalibration(props: HomingAndCalibrationProps) { @@ -31,16 +32,17 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { const scale = calculateScale(sourceFwConfig); - return
    + return
    getDevice().setZero(axis) @@ -71,7 +73,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { hardware={hardware} botDisconnected={botDisconnected} /> -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/motors.tsx b/frontend/devices/components/hardware_settings/motors.tsx index b6281e751..39cdbabd6 100644 --- a/frontend/devices/components/hardware_settings/motors.tsx +++ b/frontend/devices/components/hardware_settings/motors.tsx @@ -1,18 +1,18 @@ 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 { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; -import { t } from "../../../i18next_wrapper"; import { Xyz, McuParamName } from "farmbot"; import { SourceFwConfig } from "../../interfaces"; import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props"; import { isTMCBoard } from "../firmware_hardware_support"; import { SingleSettingRow } from "./single_setting_row"; +import { Highlight } from "../maybe_highlight"; export const calculateScale = (sourceFwConfig: SourceFwConfig): Record => { @@ -35,15 +35,16 @@ export function Motors(props: MotorsProps) { const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); const scale = calculateScale(sourceFwConfig); - return
    + return
    {isTMCBoard(firmwareHardware) && } -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/pin_bindings.tsx b/frontend/devices/components/hardware_settings/pin_bindings.tsx index e7e716c0c..fa258ce42 100644 --- a/frontend/devices/components/hardware_settings/pin_bindings.tsx +++ b/frontend/devices/components/hardware_settings/pin_bindings.tsx @@ -3,20 +3,23 @@ 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 } = props; - return
    + return
    -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/pin_guard.tsx b/frontend/devices/components/hardware_settings/pin_guard.tsx index 9bd49c83b..1886382fc 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
    @@ -79,5 +81,5 @@ export function PinGuard(props: PinGuardProps) { resources={resources} sourceFwConfig={sourceFwConfig} /> -
    ; + ; } diff --git a/frontend/devices/components/hardware_settings/single_setting_row.tsx b/frontend/devices/components/hardware_settings/single_setting_row.tsx index 02075c813..0ab1c4bba 100644 --- a/frontend/devices/components/hardware_settings/single_setting_row.tsx +++ b/frontend/devices/components/hardware_settings/single_setting_row.tsx @@ -1,20 +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 }: { - label: string, - tooltip: string, - children: React.ReactChild, - settingType: "button" | "input", - }) => + ({ label, tooltip, settingType, children }: SingleSettingRowProps) => - - - - - {settingType === "button" - ? {children} - : {children}} + + + + + + {settingType === "button" + ? {children} + : {children}} + ; diff --git a/frontend/devices/components/interfaces.ts b/frontend/devices/components/interfaces.ts index a76dd30ec..4142d3284 100644 --- a/frontend/devices/components/interfaces.ts +++ b/frontend/devices/components/interfaces.ts @@ -6,6 +6,7 @@ import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist"; import { IntegerSize } from "../../util"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { ResourceIndex } from "../../resources/interfaces"; +import { DeviceSetting } from "../../constants"; export interface ZeroRowProps { botDisconnected: boolean; @@ -25,7 +26,7 @@ export interface BooleanMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; tooltip: string; - name: string; + label: DeviceSetting; x: McuParamName; y: McuParamName; z: McuParamName; @@ -41,7 +42,7 @@ export interface CalibrationRowProps { botDisconnected: boolean; action(axis: Axis): void; toolTip: string; - title: string; + title: DeviceSetting; axisTitle: string; } @@ -49,7 +50,7 @@ export interface NumericMCUInputGroupProps { sourceFwConfig: SourceFwConfig; dispatch: Function; tooltip: string; - name: string; + label: DeviceSetting; x: McuParamName; xScale?: number; y: McuParamName; diff --git a/frontend/devices/components/maybe_highlight.tsx b/frontend/devices/components/maybe_highlight.tsx new file mode 100644 index 000000000..eab806b0f --- /dev/null +++ b/frontend/devices/components/maybe_highlight.tsx @@ -0,0 +1,162 @@ +import * as React from "react"; +import { ControlPanelState } from "../interfaces"; +import { toggleControlPanel } from "../actions"; +import { urlFriendly } from "../../util"; +import { DeviceSetting } from "../../constants"; + +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, +]; + +/** 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"); + +/** 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); + +/** 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]).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/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/interfaces.ts b/frontend/devices/interfaces.ts index 767ea8bd5..c74ff5e9f 100644 --- a/frontend/devices/interfaces.ts +++ b/frontend/devices/interfaces.ts @@ -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 { @@ -248,8 +250,8 @@ export interface ControlPanelState { encoders: boolean; endstops: boolean; error_handling: boolean; - pin_bindings: boolean; - danger_zone: boolean; - power_and_reset: boolean; pin_guard: boolean; + danger_zone: boolean; + pin_bindings: boolean; + power_and_reset: boolean; } diff --git a/frontend/devices/pin_bindings/list_and_label_support.tsx b/frontend/devices/pin_bindings/list_and_label_support.tsx index 09ea49a71..ddc7be7c6 100644 --- a/frontend/devices/pin_bindings/list_and_label_support.tsx +++ b/frontend/devices/pin_bindings/list_and_label_support.tsx @@ -89,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/external_urls.ts b/frontend/external_urls.ts index 47e123e98..6caa90822 100644 --- a/frontend/external_urls.ts +++ b/frontend/external_urls.ts @@ -20,10 +20,11 @@ export namespace ExternalUrl { const GITHUB_API = "https://api.github.com"; const OPENFARM = "https://openfarm.cc"; const SOFTWARE_DOCS = "https://software.farm.bot"; - const FORUM = "http://forum.farmbot.org"; + 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}`; + const FBOS_RAW = + `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/staging`; export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`; export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`; @@ -31,8 +32,7 @@ export namespace ExternalUrl { `${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`; export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`; - export const webAppRepo = - `${GITHUB}/${Org.FarmBot}/${FarmBotRepo.FarmBotWebApp}`; + export const webAppRepo = `${gitHubFarmBot}/${FarmBotRepo.FarmBotWebApp}`; export const softwareDocs = `${SOFTWARE_DOCS}/docs`; export const softwareForum = `${FORUM}/c/software`; @@ -43,7 +43,7 @@ export namespace ExternalUrl { export const newCrop = `${OPENFARM}/en/crops/new`; } - export namespace Videos { + 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/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 90c76eafb..063c5ccbf 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -386,6 +386,7 @@ export class GardenMap extends visible={!!this.props.showPlants} plants={this.props.plants} currentPlant={this.getPlant()} + hoveredPlant={this.props.hoveredPlant} dragging={!!this.state.isDragging} editing={this.isEditing} boxSelected={this.props.designer.selectedPlants} diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts index 8b647c2fc..aca542188 100644 --- a/frontend/farm_designer/map/interfaces.ts +++ b/frontend/farm_designer/map/interfaces.ts @@ -14,6 +14,7 @@ export type TaggedPlant = TaggedPlantPointer | TaggedPlantTemplate; export interface PlantLayerProps { plants: TaggedPlant[]; currentPlant: TaggedPlant | undefined; + hoveredPlant: TaggedPlant | undefined; dragging: boolean; editing: boolean; visible: boolean; @@ -57,12 +58,14 @@ export interface GardenPlantProps { dispatch: Function; plant: Readonly; selected: boolean; + current: boolean; editing: boolean; dragging: boolean; zoomLvl: number; activeDragXY: BotPosition | undefined; uuid: string; animate: boolean; + hovered: boolean; } export interface GardenPlantState { diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx index b17fa5345..d523f8f1e 100644 --- a/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx +++ b/frontend/farm_designer/map/layers/farmbot/__tests__/bot_figure_test.tsx @@ -15,17 +15,19 @@ describe("", () => { plantAreaOffset: { x: 100, y: 100 }, }); + const EXPECTED_MOTORS_OPACITY = 0.5; + it.each<[ string, BotOriginQuadrant, Record<"x" | "y", number>, boolean, number ]>([ - ["motors", 1, { x: 3000, y: 0 }, false, 0.75], - ["motors", 2, { x: 0, y: 0 }, false, 0.75], - ["motors", 3, { x: 0, y: 1500 }, false, 0.75], - ["motors", 4, { x: 3000, y: 1500 }, false, 0.75], - ["motors", 1, { x: 0, y: 1500 }, true, 0.75], - ["motors", 2, { x: 0, y: 0 }, true, 0.75], - ["motors", 3, { x: 3000, y: 0 }, true, 0.75], - ["motors", 4, { x: 3000, y: 1500 }, true, 0.75], + ["motors", 1, { x: 3000, y: 0 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 2, { x: 0, y: 0 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 3, { x: 0, y: 1500 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 4, { x: 3000, y: 1500 }, false, EXPECTED_MOTORS_OPACITY], + ["motors", 1, { x: 0, y: 1500 }, true, EXPECTED_MOTORS_OPACITY], + ["motors", 2, { x: 0, y: 0 }, true, EXPECTED_MOTORS_OPACITY], + ["motors", 3, { x: 3000, y: 0 }, true, EXPECTED_MOTORS_OPACITY], + ["motors", 4, { x: 3000, y: 1500 }, true, EXPECTED_MOTORS_OPACITY], ["encoders", 2, { x: 0, y: 0 }, false, 0.25], ])("shows %s in correct location for quadrant %i", (name, quadrant, expected, xySwap, opacity) => { diff --git a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx index bf07f7de2..59d699041 100644 --- a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx +++ b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx @@ -31,7 +31,7 @@ export class BotFigure extends const positionQ = transformXY( (position.x || 0), (position.y || 0), mapTransformProps); const color = eStopStatus ? Color.virtualRed : Color.darkGray; - const opacity = name.includes("encoder") ? 0.25 : 0.75; + const opacity = name.includes("encoder") ? 0.25 : 0.5; return ", () => { return { mapTransformProps: fakeMapTransformProps(), plant: fakePlant(), + current: false, selected: false, editing: false, dragging: false, @@ -21,6 +22,7 @@ describe("", () => { activeDragXY: { x: undefined, y: undefined, z: undefined }, uuid: "plantUuid", animate: false, + hovered: false, }; } @@ -31,6 +33,8 @@ describe("", () => { const wrapper = shallow(); expect(wrapper.find("image").length).toEqual(1); expect(wrapper.find("image").props().opacity).toEqual(1); + expect(wrapper.find("image").props().visibility).toEqual("visible"); + expect(wrapper.find("image").props().opacity).toEqual(1.0); expect(wrapper.find("text").length).toEqual(0); expect(wrapper.find("rect").length).toBeLessThanOrEqual(1); expect(wrapper.find("use").length).toEqual(0); @@ -88,4 +92,21 @@ describe("", () => { expect(wrapper.find(".plant-indicator").length).toEqual(1); expect(wrapper.find("Circle").length).toEqual(1); }); + + it("doesn't render indicator circle twice", () => { + const p = fakeProps(); + p.selected = true; + p.hovered = true; + const wrapper = shallow(); + expect(wrapper.find(".plant-indicator").length).toEqual(0); + expect(wrapper.find("Circle").length).toEqual(0); + }); + + it("renders while dragging", () => { + const p = fakeProps(); + p.dragging = true; + const wrapper = shallow(); + expect(wrapper.find("image").props().visibility).toEqual("hidden"); + expect(wrapper.find("image").props().opacity).toEqual(0.4); + }); }); diff --git a/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx b/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx index b981856b5..09dc7f1c1 100644 --- a/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx +++ b/frontend/farm_designer/map/layers/plants/__tests__/plant_layer_test.tsx @@ -8,11 +8,13 @@ import { PlantLayer } from "../plant_layer"; import { fakePlant, fakePlantTemplate } from "../../../../../__test_support__/fake_state/resources"; -import { PlantLayerProps, GardenPlantProps } from "../../../interfaces"; +import { PlantLayerProps } from "../../../interfaces"; import { fakeMapTransformProps } from "../../../../../__test_support__/map_transform_props"; import { svgMount } from "../../../../../__test_support__/svg_mount"; +import { shallow } from "enzyme"; +import { GardenPlant } from "../garden_plant"; describe("", () => { const fakeProps = (): PlantLayerProps => ({ @@ -28,6 +30,7 @@ describe("", () => { zoomLvl: 1, activeDragXY: { x: undefined, y: undefined, z: undefined }, animate: true, + hoveredPlant: undefined, }); it("shows plants", () => { @@ -88,14 +91,14 @@ describe("", () => { .toEqual("/app/designer/gardens/templates/5"); }); - it("has selected plant", () => { + it("has hovered plant", () => { mockPath = "/app/designer/plants"; const p = fakeProps(); const plant = fakePlant(); p.plants = [plant]; - p.currentPlant = plant; - const wrapper = svgMount(); - expect(wrapper.find("GardenPlant").props().selected).toEqual(true); + p.hoveredPlant = plant; + const wrapper = shallow(); + expect(wrapper.find(GardenPlant).props().hovered).toEqual(true); }); it("has plant selected by selection box", () => { @@ -105,8 +108,7 @@ describe("", () => { p.plants = [plant]; p.boxSelected = [plant.uuid]; const wrapper = svgMount(); - expect((wrapper.find("GardenPlant").props() as GardenPlantProps).selected) - .toEqual(true); + expect(wrapper.find("GardenPlant").props().selected).toEqual(true); }); it("allows clicking of unsaved plants", () => { diff --git a/frontend/farm_designer/map/layers/plants/garden_plant.tsx b/frontend/farm_designer/map/layers/plants/garden_plant.tsx index cc3a2b112..8bdec572e 100644 --- a/frontend/farm_designer/map/layers/plants/garden_plant.tsx +++ b/frontend/farm_designer/map/layers/plants/garden_plant.tsx @@ -36,22 +36,22 @@ export class GardenPlant extends }; get radius() { - const { selected, plant } = this.props; + const { plant } = this.props; const { hover } = this.state; const { radius } = plant.body; - return (hover && !selected) ? radius * 1.1 : radius; + return hover ? radius * 1.1 : radius; } render() { - const { selected, dragging, plant, mapTransformProps, - activeDragXY, zoomLvl, animate, editing } = this.props; + const { current, selected, dragging, plant, mapTransformProps, + activeDragXY, zoomLvl, animate, editing, hovered } = this.props; const { id, radius, x, y } = plant.body; const { icon } = this.state; const { qx, qy } = transformXY(round(x), round(y), mapTransformProps); const alpha = dragging ? 0.4 : 1.0; const className = [ - "plant-image", `is-chosen-${selected}`, animate ? "animate" : "" + "plant-image", `is-chosen-${current || selected}`, animate ? "animate" : "" ].join(" "); return @@ -65,7 +65,7 @@ export class GardenPlant extends fill={Color.soilCloud} fillOpacity={0} />} - {selected && !editing && + {(current || selected) && !editing && !hovered && {visible && plants.map(p => { - const selected = !!(p.uuid === currentPlant?.uuid); + const current = p.uuid === currentPlant?.uuid; + const hovered = p.uuid === hoveredPlant?.uuid; const selectedByBox = !!boxSelected?.includes(p.uuid); const selectedByGroup = groupSelected.includes(p.uuid); const plantCategory = unpackUUID(p.uuid).kind === "PlantTemplate" @@ -33,12 +35,14 @@ export function PlantLayer(props: PlantLayerProps) { uuid={p.uuid} mapTransformProps={mapTransformProps} plant={p} - selected={selected || selectedByBox || selectedByGroup} + selected={selectedByBox || selectedByGroup} + current={current} editing={editing} - dragging={selected && dragging && editing} + dragging={current && dragging && editing} dispatch={dispatch} zoomLvl={zoomLvl} activeDragXY={activeDragXY} + hovered={hovered} animate={animate} />; const wrapperProps = { className: "plant-link-wrapper", diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index 75451aefc..f47a485eb 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -66,12 +66,12 @@ describe("", () => { expect(wrapper.find("text").props().dx).toEqual(-40); }); - it("displays 'no tool'", () => { + it("displays 'empty'", () => { const p = fakeProps(); p.slot.tool = undefined; p.hoveredToolSlot = p.slot.toolSlot.uuid; const wrapper = svgMount(); - expect(wrapper.find("text").text()).toEqual("no tool"); + expect(wrapper.find("text").text()).toEqual("empty"); expect(wrapper.find("text").props().dx).toEqual(40); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx index ddb889b00..e34858e6d 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_point.tsx @@ -32,7 +32,7 @@ export const ToolSlotPoint = (props: TSPProps) => { const { quadrant, xySwap } = mapTransformProps; const xPosition = gantry_mounted ? (botPositionX || 0) : x; const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps); - const toolName = props.slot.tool ? props.slot.tool.body.name : "no tool"; + const toolName = props.slot.tool ? props.slot.tool.body.name : "empty"; const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot; const toolProps = { x: qx, diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx index 2d7909c5b..1054d9806 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx @@ -40,6 +40,7 @@ describe("", () => { allPoints: [], shouldDisplay: () => true, slugs: [], + hovered: undefined, }; }; diff --git a/frontend/farm_designer/point_groups/group_detail.tsx b/frontend/farm_designer/point_groups/group_detail.tsx index c9bf393d9..506c5a0ed 100644 --- a/frontend/farm_designer/point_groups/group_detail.tsx +++ b/frontend/farm_designer/point_groups/group_detail.tsx @@ -10,6 +10,7 @@ import { GroupDetailActive } from "./group_detail_active"; import { ShouldDisplay } from "../../devices/interfaces"; import { getShouldDisplayFn } from "../../farmware/state_to_props"; import { uniq } from "lodash"; +import { UUID } from "../../resources/interfaces"; interface GroupDetailProps { dispatch: Function; @@ -17,6 +18,7 @@ interface GroupDetailProps { allPoints: TaggedPoint[]; shouldDisplay: ShouldDisplay; slugs: string[]; + hovered: UUID | undefined; } /** Find a group from a URL-provided ID. */ @@ -35,6 +37,7 @@ function mapStateToProps(props: Everything): GroupDetailProps { shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot), slugs: uniq(selectAllPlantPointers(props.resources.index) .map(p => p.body.openfarm_slug)), + hovered: props.resources.consumers.farm_designer.hoveredPlantListItem, }; } diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index b56ed3e71..5c50e92a3 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -18,6 +18,7 @@ import { GroupCriteria, GroupPointCountBreakdown, pointsSelectedByGroup } from "./criteria"; import { Content } from "../../constants"; +import { UUID } from "../../resources/interfaces"; export interface GroupDetailActiveProps { dispatch: Function; @@ -25,6 +26,7 @@ export interface GroupDetailActiveProps { allPoints: TaggedPoint[]; shouldDisplay: ShouldDisplay; slugs: string[]; + hovered: UUID | undefined; } type State = { timerId?: ReturnType }; @@ -47,7 +49,7 @@ export class GroupDetailActive return sortedPoints.map(point => { return ; diff --git a/frontend/farm_designer/point_groups/group_order_visual.tsx b/frontend/farm_designer/point_groups/group_order_visual.tsx index 7f4aead09..cdf9f21cc 100644 --- a/frontend/farm_designer/point_groups/group_order_visual.tsx +++ b/frontend/farm_designer/point_groups/group_order_visual.tsx @@ -36,7 +36,7 @@ export interface PointsPathLineProps { } export const PointsPathLine = (props: PointsPathLineProps) => - diff --git a/frontend/farm_designer/point_groups/point_group_item.tsx b/frontend/farm_designer/point_groups/point_group_item.tsx index cee43c163..830c25bbf 100644 --- a/frontend/farm_designer/point_groups/point_group_item.tsx +++ b/frontend/farm_designer/point_groups/point_group_item.tsx @@ -93,6 +93,7 @@ export class PointGroupItem style={{ border: this.criteriaIcon ? "1px solid gray" : "none", borderRadius: "5px", + background: this.props.hovered ? "lightgray" : "none", }} src={DEFAULT_ICON} onLoad={this.maybeGetCachedIcon} diff --git a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx index 616f081d9..76a5c991b 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx @@ -22,6 +22,7 @@ import { import { init, save, edit, destroy } from "../../../api/crud"; import { history } from "../../../history"; import { SpecialStatus } from "farmbot"; +import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; describe("", () => { const fakeProps = (): AddToolSlotProps => ({ @@ -30,14 +31,20 @@ describe("", () => { botPosition: { x: undefined, y: undefined, z: undefined }, dispatch: jest.fn(), findToolSlot: fakeToolSlot, + firmwareHardware: undefined, }); it("renders", () => { const wrapper = mount(); - ["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone", + ["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", "change slot direction", "use current location", "gantry-mounted" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); - expect(init).toHaveBeenCalled(); + expect(init).toHaveBeenCalledWith("Point", { + pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + x: 0, y: 0, z: 0, tool_id: undefined, + pullout_direction: ToolPulloutDirection.NONE, + gantry_mounted: false, + }); }); it("renders while loading", () => { @@ -102,6 +109,19 @@ describe("", () => { const wrapper = mount(); expect(wrapper.instance().tool).toEqual(undefined); }); + + it("renders for express bots", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("tool"); + expect(init).toHaveBeenCalledWith("Point", { + pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + x: 0, y: 0, z: 0, tool_id: undefined, + pullout_direction: ToolPulloutDirection.NONE, + gantry_mounted: true, + }); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx index e982afc55..c8d06b478 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx @@ -11,11 +11,13 @@ import { fakeState } from "../../../__test_support__/fake_state"; import { SaveBtn } from "../../../ui"; import { initSave } from "../../../api/crud"; import { history } from "../../../history"; -import { error } from "../../../toast/toast"; +import { FirmwareHardware } from "farmbot"; describe("", () => { const fakeProps = (): AddToolProps => ({ dispatch: jest.fn(), + existingToolNames: [], + firmwareHardware: undefined, }); it("renders", () => { @@ -38,19 +40,29 @@ describe("", () => { expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" }); }); - it("doesn't add stock tools", () => { - const wrapper = mount(); + it.each<[FirmwareHardware, number]>([ + ["arduino", 6], + ["farmduino", 6], + ["farmduino_k14", 6], + ["farmduino_k15", 8], + ["express_k10", 2], + ])("adds peripherals: %s", (firmware, expectedAdds) => { + const p = fakeProps(); + p.firmwareHardware = firmware; + const wrapper = mount(); wrapper.find("button").last().simulate("click"); - expect(error).toHaveBeenCalledWith("Please choose a FarmBot model."); - expect(initSave).not.toHaveBeenCalledTimes(6); - expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools"); + expect(initSave).toHaveBeenCalledTimes(expectedAdds); + expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); - it("adds stock tools", () => { - const wrapper = mount(); + it("doesn't add stock tools twice", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + p.existingToolNames = ["Seed Trough 1"]; + const wrapper = mount(); wrapper.setState({ model: "express" }); wrapper.find("button").last().simulate("click"); - expect(initSave).toHaveBeenCalledTimes(2); + expect(initSave).toHaveBeenCalledTimes(1); expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); }); diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx index f251c62e0..7057f20dd 100644 --- a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx @@ -28,6 +28,7 @@ describe("", () => { findTool: jest.fn(), botPosition: { x: undefined, y: undefined, z: undefined }, dispatch: jest.fn(), + firmwareHardware: undefined, }); it("redirects", () => { @@ -39,7 +40,7 @@ describe("", () => { const p = fakeProps(); p.findToolSlot = () => fakeToolSlot(); const wrapper = mount(); - ["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "toolnone", + ["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", "change slot direction", "use current location", "gantry-mounted" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); diff --git a/frontend/farm_designer/tools/__tests__/index_test.tsx b/frontend/farm_designer/tools/__tests__/index_test.tsx index 39e83f38a..c4584f781 100644 --- a/frontend/farm_designer/tools/__tests__/index_test.tsx +++ b/frontend/farm_designer/tools/__tests__/index_test.tsx @@ -39,6 +39,7 @@ describe("", () => { bot, botToMqttStatus: "down", hoveredToolSlot: undefined, + firmwareHardware: undefined, }); it("renders with no tools", () => { @@ -64,7 +65,7 @@ describe("", () => { p.toolSlots[1].body.y = 2; const wrapper = mount(); [ - "foo", "my tool", "unnamed tool", "(1, 0, 0)", "unknown", "(gantry, 2, 0)" + "foo", "my tool", "unnamed", "(1, 0, 0)", "unknown", "(gantry, 2, 0)" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); @@ -158,6 +159,7 @@ describe("", () => { p.bot.hardware.informational_settings.sync_status = "synced"; p.botToMqttStatus = "up"; const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("mounted tool"); wrapper.find(".yellow").first().simulate("click"); expect(mockDevice.readPin).toHaveBeenCalledWith({ label: "pin63", pin_mode: 0, pin_number: 63 @@ -173,6 +175,13 @@ describe("", () => { expect(mockDevice.readPin).not.toHaveBeenCalled(); expect(error).toHaveBeenCalledWith(Content.NOT_AVAILABLE_WHEN_OFFLINE); }); + + it("doesn't display mounted tool on express models", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("mounted tool"); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx index 7a58f2c93..861c26b3f 100644 --- a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx +++ b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx @@ -136,6 +136,7 @@ describe("", () => { tools: [], selectedTool: undefined, onChange: jest.fn(), + isExpress: false, }); it("renders", () => { @@ -149,6 +150,13 @@ describe("", () => { const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("foo"); }); + + it("renders for express bots", () => { + const p = fakeProps(); + p.isExpress = true; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("seed container"); + }); }); describe("", () => { diff --git a/frontend/farm_designer/tools/add_tool.tsx b/frontend/farm_designer/tools/add_tool.tsx index 2eba033c9..0eb08d86d 100644 --- a/frontend/farm_designer/tools/add_tool.tsx +++ b/frontend/farm_designer/tools/add_tool.tsx @@ -5,36 +5,37 @@ import { } from "../designer_panel"; import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; -import { SaveBtn, FBSelect, DropDownItem } from "../../ui"; -import { SpecialStatus } from "farmbot"; +import { SaveBtn } from "../../ui"; +import { SpecialStatus, FirmwareHardware } from "farmbot"; import { initSave } from "../../api/crud"; import { Panel } from "../panel_header"; import { history } from "../../history"; -import { error } from "../../toast/toast"; - -enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" } - -const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ - [Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 }, - [Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 }, - [Model.express]: { label: t("Express"), value: Model.express }, -}); +import { selectAllTools } from "../../resources/selectors"; +import { betterCompact } from "../../util"; +import { + isExpressBoard, getFwHardwareValue +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; export interface AddToolProps { dispatch: Function; + existingToolNames: string[]; + firmwareHardware: FirmwareHardware | undefined; } export interface AddToolState { toolName: string; - model: Model | undefined; } export const mapStateToProps = (props: Everything): AddToolProps => ({ dispatch: props.dispatch, + existingToolNames: betterCompact(selectAllTools(props.resources.index) + .map(tool => tool.body.name)), + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); export class RawAddTool extends React.Component { - state: AddToolState = { toolName: "", model: undefined }; + state: AddToolState = { toolName: "" }; newTool = (name: string) => { this.props.dispatch(initSave("Tool", { name })); @@ -45,9 +46,12 @@ export class RawAddTool extends React.Component { history.push("/app/designer/tools"); } - stockToolNames = (model: Model) => { - switch (model) { - case Model.genesis14: + stockToolNames = () => { + switch (this.props.firmwareHardware) { + case "arduino": + case "farmduino": + case "farmduino_k14": + default: return [ t("Seeder"), t("Watering Nozzle"), @@ -56,7 +60,7 @@ export class RawAddTool extends React.Component { t("Seed Bin"), t("Seed Tray"), ]; - case Model.genesis15: + case "farmduino_k15": return [ t("Seeder"), t("Watering Nozzle"), @@ -67,7 +71,7 @@ export class RawAddTool extends React.Component { t("Seed Trough 1"), t("Seed Trough 2"), ]; - case Model.express: + case "express_k10": return [ t("Seed Trough 1"), t("Seed Trough 2"), @@ -77,31 +81,20 @@ export class RawAddTool extends React.Component { AddStockTools = () =>
    - - this.setState({ model: ddi.value as Model })} - /> - {this.state.model && -
      - {this.stockToolNames(this.state.model).map(n =>
    • {n}
    • )} -
    } + +
      + {this.stockToolNames().map(n =>
    • {n}
    • )} +
    @@ -110,12 +103,14 @@ export class RawAddTool extends React.Component { return
    - + this.setState({ toolName: e.currentTarget.value })} /> diff --git a/frontend/farm_designer/tools/add_tool_slot.tsx b/frontend/farm_designer/tools/add_tool_slot.tsx index 23de61eb3..ffd4062d8 100644 --- a/frontend/farm_designer/tools/add_tool_slot.tsx +++ b/frontend/farm_designer/tools/add_tool_slot.tsx @@ -6,7 +6,9 @@ import { import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { SaveBtn } from "../../ui"; -import { SpecialStatus, TaggedTool, TaggedToolSlotPointer } from "farmbot"; +import { + SpecialStatus, TaggedTool, TaggedToolSlotPointer, FirmwareHardware +} from "farmbot"; import { init, save, edit, destroy } from "../../api/crud"; import { Panel } from "../panel_header"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; @@ -18,6 +20,10 @@ import { validBotLocationData } from "../../util"; import { history } from "../../history"; import { SlotEditRows } from "./tool_slot_edit_components"; import { UUID } from "../../resources/interfaces"; +import { + isExpressBoard, getFwHardwareValue +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; export interface AddToolSlotProps { tools: TaggedTool[]; @@ -25,6 +31,7 @@ export interface AddToolSlotProps { botPosition: BotPosition; findTool(id: number): TaggedTool | undefined; findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined; + firmwareHardware: FirmwareHardware | undefined; } export interface AddToolSlotState { @@ -38,6 +45,7 @@ export const mapStateToProps = (props: Everything): AddToolSlotProps => ({ findTool: (id: number) => maybeFindToolById(props.resources.index, id), findToolSlot: (uuid: UUID | undefined) => maybeGetToolSlot(props.resources.index, uuid), + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); export class RawAddToolSlot @@ -48,7 +56,8 @@ export class RawAddToolSlot const action = init("Point", { pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, x: 0, y: 0, z: 0, tool_id: undefined, - pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: false + pullout_direction: ToolPulloutDirection.NONE, + gantry_mounted: isExpressBoard(this.props.firmwareHardware) ? true : false, }); this.setState({ uuid: action.payload.uuid }); this.props.dispatch(action); @@ -57,7 +66,7 @@ export class RawAddToolSlot componentWillUnmount() { if (this.state.uuid && this.toolSlot && this.toolSlot.specialStatus == SpecialStatus.DIRTY) { - confirm(t("Save new tool?")) + confirm(t("Save new slot?")) ? this.props.dispatch(save(this.state.uuid)) : this.props.dispatch(destroy(this.state.uuid, true)); } @@ -86,12 +95,15 @@ export class RawAddToolSlot return {this.toolSlot ? { backTo={"/app/designer/tools"} panel={Panel.Tools} /> - + this.setState({ toolName: e.currentTarget.value })} /> diff --git a/frontend/farm_designer/tools/edit_tool_slot.tsx b/frontend/farm_designer/tools/edit_tool_slot.tsx index 21a114be3..210b283d3 100644 --- a/frontend/farm_designer/tools/edit_tool_slot.tsx +++ b/frontend/farm_designer/tools/edit_tool_slot.tsx @@ -6,7 +6,7 @@ import { import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { getPathArray } from "../../history"; -import { TaggedToolSlotPointer, TaggedTool } from "farmbot"; +import { TaggedToolSlotPointer, TaggedTool, FirmwareHardware } from "farmbot"; import { edit, save, destroy } from "../../api/crud"; import { history } from "../../history"; import { Panel } from "../panel_header"; @@ -17,6 +17,10 @@ import { BotPosition } from "../../devices/interfaces"; import { validBotLocationData } from "../../util"; import { SlotEditRows } from "./tool_slot_edit_components"; import { moveAbs } from "../../devices/actions"; +import { + getFwHardwareValue, isExpressBoard +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; export interface EditToolSlotProps { findToolSlot(id: string): TaggedToolSlotPointer | undefined; @@ -24,6 +28,7 @@ export interface EditToolSlotProps { findTool(id: number): TaggedTool | undefined; dispatch: Function; botPosition: BotPosition; + firmwareHardware: FirmwareHardware | undefined; } export const mapStateToProps = (props: Everything): EditToolSlotProps => ({ @@ -33,6 +38,7 @@ export const mapStateToProps = (props: Everything): EditToolSlotProps => ({ findTool: (id: number) => maybeFindToolById(props.resources.index, id), dispatch: props.dispatch, botPosition: validBotLocationData(props.bot.hardware.location_data).position, + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); export class RawEditToolSlot extends React.Component { @@ -64,6 +70,7 @@ export class RawEditToolSlot extends React.Component { panel={Panel.Tools} /> ({ bot: props.bot, botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]), hoveredToolSlot: props.resources.consumers.farm_designer.hoveredToolSlot, + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), }); const toolStatus = (value: number | undefined): string => { @@ -108,6 +115,8 @@ export class RawTools extends React.Component { this.props.botToMqttStatus); } + get isExpress() { return isExpressBoard(this.props.firmwareHardware); } + MountedToolInfo = () =>
    @@ -141,10 +150,10 @@ export class RawTools extends React.Component { ToolSlots = () =>
    - +
    - +
    @@ -162,10 +171,10 @@ export class RawTools extends React.Component { Tools = () =>
    - +
    - +
    @@ -176,9 +185,32 @@ export class RawTools extends React.Component { .map(tool => )} + toolName={tool.body.name || t("Unnamed")} />)}
    + get strings() { + return { + placeholder: this.isExpress + ? t("Search your seed containers...") + : t("Search your tools..."), + titleText: this.isExpress + ? t("Add a seed container") + : t("Add a tool or seed container"), + emptyStateText: this.isExpress + ? Content.NO_SEED_CONTAINERS + : Content.NO_TOOLS, + tools: this.isExpress + ? t("seed containers") + : t("tools and seed containers"), + toolSlots: this.isExpress + ? t("seed container slots") + : t("tool slots"), + addSlot: this.isExpress + ? t("Add slot") + : t("Add tool slot"), + }; + } + render() { const panelName = "tools"; const hasTools = this.props.tools.length > 0; @@ -187,18 +219,19 @@ export class RawTools extends React.Component { + title={!hasTools ? this.strings.titleText : undefined}> + placeholder={this.strings.placeholder} /> - + {!this.isExpress && + } @@ -223,7 +256,7 @@ const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { onMouseLeave={() => props.dispatch(setToolHover(undefined))}> -

    {props.getToolName(tool_id) || t("No tool")}

    +

    {props.getToolName(tool_id) || t("Empty")}

    diff --git a/frontend/farm_designer/tools/tool_slot_edit_components.tsx b/frontend/farm_designer/tools/tool_slot_edit_components.tsx index 9a2fc0f86..c1e58ccd0 100644 --- a/frontend/farm_designer/tools/tool_slot_edit_components.tsx +++ b/frontend/farm_designer/tools/tool_slot_edit_components.tsx @@ -97,13 +97,18 @@ export interface ToolInputRowProps { tools: TaggedTool[]; selectedTool: TaggedTool | undefined; onChange(update: { tool_id: number }): void; + isExpress: boolean; } export const ToolInputRow = (props: ToolInputRowProps) =>

    - + ): void; + isExpress: boolean; } export const SlotEditRows = (props: SlotEditRowsProps) => @@ -153,16 +159,19 @@ export const SlotEditRows = (props: SlotEditRowsProps) => gantryMounted={props.toolSlot.body.gantry_mounted} onChange={props.updateToolSlot} /> - + {!props.toolSlot.body.gantry_mounted && + } - + {!props.isExpress && + }
    ; diff --git a/frontend/folders/actions.ts b/frontend/folders/actions.ts index 449501dfb..eac3583da 100644 --- a/frontend/folders/actions.ts +++ b/frontend/folders/actions.ts @@ -40,11 +40,11 @@ export const setFolderName = (id: number, name: string) => { 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/front_page/laptop_splash.tsx b/frontend/front_page/laptop_splash.tsx index bc9ddb94f..dc81416fa 100644 --- a/frontend/front_page/laptop_splash.tsx +++ b/frontend/front_page/laptop_splash.tsx @@ -7,7 +7,7 @@ export const LaptopSplash = ({ className }: { className: string }) =>
    diff --git a/frontend/help/__tests__/tour_test.tsx b/frontend/help/__tests__/tour_test.tsx index a3c5908ad..38be5e14e 100644 --- a/frontend/help/__tests__/tour_test.tsx +++ b/frontend/help/__tests__/tour_test.tsx @@ -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..98be2687a 100644 --- a/frontend/help/__tests__/tours_test.ts +++ b/frontend/help/__tests__/tours_test.ts @@ -1,7 +1,22 @@ jest.mock("../../history", () => ({ history: { push: jest.fn() } })); -import { tourPageNavigation } from "../tours"; +let mockDev = false; +jest.mock("../../account/dev/dev_support", () => ({ + DevSettings: { + futureFeaturesEnabled: () => mockDev, + } +})); + +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 +35,47 @@ 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); + mockDev = false; + 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); + mockDev = false; + mockState.resources = buildResourceIndex([]); + expect(getTitles()).toContain("Add tools and tool 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"); + }); + + it("includes correct tour steps", () => { + mockDev = true; + const targets = + Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target); + expect(targets).not.toContain(".tools"); + expect(targets).toContain(".tool-list"); + expect(targets).toContain(".toolbay-list"); + }); }); diff --git a/frontend/help/tour.tsx b/frontend/help/tour.tsx index ece8f5064..3f2e6408f 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"), @@ -65,15 +66,17 @@ export class Tour extends React.Component { return step; }); return
    - + + +
    ; } } diff --git a/frontend/help/tours.ts b/frontend/help/tours.ts index f01fe0dec..fc4339da5 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -2,6 +2,13 @@ import { history } from "../history"; import { Step as TourStep } from "react-joyride"; import { TourContent } from "../constants"; import { t } from "../i18next_wrapper"; +import { DevSettings } from "../account/dev/dev_support"; +import { selectAllTools } from "../resources/selectors"; +import { store } from "../redux/store"; +import { getFbosConfig } from "../resources/getters"; +import { + isExpressBoard, getFwHardwareValue +} from "../devices/components/firmware_hardware_support"; export enum Tours { gettingStarted = "gettingStarted", @@ -15,70 +22,105 @@ export const tourNames = () => [ { name: Tours.funStuff, description: t("find new features") }, ]; +const hasTools = () => + selectAllTools(store.getState().resources.index).length > 0; + +const isExpress = () => + isExpressBoard(getFwHardwareValue( + getFbosConfig(store.getState().resources.index))); + +const toolsStep = () => hasTools() + ? [{ + target: ".tools", + content: isExpress() + ? t(TourContent.ADD_SEED_CONTAINERS) + : t(TourContent.ADD_TOOLS), + title: isExpress() + ? t("Add seed containers") + : t("Add tools and seed containers"), + }] + : [{ + target: ".tools", + content: isExpress() + ? t(TourContent.ADD_SEED_CONTAINERS_AND_SLOTS) + : t(TourContent.ADD_TOOLS_AND_SLOTS), + title: isExpress() + ? t("Add seed containers and slots") + : t("Add tools and tool slots"), + }]; + +const toolSlotsStep = () => hasTools() + ? [{ + target: ".tool-slots", + content: t(TourContent.ADD_TOOLS_AND_SLOTS), + title: t("Add tool slots"), + }] + : []; + export const TOUR_STEPS = (): { [x: string]: TourStep[] } => ({ [Tours.gettingStarted]: [ { target: ".plant-inventory-panel", - content: TourContent.ADD_PLANTS, + content: t(TourContent.ADD_PLANTS), title: t("Add plants"), }, - { + ...(DevSettings.futureFeaturesEnabled() ? [{ target: ".tool-list", - content: TourContent.ADD_TOOLS, - title: t("Add tools"), - }, - { + content: t(TourContent.ADD_TOOLS), + title: t("Add tools and seed containers"), + }] : toolsStep()), + ...(DevSettings.futureFeaturesEnabled() ? [{ target: ".toolbay-list", - content: TourContent.ADD_TOOLS_SLOTS, + content: t(TourContent.ADD_TOOLS_SLOTS), title: t("Add tools to tool bay"), - }, + }] : toolSlotsStep()), { target: ".peripherals-widget", - content: TourContent.ADD_PERIPHERALS, + content: t(TourContent.ADD_PERIPHERALS), title: t("Add peripherals"), }, { target: ".sequence-list-panel", - content: TourContent.ADD_SEQUENCES, + content: t(TourContent.ADD_SEQUENCES), title: t("Create sequences"), }, { target: ".regimen-list-panel", - content: TourContent.ADD_REGIMENS, + content: t(TourContent.ADD_REGIMENS), title: t("Create regimens"), }, { target: ".farm-event-panel", - content: TourContent.ADD_FARM_EVENTS, + content: t(TourContent.ADD_FARM_EVENTS), title: t("Create events"), }, ], [Tours.monitoring]: [ { target: ".move-widget", - content: TourContent.LOCATION_GRID, + content: t(TourContent.LOCATION_GRID), title: t("View current location"), }, { target: ".farm-designer", - content: TourContent.VIRTUAL_FARMBOT, + content: t(TourContent.VIRTUAL_FARMBOT), title: t("View current location"), }, { target: ".logs-table", - content: TourContent.LOGS_TABLE, + content: t(TourContent.LOGS_TABLE), title: t("View log messages"), }, { target: ".photos", - content: TourContent.PHOTOS, + content: t(TourContent.PHOTOS), title: t("Take and view photos"), }, ], [Tours.funStuff]: [ { target: ".app-settings-widget", - content: TourContent.APP_SETTINGS, + content: t(TourContent.APP_SETTINGS), title: t("Customize your web app experience"), }, ], @@ -112,6 +154,10 @@ export const tourPageNavigation = (nextStepTarget: string | HTMLElement) => { case ".toolbay-list": history.push("/app/tools"); break; + case ".tools": + case ".tool-slots": + history.push("/app/designer/tools"); + break; case ".photos": history.push("/app/farmware"); break; 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/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 => { step: resourceUpdate({ label: "mounted_tool_id", value: 0 }), resourceIndex: fakeResourceIndex() }); - expect(result).toEqual(DISMOUNTED); + expect(result).toEqual(DISMOUNTED()); }); it("unpacks valid tool_ids", () => { @@ -37,7 +37,7 @@ describe("unpackStep()", () => { resourceIndex }); const actionLabel = "Mounted to: Generic Tool"; - const { label, value } = TOOL_MOUNT; + const { label, value } = TOOL_MOUNT(); assertGoodness(result, actionLabel, "mounted", label, value); }); @@ -47,7 +47,7 @@ describe("unpackStep()", () => { resourceIndex: fakeResourceIndex() }); const actionLabel = "Mounted to: an unknown tool"; - const { label, value } = TOOL_MOUNT; + const { label, value } = TOOL_MOUNT(); assertGoodness(result, actionLabel, "mounted", label, value); }); diff --git a/frontend/sequences/step_tiles/mark_as/unpack_step.ts b/frontend/sequences/step_tiles/mark_as/unpack_step.ts index ccf7a59f0..b4d5f7da8 100644 --- a/frontend/sequences/step_tiles/mark_as/unpack_step.ts +++ b/frontend/sequences/step_tiles/mark_as/unpack_step.ts @@ -9,15 +9,16 @@ import { GenericPointer } from "farmbot/dist/resources/api_resources"; import { MOUNTED_TO } from "./constants"; import { DropDownPair, StepWithResourceIndex } from "./interfaces"; import { TaggedPoint, TaggedPlantPointer } from "farmbot"; +import { t } from "../../../i18next_wrapper"; -export const TOOL_MOUNT: DropDownItem = { - label: "Tool Mount", value: "tool_mount" -}; -const NOT_IN_USE: DropDownItem = { label: "Not Mounted", value: 0 }; -export const DISMOUNTED: DropDownPair = { - leftSide: TOOL_MOUNT, - rightSide: NOT_IN_USE -}; +export const TOOL_MOUNT = (): DropDownItem => ({ + label: t("Tool Mount"), value: "tool_mount" +}); +const NOT_IN_USE = (): DropDownItem => ({ label: t("Not Mounted"), value: 0 }); +export const DISMOUNTED = (): DropDownPair => ({ + leftSide: TOOL_MOUNT(), + rightSide: NOT_IN_USE() +}); const DEFAULT_TOOL_NAME = "Untitled Tool"; const REMOVED_ACTION = { label: "Removed", value: "removed" }; @@ -30,13 +31,13 @@ function mountTool(i: StepWithResourceIndex): DropDownPair { if (typeof value === "number" && value > 0) { try { // Good tool id const tool = findToolById(i.resourceIndex, value as number); - return { leftSide: TOOL_MOUNT, rightSide: mountedTo(tool.body.name) }; + return { leftSide: TOOL_MOUNT(), rightSide: mountedTo(tool.body.name) }; } catch { // Bad tool ID or app still loading. - return { leftSide: TOOL_MOUNT, rightSide: mountedTo("an unknown tool") }; + return { leftSide: TOOL_MOUNT(), rightSide: mountedTo("an unknown tool") }; } } else { // No tool id - return DISMOUNTED; + return DISMOUNTED(); } } @@ -55,10 +56,10 @@ function unknownOption(i: StepWithResourceIndex): DropDownPair { /** The user wants to mark a the `discarded_at` attribute of a Point. */ function discardPoint(i: StepWithResourceIndex): DropDownPair { const { resource_id } = i.step.args; - const t = + const genericPointerBody = findPointerByTypeAndId(i.resourceIndex, "GenericPointer", resource_id).body; return { - leftSide: pointer2ddi(t as GenericPointer), + leftSide: pointer2ddi(genericPointerBody as GenericPointer), rightSide: REMOVED_ACTION }; } diff --git a/public/app-resources/languages/_helper.js b/public/app-resources/languages/_helper.js index c01b7d3b9..ae133fc46 100644 --- a/public/app-resources/languages/_helper.js +++ b/public/app-resources/languages/_helper.js @@ -48,7 +48,7 @@ var HelperNamespace = (function () { var T_REGEX = /[.{[(\s]t\(["`]([\w\s{}().,:'\-=\\?\/%!]*)["`],*\s*.*\)/g; // '``' - var C_REGEX = /[`]([\w\s{}().,:'\-=\\?"+!]*)[`].*/g; + var C_REGEX = /[`]([\w\s{}().,:'\-=\/\\?"+!]*)[`].*/g; /** Some additional phrases the regex can't find. */ var EXTRA_TAGS = [ @@ -60,6 +60,7 @@ var HelperNamespace = (function () { "Else Execute", "Connecting FarmBot to the Internet", "move to home", "emergency stop", "SYNC ERROR", "inactive", "error", "No messages.", "back to regimens", "back to sequences", "back to farmware list", + "Verify Password", ]; /** diff --git a/public/app-resources/languages/translation_metrics.md b/public/app-resources/languages/translation_metrics.md index e55daf997..08b1e8de3 100644 --- a/public/app-resources/languages/translation_metrics.md +++ b/public/app-resources/languages/translation_metrics.md @@ -21,20 +21,20 @@ For example, `sudo docker-compose run web npm run translation-check`._ See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app-into-your-language) for contribution instructions. -Total number of phrases identified by the language helper for translation: __1139__ +Total number of phrases identified by the language helper for translation: __1238__ |Language|Percent translated|Translated|Untranslated|Other Translations| |:---:|---:|---:|---:|---:| -|da|10%|109|1030|44| -|de|36%|413|726|141| -|es|88%|1002|137|173| -|fr|90%|1022|117|198| -|it|8%|91|1048|189| -|nl|7%|79|1060|161| -|pt|6%|71|1068|180| -|ru|52%|596|543|221| -|th|0%|0|1139|0| -|zh|8%|86|1053|161| +|da|8%|105|1133|77| +|de|32%|397|841|168| +|es|78%|965|273|210| +|fr|80%|985|253|242| +|it|7%|87|1151|215| +|nl|6%|75|1163|187| +|pt|5%|66|1172|207| +|ru|46%|575|663|246| +|th|0%|0|1238|0| +|zh|7%|82|1156|187| **Percent translated** refers to the percent of phrases identified by the language helper that have been translated. Additional phrases not identified From 73222de6277316e63a732fcf7dfde372944cf73b Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Wed, 19 Feb 2020 11:15:18 -0600 Subject: [PATCH 11/26] Dismount tool prior to deletion --- app/mutations/tools/destroy.rb | 17 +++++-- spec/controllers/api/tools/destroy_spec.rb | 58 +++++++++++++--------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/mutations/tools/destroy.rb b/app/mutations/tools/destroy.rb index e504606b7..f60fb3c29 100644 --- a/app/mutations/tools/destroy.rb +++ b/app/mutations/tools/destroy.rb @@ -1,8 +1,8 @@ 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. "\ + 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." required do @@ -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/spec/controllers/api/tools/destroy_spec.rb b/spec/controllers/api/tools/destroy_spec.rb index d860940ba..446542489 100644 --- a/spec/controllers/api/tools/destroy_spec.rb +++ b/spec/controllers/api/tools/destroy_spec.rb @@ -1,17 +1,18 @@ -require 'spec_helper' +require "spec_helper" describe Api::ToolsController do include Devise::Test::ControllerHelpers - describe '#destroy' do + describe "#destroy" do let(:user) { FactoryBot.create(:user) } let(:tool_slot) { FactoryBot.create(:tool_slot) } let!(:tool) { - Point.destroy_all - FactoryBot.create(:tool, - tool_slot: tool_slot, - device: user.device) } + Point.destroy_all + FactoryBot.create(:tool, + tool_slot: tool_slot, + device: user.device) + } - it 'destroy a tool' do + it "destroy a tool" do sign_in user before = Tool.count tool.tool_slot.update(tool: nil) @@ -21,24 +22,23 @@ describe Api::ToolsController do expect(before).to be > after end - it 'does not destroy a tool when in use by a sequence' do + it "does not destroy a tool when in use by a sequence" do PinBinding.destroy_all Sequence.destroy_all - Sequences::Create.run!(name: "Dep. tracking", + Sequences::Create.run!(name: "Dep. tracking", device: user.device, - body: [ - { - kind: "move_absolute", - args: { - location: { kind: "tool", args: { tool_id: tool.id } - }, - offset: { kind: "coordinate", args: {x: 1, y: 2, z: 3} }, - speed: 100 - } - } - ]) + body: [ + { + kind: "move_absolute", + args: { + location: { kind: "tool", args: { tool_id: tool.id } }, + offset: { kind: "coordinate", args: { x: 1, y: 2, z: 3 } }, + speed: 100, + }, + }, + ]) sequence = Sequence.last - sd_list = EdgeNode + sd_list = EdgeNode .where(kind: "tool_id", sequence: sequence) .map { |x| Tool.find(x.value) } sign_in user @@ -52,11 +52,21 @@ describe Api::ToolsController do expect(response.status).to eq(422) expect(before).to eq(after) - expect(json[:tool]) - .to include(Tools::Destroy::STILL_IN_USE % sequence.name) + expect(json[:tool]).to include(Tools::Destroy::STILL_IN_USE % sequence.name) end - it 'does not destroy a tool when in a slot' do + it "dismounts a tool when it is deleted" do + sign_in user + before = Tool.count + tool.tool_slot.update(tool: nil) + tool.device.update!(mounted_tool_id: tool.id) + delete :destroy, params: { id: tool.id } + after = Tool.count + expect(response.status).to eq(200) + expect(before).to be > after + end + + it "does not destroy a tool when in a slot" do sign_in user before = Tool.count delete :destroy, params: { id: tool.id } From 40150a307cdd48680b09974a7c37df05bb80c4e1 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 20 Feb 2020 18:38:50 -0800 Subject: [PATCH 12/26] tool updates --- app/models/in_use_point.rb | 2 +- app/models/tool_slot.rb | 2 +- app/mutations/tools/destroy.rb | 8 +- frontend/constants.ts | 19 +- frontend/css/farm_designer/farm_designer.scss | 3 + .../farm_designer/farm_designer_panels.scss | 82 +++++++- frontend/css/global.scss | 17 +- frontend/css/inputs.scss | 8 + frontend/devices/__tests__/actions_test.ts | 3 +- frontend/devices/actions.ts | 2 +- .../__tests__/fbos_details_test.tsx | 2 + .../components/fbos_settings/fbos_details.tsx | 6 +- .../fbos_settings/ota_time_selector.tsx | 33 ++-- .../devices/components/maybe_highlight.tsx | 14 +- .../__tests__/farm_designer_test.tsx | 1 + .../__tests__/state_to_props_test.tsx | 19 +- .../__tests__/add_farm_event_test.tsx | 2 +- .../farm_events/add_farm_event.tsx | 4 +- frontend/farm_designer/index.tsx | 1 + frontend/farm_designer/interfaces.ts | 2 + .../map/__tests__/garden_map_test.tsx | 24 ++- frontend/farm_designer/map/garden_map.tsx | 5 + frontend/farm_designer/map/interfaces.ts | 1 + .../farmbot/__tests__/bot_figure_test.tsx | 12 +- .../farmbot/__tests__/farmbot_layer_test.tsx | 1 + .../layers/farmbot/__tests__/index_test.tsx | 1 + .../map/layers/farmbot/bot_figure.tsx | 43 +++- .../map/layers/farmbot/farmbot_layer.tsx | 3 +- .../map/layers/farmbot/index.tsx | 1 + .../__tests__/tool_graphics_test.tsx | 172 ++++++++++++++-- .../tool_slots/__tests__/tool_label_test.ts | 77 ++++---- .../__tests__/tool_slot_point_test.tsx | 30 ++- .../map/layers/tool_slots/tool_graphics.tsx | 184 +++++++++++++++++- .../map/layers/tool_slots/tool_label.tsx | 21 +- .../map/layers/tool_slots/tool_slot_point.tsx | 16 +- .../__tests__/group_detail_active_test.tsx | 15 -- .../point_groups/__tests__/paths_test.tsx | 2 +- ...ctor_test.tsx => point_group_sort_test.ts} | 39 +--- .../point_groups/criteria/add.tsx | 2 +- .../point_groups/group_detail_active.tsx | 19 +- .../point_groups/group_list_panel.tsx | 2 +- .../point_groups/group_order_visual.tsx | 2 +- frontend/farm_designer/point_groups/paths.tsx | 2 +- ..._sort_selector.tsx => point_group_sort.ts} | 32 --- .../points/__tests__/create_points_test.tsx | 4 +- .../farm_designer/points/create_points.tsx | 2 +- .../__tests__/garden_add_test.tsx | 2 +- .../saved_gardens/garden_add.tsx | 2 +- .../saved_gardens/garden_snapshot.tsx | 2 +- frontend/farm_designer/state_to_props.ts | 10 +- .../tools/__tests__/add_tool_slot_test.tsx | 27 +-- .../tools/__tests__/add_tool_test.tsx | 29 ++- .../tools/__tests__/edit_tool_slot_test.tsx | 59 +++++- .../tools/__tests__/edit_tool_test.tsx | 51 ++++- .../tools/__tests__/index_test.tsx | 62 +++++- .../tool_slot_edit_components_test.tsx | 78 ++++---- frontend/farm_designer/tools/add_tool.tsx | 54 +++-- .../farm_designer/tools/add_tool_slot.tsx | 44 +---- frontend/farm_designer/tools/edit_tool.tsx | 31 ++- .../farm_designer/tools/edit_tool_slot.tsx | 45 ++--- frontend/farm_designer/tools/index.tsx | 74 +++++-- .../tools/map_to_props_add_edit.ts | 67 +++++++ .../tools/tool_slot_edit_components.tsx | 95 +++++---- frontend/help/__tests__/tours_test.ts | 2 +- frontend/help/tours.ts | 4 +- frontend/resources/sequence_meta.ts | 6 +- .../__tests__/location_form_list_test.ts | 2 +- .../locals_list/location_form_list.ts | 17 +- .../__tests__/tile_move_absolute_test.tsx | 22 ++- .../step_tiles/tile_move_absolute.tsx | 10 +- package.json | 2 +- 71 files changed, 1283 insertions(+), 456 deletions(-) rename frontend/farm_designer/point_groups/__tests__/{point_group_sort_selector_test.tsx => point_group_sort_test.ts} (59%) rename frontend/farm_designer/point_groups/{point_group_sort_selector.tsx => point_group_sort.ts} (58%) create mode 100644 frontend/farm_designer/tools/map_to_props_add_edit.ts 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/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/tools/destroy.rb b/app/mutations/tools/destroy.rb index f60fb3c29..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 diff --git a/frontend/constants.ts b/frontend/constants.ts index 7adb0a80f..2c9f6449d 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -648,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 @@ -663,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 = @@ -897,16 +897,16 @@ export namespace TourContent { export const ADD_TOOLS_AND_SLOTS = trim(`Press the + button to add tools and seed containers. Then create - tool slots for them to by pressing the tool slot + button.`); + 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 seed container slot + button.`); + 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 the + button 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.`); @@ -998,7 +998,7 @@ export enum DeviceSetting { pinGuard = `Pin Guard`, // Danger Zone - dangerZone = `dangerZone`, + dangerZone = `Danger Zone`, resetHardwareParams = `Reset hardware parameter defaults`, // Pin Bindings @@ -1009,7 +1009,8 @@ export enum DeviceSetting { timezone = `timezone`, camera = `camera`, firmware = `firmware`, - farmbotOSAutoUpdate = `Farmbot OS Auto Update`, + applySoftwareUpdates = `update time`, + farmbotOSAutoUpdate = `auto update`, farmbotOS = `Farmbot OS`, autoSync = `Auto Sync`, bootSequence = `Boot Sequence`, 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 fea4106db..a915f1f5e 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 - 15rem); + 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: 2.5rem; + } + } + svg { + vertical-align: middle; + } + .tool-slot-position-info { + padding: 0; + padding-right: 1rem; + } } .mounted-tool-header { display: flex; @@ -624,6 +649,13 @@ float: left; } } + svg { + display: block; + margin: auto; + width: 10rem; + height: 10rem; + margin-top: 2rem; + } .add-stock-tools { .filter-search { margin-bottom: 1rem; @@ -634,6 +666,25 @@ 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 { @@ -645,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; } @@ -657,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 { diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 02ff86b62..e8530d2c6 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%); @@ -1543,16 +1543,21 @@ textarea:focus { cursor: pointer; margin-top: 0.25rem; margin-bottom: 0.25rem; - border: 2px solid $panel_light_blue; + border: 2px solid darken($panel_light_blue, 30%); + border-radius: 5px; &:hover, &.selected { - border: 2px solid $medium_gray; - border-radius: 2px; .sort-path-info-bar { - background: darken($light_gray, 10%); + background: darken($panel_light_blue, 40%); } } + &:hover { + border: 2px solid darken($panel_light_blue, 40%); + } + &.selected { + border: 2px solid $medium_gray; + } .sort-path-info-bar { - background: $light_gray; + background: darken($panel_light_blue, 30%); font-size: 1.2rem; padding-left: 0.5rem; white-space: nowrap; 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/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts index 2d0e5a1cd..2f69f5bc3 100644 --- a/frontend/devices/__tests__/actions_test.ts +++ b/frontend/devices/__tests__/actions_test.ts @@ -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/actions.ts b/frontend/devices/actions.ts index edea6ce0f..c1a9565ea 100644 --- a/frontend/devices/actions.ts +++ b/frontend/devices/actions.ts @@ -212,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" 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 0c4cdb602..55042b2b4 100644 --- a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx @@ -89,6 +89,7 @@ describe("", () => { 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]) @@ -115,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/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index c06a4b627..c4b138cbd 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -69,7 +69,7 @@ export function WiFiStrengthDisplay( return

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

    {wifiStrength &&
    @@ -261,8 +261,8 @@ export function FbosDetails(props: FbosDetailsProps) { wifi_level_percent, cpu_usage, private_ip, } = props.botInfoSettings; const { last_ota, last_ota_checkup } = props.deviceAccount.body; - const firmwareCommit = [firmware_commit, firmware_version].includes("---") - ? firmware_commit : firmware_version?.split("-")[1] || firmware_commit; + const infoFwCommit = firmware_version?.includes(".") ? firmware_commit : "---"; + const firmwareCommit = firmware_version?.split("-")[1] || infoFwCommit; return
    ; type EveryTimeTable = Record; +const ASAP = () => t("As soon as possible"); const TIME_TABLE_12H = (): TimeTable => ({ 0: { label: t("Midnight"), value: 0 }, 1: { label: "1:00 AM", value: 1 }, @@ -62,7 +65,7 @@ const TIME_TABLE_12H = (): TimeTable => ({ 21: { label: "9:00 PM", value: 21 }, 22: { label: "10:00 PM", value: 22 }, 23: { label: "11:00 PM", value: 23 }, - [IMMEDIATELY]: { label: t("as soon as possible"), value: IMMEDIATELY }, + [IMMEDIATELY]: { label: ASAP(), value: IMMEDIATELY }, }); const TIME_TABLE_24H = (): TimeTable => ({ 0: { label: "00:00", value: 0 }, @@ -89,7 +92,7 @@ const TIME_TABLE_24H = (): TimeTable => ({ 21: { label: "21:00", value: 21 }, 22: { label: "22:00", value: 22 }, 23: { label: "23:00", value: 23 }, - [IMMEDIATELY]: { label: t("as soon as possible"), value: IMMEDIATELY }, + [IMMEDIATELY]: { label: ASAP(), value: IMMEDIATELY }, }); const DEFAULT_HOUR: keyof TimeTable = IMMEDIATELY; @@ -144,17 +147,19 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => { const selectedItem = (typeof value == "number") ? theTimeTable[value as HOUR] : theTimeTable[DEFAULT_HOUR]; return - - - - - - + + + + + + + + ; }; diff --git a/frontend/devices/components/maybe_highlight.tsx b/frontend/devices/components/maybe_highlight.tsx index eab806b0f..7381534fa 100644 --- a/frontend/devices/components/maybe_highlight.tsx +++ b/frontend/devices/components/maybe_highlight.tsx @@ -3,6 +3,7 @@ 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, @@ -86,10 +87,15 @@ 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"); +/** 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); +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 = @@ -100,7 +106,9 @@ 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]).map(s => urlFriendly(s)); + (ALTERNATE_NAMES[settingName] as string[]) + .concat(stripUnits(settingName)) + .map(s => urlFriendly(s)); /** Retrieve a highlight search term. */ const getHighlightName = () => location.search.split("?highlight=").pop(); diff --git a/frontend/farm_designer/__tests__/farm_designer_test.tsx b/frontend/farm_designer/__tests__/farm_designer_test.tsx index 82249de16..4b7251dcc 100644 --- a/frontend/farm_designer/__tests__/farm_designer_test.tsx +++ b/frontend/farm_designer/__tests__/farm_designer_test.tsx @@ -62,6 +62,7 @@ describe("", () => { sensors: [], groups: [], shouldDisplay: () => false, + mountedToolName: undefined, }); it("loads default map settings", () => { diff --git a/frontend/farm_designer/__tests__/state_to_props_test.tsx b/frontend/farm_designer/__tests__/state_to_props_test.tsx index 68dc494d8..5e48b7ef3 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, @@ -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/farm_events/__tests__/add_farm_event_test.tsx b/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx index 04dfb8a2d..85d74e786 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 @@ -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/add_farm_event.tsx b/frontend/farm_designer/farm_events/add_farm_event.tsx index 95e11f9af..878f6dabb 100644 --- a/frontend/farm_designer/farm_events/add_farm_event.tsx +++ b/frontend/farm_designer/farm_events/add_farm_event.tsx @@ -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/index.tsx b/frontend/farm_designer/index.tsx index 746bb48cb..ab6721152 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -211,6 +211,7 @@ export class RawFarmDesigner extends React.Component> { timeSettings={this.props.timeSettings} sensors={this.props.sensors} groups={this.props.groups} + mountedToolName={this.props.mountedToolName} shouldDisplay={this.props.shouldDisplay} />
    diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index d265e5fd9..2f243ae78 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -80,6 +80,7 @@ export interface Props { sensors: TaggedSensor[]; groups: TaggedPointGroup[]; shouldDisplay: ShouldDisplay; + mountedToolName: string | undefined; } export interface MovePlantProps { @@ -210,6 +211,7 @@ export interface GardenMapProps { timeSettings: TimeSettings; groups: TaggedPointGroup[]; shouldDisplay: ShouldDisplay; + mountedToolName: string | undefined; } export interface GardenMapState { diff --git a/frontend/farm_designer/map/__tests__/garden_map_test.tsx b/frontend/farm_designer/map/__tests__/garden_map_test.tsx index f2d6b6ac7..61b48f64f 100644 --- a/frontend/farm_designer/map/__tests__/garden_map_test.tsx +++ b/frontend/farm_designer/map/__tests__/garden_map_test.tsx @@ -124,6 +124,7 @@ const fakeProps = (): GardenMapProps => ({ timeSettings: fakeTimeSettings(), groups: [], shouldDisplay: () => false, + mountedToolName: undefined, }); describe("", () => { @@ -200,6 +201,16 @@ describe("", () => { expect(getGardenCoordinates).not.toHaveBeenCalled(); }); + it("starts drag on background: does nothing when in move mode", () => { + const wrapper = mount(); + mockMode = Mode.moveTo; + const e = { pageX: 1000, pageY: 2000 }; + wrapper.find(".drop-area-background").simulate("mouseDown", e); + expect(startNewSelectionBox).not.toHaveBeenCalled(); + expect(history.push).not.toHaveBeenCalled(); + expect(getGardenCoordinates).not.toHaveBeenCalled(); + }); + it("starts drag on background: creating points", () => { const wrapper = mount(); mockMode = Mode.createPoint; @@ -348,7 +359,7 @@ describe("", () => { expect(closePlantInfo).toHaveBeenCalled(); }); - it("doesn't close panel", () => { + it("doesn't close panel: box select", () => { mockMode = Mode.boxSelect; const p = fakeProps(); p.designer.selectedPlants = [fakePlant().uuid]; @@ -357,6 +368,15 @@ describe("", () => { expect(closePlantInfo).not.toHaveBeenCalled(); }); + it("doesn't close panel: move mode", () => { + mockMode = Mode.moveTo; + const p = fakeProps(); + p.designer.selectedPlants = [fakePlant().uuid]; + const wrapper = mount(); + wrapper.instance().closePanel()(); + expect(closePlantInfo).not.toHaveBeenCalled(); + }); + it("calls unselectPlant on unmount", () => { const wrapper = shallow(); wrapper.unmount(); @@ -405,7 +425,7 @@ describe("", () => { const point = fakePoint(); point.body.id = 1; p.allPoints = [point]; - const wrapper = shallow(); + const wrapper = mount(); expect(wrapper.instance().pointsSelectedByGroup).toEqual([point]); }); }); diff --git a/frontend/farm_designer/map/garden_map.tsx b/frontend/farm_designer/map/garden_map.tsx index 063c5ccbf..fa2694827 100644 --- a/frontend/farm_designer/map/garden_map.tsx +++ b/frontend/farm_designer/map/garden_map.tsx @@ -160,6 +160,8 @@ export class GardenMap extends /** Map background drag start actions. */ startDragOnBackground = (e: React.MouseEvent): void => { switch (getMode()) { + case Mode.moveTo: + break; case Mode.createPoint: case Mode.clickToAdd: case Mode.editPlant: @@ -301,6 +303,8 @@ export class GardenMap extends /** Return to garden (unless selecting more plants). */ closePanel = () => { switch (getMode()) { + case Mode.moveTo: + return () => { }; case Mode.boxSelect: return this.props.designer.selectedPlants ? () => { } @@ -410,6 +414,7 @@ export class GardenMap extends plantAreaOffset={this.props.gridOffset} peripherals={this.props.peripherals} eStopStatus={this.props.eStopStatus} + mountedToolName={this.props.mountedToolName} getConfigValue={this.props.getConfigValue} /> HoveredPlant = () => ", () => { p.position.x = 100; const wrapper = shallow(); expect(wrapper.instance().state.hovered).toBeFalsy(); - const utm = wrapper.find("#UTM"); + const utm = wrapper.find("#UTM-wrapper"); utm.simulate("mouseOver"); expect(wrapper.instance().state.hovered).toBeTruthy(); expect(wrapper.find("text").props()).toEqual(expect.objectContaining({ @@ -105,7 +105,7 @@ describe("", () => { p.position.x = 100; p.mapTransformProps.xySwap = true; const wrapper = shallow(); - const utm = wrapper.find("#UTM"); + const utm = wrapper.find("#UTM-wrapper"); utm.simulate("mouseOver"); expect(wrapper.instance().state.hovered).toBeTruthy(); expect(wrapper.find("text").props()).toEqual(expect.objectContaining({ @@ -114,4 +114,12 @@ describe("", () => { })); expect(wrapper.text()).toEqual("(100, 0, 0)"); }); + + it("shows mounted tool", () => { + const p = fakeProps(); + p.mountedToolName = "Seeder"; + const wrapper = shallow(); + expect(wrapper.find("#UTM-wrapper").find("#mounted-tool").length) + .toEqual(1); + }); }); diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx index 8649870e5..c61de5f08 100644 --- a/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx +++ b/frontend/farm_designer/map/layers/farmbot/__tests__/farmbot_layer_test.tsx @@ -25,6 +25,7 @@ describe("", () => { peripherals: [], eStopStatus: false, getConfigValue: jest.fn(), + mountedToolName: undefined, }; } diff --git a/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx b/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx index 305d29d8c..3ea77f5b4 100644 --- a/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx +++ b/frontend/farm_designer/map/layers/farmbot/__tests__/index_test.tsx @@ -19,6 +19,7 @@ describe("", () => { peripherals: [], eStopStatus: false, getConfigValue: () => true, + mountedToolName: undefined, }; } diff --git a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx index 59d699041..9ceef1efa 100644 --- a/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx +++ b/frontend/farm_designer/map/layers/farmbot/bot_figure.tsx @@ -4,6 +4,9 @@ import { getMapSize, transformXY } from "../../util"; import { BotPosition } from "../../../../devices/interfaces"; import { Color } from "../../../../ui/index"; import { botPositionLabel } from "./bot_position_label"; +import { Tool } from "../tool_slots/tool_graphics"; +import { reduceToolName } from "../tool_slots/tool_slot_point"; +import { noop } from "lodash"; export interface BotFigureProps { name: string; @@ -11,6 +14,7 @@ export interface BotFigureProps { mapTransformProps: MapTransformProps; plantAreaOffset: AxisNumberProperty; eStopStatus?: boolean; + mountedToolName?: string | undefined; } interface BotFigureState { @@ -24,7 +28,8 @@ export class BotFigure extends setHover = (state: boolean) => { this.setState({ hovered: state }); }; render() { - const { name, position, plantAreaOffset, eStopStatus, mapTransformProps + const { + name, position, plantAreaOffset, eStopStatus, mapTransformProps, } = this.props; const { xySwap } = mapTransformProps; const mapSize = getMapSize(mapTransformProps, plantAreaOffset); @@ -32,6 +37,14 @@ export class BotFigure extends (position.x || 0), (position.y || 0), mapTransformProps); const color = eStopStatus ? Color.virtualRed : Color.darkGray; const opacity = name.includes("encoder") ? 0.25 : 0.5; + const toolProps = { + x: positionQ.qx, + y: positionQ.qy, + hovered: this.state.hovered, + dispatch: noop, + uuid: "utm", + xySwap, + }; return - this.setHover(true)} onMouseLeave={() => this.setHover(false)} - cx={positionQ.qx} - cy={positionQ.qy} - r={35} fillOpacity={opacity} - fill={color} /> + fill={color}> + {this.props.mountedToolName + ? + + + + : } + {encoderFigure && ", () => { const fakeProps = (): ToolSlotGraphicProps => ({ @@ -15,6 +19,7 @@ describe("", () => { pulloutDirection: 0, quadrant: 2, xySwap: false, + occupied: true, }); it.each<[number, BotOriginQuadrant, boolean, string]>([ @@ -89,6 +94,29 @@ describe("", () => { }); }; + it("renders empty tool slot styling", () => { + const p = fakeProps(); + p.tool = ToolNames.emptyToolSlot; + const wrapper = svgMount(); + const props = wrapper.find("circle").last().props(); + expect(props.r).toEqual(34); + expect(props.fill).toEqual("none"); + expect(props.strokeDasharray).toEqual("10 5"); + }); + + it("renders empty tool slot hover styling", () => { + const p = fakeProps(); + p.tool = ToolNames.emptyToolSlot; + p.toolProps.hovered = true; + const wrapper = svgMount(); + const props = wrapper.find("circle").first().props(); + expect(props.fill).toEqual(Color.darkGray); + }); + + it("sets hover state for empty tool slot", () => { + testHoverActions(ToolNames.emptyToolSlot); + }); + it("renders standard tool styling", () => { const wrapper = svgMount(); const props = wrapper.find("circle").last().props(); @@ -107,12 +135,96 @@ describe("", () => { }); it("sets hover state for tool", () => { - testHoverActions("tool"); + testHoverActions(ToolNames.tool); + }); + + it("renders special tool styling: weeder", () => { + const p = fakeProps(); + p.tool = ToolNames.weeder; + const wrapper = svgMount(); + const elements = wrapper.find("#weeder").find("line"); + expect(elements.length).toEqual(2); + }); + + it("renders weeder hover styling", () => { + const p = fakeProps(); + p.tool = ToolNames.weeder; + p.toolProps.hovered = true; + const wrapper = svgMount(); + expect(wrapper.find("#weeder").find("circle").props().fill) + .toEqual(Color.darkGray); + }); + + it("sets hover state for weeder", () => { + testHoverActions(ToolNames.weeder); + }); + + it("renders special tool styling: watering nozzle", () => { + const p = fakeProps(); + p.tool = ToolNames.wateringNozzle; + const wrapper = svgMount(); + const elements = wrapper.find("#watering-nozzle").find("circle"); + expect(elements.length).toEqual(3); + }); + + it("renders watering nozzle hover styling", () => { + const p = fakeProps(); + p.tool = ToolNames.wateringNozzle; + p.toolProps.hovered = true; + const wrapper = svgMount(); + expect(wrapper.find("#watering-nozzle").find("circle").at(1).props().fill) + .toEqual(Color.darkGray); + }); + + it("sets hover state for watering nozzle", () => { + testHoverActions(ToolNames.wateringNozzle); + }); + + it("renders special tool styling: seeder", () => { + const p = fakeProps(); + p.tool = ToolNames.seeder; + const wrapper = svgMount(); + const elements = wrapper.find("#seeder").find("circle"); + expect(elements.length).toEqual(2); + }); + + it("renders seeder hover styling", () => { + const p = fakeProps(); + p.tool = ToolNames.seeder; + p.toolProps.hovered = true; + const wrapper = svgMount(); + expect(wrapper.find("#seeder").find("circle").first().props().fill) + .toEqual(Color.darkGray); + }); + + it("sets hover state for seeder", () => { + testHoverActions(ToolNames.seeder); + }); + + it("renders special tool styling: soil sensor", () => { + const p = fakeProps(); + p.tool = ToolNames.soilSensor; + const wrapper = svgMount(); + const elements = wrapper.find("#soil-sensor").find("line"); + expect(elements.length).toEqual(2); + }); + + it("renders soil sensor hover styling", () => { + const p = fakeProps(); + p.tool = ToolNames.soilSensor; + p.toolProps.hovered = true; + const wrapper = svgMount(); + expect(wrapper.find("#soil-sensor").find("circle").props().fill) + .toEqual(Color.darkGray); + }); + + it("sets hover state for soil sensor", () => { + testHoverActions(ToolNames.soilSensor); }); it("renders special tool styling: bin", () => { const p = fakeProps(); - p.tool = "seedBin"; + p.tool = ToolNames.seedBin; const wrapper = svgMount(); const elements = wrapper.find("#seed-bin").find("circle"); expect(elements.length).toEqual(2); @@ -121,20 +233,19 @@ describe("", () => { it("renders bin hover styling", () => { const p = fakeProps(); - p.tool = "seedBin"; + p.tool = ToolNames.seedBin; p.toolProps.hovered = true; const wrapper = svgMount(); - p.toolProps.hovered = true; expect(wrapper.find("#seed-bin").find("circle").length).toEqual(3); }); it("sets hover state for bin", () => { - testHoverActions("seedBin"); + testHoverActions(ToolNames.seedBin); }); it("renders special tool styling: tray", () => { const p = fakeProps(); - p.tool = "seedTray"; + p.tool = ToolNames.seedTray; const wrapper = svgMount(); const elements = wrapper.find("#seed-tray"); expect(elements.find("circle").length).toEqual(2); @@ -144,20 +255,19 @@ describe("", () => { it("renders tray hover styling", () => { const p = fakeProps(); - p.tool = "seedTray"; + p.tool = ToolNames.seedTray; p.toolProps.hovered = true; const wrapper = svgMount(); - p.toolProps.hovered = true; expect(wrapper.find("#seed-tray").find("circle").length).toEqual(3); }); it("sets hover state for tray", () => { - testHoverActions("seedTray"); + testHoverActions(ToolNames.seedTray); }); it("renders special tool styling: trough", () => { const p = fakeProps(); - p.tool = "seedTrough"; + p.tool = ToolNames.seedTrough; const wrapper = svgMount(); const elements = wrapper.find("#seed-trough"); expect(elements.find("circle").length).toEqual(0); @@ -166,15 +276,49 @@ describe("", () => { it("renders trough hover styling", () => { const p = fakeProps(); - p.tool = "seedTrough"; + p.tool = ToolNames.seedTrough; p.toolProps.hovered = true; const wrapper = svgMount(); - p.toolProps.hovered = true; expect(wrapper.find("#seed-trough").find("circle").length).toEqual(0); expect(wrapper.find("#seed-trough").find("rect").length).toEqual(1); }); it("sets hover state for trough", () => { - testHoverActions("seedTrough"); + testHoverActions(ToolNames.seedTrough); + }); +}); + +describe("", () => { + const fakeProps = (): ToolSVGProps => ({ + toolName: "seed trough", + }); + + it("renders trough", () => { + const wrapper = shallow(); + expect(wrapper.find("svg").props().viewBox).toEqual("-25 0 50 1"); + }); +}); + +describe("", () => { + const fakeProps = (): ToolSlotSVGProps => ({ + toolSlot: fakeToolSlot(), + toolName: "seeder", + renderRotation: false, + xySwap: false, + quadrant: 2, + }); + + it("renders slot", () => { + const p = fakeProps(); + p.toolSlot.body.pullout_direction = ToolPulloutDirection.POSITIVE_X; + const wrapper = shallow(); + expect(wrapper.find(ToolbaySlot).length).toEqual(1); + }); + + it("doesn't render slot", () => { + const p = fakeProps(); + p.toolSlot.body.pullout_direction = ToolPulloutDirection.NONE; + const wrapper = shallow(); + expect(wrapper.find(ToolbaySlot).length).toEqual(0); }); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts index 41323925d..7b72f45ae 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_label_test.ts @@ -7,50 +7,61 @@ describe("textAnchorPosition()", () => { const MIDDLE_BOTTOM = { anchor: "middle", x: 0, y: -40 }; it("returns correct label position: positive x", () => { - expect(textAnchorPosition(1, 1, false)).toEqual(END); - expect(textAnchorPosition(1, 2, false)).toEqual(START); - expect(textAnchorPosition(1, 3, false)).toEqual(START); - expect(textAnchorPosition(1, 4, false)).toEqual(END); - expect(textAnchorPosition(1, 1, true)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(1, 2, true)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(1, 3, true)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(1, 4, true)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(1, 1, false, false)).toEqual(END); + expect(textAnchorPosition(1, 2, false, false)).toEqual(START); + expect(textAnchorPosition(1, 3, false, false)).toEqual(START); + expect(textAnchorPosition(1, 4, false, false)).toEqual(END); + expect(textAnchorPosition(1, 1, true, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(1, 2, true, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(1, 3, true, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(1, 4, true, false)).toEqual(MIDDLE_BOTTOM); }); it("returns correct label position: negative x", () => { - expect(textAnchorPosition(2, 1, false)).toEqual(START); - expect(textAnchorPosition(2, 2, false)).toEqual(END); - expect(textAnchorPosition(2, 3, false)).toEqual(END); - expect(textAnchorPosition(2, 4, false)).toEqual(START); - expect(textAnchorPosition(2, 1, true)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(2, 2, true)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(2, 3, true)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(2, 4, true)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(2, 1, false, false)).toEqual(START); + expect(textAnchorPosition(2, 2, false, false)).toEqual(END); + expect(textAnchorPosition(2, 3, false, false)).toEqual(END); + expect(textAnchorPosition(2, 4, false, false)).toEqual(START); + expect(textAnchorPosition(2, 1, true, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(2, 2, true, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(2, 3, true, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(2, 4, true, false)).toEqual(MIDDLE_TOP); }); it("returns correct label position: positive y", () => { - expect(textAnchorPosition(3, 1, false)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(3, 2, false)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(3, 3, false)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(3, 4, false)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(3, 1, true)).toEqual(END); - expect(textAnchorPosition(3, 2, true)).toEqual(START); - expect(textAnchorPosition(3, 3, true)).toEqual(START); - expect(textAnchorPosition(3, 4, true)).toEqual(END); + expect(textAnchorPosition(3, 1, false, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(3, 2, false, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(3, 3, false, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(3, 4, false, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(3, 1, true, false)).toEqual(END); + expect(textAnchorPosition(3, 2, true, false)).toEqual(START); + expect(textAnchorPosition(3, 3, true, false)).toEqual(START); + expect(textAnchorPosition(3, 4, true, false)).toEqual(END); }); it("returns correct label position: negative y", () => { - expect(textAnchorPosition(4, 1, false)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(4, 2, false)).toEqual(MIDDLE_BOTTOM); - expect(textAnchorPosition(4, 3, false)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(4, 4, false)).toEqual(MIDDLE_TOP); - expect(textAnchorPosition(4, 1, true)).toEqual(START); - expect(textAnchorPosition(4, 2, true)).toEqual(END); - expect(textAnchorPosition(4, 3, true)).toEqual(END); - expect(textAnchorPosition(4, 4, true)).toEqual(START); + expect(textAnchorPosition(4, 1, false, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(4, 2, false, false)).toEqual(MIDDLE_BOTTOM); + expect(textAnchorPosition(4, 3, false, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(4, 4, false, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(4, 1, true, false)).toEqual(START); + expect(textAnchorPosition(4, 2, true, false)).toEqual(END); + expect(textAnchorPosition(4, 3, true, false)).toEqual(END); + expect(textAnchorPosition(4, 4, true, false)).toEqual(START); + }); + + it("returns correct label position: no pullout direction", () => { + expect(textAnchorPosition(0, 1, false, false)).toEqual(END); + expect(textAnchorPosition(1, 1, false, true)).toEqual(END); + expect(textAnchorPosition(0, 1, true, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(1, 1, true, true)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(0, 2, false, false)).toEqual(START); + expect(textAnchorPosition(1, 2, false, true)).toEqual(START); + expect(textAnchorPosition(0, 2, true, false)).toEqual(MIDDLE_TOP); + expect(textAnchorPosition(1, 2, true, true)).toEqual(MIDDLE_TOP); }); it("handles bad data", () => { - expect(textAnchorPosition(1.1, 1.1, false)).toEqual(START); + expect(textAnchorPosition(1.1, 1.1, false, false)).toEqual(START); }); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index f47a485eb..a53c1508f 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -71,7 +71,7 @@ describe("", () => { p.slot.tool = undefined; p.hoveredToolSlot = p.slot.toolSlot.uuid; const wrapper = svgMount(); - expect(wrapper.find("text").text()).toEqual("empty"); + expect(wrapper.find("text").text()).toEqual("Empty"); expect(wrapper.find("text").props().dx).toEqual(40); }); @@ -80,6 +80,34 @@ describe("", () => { expect(wrapper.find("text").props().visibility).toEqual("hidden"); }); + it("renders weeder", () => { + const p = fakeProps(); + if (p.slot.tool) { p.slot.tool.body.name = "weeder"; } + const wrapper = svgMount(); + expect(wrapper.find("#weeder").length).toEqual(1); + }); + + it("renders watering nozzle", () => { + const p = fakeProps(); + if (p.slot.tool) { p.slot.tool.body.name = "watering nozzle"; } + const wrapper = svgMount(); + expect(wrapper.find("#watering-nozzle").length).toEqual(1); + }); + + it("renders seeder", () => { + const p = fakeProps(); + if (p.slot.tool) { p.slot.tool.body.name = "seeder"; } + const wrapper = svgMount(); + expect(wrapper.find("#seeder").length).toEqual(1); + }); + + it("renders soil sensor", () => { + const p = fakeProps(); + if (p.slot.tool) { p.slot.tool.body.name = "soil sensor"; } + const wrapper = svgMount(); + expect(wrapper.find("#soil-sensor").length).toEqual(1); + }); + it("renders bin", () => { const p = fakeProps(); if (p.slot.tool) { p.slot.tool.body.name = "seed bin"; } diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx index b87288589..51dc35f5d 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_graphics.tsx @@ -5,6 +5,9 @@ import { BotOriginQuadrant } from "../../../interfaces"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { Actions } from "../../../../constants"; import { UUID } from "../../../../resources/interfaces"; +import { TaggedToolSlotPointer } from "farmbot"; +import { reduceToolName } from "./tool_slot_point"; +import { noop } from "lodash"; export interface ToolGraphicProps { x: number; @@ -27,6 +30,7 @@ export interface ToolSlotGraphicProps { pulloutDirection: ToolPulloutDirection; quadrant: BotOriginQuadrant; xySwap: boolean; + occupied: boolean; } const toolbaySlotAngle = ( @@ -57,10 +61,15 @@ const toolbaySlotAngle = ( }; export enum ToolNames { + weeder = "weeder", + wateringNozzle = "wateringNozzle", + seeder = "seeder", + soilSensor = "soilSensor", seedBin = "seedBin", seedTray = "seedTray", seedTrough = "seedTrough", tool = "tool", + emptyToolSlot = "emptyToolSlot", } export const ToolbaySlot = (props: ToolSlotGraphicProps) => { @@ -82,7 +91,7 @@ export const ToolbaySlot = (props: ToolSlotGraphicProps) => { - @@ -91,9 +100,14 @@ export const ToolbaySlot = (props: ToolSlotGraphicProps) => { export const Tool = (props: ToolProps) => { switch (props.tool) { + case ToolNames.weeder: return ; + case ToolNames.wateringNozzle: return ; + case ToolNames.seeder: return ; + case ToolNames.soilSensor: return ; case ToolNames.seedBin: return ; case ToolNames.seedTray: return ; case ToolNames.seedTrough: return ; + case ToolNames.emptyToolSlot: return ; default: return ; } }; @@ -115,6 +129,115 @@ const StandardTool = (props: ToolGraphicProps) => { ; }; +const EmptySlot = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + ; +}; + +const Weeder = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + const size = 10; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + + ; +}; + +const WateringNozzle = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + + + + + + + + ; +}; + +const Seeder = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + const size = 10; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + ; +}; + +const SoilSensor = (props: ToolGraphicProps) => { + const { x, y, hovered, dispatch, uuid } = props; + const size = 20; + return dispatch(setToolHover(uuid))} + onMouseLeave={() => dispatch(setToolHover(undefined))}> + + + + ; +}; + const seedBinGradient = @@ -214,3 +337,62 @@ const SeedTrough = (props: ToolGraphicProps) => { fill={hovered ? Color.darkGray : Color.mediumGray} /> ; }; + +export interface ToolSlotSVGProps { + toolSlot: TaggedToolSlotPointer; + toolName: string | undefined; + renderRotation: boolean; + xySwap?: boolean; + quadrant?: BotOriginQuadrant; +} + +export const ToolSlotSVG = (props: ToolSlotSVGProps) => { + const xySwap = props.renderRotation ? !!props.xySwap : false; + const toolProps = { + x: 0, y: 0, + hovered: false, + dispatch: noop, + uuid: props.toolSlot.uuid, + xySwap, + }; + const pulloutDirection = props.renderRotation + ? props.toolSlot.body.pullout_direction + : ToolPulloutDirection.POSITIVE_X; + const quadrant = props.renderRotation && props.quadrant ? props.quadrant : 2; + const viewBox = props.renderRotation ? "-25 0 50 1" : "-25 0 50 1"; + return props.toolSlot.body.gantry_mounted + ? + + {props.toolSlot.body.tool_id && + } + + : + {props.toolSlot.body.pullout_direction && + } + {(props.toolSlot.body.tool_id || + !props.toolSlot.body.pullout_direction) && + } + ; +}; + +export interface ToolSVGProps { + toolName: string | undefined; +} + +export const ToolSVG = (props: ToolSVGProps) => { + const toolProps = { + x: 0, y: 0, hovered: false, dispatch: noop, uuid: "", xySwap: false, + }; + const viewBox = reduceToolName(props.toolName) === ToolNames.seedTrough + ? "-25 0 50 1" : "-40 0 80 1"; + return + } +; +}; diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx index 18d9ff713..93bfcf04f 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_label.tsx @@ -13,9 +13,17 @@ enum Anchor { export const textAnchorPosition = ( pulloutDirection: ToolPulloutDirection, quadrant: BotOriginQuadrant, - xySwap: boolean): { x: number, y: number, anchor: string } => { + xySwap: boolean, + gantryMounted: boolean, +): { x: number, y: number, anchor: string } => { const rawAnchor = () => { - const direction = pulloutDirection + (xySwap ? 2 : 0); + const noDirection = !pulloutDirection || gantryMounted; + const noDirectionXY = xySwap + ? ToolPulloutDirection.POSITIVE_Y + : ToolPulloutDirection.POSITIVE_X; + const direction = noDirection + ? noDirectionXY + : pulloutDirection + (xySwap ? 2 : 0); switch (direction > 4 ? direction % 4 : direction) { case ToolPulloutDirection.POSITIVE_X: return Anchor.start; case ToolPulloutDirection.NEGATIVE_X: return Anchor.end; @@ -51,12 +59,15 @@ interface ToolLabelProps { pulloutDirection: ToolPulloutDirection; quadrant: BotOriginQuadrant; xySwap: boolean; + gantryMounted: boolean; } export const ToolLabel = (props: ToolLabelProps) => { - const { toolName, hovered, x, y, pulloutDirection, quadrant, xySwap } = props; - const labelAnchor = textAnchorPosition(pulloutDirection, quadrant, xySwap); - + const { + toolName, hovered, x, y, pulloutDirection, quadrant, xySwap, gantryMounted, + } = props; + const labelAnchor = textAnchorPosition + (pulloutDirection, quadrant, xySwap, gantryMounted); return { +export const reduceToolName = (raw: string | undefined) => { const lower = (raw || "").toLowerCase(); + if (raw == "Empty") { return ToolNames.emptyToolSlot; } + if (includes(lower, "weeder")) { return ToolNames.weeder; } + if (includes(lower, "watering nozzle")) { return ToolNames.wateringNozzle; } + if (includes(lower, "seeder")) { return ToolNames.seeder; } + if (includes(lower, "soil sensor")) { return ToolNames.soilSensor; } if (includes(lower, "seed bin")) { return ToolNames.seedBin; } if (includes(lower, "seed tray")) { return ToolNames.seedTray; } if (includes(lower, "seed trough")) { return ToolNames.seedTrough; } @@ -32,7 +38,7 @@ export const ToolSlotPoint = (props: TSPProps) => { const { quadrant, xySwap } = mapTransformProps; const xPosition = gantry_mounted ? (botPositionX || 0) : x; const { qx, qy } = transformXY(xPosition, y, props.mapTransformProps); - const toolName = props.slot.tool ? props.slot.tool.body.name : "empty"; + const toolName = props.slot.tool ? props.slot.tool.body.name : t("Empty"); const hovered = props.slot.toolSlot.uuid === props.hoveredToolSlot; const toolProps = { x: qx, @@ -45,13 +51,14 @@ export const ToolSlotPoint = (props: TSPProps) => { return !DevSettings.futureFeaturesEnabled() && history.push(`/app/designer/tool-slots/${id}`)}> - {pullout_direction && + {pullout_direction && !gantry_mounted && } {gantry_mounted && } @@ -67,6 +74,7 @@ export const ToolSlotPoint = (props: TSPProps) => { x={qx} y={qy} pulloutDirection={pullout_direction} + gantryMounted={gantry_mounted} quadrant={quadrant} xySwap={xySwap} /> ; diff --git a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx index 1054d9806..d987e4db7 100644 --- a/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/group_detail_active_test.tsx @@ -6,13 +6,6 @@ jest.mock("../../../api/crud", () => ({ jest.mock("../../map/actions", () => ({ setHoveredPlant: jest.fn() })); -let mockDev = false; -jest.mock("../../../account/dev/dev_support", () => ({ - DevSettings: { - futureFeaturesEnabled: () => mockDev, - } -})); - import React from "react"; import { GroupDetailActive, GroupDetailActiveProps @@ -107,19 +100,11 @@ describe("", () => { }); it("shows paths", () => { - mockDev = false; const p = fakeProps(); const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("0m"); }); - it("doesn't show paths", () => { - mockDev = true; - const p = fakeProps(); - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("0m"); - }); - it("shows random warning text", () => { const p = fakeProps(); p.group.body.sort_type = "random"; diff --git a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx index 31cecd0ba..4ad44cabf 100644 --- a/frontend/farm_designer/point_groups/__tests__/paths_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/paths_test.tsx @@ -22,7 +22,7 @@ import { Actions } from "../../../constants"; import { edit } from "../../../api/crud"; import { error } from "../../../toast/toast"; import { svgMount } from "../../../__test_support__/svg_mount"; -import { SORT_OPTIONS } from "../point_group_sort_selector"; +import { SORT_OPTIONS } from "../point_group_sort"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; /** diff --git a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx b/frontend/farm_designer/point_groups/__tests__/point_group_sort_test.ts similarity index 59% rename from frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx rename to frontend/farm_designer/point_groups/__tests__/point_group_sort_test.ts index bfd1f1d60..096e36b7d 100644 --- a/frontend/farm_designer/point_groups/__tests__/point_group_sort_selector_test.tsx +++ b/frontend/farm_designer/point_groups/__tests__/point_group_sort_test.ts @@ -1,45 +1,8 @@ -import { - isSortType, sortTypeChange, SORT_OPTIONS -} from "../point_group_sort_selector"; -import { DropDownItem } from "../../../ui"; +import { SORT_OPTIONS } from "../point_group_sort"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { TaggedPoint } from "farmbot"; import { fakePlant } from "../../../__test_support__/fake_state/resources"; -const tests: [string, boolean][] = [ - ["", false], - ["nope", false], - ["random", true], - ["xy_ascending", true], - ["xy_descending", true], - ["yx_ascending", true], - ["yx_descending", true] -]; - -describe("isSortType", () => { - it("identifies malformed sort types", () => { - tests.map(([sortType, valid]) => { - expect(isSortType(sortType)).toBe(valid); - }); - }); -}); - -describe("sortTypeChange", () => { - it("selectively triggers the callback", () => { - tests.map(([value, valid]) => { - const cb = jest.fn(); - const ddi: DropDownItem = { value, label: "TEST" }; - if (valid) { - sortTypeChange(cb)(ddi); - expect(cb).toHaveBeenCalledWith(value); - } else { - sortTypeChange(cb)(ddi); - expect(cb).not.toHaveBeenCalled(); - } - }); - }); -}); - describe("sort()", () => { const phony = (name: string, x: number, y: number): TaggedPoint => { const plant = fakePlant(); diff --git a/frontend/farm_designer/point_groups/criteria/add.tsx b/frontend/farm_designer/point_groups/criteria/add.tsx index d4f9275b2..a24826756 100644 --- a/frontend/farm_designer/point_groups/criteria/add.tsx +++ b/frontend/farm_designer/point_groups/criteria/add.tsx @@ -70,7 +70,7 @@ export const CRITERIA_TYPE_LIST = () => [ export const POINTER_TYPE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ Plant: { label: t("Plants"), value: "Plant" }, GenericPointer: { label: t("Points"), value: "GenericPointer" }, - ToolSlot: { label: t("Tool Slots"), value: "ToolSlot" }, + ToolSlot: { label: t("Slots"), value: "ToolSlot" }, }); export const POINTER_TYPE_LIST = () => [ POINTER_TYPE_DDI_LOOKUP().Plant, diff --git a/frontend/farm_designer/point_groups/group_detail_active.tsx b/frontend/farm_designer/point_groups/group_detail_active.tsx index 5c50e92a3..66ae340e7 100644 --- a/frontend/farm_designer/point_groups/group_detail_active.tsx +++ b/frontend/farm_designer/point_groups/group_detail_active.tsx @@ -7,11 +7,10 @@ import { import { TaggedPointGroup, TaggedPoint } from "farmbot"; import { DeleteButton } from "../../ui/delete_button"; import { save, edit } from "../../api/crud"; -import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector"; +import { sortGroupBy } from "./point_group_sort"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; import { PointGroupItem } from "./point_group_item"; import { Paths } from "./paths"; -import { DevSettings } from "../../account/dev/dev_support"; import { Feature, ShouldDisplay } from "../../devices/interfaces"; import { ErrorBoundary } from "../../error_boundary"; import { @@ -103,16 +102,12 @@ export class GroupDetailActive - {!DevSettings.futureFeaturesEnabled() - ? p.body.id))} - pathPoints={this.pointsSelectedByGroup} - dispatch={dispatch} - group={group} /> - : } + p.body.id))} + pathPoints={this.pointsSelectedByGroup} + dispatch={dispatch} + group={group} />

    {group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)}

    diff --git a/frontend/farm_designer/point_groups/group_list_panel.tsx b/frontend/farm_designer/point_groups/group_list_panel.tsx index 6f7949970..93f7884d8 100644 --- a/frontend/farm_designer/point_groups/group_list_panel.tsx +++ b/frontend/farm_designer/point_groups/group_list_panel.tsx @@ -48,7 +48,7 @@ export class RawGroupListPanel extends React.Component + title={t("Add group")}> diff --git a/frontend/farm_designer/point_groups/group_order_visual.tsx b/frontend/farm_designer/point_groups/group_order_visual.tsx index cdf9f21cc..800df2c13 100644 --- a/frontend/farm_designer/point_groups/group_order_visual.tsx +++ b/frontend/farm_designer/point_groups/group_order_visual.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { store } from "../../redux/store"; import { MapTransformProps } from "../map/interfaces"; import { isUndefined } from "lodash"; -import { sortGroupBy } from "./point_group_sort_selector"; +import { sortGroupBy } from "./point_group_sort"; import { Color } from "../../ui"; import { transformXY } from "../map/util"; import { nn } from "./paths"; diff --git a/frontend/farm_designer/point_groups/paths.tsx b/frontend/farm_designer/point_groups/paths.tsx index e2368e967..9c9b5b134 100644 --- a/frontend/farm_designer/point_groups/paths.tsx +++ b/frontend/farm_designer/point_groups/paths.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { MapTransformProps } from "../map/interfaces"; -import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector"; +import { sortGroupBy, sortOptionsTable } from "./point_group_sort"; import { sortBy, isNumber } from "lodash"; import { PointsPathLine } from "./group_order_visual"; import { Color } from "../../ui"; diff --git a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx b/frontend/farm_designer/point_groups/point_group_sort.ts similarity index 58% rename from frontend/farm_designer/point_groups/point_group_sort_selector.tsx rename to frontend/farm_designer/point_groups/point_group_sort.ts index e1c335dd9..36803a2d2 100644 --- a/frontend/farm_designer/point_groups/point_group_sort_selector.tsx +++ b/frontend/farm_designer/point_groups/point_group_sort.ts @@ -1,6 +1,4 @@ -import * as React from "react"; import { PointGroupSortType } from "farmbot/dist/resources/api_resources"; -import { FBSelect, DropDownItem } from "../../ui"; import { t } from "../../i18next_wrapper"; import { shuffle, sortBy } from "lodash"; import { TaggedPoint } from "farmbot"; @@ -18,36 +16,6 @@ export const sortOptionsTable = (): Record => ({ "yx_descending": t("Y/X, Descending"), }); // Typechecker will remind us when this needs an update. Don't simplify - RC -const optionPlusDescriptions = () => - (Object - .entries(sortOptionsTable()) as [PointGroupSortType, string][]) - .map(x => ({ label: x[1], value: x[0] })); - -const optionList = - optionPlusDescriptions().map(x => x.value); - -export const isSortType = (x: unknown): x is PointGroupSortType => { - return optionList.includes(x as PointGroupSortType); -}; - -const selected = (value: PointGroupSortType) => ({ - label: t(sortOptionsTable()[value] || value), - value: value -}); - -export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => { - const { value } = ddi; - isSortType(value) && cb(value); -}; - -export function PointGroupSortSelector(p: PointGroupSortSelectorProps) { - return ; -} - type Sorter = (p: TaggedPoint[]) => TaggedPoint[]; type SortDictionary = Record; diff --git a/frontend/farm_designer/points/__tests__/create_points_test.tsx b/frontend/farm_designer/points/__tests__/create_points_test.tsx index 669958c6e..1d66b188c 100644 --- a/frontend/farm_designer/points/__tests__/create_points_test.tsx +++ b/frontend/farm_designer/points/__tests__/create_points_test.tsx @@ -70,14 +70,14 @@ describe("", () => { it("renders for points", () => { mockPath = "/app/designer"; const wrapper = mount(); - ["create point", "delete", "x", "y", "radius", "color"] + ["add point", "delete", "x", "y", "radius", "color"] .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); it("renders for weeds", () => { mockPath = "/app/designer/weeds/add"; const wrapper = mount(); - ["create weed", "delete", "x", "y", "radius", "color"] + ["add weed", "delete", "x", "y", "radius", "color"] .map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); diff --git a/frontend/farm_designer/points/create_points.tsx b/frontend/farm_designer/points/create_points.tsx index 227ab3051..a0ebd2143 100644 --- a/frontend/farm_designer/points/create_points.tsx +++ b/frontend/farm_designer/points/create_points.tsx @@ -274,7 +274,7 @@ export class RawCreatePoints diff --git a/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx b/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx index 883600f9c..aeaa2a0b1 100644 --- a/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx +++ b/frontend/farm_designer/saved_gardens/__tests__/garden_add_test.tsx @@ -15,7 +15,7 @@ describe("", () => { it("renders add garden panel", () => { const wrapper = mount(); - expect(wrapper.text()).toContain("create new garden"); + expect(wrapper.text().toLowerCase()).toContain("add garden"); }); }); diff --git a/frontend/farm_designer/saved_gardens/garden_add.tsx b/frontend/farm_designer/saved_gardens/garden_add.tsx index b0a1b3daa..255f43dde 100644 --- a/frontend/farm_designer/saved_gardens/garden_add.tsx +++ b/frontend/farm_designer/saved_gardens/garden_add.tsx @@ -29,7 +29,7 @@ export class RawAddGarden extends React.Component { diff --git a/frontend/farm_designer/saved_gardens/garden_snapshot.tsx b/frontend/farm_designer/saved_gardens/garden_snapshot.tsx index 6687f10e0..2c906ae38 100644 --- a/frontend/farm_designer/saved_gardens/garden_snapshot.tsx +++ b/frontend/farm_designer/saved_gardens/garden_snapshot.tsx @@ -49,7 +49,7 @@ export class GardenSnapshot
    ; } diff --git a/frontend/farm_designer/state_to_props.ts b/frontend/farm_designer/state_to_props.ts index b5f76e15b..c1e32879b 100644 --- a/frontend/farm_designer/state_to_props.ts +++ b/frontend/farm_designer/state_to_props.ts @@ -11,7 +11,9 @@ import { selectAllSensors, maybeGetTimeSettings, selectAllPoints, - selectAllPointGroups + selectAllPointGroups, + getDeviceAccountSettings, + maybeFindToolById } from "../resources/selectors"; import { validBotLocationData, validFwConfig, unpackUUID } from "../util"; import { getWebAppConfigValue } from "../config_storage/actions"; @@ -64,6 +66,11 @@ export function mapStateToProps(props: Everything): Props { y: calcMicrostepsPerMm(fw.movement_step_per_mm_y, fw.movement_microsteps_y), }; + const mountedToolId = + getDeviceAccountSettings(props.resources.index).body.mounted_tool_id; + const mountedToolName = + maybeFindToolById(props.resources.index, mountedToolId)?.body.name; + const peripherals = uniq(selectAllPeripherals(props.resources.index)) .map(x => { const label = x.body.label; @@ -123,5 +130,6 @@ export function mapStateToProps(props: Everything): Props { sensors: selectAllSensors(props.resources.index), groups: selectAllPointGroups(props.resources.index), shouldDisplay, + mountedToolName, }; } diff --git a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx index 76a5c991b..3c631136f 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_slot_test.tsx @@ -9,12 +9,10 @@ jest.mock("../../../history", () => ({ history: { push: jest.fn() } })); import * as React from "react"; import { mount, shallow } from "enzyme"; -import { - RawAddToolSlot as AddToolSlot, AddToolSlotProps, mapStateToProps -} from "../add_tool_slot"; +import { RawAddToolSlot as AddToolSlot } from "../add_tool_slot"; import { fakeState } from "../../../__test_support__/fake_state"; import { - fakeTool, fakeToolSlot + fakeTool, fakeToolSlot, fakeWebAppConfig } from "../../../__test_support__/fake_state/resources"; import { buildResourceIndex @@ -23,6 +21,7 @@ import { init, save, edit, destroy } from "../../../api/crud"; import { history } from "../../../history"; import { SpecialStatus } from "farmbot"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; +import { AddToolSlotProps, mapStateToPropsAdd } from "../map_to_props_add_edit"; describe("", () => { const fakeProps = (): AddToolSlotProps => ({ @@ -32,15 +31,18 @@ describe("", () => { dispatch: jest.fn(), findToolSlot: fakeToolSlot, firmwareHardware: undefined, + xySwap: false, + quadrant: 2, + isActive: jest.fn(), }); it("renders", () => { const wrapper = mount(); - ["add new tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", - "change slot direction", "use current location", "gantry-mounted" + ["add new slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", + "change direction", "gantry-mounted" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); expect(init).toHaveBeenCalledWith("Point", { - pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + pointer_type: "ToolSlot", name: "Slot", radius: 0, meta: {}, x: 0, y: 0, z: 0, tool_id: undefined, pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: false, @@ -116,7 +118,7 @@ describe("", () => { const wrapper = mount(); expect(wrapper.text().toLowerCase()).not.toContain("tool"); expect(init).toHaveBeenCalledWith("Point", { - pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + pointer_type: "ToolSlot", name: "Slot", radius: 0, meta: {}, x: 0, y: 0, z: 0, tool_id: undefined, pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: true, @@ -124,14 +126,17 @@ describe("", () => { }); }); -describe("mapStateToProps()", () => { +describe("mapStateToPropsAdd()", () => { it("returns props", () => { + const webAppConfig = fakeWebAppConfig(); + webAppConfig.body.bot_origin_quadrant = 1; const tool = fakeTool(); tool.body.id = 1; const toolSlot = fakeToolSlot(); const state = fakeState(); - state.resources = buildResourceIndex([tool, toolSlot]); - const props = mapStateToProps(state); + state.resources = buildResourceIndex([tool, toolSlot, webAppConfig]); + const props = mapStateToPropsAdd(state); + expect(props.quadrant).toEqual(1); expect(props.findTool(1)).toEqual(tool); expect(props.findToolSlot(toolSlot.uuid)).toEqual(toolSlot); }); diff --git a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx index c8d06b478..91b6f2d1f 100644 --- a/frontend/farm_designer/tools/__tests__/add_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/add_tool_test.tsx @@ -22,7 +22,7 @@ describe("", () => { it("renders", () => { const wrapper = mount(); - expect(wrapper.text()).toContain("Add new tool"); + expect(wrapper.text()).toContain("Add new"); }); it("edits tool name", () => { @@ -60,11 +60,36 @@ describe("", () => { p.firmwareHardware = "express_k10"; p.existingToolNames = ["Seed Trough 1"]; const wrapper = mount(); - wrapper.setState({ model: "express" }); wrapper.find("button").last().simulate("click"); expect(initSave).toHaveBeenCalledTimes(1); expect(history.push).toHaveBeenCalledWith("/app/designer/tools"); }); + + it("copies a tool name", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + wrapper.find("p").last().simulate("click"); + expect(wrapper.state().toolName).toEqual("Seed Trough 2"); + }); + + it("deselects a tool", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.state().toAdd).toEqual(["Seed Trough 1", "Seed Trough 2"]); + wrapper.find("input").last().simulate("change"); + expect(wrapper.state().toAdd).toEqual(["Seed Trough 1"]); + }); + + it("selects a tool", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + wrapper.setState({ toAdd: [] }); + wrapper.find("input").last().simulate("change"); + expect(wrapper.state().toAdd).toEqual(["Seed Trough 2"]); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx index 7057f20dd..d1938eed9 100644 --- a/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx +++ b/frontend/farm_designer/tools/__tests__/edit_tool_slot_test.tsx @@ -9,9 +9,7 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); import * as React from "react"; import { mount, shallow } from "enzyme"; -import { - RawEditToolSlot as EditToolSlot, EditToolSlotProps, mapStateToProps -} from "../edit_tool_slot"; +import { RawEditToolSlot as EditToolSlot } from "../edit_tool_slot"; import { fakeState } from "../../../__test_support__/fake_state"; import { fakeToolSlot, fakeTool @@ -20,6 +18,10 @@ import { buildResourceIndex } from "../../../__test_support__/resource_index_builder"; import { destroy, edit, save } from "../../../api/crud"; +import { + EditToolSlotProps, mapStateToPropsEdit +} from "../map_to_props_add_edit"; +import { SlotEditRows } from "../tool_slot_edit_components"; describe("", () => { const fakeProps = (): EditToolSlotProps => ({ @@ -29,6 +31,9 @@ describe("", () => { botPosition: { x: undefined, y: undefined, z: undefined }, dispatch: jest.fn(), firmwareHardware: undefined, + xySwap: false, + quadrant: 2, + isActive: jest.fn(), }); it("redirects", () => { @@ -40,8 +45,8 @@ describe("", () => { const p = fakeProps(); p.findToolSlot = () => fakeToolSlot(); const wrapper = mount(); - ["edit tool slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", - "change slot direction", "use current location", "gantry-mounted" + ["edit slot", "x (mm)", "y (mm)", "z (mm)", "tool or seed container", + "change direction", "gantry-mounted" ].map(string => expect(wrapper.text().toLowerCase()).toContain(string)); }); @@ -65,6 +70,34 @@ describe("", () => { expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 }); }); + it("moves to gantry-mounted tool slot", () => { + const p = fakeProps(); + p.botPosition = { x: 10, y: 20, z: 30 }; + const toolSlot = fakeToolSlot(); + toolSlot.body.gantry_mounted = true; + toolSlot.body.x = 1; + toolSlot.body.y = 2; + toolSlot.body.z = 3; + p.findToolSlot = () => toolSlot; + const wrapper = shallow(); + wrapper.find(".gray").last().simulate("click"); + expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 10, y: 2, z: 3 }); + }); + + it("falls back to tool slot when moving to gantry-mounted tool slot", () => { + const p = fakeProps(); + p.botPosition = { x: undefined, y: undefined, z: undefined }; + const toolSlot = fakeToolSlot(); + toolSlot.body.gantry_mounted = true; + toolSlot.body.x = 1; + toolSlot.body.y = 2; + toolSlot.body.z = 3; + p.findToolSlot = () => toolSlot; + const wrapper = shallow(); + wrapper.find(".gray").last().simulate("click"); + expect(mockDevice.moveAbsolute).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 }); + }); + it("removes tool slot", () => { const p = fakeProps(); const toolSlot = fakeToolSlot(); @@ -73,9 +106,19 @@ describe("", () => { wrapper.find("button").last().simulate("click"); expect(destroy).toHaveBeenCalledWith(toolSlot.uuid); }); + + it("finds tool", () => { + const p = fakeProps(); + const toolSlot = fakeToolSlot(); + p.findToolSlot = () => toolSlot; + const tool = fakeTool(); + p.findTool = () => tool; + const wrapper = mount(); + expect(wrapper.find(SlotEditRows).props().tool).toEqual(tool); + }); }); -describe("mapStateToProps()", () => { +describe("mapStateToPropsEdit()", () => { it("returns props", () => { const tool = fakeTool(); tool.body.id = 1; @@ -83,7 +126,7 @@ describe("mapStateToProps()", () => { toolSlot.body.id = 1; const state = fakeState(); state.resources = buildResourceIndex([tool, toolSlot]); - const props = mapStateToProps(state); + const props = mapStateToPropsEdit(state); expect(props.findTool(1)).toEqual(tool); expect(props.findToolSlot("1")).toEqual(toolSlot); }); @@ -91,7 +134,7 @@ describe("mapStateToProps()", () => { it("doesn't find tool slot", () => { const state = fakeState(); state.resources = buildResourceIndex([]); - const props = mapStateToProps(state); + const props = mapStateToPropsEdit(state); expect(props.findToolSlot("1")).toEqual(undefined); }); }); diff --git a/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx b/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx index 88193b14c..2bef0eae0 100644 --- a/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx +++ b/frontend/farm_designer/tools/__tests__/edit_tool_test.tsx @@ -13,16 +13,17 @@ jest.mock("../../../history", () => ({ import * as React from "react"; import { mount, shallow } from "enzyme"; import { - RawEditTool as EditTool, EditToolProps, mapStateToProps + RawEditTool as EditTool, EditToolProps, mapStateToProps, isActive } from "../edit_tool"; -import { fakeTool } from "../../../__test_support__/fake_state/resources"; +import { fakeTool, fakeToolSlot } from "../../../__test_support__/fake_state/resources"; import { fakeState } from "../../../__test_support__/fake_state"; import { - buildResourceIndex + buildResourceIndex, fakeDevice } from "../../../__test_support__/resource_index_builder"; import { SaveBtn } from "../../../ui"; import { history } from "../../../history"; import { edit, destroy } from "../../../api/crud"; +import { clickButton } from "../../../__test_support__/helpers"; describe("", () => { beforeEach(() => { @@ -32,6 +33,8 @@ describe("", () => { const fakeProps = (): EditToolProps => ({ findTool: jest.fn(() => fakeTool()), dispatch: jest.fn(), + mountedToolId: undefined, + isActive: jest.fn(), }); it("renders", () => { @@ -75,11 +78,38 @@ describe("", () => { it("removes tool", () => { const p = fakeProps(); const tool = fakeTool(); + tool.body.id = 1; p.findTool = () => tool; + p.isActive = () => false; + p.mountedToolId = undefined; const wrapper = shallow(); - wrapper.find("button").last().simulate("click"); + clickButton(wrapper, 0, "delete"); expect(destroy).toHaveBeenCalledWith(tool.uuid); }); + + it("doesn't remove tool: active", () => { + const p = fakeProps(); + const tool = fakeTool(); + tool.body.id = 1; + p.findTool = () => tool; + p.isActive = () => true; + p.mountedToolId = undefined; + const wrapper = shallow(); + clickButton(wrapper, 0, "delete"); + expect(destroy).not.toHaveBeenCalledWith(tool.uuid); + }); + + it("doesn't remove tool: mounted", () => { + const p = fakeProps(); + const tool = fakeTool(); + tool.body.id = 1; + p.findTool = () => tool; + p.isActive = () => false; + p.mountedToolId = tool.body.id; + const wrapper = shallow(); + clickButton(wrapper, 0, "delete"); + expect(destroy).not.toHaveBeenCalledWith(tool.uuid); + }); }); describe("mapStateToProps()", () => { @@ -87,8 +117,19 @@ describe("mapStateToProps()", () => { const state = fakeState(); const tool = fakeTool(); tool.body.id = 123; - state.resources = buildResourceIndex([tool]); + state.resources = buildResourceIndex([tool, fakeDevice()]); const props = mapStateToProps(state); expect(props.findTool("" + tool.body.id)).toEqual(tool); }); }); + +describe("isActive()", () => { + it("returns tool state", () => { + const toolSlot = fakeToolSlot(); + toolSlot.body.tool_id = 1; + const active = isActive([toolSlot]); + expect(active(1)).toEqual(true); + expect(active(2)).toEqual(false); + expect(active(undefined)).toEqual(false); + }); +}); diff --git a/frontend/farm_designer/tools/__tests__/index_test.tsx b/frontend/farm_designer/tools/__tests__/index_test.tsx index c4584f781..22f074c0e 100644 --- a/frontend/farm_designer/tools/__tests__/index_test.tsx +++ b/frontend/farm_designer/tools/__tests__/index_test.tsx @@ -13,7 +13,10 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); import * as React from "react"; import { mount, shallow } from "enzyme"; -import { RawTools as Tools, ToolsProps, mapStateToProps } from "../index"; +import { + RawTools as Tools, ToolsProps, mapStateToProps, + ToolSlotInventoryItem, ToolSlotInventoryItemProps, +} from "../index"; import { fakeTool, fakeToolSlot, fakeSensor } from "../../../__test_support__/fake_state/resources"; @@ -40,6 +43,7 @@ describe("", () => { botToMqttStatus: "down", hoveredToolSlot: undefined, firmwareHardware: undefined, + isActive: jest.fn(), }); it("renders with no tools", () => { @@ -182,6 +186,62 @@ describe("", () => { const wrapper = mount(); expect(wrapper.text().toLowerCase()).not.toContain("mounted tool"); }); + + it("displays tool as active", () => { + const p = fakeProps(); + p.tools = [fakeTool()]; + p.isActive = () => true; + p.device.body.mounted_tool_id = undefined; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("active"); + }); + + it("displays tool as mounted", () => { + const p = fakeProps(); + const tool = fakeTool(); + tool.body.id = 1; + p.findTool = () => tool; + p.tools = [tool]; + p.device.body.mounted_tool_id = 1; + const wrapper = mount(); + expect(wrapper.find("p").last().text().toLowerCase()).toContain("mounted"); + }); + + it("handles missing tools", () => { + const p = fakeProps(); + const tool = fakeTool(); + tool.body.id = 1; + p.findTool = () => undefined; + p.tools = [tool]; + p.device.body.mounted_tool_id = 1; + const wrapper = mount(); + expect(wrapper.find("p").last().text().toLowerCase()).not.toContain("mounted"); + }); +}); + +describe("", () => { + const fakeProps = (): ToolSlotInventoryItemProps => ({ + toolSlot: fakeToolSlot(), + tools: [], + hovered: false, + dispatch: jest.fn(), + isActive: jest.fn(), + }); + + it("changes tool", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(ToolSelection).simulate("change", { tool_id: 1 }); + expect(edit).toHaveBeenCalledWith(p.toolSlot, { tool_id: 1 }); + expect(save).toHaveBeenCalledWith(p.toolSlot.uuid); + }); + + it("doesn't open tool slot", () => { + const wrapper = shallow(); + const e = { stopPropagation: jest.fn() }; + wrapper.find(".tool-selection-wrapper").first().simulate("click", e); + expect(e.stopPropagation).toHaveBeenCalled(); + }); }); describe("mapStateToProps()", () => { diff --git a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx index 861c26b3f..e75dbf479 100644 --- a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx +++ b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx @@ -2,14 +2,13 @@ import * as React from "react"; import { shallow, mount } from "enzyme"; import { GantryMountedInput, GantryMountedInputProps, - UseCurrentLocationInputRow, UseCurrentLocationInputRowProps, SlotDirectionInputRow, SlotDirectionInputRowProps, ToolInputRow, ToolInputRowProps, SlotLocationInputRow, SlotLocationInputRowProps, - ToolSelection, ToolSelectionProps, + ToolSelection, ToolSelectionProps, SlotEditRows, SlotEditRowsProps, } from "../tool_slot_edit_components"; -import { fakeTool } from "../../../__test_support__/fake_state/resources"; -import { FBSelect } from "../../../ui"; +import { fakeTool, fakeToolSlot } from "../../../__test_support__/fake_state/resources"; +import { FBSelect, NULL_CHOICE } from "../../../ui"; describe("", () => { const fakeProps = (): GantryMountedInputProps => ({ @@ -30,33 +29,6 @@ describe("", () => { }); }); -describe("", () => { - const fakeProps = (): UseCurrentLocationInputRowProps => ({ - botPosition: { x: undefined, y: undefined, z: undefined }, - onChange: jest.fn(), - }); - - it("renders", () => { - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("use current location"); - }); - - it("doesn't change value", () => { - const p = fakeProps(); - const wrapper = shallow(); - wrapper.find("button").simulate("click"); - expect(p.onChange).not.toHaveBeenCalled(); - }); - - it("changes value", () => { - const p = fakeProps(); - p.botPosition = { x: 0, y: 1, z: 2 }; - const wrapper = shallow(); - wrapper.find("button").simulate("click"); - expect(p.onChange).toHaveBeenCalledWith(p.botPosition); - }); -}); - describe("", () => { const fakeProps = (): SlotDirectionInputRowProps => ({ toolPulloutDirection: 0, @@ -65,7 +37,7 @@ describe("", () => { it("renders", () => { const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("change slot direction"); + expect(wrapper.text().toLowerCase()).toContain("change direction"); }); it("changes value by click", () => { @@ -89,6 +61,7 @@ describe("", () => { selectedTool: undefined, onChange: jest.fn(), filterSelectedTool: false, + isActive: jest.fn(), }); it("renders", () => { @@ -98,12 +71,13 @@ describe("", () => { it("handles missing tool data", () => { const p = fakeProps(); + p.filterSelectedTool = true; const tool = fakeTool(); tool.body.name = undefined; tool.body.id = undefined; p.tools = [tool]; const wrapper = shallow(); - expect(wrapper.find("FBSelect").props().list).toEqual([]); + expect(wrapper.find("FBSelect").props().list).toEqual([NULL_CHOICE]); }); it("handles missing selected tool data", () => { @@ -137,6 +111,7 @@ describe("", () => { selectedTool: undefined, onChange: jest.fn(), isExpress: false, + isActive: jest.fn(), }); it("renders", () => { @@ -164,6 +139,7 @@ describe("", () => { slotLocation: { x: 0, y: 0, z: 0 }, gantryMounted: false, onChange: jest.fn(), + botPosition: { x: undefined, y: undefined, z: undefined }, }); it("renders", () => { @@ -195,4 +171,40 @@ describe("", () => { expect(p.onChange).toHaveBeenCalledWith({ y: 2 }); expect(p.onChange).toHaveBeenCalledWith({ z: 3 }); }); + + it("doesn't use current coordinates", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("button").simulate("click"); + expect(p.onChange).not.toHaveBeenCalled(); + }); + + it("uses current coordinates", () => { + const p = fakeProps(); + p.botPosition = { x: 0, y: 1, z: 2 }; + const wrapper = shallow(); + wrapper.find("button").simulate("click"); + expect(p.onChange).toHaveBeenCalledWith(p.botPosition); + }); +}); + +describe("", () => { + const fakeProps = (): SlotEditRowsProps => ({ + toolSlot: fakeToolSlot(), + tools: [], + tool: undefined, + botPosition: { x: undefined, y: undefined, z: undefined }, + updateToolSlot: jest.fn(), + isExpress: false, + xySwap: false, + quadrant: 2, + isActive: () => false, + }); + + it("handles missing tool", () => { + const p = fakeProps(); + p.tool = undefined; + const wrapper = mount(); + expect(wrapper.text()).toContain("None"); + }); }); diff --git a/frontend/farm_designer/tools/add_tool.tsx b/frontend/farm_designer/tools/add_tool.tsx index 0eb08d86d..4078e7957 100644 --- a/frontend/farm_designer/tools/add_tool.tsx +++ b/frontend/farm_designer/tools/add_tool.tsx @@ -13,9 +13,10 @@ import { history } from "../../history"; import { selectAllTools } from "../../resources/selectors"; import { betterCompact } from "../../util"; import { - isExpressBoard, getFwHardwareValue + getFwHardwareValue } from "../../devices/components/firmware_hardware_support"; import { getFbosConfig } from "../../resources/getters"; +import { ToolSVG } from "../map/layers/tool_slots/tool_graphics"; export interface AddToolProps { dispatch: Function; @@ -25,6 +26,7 @@ export interface AddToolProps { export interface AddToolState { toolName: string; + toAdd: string[]; } export const mapStateToProps = (props: Everything): AddToolProps => ({ @@ -35,7 +37,19 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({ }); export class RawAddTool extends React.Component { - state: AddToolState = { toolName: "" }; + state: AddToolState = { toolName: "", toAdd: [] }; + + filterExisting = (n: string) => !this.props.existingToolNames.includes(n); + + add = (n: string) => this.filterExisting(n) && !this.state.toAdd.includes(n) && + this.setState({ toAdd: this.state.toAdd.concat([n]) }); + + remove = (n: string) => + this.setState({ toAdd: this.state.toAdd.filter(name => name != n) }); + + componentDidMount = () => this.setState({ + toAdd: this.stockToolNames().filter(this.filterExisting) + }); newTool = (name: string) => { this.props.dispatch(initSave("Tool", { name })); @@ -79,22 +93,38 @@ export class RawAddTool extends React.Component { } } + StockToolCheckbox = ({ toolName }: { toolName: string }) => { + const alreadyAdded = !this.filterExisting(toolName); + const checked = this.state.toAdd.includes(toolName) || alreadyAdded; + return
    + checked + ? this.remove(toolName) + : this.add(toolName)} /> +
    ; + } + AddStockTools = () =>
    - +
      - {this.stockToolNames().map(n =>
    • {n}
    • )} + {this.stockToolNames().map(n => +
    • + +

      this.setState({ toolName: n })}>{n}

      +
    • )}
    @@ -103,16 +133,16 @@ export class RawAddTool extends React.Component { return
    + - - this.setState({ toolName: e.currentTarget.value })} /> + + this.setState({ toolName: e.currentTarget.value })} />
    diff --git a/frontend/farm_designer/tools/add_tool_slot.tsx b/frontend/farm_designer/tools/add_tool_slot.tsx index ffd4062d8..381abcfdc 100644 --- a/frontend/farm_designer/tools/add_tool_slot.tsx +++ b/frontend/farm_designer/tools/add_tool_slot.tsx @@ -3,58 +3,31 @@ import { connect } from "react-redux"; import { DesignerPanel, DesignerPanelContent, DesignerPanelHeader } from "../designer_panel"; -import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { SaveBtn } from "../../ui"; -import { - SpecialStatus, TaggedTool, TaggedToolSlotPointer, FirmwareHardware -} from "farmbot"; +import { SpecialStatus, TaggedToolSlotPointer } from "farmbot"; import { init, save, edit, destroy } from "../../api/crud"; import { Panel } from "../panel_header"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; -import { - selectAllTools, maybeFindToolById, maybeGetToolSlot -} from "../../resources/selectors"; -import { BotPosition } from "../../devices/interfaces"; -import { validBotLocationData } from "../../util"; import { history } from "../../history"; import { SlotEditRows } from "./tool_slot_edit_components"; import { UUID } from "../../resources/interfaces"; import { - isExpressBoard, getFwHardwareValue + isExpressBoard } from "../../devices/components/firmware_hardware_support"; -import { getFbosConfig } from "../../resources/getters"; - -export interface AddToolSlotProps { - tools: TaggedTool[]; - dispatch: Function; - botPosition: BotPosition; - findTool(id: number): TaggedTool | undefined; - findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined; - firmwareHardware: FirmwareHardware | undefined; -} +import { AddToolSlotProps, mapStateToPropsAdd } from "./map_to_props_add_edit"; export interface AddToolSlotState { uuid: UUID | undefined; } -export const mapStateToProps = (props: Everything): AddToolSlotProps => ({ - tools: selectAllTools(props.resources.index), - dispatch: props.dispatch, - botPosition: validBotLocationData(props.bot.hardware.location_data).position, - findTool: (id: number) => maybeFindToolById(props.resources.index, id), - findToolSlot: (uuid: UUID | undefined) => - maybeGetToolSlot(props.resources.index, uuid), - firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), -}); - export class RawAddToolSlot extends React.Component { state: AddToolSlotState = { uuid: undefined }; componentDidMount() { const action = init("Point", { - pointer_type: "ToolSlot", name: "Tool Slot", radius: 0, meta: {}, + pointer_type: "ToolSlot", name: t("Slot"), radius: 0, meta: {}, x: 0, y: 0, z: 0, tool_id: undefined, pullout_direction: ToolPulloutDirection.NONE, gantry_mounted: isExpressBoard(this.props.firmwareHardware) ? true : false, @@ -95,9 +68,7 @@ export class RawAddToolSlot return @@ -108,6 +79,9 @@ export class RawAddToolSlot tools={this.props.tools} tool={this.tool} botPosition={this.props.botPosition} + xySwap={this.props.xySwap} + quadrant={this.props.quadrant} + isActive={this.props.isActive} updateToolSlot={this.updateSlot(this.toolSlot)} /> : "initializing"} @@ -116,4 +90,4 @@ export class RawAddToolSlot } } -export const AddToolSlot = connect(mapStateToProps)(RawAddToolSlot); +export const AddToolSlot = connect(mapStateToPropsAdd)(RawAddToolSlot); diff --git a/frontend/farm_designer/tools/edit_tool.tsx b/frontend/farm_designer/tools/edit_tool.tsx index 41b0fe387..27cfbbca7 100644 --- a/frontend/farm_designer/tools/edit_tool.tsx +++ b/frontend/farm_designer/tools/edit_tool.tsx @@ -6,16 +6,26 @@ import { import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { getPathArray } from "../../history"; -import { TaggedTool, SpecialStatus } from "farmbot"; -import { maybeFindToolById } from "../../resources/selectors"; +import { TaggedTool, SpecialStatus, TaggedToolSlotPointer } from "farmbot"; +import { + maybeFindToolById, getDeviceAccountSettings, selectAllToolSlotPointers +} from "../../resources/selectors"; import { SaveBtn } from "../../ui"; import { edit, destroy } from "../../api/crud"; import { history } from "../../history"; import { Panel } from "../panel_header"; +import { ToolSVG } from "../map/layers/tool_slots/tool_graphics"; +import { error } from "../../toast/toast"; + +export const isActive = (toolSlots: TaggedToolSlotPointer[]) => + (toolId: number | undefined) => + !!(toolId && toolSlots.map(x => x.body.tool_id).includes(toolId)); export interface EditToolProps { findTool(id: string): TaggedTool | undefined; dispatch: Function; + mountedToolId: number | undefined; + isActive(id: number | undefined): boolean; } export interface EditToolState { @@ -26,6 +36,9 @@ export const mapStateToProps = (props: Everything): EditToolProps => ({ findTool: (id: string) => maybeFindToolById(props.resources.index, parseInt(id)), dispatch: props.dispatch, + mountedToolId: getDeviceAccountSettings(props.resources.index) + .body.mounted_tool_id, + isActive: isActive(selectAllToolSlotPointers(props.resources.index)), }); export class RawEditTool extends React.Component { @@ -44,6 +57,11 @@ export class RawEditTool extends React.Component { const { dispatch } = this.props; const { toolName } = this.state; const panelName = "edit-tool"; + const isMounted = this.props.mountedToolId == tool.body.id; + const message = isMounted + ? t("Cannot delete while mounted.") + : t("Cannot delete while in a slot."); + const activeOrMounted = this.props.isActive(tool.body.id) || isMounted; return { backTo={"/app/designer/tools"} panel={Panel.Tools} /> + { }} status={SpecialStatus.DIRTY} /> diff --git a/frontend/farm_designer/tools/edit_tool_slot.tsx b/frontend/farm_designer/tools/edit_tool_slot.tsx index 210b283d3..7bf2bc05e 100644 --- a/frontend/farm_designer/tools/edit_tool_slot.tsx +++ b/frontend/farm_designer/tools/edit_tool_slot.tsx @@ -3,43 +3,18 @@ import { connect } from "react-redux"; import { DesignerPanel, DesignerPanelContent, DesignerPanelHeader } from "../designer_panel"; -import { Everything } from "../../interfaces"; import { t } from "../../i18next_wrapper"; import { getPathArray } from "../../history"; -import { TaggedToolSlotPointer, TaggedTool, FirmwareHardware } from "farmbot"; +import { TaggedToolSlotPointer } from "farmbot"; import { edit, save, destroy } from "../../api/crud"; import { history } from "../../history"; import { Panel } from "../panel_header"; -import { - maybeFindToolSlotById, selectAllTools, maybeFindToolById -} from "../../resources/selectors"; -import { BotPosition } from "../../devices/interfaces"; -import { validBotLocationData } from "../../util"; import { SlotEditRows } from "./tool_slot_edit_components"; import { moveAbs } from "../../devices/actions"; import { - getFwHardwareValue, isExpressBoard + isExpressBoard } from "../../devices/components/firmware_hardware_support"; -import { getFbosConfig } from "../../resources/getters"; - -export interface EditToolSlotProps { - findToolSlot(id: string): TaggedToolSlotPointer | undefined; - tools: TaggedTool[]; - findTool(id: number): TaggedTool | undefined; - dispatch: Function; - botPosition: BotPosition; - firmwareHardware: FirmwareHardware | undefined; -} - -export const mapStateToProps = (props: Everything): EditToolSlotProps => ({ - findToolSlot: (id: string) => - maybeFindToolSlotById(props.resources.index, parseInt(id)), - tools: selectAllTools(props.resources.index), - findTool: (id: number) => maybeFindToolById(props.resources.index, id), - dispatch: props.dispatch, - botPosition: validBotLocationData(props.bot.hardware.location_data).position, - firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), -}); +import { EditToolSlotProps, mapStateToPropsEdit } from "./map_to_props_add_edit"; export class RawEditToolSlot extends React.Component { @@ -65,7 +40,7 @@ export class RawEditToolSlot extends React.Component { return @@ -75,14 +50,20 @@ export class RawEditToolSlot extends React.Component { tools={this.props.tools} tool={this.tool} botPosition={this.props.botPosition} + xySwap={this.props.xySwap} + quadrant={this.props.quadrant} + isActive={this.props.isActive} updateToolSlot={this.updateSlot(toolSlot)} /> -

    {positionButtonTitle(props.botPosition)}

    - ; - export interface SlotDirectionInputRowProps { toolPulloutDirection: ToolPulloutDirection; onChange(update: { pullout_direction: ToolPulloutDirection }): void; @@ -51,7 +35,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.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) })} />; @@ -98,6 +83,7 @@ export interface ToolInputRowProps { selectedTool: TaggedTool | undefined; onChange(update: { tool_id: number }): void; isExpress: boolean; + isActive(id: number | undefined): boolean; } export const ToolInputRow = (props: ToolInputRowProps) => @@ -113,6 +99,7 @@ export const ToolInputRow = (props: ToolInputRowProps) => tools={props.tools} selectedTool={props.selectedTool} onChange={props.onChange} + isActive={props.isActive} filterSelectedTool={false} /> @@ -122,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)}

    +
    +
    + +
    ; @@ -150,26 +156,31 @@ export interface SlotEditRowsProps { 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 && { Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.title); mockDev = false; mockState.resources = buildResourceIndex([]); - expect(getTitles()).toContain("Add tools and tool slots"); + expect(getTitles()).toContain("Add tools and slots"); expect(getTitles()).not.toContain("Add seed containers"); const fbosConfig = fakeFbosConfig(); fbosConfig.body.firmware_hardware = "express_k10"; diff --git a/frontend/help/tours.ts b/frontend/help/tours.ts index fc4339da5..26f78eda6 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -46,14 +46,14 @@ const toolsStep = () => hasTools() : t(TourContent.ADD_TOOLS_AND_SLOTS), title: isExpress() ? t("Add seed containers and slots") - : t("Add tools and tool slots"), + : t("Add tools and slots"), }]; const toolSlotsStep = () => hasTools() ? [{ target: ".tool-slots", content: t(TourContent.ADD_TOOLS_AND_SLOTS), - title: t("Add tool slots"), + title: t("Add slots"), }] : []; diff --git a/frontend/resources/sequence_meta.ts b/frontend/resources/sequence_meta.ts index be03fff20..b137f2032 100644 --- a/frontend/resources/sequence_meta.ts +++ b/frontend/resources/sequence_meta.ts @@ -20,10 +20,14 @@ import { import { VariableNode } from "../sequences/locals_list/locals_list_support"; import { t } from "../i18next_wrapper"; +export interface Vector3Plus extends Vector3 { + gantry_mounted: boolean; +} + export interface SequenceMeta { celeryNode: VariableNode; dropdown: DropDownItem; - vector: Vector3 | undefined; + vector: Vector3 | Vector3Plus | undefined; default?: boolean; } diff --git a/frontend/sequences/locals_list/__tests__/location_form_list_test.ts b/frontend/sequences/locals_list/__tests__/location_form_list_test.ts index c857e97c6..2d431305e 100644 --- a/frontend/sequences/locals_list/__tests__/location_form_list_test.ts +++ b/frontend/sequences/locals_list/__tests__/location_form_list_test.ts @@ -82,7 +82,7 @@ describe("formatTool()", () => { const toolSlot = fakeToolSlot(); toolSlot.body.gantry_mounted = true; const ddi = formatTool(fakeTool(), toolSlot); - expect(ddi.label).toEqual("Foo (---, 0, 0)"); + expect(ddi.label).toEqual("Foo (gantry, 0, 0)"); }); }); diff --git a/frontend/sequences/locals_list/location_form_list.ts b/frontend/sequences/locals_list/location_form_list.ts index 2faa683ab..4d93c95e8 100644 --- a/frontend/sequences/locals_list/location_form_list.ts +++ b/frontend/sequences/locals_list/location_form_list.ts @@ -38,7 +38,7 @@ type DropdownHeadingId = export const NAME_MAP: Record = { "GenericPointer": "Map Points", "Plant": "Plants", - "ToolSlot": "Tool Slots", + "ToolSlot": "Slots", "Tool": "Tools and Seed Containers", "PointGroup": "Groups", "Other": "Other", @@ -100,24 +100,27 @@ export const formatTool = const { id, name } = tool.body; const coordinate = slot ? { - x: slot.body.gantry_mounted ? undefined : slot.body.x, + x: slot.body.x, y: slot.body.y, z: slot.body.z } : undefined; + const gantryMounted = !!slot?.body.gantry_mounted; return { - label: dropDownName((name || "Untitled tool"), coordinate), + label: dropDownName((name || "Untitled tool"), coordinate, gantryMounted), value: "" + id, headingId: TOOL }; }; /** Uniformly generate a label for things that have an X/Y/Z value. */ -export function dropDownName(name: string, v?: Record) { +export function dropDownName(name: string, v?: Record, + gantryMounted = false) { let label = name || "untitled"; if (v) { const labelFor = (axis: number | undefined) => isNumber(axis) ? axis : "---"; - label += ` (${labelFor(v.x)}, ${labelFor(v.y)}, ${labelFor(v.z)})`; + const xLabel = gantryMounted ? t("Gantry") : labelFor(v.x); + label += ` (${xLabel}, ${labelFor(v.y)}, ${labelFor(v.z)})`; } return capitalize(label); } @@ -125,8 +128,8 @@ export function dropDownName(name: string, v?: Record) export const ALL_POINT_LABELS = { "Plant": "All plants", "GenericPointer": "All map points", - "Tool": "All tools", - "ToolSlot": "All tool slots", + "Tool": "All tools and seed containers", + "ToolSlot": "All slots", }; export type EveryPointType = keyof typeof ALL_POINT_LABELS; diff --git a/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx index b34d3d098..d398b1b0f 100644 --- a/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx +++ b/frontend/sequences/step_tiles/__tests__/tile_move_absolute_test.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { TileMoveAbsolute } from "../tile_move_absolute"; import { mount, ReactWrapper } from "enzyme"; import { - fakeSequence, fakePoint, fakeTool + fakeSequence, fakePoint, fakeTool, fakeToolSlot } from "../../../__test_support__/fake_state/resources"; import { Coordinate, @@ -17,6 +17,7 @@ import { import { emptyState } from "../../../resources/reducer"; import { inputEvent } from "../../../__test_support__/fake_html_events"; import { StepParams } from "../../interfaces"; +import { buildResourceIndex } from "../../../__test_support__/resource_index_builder"; describe("", () => { const fakeProps = (): StepParams => { @@ -75,6 +76,25 @@ describe("", () => { checkField(block, 5, "z-offset", "6"); }); + it("disables x-offset", () => { + const p = fakeProps(); + const toolSlot = fakeToolSlot(); + toolSlot.body.gantry_mounted = true; + toolSlot.body.tool_id = 1; + const tool = fakeTool(); + tool.body.id = 1; + p.resources = buildResourceIndex([toolSlot, tool]).index; + const toolKind: Tool = { kind: "tool", args: { tool_id: 1 } }; + (p.currentStep as MoveAbsolute).args.location = toolKind; + const block = mount(); + const xOffsetInput = block.find("input").at(1); + expect(xOffsetInput.props().name).toEqual("offset-x"); + expect(xOffsetInput.props().disabled).toBeTruthy(); + const yOffsetInput = block.find("input").at(2); + expect(yOffsetInput.props().name).toEqual("offset-y"); + expect(yOffsetInput.props().disabled).toBeFalsy(); + }); + it("updates input value", () => { const tma = ordinaryMoveAbs(); const mock = jest.fn(); diff --git a/frontend/sequences/step_tiles/tile_move_absolute.tsx b/frontend/sequences/step_tiles/tile_move_absolute.tsx index 5a87fe7b9..a909cfe47 100644 --- a/frontend/sequences/step_tiles/tile_move_absolute.tsx +++ b/frontend/sequences/step_tiles/tile_move_absolute.tsx @@ -11,7 +11,7 @@ import { ToolTips } from "../../constants"; import { StepWrapper, StepHeader, StepContent } from "../step_ui"; import { StepInputBox } from "../inputs/step_input_box"; import { - determineDropdown, determineVector + determineDropdown, determineVector, Vector3Plus } from "../../resources/sequence_meta"; import { LocationForm } from "../locals_list/location_form"; import { @@ -75,11 +75,16 @@ export class TileMoveAbsolute extends React.Component }; } - get vector(): Vector3 | undefined { + get vector(): Vector3 | Vector3Plus | undefined { const sequenceUuid = this.props.currentSequence.uuid; return determineVector(this.celeryNode, this.props.resources, sequenceUuid); } + get gantryMounted() { + return this.vector && ("gantry_mounted" in this.vector) + && this.vector.gantry_mounted; + } + LocationForm = () => {t("{{axis}}-Offset", { axis })} diff --git a/package.json b/package.json index d6fa95495..512d04ad1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "sass-lint": "./node_modules/sass-lint/bin/sass-lint.js -c .sass-lint.yml -v -q", "sass-check": "./node_modules/sass/sass.js --no-source-map frontend/css/_index.scss sass.log", "translation-check": " ./node_modules/jshint/bin/jshint --config public/app-resources/languages/.config public/app-resources/languages/*.js*", - "linters": "npm run typecheck && npm run tslint && npm run sass-lint && npm run sass-check && npm run translation-check" + "linters": "npm run typecheck; npm run tslint; npm run sass-lint; npm run sass-check; npm run translation-check" }, "keywords": [ "farmbot" From 1a4a106179833c01b7cb4da7bf01b00df5d7c352 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Fri, 21 Feb 2020 20:13:29 -0600 Subject: [PATCH 13/26] days => days_ago --- app/models/point_group.rb | 2 +- app/mutations/point_groups/helpers.rb | 2 +- frontend/__test_support__/fake_state/resources.ts | 2 +- .../point_groups/criteria/__tests__/apply_test.ts | 6 +++--- .../point_groups/criteria/__tests__/edit_test.ts | 2 +- .../point_groups/criteria/__tests__/show_test.tsx | 2 +- frontend/farm_designer/point_groups/criteria/apply.ts | 4 ++-- .../farm_designer/point_groups/criteria/interfaces.ts | 2 +- frontend/farm_designer/point_groups/criteria/show.tsx | 8 ++++---- package.json | 2 +- spec/controllers/api/point_groups/create_spec.rb | 4 ++-- spec/controllers/api/point_groups/update_spec.rb | 6 +++--- 12 files changed, 21 insertions(+), 21 deletions(-) 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/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/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index 641cba1bb..cbddcceae 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -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/farm_designer/point_groups/criteria/__tests__/apply_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts index 48b10dca6..a7ac5efdb 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts +++ b/frontend/farm_designer/point_groups/criteria/__tests__/apply_test.ts @@ -57,7 +57,7 @@ describe("selectPointsByCriteria()", () => { it("matches age greater than 1 day old", () => { const criteria = fakeCriteria(); - criteria.day = { days: 1, op: ">" }; + criteria.day = { days_ago: 1, op: ">" }; const matchingPoint = fakePoint(); matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z"; const otherPoint = fakePoint(); @@ -70,7 +70,7 @@ describe("selectPointsByCriteria()", () => { it("matches age less than 1 day old", () => { const criteria = fakeCriteria(); - criteria.day = { days: 1, op: "<" }; + criteria.day = { days_ago: 1, op: "<" }; const matchingPoint = fakePoint(); matchingPoint.body.created_at = "2020-02-20T20:00:00.000Z"; const otherPoint = fakePoint(); @@ -83,7 +83,7 @@ describe("selectPointsByCriteria()", () => { it("matches planted date less than 1 day old", () => { const criteria = fakeCriteria(); - criteria.day = { days: 1, op: "<" }; + criteria.day = { days_ago: 1, op: "<" }; const matchingPoint = fakePlant(); matchingPoint.body.planted_at = "2020-02-20T20:00:00.000Z"; matchingPoint.body.created_at = "2020-01-20T20:00:00.000Z"; diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts b/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts index 04fc25cf3..572e4ca91 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts +++ b/frontend/farm_designer/point_groups/criteria/__tests__/edit_test.ts @@ -36,7 +36,7 @@ describe("editCriteria()", () => { it("edits criteria: full update", () => { const group = fakePointGroup(); const criteria: PointGroup["criteria"] = { - day: { days: 1, op: "<" }, + day: { days_ago: 1, op: "<" }, string_eq: { openfarm_slug: ["slug"] }, number_eq: { x: [0] }, number_gt: { x: 0 }, diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx index 4e5550fe9..626fabd24 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx +++ b/frontend/farm_designer/point_groups/criteria/__tests__/show_test.tsx @@ -100,7 +100,7 @@ describe("", () => { currentTarget: { value: "1" } }); const expectedBody = cloneDeep(p.group.body); - expectedBody.criteria.day.days = 1; + expectedBody.criteria.day.days_ago = 1; expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody); }); diff --git a/frontend/farm_designer/point_groups/criteria/apply.ts b/frontend/farm_designer/point_groups/criteria/apply.ts index 87ea058c3..10aa45403 100644 --- a/frontend/farm_designer/point_groups/criteria/apply.ts +++ b/frontend/farm_designer/point_groups/criteria/apply.ts @@ -30,11 +30,11 @@ const checkCriteria = ? point.body.planted_at : point.body.created_at); const compareDate = moment(now) - .subtract(criteria[criteriaKey].days, "days"); + .subtract(criteria[criteriaKey].days_ago, "days"); const matchesDays = criteria[criteriaKey].op == "<" ? pointDate.isAfter(compareDate) : pointDate.isBefore(compareDate); - return matchesDays || !criteria[criteriaKey].days; + return matchesDays || !criteria[criteriaKey].days_ago; } }; diff --git a/frontend/farm_designer/point_groups/criteria/interfaces.ts b/frontend/farm_designer/point_groups/criteria/interfaces.ts index 70c1d90ae..4ca2bd4fc 100644 --- a/frontend/farm_designer/point_groups/criteria/interfaces.ts +++ b/frontend/farm_designer/point_groups/criteria/interfaces.ts @@ -2,7 +2,7 @@ import { TaggedPointGroup } from "farmbot"; import { PointGroup } from "farmbot/dist/resources/api_resources"; export const DEFAULT_CRITERIA: Readonly = { - day: { op: "<", days: 0 }, + day: { op: "<", days_ago: 0 }, number_eq: {}, number_gt: {}, number_lt: {}, diff --git a/frontend/farm_designer/point_groups/criteria/show.tsx b/frontend/farm_designer/point_groups/criteria/show.tsx index bc90265f6..ad79f2326 100644 --- a/frontend/farm_designer/point_groups/criteria/show.tsx +++ b/frontend/farm_designer/point_groups/criteria/show.tsx @@ -105,16 +105,16 @@ export const DaySelection = (props: CriteriaSelectionProps) => { selectedItem={DAY_OPERATOR_DDI_LOOKUP()[dayCriteria.op]} onChange={ddi => dispatch(editCriteria(group, { day: { - days: dayCriteria.days, + days_ago: dayCriteria.days_ago, op: ddi.value as PointGroup["criteria"]["day"]["op"] } }))} /> - { + { const { op } = dayCriteria; - const days = parseInt(e.currentTarget.value); - dispatch(editCriteria(group, { day: { days, op } })); + const days_ago = parseInt(e.currentTarget.value); + dispatch(editCriteria(group, { day: { days_ago, op } })); }} /> diff --git a/package.json b/package.json index d6fa95495..304669d29 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "coveralls": "3.0.9", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", - "farmbot": "9.1.0", + "farmbot": "9.1.2", "i18next": "19.0.3", "install": "0.13.0", "lodash": "4.17.15", diff --git a/spec/controllers/api/point_groups/create_spec.rb b/spec/controllers/api/point_groups/create_spec.rb index 2a534624f..6ba7d173d 100644 --- a/spec/controllers/api/point_groups/create_spec.rb +++ b/spec/controllers/api/point_groups/create_spec.rb @@ -70,7 +70,7 @@ describe Api::PointGroupsController do }, day: { op: "<", - days: 0, + days_ago: 0, }, }, } @@ -85,7 +85,7 @@ describe Api::PointGroupsController do expect(hash.dig(:number_gt, :x)).to eq(1) expect(hash.dig(:number_gt, :y)).to eq(1) expect(hash.dig(:day, :op)).to eq("<") - expect(hash.dig(:day, :days)).to eq(0) + expect(hash.dig(:day, :days_ago)).to eq(0) expect(hash.dig(:string_eq, :openfarm_slug)).to eq(["carrot"]) end end diff --git a/spec/controllers/api/point_groups/update_spec.rb b/spec/controllers/api/point_groups/update_spec.rb index 0ea2c04ad..87db51144 100644 --- a/spec/controllers/api/point_groups/update_spec.rb +++ b/spec/controllers/api/point_groups/update_spec.rb @@ -57,7 +57,7 @@ describe Api::PointGroupsController do number_eq: { z: [24, 25, 26] }, number_lt: { x: 4, y: 4 }, number_gt: { x: 1, y: 1 }, - day: { op: "<", days: 0 }, + day: { op: "<", days_ago: 0 }, }, } pg = PointGroups::Create.run!(initial_params) @@ -68,12 +68,12 @@ describe Api::PointGroupsController do number_eq: { x: [42, 52, 62] }, number_lt: { y: 8 }, number_gt: { z: 2 }, - day: { op: ">", days: 10 }, + day: { op: ">", days_ago: 10 }, }, } put :update, body: payload.to_json, format: :json, params: { id: pg.id } expect(response.status).to eq(200) - expect(json.dig(:criteria, :day, :days)).to eq(10) + expect(json.dig(:criteria, :day, :days_ago)).to eq(10) expect(json.dig(:criteria, :day, :op)).to eq(">") expect(json.dig(:criteria, :number_eq, :x)).to eq([42, 52, 62]) expect(json.dig(:criteria, :number_eq, :z)).to eq(nil) From 19eebde8e2116aa7ac17ccf341d5f27c15efdd16 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Mon, 24 Feb 2020 08:55:37 -0800 Subject: [PATCH 14/26] dep updates (fe) --- package.json | 22 +++++++++++----------- tsconfig.json | 1 - 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 58e964b81..58a807c65 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,18 @@ "author": "farmbot.io", "license": "MIT", "dependencies": { - "@babel/core": "7.8.3", + "@babel/core": "7.8.4", "@blueprintjs/core": "3.23.1", "@blueprintjs/datetime": "3.15.2", "@blueprintjs/select": "3.11.2", - "@types/enzyme": "3.10.4", - "@types/jest": "24.9.1", + "@types/enzyme": "3.10.5", + "@types/jest": "25.1.3", "@types/lodash": "4.14.149", "@types/markdown-it": "0.0.9", "@types/moxios": "0.4.9", - "@types/node": "13.5.0", + "@types/node": "13.7.4", "@types/promise-timeout": "1.3.0", - "@types/react": "16.9.19", + "@types/react": "16.9.22", "@types/react-color": "3.0.1", "@types/react-dom": "16.9.5", "@types/react-redux": "7.1.7", @@ -46,7 +46,7 @@ "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", "farmbot": "9.1.2", - "i18next": "19.0.3", + "i18next": "19.3.1", "install": "0.13.0", "lodash": "4.17.15", "markdown-it": "10.0.0", @@ -54,7 +54,7 @@ "moment": "2.24.0", "moxios": "0.4.0", "mqtt": "3.0.0", - "npm": "6.13.6", + "npm": "6.13.7", "parcel-bundler": "1.12.4", "promise-timeout": "1.3.0", "raf": "3.4.1", @@ -63,7 +63,7 @@ "react-color": "2.18.0", "react-dom": "16.12.0", "react-joyride": "2.2.1", - "react-redux": "7.1.3", + "react-redux": "7.2.0", "react-test-renderer": "16.12.0", "react-transition-group": "4.3.0", "redux": "4.0.5", @@ -71,10 +71,10 @@ "redux-thunk": "2.3.0", "sass-lint": "1.13.1", "takeme": "0.11.3", - "ts-jest": "25.0.0", + "ts-jest": "25.2.1", "ts-lint": "4.5.1", "tslint": "6.0.0", - "typescript": "3.7.5", + "typescript": "3.8.2", "which": "2.0.2" }, "devDependencies": { @@ -83,7 +83,7 @@ "jest-junit": "10.0.0", "jest-skipped-reporter": "0.0.5", "jshint": "2.11.0", - "madge": "3.6.0", + "madge": "3.7.0", "sass": "1.25.0" } } diff --git a/tsconfig.json b/tsconfig.json index 569f7f997..2c63ffef7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "incremental": true, "lib": [ "es7", "dom", From 4a0035b9ebc5872dc5facc2d7f28b043702406a5 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Mon, 24 Feb 2020 08:55:57 -0800 Subject: [PATCH 15/26] minor bug fixes --- frontend/css/farm_designer/farm_designer_panels.scss | 4 ++-- .../devices/components/fbos_settings/fbos_details.tsx | 9 ++++++--- frontend/devices/connectivity/connectivity.tsx | 7 +++++-- .../__tests__/list_and_label_support_test.tsx | 4 +++- frontend/devices/pin_bindings/list_and_label_support.tsx | 2 +- .../tools/__tests__/tool_slot_edit_components_test.tsx | 1 + frontend/farm_designer/tools/index.tsx | 6 ++++-- .../farm_designer/tools/tool_slot_edit_components.tsx | 7 +++++-- 8 files changed, 27 insertions(+), 13 deletions(-) diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index a915f1f5e..6c5a5614d 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -552,7 +552,7 @@ } .tool-slots-panel-content, .tools-panel-content { - max-height: calc(100vh - 15rem); + max-height: calc(100vh - 19rem); overflow-y: auto; overflow-x: hidden; .tool-search-item, @@ -582,7 +582,7 @@ } } i { - line-height: 2.5rem; + line-height: 2rem; } } svg { diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index c4b138cbd..10cee6822 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -55,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} (${percentString})` : "N/A"} + {wifiStrength ? numberDisplay : "N/A"}

    {wifiStrength &&
    @@ -287,7 +290,7 @@ export function FbosDetails(props: FbosDetailsProps) { {isNumber(disk_usage) &&

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

    } {isNumber(cpu_usage) &&

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

    } - @@ -42,7 +44,8 @@ export class Connectivity
    - +
    diff --git a/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx b/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx index 6742019cf..24a8443b6 100644 --- a/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx +++ b/frontend/devices/pin_bindings/__tests__/list_and_label_support_test.tsx @@ -1,4 +1,6 @@ -import { sortByNameAndPin, ButtonPin, getSpecialActionLabel } from "../list_and_label_support"; +import { + sortByNameAndPin, ButtonPin, getSpecialActionLabel +} from "../list_and_label_support"; import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources"; describe("sortByNameAndPin()", () => { diff --git a/frontend/devices/pin_bindings/list_and_label_support.tsx b/frontend/devices/pin_bindings/list_and_label_support.tsx index ddc7be7c6..5d803b118 100644 --- a/frontend/devices/pin_bindings/list_and_label_support.tsx +++ b/frontend/devices/pin_bindings/list_and_label_support.tsx @@ -92,7 +92,7 @@ export const reservedPiGPIO = piI2c0Pins; 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.btn3]: t("Button {{ num }}", { num: 3 }), [ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }), [ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }), }); diff --git a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx index e75dbf479..1ee855342 100644 --- a/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx +++ b/frontend/farm_designer/tools/__tests__/tool_slot_edit_components_test.tsx @@ -62,6 +62,7 @@ describe("", () => { onChange: jest.fn(), filterSelectedTool: false, isActive: jest.fn(), + filterActiveTools: true, }); it("renders", () => { diff --git a/frontend/farm_designer/tools/index.tsx b/frontend/farm_designer/tools/index.tsx index 3e4adbb4b..222c4face 100644 --- a/frontend/farm_designer/tools/index.tsx +++ b/frontend/farm_designer/tools/index.tsx @@ -137,7 +137,8 @@ export class RawTools extends React.Component { this.props.dispatch(save(this.props.device.uuid)); }} isActive={this.props.isActive} - filterSelectedTool={true} /> + filterSelectedTool={true} + filterActiveTools={false} />

    {t("status")}: {toolStatus(this.toolVerificationValue)}

    diff --git a/frontend/farm_designer/tools/tool_slot_edit_components.tsx b/frontend/farm_designer/tools/tool_slot_edit_components.tsx index cf63a55e1..903088e82 100644 --- a/frontend/farm_designer/tools/tool_slot_edit_components.tsx +++ b/frontend/farm_designer/tools/tool_slot_edit_components.tsx @@ -57,6 +57,7 @@ export interface ToolSelectionProps { onChange(update: { tool_id: number }): void; filterSelectedTool: boolean; isActive(id: number | undefined): boolean; + filterActiveTools: boolean; } export const ToolSelection = (props: ToolSelectionProps) => @@ -64,7 +65,8 @@ export const ToolSelection = (props: ToolSelectionProps) => list={([NULL_CHOICE] as DropDownItem[]).concat(props.tools .filter(tool => !props.filterSelectedTool || tool.body.id != props.selectedTool?.body.id) - .filter(tool => !props.isActive(tool.body.id)) + .filter(tool => !props.filterActiveTools + || !props.isActive(tool.body.id)) .map(tool => ({ label: tool.body.name || "untitled", value: tool.body.id || 0, @@ -100,7 +102,8 @@ export const ToolInputRow = (props: ToolInputRowProps) => selectedTool={props.selectedTool} onChange={props.onChange} isActive={props.isActive} - filterSelectedTool={false} /> + filterSelectedTool={false} + filterActiveTools={true} />
    ; From 6bc0034d676cae8da92666c5a7ad8f052ca9be57 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Wed, 26 Feb 2020 08:18:25 -0600 Subject: [PATCH 16/26] Dep updates --- Gemfile.lock | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f0f340215..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) @@ -174,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) @@ -183,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) @@ -202,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) @@ -252,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) @@ -276,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) @@ -285,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) From 9bd98aca1e573e8cc069eafd0116fbaf50c2eb5b Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 26 Feb 2020 10:10:59 -0800 Subject: [PATCH 17/26] misc updates --- frontend/controls/controls.tsx | 4 +- frontend/controls/move/bot_position_rows.tsx | 6 +-- frontend/controls/move/settings_menu.tsx | 4 +- .../peripherals/__tests__/index_test.tsx | 10 +++++ frontend/controls/peripherals/index.tsx | 2 +- .../controls/sensors/__tests__/index_test.tsx | 15 ++++++- frontend/controls/sensors/index.tsx | 18 ++++----- .../farm_designer/farm_designer_panels.scss | 7 ++++ frontend/css/global.scss | 18 +++++++++ frontend/css/sequences.scss | 17 ++++++-- .../__tests__/hardware_settings_test.tsx | 39 +++++++++++++++++++ .../components/firmware_hardware_support.ts | 20 +++++++++- .../devices/components/hardware_settings.tsx | 38 ++++++++++++++++-- .../__tests__/pin_bindings_test.tsx | 1 + .../components/hardware_settings/encoders.tsx | 20 +++++----- .../homing_and_calibration.tsx | 8 ++-- .../hardware_settings/pin_bindings.tsx | 5 ++- frontend/devices/components/interfaces.ts | 1 + .../connectivity/__tests__/diagram_test.tsx | 7 ++-- .../__tests__/pin_bindings_test.tsx | 1 + .../tagged_pin_binding_init_test.tsx | 30 +++++++++----- frontend/devices/pin_bindings/interfaces.ts | 2 + .../devices/pin_bindings/pin_bindings.tsx | 5 ++- .../pin_bindings/tagged_pin_binding_init.tsx | 14 +++++-- .../farm_designer/__tests__/plant_test.ts | 19 +++++++++ .../farm_designer/__tests__/reducer_test.ts | 13 +++++++ frontend/farm_designer/designer_panel.tsx | 2 +- .../farm_events/edit_farm_event.tsx | 4 +- .../farm_events/edit_fe_form.tsx | 32 ++++++++------- .../farm_designer/farm_events/farm_events.tsx | 2 +- .../images/__tests__/map_image_test.tsx | 36 +++++++++-------- frontend/farm_designer/panel_header.tsx | 8 +++- .../point_groups/group_detail_active.tsx | 2 +- .../points/__tests__/create_points_test.tsx | 1 + .../farm_designer/points/create_points.tsx | 9 ++++- .../points/point_inventory_item.tsx | 2 +- .../saved_gardens/garden_edit.tsx | 2 +- .../saved_gardens/garden_list.tsx | 2 +- .../tools/__tests__/index_test.tsx | 2 +- frontend/farm_designer/tools/index.tsx | 6 +-- frontend/folders/__tests__/component_test.tsx | 4 +- frontend/folders/component.tsx | 4 +- frontend/folders/data_transfer.ts | 9 +++-- frontend/help/tours.ts | 14 +++---- frontend/logs/__tests__/index_test.tsx | 10 ++++- .../components/__tests__/filter_menu_test.tsx | 7 ++-- .../components/__tests__/logs_table_test.tsx | 20 ++++++++++ frontend/logs/components/filter_menu.tsx | 2 +- frontend/logs/components/logs_table.tsx | 18 ++++++++- frontend/logs/index.tsx | 18 ++++++++- frontend/logs/interfaces.ts | 1 + frontend/nav/additional_menu.tsx | 8 ++-- frontend/read_only_mode/index.tsx | 16 +++----- frontend/regimens/index.tsx | 3 +- frontend/regimens/list/index.tsx | 4 +- frontend/resources/selectors_by_id.ts | 16 -------- .../sequences/step_tiles/tile_if/index.tsx | 2 +- frontend/ui/back_arrow.tsx | 2 +- 58 files changed, 424 insertions(+), 168 deletions(-) create mode 100644 frontend/farm_designer/__tests__/plant_test.ts create mode 100644 frontend/logs/components/__tests__/logs_table_test.tsx diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index 5195c58ba..5149af504 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -10,6 +10,7 @@ import { Move } from "./move/move"; import { BooleanSetting } from "../session_keys"; import { SensorReadings } from "./sensor_readings/sensor_readings"; import { isBotOnline } from "../devices/must_be_online"; +import { hasSensors } from "../devices/components/firmware_hardware_support"; /** Controls page. */ export class RawControls extends React.Component { @@ -24,7 +25,8 @@ export class RawControls extends React.Component { } get hideSensors() { - return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors); + return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors) + || !hasSensors(this.props.firmwareHardware); } move = () => { - {!isExpressBoard(props.firmwareHardware) && + {hasEncoders(props.firmwareHardware) && getValue(BooleanSetting.scaled_encoders) && } - {!isExpressBoard(props.firmwareHardware) && + {hasEncoders(props.firmwareHardware) && getValue(BooleanSetting.raw_encoders) && @@ -36,7 +36,7 @@ export const MoveWidgetSettingsMenu = ( - {!isExpressBoard(firmwareHardware) && + {hasEncoders(firmwareHardware) &&

    {t("Display Encoder Data")}

    ", () => { clickButton(wrapper, 3, "stock"); expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds); }); + + it("hides stock button", () => { + const p = fakeProps(); + p.firmwareHardware = "none"; + const wrapper = mount(); + wrapper.setState({ isEditing: true }); + const btn = wrapper.find("button").at(3); + expect(btn.text().toLowerCase()).toContain("stock"); + expect(btn.props().hidden).toBeTruthy(); + }); }); diff --git a/frontend/controls/peripherals/index.tsx b/frontend/controls/peripherals/index.tsx index 69decd905..de5e6eba3 100644 --- a/frontend/controls/peripherals/index.tsx +++ b/frontend/controls/peripherals/index.tsx @@ -108,7 +108,7 @@ export class Peripherals - {!isExpressBoard(this.props.firmwareHardware) && - } + {this.showPins()} diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index 6c5a5614d..e67e5ac60 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -958,3 +958,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 e8530d2c6..9b63e725d 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -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; @@ -1654,3 +1666,9 @@ textarea:focus { background-color: transparent; box-shadow: none; } + +.read-only-icon { + margin: 9px 0px 0px 9px; + float: right; + box-sizing: inherit; +} 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/devices/components/__tests__/hardware_settings_test.tsx b/frontend/devices/components/__tests__/hardware_settings_test.tsx index 427070cce..840ada969 100644 --- a/frontend/devices/components/__tests__/hardware_settings_test.tsx +++ b/frontend/devices/components/__tests__/hardware_settings_test.tsx @@ -12,6 +12,8 @@ import { clickButton } from "../../../__test_support__/helpers"; import { buildResourceIndex } from "../../../__test_support__/resource_index_builder"; +import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; +import { Color } from "../../../ui"; describe("", () => { const fakeProps = (): HardwareSettingsProps => ({ @@ -68,4 +70,41 @@ describe("", () => { const wrapper = shallow(); expect(wrapper.html()).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/firmware_hardware_support.ts b/frontend/devices/components/firmware_hardware_support.ts index 71756e221..d3c4d95e3 100644 --- a/frontend/devices/components/firmware_hardware_support.ts +++ b/frontend/devices/components/firmware_hardware_support.ts @@ -16,15 +16,31 @@ export const getFwHardwareValue = return isFwHardwareValue(value) ? value : undefined; }; -const TMC_BOARDS = ["express_k10", "farmduino_k15"]; +const NO_BUTTONS = ["arduino", "farmduino", "none"]; const EXPRESS_BOARDS = ["express_k10"]; +const NO_SENSORS = [...EXPRESS_BOARDS]; +const NO_ENCODERS = [...EXPRESS_BOARDS]; +const NO_TOOLS = [...EXPRESS_BOARDS]; +const NO_TMC = ["arduino", "farmduino", "farmduino_k14"]; export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) => - !!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware)); + !firmwareHardware || !NO_TMC.includes(firmwareHardware); export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) => !!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware)); +export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) => + !firmwareHardware || !NO_BUTTONS.includes(firmwareHardware); + +export const hasEncoders = (firmwareHardware: FirmwareHardware | undefined) => + !firmwareHardware || !NO_ENCODERS.includes(firmwareHardware); + +export const hasSensors = (firmwareHardware: FirmwareHardware | undefined) => + !firmwareHardware || !NO_SENSORS.includes(firmwareHardware); + +export const hasUTM = (firmwareHardware: FirmwareHardware | undefined) => + !firmwareHardware || !NO_TOOLS.includes(firmwareHardware); + export const getBoardIdentifier = (firmwareVersion: string | undefined): string => firmwareVersion ? firmwareVersion.split(".")[3] : "undefined"; diff --git a/frontend/devices/components/hardware_settings.tsx b/frontend/devices/components/hardware_settings.tsx index 1f12f68fe..f4a2c6612 100644 --- a/frontend/devices/components/hardware_settings.tsx +++ b/frontend/devices/components/hardware_settings.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { MCUFactoryReset, bulkToggleControlPanel } from "../actions"; -import { Widget, WidgetHeader, WidgetBody } from "../../ui/index"; -import { HardwareSettingsProps } from "../interfaces"; +import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index"; +import { HardwareSettingsProps, SourceFwConfig } from "../interfaces"; import { isBotOnline } from "../must_be_online"; import { ToolTips } from "../../constants"; import { DangerZone } from "./hardware_settings/danger_zone"; @@ -19,6 +19,8 @@ import { t } from "../../i18next_wrapper"; import { PinBindings } from "./hardware_settings/pin_bindings"; import { ErrorHandling } from "./hardware_settings/error_handling"; import { maybeOpenPanel } from "./maybe_highlight"; +import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; +import type { McuParamName } from "farmbot"; export class HardwareSettings extends React.Component { @@ -36,7 +38,10 @@ export class HardwareSettings extends const botDisconnected = !isBotOnline(sync_status, botToMqttStatus); const commonProps = { dispatch, controlPanelState }; return - + + +
    ; 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..432e9aa14 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -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/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/edit_farm_event.tsx b/frontend/farm_designer/farm_events/edit_farm_event.tsx index 5a86238d3..a89af9150 100644 --- a/frontend/farm_designer/farm_events/edit_farm_event.tsx +++ b/frontend/farm_designer/farm_events/edit_farm_event.tsx @@ -23,7 +23,7 @@ export class RawEditFarmEvent extends React.Component + title={t("Edit event")} /> executableOptions={this.props.executableOptions} dispatch={this.props.dispatch} findExecutable={this.props.findExecutable} - title={t("Edit Event")} + title={t("Edit event")} deleteBtn={true} timeSettings={this.props.timeSettings} autoSyncEnabled={this.props.autoSyncEnabled} diff --git a/frontend/farm_designer/farm_events/edit_fe_form.tsx b/frontend/farm_designer/farm_events/edit_fe_form.tsx index d2bb3d66b..681ef3a91 100644 --- a/frontend/farm_designer/farm_events/edit_fe_form.tsx +++ b/frontend/farm_designer/farm_events/edit_fe_form.tsx @@ -41,6 +41,7 @@ import { } 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 @@ -360,19 +361,24 @@ export class EditFEForm extends React.Component { render() { const { farmEvent } = this.props; return
    - this.commitViewModel()}> - - + + this.commitViewModel()}> + + + + +
    { 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/help/tours.ts b/frontend/help/tours.ts index 26f78eda6..505f9a626 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -7,7 +7,7 @@ import { selectAllTools } from "../resources/selectors"; import { store } from "../redux/store"; import { getFbosConfig } from "../resources/getters"; import { - isExpressBoard, getFwHardwareValue + getFwHardwareValue, hasUTM } from "../devices/components/firmware_hardware_support"; export enum Tours { @@ -25,26 +25,26 @@ export const tourNames = () => [ const hasTools = () => selectAllTools(store.getState().resources.index).length > 0; -const isExpress = () => - isExpressBoard(getFwHardwareValue( +const noUTM = () => + !hasUTM(getFwHardwareValue( getFbosConfig(store.getState().resources.index))); const toolsStep = () => hasTools() ? [{ target: ".tools", - content: isExpress() + content: noUTM() ? t(TourContent.ADD_SEED_CONTAINERS) : t(TourContent.ADD_TOOLS), - title: isExpress() + title: noUTM() ? t("Add seed containers") : t("Add tools and seed containers"), }] : [{ target: ".tools", - content: isExpress() + content: noUTM() ? t(TourContent.ADD_SEED_CONTAINERS_AND_SLOTS) : t(TourContent.ADD_TOOLS_AND_SLOTS), - title: isExpress() + title: noUTM() ? t("Add seed containers and slots") : t("Add tools and slots"), }]; diff --git a/frontend/logs/__tests__/index_test.tsx b/frontend/logs/__tests__/index_test.tsx index 9d68e66af..66c4d5498 100644 --- a/frontend/logs/__tests__/index_test.tsx +++ b/frontend/logs/__tests__/index_test.tsx @@ -1,7 +1,7 @@ const mockStorj: Dictionary = {}; import * as React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { RawLogs as Logs } from "../index"; import { ToolTips } from "../../constants"; import { TaggedLog, Dictionary } from "farmbot"; @@ -172,4 +172,12 @@ describe("", () => { wrapper.setState({ markdown: false }); expect(wrapper.html()).not.toContain("message"); }); + + it("changes search term", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("input").first().simulate("change", + { currentTarget: { value: "one" } }); + expect(wrapper.state().searchTerm).toEqual("one"); + }); }); diff --git a/frontend/logs/components/__tests__/filter_menu_test.tsx b/frontend/logs/components/__tests__/filter_menu_test.tsx index 968fcd1b4..e885bdccb 100644 --- a/frontend/logs/components/__tests__/filter_menu_test.tsx +++ b/frontend/logs/components/__tests__/filter_menu_test.tsx @@ -9,7 +9,8 @@ const logTypes = MESSAGE_TYPES; describe("", () => { const fakeState: LogsState = { - autoscroll: true, markdown: false, success: 1, busy: 1, warn: 1, + autoscroll: true, markdown: false, searchTerm: "", + success: 1, busy: 1, warn: 1, error: 1, info: 1, fun: 1, debug: 1, assertion: 1, }; @@ -24,7 +25,7 @@ describe("", () => { const wrapper = mount(); logTypes.filter(x => x !== "assertion").map(string => expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); - ["autoscroll", "markdown"].map(string => + ["autoscroll", "markdown", "searchTerm"].map(string => expect(wrapper.text().toLowerCase()).not.toContain(string)); }); @@ -34,7 +35,7 @@ describe("", () => { const wrapper = mount(); logTypes.map(string => expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); - ["autoscroll", "markdown"].map(string => + ["autoscroll", "markdown", "searchTerm"].map(string => expect(wrapper.text().toLowerCase()).not.toContain(string)); }); diff --git a/frontend/logs/components/__tests__/logs_table_test.tsx b/frontend/logs/components/__tests__/logs_table_test.tsx new file mode 100644 index 000000000..fa5eff31a --- /dev/null +++ b/frontend/logs/components/__tests__/logs_table_test.tsx @@ -0,0 +1,20 @@ +import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; +import { bySearchTerm } from "../logs_table"; +import { fakeLog } from "../../../__test_support__/fake_state/resources"; + +describe("bySearchTerm()", () => { + it("includes log", () => { + const log = fakeLog(); + log.body.message = "include this log"; + const result = bySearchTerm("include", fakeTimeSettings())(log); + expect(result).toBeTruthy(); + }); + + it("excludes log", () => { + const log = fakeLog(); + log.body.created_at = undefined; + log.body.message = "exclude this log"; + const result = bySearchTerm("include", fakeTimeSettings())(log); + expect(result).toBeFalsy(); + }); +}); diff --git a/frontend/logs/components/filter_menu.tsx b/frontend/logs/components/filter_menu.tsx index 12e8c6cd9..74cafce74 100644 --- a/frontend/logs/components/filter_menu.tsx +++ b/frontend/logs/components/filter_menu.tsx @@ -28,7 +28,7 @@ const menuSort = (a: string, b: string) => export const filterStateKeys = (state: LogsState, shouldDisplay: ShouldDisplay) => Object.keys(state) - .filter(key => !["autoscroll", "markdown"].includes(key)) + .filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key)) .filter(key => shouldDisplay(Feature.assertion_block) || key !== "assertion"); diff --git a/frontend/logs/components/logs_table.tsx b/frontend/logs/components/logs_table.tsx index 71f9076fe..14277cd72 100644 --- a/frontend/logs/components/logs_table.tsx +++ b/frontend/logs/components/logs_table.tsx @@ -3,7 +3,7 @@ import { TaggedLog, ALLOWED_MESSAGE_TYPES } from "farmbot"; import { LogsState, LogsTableProps, Filters } from "../interfaces"; import { formatLogTime } from "../index"; import { Classes } from "@blueprintjs/core"; -import { isNumber, startCase } from "lodash"; +import { isNumber, startCase, some } from "lodash"; import { t } from "../../i18next_wrapper"; import { TimeSettings } from "../../interfaces"; import { UUID } from "../../resources/interfaces"; @@ -81,6 +81,7 @@ export const LogsTable = (props: LogsTableProps) => { {filterByVerbosity(getFilterLevel(props.state), props.logs) + .filter(bySearchTerm(props.state.searchTerm, props.timeSettings)) .map((log: TaggedLog) => + (log: TaggedLog) => { + const { x, y, z, created_at, message, type } = log.body; + const displayedTime = formatLogTime(created_at || NaN, timeSettings); + const displayedPosition = xyzTableEntry(x, y, z); + const lowerSearchTerm = searchTerm.toLowerCase(); + return some([message, type] + .map(string => string.toLowerCase().includes(lowerSearchTerm)) + .concat([ + displayedTime.toLowerCase().includes(lowerSearchTerm), + displayedPosition.includes(lowerSearchTerm), + ])); + }; diff --git a/frontend/logs/index.tsx b/frontend/logs/index.tsx index cb7b8ffd2..a6a5e13ed 100644 --- a/frontend/logs/index.tsx +++ b/frontend/logs/index.tsx @@ -49,6 +49,7 @@ export class RawLogs extends React.Component> { fun: this.initialize(NumericSetting.fun_log, 1), debug: this.initialize(NumericSetting.debug_log, 1), assertion: this.initialize(NumericSetting.assertion_log, 1), + searchTerm: "", markdown: true, }; @@ -85,13 +86,13 @@ export class RawLogs extends React.Component> { const filterBtnColor = this.filterActive ? "green" : "gray"; return - +

    {t("Logs")}

    - +
    @@ -121,6 +122,19 @@ export class RawLogs extends React.Component> {
    + + +
    +
    + + + 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/nav/additional_menu.tsx b/frontend/nav/additional_menu.tsx index 4e0b1290f..f9e627495 100644 --- a/frontend/nav/additional_menu.tsx +++ b/frontend/nav/additional_menu.tsx @@ -9,23 +9,23 @@ export const AdditionalMenu = (props: AccountMenuProps) => { return
    - + {t("Account Settings")}
    - + {t("Logs")}
    - + {t("Help")} diff --git a/frontend/read_only_mode/index.tsx b/frontend/read_only_mode/index.tsx index 67ca1d555..1166ded2b 100644 --- a/frontend/read_only_mode/index.tsx +++ b/frontend/read_only_mode/index.tsx @@ -3,6 +3,7 @@ import { store } from "../redux/store"; import { warning } from "../toast/toast"; import React from "react"; import { appIsReadonly } from "./app_is_read_only"; +import { t } from "../i18next_wrapper"; export const readOnlyInterceptor = (config: AxiosRequestConfig) => { const method = (config.method || "get").toLowerCase(); @@ -10,7 +11,7 @@ export const readOnlyInterceptor = (config: AxiosRequestConfig) => { if (relevant && appIsReadonly(store.getState().resources.index)) { if (!(config.url || "").includes("web_app_config")) { - warning("Refusing to modify data in read-only mode"); + warning(t("Refusing to modify data in read-only mode")); return Promise.reject(config); } } @@ -18,19 +19,12 @@ export const readOnlyInterceptor = (config: AxiosRequestConfig) => { return Promise.resolve(config); }; -const MOVE_ME_ELSEWHERE: React.CSSProperties = { - float: "right", - boxSizing: "inherit", - margin: "9px 0px 0px 9px" -}; - export const ReadOnlyIcon = (p: { locked: boolean }) => { if (p.locked) { - return
    - - + return
    + +
    ; - } else { return
    ; } diff --git a/frontend/regimens/index.tsx b/frontend/regimens/index.tsx index 502a8c086..14b2f2e72 100644 --- a/frontend/regimens/index.tsx +++ b/frontend/regimens/index.tsx @@ -42,8 +42,7 @@ export class RawRegimens extends React.Component { + title={t("Regimens")}>
    - + + placeholder={t("Search regimens...")} />
    diff --git a/frontend/resources/selectors_by_id.ts b/frontend/resources/selectors_by_id.ts index 95fe95624..dc00eb0fe 100644 --- a/frontend/resources/selectors_by_id.ts +++ b/frontend/resources/selectors_by_id.ts @@ -11,8 +11,6 @@ import { isTaggedGenericPointer, isTaggedSavedGarden, isTaggedFolder, - isTaggedPoint, - isTaggedPointGroup, } from "./tagged_resources"; import { ResourceName, @@ -127,20 +125,6 @@ export function maybeFindGenericPointerById(index: ResourceIndex, id: number) { if (resource && isTaggedGenericPointer(resource)) { return resource; } } -/** Unlike other findById methods, this one allows undefined (missed) values */ -export function maybeFindPointById(index: ResourceIndex, id: number) { - const uuid = index.byKindAndId[joinKindAndId("Point", id)]; - const resource = index.references[uuid || "nope"]; - if (resource && isTaggedPoint(resource)) { return resource; } -} - -/** Unlike other findById methods, this one allows undefined (missed) values */ -export function maybeFindGroupById(index: ResourceIndex, id: number) { - const uuid = index.byKindAndId[joinKindAndId("PointGroup", id)]; - const resource = index.references[uuid || "nope"]; - if (resource && isTaggedPointGroup(resource)) { return resource; } -} - /** Unlike other findById methods, this one allows undefined (missed) values */ export function maybeFindSavedGardenById(index: ResourceIndex, id: number) { const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)]; diff --git a/frontend/sequences/step_tiles/tile_if/index.tsx b/frontend/sequences/step_tiles/tile_if/index.tsx index 393abb6d0..ddac3eadb 100644 --- a/frontend/sequences/step_tiles/tile_if/index.tsx +++ b/frontend/sequences/step_tiles/tile_if/index.tsx @@ -100,7 +100,7 @@ export function InnerIf(props: IfParams) { confirmStepDeletion={confirmStepDeletion}> {recursive && - +  {t("Recursive condition.")} } diff --git a/frontend/ui/back_arrow.tsx b/frontend/ui/back_arrow.tsx index f317111bf..b386d1934 100644 --- a/frontend/ui/back_arrow.tsx +++ b/frontend/ui/back_arrow.tsx @@ -10,6 +10,6 @@ export function BackArrow(props: BackArrowProps) { }; return - + ; } From 90ddd78bb8af678ff538fad571dc69e8f0b39a8d Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 26 Feb 2020 10:11:31 -0800 Subject: [PATCH 18/26] remove old tools page --- frontend/__tests__/interface_test.ts | 1 - .../tools/tool_slot_edit_components.tsx | 52 ++++++++-- frontend/route_config.tsx | 6 -- frontend/tools/__tests__/index_test.tsx | 48 --------- .../tools/__tests__/state_to_props_test.ts | 30 ------ .../components/__tests__/tool_form_test.tsx | 91 ----------------- .../components/__tests__/tool_list_test.tsx | 24 ----- .../__tests__/tool_slot_row_test.tsx | 26 ----- .../__tests__/toolbay_form_test.tsx | 50 ---------- .../__tests__/toolbay_header_test.tsx | 10 -- .../__tests__/toolbay_list_test.tsx | 31 ------ .../__tests__/toolbay_number_column_test.tsx | 25 ----- .../toolbay_slot_direction_selection_test.tsx | 31 ------ .../__tests__/toolbay_slot_menu_test.tsx | 95 ------------------ frontend/tools/components/empty_tool_slot.ts | 14 --- frontend/tools/components/index.ts | 4 - frontend/tools/components/tool_form.tsx | 98 ------------------- frontend/tools/components/tool_list.tsx | 50 ---------- frontend/tools/components/tool_slot_row.tsx | 65 ------------ frontend/tools/components/toolbay_form.tsx | 70 ------------- frontend/tools/components/toolbay_header.tsx | 23 ----- frontend/tools/components/toolbay_list.tsx | 41 -------- .../components/toolbay_number_column.tsx | 23 ----- .../toolbay_slot_direction_selection.tsx | 50 ---------- .../tools/components/toolbay_slot_menu.tsx | 84 ---------------- frontend/tools/index.tsx | 51 ---------- frontend/tools/interfaces.ts | 56 ----------- frontend/tools/state_to_props.ts | 75 -------------- 28 files changed, 46 insertions(+), 1178 deletions(-) delete mode 100644 frontend/tools/__tests__/index_test.tsx delete mode 100644 frontend/tools/__tests__/state_to_props_test.ts delete mode 100644 frontend/tools/components/__tests__/tool_form_test.tsx delete mode 100644 frontend/tools/components/__tests__/tool_list_test.tsx delete mode 100644 frontend/tools/components/__tests__/tool_slot_row_test.tsx delete mode 100644 frontend/tools/components/__tests__/toolbay_form_test.tsx delete mode 100644 frontend/tools/components/__tests__/toolbay_header_test.tsx delete mode 100644 frontend/tools/components/__tests__/toolbay_list_test.tsx delete mode 100644 frontend/tools/components/__tests__/toolbay_number_column_test.tsx delete mode 100644 frontend/tools/components/__tests__/toolbay_slot_direction_selection_test.tsx delete mode 100644 frontend/tools/components/__tests__/toolbay_slot_menu_test.tsx delete mode 100644 frontend/tools/components/empty_tool_slot.ts delete mode 100644 frontend/tools/components/index.ts delete mode 100644 frontend/tools/components/tool_form.tsx delete mode 100644 frontend/tools/components/tool_list.tsx delete mode 100644 frontend/tools/components/tool_slot_row.tsx delete mode 100644 frontend/tools/components/toolbay_form.tsx delete mode 100644 frontend/tools/components/toolbay_header.tsx delete mode 100644 frontend/tools/components/toolbay_list.tsx delete mode 100644 frontend/tools/components/toolbay_number_column.tsx delete mode 100644 frontend/tools/components/toolbay_slot_direction_selection.tsx delete mode 100644 frontend/tools/components/toolbay_slot_menu.tsx delete mode 100644 frontend/tools/index.tsx delete mode 100644 frontend/tools/interfaces.ts delete mode 100644 frontend/tools/state_to_props.ts 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/farm_designer/tools/tool_slot_edit_components.tsx b/frontend/farm_designer/tools/tool_slot_edit_components.tsx index 903088e82..6654bcde0 100644 --- a/frontend/farm_designer/tools/tool_slot_edit_components.tsx +++ b/frontend/farm_designer/tools/tool_slot_edit_components.tsx @@ -1,18 +1,15 @@ import React from "react"; import { t } from "../../i18next_wrapper"; import { Xyz, TaggedTool, TaggedToolSlotPointer } from "farmbot"; -import { Row, Col, BlurableInput, FBSelect, NULL_CHOICE, DropDownItem } from "../../ui"; import { - directionIconClass, positionButtonTitle, newSlotDirection, positionIsDefined -} from "../../tools/components/toolbay_slot_menu"; -import { - DIRECTION_CHOICES, DIRECTION_CHOICES_DDI -} from "../../tools/components/toolbay_slot_direction_selection"; + Row, Col, BlurableInput, FBSelect, NULL_CHOICE, DropDownItem +} from "../../ui"; import { BotPosition } from "../../devices/interfaces"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { Popover } from "@blueprintjs/core"; import { ToolSlotSVG } from "../map/layers/tool_slots/tool_graphics"; import { BotOriginQuadrant } from "../interfaces"; +import { isNumber } from "lodash"; export interface GantryMountedInputProps { gantryMounted: boolean; @@ -189,3 +186,46 @@ export const SlotEditRows = (props: SlotEditRowsProps) => gantryMounted={props.toolSlot.body.gantry_mounted} onChange={props.updateToolSlot} />}
    ; + +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/route_config.tsx b/frontend/route_config.tsx index 71bae5fa3..77236b088 100644 --- a/frontend/route_config.tsx +++ b/frontend/route_config.tsx @@ -149,12 +149,6 @@ export const UNBOUND_ROUTES = [ getModule: () => import("./sequences/sequences"), key: "Sequences", }), - route({ - children: false, - $: "/tools", - getModule: () => import("./tools"), - key: "Tools", - }), route({ children: false, $: "/designer", diff --git a/frontend/tools/__tests__/index_test.tsx b/frontend/tools/__tests__/index_test.tsx deleted file mode 100644 index c7bd984f2..000000000 --- a/frontend/tools/__tests__/index_test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from "react"; -import { mount, shallow } from "enzyme"; -import { RawTools as Tools } from "../index"; -import { Props } from "../interfaces"; -import { - fakeToolSlot, fakeTool -} from "../../__test_support__/fake_state/resources"; - -describe("", () => { - const fakeProps = (): Props => ({ - toolSlots: [], - tools: [fakeTool()], - getToolSlots: () => [fakeToolSlot()], - getToolOptions: () => [], - getChosenToolOption: () => ({ label: "None", value: "" }), - getToolByToolSlotUUID: fakeTool, - changeToolSlot: jest.fn(), - isActive: () => true, - dispatch: jest.fn(), - botPosition: { x: undefined, y: undefined, z: undefined } - }); - - it("renders", () => { - const wrapper = mount(); - const txt = wrapper.text(); - const strings = [ - "Tool Slots", - "SlotXYZ", - "Tool or Seed Container", - "Tools", - "NameStatus", - "Fooactive"]; - strings.map(string => expect(txt).toContain(string)); - }); - - it("shows forms", () => { - const wrapper = shallow(); - expect(wrapper.find("ToolList").length).toEqual(1); - expect(wrapper.find("ToolBayList").length).toEqual(1); - expect(wrapper.find("ToolForm").length).toEqual(0); - expect(wrapper.find("ToolBayForm").length).toEqual(0); - wrapper.setState({ editingBays: true, editingTools: true }); - expect(wrapper.find("ToolList").length).toEqual(0); - expect(wrapper.find("ToolBayList").length).toEqual(0); - expect(wrapper.find("ToolForm").length).toEqual(1); - expect(wrapper.find("ToolBayForm").length).toEqual(1); - }); -}); diff --git a/frontend/tools/__tests__/state_to_props_test.ts b/frontend/tools/__tests__/state_to_props_test.ts deleted file mode 100644 index bf4c32c08..000000000 --- a/frontend/tools/__tests__/state_to_props_test.ts +++ /dev/null @@ -1,30 +0,0 @@ -jest.mock("../../api/crud", () => ({ edit: jest.fn() })); - -import { mapStateToProps } from "../state_to_props"; -import { fakeState } from "../../__test_support__/fake_state"; -import { NULL_CHOICE } from "../../ui"; -import { fakeToolSlot } from "../../__test_support__/fake_state/resources"; -import { edit } from "../../api/crud"; - -describe("mapStateToProps()", () => { - it("getChosenToolOption()", () => { - const props = mapStateToProps(fakeState()); - const result = props.getChosenToolOption(undefined); - expect(result).toEqual(NULL_CHOICE); - }); - - it("changeToolSlot(): no tool_id", () => { - const props = mapStateToProps(fakeState()); - const tool = fakeToolSlot(); - props.changeToolSlot(tool, jest.fn())({ label: "", value: "" }); - // tslint:disable-next-line:no-null-keyword - expect(edit).toHaveBeenCalledWith(tool, { tool_id: null }); - }); - - it("changeToolSlot(): tool_id", () => { - const props = mapStateToProps(fakeState()); - const tool = fakeToolSlot(); - props.changeToolSlot(tool, jest.fn())({ label: "", value: 1 }); - expect(edit).toHaveBeenCalledWith(tool, { tool_id: 1 }); - }); -}); diff --git a/frontend/tools/components/__tests__/tool_form_test.tsx b/frontend/tools/components/__tests__/tool_form_test.tsx deleted file mode 100644 index 134f6cb93..000000000 --- a/frontend/tools/components/__tests__/tool_form_test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -jest.mock("../../../api/crud", () => ({ - init: jest.fn(), - saveAll: jest.fn(), - destroy: jest.fn(), - edit: jest.fn(), -})); - -import { SpecialStatus } from "farmbot"; -jest.mock("../../../resources/tagged_resources", () => ({ - getArrayStatus: () => SpecialStatus.SAVED, - isTaggedResource: () => true -})); - -import * as React from "react"; -import { ToolForm } from "../tool_form"; -import { mount, shallow } from "enzyme"; -import { fakeTool } from "../../../__test_support__/fake_state/resources"; -import { ToolListAndFormProps } from "../../interfaces"; -import { clickButton } from "../../../__test_support__/helpers"; -import { init, saveAll, destroy, edit } from "../../../api/crud"; - -describe("", () => { - function fakeProps(): ToolListAndFormProps { - return { - dispatch: jest.fn(), - toggle: jest.fn(), - tools: [fakeTool(), fakeTool()], - isActive: jest.fn(), - }; - } - - it("renders", () => { - const p = fakeProps(); - const wrapper = mount(); - expect(wrapper.find("input").length).toEqual(p.tools.length); - }); - - it("saves tools", () => { - const wrapper = mount(); - clickButton(wrapper, 1, "saved", { partial_match: true }); - expect(saveAll).toHaveBeenCalledTimes(1); - }); - - it("adds new tool", () => { - const wrapper = mount(); - expect(wrapper.props().tools.length).toEqual(2); - clickButton(wrapper, 2, ""); - expect(init).toHaveBeenCalledWith("Tool", { name: "Tool 3" }); - }); - - it("adds stock tools", () => { - const wrapper = mount(); - clickButton(wrapper, 3, "stock tools"); - expect(init).toHaveBeenCalledTimes(6); - }); - - it("changes tool name", () => { - const p = fakeProps(); - const wrapper = shallow(); - wrapper.find("BlurableInput").first().simulate("commit", { - currentTarget: { value: "New Tool Name" } - }); - expect(edit).toHaveBeenCalledWith(p.tools[0], { name: "New Tool Name" }); - }); - - it("has red delete button", () => { - const p = fakeProps(); - p.isActive = () => false; - const wrapper = mount(); - const delBtn = wrapper.find("button").last(); - expect(delBtn.hasClass("red")).toBeTruthy(); - }); - - it("deletes tool", () => { - const p = fakeProps(); - p.isActive = () => false; - const wrapper = mount(); - const delBtn = wrapper.find("button").last(); - delBtn.simulate("click"); - expect(destroy).toHaveBeenCalledWith(p.tools[1].uuid); - }); - - it("has gray delete button", () => { - const p = fakeProps(); - p.isActive = () => true; - const wrapper = mount(); - const delBtn = wrapper.find("button").last(); - expect(delBtn.hasClass("pseudo-disabled")).toBeTruthy(); - expect(delBtn.props().title).toContain("in slot"); - }); -}); diff --git a/frontend/tools/components/__tests__/tool_list_test.tsx b/frontend/tools/components/__tests__/tool_list_test.tsx deleted file mode 100644 index 9addb818b..000000000 --- a/frontend/tools/components/__tests__/tool_list_test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from "react"; -import { ToolList } from "../tool_list"; -import { mount } from "enzyme"; -import { mapStateToProps } from "../../state_to_props"; -import { fakeState } from "../../../__test_support__/fake_state"; -import { ToolListAndFormProps } from "../../interfaces"; - -describe("", () => { - const fakeProps = (): ToolListAndFormProps => { - const props = mapStateToProps(fakeState()); - return { - dispatch: jest.fn(), - tools: props.tools, - toggle: jest.fn(), - isActive: props.isActive, - }; - }; - - it("renders tool names and statuses", () => { - const wrapper = mount(); - expect(wrapper.text()).toContain("Trench Digging Toolactive"); - expect(wrapper.text()).toContain("Berry Picking Toolinactive"); - }); -}); diff --git a/frontend/tools/components/__tests__/tool_slot_row_test.tsx b/frontend/tools/components/__tests__/tool_slot_row_test.tsx deleted file mode 100644 index 5970ddb5d..000000000 --- a/frontend/tools/components/__tests__/tool_slot_row_test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -jest.mock("../../../api/crud", () => ({ destroy: jest.fn() })); - -import * as React from "react"; -import { ToolSlotRowProps, ToolSlotRow } from "../tool_slot_row"; -import { mount } from "enzyme"; -import { destroy } from "../../../api/crud"; -import { fakeToolSlot } from "../../../__test_support__/fake_state/resources"; - -describe("", () => { - const fakeProps = (): ToolSlotRowProps => ({ - dispatch: jest.fn(), - slot: fakeToolSlot(), - botPosition: { x: undefined, y: undefined, z: undefined }, - toolOptions: [], - chosenToolOption: { label: "", value: "" }, - onToolSlotChange: jest.fn(), - gantryMounted: false, - }); - - it("deletes slot", () => { - const p = fakeProps(); - const wrapper = mount(); - wrapper.find("button").last().simulate("click"); - expect(destroy).toHaveBeenCalledWith(p.slot.uuid); - }); -}); diff --git a/frontend/tools/components/__tests__/toolbay_form_test.tsx b/frontend/tools/components/__tests__/toolbay_form_test.tsx deleted file mode 100644 index 8d2a05348..000000000 --- a/frontend/tools/components/__tests__/toolbay_form_test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -jest.mock("../../../api/crud", () => ({ - init: jest.fn(), - saveAll: jest.fn(), -})); - -import * as React from "react"; -import { ToolBayForm } from "../toolbay_form"; -import { mount } from "enzyme"; -import { mapStateToProps } from "../../state_to_props"; -import { fakeState } from "../../../__test_support__/fake_state"; -import { ToolBayFormProps } from "../../interfaces"; -import { clickButton } from "../../../__test_support__/helpers"; -import { saveAll, init } from "../../../api/crud"; -import { emptyToolSlotBody } from "../empty_tool_slot"; - -describe("", () => { - const fakeProps = (): ToolBayFormProps => { - const props = mapStateToProps(fakeState()); - return { - toggle: jest.fn(), - dispatch: jest.fn(), - toolSlots: props.toolSlots, - getToolSlots: props.getToolSlots, - getChosenToolOption: props.getChosenToolOption, - getToolOptions: props.getToolOptions, - changeToolSlot: props.changeToolSlot, - botPosition: { x: 1, y: 2, z: 3 }, - }; - }; - - it("renders ToolSlot", () => { - const wrapper = mount(); - const inputs = wrapper.find("input"); - expect(inputs.length).toEqual(3); - expect(wrapper.text()).toContain("Trench Digging Tool"); - [0, 1, 2].map(i => expect(inputs.at(i).props().value).toEqual("10")); - }); - - it("saves tool slots", () => { - const wrapper = mount(); - clickButton(wrapper, 1, "saved", { partial_match: true }); - expect(saveAll).toHaveBeenCalledTimes(1); - }); - - it("adds new tool slot", () => { - const wrapper = mount(); - clickButton(wrapper, 2, ""); - expect(init).toHaveBeenCalledWith("Point", emptyToolSlotBody()); - }); -}); diff --git a/frontend/tools/components/__tests__/toolbay_header_test.tsx b/frontend/tools/components/__tests__/toolbay_header_test.tsx deleted file mode 100644 index 5e623a9d8..000000000 --- a/frontend/tools/components/__tests__/toolbay_header_test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from "react"; -import { ToolBayHeader } from "../toolbay_header"; -import { mount } from "enzyme"; - -describe("", () => { - it("renders", () => { - const header = mount(); - expect(header.text()).toEqual("SlotXYZTool or Seed Container"); - }); -}); diff --git a/frontend/tools/components/__tests__/toolbay_list_test.tsx b/frontend/tools/components/__tests__/toolbay_list_test.tsx deleted file mode 100644 index 3981a4214..000000000 --- a/frontend/tools/components/__tests__/toolbay_list_test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from "react"; -import { ToolBayList } from "../toolbay_list"; -import { mount } from "enzyme"; -import { mapStateToProps } from "../../state_to_props"; -import { fakeState } from "../../../__test_support__/fake_state"; -import { ToolBayListProps } from "../../interfaces"; - -describe("", () => { - const fakeProps = (): ToolBayListProps => { - const props = mapStateToProps(fakeState()); - return { - getToolByToolSlotUUID: props.getToolByToolSlotUUID, - getToolSlots: props.getToolSlots, - toggle: jest.fn(), - }; - }; - - it("renders", () => { - const wrapper = mount(); - expect(wrapper.text()).toContain("1101010Trench Digging Tool"); - }); - - it("renders gantry mounted slot", () => { - const p = fakeProps(); - const slots = p.getToolSlots(); - slots[0].body.gantry_mounted = true; - p.getToolSlots = () => slots; - const wrapper = mount(); - expect(wrapper.text()).toContain("1Gantry1010Trench Digging Tool"); - }); -}); diff --git a/frontend/tools/components/__tests__/toolbay_number_column_test.tsx b/frontend/tools/components/__tests__/toolbay_number_column_test.tsx deleted file mode 100644 index 9f5a85915..000000000 --- a/frontend/tools/components/__tests__/toolbay_number_column_test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -jest.mock("../../../api/crud", () => ({ edit: jest.fn() })); - -import * as React from "react"; -import { shallow } from "enzyme"; -import { ToolBayNumberCol, TBNumColProps } from "../toolbay_number_column"; -import { edit } from "../../../api/crud"; -import { fakeToolSlot } from "../../../__test_support__/fake_state/resources"; - -describe("", () => { - const fakeProps = (): TBNumColProps => ({ - axis: "x", - value: 0, - dispatch: jest.fn(), - slot: fakeToolSlot(), - }); - - it("edits value", () => { - const p = fakeProps(); - const wrapper = shallow(); - wrapper.find("BlurableInput").simulate("commit", { - currentTarget: { value: "1.23" } - }); - expect(edit).toHaveBeenCalledWith(p.slot, { x: 1.23 }); - }); -}); diff --git a/frontend/tools/components/__tests__/toolbay_slot_direction_selection_test.tsx b/frontend/tools/components/__tests__/toolbay_slot_direction_selection_test.tsx deleted file mode 100644 index aeb9c2b23..000000000 --- a/frontend/tools/components/__tests__/toolbay_slot_direction_selection_test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from "react"; -import { shallow } from "enzyme"; -import { - SlotDirectionSelect, SlotDirectionSelectProps -} from "../toolbay_slot_direction_selection"; -import { fakeToolSlot } from "../../../__test_support__/fake_state/resources"; -import { Actions } from "../../../constants"; -import { SpecialStatus } from "farmbot"; - -describe("", () => { - const fakeProps = (): SlotDirectionSelectProps => { - return { - dispatch: jest.fn(), - slot: fakeToolSlot() - }; - }; - - it("changes slot direction", () => { - const p = fakeProps(); - const wrapper = shallow(); - wrapper.simulate("change", { value: 1 }); - expect(p.dispatch).toHaveBeenCalledWith({ - payload: { - specialStatus: SpecialStatus.DIRTY, - update: { pullout_direction: 1 }, - uuid: expect.any(String) - }, - type: Actions.EDIT_RESOURCE - }); - }); -}); diff --git a/frontend/tools/components/__tests__/toolbay_slot_menu_test.tsx b/frontend/tools/components/__tests__/toolbay_slot_menu_test.tsx deleted file mode 100644 index 31272cca1..000000000 --- a/frontend/tools/components/__tests__/toolbay_slot_menu_test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from "react"; -import { mount } from "enzyme"; -import { SlotMenu, SlotMenuProps } from "../toolbay_slot_menu"; -import { fakeToolSlot } from "../../../__test_support__/fake_state/resources"; -import { Actions } from "../../../constants"; -import { SpecialStatus } from "farmbot"; - -describe("", () => { - const fakeProps = (): SlotMenuProps => { - return { - dispatch: jest.fn(), - slot: fakeToolSlot(), - botPosition: { x: 1, y: 2, z: 3 } - }; - }; - - it("changes slot direction", () => { - const p = fakeProps(); - const wrapper = mount(); - wrapper.find("i").first().simulate("click"); - expect(p.dispatch).toHaveBeenCalledWith({ - payload: { - specialStatus: SpecialStatus.DIRTY, - update: { pullout_direction: 1 }, - uuid: expect.any(String) - }, - type: Actions.EDIT_RESOURCE - }); - }); - - it("changes slot direction: reset", () => { - const p = fakeProps(); - p.slot.body.pullout_direction = 4; - const wrapper = mount(); - wrapper.find("i").first().simulate("click"); - expect(p.dispatch).toHaveBeenCalledWith({ - payload: { - specialStatus: SpecialStatus.DIRTY, - update: { pullout_direction: 0 }, - uuid: expect.any(String) - }, - type: Actions.EDIT_RESOURCE - }); - }); - - const checkDirection = (direction: number, expected: string) => { - it("icon shows direction", () => { - const p = fakeProps(); - p.slot.body.pullout_direction = direction; - const wrapper = mount(); - expect(wrapper.html()).toContain(expected); - }); - }; - checkDirection(1, "right"); - checkDirection(2, "left"); - checkDirection(3, "up"); - checkDirection(4, "down"); - - it("fills inputs with bot position", () => { - const p = fakeProps(); - const wrapper = mount(); - const buttons = wrapper.find("button"); - buttons.last().simulate("click"); - expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.EDIT_RESOURCE, - payload: expect.objectContaining({ - update: { x: 1, y: 2, z: 3 } - }) - }); - }); - - it("doesn't fills inputs with bot position unknown", () => { - const p = fakeProps(); - p.botPosition = { x: undefined, y: undefined, z: undefined }; - const wrapper = mount(); - const buttons = wrapper.find("button"); - buttons.last().simulate("click"); - expect(p.dispatch).not.toHaveBeenCalled(); - }); - - it("sets gantry_mounted", () => { - const p = fakeProps(); - p.slot.body.gantry_mounted = false; - const wrapper = mount(); - wrapper.find("input").last().simulate("change"); - expect(p.dispatch).toHaveBeenCalledWith({ - payload: { - specialStatus: SpecialStatus.DIRTY, - update: { gantry_mounted: true }, - uuid: expect.any(String) - }, - type: Actions.EDIT_RESOURCE - }); - }); -}); diff --git a/frontend/tools/components/empty_tool_slot.ts b/frontend/tools/components/empty_tool_slot.ts deleted file mode 100644 index 57125bdfc..000000000 --- a/frontend/tools/components/empty_tool_slot.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ToolSlotPointer } from "farmbot/dist/resources/api_resources"; - -export const emptyToolSlotBody = (): ToolSlotPointer => ({ - x: 0, - y: 0, - z: 0, - radius: 25, - pointer_type: "ToolSlot", - meta: {}, - tool_id: undefined, - name: "Tool Slot", - pullout_direction: 0, - gantry_mounted: false, -}); diff --git a/frontend/tools/components/index.ts b/frontend/tools/components/index.ts deleted file mode 100644 index 9dcea62c6..000000000 --- a/frontend/tools/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./toolbay_form"; -export * from "./toolbay_list"; -export * from "./tool_list"; -export * from "./tool_form"; diff --git a/frontend/tools/components/tool_form.tsx b/frontend/tools/components/tool_form.tsx deleted file mode 100644 index 42dbb8593..000000000 --- a/frontend/tools/components/tool_form.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import * as React from "react"; -import { ToolListAndFormProps } from "../interfaces"; -import { - Row, - Col, - Widget, - WidgetBody, - WidgetHeader, - BlurableInput, - SaveBtn -} from "../../ui"; -import { getArrayStatus } from "../../resources/tagged_resources"; -import { edit, destroy, init, saveAll } from "../../api/crud"; -import { ToolTips } from "../../constants"; -import { TaggedTool } from "farmbot"; -import { t } from "../../i18next_wrapper"; - -export class ToolForm extends React.Component { - get newToolName() { return t("Tool ") + (this.props.tools.length + 1); } - - newTool = (name = this.newToolName) => { - this.props.dispatch(init("Tool", { name })); - }; - - stockTools = () => { - this.newTool(t("Seeder")); - this.newTool(t("Watering Nozzle")); - this.newTool(t("Weeder")); - this.newTool(t("Soil Sensor")); - this.newTool(t("Seed Bin")); - this.newTool(t("Seed Tray")); - } - - HeaderButtons = () => { - const { dispatch, tools, toggle } = this.props; - const specialStatus = getArrayStatus(tools); - return
    - - dispatch(saveAll(tools, toggle))} /> - - -
    ; - } - - ToolForm = (tool: TaggedTool, index: number) => { - const { dispatch, isActive } = this.props; - const inSlotClass = isActive(tool) ? "pseudo-disabled" : ""; - return - - - dispatch(edit(tool, { name: e.currentTarget.value }))} /> - - - - - ; - } - - render() { - return - - - - - - - - - - {this.props.tools.map(this.ToolForm)} - - ; - } -} diff --git a/frontend/tools/components/tool_list.tsx b/frontend/tools/components/tool_list.tsx deleted file mode 100644 index 2de0a9eac..000000000 --- a/frontend/tools/components/tool_list.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from "react"; -import { Row, Col, Widget, WidgetBody, WidgetHeader } from "../../ui"; -import { ToolListAndFormProps } from "../interfaces"; -import { TaggedTool } from "farmbot"; -import { ToolTips } from "../../constants"; -import { t } from "../../i18next_wrapper"; - -enum ColWidth { - toolName = 8, - status = 4, -} - -export class ToolList extends React.Component { - - ToolListItem = (tool: TaggedTool) => { - return - - {tool.body.name || "Name not found"} - - - {this.props.isActive(tool) ? t("active") : t("inactive")} - - ; - } - - render() { - const { tools, toggle } = this.props; - - return - - - - - - - - - - - - - {tools.map(this.ToolListItem)} - - ; - } -} diff --git a/frontend/tools/components/tool_slot_row.tsx b/frontend/tools/components/tool_slot_row.tsx deleted file mode 100644 index 53df2919f..000000000 --- a/frontend/tools/components/tool_slot_row.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react"; -import { Row, Col, FBSelect, DropDownItem } from "../../ui"; -import { Popover, Position } from "@blueprintjs/core"; -import { SlotMenu } from "./toolbay_slot_menu"; -import { TaggedToolSlotPointer } from "farmbot"; -import { destroy } from "../../api/crud"; -import { Xyz } from "../../devices/interfaces"; -import { ToolBayNumberCol } from "./toolbay_number_column"; -import { t } from "../../i18next_wrapper"; - -export interface ToolSlotRowProps { - dispatch: Function; - slot: TaggedToolSlotPointer; - botPosition: Record<"x" | "y" | "z", number | undefined>; - /** List of all legal tool options for the current tool slot. */ - toolOptions: DropDownItem[]; - /** The current tool (if any) in the slot. */ - chosenToolOption: DropDownItem; - /** Broadcast tool change back up to parent. */ - onToolSlotChange(item: DropDownItem): void; - /** Gantry-mounted tool slot. */ - gantryMounted: boolean; -} - -type Axis = Xyz & keyof (TaggedToolSlotPointer["body"]); -const axes: Axis[] = ["x", "y", "z"]; - -export function ToolSlotRow(props: ToolSlotRowProps) { - const { dispatch, slot, botPosition, toolOptions, onToolSlotChange, - chosenToolOption, gantryMounted } = props; - - return - - - - - - - {axes - .map(axis => ({ axis, dispatch, slot, value: (slot.body[axis] || 0) })) - .map(axisProps => (axisProps.axis === "x" && gantryMounted) - ? - - - : )} - - - - - - - ; -} diff --git a/frontend/tools/components/toolbay_form.tsx b/frontend/tools/components/toolbay_form.tsx deleted file mode 100644 index 1ccee1a77..000000000 --- a/frontend/tools/components/toolbay_form.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react"; -import { ToolBayFormProps } from "../interfaces"; -import { - Widget, - WidgetBody, - WidgetHeader, - SaveBtn, -} from "../../ui"; - -import { - getArrayStatus -} from "../../resources/tagged_resources"; -import { saveAll, init } from "../../api/crud"; -import { ToolBayHeader } from "./toolbay_header"; -import { ToolTips } from "../../constants"; -import { ToolSlotRow } from "./tool_slot_row"; -import { emptyToolSlotBody } from "./empty_tool_slot"; -import { TaggedToolSlotPointer } from "farmbot"; -import { t } from "../../i18next_wrapper"; - -export class ToolBayForm extends React.Component { - - HeaderButtons = () => { - const { toggle, dispatch, toolSlots } = this.props; - const toolSlotStatus = getArrayStatus(toolSlots); - return
    - - dispatch(saveAll(toolSlots, toggle))} /> - -
    ; - } - - ToolbayForm = (slot: TaggedToolSlotPointer) => { - const { dispatch, botPosition } = this.props; - return ; - } - - render() { - return
    - - - - - - - {this.props.getToolSlots().map(this.ToolbayForm)} - - -
    ; - } -} diff --git a/frontend/tools/components/toolbay_header.tsx b/frontend/tools/components/toolbay_header.tsx deleted file mode 100644 index f3c9eac33..000000000 --- a/frontend/tools/components/toolbay_header.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from "react"; -import { Col, Row } from "../../ui"; -import { t } from "../../i18next_wrapper"; - -export function ToolBayHeader() { - return - - - - - - - - - - - - - - - - ; -} diff --git a/frontend/tools/components/toolbay_list.tsx b/frontend/tools/components/toolbay_list.tsx deleted file mode 100644 index 76c722556..000000000 --- a/frontend/tools/components/toolbay_list.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; -import { Row, Col, Widget, WidgetBody, WidgetHeader } from "../../ui"; -import { ToolBayListProps } from "../interfaces"; -import { TaggedToolSlotPointer } from "farmbot"; -import { ToolBayHeader } from "./toolbay_header"; -import { ToolTips } from "../../constants"; -import { t } from "../../i18next_wrapper"; - -export class ToolBayList extends React.Component { - - ToolSlotListItem = (slot: TaggedToolSlotPointer, index: number) => { - const { getToolByToolSlotUUID } = this.props; - const tool = getToolByToolSlotUUID(slot.uuid); - const name = (tool?.body.name) || t("None"); - return - - {slot.body.gantry_mounted ? t("Gantry") : slot.body.x} - {slot.body.y} - {slot.body.z} - {name} - ; - } - - render() { - return - - - - - - {this.props.getToolSlots().map(this.ToolSlotListItem)} - - ; - } -} diff --git a/frontend/tools/components/toolbay_number_column.tsx b/frontend/tools/components/toolbay_number_column.tsx deleted file mode 100644 index ae9cdccd8..000000000 --- a/frontend/tools/components/toolbay_number_column.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from "react"; -import { TaggedToolSlotPointer } from "farmbot"; -import { Col, BlurableInput } from "../../ui"; -import { edit } from "../../api/crud"; - -export interface TBNumColProps { - axis: "x" | "y" | "z"; - value: number; - dispatch: Function; - slot: TaggedToolSlotPointer; -} - -/** Used to display and edit the X/Y/Z numeric values in the tool bay form. */ -export function ToolBayNumberCol(props: TBNumColProps) { - const { axis, value, dispatch, slot } = props; - return - - dispatch(edit(slot, { [axis]: parseFloat(e.currentTarget.value) }))} - type="number" /> - ; -} diff --git a/frontend/tools/components/toolbay_slot_direction_selection.tsx b/frontend/tools/components/toolbay_slot_direction_selection.tsx deleted file mode 100644 index c07383527..000000000 --- a/frontend/tools/components/toolbay_slot_direction_selection.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from "react"; -import { FBSelect, DropDownItem } from "../../ui"; -import { TaggedToolSlotPointer } from "farmbot"; -import { edit } from "../../api/crud"; -import { isNumber } from "lodash"; -import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; -import { t } from "../../i18next_wrapper"; - -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], -]; - -export interface SlotDirectionSelectProps { - dispatch: Function; - slot: TaggedToolSlotPointer; -} - -export function SlotDirectionSelect(props: SlotDirectionSelectProps) { - const { dispatch, slot } = props; - const direction = slot.body.pullout_direction; - - const changePulloutDirection = (selectedDirection: DropDownItem) => { - const { value } = selectedDirection; - dispatch(edit(slot, { - pullout_direction: isNumber(value) ? value : parseInt(value) - })); - }; - - return ; -} diff --git a/frontend/tools/components/toolbay_slot_menu.tsx b/frontend/tools/components/toolbay_slot_menu.tsx deleted file mode 100644 index b9688890b..000000000 --- a/frontend/tools/components/toolbay_slot_menu.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as React from "react"; -import { isNumber } from "lodash"; -import { BotPosition } from "../../devices/interfaces"; -import { TaggedToolSlotPointer } from "farmbot"; -import { edit } from "../../api/crud"; -import { SlotDirectionSelect } from "./toolbay_slot_direction_selection"; -import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; -import { t } from "../../i18next_wrapper"; - -export const positionIsDefined = (position: BotPosition): boolean => - isNumber(position.x) && isNumber(position.y) && isNumber(position.z); - -export const useCurrentPosition = ( - dispatch: Function, slot: TaggedToolSlotPointer, position: BotPosition) => { - if (positionIsDefined(position)) { - dispatch(edit(slot, { x: position.x, y: position.y, z: position.z })); - } -}; - -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 changePulloutDirection = - (dispatch: Function, slot: TaggedToolSlotPointer) => () => { - dispatch(edit(slot, - { pullout_direction: newSlotDirection(slot.body.pullout_direction) })); - }; - -export 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 interface SlotMenuProps { - dispatch: Function, - slot: TaggedToolSlotPointer, - botPosition: BotPosition -} - -export const SlotMenu = (props: SlotMenuProps) => { - const { dispatch, slot, botPosition } = props; - const { pullout_direction, gantry_mounted } = slot.body; - return
    -
    - - - -
    -
    - - -

    {positionButtonTitle(botPosition)}

    -
    -
    - - - dispatch(edit(slot, { gantry_mounted: !gantry_mounted }))} - checked={gantry_mounted} /> -
    -
    ; -}; diff --git a/frontend/tools/index.tsx b/frontend/tools/index.tsx deleted file mode 100644 index ebaa70ef4..000000000 --- a/frontend/tools/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from "react"; -import { connect } from "react-redux"; -import { ToolsState, Props } from "./interfaces"; -import { Col, Row, Page } from "../ui"; -import { ToolBayList, ToolBayForm, ToolList, ToolForm } from "./components"; -import { mapStateToProps } from "./state_to_props"; - -export class RawTools extends React.Component> { - state: ToolsState = { editingBays: false, editingTools: false }; - - toggle = (name: keyof ToolsState) => - () => this.setState({ [name]: !this.state[name] }); - - render() { - return - - - {this.state.editingBays - ? - : } - - - {this.state.editingTools - ? - : } - - - ; - } -} - -export const Tools = connect(mapStateToProps)(RawTools); diff --git a/frontend/tools/interfaces.ts b/frontend/tools/interfaces.ts deleted file mode 100644 index eed35a9ef..000000000 --- a/frontend/tools/interfaces.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { DropDownItem } from "../ui/index"; -import { - TaggedTool, - TaggedToolSlotPointer, -} from "farmbot"; -import { BotPosition } from "../devices/interfaces"; - -export interface ToolsState { - editingTools: boolean; - editingBays: boolean; -} - -export interface Props { - toolSlots: TaggedToolSlotPointer[]; - tools: TaggedTool[]; - getToolOptions(): DropDownItem[]; - getChosenToolOption(toolSlotUuid: string | undefined): DropDownItem; - getToolByToolSlotUUID(uuid: string): TaggedTool | undefined; - getToolSlots(): TaggedToolSlotPointer[]; - dispatch: Function; - isActive: (tool: TaggedTool) => boolean; - changeToolSlot(t: TaggedToolSlotPointer, dispatch: Function): - (d: DropDownItem) => void; - botPosition: BotPosition; -} - -export interface Tool { - id?: number | undefined; - name?: string; - status?: string | undefined; -} - -export interface ToolBayListProps { - toggle(): void; - getToolByToolSlotUUID(uuid: string): TaggedTool | undefined; - getToolSlots(): TaggedToolSlotPointer[]; -} - -export interface ToolBayFormProps { - dispatch: Function; - toolSlots: TaggedToolSlotPointer[]; - botPosition: BotPosition; - toggle(): void; - getToolOptions(): DropDownItem[]; - getChosenToolOption(uuid: string | undefined): DropDownItem; - getToolSlots(): TaggedToolSlotPointer[]; - changeToolSlot(t: TaggedToolSlotPointer, dispatch: Function): - (d: DropDownItem) => void; -} - -export interface ToolListAndFormProps { - dispatch: Function; - tools: TaggedTool[]; - toggle(): void; - isActive(tool: TaggedTool): boolean; -} diff --git a/frontend/tools/state_to_props.ts b/frontend/tools/state_to_props.ts deleted file mode 100644 index c421b42e7..000000000 --- a/frontend/tools/state_to_props.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Everything } from "../interfaces"; -import { Props } from "./interfaces"; -import { - selectAllToolSlotPointers, - selectAllTools, - currentToolInSlot, -} from "../resources/selectors"; -import { isTaggedTool } from "../resources/tagged_resources"; -import { edit } from "../api/crud"; -import { DropDownItem, NULL_CHOICE } from "../ui"; -import { validBotLocationData } from "../util"; -import { TaggedTool, TaggedToolSlotPointer } from "farmbot"; -import { isNumber, noop, compact } from "lodash"; - -export function mapStateToProps(props: Everything): Props { - const toolSlots = selectAllToolSlotPointers(props.resources.index); - const tools = selectAllTools(props.resources.index); - - /** Returns sorted tool slots specific to the tool bay id passed. */ - const getToolSlots = () => toolSlots; - - /** Returns all tools in an compatible format. */ - const getToolOptions = () => { - return compact(tools - .map(tool => ({ - label: tool.body.name || "untitled", - value: tool.body.id || 0, - })) - .filter(ddi => isNumber(ddi.value) && ddi.value > 0)); - }; - - const activeTools = compact(toolSlots.map(x => x.body.tool_id)); - - const isActive = - (t: TaggedTool) => !!(t.body.id && activeTools.includes(t.body.id)); - - const getToolByToolSlotUUID = currentToolInSlot(props.resources.index); - - /** Returns the current tool chosen in a slot based off the slot's id - * and in an compatible format. */ - const getChosenToolOption = (toolSlotUUID: string | undefined) => { - const chosenTool = toolSlotUUID && getToolByToolSlotUUID(toolSlotUUID); - return (chosenTool && isTaggedTool(chosenTool) && chosenTool.body.id) - ? { label: chosenTool.body.name || "untitled", value: chosenTool.uuid } - : NULL_CHOICE; - }; - - const changeToolSlot = (t: TaggedToolSlotPointer, - dispatch: Function) => - (d: DropDownItem) => { - // THIS IS IMPORTANT: - // If you remove the `any`, the tool will be serialized wrong and - // cause errors. - // tslint:disable-next-line:no-null-keyword no-any - const tool_id = d.value ? d.value : (null as any); - dispatch(edit(t, { tool_id })); - }; - - const botPosition = - validBotLocationData(props.bot.hardware.location_data).position; - - return { - toolSlots, - tools, - getToolSlots, - getToolOptions, - getChosenToolOption, - getToolByToolSlotUUID, - changeToolSlot, - isActive, - dispatch: noop, - botPosition, - }; - -} From edb96d3ca8d93daf1f94ac85666d734688fe6da7 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 26 Feb 2020 12:08:49 -0800 Subject: [PATCH 19/26] cleanup and refactoring --- .../components/farmbot_os_settings.tsx | 10 ++-- .../__tests__/auto_update_row_test.tsx | 10 ++-- .../fbos_settings/auto_update_row.tsx | 50 ++++++++----------- .../components/fbos_settings/interfaces.ts | 8 ++- .../fbos_settings/ota_time_selector.tsx | 13 ++++- .../__tests__/panel_header_test.tsx | 8 +++ .../__tests__/tool_slot_layer_test.tsx | 15 ------ .../__tests__/tool_slot_point_test.tsx | 13 ----- .../map/layers/tool_slots/tool_slot_layer.tsx | 10 +--- .../map/layers/tool_slots/tool_slot_point.tsx | 4 +- frontend/farm_designer/panel_header.tsx | 9 ++-- .../plants/edit_plant_status.tsx | 41 ++++++--------- .../criteria/__tests__/add_test.tsx | 2 +- .../point_groups/criteria/add.tsx | 16 ++---- .../point_groups/criteria/show.tsx | 3 +- frontend/help/__tests__/tours_test.ts | 18 ------- frontend/help/tours.ts | 13 +---- frontend/nav/__tests__/nav_links_test.tsx | 6 --- frontend/nav/nav_links.tsx | 3 -- 19 files changed, 85 insertions(+), 167 deletions(-) diff --git a/frontend/devices/components/farmbot_os_settings.tsx b/frontend/devices/components/farmbot_os_settings.tsx index 1498f5868..6b6dbf5f7 100644 --- a/frontend/devices/components/farmbot_os_settings.tsx +++ b/frontend/devices/components/farmbot_os_settings.tsx @@ -17,6 +17,7 @@ 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, @@ -78,8 +79,6 @@ 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()}> @@ -133,11 +132,14 @@ export class FarmbotOsSettings shouldDisplay={this.props.shouldDisplay} timeSettings={this.props.timeSettings} sourceFbosConfig={sourceFbosConfig} /> - + ", () => { @@ -20,10 +20,8 @@ describe("", () => { state.resources = buildResourceIndex([fakeConfig]); const fakeProps = (): AutoUpdateRowProps => ({ - timeFormat: "12h", - device: fakeDevice(), dispatch: jest.fn(x => x(jest.fn(), () => state)), - sourceFbosConfig: () => ({ value: 1, consistent: true }) + sourceFbosConfig: () => ({ value: 1, consistent: true }), }); it("renders", () => { @@ -35,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); }); @@ -44,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/auto_update_row.tsx b/frontend/devices/components/fbos_settings/auto_update_row.tsx index 2540daecf..173ed5ddf 100644 --- a/frontend/devices/components/fbos_settings/auto_update_row.tsx +++ b/frontend/devices/components/fbos_settings/auto_update_row.tsx @@ -6,38 +6,30 @@ import { updateConfig } from "../../actions"; import { Content, DeviceSetting } from "../../../constants"; import { AutoUpdateRowProps } from "./interfaces"; import { t } from "../../../i18next_wrapper"; -import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector"; import { Highlight } from "../maybe_highlight"; export function AutoUpdateRow(props: AutoUpdateRowProps) { const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); - return
    - - - - - - - -

    - {t(Content.OS_AUTO_UPDATE)} -

    - - - props.dispatch(updateConfig({ - os_auto_update: !osAutoUpdate.value - }))} /> - -
    -
    -
    ; + return + + + + + +

    + {t(Content.OS_AUTO_UPDATE)} +

    + + + props.dispatch(updateConfig({ + os_auto_update: !osAutoUpdate.value + }))} /> + +
    +
    ; } diff --git a/frontend/devices/components/fbos_settings/interfaces.ts b/frontend/devices/components/fbos_settings/interfaces.ts index c9549e389..1056ba10f 100644 --- a/frontend/devices/components/fbos_settings/interfaces.ts +++ b/frontend/devices/components/fbos_settings/interfaces.ts @@ -12,7 +12,6 @@ import { TaggedDevice, } from "farmbot"; import { TimeSettings } from "../../../interfaces"; -import { PreferredHourFormat } from "./ota_time_selector"; export interface AutoSyncRowProps { dispatch: Function; @@ -21,9 +20,14 @@ export interface AutoSyncRowProps { export interface AutoUpdateRowProps { dispatch: Function; - timeFormat: PreferredHourFormat; + sourceFbosConfig: SourceFbosConfig; +} + +export interface OtaTimeSelectorRowProps { + dispatch: Function; sourceFbosConfig: SourceFbosConfig; device: TaggedDevice; + timeSettings: TimeSettings; } export interface CameraSelectionProps { diff --git a/frontend/devices/components/fbos_settings/ota_time_selector.tsx b/frontend/devices/components/fbos_settings/ota_time_selector.tsx index 182ca4eb6..425579545 100644 --- a/frontend/devices/components/fbos_settings/ota_time_selector.tsx +++ b/frontend/devices/components/fbos_settings/ota_time_selector.tsx @@ -6,11 +6,12 @@ import { edit, save } from "../../../api/crud"; import { ColWidth } from "../farmbot_os_settings"; import { DeviceSetting } from "../../../constants"; import { Highlight } from "../maybe_highlight"; +import { OtaTimeSelectorRowProps } from "./interfaces"; // tslint:disable-next-line:no-null-keyword const UNDEFINED = null as unknown as undefined; const IMMEDIATELY = -1; -export type PreferredHourFormat = "12h" | "24h"; +type PreferredHourFormat = "12h" | "24h"; type HOUR = | typeof IMMEDIATELY | 0 @@ -163,3 +164,13 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => { ; }; + +export function OtaTimeSelectorRow(props: OtaTimeSelectorRowProps) { + const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); + const timeFormat = props.timeSettings.hour24 ? "24h" : "12h"; + return ; +} 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/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx index 5788588ad..f9aa07f4f 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_layer_test.tsx @@ -4,11 +4,6 @@ jest.mock("../../../../../history", () => ({ getPathArray: jest.fn(() => { return mockPath.split("/"); }) })); -let mockDev = false; -jest.mock("../../../../../account/dev/dev_support", () => ({ - DevSettings: { futureFeaturesEnabled: () => mockDev } -})); - import * as React from "react"; import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer"; import { @@ -57,16 +52,6 @@ describe("", () => { expect(result.find(ToolSlotPoint).length).toEqual(1); }); - it("navigates to tools page", async () => { - mockDev = true; - mockPath = "/app/designer/plants"; - const p = fakeProps(); - const wrapper = shallow(); - const tools = wrapper.find("g").first(); - await tools.simulate("click"); - expect(history.push).toHaveBeenCalledWith("/app/tools"); - }); - it("doesn't navigate to tools page", async () => { mockPath = "/app/designer/plants/1"; const p = fakeProps(); diff --git a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx index a53c1508f..1aa96fc12 100644 --- a/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/__tests__/tool_slot_point_test.tsx @@ -1,8 +1,3 @@ -let mockDev = false; -jest.mock("../../../../../account/dev/dev_support", () => ({ - DevSettings: { futureFeaturesEnabled: () => mockDev } -})); - jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } })); import * as React from "react"; @@ -17,10 +12,6 @@ import { svgMount } from "../../../../../__test_support__/svg_mount"; import { history } from "../../../../../history"; describe("", () => { - beforeEach(() => { - mockDev = false; - }); - const fakeProps = (): TSPProps => ({ mapTransformProps: fakeMapTransformProps(), botPositionX: undefined, @@ -48,10 +39,6 @@ describe("", () => { const p = fakeProps(); p.slot.toolSlot.body.id = 1; const wrapper = svgMount(); - mockDev = true; - wrapper.find("g").first().simulate("click"); - expect(history.push).not.toHaveBeenCalled(); - mockDev = false; wrapper.find("g").first().simulate("click"); expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1"); }); diff --git a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx index d03dcef25..cf13ff88a 100644 --- a/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx +++ b/frontend/farm_designer/map/layers/tool_slots/tool_slot_layer.tsx @@ -2,9 +2,7 @@ import * as React from "react"; import { SlotWithTool, UUID } from "../../../../resources/interfaces"; import { ToolSlotPoint } from "./tool_slot_point"; import { MapTransformProps } from "../../interfaces"; -import { history, getPathArray } from "../../../../history"; import { maybeNoPointer } from "../../util"; -import { DevSettings } from "../../../../account/dev/dev_support"; export interface ToolSlotLayerProps { visible: boolean; @@ -16,17 +14,11 @@ export interface ToolSlotLayerProps { } export function ToolSlotLayer(props: ToolSlotLayerProps) { - const pathArray = getPathArray(); - const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4); - const goToToolsPage = () => canClickTool && - DevSettings.futureFeaturesEnabled() && history.push("/app/tools"); const { slots, visible, mapTransformProps } = props; - const cursor = canClickTool ? "pointer" : "default"; return + style={maybeNoPointer({ cursor: "pointer" })}> {visible && slots.map(slot => { xySwap, }; return !DevSettings.futureFeaturesEnabled() && - history.push(`/app/designer/tool-slots/${id}`)}> + onClick={() => history.push(`/app/designer/tool-slots/${id}`)}> {pullout_direction && !gantry_mounted && - {!DevSettings.futureFeaturesEnabled() && - } + ({ + planned: { label: t("Planned"), value: "planned" }, + planted: { label: t("Planted"), value: "planted" }, + sprouted: { label: t("Sprouted"), value: "sprouted" }, + harvested: { label: t("Harvested"), value: "harvested" }, +}); +export const PLANT_STAGE_LIST = () => [ + PLANT_STAGE_DDI_LOOKUP().planned, + PLANT_STAGE_DDI_LOOKUP().planted, + PLANT_STAGE_DDI_LOOKUP().sprouted, + PLANT_STAGE_DDI_LOOKUP().harvested, ]; -const PLANT_STAGES_DDI = { - [PLANT_STAGES[0].value]: { - label: PLANT_STAGES[0].label, - value: PLANT_STAGES[0].value - }, - [PLANT_STAGES[1].value]: { - label: PLANT_STAGES[1].label, - value: PLANT_STAGES[1].value - }, - [PLANT_STAGES[2].value]: { - label: PLANT_STAGES[2].label, - value: PLANT_STAGES[2].value - }, - [PLANT_STAGES[3].value]: { - label: PLANT_STAGES[3].label, - value: PLANT_STAGES[3].value - }, -}; - /** Change `planted_at` value based on `plant_stage` update. */ const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => { const update: PlantOptions = { plant_stage }; @@ -52,8 +39,8 @@ const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => { export function EditPlantStatus(props: EditPlantStatusProps) { const { plantStatus, updatePlant, uuid } = props; return updatePlant(uuid, getUpdateByPlantStage(ddi.value as PlantStage))} />; } @@ -70,7 +57,7 @@ export const PlantStatusBulkUpdate = (props: PlantStatusBulkUpdateProps) =>

    {t("update plant status to")}

    { diff --git a/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx b/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx index f1e170901..b5cd40b6f 100644 --- a/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx +++ b/frontend/farm_designer/point_groups/criteria/__tests__/add_test.tsx @@ -9,7 +9,6 @@ import { AddEqCriteria, AddNumberCriteria, editCriteria, AddStringCriteria, toggleStringCriteria, POINTER_TYPE_LIST, - PLANT_STAGE_LIST } from ".."; import { AddEqCriteriaProps, NumberCriteriaProps, DEFAULT_CRITERIA, @@ -19,6 +18,7 @@ import { fakePointGroup } from "../../../../__test_support__/fake_state/resources"; import { PointGroup } from "farmbot/dist/resources/api_resources"; +import { PLANT_STAGE_LIST } from "../../../plants/edit_plant_status"; describe(" />", () => { const fakeProps = (): AddEqCriteriaProps => ({ diff --git a/frontend/farm_designer/point_groups/criteria/add.tsx b/frontend/farm_designer/point_groups/criteria/add.tsx index a24826756..5081181a8 100644 --- a/frontend/farm_designer/point_groups/criteria/add.tsx +++ b/frontend/farm_designer/point_groups/criteria/add.tsx @@ -10,6 +10,9 @@ import { AddNumberCriteriaState, AddStringCriteriaProps, } from "./interfaces"; +import { + PLANT_STAGE_DDI_LOOKUP, PLANT_STAGE_LIST +} from "../../plants/edit_plant_status"; export class AddEqCriteria extends React.Component, AddEqCriteriaState> { @@ -78,19 +81,6 @@ export const POINTER_TYPE_LIST = () => [ POINTER_TYPE_DDI_LOOKUP().ToolSlot, ]; -export const PLANT_STAGE_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({ - planned: { label: t("Planned"), value: "planned" }, - planted: { label: t("Planted"), value: "planted" }, - sprouted: { label: t("Sprouted"), value: "sprouted" }, - harvested: { label: t("Harvested"), value: "harvested" }, -}); -export const PLANT_STAGE_LIST = () => [ - PLANT_STAGE_DDI_LOOKUP().planned, - PLANT_STAGE_DDI_LOOKUP().planted, - PLANT_STAGE_DDI_LOOKUP().sprouted, - PLANT_STAGE_DDI_LOOKUP().harvested, -]; - export class AddStringCriteria extends React.Component { state: AddEqCriteriaState = { key: "", value: "" }; diff --git a/frontend/farm_designer/point_groups/criteria/show.tsx b/frontend/farm_designer/point_groups/criteria/show.tsx index ad79f2326..22f301e8a 100644 --- a/frontend/farm_designer/point_groups/criteria/show.tsx +++ b/frontend/farm_designer/point_groups/criteria/show.tsx @@ -3,7 +3,7 @@ import { cloneDeep, capitalize } from "lodash"; import { Row, Col, FBSelect, DropDownItem } from "../../../ui"; import { AddEqCriteria, toggleEqCriteria, editCriteria, AddNumberCriteria, - POINTER_TYPE_DDI_LOOKUP, PLANT_STAGE_DDI_LOOKUP, AddStringCriteria, + POINTER_TYPE_DDI_LOOKUP, AddStringCriteria, CRITERIA_TYPE_DDI_LOOKUP, toggleStringCriteria } from "."; import { @@ -14,6 +14,7 @@ import { } from "./interfaces"; import { t } from "../../../i18next_wrapper"; import { PointGroup } from "farmbot/dist/resources/api_resources"; +import { PLANT_STAGE_DDI_LOOKUP } from "../../plants/edit_plant_status"; export class EqCriteriaSelection extends React.Component> { diff --git a/frontend/help/__tests__/tours_test.ts b/frontend/help/__tests__/tours_test.ts index ef6dd92db..de9ee790f 100644 --- a/frontend/help/__tests__/tours_test.ts +++ b/frontend/help/__tests__/tours_test.ts @@ -1,12 +1,5 @@ jest.mock("../../history", () => ({ history: { push: jest.fn() } })); -let mockDev = false; -jest.mock("../../account/dev/dev_support", () => ({ - DevSettings: { - futureFeaturesEnabled: () => mockDev, - } -})); - import { fakeState } from "../../__test_support__/fake_state"; const mockState = fakeState(); jest.mock("../../redux/store", () => ({ @@ -46,7 +39,6 @@ describe("tourPageNavigation()", () => { it("includes steps based on tool count", () => { const getTargets = () => Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target); - mockDev = false; mockState.resources = buildResourceIndex([]); expect(getTargets()).not.toContain(".tool-slots"); mockState.resources = buildResourceIndex([fakeTool()]); @@ -56,7 +48,6 @@ describe("tourPageNavigation()", () => { it("has correct content based on board version", () => { const getTitles = () => Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.title); - mockDev = false; mockState.resources = buildResourceIndex([]); expect(getTitles()).toContain("Add tools and slots"); expect(getTitles()).not.toContain("Add seed containers"); @@ -69,13 +60,4 @@ describe("tourPageNavigation()", () => { expect(getTitles()).not.toContain("Add seed containers and slots"); expect(getTitles()).toContain("Add seed containers"); }); - - it("includes correct tour steps", () => { - mockDev = true; - const targets = - Object.values(TOUR_STEPS()[Tours.gettingStarted]).map(t => t.target); - expect(targets).not.toContain(".tools"); - expect(targets).toContain(".tool-list"); - expect(targets).toContain(".toolbay-list"); - }); }); diff --git a/frontend/help/tours.ts b/frontend/help/tours.ts index 505f9a626..6d27b3b7b 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -2,7 +2,6 @@ import { history } from "../history"; import { Step as TourStep } from "react-joyride"; import { TourContent } from "../constants"; import { t } from "../i18next_wrapper"; -import { DevSettings } from "../account/dev/dev_support"; import { selectAllTools } from "../resources/selectors"; import { store } from "../redux/store"; import { getFbosConfig } from "../resources/getters"; @@ -64,16 +63,8 @@ export const TOUR_STEPS = (): { [x: string]: TourStep[] } => ({ content: t(TourContent.ADD_PLANTS), title: t("Add plants"), }, - ...(DevSettings.futureFeaturesEnabled() ? [{ - target: ".tool-list", - content: t(TourContent.ADD_TOOLS), - title: t("Add tools and seed containers"), - }] : toolsStep()), - ...(DevSettings.futureFeaturesEnabled() ? [{ - target: ".toolbay-list", - content: t(TourContent.ADD_TOOLS_SLOTS), - title: t("Add tools to tool bay"), - }] : toolSlotsStep()), + ...toolsStep(), + ...toolSlotsStep(), { target: ".peripherals-widget", content: t(TourContent.ADD_PERIPHERALS), diff --git a/frontend/nav/__tests__/nav_links_test.tsx b/frontend/nav/__tests__/nav_links_test.tsx index 3550df51c..db3682ccb 100644 --- a/frontend/nav/__tests__/nav_links_test.tsx +++ b/frontend/nav/__tests__/nav_links_test.tsx @@ -3,11 +3,6 @@ jest.mock("../../history", () => ({ getPathArray: jest.fn(() => mockPath.split("/")), })); -let mockDev = false; -jest.mock("../../account/dev/dev_support", () => ({ - DevSettings: { futureFeaturesEnabled: () => mockDev } -})); - import * as React from "react"; import { shallow, mount } from "enzyme"; import { NavLinks } from "../nav_links"; @@ -28,7 +23,6 @@ describe("", () => { }); it("shows links", () => { - mockDev = false; const wrapper = mount(); expect(wrapper.text().toLowerCase()).not.toContain("tools"); }); diff --git a/frontend/nav/nav_links.tsx b/frontend/nav/nav_links.tsx index e4d7bc76c..a0f0a6fb4 100644 --- a/frontend/nav/nav_links.tsx +++ b/frontend/nav/nav_links.tsx @@ -7,7 +7,6 @@ import { import { Link } from "../link"; import { t } from "../i18next_wrapper"; import { betterCompact } from "../util"; -import { DevSettings } from "../account/dev/dev_support"; /** Uses a slug and a child path to compute the `href` of a navbar link. */ export type LinkComputeFn = (slug: string, childPath: string) => string; @@ -37,8 +36,6 @@ export const getLinks = (): NavLinkParams[] => betterCompact([ name: "Regimens", icon: "calendar-check-o", slug: "regimens", computeHref: computeEditorUrlFromState("Regimen") }, - !DevSettings.futureFeaturesEnabled() ? undefined : - { name: "Tools", icon: "wrench", slug: "tools" }, { name: "Farmware", icon: "crosshairs", slug: "farmware", computeHref: computeFarmwareUrlFromState From c2308cb987eabf21199cf7baa9c05443d3f064d8 Mon Sep 17 00:00:00 2001 From: MarcRoland <56369838+MarcRoland@users.noreply.github.com> Date: Thu, 27 Feb 2020 11:52:14 -0800 Subject: [PATCH 20/26] Update de.json --- public/app-resources/languages/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app-resources/languages/de.json b/public/app-resources/languages/de.json index 474663aa8..8eeb1d5dc 100644 --- a/public/app-resources/languages/de.json +++ b/public/app-resources/languages/de.json @@ -536,7 +536,7 @@ "Can't execute unsaved sequences": "Can't execute unsaved sequences", "Cannot change from a Regimen to a Sequence.": "Cannot change from a Regimen to a Sequence.", "Cannot delete built-in pin binding.": "Cannot delete built-in pin binding.", - "clear filters": "clear filters", + "clear filters": "Filter löschen", "Click a spot in the grid to choose a location. Once selected, press button to move FarmBot to this position. Press the back arrow to exit.": "Click a spot in the grid to choose a location. Once selected, press button to move FarmBot to this position. Press the back arrow to exit.", "Click and drag to draw a point or use the inputs and press update. Press CREATE POINT to save, or the back arrow to exit.": "Click and drag to draw a point or use the inputs and press update. Press CREATE POINT to save, or the back arrow to exit.", "Click one in the Regimens panel to edit, or click \"+\" to create a new one.": "Click one in the Regimens panel to edit, or click \"+\" to create a new one.", From 800625e8a1e1e0c1e0518a9e086fd668c01872ce Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 28 Feb 2020 08:34:28 -0800 Subject: [PATCH 21/26] improve element annotation --- .../components/dangerous_delete_widget.tsx | 3 +- .../components/export_account_panel.tsx | 3 +- frontend/account/index.tsx | 11 +- .../account/labs/labs_features_list_ui.tsx | 3 +- frontend/account/request_account_export.ts | 5 +- frontend/controls/axis_display_group.tsx | 14 +-- frontend/controls/key_val_edit_row.tsx | 2 + frontend/controls/move/bot_position_rows.tsx | 2 +- frontend/controls/move/jog_controls_group.tsx | 2 +- .../controls/move/motor_position_plot.tsx | 24 ++--- frontend/controls/move/settings_menu.tsx | 2 +- frontend/controls/move/step_size_selector.tsx | 7 +- frontend/controls/peripherals/index.tsx | 10 +- frontend/controls/pin_form_fields.tsx | 1 + .../__tests__/filter_readings_test.ts | 14 +-- .../__tests__/location_selection_test.tsx | 8 +- .../__tests__/sensor_readings_test.tsx | 8 +- .../sensor_readings/filter_readings.ts | 6 +- .../controls/sensor_readings/interfaces.ts | 6 +- .../sensor_readings/location_selection.tsx | 14 +-- .../sensor_readings/sensor_readings.tsx | 16 +-- .../sensor_readings/time_period_selection.tsx | 1 + frontend/controls/sensors/index.tsx | 10 +- .../controls/webcam/__tests__/show_test.tsx | 2 +- frontend/controls/webcam/edit.tsx | 3 + frontend/controls/webcam/show.tsx | 5 +- .../farm_designer/farm_designer_panels.scss | 4 +- frontend/demo/demo_iframe.tsx | 7 +- frontend/devices/actions.ts | 4 +- .../__tests__/pin_guard_input_group_test.tsx | 2 +- .../__tests__/pin_number_dropdown_test.tsx | 2 +- .../components/farmbot_os_settings.tsx | 8 +- .../components/fbos_settings/board_type.tsx | 14 ++- .../fbos_settings/camera_selection.tsx | 14 ++- .../fbos_settings/change_ownership_form.tsx | 1 + .../fbos_settings/factory_reset_row.tsx | 7 +- .../fbos_settings/fbos_button_row.tsx | 1 + .../components/fbos_settings/fbos_details.tsx | 2 +- .../firmware_hardware_status.tsx | 1 + .../components/fbos_settings/interfaces.ts | 2 +- .../fbos_settings/power_and_reset.tsx | 4 +- .../devices/components/hardware_settings.tsx | 2 + .../hardware_settings/calibration_row.tsx | 1 + .../hardware_settings/danger_zone.tsx | 1 + .../components/hardware_settings/motors.tsx | 2 +- .../hardware_settings/pin_guard.tsx | 10 +- frontend/devices/components/interfaces.ts | 2 +- .../devices/components/lockable_button.tsx | 4 +- .../components/pin_guard_input_group.tsx | 4 +- .../devices/connectivity/connectivity.tsx | 4 +- frontend/devices/connectivity/diagnosis.tsx | 2 +- frontend/devices/connectivity/diagram.tsx | 5 +- frontend/devices/must_be_online.tsx | 2 +- .../pin_binding_input_group_test.tsx | 8 +- .../pin_bindings/pin_binding_input_group.tsx | 3 +- .../devices/pin_bindings/pin_bindings.tsx | 4 +- .../pin_bindings/tagged_pin_binding_init.tsx | 1 + frontend/error_boundary.tsx | 2 +- .../farm_events/edit_fe_form.tsx | 28 ++--- .../farm_events/farm_event_repeat_form.tsx | 102 +++++++++--------- .../farm_designer/farm_events/farm_events.tsx | 1 + frontend/farm_designer/index.tsx | 14 +-- .../farm_designer/map/easter_eggs/bugs.tsx | 3 +- .../farmbot/__tests__/bot_figure_test.tsx | 6 +- .../layers/farmbot/__tests__/index_test.tsx | 9 +- .../map/layers/farmbot/bot_figure.tsx | 8 +- .../map/layers/farmbot/index.tsx | 4 +- .../map/layers/images/map_image.tsx | 10 +- frontend/farm_designer/map_size_setting.tsx | 1 + frontend/farm_designer/move_to.tsx | 10 +- .../plants/__tests__/plant_panel_test.tsx | 2 +- frontend/farm_designer/plants/add_plant.tsx | 2 +- .../farm_designer/plants/crop_catalog.tsx | 1 + frontend/farm_designer/plants/crop_info.tsx | 15 +-- .../plants/grid/__tests__/plant_grid_test.tsx | 7 +- .../farm_designer/plants/grid/plant_grid.tsx | 18 ++-- .../plants/openfarm_search_results.tsx | 54 +++++----- .../farm_designer/plants/plant_inventory.tsx | 2 +- frontend/farm_designer/plants/plant_panel.tsx | 17 +-- .../farm_designer/plants/select_plants.tsx | 4 + .../__tests__/point_group_sort_test.ts | 4 +- .../point_groups/criteria/add.tsx | 15 ++- .../point_groups/criteria/component.tsx | 18 ++-- .../point_groups/criteria/show.tsx | 59 ++++++---- .../point_groups/group_list_panel.tsx | 1 + .../__tests__/point_edit_actions_test.tsx | 2 +- .../farm_designer/points/create_points.tsx | 4 +- .../points/point_edit_actions.tsx | 15 ++- .../farm_designer/points/point_inventory.tsx | 2 +- .../farm_designer/points/weeds_inventory.tsx | 2 +- .../__tests__/garden_edit_test.tsx | 1 + .../__tests__/garden_snapshot_test.tsx | 6 +- .../farm_designer/saved_gardens/actions.ts | 13 +-- .../saved_gardens/garden_edit.tsx | 8 +- .../saved_gardens/garden_snapshot.tsx | 22 ++-- .../saved_gardens/saved_gardens.tsx | 6 +- frontend/farm_designer/tools/add_tool.tsx | 3 + frontend/farm_designer/tools/index.tsx | 2 +- .../tools/tool_slot_edit_components.tsx | 4 +- .../farm_designer/zones/zones_inventory.tsx | 2 +- frontend/farmware/farmware_config_menu.tsx | 3 + frontend/farmware/farmware_forms.tsx | 3 +- frontend/farmware/farmware_info.tsx | 59 +++++----- frontend/farmware/farmware_list.tsx | 3 +- frontend/farmware/images/image_flipper.tsx | 4 +- frontend/farmware/images/photos.tsx | 3 +- frontend/farmware/index.tsx | 4 +- frontend/farmware/state_to_props.ts | 6 +- frontend/farmware/weed_detector/config.tsx | 3 +- .../farmware/weed_detector/farmbot_picker.tsx | 2 +- frontend/farmware/weed_detector/index.tsx | 1 + frontend/folders/actions.ts | 4 +- frontend/folders/component.tsx | 11 +- frontend/front_page/create_account.tsx | 16 +-- frontend/front_page/forgot_password.tsx | 2 + frontend/front_page/front_page.tsx | 14 ++- frontend/front_page/login.tsx | 7 +- frontend/front_page/resend_panel_body.tsx | 1 + frontend/front_page/resend_verification.tsx | 1 + frontend/front_page/terms_checkbox.tsx | 2 +- frontend/help/__tests__/tour_test.tsx | 2 +- frontend/help/docs.tsx | 8 +- frontend/help/tour.tsx | 4 +- frontend/help/tour_list.tsx | 1 + frontend/logs/components/filter_menu.tsx | 9 +- frontend/logs/components/settings_menu.tsx | 6 +- frontend/logs/index.tsx | 25 ++--- frontend/nav/additional_menu.tsx | 8 +- frontend/nav/index.tsx | 12 +-- frontend/nav/mobile_menu.tsx | 2 +- frontend/nav/sync_button.tsx | 1 + frontend/password_reset/password_reset.tsx | 1 + .../__tests__/read_only_icon_test.tsx | 6 +- frontend/read_only_mode/index.tsx | 2 +- .../set_active_regimen_by_name_test.ts | 4 +- .../__tests__/add_button_test.tsx | 2 +- .../regimens/bulk_scheduler/add_button.tsx | 4 +- frontend/regimens/bulk_scheduler/index.tsx | 4 +- .../regimens/bulk_scheduler/week_grid.tsx | 10 +- frontend/regimens/bulk_scheduler/week_row.tsx | 1 + frontend/regimens/editor/active_editor.tsx | 2 + frontend/regimens/editor/copy_button.tsx | 1 + .../regimens/editor/regimen_name_input.tsx | 1 + frontend/regimens/list/add_button.tsx | 1 + frontend/regimens/list/index.tsx | 4 +- frontend/regimens/list/regimen_list_item.tsx | 5 +- .../regimens/set_active_regimen_by_name.ts | 4 +- frontend/resources/selectors.ts | 3 +- frontend/resources/selectors_by_id.ts | 6 +- .../set_active_sequence_by_name_test.ts | 4 +- frontend/sequences/inputs/input_unknown.tsx | 2 +- .../locals_list/default_value_form.tsx | 2 +- .../location_form_coordinate_input_boxes.tsx | 2 +- .../locals_list/location_form_list.ts | 6 +- .../sequence_editor_middle_active.tsx | 3 + .../sequences/set_active_sequence_by_name.ts | 4 +- frontend/sequences/step_button_cluster.tsx | 18 ++-- frontend/sequences/step_buttons.tsx | 1 + .../step_tiles/mark_as/unpack_step.ts | 4 +- .../step_tiles/tile_assertion/lua_part.tsx | 2 +- .../step_tiles/tile_execute_script.tsx | 14 +-- .../tile_execute_script_support.tsx | 58 +++++----- .../tile_move_absolute_conflict_check.tsx | 2 +- .../step_tiles/tile_send_message.tsx | 17 ++- frontend/sequences/step_ui/step_radio.tsx | 2 +- frontend/sequences/step_ui/step_warning.tsx | 2 +- frontend/sequences/test_button.tsx | 6 +- frontend/session.ts | 16 +-- frontend/tos_update/component.tsx | 11 +- frontend/ui/back_arrow.tsx | 3 +- frontend/ui/color_picker.tsx | 2 + frontend/ui/empty_state_wrapper.tsx | 2 +- frontend/ui/help.tsx | 2 +- frontend/ui/input_error.tsx | 2 +- frontend/ui/save_button.tsx | 3 +- 175 files changed, 741 insertions(+), 556 deletions(-) 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/index.tsx b/frontend/account/index.tsx index 658704fae..dac34ae48 100644 --- a/frontend/account/index.tsx +++ b/frontend/account/index.tsx @@ -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/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/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 - - - + + +
    @@ -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..35a972aa1 100644 --- a/frontend/farmware/index.tsx +++ b/frontend/farmware/index.tsx @@ -102,10 +102,11 @@ interface BasicFarmwarePageProps { export const BasicFarmwarePage = ({ farmwareName, farmware, botOnline }: BasicFarmwarePageProps) => -
    +
    @@ -208,6 +209,7 @@ export class RawFarmwarePage extends React.Component { ; }; @@ -92,6 +94,7 @@ const ToggleFolderBtn = (props: ToggleFolderBtnProps) => { const AddFolderBtn = ({ folder, close }: AddFolderBtn) => { return @@ -139,6 +145,7 @@ export const FolderNameInput = ({ node }: FolderNameInputProps) => }} /> @@ -327,7 +334,7 @@ export const FolderPanelTop = (props: FolderPanelTopProps) => updateSearchTerm(e.currentTarget.value)} - type="text" + type="text" name="searchTerm" placeholder={t("Search sequences...")} />
    diff --git a/frontend/front_page/create_account.tsx b/frontend/front_page/create_account.tsx index a4420daa4..b45978448 100644 --- a/frontend/front_page/create_account.tsx +++ b/frontend/front_page/create_account.tsx @@ -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/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 38be5e14e..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(); diff --git a/frontend/help/docs.tsx b/frontend/help/docs.tsx index 8ad5a5c2d..b6d37d933 100644 --- a/frontend/help/docs.tsx +++ b/frontend/help/docs.tsx @@ -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 3f2e6408f..d2a2f712b 100644 --- a/frontend/help/tour.tsx +++ b/frontend/help/tour.tsx @@ -82,5 +82,7 @@ export class Tour extends React.Component { } 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...")} /> diff --git a/frontend/nav/additional_menu.tsx b/frontend/nav/additional_menu.tsx index f9e627495..d5d5ec10d 100644 --- a/frontend/nav/additional_menu.tsx +++ b/frontend/nav/additional_menu.tsx @@ -7,13 +7,13 @@ import { ExternalUrl } from "../external_urls"; export const AdditionalMenu = (props: AccountMenuProps) => { return
    -
    +
    {t("Account Settings")}
    -
    +
    {t("Logs")} @@ -23,8 +23,8 @@ export const AdditionalMenu = (props: AccountMenuProps) => { {t("Help")} -
    - +
    + {t("Logout")} 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> {