diff --git a/db/migrate/20190411171401_add_show_pins_to_web_app_configs.rb b/db/migrate/20190411171401_add_show_pins_to_web_app_configs.rb new file mode 100644 index 000000000..90c1bcfbd --- /dev/null +++ b/db/migrate/20190411171401_add_show_pins_to_web_app_configs.rb @@ -0,0 +1,9 @@ +class AddShowPinsToWebAppConfigs < ActiveRecord::Migration[5.2] + safety_assured + def change + add_column :web_app_configs, + :show_pins, + :boolean, + default: false + end +end diff --git a/db/migrate/20190411222900_change_defaults_auto_sync_and_homing.rb b/db/migrate/20190411222900_change_defaults_auto_sync_and_homing.rb new file mode 100644 index 000000000..cdbf8de65 --- /dev/null +++ b/db/migrate/20190411222900_change_defaults_auto_sync_and_homing.rb @@ -0,0 +1,6 @@ +class ChangeDefaultsAutoSyncAndHoming < ActiveRecord::Migration[5.2] + def change + change_column_default(:fbos_configs, :auto_sync, from: false, to: true) + change_column_default(:web_app_configs, :home_button_homing, from: false, to: true) + end +end diff --git a/frontend/__tests__/app_test.tsx b/frontend/__tests__/app_test.tsx index ac253c2cd..88a8ffe5c 100644 --- a/frontend/__tests__/app_test.tsx +++ b/frontend/__tests__/app_test.tsx @@ -2,7 +2,8 @@ jest.mock("react-redux", () => ({ connect: jest.fn() })); let mockPath = ""; jest.mock("../history", () => ({ - getPathArray: jest.fn(() => { return mockPath.split("/"); }) + getPathArray: jest.fn(() => mockPath.split("/")), + history: { getCurrentLocation: () => ({ pathname: mockPath }) } })); import * as React from "react"; @@ -39,6 +40,7 @@ const fakeProps = (): AppProps => { getConfigValue: jest.fn(), tour: undefined, resources: buildResourceIndex().index, + autoSync: false, }; }; diff --git a/frontend/account/labs/__tests__/fetch_lab_features_test.ts b/frontend/account/labs/__tests__/fetch_lab_features_test.ts index 40835b2bf..6632375e2 100644 --- a/frontend/account/labs/__tests__/fetch_lab_features_test.ts +++ b/frontend/account/labs/__tests__/fetch_lab_features_test.ts @@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => { Object.defineProperty(window.location, "reload", { value: jest.fn() }); it("basically just initializes stuff", () => { const val = fetchLabFeatures(jest.fn()); - expect(val.length).toBe(10); + expect(val.length).toBe(9); expect(val[0].value).toBeFalsy(); const { callback } = val[0]; if (callback) { diff --git a/frontend/account/labs/labs_features_list_data.ts b/frontend/account/labs/labs_features_list_data.ts index a182c470b..cfb0a2448 100644 --- a/frontend/account/labs/labs_features_list_data.ts +++ b/frontend/account/labs/labs_features_list_data.ts @@ -1,4 +1,3 @@ - import { BooleanSetting } from "../../session_keys"; import { Content } from "../../constants"; import { VirtualTrail } from "../../farm_designer/map/layers/farmbot/bot_trail"; @@ -33,12 +32,6 @@ export const fetchLabFeatures = displayInvert: true, callback: () => window.location.reload() }, - { - name: t("Confirm Sequence step deletion"), - description: t(Content.CONFIRM_STEP_DELETION), - storageKey: BooleanSetting.confirm_step_deletion, - value: false - }, { name: t("Hide Webcam widget"), description: t(Content.HIDE_WEBCAM_WIDGET), diff --git a/frontend/app.tsx b/frontend/app.tsx index 8fcfbd1dd..d2dd28839 100644 --- a/frontend/app.tsx +++ b/frontend/app.tsx @@ -14,7 +14,7 @@ import { import { HotKeys } from "./hotkeys"; import { ControlsPopup } from "./controls_popup"; import { Content } from "./constants"; -import { validBotLocationData, validFwConfig } from "./util"; +import { validBotLocationData, validFwConfig, validFbosConfig } from "./util"; import { BooleanSetting } from "./session_keys"; import { getPathArray } from "./history"; import { @@ -22,7 +22,7 @@ import { } from "./config_storage/actions"; import { takeSortedLogs } from "./logs/state_to_props"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; -import { getFirmwareConfig } from "./resources/getters"; +import { getFirmwareConfig, getFbosConfig } from "./resources/getters"; import { intersection } from "lodash"; import { t } from "./i18next_wrapper"; import { ResourceIndex } from "./resources/interfaces"; @@ -47,10 +47,12 @@ export interface AppProps { getConfigValue: GetWebAppConfigValue; tour: string | undefined; resources: ResourceIndex; + autoSync: boolean; } export function mapStateToProps(props: Everything): AppProps { const webAppConfigValue = getWebAppConfigValue(() => props); + const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index)); return { timeSettings: maybeGetTimeSettings(props.resources.index), dispatch: props.dispatch, @@ -70,6 +72,7 @@ export function mapStateToProps(props: Everything): AppProps { getConfigValue: webAppConfigValue, tour: props.resources.consumers.help.currentTour, resources: props.resources.index, + autoSync: !!(fbosConfig && fbosConfig.auto_sync), }; } /** Time at which the app gives up and asks the user to refresh */ @@ -125,6 +128,7 @@ export class App extends React.Component { logs={this.props.logs} getConfigValue={this.props.getConfigValue} tour={this.props.tour} + autoSync={this.props.autoSync} device={getDeviceAccountSettings(this.props.resources)} />} {syncLoaded && this.props.children} {!(["controls", "account", "regimens"].includes(currentPage)) && diff --git a/frontend/constants.ts b/frontend/constants.ts index 893b73245..457dbd37d 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -423,6 +423,9 @@ export namespace Content { trim(`Display time using the 24-hour notation, i.e., 23:00 instead of 11:00pm`); + export const SHOW_PINS = + trim(`Show raw pin lists in Read Sensor and Control Peripheral steps.`); + // Device export const NOT_HTTPS = trim(`WARNING: Sending passwords via HTTP:// is not secure.`); @@ -561,6 +564,9 @@ export namespace Content { trim(`Click one in the Sequences panel to edit, or click "+" to create a new one.`); + export const NO_SEQUENCES = + trim(`Click "+" to create a new sequence.`); + export const END_DETECTION_DISABLED = trim(`This command will not execute correctly because you do not have encoders or endstops enabled for the chosen axis. Enable endstops or @@ -574,6 +580,9 @@ export namespace Content { trim(`Click one in the Regimens panel to edit, or click "+" to create a new one.`); + export const NO_REGIMENS = + trim(`Click "+" to create a new regimen.`); + // Farm Designer export const OUTSIDE_PLANTING_AREA = trim(`Outside of planting area. Plants must be placed within the grid.`); diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index a0574c11b..5a3c22fe1 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -37,7 +37,7 @@ export class Controls extends React.Component { bot={this.props.bot} peripherals={this.props.peripherals} dispatch={this.props.dispatch} - disabled={this.arduinoBusy} /> + disabled={this.arduinoBusy || !this.botOnline} /> webcams = () => + className={`controls-popup ${isOpen} ${mapPanelClassName()}`}>
diff --git a/frontend/css/_mobile.scss b/frontend/css/_mobile.scss index d911d9737..9b136adfa 100644 --- a/frontend/css/_mobile.scss +++ b/frontend/css/_mobile.scss @@ -1,4 +1,4 @@ -@media screen and (max-width: 974px) { +@media screen and (max-width: 1075px) { .all-content-wrapper { padding: 11rem 0 0; overflow: hidden; diff --git a/frontend/css/buttons.scss b/frontend/css/buttons.scss index 17dd64ad7..71d5751ba 100644 --- a/frontend/css/buttons.scss +++ b/frontend/css/buttons.scss @@ -184,6 +184,7 @@ margin-bottom: 1.5rem; font-size: 1.2rem; color: $dark_gray; + text-align: left; &.active { box-shadow: none !important; border: 1px solid $white; diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index ffb4e0e7e..06eb01c8c 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -2,6 +2,19 @@ position: relative; height: 100vh; overflow-y: hidden; + .garden-map-legend { + @media screen and (max-width: 450px) { + &.panel-open { + display: none; + } + &.short-panel { + top: 35rem; + } + &.panel-closed { + top: 15rem; + } + } + } } .farm-designer-map { @@ -13,6 +26,9 @@ &.panel-open { padding: 11rem 2rem 2rem 31.8rem; // at zoom = 1.0: 110px 20px 20px 318px } + &.short-panel { + padding: 35rem 2rem 2rem 2rem; // at zoom = 1.0: 350px 20px 20px 20px + } } .drop-area { @@ -42,7 +58,6 @@ .crop-drag-info-image { width: 100% !important; background-color: $translucent; - max-width: 28rem; } .plant-catalog-image { @@ -104,15 +119,15 @@ .text-input-wrapper { position: relative; margin: 1rem; - border-bottom: 1px solid #000; + border-bottom: 1px solid $dark_gray; &:before, &:after { content: ""; position: absolute; bottom: 0; - background: #000; + background: $dark_gray; width: 1px; - height: 10px; + height: 3px; } &:before { left: 0; @@ -120,6 +135,9 @@ &:after { right: 0; } + i { + font-size: 1.5rem; + } .fa-search { position: absolute; top: 0.8rem; diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index 02ef5c09b..324568e6d 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -3,6 +3,9 @@ position: fixed; top: 8.9rem; width: 30rem; + @media screen and (max-width: 450px) { + width: 100%; + } } @keyframes panel-pullout { @@ -17,6 +20,12 @@ .farm-designer-panels { bottom: 0; z-index: 1; + &.panel-closed { + display: none !important; + } + &.short-panel { + height: 24rem; + } .panel-container { width: 100%; height: 100%; @@ -134,33 +143,30 @@ .panel-title { height: 50px; - padding-top: 1.8rem; - padding-left: 1.4rem; - padding-right: 2rem; .back-arrow { - display: inline-block; + float: left; color: $off_white; - margin-right: 1rem; + text-align: center; font-size: 1.8rem; - margin-top: -1.8rem; - vertical-align: middle; + width: 50px; + line-height: 50px; &:hover { color: $white; } } .title { - display: inline-block; - color: $white; + float: left; + color: $off_white; font-size: 1.8rem; - margin-top: 0.4rem; white-space: nowrap; - width: 10em; + width: 50%; overflow: hidden; text-overflow: ellipsis; - height: 2rem; - padding: 0.2rem; + line-height: 50px; } .right-button { + position: absolute; + right: 0; float: right; text-transform: uppercase; font-size: 1rem; @@ -170,6 +176,8 @@ letter-spacing: 1px; border-radius: 4px; color: $off_white; + margin-top: 1.25rem; + margin-right: 1.5rem; &:hover { color: $white; } @@ -250,8 +258,15 @@ &.with-button { display: flex; margin-top: 5rem; - a { + .fb-button { margin: 1rem; + margin-left: 0; + } + a { + margin-top: 0.5rem; + } + i { + font-size: 1.5rem; } } } @@ -268,7 +283,7 @@ input { background: $white; } - .is-saved { + .save-btn { margin: 1rem; } } @@ -284,7 +299,6 @@ .panel-nav { position: fixed; z-index: 2; - width: 300px; } .panel-header { @@ -301,9 +315,6 @@ } .crop-info-panel { - .title { - width: 50%; - } .panel-header { position: inherit; background-size: 144% !important; @@ -339,6 +350,17 @@ } } +.add-plant-panel, +.move-to-panel { + padding-bottom: 0 !important; +} + +.add-plant-panel { + .panel-header { + height: 100%; + } +} + .move-to-panel-content { &.with-nav { margin-top: 6rem; diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 6068558cc..9284866e5 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -706,6 +706,11 @@ ul { .controls-popup { color: $off_white; + @media screen and (max-width: 450px) { + &.panel-open { + display: none; + } + } i { position: fixed; bottom: 3rem; @@ -765,7 +770,7 @@ ul { .empty-state-graphic { display: flex; margin: auto; - margin-top: 10%; + margin-top: 25%; width: 50%; } @@ -867,6 +872,9 @@ ul { margin-right: 1rem; margin-left: 1rem; } + .fb-button { + margin-top: 0; + } } .logs-page { diff --git a/frontend/css/navbar.scss b/frontend/css/navbar.scss index 351ab08d1..440a83beb 100644 --- a/frontend/css/navbar.scss +++ b/frontend/css/navbar.scss @@ -23,6 +23,19 @@ nav { padding: 0 1rem; } +.nav-sync { + min-width: 90px; + &.auto-sync { + background: none !important; + box-shadow: none; + font-style: italic; + text-transform: none; + } + &:hover { + background: none !important; + } +} + .links { display: inline-block; a { @@ -81,6 +94,14 @@ nav { } .nav-right { + height: 5rem; + overflow: hidden; + .connection-status-popover { + display: inline; + .bp3-popover-wrapper { + margin: 1.85rem; + } + } a { font-weight: normal; color: $black; @@ -92,54 +113,42 @@ nav { margin-right: 0.8rem; } } - .connection-status-popover { - display: inline; - .bp3-popover-wrapper { - margin: 1.85rem; +} + +.menu-popover { + display: inline; + .bp3-popover-content { + position: relative; + width: 22rem; + background: $dark_gray; + i { + margin-right: 0.8rem; } - } - .menu-popover { - display: inline; - .bp3-popover-content { - position: relative; - width: 22rem; - a:not(.app-version) { - display: inline-block; - margin-bottom: 0.6rem; - } - .app-version { - margin: 1rem -1rem -1rem; - background: $dark_gray; - color: $white; - padding: 0.5rem 0 0 1rem; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - label { - color: $white; - } - a { - color: $white; - } - p { - display: inline; - color: $gray; - font-size: 1.2rem; - } - } + font-size: 1.2rem; + letter-spacing: 1.2px; + a:not(.app-version) { + display: inline-block; + text-transform: uppercase; + color: $off_white; + margin-bottom: 0.6rem; } - .bp3-overlay-content { - margin-top: 1.6rem; + .app-version { + margin: 1rem -1rem -1rem; + background: $dark_gray; color: $white; - } - .bp3-popover-wrapper { + padding: 0.5rem 0 0 1rem; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + label { + color: $white; + } a { color: $white; } - .bp3-popover-arrow-fill { - fill: $dark_gray; - } - .bp3-popover-content { - background: $dark_gray; + p { + display: inline; + color: $gray; + font-size: 1.2rem; } } } @@ -157,13 +166,13 @@ nav { } } -@media screen and (max-width: 974px) { +@media screen and (max-width: 1075px) { .top-menu-container .nav-links { display: none; } } -@media screen and (min-width: 975px) { +@media screen and (min-width: 1075px) { .mobile-menu-icon { display: none !important; } diff --git a/frontend/css/regimens.scss b/frontend/css/regimens.scss index e46ff389b..70782766a 100644 --- a/frontend/css/regimens.scss +++ b/frontend/css/regimens.scss @@ -31,6 +31,10 @@ } } +.bulk-scheduler-content { + margin-top: 1rem; +} + // Regimen Editor .regimen-day { margin: 1.5rem 0; diff --git a/frontend/css/sequences.scss b/frontend/css/sequences.scss index 09e39931a..f5fea7317 100644 --- a/frontend/css/sequences.scss +++ b/frontend/css/sequences.scss @@ -1,10 +1,10 @@ .farmware-input-panel, .sequence-editor-panel, .regimen-editor-panel { - margin: -3rem -1.5rem -6rem; + margin: -3rem -1.5rem -3rem; height: calc(100vh - 5rem); background: $light_gray; - @media screen and (max-width: 768px) { + @media screen and (max-width: 767px) { display: none; &.open { display: block; @@ -22,7 +22,7 @@ float: left; margin-top: 0.4rem; } - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { h3, p { margin-left: 15px; @@ -33,6 +33,7 @@ } .button-group { margin-right: 15px; + margin-top: -1rem; } .title-help-text { padding-left: 15px; @@ -51,7 +52,7 @@ padding-left: 15px; padding-right: 15px; } - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { .title-help-text { padding-left: 3rem; padding-right: 3rem; @@ -63,7 +64,7 @@ .sequence-editor-content, .regimen-editor-content { margin-right: -15px; - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { margin-left: 15px; margin-right: 0; } @@ -72,7 +73,7 @@ .sequence-editor-tools, .regimen-editor-tools { margin-right: 15px; - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { margin-right: 10px; } .locals-list { @@ -146,7 +147,7 @@ .farmware-info-panel, .step-button-cluster-panel { - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { display: none; &.farmware-info-open, &.inserting-step { @@ -157,12 +158,18 @@ } } +.step-button-cluster { + @media screen and (max-width: 767px) { + width: 40rem; + } +} + .farmware-info-panel button { margin-bottom: 3rem; } .step-button-cluster { - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { margin-left: 0; margin-right: 0; } @@ -198,12 +205,31 @@ padding-top: 0.4rem; margin-bottom: 3rem; margin-right: 5px; - @media screen and (max-width: 974px) { + @media screen and (max-width: 1075px) { + margin-left: 15px; + } + .empty-state { + display: none; + .empty-state-graphic { + margin-top: 25%; + } + } + @media screen and (max-width: 767px) { &.open { display: none; } margin-left: 15px; margin-right: 15px; + .empty-state { + display: block; + } + } + .panel-top { + margin: 0; + .text-input-wrapper { + margin: 0.1rem; + margin-right: 1rem; + } } } @@ -223,24 +249,39 @@ } .farmware-input-panel-contents { - @media screen and (max-width: 974px) { + @media screen and (max-width: 767px) { margin-left: 15px; margin-right: 15px; padding-right: 5rem; } } -.sequence-list-panel input, -.regimen-list-panel input { - margin-bottom: 1rem; -} - -.back-to-farmware, .back-to-regimens, .back-to-sequences { display: none; &.open { - @media screen and (max-width: 768px) { + @media screen and (max-width: 767px) { + display: block; + margin-top: -2rem; + float: left !important; + height: 6rem; + width: 4rem; + font-size: 2rem; + text-align: center; + line-height: 6rem; + margin-left: 15px; + &.inserting-step, + &.inserting-item { + margin-left: 0; + } + } + } +} + +.back-to-farmware { + display: none; + &.open { + @media screen and (max-width: 767px) { display: block; margin: 4rem; margin-top: 0; @@ -249,22 +290,19 @@ i { margin-right: 1rem; } - &.inserting-step { - display: none; - } } } } .drag-drop-area { - @media screen and (max-width: 768px) { + @media screen and (max-width: 767px) { display: none; } } .add-command-button-container { display: none; - @media screen and (max-width: 768px) { + @media screen and (max-width: 767px) { display: block; min-height: 3rem; .add-command { @@ -278,7 +316,7 @@ .farmware-info-button { display: none; - @media screen and (max-width: 768px) { + @media screen and (max-width: 767px) { &.open { display: block; margin: 4rem; diff --git a/frontend/css/steps.scss b/frontend/css/steps.scss index 707de5a11..270631d89 100644 --- a/frontend/css/steps.scss +++ b/frontend/css/steps.scss @@ -58,6 +58,7 @@ box-shadow: none; color: $dark_gray; font-weight: bold; + min-width: 30%; } p { font-size: 1rem; diff --git a/frontend/devices/components/__tests__/e_stop_button_test.tsx b/frontend/devices/components/__tests__/e_stop_button_test.tsx index 8e6351734..b5f765dc6 100644 --- a/frontend/devices/components/__tests__/e_stop_button_test.tsx +++ b/frontend/devices/components/__tests__/e_stop_button_test.tsx @@ -17,7 +17,7 @@ describe("", () => { bot.hardware.informational_settings.sync_status = undefined; const wrapper = mount(); expect(wrapper.text()).toEqual("E-STOP"); - expect(wrapper.find("button").hasClass("gray")).toBeTruthy(); + expect(wrapper.find("button").hasClass("pseudo-disabled")).toBeTruthy(); }); it("locked", () => { diff --git a/frontend/devices/components/e_stop_btn.tsx b/frontend/devices/components/e_stop_btn.tsx index ca0ed287a..59aa66cc8 100644 --- a/frontend/devices/components/e_stop_btn.tsx +++ b/frontend/devices/components/e_stop_btn.tsx @@ -1,17 +1,18 @@ import * as React from "react"; - import { emergencyLock, emergencyUnlock } from "../actions"; import { EStopButtonProps } from "../interfaces"; import { isBotUp } from "../must_be_online"; import { t } from "../../i18next_wrapper"; +const GRAY = "pseudo-disabled"; + export class EStopButton extends React.Component { render() { const i = this.props.bot.hardware.informational_settings; const isLocked = !!i.locked; const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock; const color = isLocked ? "yellow" : "red"; - const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : "gray"; + const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : GRAY; const emergencyLockStatusText = isLocked ? t("UNLOCK") : "E-STOP"; return ; diff --git a/frontend/farm_designer/plants/designer_panel.tsx b/frontend/farm_designer/plants/designer_panel.tsx index 6c8c98f45..ef67859ae 100644 --- a/frontend/farm_designer/plants/designer_panel.tsx +++ b/frontend/farm_designer/plants/designer_panel.tsx @@ -1,5 +1,4 @@ import * as React from "react"; - import { history as routeHistory } from "../../history"; import { last, trim } from "lodash"; import { Link } from "../../link"; @@ -81,7 +80,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => { {props.linkTo &&
- +
}
; diff --git a/frontend/farm_designer/plants/plant_panel.tsx b/frontend/farm_designer/plants/plant_panel.tsx index 507bc4d68..6a902c4bb 100644 --- a/frontend/farm_designer/plants/plant_panel.tsx +++ b/frontend/farm_designer/plants/plant_panel.tsx @@ -27,6 +27,7 @@ export interface PlantPanelProps { export const PLANT_STAGES: DropDownItem[] = [ { value: "planned", label: t("Planned") }, { value: "planted", label: t("Planted") }, + { value: "sprouted", label: t("Sprouted") }, { value: "harvested", label: t("Harvested") }, ]; @@ -43,6 +44,10 @@ export const PLANT_STAGES_DDI = { label: PLANT_STAGES[2].label, value: PLANT_STAGES[2].value }, + [PLANT_STAGES[3].value]: { + label: PLANT_STAGES[3].label, + value: PLANT_STAGES[3].value + }, }; interface EditPlantProperty { diff --git a/frontend/nav/__tests__/nav_index_test.tsx b/frontend/nav/__tests__/nav_index_test.tsx index c59ddcd09..8603662dd 100644 --- a/frontend/nav/__tests__/nav_index_test.tsx +++ b/frontend/nav/__tests__/nav_index_test.tsx @@ -23,6 +23,7 @@ describe("NavBar", () => { getConfigValue: jest.fn(), tour: undefined, device: fakeDevice(), + autoSync: false, }); it("has correct parent classname", () => { diff --git a/frontend/nav/__tests__/sync_button_test.tsx b/frontend/nav/__tests__/sync_button_test.tsx index 08c9d332d..e45c80b83 100644 --- a/frontend/nav/__tests__/sync_button_test.tsx +++ b/frontend/nav/__tests__/sync_button_test.tsx @@ -3,22 +3,22 @@ import { SyncButton } from "../sync_button"; import { bot } from "../../__test_support__/fake_state/bot"; import { shallow } from "enzyme"; import { SyncButtonProps } from "../interfaces"; +import { SyncStatus } from "farmbot"; describe("", function () { - const fakeProps = (): SyncButtonProps => { - return { - dispatch: jest.fn(), - bot: bot, - consistent: true, - }; - }; + const fakeProps = (): SyncButtonProps => ({ + dispatch: jest.fn(), + bot: bot, + consistent: true, + autoSync: false, + }); it("is gray when inconsistent", () => { const p = fakeProps(); p.consistent = false; p.bot.hardware.informational_settings.sync_status = "sync_now"; const result = shallow(); - expect(result.hasClass("gray")).toBeTruthy(); + expect(result.hasClass("pseudo-disabled")).toBeTruthy(); }); it("is gray when disconnected", () => { @@ -26,16 +26,16 @@ describe("", function () { p.consistent = false; p.bot.hardware.informational_settings.sync_status = "unknown"; const result = shallow(); - expect(result.hasClass("gray")).toBeTruthy(); + expect(result.hasClass("pseudo-disabled")).toBeTruthy(); }); - it("defaults to `unknown` and `gray` when uncertain", () => { + it("defaults to `unknown` and gray when uncertain", () => { const p = fakeProps(); // tslint:disable-next-line:no-any p.bot.hardware.informational_settings.sync_status = "new" as any; const result = shallow(); expect(result.text()).toContain("new"); - expect(result.hasClass("gray")).toBeTruthy(); + expect(result.hasClass("pseudo-disabled")).toBeTruthy(); }); it("syncs when clicked", () => { @@ -58,4 +58,23 @@ describe("", function () { const result = shallow(); expect(result.find(".btn-spinner").length).toEqual(1); }); + + const testCase = (input: SyncStatus, expected: string) => { + const p = fakeProps(); + p.bot.hardware.informational_settings.sync_status = input; + p.autoSync = true; + const result = shallow(); + expect(result.find(".auto-sync").length).toEqual(1); + expect(result.text()).toContain(expected); + }; + + it("renders differently with auto-sync enabled", () => { + testCase("syncing", "Syncing..."); + testCase("sync_now", "Syncing..."); + testCase("synced", "Synced"); + testCase("booting", "Sync unknown"); + testCase("unknown", "Sync unknown"); + testCase("maintenance", "Sync unknown"); + testCase("sync_error", "Sync error"); + }); }); diff --git a/frontend/nav/index.tsx b/frontend/nav/index.tsx index 60055f582..930e447ed 100644 --- a/frontend/nav/index.tsx +++ b/frontend/nav/index.tsx @@ -44,6 +44,7 @@ export class NavBar extends React.Component> { return ; } @@ -98,10 +99,11 @@ export class NavBar extends React.Component> {
+ onClose={this.close("accountMenuOpen")}>
{firstName} diff --git a/frontend/nav/interfaces.ts b/frontend/nav/interfaces.ts index 279c20bfe..bc38434c0 100644 --- a/frontend/nav/interfaces.ts +++ b/frontend/nav/interfaces.ts @@ -8,6 +8,7 @@ export interface SyncButtonProps { bot: BotState; consistent: boolean; onClick?: () => void; + autoSync: boolean; } export interface NavBarProps { @@ -20,6 +21,7 @@ export interface NavBarProps { getConfigValue: GetWebAppConfigValue; tour: string | undefined; device: TaggedDevice; + autoSync: boolean; } export interface NavBarState { diff --git a/frontend/nav/sync_button.tsx b/frontend/nav/sync_button.tsx index f0901fc5a..1027cc557 100644 --- a/frontend/nav/sync_button.tsx +++ b/frontend/nav/sync_button.tsx @@ -1,45 +1,50 @@ import * as React from "react"; - import { SyncStatus } from "farmbot/dist"; import { SyncButtonProps } from "./interfaces"; import { sync } from "../devices/actions"; import { t } from "../i18next_wrapper"; +const GRAY = "pseudo-disabled"; + const COLOR_MAPPING: Record = { "synced": "green", "sync_now": "yellow", "syncing": "yellow", "sync_error": "red", - "booting": "gray", - "maintenance": "gray", - "unknown": "gray" + "booting": GRAY, + "maintenance": GRAY, + "unknown": GRAY }; -const TEXT_MAPPING: () => Record = () => ({ - "synced": t("SYNCED"), - "sync_now": t("SYNC NOW"), - "syncing": t("SYNCING"), - "sync_error": t("SYNC ERROR"), - "booting": t("UNKNOWN"), - "unknown": t("UNKNOWN"), - "maintenance": t("UNKNOWN") +const TEXT_MAPPING = (autoSync: boolean): Record => ({ + "synced": autoSync ? t("Synced") : t("SYNCED"), + "sync_now": autoSync ? t("Syncing...") : t("SYNC NOW"), + "syncing": autoSync ? t("Syncing...") : t("SYNCING"), + "sync_error": autoSync ? t("Sync error") : t("SYNC ERROR"), + "booting": autoSync ? t("Sync unknown") : t("UNKNOWN"), + "unknown": autoSync ? t("Sync unknown") : t("UNKNOWN"), + "maintenance": autoSync ? t("Sync unknown") : t("UNKNOWN") }); /** Animation during syncing action */ const spinner = ; -export function SyncButton({ bot, dispatch, consistent }: SyncButtonProps) { +export function SyncButton(props: SyncButtonProps) { + const { bot, dispatch, consistent, autoSync } = props; const { sync_status } = bot.hardware.informational_settings; const syncStatus = sync_status || "unknown"; - const normalColor = COLOR_MAPPING[syncStatus] || "gray"; + const normalColor = COLOR_MAPPING[syncStatus] || GRAY; const color = (!consistent && (syncStatus === "sync_now")) - ? "gray" + ? GRAY : normalColor; - const text = TEXT_MAPPING()[syncStatus] || syncStatus.replace("_", " "); + const text = TEXT_MAPPING(autoSync)[syncStatus] || syncStatus.replace("_", " "); const spinnerEl = (syncStatus === "syncing") ? spinner : ""; + const className = autoSync + ? "nav-sync fb-button auto-sync" + : `nav-sync ${color} fb-button`; return ; diff --git a/frontend/regimens/__tests__/index_test.tsx b/frontend/regimens/__tests__/index_test.tsx index 059530538..c3030d83a 100644 --- a/frontend/regimens/__tests__/index_test.tsx +++ b/frontend/regimens/__tests__/index_test.tsx @@ -1,23 +1,18 @@ -jest.mock("react-redux", () => ({ - connect: jest.fn() -})); +jest.mock("react-redux", () => ({ connect: jest.fn() })); jest.mock("../../history", () => ({ push: () => jest.fn(), - history: { - getCurrentLocation: () => ({ pathname: "" }) - } + history: { getCurrentLocation: () => ({ pathname: "" }) } })); import * as React from "react"; import { mount } from "enzyme"; -import { Regimens } from "../index"; +import { Regimens, RegimenBackButtonProps, RegimenBackButton } from "../index"; import { Props } from "../interfaces"; import { bot } from "../../__test_support__/fake_state/bot"; import { auth } from "../../__test_support__/fake_state/token"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { fakeRegimen } from "../../__test_support__/fake_state/resources"; -import { clickButton } from "../../__test_support__/helpers"; import { Actions } from "../../constants"; describe("", () => { @@ -43,7 +38,7 @@ describe("", () => { it("renders", () => { const wrapper = mount(); - ["Regimens", "Regimen Editor", "Scheduler"].map(string => + ["Regimens", "Edit Regimen", "Scheduler"].map(string => expect(wrapper.text()).toContain(string)); }); @@ -60,12 +55,19 @@ describe("", () => { const wrapper = mount(); expect(wrapper.html()).toContain("inserting-item"); }); +}); + +describe("", () => { + const fakeProps = (): RegimenBackButtonProps => ({ + dispatch: jest.fn(), + className: "", + }); it("returns to regimen", () => { const p = fakeProps(); - p.schedulerOpen = true; - const wrapper = mount(); - clickButton(wrapper, 0, "back to regimen"); + p.className = "inserting-item"; + const wrapper = mount(); + wrapper.find("i").simulate("click"); expect(p.dispatch).toHaveBeenCalledWith({ type: Actions.SET_SCHEDULER_STATE, payload: false }); @@ -73,9 +75,9 @@ describe("", () => { it("returns to regimen list", () => { const p = fakeProps(); - p.schedulerOpen = false; - const wrapper = mount(); - clickButton(wrapper, 0, "back to regimens"); + p.className = ""; + const wrapper = mount(); + wrapper.find("i").simulate("click"); expect(p.dispatch).toHaveBeenCalledWith({ type: Actions.SELECT_REGIMEN, payload: undefined }); diff --git a/frontend/regimens/bulk_scheduler/actions.ts b/frontend/regimens/bulk_scheduler/actions.ts index 62c2af85b..40d086e64 100644 --- a/frontend/regimens/bulk_scheduler/actions.ts +++ b/frontend/regimens/bulk_scheduler/actions.ts @@ -1,6 +1,5 @@ import { isNaN, isNumber } from "lodash"; - -import { error, warning } from "farmbot-toastr"; +import { error, warning, success } from "farmbot-toastr"; import { ReduxAction, Thunk } from "../../redux/interfaces"; import { ToggleDayParams } from "./interfaces"; import { findSequence, findRegimen } from "../../resources/selectors"; @@ -93,6 +92,7 @@ export function commitBulkEditor(): Thunk { clonedRegimen.body = mergeDeclarations(varData, regimen.body.body); console.log(JSON.stringify(clonedRegimen.body, undefined, 2)); dispatch(overwrite(regimen, clonedRegimen)); + success(t("Item(s) added.")); } else { return error(t("No day(s) selected.")); } diff --git a/frontend/regimens/bulk_scheduler/index.tsx b/frontend/regimens/bulk_scheduler/index.tsx index 2911e64cd..1eb373387 100644 --- a/frontend/regimens/bulk_scheduler/index.tsx +++ b/frontend/regimens/bulk_scheduler/index.tsx @@ -7,7 +7,6 @@ import { BlurableInput, Row, Col, FBSelect, DropDownItem, NULL_CHOICE } from "../../ui/index"; import moment from "moment"; - import { isString } from "lodash"; import { betterCompact, bail } from "../../util"; import { msToTime, timeToMs } from "./utils"; @@ -69,7 +68,7 @@ export class BulkScheduler extends React.Component { render() { const { dispatch, weeks, sequences } = this.props; const active = !!(sequences && sequences.length); - return
+ return
dispatch(commitBulkEditor())} /> diff --git a/frontend/regimens/index.tsx b/frontend/regimens/index.tsx index d727baae0..b8a3b6a7f 100644 --- a/frontend/regimens/index.tsx +++ b/frontend/regimens/index.tsx @@ -12,18 +12,19 @@ import { t } from "../i18next_wrapper"; import { ToolTips, Actions } from "../constants"; import { unselectRegimen } from "./actions"; -const RegimenBackButton = (props: { dispatch: Function, className: string }) => { +export interface RegimenBackButtonProps { + dispatch: Function; + className: string; +} + +export const RegimenBackButton = (props: RegimenBackButtonProps) => { const schedulerOpen = props.className.includes("inserting-item"); - return - - ; + return schedulerOpen + ? props.dispatch({ type: Actions.SET_SCHEDULER_STATE, payload: false }) + : props.dispatch(unselectRegimen())} + title={schedulerOpen ? t("back to regimen") : t("back to regimens")} />; }; @connect(mapStateToProps) @@ -39,7 +40,6 @@ export class Regimens extends React.Component { const insertingItem = this.props.schedulerOpen ? "inserting-item" : ""; const activeClasses = [regimenOpen, insertingItem].join(" "); return - { } + title={regimenOpen ? t("Edit Regimen") : t("Regimen Editor")} helpText={t(ToolTips.REGIMEN_EDITOR)} width={5}> { } + title={insertingItem ? t("Add Regimen Item") : t("Scheduler")} helpText={t(ToolTips.BULK_SCHEDULER)} show={!!regimenSelected} width={4}> ", () => { function fakeProps(): RegimensListProps { @@ -24,9 +25,8 @@ describe("", () => { }); it("sets search term", () => { - const wrapper = shallow(); - wrapper.find("input").simulate("change", - { currentTarget: { value: "term" } }); - expect(wrapper.instance().state.searchTerm).toEqual("term"); + const wrapper = mount(); + wrapper.instance().onChange(inputEvent("term")); + expect(wrapper.state().searchTerm).toEqual("term"); }); }); diff --git a/frontend/regimens/list/index.tsx b/frontend/regimens/list/index.tsx index 6c0bc98ae..dee711a7f 100644 --- a/frontend/regimens/list/index.tsx +++ b/frontend/regimens/list/index.tsx @@ -1,11 +1,31 @@ import * as React from "react"; - import { RegimenListItem } from "./regimen_list_item"; import { AddRegimen } from "./add_button"; import { Row, Col } from "../../ui/index"; import { RegimensListProps, RegimensListState } from "../interfaces"; import { sortResourcesById } from "../../util"; import { t } from "../../i18next_wrapper"; +import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper"; +import { Content } from "../../constants"; + +interface RegimenListHeaderProps { + onChange(e: React.SyntheticEvent): void; + regimenCount: number; + dispatch: Function; +} + +const RegimenListHeader = (props: RegimenListHeaderProps) => +
+
+
+ + +
+
+ +
; export class RegimensList extends React.Component { @@ -40,17 +60,22 @@ export class RegimensList extends } render() { - const { dispatch, regimens } = this.props; - return
- - + -
- {this.rows()} -
+ 0} + graphic={EmptyStateGraphic.regimens} + title={t("No Regimens.")} + text={Content.NO_REGIMENS}> + {this.props.regimens.length > 0 && +
+ {this.rows()} +
} +
; } diff --git a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx index 372b43b81..ca7374119 100644 --- a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx +++ b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx @@ -73,6 +73,7 @@ describe("", () => { shouldDisplay: jest.fn(), confirmStepDeletion: false, menuOpen: false, + showPins: true, }; }; @@ -253,9 +254,13 @@ describe("", () => { it("renders settings", () => { const wrapper = mount(); - wrapper.find("button").simulate("click"); + confirmStepDeletion={false} + showPins={false} />); + wrapper.find("button").first().simulate("click"); expect(setWebAppConfigValue).toHaveBeenCalledWith( BooleanSetting.confirm_step_deletion, true); + wrapper.find("button").last().simulate("click"); + expect(setWebAppConfigValue).toHaveBeenCalledWith( + "show_pins", true); }); }); diff --git a/frontend/sequences/__tests__/sequence_editor_middle_test.tsx b/frontend/sequences/__tests__/sequence_editor_middle_test.tsx index 2794315bc..21b2ae7cf 100644 --- a/frontend/sequences/__tests__/sequence_editor_middle_test.tsx +++ b/frontend/sequences/__tests__/sequence_editor_middle_test.tsx @@ -27,6 +27,7 @@ describe("", () => { shouldDisplay: jest.fn(), confirmStepDeletion: false, menuOpen: false, + showPins: true, }; } diff --git a/frontend/sequences/__tests__/sequences_list_test.tsx b/frontend/sequences/__tests__/sequences_list_test.tsx index eedbca39b..fb93c6d09 100644 --- a/frontend/sequences/__tests__/sequences_list_test.tsx +++ b/frontend/sequences/__tests__/sequences_list_test.tsx @@ -24,6 +24,7 @@ import { buildResourceIndex } from "../../__test_support__/resource_index_builde import { resourceReducer } from "../../resources/reducer"; import { resourceReady } from "../../sync/actions"; import { setActiveSequenceByName } from "../set_active_sequence_by_name"; +import { inputEvent } from "../../__test_support__/fake_input_event"; describe("", () => { const fakeSequences = () => { @@ -105,13 +106,8 @@ describe("", () => { it("sets search term", () => { const wrapper = shallow(); - expect(wrapper.instance().state.searchTerm).toEqual(""); - const searchField = wrapper.find("input").first(); - expect(searchField.props().placeholder) - .toEqual("Search Sequences..."); - searchField.simulate("change", { - currentTarget: { value: "search this" } - }); + expect(wrapper.state().searchTerm).toEqual(""); + wrapper.instance().onChange(inputEvent("search this")); expect(wrapper.instance().state.searchTerm).toEqual("search this"); }); diff --git a/frontend/sequences/__tests__/sequences_test.tsx b/frontend/sequences/__tests__/sequences_test.tsx index c93339e85..bfb6ad8ec 100644 --- a/frontend/sequences/__tests__/sequences_test.tsx +++ b/frontend/sequences/__tests__/sequences_test.tsx @@ -1,6 +1,4 @@ -jest.mock("react-redux", () => ({ - connect: jest.fn() -})); +jest.mock("react-redux", () => ({ connect: jest.fn() })); jest.mock("../../history", () => ({ push: jest.fn(), @@ -8,7 +6,7 @@ jest.mock("../../history", () => ({ })); import * as React from "react"; -import { Sequences } from "../sequences"; +import { Sequences, SequenceBackButtonProps, SequenceBackButton } from "../sequences"; import { shallow, mount } from "enzyme"; import { Props } from "../interfaces"; import { @@ -39,12 +37,13 @@ describe("", () => { confirmStepDeletion: false, menuOpen: false, stepIndex: undefined, + showPins: true, }); it("renders", () => { const wrapper = shallow(); expect(wrapper.html()).toContain("Sequences"); - expect(wrapper.html()).toContain("Sequence Editor"); + expect(wrapper.html()).toContain("Edit Sequence"); expect(wrapper.html()).toContain(ToolTips.SEQUENCE_EDITOR); expect(wrapper.html()).toContain("Commands"); }); @@ -62,11 +61,18 @@ describe("", () => { const wrapper = shallow(); expect(wrapper.html()).toContain("inserting-step"); }); +}); + +describe("", () => { + const fakeProps = (): SequenceBackButtonProps => ({ + dispatch: jest.fn(), + className: "", + }); it("goes back", () => { const p = fakeProps(); - const wrapper = mount(); - wrapper.find("button").first().simulate("click"); + const wrapper = mount(); + wrapper.find("i").first().simulate("click"); expect(p.dispatch).toHaveBeenCalledWith({ type: Actions.SELECT_SEQUENCE, payload: undefined }); diff --git a/frontend/sequences/actions.ts b/frontend/sequences/actions.ts index 6a5f40241..9dda6c0af 100644 --- a/frontend/sequences/actions.ts +++ b/frontend/sequences/actions.ts @@ -47,3 +47,8 @@ export const unselectSequence = () => { push("/app/sequences"); return { type: Actions.SELECT_SEQUENCE, payload: undefined }; }; + +export const closeCommandMenu = () => ({ + type: Actions.SET_SEQUENCE_STEP_POSITION, + payload: undefined, +}); diff --git a/frontend/sequences/all_steps.tsx b/frontend/sequences/all_steps.tsx index a62512d7d..fef5a5f6f 100644 --- a/frontend/sequences/all_steps.tsx +++ b/frontend/sequences/all_steps.tsx @@ -19,6 +19,7 @@ interface AllStepsProps { farmwareInfo?: FarmwareInfo; shouldDisplay?: ShouldDisplay; confirmStepDeletion: boolean; + showPins?: boolean; } export class AllSteps extends React.Component { @@ -54,6 +55,7 @@ export class AllSteps extends React.Component { farmwareInfo, shouldDisplay, confirmStepDeletion: this.props.confirmStepDeletion, + showPins: this.props.showPins, })}
diff --git a/frontend/sequences/interfaces.ts b/frontend/sequences/interfaces.ts index 4152ed702..091c6c12a 100644 --- a/frontend/sequences/interfaces.ts +++ b/frontend/sequences/interfaces.ts @@ -47,6 +47,7 @@ export interface Props { confirmStepDeletion: boolean; menuOpen: boolean; stepIndex: number | undefined; + showPins: boolean; } export interface SequenceEditorMiddleProps { @@ -59,6 +60,7 @@ export interface SequenceEditorMiddleProps { shouldDisplay: ShouldDisplay; confirmStepDeletion: boolean; menuOpen: boolean; + showPins: boolean; } export interface ActiveMiddleProps extends SequenceEditorMiddleProps { @@ -75,6 +77,7 @@ export interface SequenceHeaderProps { variablesCollapsed: boolean; toggleVarShow: () => void; confirmStepDeletion: boolean; + showPins: boolean; } export type ChannelName = ALLOWED_CHANNEL_NAMES; @@ -202,4 +205,5 @@ export interface StepParams { farmwareInfo?: FarmwareInfo; shouldDisplay?: ShouldDisplay; confirmStepDeletion: boolean; + showPins?: boolean; } diff --git a/frontend/sequences/locals_list/location_form_list.ts b/frontend/sequences/locals_list/location_form_list.ts index 71056ab2c..50178113a 100644 --- a/frontend/sequences/locals_list/location_form_list.ts +++ b/frontend/sequences/locals_list/location_form_list.ts @@ -73,15 +73,15 @@ export function locationFormList(resources: ResourceIndex, return [COORDINATE_DDI()] .concat(additionalItems) .concat(heading("Tool")) - .concat(toolDDI) .concat(group(everyPointDDI("Tool"))) .concat(group(everyPointDDI("ToolSlot"))) + .concat(toolDDI) .concat(heading("Plant")) - .concat(plantDDI) .concat(group(everyPointDDI("Plant"))) + .concat(plantDDI) .concat(heading("GenericPointer")) - .concat(genericPointerDDI) - .concat(group(everyPointDDI("GenericPointer"))); + .concat(group(everyPointDDI("GenericPointer"))) + .concat(genericPointerDDI); } /** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */ diff --git a/frontend/sequences/sequence_editor_middle.tsx b/frontend/sequences/sequence_editor_middle.tsx index 0926999b2..14b025535 100644 --- a/frontend/sequences/sequence_editor_middle.tsx +++ b/frontend/sequences/sequence_editor_middle.tsx @@ -26,6 +26,7 @@ export class SequenceEditorMiddle farmwareInfo={this.props.farmwareInfo} shouldDisplay={this.props.shouldDisplay} confirmStepDeletion={this.props.confirmStepDeletion} + showPins={this.props.showPins} menuOpen={this.props.menuOpen} />} ; } diff --git a/frontend/sequences/sequence_editor_middle_active.tsx b/frontend/sequences/sequence_editor_middle_active.tsx index 9a01ea1d3..6a4135496 100644 --- a/frontend/sequences/sequence_editor_middle_active.tsx +++ b/frontend/sequences/sequence_editor_middle_active.tsx @@ -24,6 +24,7 @@ import { ToggleButton } from "../controls/toggle_button"; import { Content } from "../constants"; import { setWebAppConfigValue } from "../config_storage/actions"; import { BooleanSetting } from "../session_keys"; +import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; export const onDrop = (dispatch1: Function, sequence: TaggedSequence) => @@ -49,19 +50,32 @@ export const onDrop = export interface SequenceSettingsMenuProps { dispatch: Function; confirmStepDeletion: boolean; + showPins: boolean; } export const SequenceSettingsMenu = - ({ dispatch, confirmStepDeletion }: SequenceSettingsMenuProps) => + ({ dispatch, confirmStepDeletion, showPins }: SequenceSettingsMenuProps) =>
- - - dispatch(setWebAppConfigValue( - BooleanSetting.confirm_step_deletion, !confirmStepDeletion))} /> +
+ + + dispatch(setWebAppConfigValue( + BooleanSetting.confirm_step_deletion, !confirmStepDeletion))} /> +
+
+ + + dispatch(setWebAppConfigValue( + "show_pins" as BooleanConfigKey, !showPins))} /> +
; interface SequenceBtnGroupProps { @@ -72,11 +86,12 @@ interface SequenceBtnGroupProps { shouldDisplay: ShouldDisplay; menuOpen: boolean; confirmStepDeletion: boolean; + showPins: boolean; } const SequenceBtnGroup = ({ dispatch, sequence, syncStatus, resources, shouldDisplay, menuOpen, - confirmStepDeletion + confirmStepDeletion, showPins }: SequenceBtnGroupProps) =>
@@ -140,6 +156,7 @@ const SequenceHeader = (props: SequenceHeaderProps) => { resources={props.resources} shouldDisplay={props.shouldDisplay} confirmStepDeletion={props.confirmStepDeletion} + showPins={props.showPins} menuOpen={props.menuOpen} /> this.setState({ variablesCollapsed: !this.state.variablesCollapsed })} confirmStepDeletion={this.props.confirmStepDeletion} + showPins={this.props.showPins} menuOpen={this.props.menuOpen} />
- - - ; +export interface SequenceBackButtonProps { + dispatch: Function; + className: string; +} + +export const SequenceBackButton = (props: SequenceBackButtonProps) => { + const insertingStep = props.className.includes("inserting-step"); + return props.dispatch( + insertingStep ? closeCommandMenu() : unselectSequence())} + title={insertingStep ? t("back to sequence") : t("back to sequences")} />; +}; @connect(mapStateToProps) export class Sequences extends React.Component { @@ -38,7 +42,6 @@ export class Sequences extends React.Component { const insertingStep = isNumber(this.props.stepIndex) ? "inserting-step" : ""; const activeClasses = [sequenceOpen, insertingStep].join(" "); return - { } + title={sequenceOpen ? t("Edit Sequence") : t("Sequence Editor")} helpText={t(ToolTips.SEQUENCE_EDITOR)}> { farmwareInfo={this.props.farmwareInfo} shouldDisplay={this.props.shouldDisplay} confirmStepDeletion={this.props.confirmStepDeletion} + showPins={this.props.showPins} menuOpen={this.props.menuOpen} /> } + title={insertingStep ? t("Add Command") : t("Commands")} helpText={t(ToolTips.SEQUENCE_COMMANDS)} show={sequenceSelected}> (seq: TaggedSequence): boolean => seq .body @@ -58,6 +58,45 @@ const sequenceList = (props: {
; }; +const emptySequenceBody = (seqCount: number): TaggedSequence["body"] => ({ + name: t("new sequence {{ num }}", { num: seqCount }), + args: { + version: -999, + locals: { kind: "scope_declaration", args: {} }, + }, + color: "gray", + kind: "sequence", + body: [] +}); + +interface SequenceListHeaderProps { + onChange(e: React.SyntheticEvent): void; + sequenceCount: number; + dispatch: Function; +} + +const SequenceListHeader = (props: SequenceListHeaderProps) => +
+
+
+ + +
+
+ +
; + export class SequencesList extends React.Component { @@ -68,41 +107,28 @@ export class SequencesList extends onChange = (e: React.SyntheticEvent) => this.setState({ searchTerm: e.currentTarget.value }); - emptySequenceBody = (): TaggedSequence["body"] => ({ - name: t("new sequence {{ num }}", { num: this.props.sequences.length }), - args: { - version: -999, - locals: { kind: "scope_declaration", args: {} }, - }, - color: "gray", - kind: "sequence", - body: [] - }); - render() { const { sequences, dispatch, resourceUsage, sequenceMetas } = this.props; const searchTerm = this.state.searchTerm.toLowerCase(); return
- - + -
- {sortResourcesById(sequences) - .filter(filterFn(searchTerm)) - .map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))} -
+ 0} + graphic={EmptyStateGraphic.sequences} + title={t("No Sequences.")} + text={Content.NO_SEQUENCES}> + {sequences.length > 0 && +
+ {sortResourcesById(sequences) + .filter(filterFn(searchTerm)) + .map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))} +
} +
; diff --git a/frontend/sequences/state_to_props.ts b/frontend/sequences/state_to_props.ts index 860bee2c1..8aa0acc0b 100644 --- a/frontend/sequences/state_to_props.ts +++ b/frontend/sequences/state_to_props.ts @@ -11,10 +11,11 @@ import { } from "../util"; import { BooleanSetting } from "../session_keys"; import { getWebAppConfigValue } from "../config_storage/actions"; -import { getFirmwareConfig, getWebAppConfig } from "../resources/getters"; +import { getFirmwareConfig } from "../resources/getters"; import { Farmwares } from "../farmware/interfaces"; import { manifestInfo } from "../farmware/generate_manifest_info"; import { DevSettings } from "../account/dev/dev_support"; +import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; export function mapStateToProps(props: Everything): Props { const uuid = props.resources.consumers.sequences.current; @@ -61,16 +62,17 @@ export function mapStateToProps(props: Everything): Props { }); const farmwareNames = Object.values(farmwares).map(fw => fw.name); const { firstPartyFarmwareNames } = props.resources.consumers.farmware; - const conf = getWebAppConfig(props.resources.index); - const showFirstPartyFarmware = !!(conf && conf.body.show_first_party_farmware); + const getConfig = getWebAppConfigValue(() => props); + const showFirstPartyFarmware = + !!getConfig(BooleanSetting.show_first_party_farmware); const farmwareConfigs: FarmwareConfigs = {}; Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config); const installedOsVersion = determineInstalledOsVersion( props.bot, maybeGetDevice(props.resources.index)); - const confirmStepDeletion = - !!getWebAppConfigValue(() => props)(BooleanSetting.confirm_step_deletion); + const confirmStepDeletion = !!getConfig(BooleanSetting.confirm_step_deletion); + const showPins = !!getConfig("show_pins" as BooleanConfigKey); const fbosVersionOverride = DevSettings.overriddenFbosVersion(); const shouldDisplay = shouldDisplayFunc( @@ -97,5 +99,6 @@ export function mapStateToProps(props: Everything): Props { confirmStepDeletion, menuOpen: props.resources.consumers.sequences.menuOpen, stepIndex: props.resources.consumers.sequences.stepIndex, + showPins, }; } diff --git a/frontend/sequences/step_buttons/index.tsx b/frontend/sequences/step_buttons/index.tsx index bcaff617c..4d55e1461 100644 --- a/frontend/sequences/step_buttons/index.tsx +++ b/frontend/sequences/step_buttons/index.tsx @@ -2,11 +2,10 @@ import * as React from "react"; import { SequenceBodyItem as Step, TaggedSequence } from "farmbot"; import { error } from "farmbot-toastr"; import { StepDragger, NULL_DRAGGER_ID } from "../../draggable/step_dragger"; -import { pushStep } from "../actions"; +import { pushStep, closeCommandMenu } from "../actions"; import { StepButtonParams } from "../interfaces"; import { Col } from "../../ui/index"; import { t } from "../../i18next_wrapper"; -import { Actions } from "../../constants"; export const stepClick = (dispatch: Function, @@ -17,10 +16,7 @@ export const stepClick = seq ? pushStep(step, dispatch, seq, index) : error(t("Select a sequence first")); - dispatch({ - type: Actions.SET_SEQUENCE_STEP_POSITION, - payload: undefined, - }); + dispatch(closeCommandMenu()); }; export function StepButton({ children, step, color, dispatch, current, index }: diff --git a/frontend/sequences/step_tiles/__tests__/pin_and_peripheral_support_test.tsx b/frontend/sequences/step_tiles/__tests__/pin_and_peripheral_support_test.tsx index e500faf30..fcf50bed2 100644 --- a/frontend/sequences/step_tiles/__tests__/pin_and_peripheral_support_test.tsx +++ b/frontend/sequences/step_tiles/__tests__/pin_and_peripheral_support_test.tsx @@ -117,7 +117,7 @@ describe("Pin and Peripheral support files", () => { s.body.label = "not displayed"; p.body.label = "not displayed"; const ri = buildResourceIndex([s, p]); - const result = pinsAsDropDownsWritePin(ri.index, () => false); + const result = pinsAsDropDownsWritePin(ri.index, () => false, true); expect(JSON.stringify(result)).not.toContain("not displayed"); }); @@ -127,7 +127,7 @@ describe("Pin and Peripheral support files", () => { s.body.label = "not displayed"; p.body.label = "displayed peripheral"; const ri = buildResourceIndex([s, p]); - const result = pinsAsDropDownsWritePin(ri.index, () => true); + const result = pinsAsDropDownsWritePin(ri.index, () => true, true); expect(JSON.stringify(result)).toContain("displayed peripheral"); expect(JSON.stringify(result)).not.toContain("not displayed"); }); @@ -140,7 +140,7 @@ describe("Pin and Peripheral support files", () => { s.body.label = "not displayed"; p.body.label = "not displayed"; const ri = buildResourceIndex([s, p]); - const result = pinsAsDropDownsReadPin(ri.index, () => false); + const result = pinsAsDropDownsReadPin(ri.index, () => false, true); expect(JSON.stringify(result)).not.toContain("not displayed"); }); @@ -150,7 +150,7 @@ describe("Pin and Peripheral support files", () => { s.body.label = "displayed sensor"; p.body.label = "displayed peripheral"; const ri = buildResourceIndex([s, p]); - const result = pinsAsDropDownsReadPin(ri.index, () => true); + const result = pinsAsDropDownsReadPin(ri.index, () => true, true); expect(JSON.stringify(result)).toContain("displayed sensor"); expect(JSON.stringify(result)).toContain("displayed peripheral"); }); diff --git a/frontend/sequences/step_tiles/pin_and_peripheral_support.tsx b/frontend/sequences/step_tiles/pin_and_peripheral_support.tsx index 812dce9ea..480767bcf 100644 --- a/frontend/sequences/step_tiles/pin_and_peripheral_support.tsx +++ b/frontend/sequences/step_tiles/pin_and_peripheral_support.tsx @@ -101,18 +101,20 @@ export function pinDropdowns( return [PIN_HEADING, ...PIN_RANGE.map(pinNumber2DropDown(valueFormat))]; } -export const pinsAsDropDownsWritePin = - (input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [ +export const pinsAsDropDownsWritePin = ( + input: ResourceIndex, shouldDisplay: ShouldDisplay, showPins: boolean +): DropDownItem[] => [ ...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []), ...(shouldDisplay(Feature.rpi_led_control) ? boxLedsAsDropDowns() : []), - ...pinDropdowns(n => n), + ...(showPins ? pinDropdowns(n => n) : []), ]; -export const pinsAsDropDownsReadPin = - (input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [ +export const pinsAsDropDownsReadPin = ( + input: ResourceIndex, shouldDisplay: ShouldDisplay, showPins: boolean +): DropDownItem[] => [ ...(shouldDisplay(Feature.named_pins) ? sensorsAsDropDowns(input) : []), ...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []), - ...pinDropdowns(n => n), + ...(showPins ? pinDropdowns(n => n) : []), ]; const TYPE_MAPPING: Record = { diff --git a/frontend/sequences/step_tiles/tile_read_pin.tsx b/frontend/sequences/step_tiles/tile_read_pin.tsx index 8306e8bc9..f4ae6b5b8 100644 --- a/frontend/sequences/step_tiles/tile_read_pin.tsx +++ b/frontend/sequences/step_tiles/tile_read_pin.tsx @@ -1,5 +1,4 @@ import * as React from "react"; - import { StepInputBox } from "../inputs/step_input_box"; import { StepParams } from "../interfaces"; import { ToolTips } from "../../constants"; @@ -48,7 +47,7 @@ export function TileReadPin(props: StepParams) { selectedItem={celery2DropDown(pin_number, props.resources)} onChange={setArgsDotPinNumber(props)} list={pinsAsDropDownsReadPin(props.resources, - shouldDisplay || (() => false))} /> + shouldDisplay || (() => false), !!props.showPins)} /> diff --git a/frontend/sequences/step_tiles/tile_write_pin.tsx b/frontend/sequences/step_tiles/tile_write_pin.tsx index 7a410102b..09f0a26c0 100644 --- a/frontend/sequences/step_tiles/tile_write_pin.tsx +++ b/frontend/sequences/step_tiles/tile_write_pin.tsx @@ -56,7 +56,7 @@ export function TileWritePin(props: StepParams) { selectedItem={celery2DropDown(pin_number, props.resources)} onChange={setArgsDotPinNumber(props)} list={pinsAsDropDownsWritePin(props.resources, - shouldDisplay || (() => false))} /> + shouldDisplay || (() => false), !!props.showPins)} /> diff --git a/frontend/ui/center_panel.tsx b/frontend/ui/center_panel.tsx index 6cc5a4e02..8fe33f90b 100644 --- a/frontend/ui/center_panel.tsx +++ b/frontend/ui/center_panel.tsx @@ -9,11 +9,13 @@ interface CenterProps { helpText: string; width?: number; docPage?: DocSlug; + backButton?: React.ReactNode; } export function CenterPanel(props: CenterProps) { return
+ {props.backButton}

{t(props.title)}

diff --git a/frontend/ui/help.tsx b/frontend/ui/help.tsx index ad06b82ae..7e55f20a6 100644 --- a/frontend/ui/help.tsx +++ b/frontend/ui/help.tsx @@ -3,12 +3,14 @@ import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core"; interface HelpProps { text: string; + requireClick?: boolean; } export function Help(props: HelpProps) { return
{props.text}
diff --git a/frontend/ui/right_panel.tsx b/frontend/ui/right_panel.tsx index cf36f9169..ed0c3f7a4 100644 --- a/frontend/ui/right_panel.tsx +++ b/frontend/ui/right_panel.tsx @@ -9,12 +9,14 @@ interface RightPanelProps { helpText: string; show: Boolean | undefined; width?: number; + backButton?: React.ReactNode; } export function RightPanel(props: RightPanelProps) { return {props.show &&
+ {props.backButton}

{t(props.title)}