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) diff --git a/app/models/in_use_point.rb b/app/models/in_use_point.rb index 820684516..4c29f3894 100644 --- a/app/models/in_use_point.rb +++ b/app/models/in_use_point.rb @@ -6,7 +6,7 @@ class InUsePoint < ApplicationRecord DEFAULT_NAME = "point" FANCY_NAMES = { GenericPointer.name => DEFAULT_NAME, - ToolSlot.name => "tool slot", + ToolSlot.name => "slot", Plant.name => "plant", } diff --git a/app/models/point_group.rb b/app/models/point_group.rb index eef492243..36c56ffd4 100644 --- a/app/models/point_group.rb +++ b/app/models/point_group.rb @@ -4,7 +4,7 @@ class PointGroup < ApplicationRecord BAD_SORT = "%{value} is not valid. Valid options are: " + SORT_TYPES.map(&:inspect).join(", ") DEFAULT_CRITERIA = { - day: { op: "<", days: 0 }, + day: { op: "<", days_ago: 0 }, string_eq: {}, number_eq: {}, number_lt: {}, diff --git a/app/models/tool_slot.rb b/app/models/tool_slot.rb index 22a0abd90..e416dadbd 100644 --- a/app/models/tool_slot.rb +++ b/app/models/tool_slot.rb @@ -11,7 +11,7 @@ class ToolSlot < Point MIN_PULLOUT = PULLOUT_DIRECTIONS.min PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\ "%{value} is not valid." - IN_USE = "already in use by another tool slot. "\ + IN_USE = "already in use by another slot. "\ "Please un-assign the tool from its current slot"\ " before reassigning." diff --git a/app/mutations/point_groups/helpers.rb b/app/mutations/point_groups/helpers.rb index 0f5a3d7f5..7d6094e45 100644 --- a/app/mutations/point_groups/helpers.rb +++ b/app/mutations/point_groups/helpers.rb @@ -5,7 +5,7 @@ module PointGroups hash :criteria do hash(:day) do string :op, in: [">", "<"] - integer :days + integer :days_ago end hash(:string_eq) { array :*, class: String } hash(:number_eq) { array :*, class: Integer } diff --git a/app/mutations/tools/destroy.rb b/app/mutations/tools/destroy.rb index 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/__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/__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/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/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.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..e67e5ac60 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -552,8 +552,12 @@ } .tool-slots-panel-content, .tools-panel-content { + max-height: calc(100vh - 19rem); + overflow-y: auto; + overflow-x: hidden; .tool-search-item, .tool-slot-search-item { + line-height: 4rem; cursor: pointer; margin-left: -15px; margin-right: -15px; @@ -562,11 +566,32 @@ margin-right: 0; } p { - line-height: 3rem; + font-size: 1.2rem; + line-height: 4rem; + &.tool-status, &.tool-slot-position { float: right; } } + .filter-search { + .bp3-button { + min-height: 2.5rem; + max-height: 2.5rem; + span { + line-height: 1.5rem; + } + } + i { + line-height: 2rem; + } + } + svg { + vertical-align: middle; + } + .tool-slot-position-info { + padding: 0; + padding-right: 1rem; + } } .mounted-tool-header { display: flex; @@ -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 { @@ -888,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 02ff86b62..9b63e725d 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -226,7 +226,7 @@ fieldset { .percent-bar { position: absolute; top: 2px; - left: 12rem; + right: 0; height: 1rem; width: 25%; clip-path: polygon(0 85%, 100% 0, 100% 100%, 0% 100%); @@ -407,6 +407,18 @@ a { } } +.load-progress-bar-wrapper { + position: absolute; + top: 3.2rem; + bottom: 0; + right: 0; + width: 100%; + height: 1px; + .load-progress-bar { + height: 100%; + } +} + .firmware-setting-export-menu { button { margin-bottom: 1rem; @@ -1543,16 +1555,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; @@ -1649,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/inputs.scss b/frontend/css/inputs.scss index e31b27443..9326c1a44 100644 --- a/frontend/css/inputs.scss +++ b/frontend/css/inputs.scss @@ -154,4 +154,12 @@ select { } } } + &.disabled { + input[type="checkbox"] { + cursor: not-allowed; + &:checked:after { + border-color: $gray; + } + } + } } diff --git a/frontend/css/sequences.scss b/frontend/css/sequences.scss index be3cdff30..83472c0e4 100644 --- a/frontend/css/sequences.scss +++ b/frontend/css/sequences.scss @@ -322,6 +322,9 @@ border-left: 4px solid transparent; &.active { border-left: 4px solid $dark_gray; + p { + font-weight: bold; + } } .fa-chevron-down, .fa-chevron-right { position: absolute; @@ -330,11 +333,11 @@ font-size: 1.1rem; } .folder-settings-icon, - .fa-bars { + .fa-arrows-v { position: absolute; right: 0; } - .fa-bars, .fa-ellipsis-v { + .fa-arrows-v, .fa-ellipsis-v { display: none; } .fa-ellipsis-v { @@ -342,8 +345,14 @@ display: block; } } + @media screen and (max-width: 450px) { + .fa-arrows-v, .fa-ellipsis-v { + display: block; + margin-right: 0.5rem; + } + } &:hover { - .fa-bars, .fa-ellipsis-v { + .fa-arrows-v, .fa-ellipsis-v { display: block; } } @@ -367,7 +376,7 @@ white-space: nowrap; text-overflow: ellipsis; font-size: 1.2rem; - font-weight: bold; + font-weight: normal; width: 75%; padding: 0.5rem; padding-left: 0; diff --git a/frontend/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/__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/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/__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/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/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index c06a4b627..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 : "N/A"} + {wifiStrength ? numberDisplay : "N/A"}

{wifiStrength &&
@@ -261,8 +264,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
{t("Disk usage")}: {disk_usage}%

} {isNumber(cpu_usage) &&

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

} - ; 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 +66,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 +93,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 +148,29 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => { const selectedItem = (typeof value == "number") ? theTimeTable[value as HOUR] : theTimeTable[DEFAULT_HOUR]; return - - - - - - + + + + + + + + ; }; + +export function OtaTimeSelectorRow(props: OtaTimeSelectorRowProps) { + const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); + const timeFormat = props.timeSettings.hour24 ? "24h" : "12h"; + return ; +} 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__/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__/panel_header_test.tsx b/frontend/farm_designer/__tests__/panel_header_test.tsx index 546976771..5df39adab 100644 --- a/frontend/farm_designer/__tests__/panel_header_test.tsx +++ b/frontend/farm_designer/__tests__/panel_header_test.tsx @@ -71,6 +71,14 @@ describe("", () => { expect(wrapper.html()).toContain("active"); }); + it("renders for tools", () => { + mockPath = "/app/designer/tools"; + mockDev = false; + const wrapper = shallow(); + expect(wrapper.hasClass("gray-panel")).toBeTruthy(); + expect(wrapper.html()).toContain("active"); + }); + it("renders for zones", () => { mockPath = "/app/designer/zones"; mockDev = true; diff --git a/frontend/farm_designer/__tests__/plant_test.ts b/frontend/farm_designer/__tests__/plant_test.ts new file mode 100644 index 000000000..12bb2eac5 --- /dev/null +++ b/frontend/farm_designer/__tests__/plant_test.ts @@ -0,0 +1,19 @@ +import { Plant } from "../plant"; + +describe("Plant()", () => { + it("returns defaults", () => { + expect(Plant({})).toEqual({ + created_at: "", + id: undefined, + meta: {}, + name: "Untitled Plant", + openfarm_slug: "not-set", + plant_stage: "planned", + pointer_type: "Plant", + radius: 25, + x: 0, + y: 0, + z: 0, + }); + }); +}); diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index 375b6106c..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/__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/designer_panel.tsx b/frontend/farm_designer/designer_panel.tsx index 3fc41850d..dd369abf5 100644 --- a/frontend/farm_designer/designer_panel.tsx +++ b/frontend/farm_designer/designer_panel.tsx @@ -90,7 +90,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
{!props.noIcon && - } + } {props.children} diff --git a/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx b/frontend/farm_designer/farm_events/__tests__/add_farm_event_test.tsx index 04dfb8a2d..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/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()}> + + + + +
; }; 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..3101ddc73 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("in slot"); + }); + + 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..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 @@ -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,8 @@ describe("", () => { selectedTool: undefined, onChange: jest.fn(), filterSelectedTool: false, + isActive: jest.fn(), + filterActiveTools: true, }); it("renders", () => { @@ -98,12 +72,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 +112,7 @@ describe("", () => { selectedTool: undefined, onChange: jest.fn(), isExpress: false, + isActive: jest.fn(), }); it("renders", () => { @@ -164,6 +140,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 +172,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)} />
Tools = () => @@ -185,6 +193,8 @@ export class RawTools extends React.Component { .map(tool => )}
@@ -202,12 +212,8 @@ export class RawTools extends React.Component { 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"), + toolSlots: t("slots"), + addSlot: t("Add slot"), }; } @@ -240,27 +246,49 @@ export class RawTools extends React.Component { } } -interface ToolSlotInventoryItemProps { +export interface ToolSlotInventoryItemProps { toolSlot: TaggedToolSlotPointer; - getToolName(toolId: number | undefined): string | undefined; + tools: TaggedTool[]; hovered: boolean; dispatch: Function; + isActive(id: number | undefined): boolean; } -const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { +export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { const { x, y, z, id, tool_id, gantry_mounted } = props.toolSlot.body; + const toolName = props.tools + .filter(tool => tool.body.id == tool_id)[0]?.body.name; return
history.push(`/app/designer/tool-slots/${id}`)} onMouseEnter={() => props.dispatch(setToolHover(props.toolSlot.uuid))} onMouseLeave={() => props.dispatch(setToolHover(undefined))}> - -

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

+ + - + +
e.stopPropagation()}> + tool.body.id == tool_id)[0]} + onChange={update => { + props.dispatch(edit(props.toolSlot, update)); + props.dispatch(save(props.toolSlot.uuid)); + }} + isActive={props.isActive} + filterSelectedTool={false} + filterActiveTools={true} /> +
+ +

- {botPositionLabel({ x, y, z }, gantry_mounted)} + {botPositionLabel({ x, y, z }, gantry_mounted)}

@@ -270,16 +298,28 @@ const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => { interface ToolInventoryItemProps { toolName: string; toolId: number | undefined; + mounted: boolean; + active: boolean; } -const ToolInventoryItem = (props: ToolInventoryItemProps) => -
{ + const activeText = props.active ? t("in slot") : t("inactive"); + return
history.push(`/app/designer/tools/${props.toolId}`)}> - + + + +

{t(props.toolName)}

+ +

+ {props.mounted ? t("mounted") : activeText} +

+
; +}; export const Tools = connect(mapStateToProps)(RawTools); diff --git a/frontend/farm_designer/tools/map_to_props_add_edit.ts b/frontend/farm_designer/tools/map_to_props_add_edit.ts new file mode 100644 index 000000000..8b3b42a37 --- /dev/null +++ b/frontend/farm_designer/tools/map_to_props_add_edit.ts @@ -0,0 +1,67 @@ +import { Everything } from "../../interfaces"; +import { TaggedTool, TaggedToolSlotPointer, FirmwareHardware } from "farmbot"; +import { + selectAllTools, maybeFindToolById, maybeGetToolSlot, maybeFindToolSlotById, + selectAllToolSlotPointers, +} from "../../resources/selectors"; +import { BotPosition } from "../../devices/interfaces"; +import { validBotLocationData } from "../../util"; +import { UUID } from "../../resources/interfaces"; +import { + getFwHardwareValue +} from "../../devices/components/firmware_hardware_support"; +import { getFbosConfig } from "../../resources/getters"; +import { getWebAppConfigValue } from "../../config_storage/actions"; +import { BooleanSetting, NumericSetting } from "../../session_keys"; +import { BotOriginQuadrant, isBotOriginQuadrant } from "../interfaces"; +import { isActive } from "./edit_tool"; + +export interface AddEditToolSlotPropsBase { + tools: TaggedTool[]; + dispatch: Function; + botPosition: BotPosition; + findTool(id: number): TaggedTool | undefined; + firmwareHardware: FirmwareHardware | undefined; + xySwap: boolean; + quadrant: BotOriginQuadrant; + isActive(id: number | undefined): boolean; +} + +export const mapStateToPropsBase = (props: Everything): AddEditToolSlotPropsBase => { + const getWebAppConfig = getWebAppConfigValue(() => props); + const xySwap = !!getWebAppConfig(BooleanSetting.xy_swap); + const rawQuadrant = getWebAppConfig(NumericSetting.bot_origin_quadrant); + const quadrant = isBotOriginQuadrant(rawQuadrant) ? rawQuadrant : 2; + return { + tools: selectAllTools(props.resources.index), + dispatch: props.dispatch, + botPosition: validBotLocationData(props.bot.hardware.location_data).position, + findTool: (id: number) => maybeFindToolById(props.resources.index, id), + firmwareHardware: getFwHardwareValue(getFbosConfig(props.resources.index)), + xySwap, + quadrant, + isActive: isActive(selectAllToolSlotPointers(props.resources.index)), + }; +}; + +export interface AddToolSlotProps extends AddEditToolSlotPropsBase { + findToolSlot(uuid: UUID | undefined): TaggedToolSlotPointer | undefined; +} + +export const mapStateToPropsAdd = (props: Everything): AddToolSlotProps => { + const mapStateToProps = mapStateToPropsBase(props) as AddToolSlotProps; + mapStateToProps.findToolSlot = (uuid: UUID | undefined) => + maybeGetToolSlot(props.resources.index, uuid); + return mapStateToProps; +}; + +export interface EditToolSlotProps extends AddEditToolSlotPropsBase { + findToolSlot(id: string): TaggedToolSlotPointer | undefined; +} + +export const mapStateToPropsEdit = (props: Everything): EditToolSlotProps => { + const mapStateToProps = mapStateToPropsBase(props) as EditToolSlotProps; + mapStateToProps.findToolSlot = (id: string) => + maybeFindToolSlotById(props.resources.index, parseInt(id)); + return mapStateToProps; +}; diff --git a/frontend/farm_designer/tools/tool_slot_edit_components.tsx b/frontend/farm_designer/tools/tool_slot_edit_components.tsx index c1e58ccd0..6654bcde0 100644 --- a/frontend/farm_designer/tools/tool_slot_edit_components.tsx +++ b/frontend/farm_designer/tools/tool_slot_edit_components.tsx @@ -1,15 +1,15 @@ import React from "react"; import { t } from "../../i18next_wrapper"; import { Xyz, TaggedTool, TaggedToolSlotPointer } from "farmbot"; -import { Row, Col, BlurableInput, FBSelect, NULL_CHOICE } 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; @@ -24,25 +24,6 @@ export const GantryMountedInput = (props: GantryMountedInputProps) => checked={props.gantryMounted} /> ; -export interface UseCurrentLocationInputRowProps { - botPosition: BotPosition; - onChange(botPosition: BotPosition): void; -} - -export const UseCurrentLocationInputRow = - (props: UseCurrentLocationInputRowProps) => -
- - -

{positionButtonTitle(props.botPosition)}

-
; - export interface SlotDirectionInputRowProps { toolPulloutDirection: ToolPulloutDirection; onChange(update: { pullout_direction: ToolPulloutDirection }): void; @@ -51,7 +32,7 @@ export interface SlotDirectionInputRowProps { export const SlotDirectionInputRow = (props: SlotDirectionInputRowProps) =>
(!props.filterSelectedTool || !props.selectedTool) - || tool.body.id != props.selectedTool.body.id) + list={([NULL_CHOICE] as DropDownItem[]).concat(props.tools + .filter(tool => !props.filterSelectedTool + || tool.body.id != props.selectedTool?.body.id) + .filter(tool => !props.filterActiveTools + || !props.isActive(tool.body.id)) .map(tool => ({ label: tool.body.name || "untitled", value: tool.body.id || 0, })) - .filter(ddi => ddi.value > 0)} + .filter(ddi => ddi.value > 0))} selectedItem={props.selectedTool ? { label: props.selectedTool.body.name || "untitled", value: "" + props.selectedTool.body.id } : NULL_CHOICE} - allowEmpty={true} onChange={ddi => props.onChange({ tool_id: parseInt("" + ddi.value) })} />; @@ -98,6 +82,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,7 +98,9 @@ export const ToolInputRow = (props: ToolInputRowProps) => tools={props.tools} selectedTool={props.selectedTool} onChange={props.onChange} - filterSelectedTool={false} /> + isActive={props.isActive} + filterSelectedTool={false} + filterActiveTools={true} />
; @@ -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,28 +156,76 @@ 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 && }
; + +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/folders/__tests__/component_test.tsx b/frontend/folders/__tests__/component_test.tsx index 927fb8cfa..94d886679 100644 --- a/frontend/folders/__tests__/component_test.tsx +++ b/frontend/folders/__tests__/component_test.tsx @@ -294,14 +294,14 @@ describe("", () => { it("starts sequence move", () => { const p = fakeProps(); const wrapper = shallow(); - wrapper.find(".fa-bars").simulate("mouseDown"); + wrapper.find(".fa-arrows-v").simulate("mouseDown"); expect(p.startSequenceMove).toHaveBeenCalledWith(p.sequence.uuid); }); it("toggles sequence move", () => { const p = fakeProps(); const wrapper = shallow(); - wrapper.find(".fa-bars").simulate("mouseUp"); + wrapper.find(".fa-arrows-v").simulate("mouseUp"); expect(p.toggleSequenceMove).toHaveBeenCalledWith(p.sequence.uuid); }); }); diff --git a/frontend/folders/component.tsx b/frontend/folders/component.tsx index 3fb2b26f6..f00381c05 100644 --- a/frontend/folders/component.tsx +++ b/frontend/folders/component.tsx @@ -75,7 +75,7 @@ export const FolderListItem = (props: FolderItemProps) => {
{props.inUse && } - props.startSequenceMove(sequence.uuid)} onMouseUp={() => props.toggleSequenceMove(sequence.uuid)} />
@@ -328,7 +328,7 @@ export const FolderPanelTop = (props: FolderPanelTopProps) => value={props.searchTerm || ""} onChange={e => updateSearchTerm(e.currentTarget.value)} type="text" - placeholder={t("Search sequences")} /> + placeholder={t("Search sequences...")} />
{ noFolder: (localMetaAttributes[PARENTLESS] || {}).sequences || [] }; const index = folders.map(setDefaultParentId).reduce(addToIndex, emptyIndex); - const childrenOf = (i: number) => sortBy(index[i] || [], (x) => x.name.toLowerCase()); + const childrenOf = (i: number) => + sortBy(index[i] || [], (x) => x.name.toLowerCase()); const terminal = (x: FolderNode): FolderNodeTerminal => ({ ...x, kind: "terminal", content: (localMetaAttributes[x.id] || {}).sequences || [], - open: true, + open: false, editing: false, // children: [], ...(localMetaAttributes[x.id] || {}) @@ -55,7 +56,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => { const medial = (x: FolderNode): FolderNodeMedial => ({ ...x, kind: "medial", - open: true, + open: false, editing: false, children: childrenOf(x.id).map(terminal), content: (localMetaAttributes[x.id] || {}).sequences || [], @@ -67,7 +68,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => { return output.folders.push({ ...root, kind: "initial", - open: true, + open: false, editing: false, children, content: (localMetaAttributes[root.id] || {}).sequences || [], diff --git a/frontend/help/__tests__/tours_test.ts b/frontend/help/__tests__/tours_test.ts index 98be2687a..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,9 +48,8 @@ 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 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"; @@ -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 fc4339da5..6d27b3b7b 100644 --- a/frontend/help/tours.ts +++ b/frontend/help/tours.ts @@ -2,12 +2,11 @@ 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 + getFwHardwareValue, hasUTM } from "../devices/components/firmware_hardware_support"; export enum Tours { @@ -25,35 +24,35 @@ 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 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"), }] : []; @@ -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/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/__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/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/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 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/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/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/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_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/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/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, - }; - -} 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 - + ; } diff --git a/package.json b/package.json index d6fa95495..58a807c65 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" @@ -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", @@ -45,8 +45,8 @@ "coveralls": "3.0.9", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", - "farmbot": "9.1.0", - "i18next": "19.0.3", + "farmbot": "9.1.2", + "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/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.", 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) 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",