misc UI fixes

pull/1148/head
gabrielburnworth 2019-04-11 20:17:18 -07:00
parent 8c053b2388
commit f303c27ce5
65 changed files with 671 additions and 350 deletions

View File

@ -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

View File

@ -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

View File

@ -2,7 +2,8 @@ jest.mock("react-redux", () => ({ connect: jest.fn() }));
let mockPath = ""; let mockPath = "";
jest.mock("../history", () => ({ jest.mock("../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); }) getPathArray: jest.fn(() => mockPath.split("/")),
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
})); }));
import * as React from "react"; import * as React from "react";
@ -39,6 +40,7 @@ const fakeProps = (): AppProps => {
getConfigValue: jest.fn(), getConfigValue: jest.fn(),
tour: undefined, tour: undefined,
resources: buildResourceIndex().index, resources: buildResourceIndex().index,
autoSync: false,
}; };
}; };

View File

@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => {
Object.defineProperty(window.location, "reload", { value: jest.fn() }); Object.defineProperty(window.location, "reload", { value: jest.fn() });
it("basically just initializes stuff", () => { it("basically just initializes stuff", () => {
const val = fetchLabFeatures(jest.fn()); const val = fetchLabFeatures(jest.fn());
expect(val.length).toBe(10); expect(val.length).toBe(9);
expect(val[0].value).toBeFalsy(); expect(val[0].value).toBeFalsy();
const { callback } = val[0]; const { callback } = val[0];
if (callback) { if (callback) {

View File

@ -1,4 +1,3 @@
import { BooleanSetting } from "../../session_keys"; import { BooleanSetting } from "../../session_keys";
import { Content } from "../../constants"; import { Content } from "../../constants";
import { VirtualTrail } from "../../farm_designer/map/layers/farmbot/bot_trail"; import { VirtualTrail } from "../../farm_designer/map/layers/farmbot/bot_trail";
@ -33,12 +32,6 @@ export const fetchLabFeatures =
displayInvert: true, displayInvert: true,
callback: () => window.location.reload() 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"), name: t("Hide Webcam widget"),
description: t(Content.HIDE_WEBCAM_WIDGET), description: t(Content.HIDE_WEBCAM_WIDGET),

View File

@ -14,7 +14,7 @@ import {
import { HotKeys } from "./hotkeys"; import { HotKeys } from "./hotkeys";
import { ControlsPopup } from "./controls_popup"; import { ControlsPopup } from "./controls_popup";
import { Content } from "./constants"; import { Content } from "./constants";
import { validBotLocationData, validFwConfig } from "./util"; import { validBotLocationData, validFwConfig, validFbosConfig } from "./util";
import { BooleanSetting } from "./session_keys"; import { BooleanSetting } from "./session_keys";
import { getPathArray } from "./history"; import { getPathArray } from "./history";
import { import {
@ -22,7 +22,7 @@ import {
} from "./config_storage/actions"; } from "./config_storage/actions";
import { takeSortedLogs } from "./logs/state_to_props"; import { takeSortedLogs } from "./logs/state_to_props";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { getFirmwareConfig } from "./resources/getters"; import { getFirmwareConfig, getFbosConfig } from "./resources/getters";
import { intersection } from "lodash"; import { intersection } from "lodash";
import { t } from "./i18next_wrapper"; import { t } from "./i18next_wrapper";
import { ResourceIndex } from "./resources/interfaces"; import { ResourceIndex } from "./resources/interfaces";
@ -47,10 +47,12 @@ export interface AppProps {
getConfigValue: GetWebAppConfigValue; getConfigValue: GetWebAppConfigValue;
tour: string | undefined; tour: string | undefined;
resources: ResourceIndex; resources: ResourceIndex;
autoSync: boolean;
} }
export function mapStateToProps(props: Everything): AppProps { export function mapStateToProps(props: Everything): AppProps {
const webAppConfigValue = getWebAppConfigValue(() => props); const webAppConfigValue = getWebAppConfigValue(() => props);
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
return { return {
timeSettings: maybeGetTimeSettings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index),
dispatch: props.dispatch, dispatch: props.dispatch,
@ -70,6 +72,7 @@ export function mapStateToProps(props: Everything): AppProps {
getConfigValue: webAppConfigValue, getConfigValue: webAppConfigValue,
tour: props.resources.consumers.help.currentTour, tour: props.resources.consumers.help.currentTour,
resources: props.resources.index, resources: props.resources.index,
autoSync: !!(fbosConfig && fbosConfig.auto_sync),
}; };
} }
/** Time at which the app gives up and asks the user to refresh */ /** Time at which the app gives up and asks the user to refresh */
@ -125,6 +128,7 @@ export class App extends React.Component<AppProps, {}> {
logs={this.props.logs} logs={this.props.logs}
getConfigValue={this.props.getConfigValue} getConfigValue={this.props.getConfigValue}
tour={this.props.tour} tour={this.props.tour}
autoSync={this.props.autoSync}
device={getDeviceAccountSettings(this.props.resources)} />} device={getDeviceAccountSettings(this.props.resources)} />}
{syncLoaded && this.props.children} {syncLoaded && this.props.children}
{!(["controls", "account", "regimens"].includes(currentPage)) && {!(["controls", "account", "regimens"].includes(currentPage)) &&

View File

@ -423,6 +423,9 @@ export namespace Content {
trim(`Display time using the 24-hour notation, trim(`Display time using the 24-hour notation,
i.e., 23:00 instead of 11:00pm`); 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 // Device
export const NOT_HTTPS = export const NOT_HTTPS =
trim(`WARNING: Sending passwords via HTTP:// is not secure.`); 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 trim(`Click one in the Sequences panel to edit, or click "+" to create
a new one.`); a new one.`);
export const NO_SEQUENCES =
trim(`Click "+" to create a new sequence.`);
export const END_DETECTION_DISABLED = export const END_DETECTION_DISABLED =
trim(`This command will not execute correctly because you do not have trim(`This command will not execute correctly because you do not have
encoders or endstops enabled for the chosen axis. Enable endstops or 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 trim(`Click one in the Regimens panel to edit, or click "+" to create
a new one.`); a new one.`);
export const NO_REGIMENS =
trim(`Click "+" to create a new regimen.`);
// Farm Designer // Farm Designer
export const OUTSIDE_PLANTING_AREA = export const OUTSIDE_PLANTING_AREA =
trim(`Outside of planting area. Plants must be placed within the grid.`); trim(`Outside of planting area. Plants must be placed within the grid.`);

View File

@ -37,7 +37,7 @@ export class Controls extends React.Component<Props, {}> {
bot={this.props.bot} bot={this.props.bot}
peripherals={this.props.peripherals} peripherals={this.props.peripherals}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
disabled={this.arduinoBusy} /> disabled={this.arduinoBusy || !this.botOnline} />
webcams = () => <WebcamPanel webcams = () => <WebcamPanel
feeds={this.props.feeds} feeds={this.props.feeds}

View File

@ -1,10 +1,10 @@
import * as React from "react"; import * as React from "react";
import { DirectionButton } from "./controls/move/direction_button"; import { DirectionButton } from "./controls/move/direction_button";
import { getDevice } from "./device"; import { getDevice } from "./device";
import { buildDirectionProps } from "./controls/move/direction_axes_props"; import { buildDirectionProps } from "./controls/move/direction_axes_props";
import { ControlsPopupProps } from "./controls/move/interfaces"; import { ControlsPopupProps } from "./controls/move/interfaces";
import { commandErr } from "./devices/actions"; import { commandErr } from "./devices/actions";
import { mapPanelClassName } from "./farm_designer/map/util";
interface State { interface State {
isOpen: boolean; isOpen: boolean;
@ -24,7 +24,7 @@ export class ControlsPopup
const rightLeft = xySwap ? "y" : "x"; const rightLeft = xySwap ? "y" : "x";
const upDown = xySwap ? "x" : "y"; const upDown = xySwap ? "x" : "y";
return <div return <div
className={"controls-popup " + isOpen}> className={`controls-popup ${isOpen} ${mapPanelClassName()}`}>
<i className="fa fa-crosshairs" <i className="fa fa-crosshairs"
onClick={this.toggle("isOpen")} /> onClick={this.toggle("isOpen")} />
<div className="controls-popup-menu-outer"> <div className="controls-popup-menu-outer">

View File

@ -1,4 +1,4 @@
@media screen and (max-width: 974px) { @media screen and (max-width: 1075px) {
.all-content-wrapper { .all-content-wrapper {
padding: 11rem 0 0; padding: 11rem 0 0;
overflow: hidden; overflow: hidden;

View File

@ -184,6 +184,7 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-size: 1.2rem; font-size: 1.2rem;
color: $dark_gray; color: $dark_gray;
text-align: left;
&.active { &.active {
box-shadow: none !important; box-shadow: none !important;
border: 1px solid $white; border: 1px solid $white;

View File

@ -2,6 +2,19 @@
position: relative; position: relative;
height: 100vh; height: 100vh;
overflow-y: hidden; 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 { .farm-designer-map {
@ -13,6 +26,9 @@
&.panel-open { &.panel-open {
padding: 11rem 2rem 2rem 31.8rem; // at zoom = 1.0: 110px 20px 20px 318px 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 { .drop-area {
@ -42,7 +58,6 @@
.crop-drag-info-image { .crop-drag-info-image {
width: 100% !important; width: 100% !important;
background-color: $translucent; background-color: $translucent;
max-width: 28rem;
} }
.plant-catalog-image { .plant-catalog-image {
@ -104,15 +119,15 @@
.text-input-wrapper { .text-input-wrapper {
position: relative; position: relative;
margin: 1rem; margin: 1rem;
border-bottom: 1px solid #000; border-bottom: 1px solid $dark_gray;
&:before, &:before,
&:after { &:after {
content: ""; content: "";
position: absolute; position: absolute;
bottom: 0; bottom: 0;
background: #000; background: $dark_gray;
width: 1px; width: 1px;
height: 10px; height: 3px;
} }
&:before { &:before {
left: 0; left: 0;
@ -120,6 +135,9 @@
&:after { &:after {
right: 0; right: 0;
} }
i {
font-size: 1.5rem;
}
.fa-search { .fa-search {
position: absolute; position: absolute;
top: 0.8rem; top: 0.8rem;

View File

@ -3,6 +3,9 @@
position: fixed; position: fixed;
top: 8.9rem; top: 8.9rem;
width: 30rem; width: 30rem;
@media screen and (max-width: 450px) {
width: 100%;
}
} }
@keyframes panel-pullout { @keyframes panel-pullout {
@ -17,6 +20,12 @@
.farm-designer-panels { .farm-designer-panels {
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
&.panel-closed {
display: none !important;
}
&.short-panel {
height: 24rem;
}
.panel-container { .panel-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -134,33 +143,30 @@
.panel-title { .panel-title {
height: 50px; height: 50px;
padding-top: 1.8rem;
padding-left: 1.4rem;
padding-right: 2rem;
.back-arrow { .back-arrow {
display: inline-block; float: left;
color: $off_white; color: $off_white;
margin-right: 1rem; text-align: center;
font-size: 1.8rem; font-size: 1.8rem;
margin-top: -1.8rem; width: 50px;
vertical-align: middle; line-height: 50px;
&:hover { &:hover {
color: $white; color: $white;
} }
} }
.title { .title {
display: inline-block; float: left;
color: $white; color: $off_white;
font-size: 1.8rem; font-size: 1.8rem;
margin-top: 0.4rem;
white-space: nowrap; white-space: nowrap;
width: 10em; width: 50%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
height: 2rem; line-height: 50px;
padding: 0.2rem;
} }
.right-button { .right-button {
position: absolute;
right: 0;
float: right; float: right;
text-transform: uppercase; text-transform: uppercase;
font-size: 1rem; font-size: 1rem;
@ -170,6 +176,8 @@
letter-spacing: 1px; letter-spacing: 1px;
border-radius: 4px; border-radius: 4px;
color: $off_white; color: $off_white;
margin-top: 1.25rem;
margin-right: 1.5rem;
&:hover { &:hover {
color: $white; color: $white;
} }
@ -250,8 +258,15 @@
&.with-button { &.with-button {
display: flex; display: flex;
margin-top: 5rem; margin-top: 5rem;
a { .fb-button {
margin: 1rem; margin: 1rem;
margin-left: 0;
}
a {
margin-top: 0.5rem;
}
i {
font-size: 1.5rem;
} }
} }
} }
@ -268,7 +283,7 @@
input { input {
background: $white; background: $white;
} }
.is-saved { .save-btn {
margin: 1rem; margin: 1rem;
} }
} }
@ -284,7 +299,6 @@
.panel-nav { .panel-nav {
position: fixed; position: fixed;
z-index: 2; z-index: 2;
width: 300px;
} }
.panel-header { .panel-header {
@ -301,9 +315,6 @@
} }
.crop-info-panel { .crop-info-panel {
.title {
width: 50%;
}
.panel-header { .panel-header {
position: inherit; position: inherit;
background-size: 144% !important; 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 { .move-to-panel-content {
&.with-nav { &.with-nav {
margin-top: 6rem; margin-top: 6rem;

View File

@ -706,6 +706,11 @@ ul {
.controls-popup { .controls-popup {
color: $off_white; color: $off_white;
@media screen and (max-width: 450px) {
&.panel-open {
display: none;
}
}
i { i {
position: fixed; position: fixed;
bottom: 3rem; bottom: 3rem;
@ -765,7 +770,7 @@ ul {
.empty-state-graphic { .empty-state-graphic {
display: flex; display: flex;
margin: auto; margin: auto;
margin-top: 10%; margin-top: 25%;
width: 50%; width: 50%;
} }
@ -867,6 +872,9 @@ ul {
margin-right: 1rem; margin-right: 1rem;
margin-left: 1rem; margin-left: 1rem;
} }
.fb-button {
margin-top: 0;
}
} }
.logs-page { .logs-page {

View File

@ -23,6 +23,19 @@ nav {
padding: 0 1rem; 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 { .links {
display: inline-block; display: inline-block;
a { a {
@ -81,6 +94,14 @@ nav {
} }
.nav-right { .nav-right {
height: 5rem;
overflow: hidden;
.connection-status-popover {
display: inline;
.bp3-popover-wrapper {
margin: 1.85rem;
}
}
a { a {
font-weight: normal; font-weight: normal;
color: $black; color: $black;
@ -92,54 +113,42 @@ nav {
margin-right: 0.8rem; margin-right: 0.8rem;
} }
} }
.connection-status-popover { }
display: inline;
.bp3-popover-wrapper { .menu-popover {
margin: 1.85rem; display: inline;
.bp3-popover-content {
position: relative;
width: 22rem;
background: $dark_gray;
i {
margin-right: 0.8rem;
} }
} font-size: 1.2rem;
.menu-popover { letter-spacing: 1.2px;
display: inline; a:not(.app-version) {
.bp3-popover-content { display: inline-block;
position: relative; text-transform: uppercase;
width: 22rem; color: $off_white;
a:not(.app-version) { margin-bottom: 0.6rem;
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;
}
}
} }
.bp3-overlay-content { .app-version {
margin-top: 1.6rem; margin: 1rem -1rem -1rem;
background: $dark_gray;
color: $white; color: $white;
} padding: 0.5rem 0 0 1rem;
.bp3-popover-wrapper { border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
label {
color: $white;
}
a { a {
color: $white; color: $white;
} }
.bp3-popover-arrow-fill { p {
fill: $dark_gray; display: inline;
} color: $gray;
.bp3-popover-content { font-size: 1.2rem;
background: $dark_gray;
} }
} }
} }
@ -157,13 +166,13 @@ nav {
} }
} }
@media screen and (max-width: 974px) { @media screen and (max-width: 1075px) {
.top-menu-container .nav-links { .top-menu-container .nav-links {
display: none; display: none;
} }
} }
@media screen and (min-width: 975px) { @media screen and (min-width: 1075px) {
.mobile-menu-icon { .mobile-menu-icon {
display: none !important; display: none !important;
} }

View File

@ -31,6 +31,10 @@
} }
} }
.bulk-scheduler-content {
margin-top: 1rem;
}
// Regimen Editor // Regimen Editor
.regimen-day { .regimen-day {
margin: 1.5rem 0; margin: 1.5rem 0;

View File

@ -1,10 +1,10 @@
.farmware-input-panel, .farmware-input-panel,
.sequence-editor-panel, .sequence-editor-panel,
.regimen-editor-panel { .regimen-editor-panel {
margin: -3rem -1.5rem -6rem; margin: -3rem -1.5rem -3rem;
height: calc(100vh - 5rem); height: calc(100vh - 5rem);
background: $light_gray; background: $light_gray;
@media screen and (max-width: 768px) { @media screen and (max-width: 767px) {
display: none; display: none;
&.open { &.open {
display: block; display: block;
@ -22,7 +22,7 @@
float: left; float: left;
margin-top: 0.4rem; margin-top: 0.4rem;
} }
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
h3, h3,
p { p {
margin-left: 15px; margin-left: 15px;
@ -33,6 +33,7 @@
} }
.button-group { .button-group {
margin-right: 15px; margin-right: 15px;
margin-top: -1rem;
} }
.title-help-text { .title-help-text {
padding-left: 15px; padding-left: 15px;
@ -51,7 +52,7 @@
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
.title-help-text { .title-help-text {
padding-left: 3rem; padding-left: 3rem;
padding-right: 3rem; padding-right: 3rem;
@ -63,7 +64,7 @@
.sequence-editor-content, .sequence-editor-content,
.regimen-editor-content { .regimen-editor-content {
margin-right: -15px; margin-right: -15px;
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
margin-left: 15px; margin-left: 15px;
margin-right: 0; margin-right: 0;
} }
@ -72,7 +73,7 @@
.sequence-editor-tools, .sequence-editor-tools,
.regimen-editor-tools { .regimen-editor-tools {
margin-right: 15px; margin-right: 15px;
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
margin-right: 10px; margin-right: 10px;
} }
.locals-list { .locals-list {
@ -146,7 +147,7 @@
.farmware-info-panel, .farmware-info-panel,
.step-button-cluster-panel { .step-button-cluster-panel {
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
display: none; display: none;
&.farmware-info-open, &.farmware-info-open,
&.inserting-step { &.inserting-step {
@ -157,12 +158,18 @@
} }
} }
.step-button-cluster {
@media screen and (max-width: 767px) {
width: 40rem;
}
}
.farmware-info-panel button { .farmware-info-panel button {
margin-bottom: 3rem; margin-bottom: 3rem;
} }
.step-button-cluster { .step-button-cluster {
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
} }
@ -198,12 +205,31 @@
padding-top: 0.4rem; padding-top: 0.4rem;
margin-bottom: 3rem; margin-bottom: 3rem;
margin-right: 5px; 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 { &.open {
display: none; display: none;
} }
margin-left: 15px; margin-left: 15px;
margin-right: 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 { .farmware-input-panel-contents {
@media screen and (max-width: 974px) { @media screen and (max-width: 767px) {
margin-left: 15px; margin-left: 15px;
margin-right: 15px; margin-right: 15px;
padding-right: 5rem; padding-right: 5rem;
} }
} }
.sequence-list-panel input,
.regimen-list-panel input {
margin-bottom: 1rem;
}
.back-to-farmware,
.back-to-regimens, .back-to-regimens,
.back-to-sequences { .back-to-sequences {
display: none; display: none;
&.open { &.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; display: block;
margin: 4rem; margin: 4rem;
margin-top: 0; margin-top: 0;
@ -249,22 +290,19 @@
i { i {
margin-right: 1rem; margin-right: 1rem;
} }
&.inserting-step {
display: none;
}
} }
} }
} }
.drag-drop-area { .drag-drop-area {
@media screen and (max-width: 768px) { @media screen and (max-width: 767px) {
display: none; display: none;
} }
} }
.add-command-button-container { .add-command-button-container {
display: none; display: none;
@media screen and (max-width: 768px) { @media screen and (max-width: 767px) {
display: block; display: block;
min-height: 3rem; min-height: 3rem;
.add-command { .add-command {
@ -278,7 +316,7 @@
.farmware-info-button { .farmware-info-button {
display: none; display: none;
@media screen and (max-width: 768px) { @media screen and (max-width: 767px) {
&.open { &.open {
display: block; display: block;
margin: 4rem; margin: 4rem;

View File

@ -58,6 +58,7 @@
box-shadow: none; box-shadow: none;
color: $dark_gray; color: $dark_gray;
font-weight: bold; font-weight: bold;
min-width: 30%;
} }
p { p {
font-size: 1rem; font-size: 1rem;

View File

@ -17,7 +17,7 @@ describe("<EStopButton />", () => {
bot.hardware.informational_settings.sync_status = undefined; bot.hardware.informational_settings.sync_status = undefined;
const wrapper = mount(<EStopButton {...fakeProps()} />); const wrapper = mount(<EStopButton {...fakeProps()} />);
expect(wrapper.text()).toEqual("E-STOP"); expect(wrapper.text()).toEqual("E-STOP");
expect(wrapper.find("button").hasClass("gray")).toBeTruthy(); expect(wrapper.find("button").hasClass("pseudo-disabled")).toBeTruthy();
}); });
it("locked", () => { it("locked", () => {

View File

@ -1,17 +1,18 @@
import * as React from "react"; import * as React from "react";
import { emergencyLock, emergencyUnlock } from "../actions"; import { emergencyLock, emergencyUnlock } from "../actions";
import { EStopButtonProps } from "../interfaces"; import { EStopButtonProps } from "../interfaces";
import { isBotUp } from "../must_be_online"; import { isBotUp } from "../must_be_online";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
const GRAY = "pseudo-disabled";
export class EStopButton extends React.Component<EStopButtonProps, {}> { export class EStopButton extends React.Component<EStopButtonProps, {}> {
render() { render() {
const i = this.props.bot.hardware.informational_settings; const i = this.props.bot.hardware.informational_settings;
const isLocked = !!i.locked; const isLocked = !!i.locked;
const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock; const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock;
const color = isLocked ? "yellow" : "red"; 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"; const emergencyLockStatusText = isLocked ? t("UNLOCK") : "E-STOP";
return <button return <button

View File

@ -93,15 +93,6 @@ describe("<BoardType/>", () => {
it("displays standard boards", () => { it("displays standard boards", () => {
const wrapper = shallow(<BoardType {...fakeProps()} />); const wrapper = shallow(<BoardType {...fakeProps()} />);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" }]);
});
it("displays new board", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = shallow(<BoardType {...p} />);
expect(wrapper.find("FBSelect").props().list).toEqual([ expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" }, { label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" }, { label: "Farmduino (Genesis v1.3)", value: "farmduino" },

View File

@ -5,7 +5,6 @@ import { FirmwareHardware } from "farmbot";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { updateConfig } from "../../actions"; import { updateConfig } from "../../actions";
import { BoardTypeProps } from "./interfaces"; import { BoardTypeProps } from "./interfaces";
import { Feature } from "../../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { FirmwareHardwareStatus } from "./firmware_hardware_status"; import { FirmwareHardwareStatus } from "./firmware_hardware_status";
@ -69,12 +68,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
return isFwHardwareValue(value) ? value : undefined; return isFwHardwareValue(value) ? value : undefined;
} }
get firmwareChoices() { get firmwareChoices() { return [ARDUINO, FARMDUINO, FARMDUINO_K14]; }
const { shouldDisplay } = this.props;
return [ARDUINO, FARMDUINO,
...(shouldDisplay(Feature.farmduino_k14) ? [FARMDUINO_K14] : [])
];
}
get firmwareVersion() { get firmwareVersion() {
return this.props.bot.hardware.informational_settings.firmware_version; return this.props.bot.hardware.informational_settings.firmware_version;

View File

@ -7,7 +7,6 @@ import { mapStateToProps } from "./state_to_props";
import { Props } from "./interfaces"; import { Props } from "./interfaces";
import { PinBindings } from "./pin_bindings/pin_bindings"; import { PinBindings } from "./pin_bindings/pin_bindings";
import { selectAllDiagnosticDumps } from "../resources/selectors"; import { selectAllDiagnosticDumps } from "../resources/selectors";
import { ConnectivityPanel } from "./connectivity";
import { getStatus } from "../connectivity/reducer_support"; import { getStatus } from "../connectivity/reducer_support";
@connect(mapStateToProps) @connect(mapStateToProps)
@ -35,11 +34,6 @@ export class Devices extends React.Component<Props, {}> {
isValidFbosConfig={this.props.isValidFbosConfig} isValidFbosConfig={this.props.isValidFbosConfig}
env={this.props.env} env={this.props.env}
saveFarmwareEnv={this.props.saveFarmwareEnv} /> saveFarmwareEnv={this.props.saveFarmwareEnv} />
<ConnectivityPanel
status={this.props.deviceAccount.specialStatus}
bot={this.props.bot}
dispatch={this.props.dispatch}
deviceAccount={this.props.deviceAccount} />
</Col> </Col>
<Col xs={12} sm={6}> <Col xs={12} sm={6}>
<HardwareSettings <HardwareSettings

View File

@ -1,13 +1,14 @@
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({ connect: jest.fn() }));
connect: jest.fn()
}));
let mockPath = "/app/designer/plants"; let mockPath = "/app/designer/plants";
jest.mock("../../history", () => ({ jest.mock("../../history", () => ({
history: { history: { getCurrentLocation: jest.fn(() => ({ pathname: mockPath })) },
getCurrentLocation: jest.fn(() => { return { pathname: mockPath }; }), getPathArray: jest.fn(() => mockPath.split("/")),
}, }));
getPathArray: jest.fn(() => { return mockPath.split("/"); }),
jest.mock("../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
})); }));
import * as React from "react"; import * as React from "react";
@ -16,9 +17,14 @@ import { mount } from "enzyme";
import { Props } from "../interfaces"; import { Props } from "../interfaces";
import { GardenMapLegendProps } from "../map/interfaces"; import { GardenMapLegendProps } from "../map/interfaces";
import { bot } from "../../__test_support__/fake_state/bot"; import { bot } from "../../__test_support__/fake_state/bot";
import { fakeImage } from "../../__test_support__/fake_state/resources"; import {
fakeImage, fakeWebAppConfig
} from "../../__test_support__/fake_state/resources";
import { fakeDesignerState } from "../../__test_support__/fake_designer_state"; import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { fakeState } from "../../__test_support__/fake_state";
import { edit } from "../../api/crud";
describe("<FarmDesigner/>", () => { describe("<FarmDesigner/>", () => {
function fakeProps(): Props { function fakeProps(): Props {
@ -93,7 +99,7 @@ describe("<FarmDesigner/>", () => {
["Map", "Plants", "Events"].map(string => ["Map", "Plants", "Events"].map(string =>
expect(wrapper.text()).toContain(string)); expect(wrapper.text()).toContain(string));
expect(wrapper.find(".panel-nav").first().hasClass("hidden")).toBeTruthy(); expect(wrapper.find(".panel-nav").first().hasClass("hidden")).toBeTruthy();
expect(wrapper.find(".farm-designer-panels").hasClass("hidden")).toBeFalsy(); expect(wrapper.find(".farm-designer-panels").hasClass("panel-open")).toBeTruthy();
expect(wrapper.find(".farm-designer-map").hasClass("panel-open")).toBeTruthy(); expect(wrapper.find(".farm-designer-map").hasClass("panel-open")).toBeTruthy();
}); });
@ -103,7 +109,7 @@ describe("<FarmDesigner/>", () => {
["Map", "Plants", "Events"].map(string => ["Map", "Plants", "Events"].map(string =>
expect(wrapper.text()).toContain(string)); expect(wrapper.text()).toContain(string));
expect(wrapper.find(".panel-nav").first().hasClass("hidden")).toBeFalsy(); expect(wrapper.find(".panel-nav").first().hasClass("hidden")).toBeFalsy();
expect(wrapper.find(".farm-designer-panels").hasClass("hidden")).toBeTruthy(); expect(wrapper.find(".farm-designer-panels").hasClass("panel-open")).toBeFalsy();
expect(wrapper.find(".farm-designer-map").hasClass("panel-open")).toBeFalsy(); expect(wrapper.find(".farm-designer-map").hasClass("panel-open")).toBeFalsy();
}); });
@ -113,4 +119,15 @@ describe("<FarmDesigner/>", () => {
const wrapper = mount(<FarmDesigner {...p} />); const wrapper = mount(<FarmDesigner {...p} />);
expect(wrapper.text().toLowerCase()).toContain("viewing saved garden"); expect(wrapper.text().toLowerCase()).toContain("viewing saved garden");
}); });
it("toggles setting", () => {
const p = fakeProps();
const state = fakeState();
const dispatch = jest.fn();
state.resources = buildResourceIndex([fakeWebAppConfig()]);
p.dispatch = jest.fn(x => x(dispatch, () => state));
const wrapper = mount<FarmDesigner>(<FarmDesigner {...p} />);
wrapper.instance().toggle("show_plants")();
expect(edit).toHaveBeenCalledWith(expect.any(Object), { bot_origin_quadrant: 2 });
});
}); });

View File

@ -495,7 +495,6 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
<this.RepeatForm /> <this.RepeatForm />
<SaveBtn <SaveBtn
status={farmEvent.specialStatus || this.state.specialStatusLocal} status={farmEvent.specialStatus || this.state.specialStatusLocal}
color="yellow"
onClick={() => this.commitViewModel()} /> onClick={() => this.commitViewModel()} />
<this.FarmEventDeleteButton /> <this.FarmEventDeleteButton />
<TzWarning deviceTimezone={this.props.deviceTimezone} /> <TzWarning deviceTimezone={this.props.deviceTimezone} />

View File

@ -3,13 +3,14 @@ import { connect } from "react-redux";
import { GardenMap } from "./map/garden_map"; import { GardenMap } from "./map/garden_map";
import { Props, State, BotOriginQuadrant, isBotOriginQuadrant } from "./interfaces"; import { Props, State, BotOriginQuadrant, isBotOriginQuadrant } from "./interfaces";
import { mapStateToProps } from "./state_to_props"; import { mapStateToProps } from "./state_to_props";
import { history } from "../history";
import { Plants } from "./plants/plant_inventory"; import { Plants } from "./plants/plant_inventory";
import { GardenMapLegend } from "./map/legend/garden_map_legend"; import { GardenMapLegend } from "./map/legend/garden_map_legend";
import { NumericSetting, BooleanSetting } from "../session_keys"; import { NumericSetting, BooleanSetting } from "../session_keys";
import { isUndefined, last } from "lodash"; import { isUndefined, last } from "lodash";
import { AxisNumberProperty, BotSize } from "./map/interfaces"; import { AxisNumberProperty, BotSize } from "./map/interfaces";
import { getBotSize, round } from "./map/util"; import {
getBotSize, round, getPanelStatus, MapPanelStatus, mapPanelClassName
} from "./map/util";
import { calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex } from "./map/zoom"; import { calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex } from "./map/zoom";
import moment from "moment"; import moment from "moment";
import { DesignerNavTabs } from "./panel_header"; import { DesignerNavTabs } from "./panel_header";
@ -98,9 +99,7 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
return this.props.children || React.createElement(Plants, props); return this.props.children || React.createElement(Plants, props);
} }
get mapOnly() { get mapPanelClassName() { return mapPanelClassName(); }
return history.getCurrentLocation().pathname === "/app/designer";
}
render() { render() {
const { const {
@ -134,11 +133,10 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
: 1; : 1;
const imageAgeInfo = { newestDate, toOldest }; const imageAgeInfo = { newestDate, toOldest };
const displayPanel = this.mapOnly ? "hidden" : "";
return <div className="farm-designer"> return <div className="farm-designer">
<GardenMapLegend <GardenMapLegend
className={this.mapPanelClassName}
zoom={this.updateZoomLevel} zoom={this.updateZoomLevel}
toggle={this.toggle} toggle={this.toggle}
updateBotOriginQuadrant={this.updateBotOriginQuadrant} updateBotOriginQuadrant={this.updateBotOriginQuadrant}
@ -155,13 +153,13 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
getConfigValue={this.props.getConfigValue} getConfigValue={this.props.getConfigValue}
imageAgeInfo={imageAgeInfo} /> imageAgeInfo={imageAgeInfo} />
<DesignerNavTabs hidden={!this.mapOnly} /> <DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
<div className={`farm-designer-panels ${displayPanel}`}> <div className={`farm-designer-panels ${this.mapPanelClassName}`}>
{this.childComponent(this.props)} {this.childComponent(this.props)}
</div> </div>
<div <div
className={`farm-designer-map ${this.mapOnly ? "" : "panel-open"}`} className={`farm-designer-map ${this.mapPanelClassName}`}
style={{ zoom: zoom_level }}> style={{ zoom: zoom_level }}>
<GardenMap <GardenMap
showPoints={show_points} showPoints={show_points}

View File

@ -1,6 +1,7 @@
let mockPath = ""; let mockPath = "";
jest.mock("../../../history", () => ({ jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); }) getPathArray: jest.fn(() => mockPath.split("/")),
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
})); }));
jest.mock("../../saved_gardens/saved_gardens", () => ({ jest.mock("../../saved_gardens/saved_gardens", () => ({
@ -16,6 +17,8 @@ import {
transformForQuadrant, transformForQuadrant,
getMode, getMode,
getGardenCoordinates, getGardenCoordinates,
MapPanelStatus,
mapPanelClassName,
} from "../util"; } from "../util";
import { McuParams } from "farmbot"; import { McuParams } from "farmbot";
import { import {
@ -41,7 +44,7 @@ describe("translateScreenToGarden()", () => {
scroll: { left: 10, top: 20 }, scroll: { left: 10, top: 20 },
zoomLvl: 1, zoomLvl: 1,
gridOffset: { x: 30, y: 40 }, gridOffset: { x: 30, y: 40 },
mapOnly: false, panelStatus: MapPanelStatus.open,
}); });
expect(result).toEqual({ x: 180, y: 80 }); expect(result).toEqual({ x: 180, y: 80 });
}); });
@ -53,7 +56,7 @@ describe("translateScreenToGarden()", () => {
scroll: { left: 10, top: 20 }, scroll: { left: 10, top: 20 },
zoomLvl: 0.33, zoomLvl: 0.33,
gridOffset: { x: 30, y: 40 }, gridOffset: { x: 30, y: 40 },
mapOnly: false, panelStatus: MapPanelStatus.open,
}); });
expect(result).toEqual({ x: 2470, y: 840 }); expect(result).toEqual({ x: 2470, y: 840 });
}); });
@ -65,7 +68,7 @@ describe("translateScreenToGarden()", () => {
scroll: { left: 10, top: 20 }, scroll: { left: 10, top: 20 },
zoomLvl: 1.5, zoomLvl: 1.5,
gridOffset: { x: 30, y: 40 }, gridOffset: { x: 30, y: 40 },
mapOnly: false, panelStatus: MapPanelStatus.open,
}); });
expect(result).toEqual({ x: 520, y: 150 }); expect(result).toEqual({ x: 520, y: 150 });
}); });
@ -80,7 +83,7 @@ describe("translateScreenToGarden()", () => {
scroll: { left: 10, top: 20 }, scroll: { left: 10, top: 20 },
zoomLvl: 0.75, zoomLvl: 0.75,
gridOffset: { x: 30, y: 40 }, gridOffset: { x: 30, y: 40 },
mapOnly: false, panelStatus: MapPanelStatus.open,
}); });
expect(result).toEqual({ x: 0, y: 130 }); expect(result).toEqual({ x: 0, y: 130 });
}); });
@ -96,7 +99,7 @@ describe("translateScreenToGarden()", () => {
scroll: { left: 10, top: 20 }, scroll: { left: 10, top: 20 },
zoomLvl: 0.75, zoomLvl: 0.75,
gridOffset: { x: 30, y: 40 }, gridOffset: { x: 30, y: 40 },
mapOnly: false, panelStatus: MapPanelStatus.open,
}); });
expect(result).toEqual({ x: 130, y: 0 }); expect(result).toEqual({ x: 130, y: 0 });
}); });
@ -108,10 +111,22 @@ describe("translateScreenToGarden()", () => {
scroll: { left: 10, top: 20 }, scroll: { left: 10, top: 20 },
zoomLvl: 1, zoomLvl: 1,
gridOffset: { x: 30, y: 40 }, gridOffset: { x: 30, y: 40 },
mapOnly: true, panelStatus: MapPanelStatus.closed,
}); });
expect(result).toEqual({ x: 480, y: 30 }); expect(result).toEqual({ x: 480, y: 30 });
}); });
it("translates screen coords to garden coords: short panel", () => {
const result = translateScreenToGarden({
mapTransformProps: fakeMapTransformProps(),
page: { x: 520, y: 412 },
scroll: { left: 10, top: 20 },
zoomLvl: 1,
gridOffset: { x: 30, y: 40 },
panelStatus: MapPanelStatus.short,
});
expect(result).toEqual({ x: 480, y: 40 });
});
}); });
describe("getbotSize()", () => { describe("getbotSize()", () => {
@ -365,3 +380,10 @@ describe("getGardenCoordinates()", () => {
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
}); });
}); });
describe("mapPanelClassName()", () => {
it("returns correct panel status", () => {
mockPath = "/app/designer/move_to";
expect(mapPanelClassName()).toEqual("short-panel");
});
});

View File

@ -45,6 +45,7 @@ export interface GardenMapLegendProps {
getConfigValue: GetWebAppConfigValue; getConfigValue: GetWebAppConfigValue;
imageAgeInfo: { newestDate: string, toOldest: number }; imageAgeInfo: { newestDate: string, toOldest: number };
gardenId?: number; gardenId?: number;
className?: string;
} }
export type MapTransformProps = { export type MapTransformProps = {

View File

@ -111,7 +111,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
export function GardenMapLegend(props: GardenMapLegendProps) { export function GardenMapLegend(props: GardenMapLegendProps) {
const menuClass = props.legendMenuOpen ? "active" : ""; const menuClass = props.legendMenuOpen ? "active" : "";
return <div return <div
className={"garden-map-legend " + menuClass} className={`garden-map-legend ${menuClass} ${props.className}`}
style={{ zoom: 1 }}> style={{ zoom: 1 }}>
<div <div
className={"menu-pullout " + menuClass} className={"menu-pullout " + menuClass}

View File

@ -5,9 +5,8 @@ import {
CheckedAxisLength, AxisNumberProperty, BotSize, MapTransformProps, Mode CheckedAxisLength, AxisNumberProperty, BotSize, MapTransformProps, Mode
} from "./interfaces"; } from "./interfaces";
import { trim } from "../../util"; import { trim } from "../../util";
import { getPathArray } from "../../history"; import { history, getPathArray } from "../../history";
import { savedGardenOpen } from "../saved_gardens/saved_gardens"; import { savedGardenOpen } from "../saved_gardens/saved_gardens";
import { last } from "lodash";
/* /*
* Farm Designer Map Utilities * Farm Designer Map Utilities
@ -51,12 +50,48 @@ export function round(num: number) {
* *
*/ */
/** Controlled by .farm-designer-map padding x10 */ /** Status of farm designer side panel. */
const paddingWhen = { export enum MapPanelStatus {
panelClosed: { left: 20, top: 160 }, open = "open",
panelOpen: { left: 318, top: 110 } closed = "closed",
short = "short",
}
/** Get farm designer side panel status. */
export const getPanelStatus = (): MapPanelStatus => {
if (history.getCurrentLocation().pathname === "/app/designer") {
return MapPanelStatus.closed;
}
const mode = getMode();
if (mode === Mode.moveTo || mode === Mode.clickToAdd) {
return MapPanelStatus.short;
}
return MapPanelStatus.open;
}; };
/** Get panel status class name for farm designer. */
export const mapPanelClassName = () => {
switch (getPanelStatus()) {
case MapPanelStatus.short: return "short-panel";
case MapPanelStatus.closed: return "panel-closed";
case MapPanelStatus.open:
default:
return "panel-open";
}
};
/** Controlled by .farm-designer-map padding x10 */
const getMapPadding =
(panelStatus: MapPanelStatus): { left: number, top: number } => {
switch (panelStatus) {
case MapPanelStatus.short: return { left: 20, top: 350 };
case MapPanelStatus.closed: return { left: 20, top: 160 };
case MapPanelStatus.open:
default:
return { left: 318, top: 110 };
}
};
/** "x" => "left" and "y" => "top" */ /** "x" => "left" and "y" => "top" */
const leftOrTop: Record<"x" | "y", "top" | "left"> = { x: "left", y: "top" }; const leftOrTop: Record<"x" | "y", "top" | "left"> = { x: "left", y: "top" };
@ -68,7 +103,7 @@ export interface ScreenToGardenParams {
zoomLvl: number; zoomLvl: number;
mapTransformProps: MapTransformProps; mapTransformProps: MapTransformProps;
gridOffset: AxisNumberProperty; gridOffset: AxisNumberProperty;
mapOnly: boolean; panelStatus: MapPanelStatus;
} }
/** Transform screen coordinates into garden coordinates */ /** Transform screen coordinates into garden coordinates */
@ -76,10 +111,10 @@ export function translateScreenToGarden(
params: ScreenToGardenParams params: ScreenToGardenParams
): XYCoordinate { ): XYCoordinate {
const { const {
page, scroll, zoomLvl, mapTransformProps, gridOffset, mapOnly page, scroll, zoomLvl, mapTransformProps, gridOffset, panelStatus
} = params; } = params;
const { xySwap } = mapTransformProps; const { xySwap } = mapTransformProps;
const mapPadding = mapOnly ? paddingWhen.panelClosed : paddingWhen.panelOpen; const mapPadding = getMapPadding(panelStatus);
const screenXY = page; const screenXY = page;
const mapXY = ["x", "y"].reduce<XYCoordinate>( const mapXY = ["x", "y"].reduce<XYCoordinate>(
(result: XYCoordinate, axis: "x" | "y") => { (result: XYCoordinate, axis: "x" | "y") => {
@ -283,7 +318,7 @@ export const getGardenCoordinates = (props: {
mapTransformProps: props.mapTransformProps, mapTransformProps: props.mapTransformProps,
gridOffset: props.gridOffset, gridOffset: props.gridOffset,
zoomLvl, zoomLvl,
mapOnly: last(getPathArray()) === "designer", panelStatus: getPanelStatus(),
}; };
return translateScreenToGarden(params); return translateScreenToGarden(params);
} else { } else {

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import { svgToUrl } from "../../open_farm/icons"; import { svgToUrl } from "../../open_farm/icons";
import { import {
CropInfoProps, CropLiveSearchResult, OpenfarmSearch CropInfoProps, CropLiveSearchResult, OpenfarmSearch
@ -147,7 +146,8 @@ const AddPlantHereButton = (props: {
cropName, slug, gardenCoords: botXY, gridSize: undefined, cropName, slug, gardenCoords: botXY, gridSize: undefined,
dispatch, openedSavedGarden dispatch, openedSavedGarden
}) : () => { }; }) : () => { };
return <button className="fb-button gray" disabled={!botXY} onClick={click}> return <button className="fb-button gray no-float"
disabled={!botXY} onClick={click}>
{t("Add plant at current FarmBot location {{coordinate}}", {t("Add plant at current FarmBot location {{coordinate}}",
{ coordinate: botXYLabel })} { coordinate: botXYLabel })}
</button>; </button>;

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import { history as routeHistory } from "../../history"; import { history as routeHistory } from "../../history";
import { last, trim } from "lodash"; import { last, trim } from "lodash";
import { Link } from "../../link"; import { Link } from "../../link";
@ -81,7 +80,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
{props.linkTo && {props.linkTo &&
<Link to={props.linkTo}> <Link to={props.linkTo}>
<div className={`fb-button ${TAB_COLOR[props.panel || Panel.Plants]}`}> <div className={`fb-button ${TAB_COLOR[props.panel || Panel.Plants]}`}>
<i className="fa fa-2x fa-plus" title={props.title} /> <i className="fa fa-plus" title={props.title} />
</div> </div>
</Link>} </Link>}
</div>; </div>;

View File

@ -27,6 +27,7 @@ export interface PlantPanelProps {
export const PLANT_STAGES: DropDownItem[] = [ export const PLANT_STAGES: DropDownItem[] = [
{ value: "planned", label: t("Planned") }, { value: "planned", label: t("Planned") },
{ value: "planted", label: t("Planted") }, { value: "planted", label: t("Planted") },
{ value: "sprouted", label: t("Sprouted") },
{ value: "harvested", label: t("Harvested") }, { value: "harvested", label: t("Harvested") },
]; ];
@ -43,6 +44,10 @@ export const PLANT_STAGES_DDI = {
label: PLANT_STAGES[2].label, label: PLANT_STAGES[2].label,
value: PLANT_STAGES[2].value value: PLANT_STAGES[2].value
}, },
[PLANT_STAGES[3].value]: {
label: PLANT_STAGES[3].label,
value: PLANT_STAGES[3].value
},
}; };
interface EditPlantProperty { interface EditPlantProperty {

View File

@ -23,6 +23,7 @@ describe("NavBar", () => {
getConfigValue: jest.fn(), getConfigValue: jest.fn(),
tour: undefined, tour: undefined,
device: fakeDevice(), device: fakeDevice(),
autoSync: false,
}); });
it("has correct parent classname", () => { it("has correct parent classname", () => {

View File

@ -3,22 +3,22 @@ import { SyncButton } from "../sync_button";
import { bot } from "../../__test_support__/fake_state/bot"; import { bot } from "../../__test_support__/fake_state/bot";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SyncButtonProps } from "../interfaces"; import { SyncButtonProps } from "../interfaces";
import { SyncStatus } from "farmbot";
describe("<SyncButton/>", function () { describe("<SyncButton/>", function () {
const fakeProps = (): SyncButtonProps => { const fakeProps = (): SyncButtonProps => ({
return { dispatch: jest.fn(),
dispatch: jest.fn(), bot: bot,
bot: bot, consistent: true,
consistent: true, autoSync: false,
}; });
};
it("is gray when inconsistent", () => { it("is gray when inconsistent", () => {
const p = fakeProps(); const p = fakeProps();
p.consistent = false; p.consistent = false;
p.bot.hardware.informational_settings.sync_status = "sync_now"; p.bot.hardware.informational_settings.sync_status = "sync_now";
const result = shallow(<SyncButton {...p} />); const result = shallow(<SyncButton {...p} />);
expect(result.hasClass("gray")).toBeTruthy(); expect(result.hasClass("pseudo-disabled")).toBeTruthy();
}); });
it("is gray when disconnected", () => { it("is gray when disconnected", () => {
@ -26,16 +26,16 @@ describe("<SyncButton/>", function () {
p.consistent = false; p.consistent = false;
p.bot.hardware.informational_settings.sync_status = "unknown"; p.bot.hardware.informational_settings.sync_status = "unknown";
const result = shallow(<SyncButton {...p} />); const result = shallow(<SyncButton {...p} />);
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(); const p = fakeProps();
// tslint:disable-next-line:no-any // tslint:disable-next-line:no-any
p.bot.hardware.informational_settings.sync_status = "new" as any; p.bot.hardware.informational_settings.sync_status = "new" as any;
const result = shallow(<SyncButton {...p} />); const result = shallow(<SyncButton {...p} />);
expect(result.text()).toContain("new"); expect(result.text()).toContain("new");
expect(result.hasClass("gray")).toBeTruthy(); expect(result.hasClass("pseudo-disabled")).toBeTruthy();
}); });
it("syncs when clicked", () => { it("syncs when clicked", () => {
@ -58,4 +58,23 @@ describe("<SyncButton/>", function () {
const result = shallow(<SyncButton {...p} />); const result = shallow(<SyncButton {...p} />);
expect(result.find(".btn-spinner").length).toEqual(1); 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(<SyncButton {...p} />);
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");
});
}); });

View File

@ -44,6 +44,7 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
return <SyncButton return <SyncButton
bot={this.props.bot} bot={this.props.bot}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
autoSync={this.props.autoSync}
consistent={this.props.consistent} />; consistent={this.props.consistent} />;
} }
@ -98,10 +99,11 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
<div className="nav-right"> <div className="nav-right">
<div className="menu-popover"> <div className="menu-popover">
<Popover <Popover
portalClassName={"nav-right"}
popoverClassName={"menu-popover"}
position={Position.BOTTOM_RIGHT} position={Position.BOTTOM_RIGHT}
isOpen={accountMenuOpen} isOpen={accountMenuOpen}
onClose={this.close("accountMenuOpen")} onClose={this.close("accountMenuOpen")}>
usePortal={false}>
<div className="nav-name" <div className="nav-name"
onClick={this.toggle("accountMenuOpen")}> onClick={this.toggle("accountMenuOpen")}>
{firstName} {firstName}

View File

@ -8,6 +8,7 @@ export interface SyncButtonProps {
bot: BotState; bot: BotState;
consistent: boolean; consistent: boolean;
onClick?: () => void; onClick?: () => void;
autoSync: boolean;
} }
export interface NavBarProps { export interface NavBarProps {
@ -20,6 +21,7 @@ export interface NavBarProps {
getConfigValue: GetWebAppConfigValue; getConfigValue: GetWebAppConfigValue;
tour: string | undefined; tour: string | undefined;
device: TaggedDevice; device: TaggedDevice;
autoSync: boolean;
} }
export interface NavBarState { export interface NavBarState {

View File

@ -1,45 +1,50 @@
import * as React from "react"; import * as React from "react";
import { SyncStatus } from "farmbot/dist"; import { SyncStatus } from "farmbot/dist";
import { SyncButtonProps } from "./interfaces"; import { SyncButtonProps } from "./interfaces";
import { sync } from "../devices/actions"; import { sync } from "../devices/actions";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
const GRAY = "pseudo-disabled";
const COLOR_MAPPING: Record<SyncStatus, string> = { const COLOR_MAPPING: Record<SyncStatus, string> = {
"synced": "green", "synced": "green",
"sync_now": "yellow", "sync_now": "yellow",
"syncing": "yellow", "syncing": "yellow",
"sync_error": "red", "sync_error": "red",
"booting": "gray", "booting": GRAY,
"maintenance": "gray", "maintenance": GRAY,
"unknown": "gray" "unknown": GRAY
}; };
const TEXT_MAPPING: () => Record<SyncStatus, string> = () => ({ const TEXT_MAPPING = (autoSync: boolean): Record<SyncStatus, string> => ({
"synced": t("SYNCED"), "synced": autoSync ? t("Synced") : t("SYNCED"),
"sync_now": t("SYNC NOW"), "sync_now": autoSync ? t("Syncing...") : t("SYNC NOW"),
"syncing": t("SYNCING"), "syncing": autoSync ? t("Syncing...") : t("SYNCING"),
"sync_error": t("SYNC ERROR"), "sync_error": autoSync ? t("Sync error") : t("SYNC ERROR"),
"booting": t("UNKNOWN"), "booting": autoSync ? t("Sync unknown") : t("UNKNOWN"),
"unknown": t("UNKNOWN"), "unknown": autoSync ? t("Sync unknown") : t("UNKNOWN"),
"maintenance": t("UNKNOWN") "maintenance": autoSync ? t("Sync unknown") : t("UNKNOWN")
}); });
/** Animation during syncing action */ /** Animation during syncing action */
const spinner = <span className="btn-spinner sync" />; const spinner = <span className="btn-spinner sync" />;
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 { sync_status } = bot.hardware.informational_settings;
const syncStatus = sync_status || "unknown"; const syncStatus = sync_status || "unknown";
const normalColor = COLOR_MAPPING[syncStatus] || "gray"; const normalColor = COLOR_MAPPING[syncStatus] || GRAY;
const color = (!consistent && (syncStatus === "sync_now")) const color = (!consistent && (syncStatus === "sync_now"))
? "gray" ? GRAY
: normalColor; : normalColor;
const text = TEXT_MAPPING()[syncStatus] || syncStatus.replace("_", " "); const text = TEXT_MAPPING(autoSync)[syncStatus] || syncStatus.replace("_", " ");
const spinnerEl = (syncStatus === "syncing") ? spinner : ""; const spinnerEl = (syncStatus === "syncing") ? spinner : "";
const className = autoSync
? "nav-sync fb-button auto-sync"
: `nav-sync ${color} fb-button`;
return <button return <button
className={`nav-sync ${color} fb-button`} className={className}
onClick={() => dispatch(sync())}> onClick={() => dispatch(sync())}>
{text} {spinnerEl} {text} {spinnerEl}
</button>; </button>;

View File

@ -1,23 +1,18 @@
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({ connect: jest.fn() }));
connect: jest.fn()
}));
jest.mock("../../history", () => ({ jest.mock("../../history", () => ({
push: () => jest.fn(), push: () => jest.fn(),
history: { history: { getCurrentLocation: () => ({ pathname: "" }) }
getCurrentLocation: () => ({ pathname: "" })
}
})); }));
import * as React from "react"; import * as React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { Regimens } from "../index"; import { Regimens, RegimenBackButtonProps, RegimenBackButton } from "../index";
import { Props } from "../interfaces"; import { Props } from "../interfaces";
import { bot } from "../../__test_support__/fake_state/bot"; import { bot } from "../../__test_support__/fake_state/bot";
import { auth } from "../../__test_support__/fake_state/token"; import { auth } from "../../__test_support__/fake_state/token";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { fakeRegimen } from "../../__test_support__/fake_state/resources"; import { fakeRegimen } from "../../__test_support__/fake_state/resources";
import { clickButton } from "../../__test_support__/helpers";
import { Actions } from "../../constants"; import { Actions } from "../../constants";
describe("<Regimens />", () => { describe("<Regimens />", () => {
@ -43,7 +38,7 @@ describe("<Regimens />", () => {
it("renders", () => { it("renders", () => {
const wrapper = mount(<Regimens {...fakeProps()} />); const wrapper = mount(<Regimens {...fakeProps()} />);
["Regimens", "Regimen Editor", "Scheduler"].map(string => ["Regimens", "Edit Regimen", "Scheduler"].map(string =>
expect(wrapper.text()).toContain(string)); expect(wrapper.text()).toContain(string));
}); });
@ -60,12 +55,19 @@ describe("<Regimens />", () => {
const wrapper = mount(<Regimens {...p} />); const wrapper = mount(<Regimens {...p} />);
expect(wrapper.html()).toContain("inserting-item"); expect(wrapper.html()).toContain("inserting-item");
}); });
});
describe("<SequenceBackButton />", () => {
const fakeProps = (): RegimenBackButtonProps => ({
dispatch: jest.fn(),
className: "",
});
it("returns to regimen", () => { it("returns to regimen", () => {
const p = fakeProps(); const p = fakeProps();
p.schedulerOpen = true; p.className = "inserting-item";
const wrapper = mount(<Regimens {...p} />); const wrapper = mount(<RegimenBackButton {...p} />);
clickButton(wrapper, 0, "back to regimen"); wrapper.find("i").simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({ expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SET_SCHEDULER_STATE, payload: false type: Actions.SET_SCHEDULER_STATE, payload: false
}); });
@ -73,9 +75,9 @@ describe("<Regimens />", () => {
it("returns to regimen list", () => { it("returns to regimen list", () => {
const p = fakeProps(); const p = fakeProps();
p.schedulerOpen = false; p.className = "";
const wrapper = mount(<Regimens {...p} />); const wrapper = mount(<RegimenBackButton {...p} />);
clickButton(wrapper, 0, "back to regimens"); wrapper.find("i").simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({ expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_REGIMEN, payload: undefined type: Actions.SELECT_REGIMEN, payload: undefined
}); });

View File

@ -1,6 +1,5 @@
import { isNaN, isNumber } from "lodash"; import { isNaN, isNumber } from "lodash";
import { error, warning, success } from "farmbot-toastr";
import { error, warning } from "farmbot-toastr";
import { ReduxAction, Thunk } from "../../redux/interfaces"; import { ReduxAction, Thunk } from "../../redux/interfaces";
import { ToggleDayParams } from "./interfaces"; import { ToggleDayParams } from "./interfaces";
import { findSequence, findRegimen } from "../../resources/selectors"; import { findSequence, findRegimen } from "../../resources/selectors";
@ -93,6 +92,7 @@ export function commitBulkEditor(): Thunk {
clonedRegimen.body = mergeDeclarations(varData, regimen.body.body); clonedRegimen.body = mergeDeclarations(varData, regimen.body.body);
console.log(JSON.stringify(clonedRegimen.body, undefined, 2)); console.log(JSON.stringify(clonedRegimen.body, undefined, 2));
dispatch(overwrite(regimen, clonedRegimen)); dispatch(overwrite(regimen, clonedRegimen));
success(t("Item(s) added."));
} else { } else {
return error(t("No day(s) selected.")); return error(t("No day(s) selected."));
} }

View File

@ -7,7 +7,6 @@ import {
BlurableInput, Row, Col, FBSelect, DropDownItem, NULL_CHOICE BlurableInput, Row, Col, FBSelect, DropDownItem, NULL_CHOICE
} from "../../ui/index"; } from "../../ui/index";
import moment from "moment"; import moment from "moment";
import { isString } from "lodash"; import { isString } from "lodash";
import { betterCompact, bail } from "../../util"; import { betterCompact, bail } from "../../util";
import { msToTime, timeToMs } from "./utils"; import { msToTime, timeToMs } from "./utils";
@ -69,7 +68,7 @@ export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
render() { render() {
const { dispatch, weeks, sequences } = this.props; const { dispatch, weeks, sequences } = this.props;
const active = !!(sequences && sequences.length); const active = !!(sequences && sequences.length);
return <div> return <div className="bulk-scheduler-content">
<AddButton <AddButton
active={active} active={active}
click={() => dispatch(commitBulkEditor())} /> click={() => dispatch(commitBulkEditor())} />

View File

@ -12,18 +12,19 @@ import { t } from "../i18next_wrapper";
import { ToolTips, Actions } from "../constants"; import { ToolTips, Actions } from "../constants";
import { unselectRegimen } from "./actions"; 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"); const schedulerOpen = props.className.includes("inserting-item");
return <Row> return <i
<button className={`back-to-regimens fa fa-arrow-left ${props.className}`}
className={`back-to-regimens fb-button gray ${props.className}`} onClick={() => schedulerOpen
onClick={() => schedulerOpen ? props.dispatch({ type: Actions.SET_SCHEDULER_STATE, payload: false })
? props.dispatch({ type: Actions.SET_SCHEDULER_STATE, payload: false }) : props.dispatch(unselectRegimen())}
: props.dispatch(unselectRegimen())}> title={schedulerOpen ? t("back to regimen") : t("back to regimens")} />;
<i className="fa fa-arrow-left" />
{schedulerOpen ? t("back to regimen") : t("back to regimens")}
</button>
</Row>;
}; };
@connect(mapStateToProps) @connect(mapStateToProps)
@ -39,7 +40,6 @@ export class Regimens extends React.Component<Props, {}> {
const insertingItem = this.props.schedulerOpen ? "inserting-item" : ""; const insertingItem = this.props.schedulerOpen ? "inserting-item" : "";
const activeClasses = [regimenOpen, insertingItem].join(" "); const activeClasses = [regimenOpen, insertingItem].join(" ");
return <Page className="regimen-page"> return <Page className="regimen-page">
<RegimenBackButton className={activeClasses} dispatch={this.props.dispatch} />
<Row> <Row>
<LeftPanel <LeftPanel
className={`regimen-list-panel ${activeClasses}`} className={`regimen-list-panel ${activeClasses}`}
@ -53,7 +53,10 @@ export class Regimens extends React.Component<Props, {}> {
</LeftPanel> </LeftPanel>
<CenterPanel <CenterPanel
className={`regimen-editor-panel ${activeClasses}`} className={`regimen-editor-panel ${activeClasses}`}
title={t("Regimen Editor")} backButton={<RegimenBackButton
className={activeClasses}
dispatch={this.props.dispatch} />}
title={regimenOpen ? t("Edit Regimen") : t("Regimen Editor")}
helpText={t(ToolTips.REGIMEN_EDITOR)} helpText={t(ToolTips.REGIMEN_EDITOR)}
width={5}> width={5}>
<RegimenEditor <RegimenEditor
@ -66,7 +69,10 @@ export class Regimens extends React.Component<Props, {}> {
</CenterPanel> </CenterPanel>
<RightPanel <RightPanel
className={`bulk-scheduler ${activeClasses}`} className={`bulk-scheduler ${activeClasses}`}
title={t("Scheduler")} backButton={<RegimenBackButton
className={activeClasses}
dispatch={this.props.dispatch} />}
title={insertingItem ? t("Add Regimen Item") : t("Scheduler")}
helpText={t(ToolTips.BULK_SCHEDULER)} helpText={t(ToolTips.BULK_SCHEDULER)}
show={!!regimenSelected} width={4}> show={!!regimenSelected} width={4}>
<BulkScheduler <BulkScheduler

View File

@ -1,8 +1,9 @@
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount } from "enzyme";
import { RegimensList } from "../index"; import { RegimensList } from "../index";
import { RegimensListProps } from "../../interfaces"; import { RegimensListProps } from "../../interfaces";
import { fakeRegimen } from "../../../__test_support__/fake_state/resources"; import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
import { inputEvent } from "../../../__test_support__/fake_input_event";
describe("<RegimensList />", () => { describe("<RegimensList />", () => {
function fakeProps(): RegimensListProps { function fakeProps(): RegimensListProps {
@ -24,9 +25,8 @@ describe("<RegimensList />", () => {
}); });
it("sets search term", () => { it("sets search term", () => {
const wrapper = shallow<RegimensList>(<RegimensList {...fakeProps()} />); const wrapper = mount<RegimensList>(<RegimensList {...fakeProps()} />);
wrapper.find("input").simulate("change", wrapper.instance().onChange(inputEvent("term"));
{ currentTarget: { value: "term" } }); expect(wrapper.state().searchTerm).toEqual("term");
expect(wrapper.instance().state.searchTerm).toEqual("term");
}); });
}); });

View File

@ -1,11 +1,31 @@
import * as React from "react"; import * as React from "react";
import { RegimenListItem } from "./regimen_list_item"; import { RegimenListItem } from "./regimen_list_item";
import { AddRegimen } from "./add_button"; import { AddRegimen } from "./add_button";
import { Row, Col } from "../../ui/index"; import { Row, Col } from "../../ui/index";
import { RegimensListProps, RegimensListState } from "../interfaces"; import { RegimensListProps, RegimensListState } from "../interfaces";
import { sortResourcesById } from "../../util"; import { sortResourcesById } from "../../util";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
interface RegimenListHeaderProps {
onChange(e: React.SyntheticEvent<HTMLInputElement>): void;
regimenCount: number;
dispatch: Function;
}
const RegimenListHeader = (props: RegimenListHeaderProps) =>
<div className={"panel-top with-button"}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search"></i>
<input
onChange={props.onChange}
placeholder={t("Search Regimens...")} />
</div>
</div>
<AddRegimen dispatch={props.dispatch} length={props.regimenCount} />
</div>;
export class RegimensList extends export class RegimensList extends
React.Component<RegimensListProps, RegimensListState> { React.Component<RegimensListProps, RegimensListState> {
@ -40,17 +60,22 @@ export class RegimensList extends
} }
render() { render() {
const { dispatch, regimens } = this.props;
return <div> return <div>
<AddRegimen dispatch={dispatch} length={regimens.length} /> <RegimenListHeader
<input dispatch={this.props.dispatch}
onChange={this.onChange} regimenCount={this.props.regimens.length}
placeholder={t("Search Regimens...")} /> onChange={this.onChange} />
<Row> <Row>
<div className="regimen-list"> <EmptyStateWrapper
{this.rows()} notEmpty={this.props.regimens.length > 0}
</div> graphic={EmptyStateGraphic.regimens}
title={t("No Regimens.")}
text={Content.NO_REGIMENS}>
{this.props.regimens.length > 0 &&
<div className="regimen-list">
{this.rows()}
</div>}
</EmptyStateWrapper>
</Row> </Row>
</div>; </div>;
} }

View File

@ -73,6 +73,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
shouldDisplay: jest.fn(), shouldDisplay: jest.fn(),
confirmStepDeletion: false, confirmStepDeletion: false,
menuOpen: false, menuOpen: false,
showPins: true,
}; };
}; };
@ -253,9 +254,13 @@ describe("<SequenceSettingsMenu />", () => {
it("renders settings", () => { it("renders settings", () => {
const wrapper = mount(<SequenceSettingsMenu const wrapper = mount(<SequenceSettingsMenu
dispatch={jest.fn()} dispatch={jest.fn()}
confirmStepDeletion={false} />); confirmStepDeletion={false}
wrapper.find("button").simulate("click"); showPins={false} />);
wrapper.find("button").first().simulate("click");
expect(setWebAppConfigValue).toHaveBeenCalledWith( expect(setWebAppConfigValue).toHaveBeenCalledWith(
BooleanSetting.confirm_step_deletion, true); BooleanSetting.confirm_step_deletion, true);
wrapper.find("button").last().simulate("click");
expect(setWebAppConfigValue).toHaveBeenCalledWith(
"show_pins", true);
}); });
}); });

View File

@ -27,6 +27,7 @@ describe("<SequenceEditorMiddle/>", () => {
shouldDisplay: jest.fn(), shouldDisplay: jest.fn(),
confirmStepDeletion: false, confirmStepDeletion: false,
menuOpen: false, menuOpen: false,
showPins: true,
}; };
} }

View File

@ -24,6 +24,7 @@ import { buildResourceIndex } from "../../__test_support__/resource_index_builde
import { resourceReducer } from "../../resources/reducer"; import { resourceReducer } from "../../resources/reducer";
import { resourceReady } from "../../sync/actions"; import { resourceReady } from "../../sync/actions";
import { setActiveSequenceByName } from "../set_active_sequence_by_name"; import { setActiveSequenceByName } from "../set_active_sequence_by_name";
import { inputEvent } from "../../__test_support__/fake_input_event";
describe("<SequencesList />", () => { describe("<SequencesList />", () => {
const fakeSequences = () => { const fakeSequences = () => {
@ -105,13 +106,8 @@ describe("<SequencesList />", () => {
it("sets search term", () => { it("sets search term", () => {
const wrapper = shallow<SequencesList>(<SequencesList {...fakeProps()} />); const wrapper = shallow<SequencesList>(<SequencesList {...fakeProps()} />);
expect(wrapper.instance().state.searchTerm).toEqual(""); expect(wrapper.state().searchTerm).toEqual("");
const searchField = wrapper.find("input").first(); wrapper.instance().onChange(inputEvent("search this"));
expect(searchField.props().placeholder)
.toEqual("Search Sequences...");
searchField.simulate("change", {
currentTarget: { value: "search this" }
});
expect(wrapper.instance().state.searchTerm).toEqual("search this"); expect(wrapper.instance().state.searchTerm).toEqual("search this");
}); });

View File

@ -1,6 +1,4 @@
jest.mock("react-redux", () => ({ jest.mock("react-redux", () => ({ connect: jest.fn() }));
connect: jest.fn()
}));
jest.mock("../../history", () => ({ jest.mock("../../history", () => ({
push: jest.fn(), push: jest.fn(),
@ -8,7 +6,7 @@ jest.mock("../../history", () => ({
})); }));
import * as React from "react"; import * as React from "react";
import { Sequences } from "../sequences"; import { Sequences, SequenceBackButtonProps, SequenceBackButton } from "../sequences";
import { shallow, mount } from "enzyme"; import { shallow, mount } from "enzyme";
import { Props } from "../interfaces"; import { Props } from "../interfaces";
import { import {
@ -39,12 +37,13 @@ describe("<Sequences/>", () => {
confirmStepDeletion: false, confirmStepDeletion: false,
menuOpen: false, menuOpen: false,
stepIndex: undefined, stepIndex: undefined,
showPins: true,
}); });
it("renders", () => { it("renders", () => {
const wrapper = shallow(<Sequences {...fakeProps()} />); const wrapper = shallow(<Sequences {...fakeProps()} />);
expect(wrapper.html()).toContain("Sequences"); 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(ToolTips.SEQUENCE_EDITOR);
expect(wrapper.html()).toContain("Commands"); expect(wrapper.html()).toContain("Commands");
}); });
@ -62,11 +61,18 @@ describe("<Sequences/>", () => {
const wrapper = shallow(<Sequences {...p} />); const wrapper = shallow(<Sequences {...p} />);
expect(wrapper.html()).toContain("inserting-step"); expect(wrapper.html()).toContain("inserting-step");
}); });
});
describe("<SequenceBackButton />", () => {
const fakeProps = (): SequenceBackButtonProps => ({
dispatch: jest.fn(),
className: "",
});
it("goes back", () => { it("goes back", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = mount(<Sequences {...p} />); const wrapper = mount(<SequenceBackButton {...p} />);
wrapper.find("button").first().simulate("click"); wrapper.find("i").first().simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({ expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SELECT_SEQUENCE, payload: undefined type: Actions.SELECT_SEQUENCE, payload: undefined
}); });

View File

@ -47,3 +47,8 @@ export const unselectSequence = () => {
push("/app/sequences"); push("/app/sequences");
return { type: Actions.SELECT_SEQUENCE, payload: undefined }; return { type: Actions.SELECT_SEQUENCE, payload: undefined };
}; };
export const closeCommandMenu = () => ({
type: Actions.SET_SEQUENCE_STEP_POSITION,
payload: undefined,
});

View File

@ -19,6 +19,7 @@ interface AllStepsProps {
farmwareInfo?: FarmwareInfo; farmwareInfo?: FarmwareInfo;
shouldDisplay?: ShouldDisplay; shouldDisplay?: ShouldDisplay;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins?: boolean;
} }
export class AllSteps extends React.Component<AllStepsProps, {}> { export class AllSteps extends React.Component<AllStepsProps, {}> {
@ -54,6 +55,7 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
farmwareInfo, farmwareInfo,
shouldDisplay, shouldDisplay,
confirmStepDeletion: this.props.confirmStepDeletion, confirmStepDeletion: this.props.confirmStepDeletion,
showPins: this.props.showPins,
})} })}
</div> </div>
</StepDragger> </StepDragger>

View File

@ -47,6 +47,7 @@ export interface Props {
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
menuOpen: boolean; menuOpen: boolean;
stepIndex: number | undefined; stepIndex: number | undefined;
showPins: boolean;
} }
export interface SequenceEditorMiddleProps { export interface SequenceEditorMiddleProps {
@ -59,6 +60,7 @@ export interface SequenceEditorMiddleProps {
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
menuOpen: boolean; menuOpen: boolean;
showPins: boolean;
} }
export interface ActiveMiddleProps extends SequenceEditorMiddleProps { export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
@ -75,6 +77,7 @@ export interface SequenceHeaderProps {
variablesCollapsed: boolean; variablesCollapsed: boolean;
toggleVarShow: () => void; toggleVarShow: () => void;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins: boolean;
} }
export type ChannelName = ALLOWED_CHANNEL_NAMES; export type ChannelName = ALLOWED_CHANNEL_NAMES;
@ -202,4 +205,5 @@ export interface StepParams {
farmwareInfo?: FarmwareInfo; farmwareInfo?: FarmwareInfo;
shouldDisplay?: ShouldDisplay; shouldDisplay?: ShouldDisplay;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins?: boolean;
} }

View File

@ -73,15 +73,15 @@ export function locationFormList(resources: ResourceIndex,
return [COORDINATE_DDI()] return [COORDINATE_DDI()]
.concat(additionalItems) .concat(additionalItems)
.concat(heading("Tool")) .concat(heading("Tool"))
.concat(toolDDI)
.concat(group(everyPointDDI("Tool"))) .concat(group(everyPointDDI("Tool")))
.concat(group(everyPointDDI("ToolSlot"))) .concat(group(everyPointDDI("ToolSlot")))
.concat(toolDDI)
.concat(heading("Plant")) .concat(heading("Plant"))
.concat(plantDDI)
.concat(group(everyPointDDI("Plant"))) .concat(group(everyPointDDI("Plant")))
.concat(plantDDI)
.concat(heading("GenericPointer")) .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)" */ /** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */

View File

@ -26,6 +26,7 @@ export class SequenceEditorMiddle
farmwareInfo={this.props.farmwareInfo} farmwareInfo={this.props.farmwareInfo}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
confirmStepDeletion={this.props.confirmStepDeletion} confirmStepDeletion={this.props.confirmStepDeletion}
showPins={this.props.showPins}
menuOpen={this.props.menuOpen} />} menuOpen={this.props.menuOpen} />}
</EmptyStateWrapper>; </EmptyStateWrapper>;
} }

View File

@ -24,6 +24,7 @@ import { ToggleButton } from "../controls/toggle_button";
import { Content } from "../constants"; import { Content } from "../constants";
import { setWebAppConfigValue } from "../config_storage/actions"; import { setWebAppConfigValue } from "../config_storage/actions";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
export const onDrop = export const onDrop =
(dispatch1: Function, sequence: TaggedSequence) => (dispatch1: Function, sequence: TaggedSequence) =>
@ -49,19 +50,32 @@ export const onDrop =
export interface SequenceSettingsMenuProps { export interface SequenceSettingsMenuProps {
dispatch: Function; dispatch: Function;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins: boolean;
} }
export const SequenceSettingsMenu = export const SequenceSettingsMenu =
({ dispatch, confirmStepDeletion }: SequenceSettingsMenuProps) => ({ dispatch, confirmStepDeletion, showPins }: SequenceSettingsMenuProps) =>
<div className="sequence-settings-menu"> <div className="sequence-settings-menu">
<label> <fieldset>
{t("Confirm step deletion")} <label>
</label> {t("Confirm step deletion")}
<Help text={t(Content.CONFIRM_STEP_DELETION)} /> </label>
<ToggleButton <Help text={t(Content.CONFIRM_STEP_DELETION)} requireClick={true} />
toggleValue={confirmStepDeletion} <ToggleButton
toggleAction={() => dispatch(setWebAppConfigValue( toggleValue={confirmStepDeletion}
BooleanSetting.confirm_step_deletion, !confirmStepDeletion))} /> toggleAction={() => dispatch(setWebAppConfigValue(
BooleanSetting.confirm_step_deletion, !confirmStepDeletion))} />
</fieldset>
<fieldset>
<label>
{t("Show pins")}
</label>
<Help text={t(Content.SHOW_PINS)} requireClick={true} />
<ToggleButton
toggleValue={showPins}
toggleAction={() => dispatch(setWebAppConfigValue(
"show_pins" as BooleanConfigKey, !showPins))} />
</fieldset>
</div>; </div>;
interface SequenceBtnGroupProps { interface SequenceBtnGroupProps {
@ -72,11 +86,12 @@ interface SequenceBtnGroupProps {
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
menuOpen: boolean; menuOpen: boolean;
confirmStepDeletion: boolean; confirmStepDeletion: boolean;
showPins: boolean;
} }
const SequenceBtnGroup = ({ const SequenceBtnGroup = ({
dispatch, sequence, syncStatus, resources, shouldDisplay, menuOpen, dispatch, sequence, syncStatus, resources, shouldDisplay, menuOpen,
confirmStepDeletion confirmStepDeletion, showPins
}: SequenceBtnGroupProps) => }: SequenceBtnGroupProps) =>
<div className="button-group"> <div className="button-group">
<SaveBtn status={sequence.specialStatus} <SaveBtn status={sequence.specialStatus}
@ -104,6 +119,7 @@ const SequenceBtnGroup = ({
<i className="fa fa-gear" /> <i className="fa fa-gear" />
<SequenceSettingsMenu <SequenceSettingsMenu
dispatch={dispatch} dispatch={dispatch}
showPins={showPins}
confirmStepDeletion={confirmStepDeletion} /> confirmStepDeletion={confirmStepDeletion} />
</Popover> </Popover>
</div> </div>
@ -140,6 +156,7 @@ const SequenceHeader = (props: SequenceHeaderProps) => {
resources={props.resources} resources={props.resources}
shouldDisplay={props.shouldDisplay} shouldDisplay={props.shouldDisplay}
confirmStepDeletion={props.confirmStepDeletion} confirmStepDeletion={props.confirmStepDeletion}
showPins={props.showPins}
menuOpen={props.menuOpen} /> menuOpen={props.menuOpen} />
<SequenceNameAndColor {...sequenceAndDispatch} /> <SequenceNameAndColor {...sequenceAndDispatch} />
<LocalsList <LocalsList
@ -190,6 +207,7 @@ export class SequenceEditorMiddleActive extends
toggleVarShow={() => toggleVarShow={() =>
this.setState({ variablesCollapsed: !this.state.variablesCollapsed })} this.setState({ variablesCollapsed: !this.state.variablesCollapsed })}
confirmStepDeletion={this.props.confirmStepDeletion} confirmStepDeletion={this.props.confirmStepDeletion}
showPins={this.props.showPins}
menuOpen={this.props.menuOpen} /> menuOpen={this.props.menuOpen} />
<hr /> <hr />
<div className="sequence" id="sequenceDiv" <div className="sequence" id="sequenceDiv"

View File

@ -12,18 +12,22 @@ import { setActiveSequenceByName } from "./set_active_sequence_by_name";
import { LeftPanel, CenterPanel, RightPanel } from "../ui"; import { LeftPanel, CenterPanel, RightPanel } from "../ui";
import { resourceUsageList } from "../resources/in_use"; import { resourceUsageList } from "../resources/in_use";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { unselectSequence } from "./actions"; import { unselectSequence, closeCommandMenu } from "./actions";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
const SequenceBackButton = (props: { dispatch: Function, className: string }) => export interface SequenceBackButtonProps {
<Row> dispatch: Function;
<button className: string;
className={`back-to-sequences fb-button gray ${props.className}`} }
onClick={() => props.dispatch(unselectSequence())}>
<i className="fa fa-arrow-left" /> export const SequenceBackButton = (props: SequenceBackButtonProps) => {
{t("back to sequences")} const insertingStep = props.className.includes("inserting-step");
</button> return <i
</Row>; className={`back-to-sequences fa fa-arrow-left ${props.className}`}
onClick={() => props.dispatch(
insertingStep ? closeCommandMenu() : unselectSequence())}
title={insertingStep ? t("back to sequence") : t("back to sequences")} />;
};
@connect(mapStateToProps) @connect(mapStateToProps)
export class Sequences extends React.Component<Props, {}> { export class Sequences extends React.Component<Props, {}> {
@ -38,7 +42,6 @@ export class Sequences extends React.Component<Props, {}> {
const insertingStep = isNumber(this.props.stepIndex) ? "inserting-step" : ""; const insertingStep = isNumber(this.props.stepIndex) ? "inserting-step" : "";
const activeClasses = [sequenceOpen, insertingStep].join(" "); const activeClasses = [sequenceOpen, insertingStep].join(" ");
return <Page className="sequence-page"> return <Page className="sequence-page">
<SequenceBackButton className={activeClasses} dispatch={this.props.dispatch} />
<Row> <Row>
<LeftPanel <LeftPanel
className={`sequence-list-panel ${activeClasses}`} className={`sequence-list-panel ${activeClasses}`}
@ -53,7 +56,10 @@ export class Sequences extends React.Component<Props, {}> {
</LeftPanel> </LeftPanel>
<CenterPanel <CenterPanel
className={`sequence-editor-panel ${activeClasses}`} className={`sequence-editor-panel ${activeClasses}`}
title={t("Sequence Editor")} backButton={<SequenceBackButton
className={activeClasses}
dispatch={this.props.dispatch} />}
title={sequenceOpen ? t("Edit Sequence") : t("Sequence Editor")}
helpText={t(ToolTips.SEQUENCE_EDITOR)}> helpText={t(ToolTips.SEQUENCE_EDITOR)}>
<SequenceEditorMiddle <SequenceEditorMiddle
syncStatus={this.props.syncStatus} syncStatus={this.props.syncStatus}
@ -64,11 +70,15 @@ export class Sequences extends React.Component<Props, {}> {
farmwareInfo={this.props.farmwareInfo} farmwareInfo={this.props.farmwareInfo}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
confirmStepDeletion={this.props.confirmStepDeletion} confirmStepDeletion={this.props.confirmStepDeletion}
showPins={this.props.showPins}
menuOpen={this.props.menuOpen} /> menuOpen={this.props.menuOpen} />
</CenterPanel> </CenterPanel>
<RightPanel <RightPanel
className={`step-button-cluster-panel ${activeClasses}`} className={`step-button-cluster-panel ${activeClasses}`}
title={t("Commands")} backButton={<SequenceBackButton
className={activeClasses}
dispatch={this.props.dispatch} />}
title={insertingStep ? t("Add Command") : t("Commands")}
helpText={t(ToolTips.SEQUENCE_COMMANDS)} helpText={t(ToolTips.SEQUENCE_COMMANDS)}
show={sequenceSelected}> show={sequenceSelected}>
<StepButtonCluster <StepButtonCluster

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import { push } from "../history"; import { push } from "../history";
import { SequencesListProps, SequencesListState } from "./interfaces"; import { SequencesListProps, SequencesListState } from "./interfaces";
import { sortResourcesById, urlFriendly, lastUrlChunk } from "../util"; import { sortResourcesById, urlFriendly, lastUrlChunk } from "../util";
@ -13,6 +12,7 @@ import { setActiveSequenceByName } from "./set_active_sequence_by_name";
import { UUID, VariableNameSet } from "../resources/interfaces"; import { UUID, VariableNameSet } from "../resources/interfaces";
import { variableList } from "./locals_list/variable_support"; import { variableList } from "./locals_list/variable_support";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic } from "../ui/empty_state_wrapper";
const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean => seq const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean => seq
.body .body
@ -58,6 +58,45 @@ const sequenceList = (props: {
</div>; </div>;
}; };
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<HTMLInputElement>): void;
sequenceCount: number;
dispatch: Function;
}
const SequenceListHeader = (props: SequenceListHeaderProps) =>
<div className={"panel-top with-button"}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search"></i>
<input
onChange={props.onChange}
placeholder={t("Search Sequences...")} />
</div>
</div>
<button
className="fb-button green add"
onClick={() => {
const newSequence = emptySequenceBody(props.sequenceCount);
props.dispatch(init("Sequence", newSequence));
push("/app/sequences/" + urlFriendly(newSequence.name));
setActiveSequenceByName();
}}>
<i className="fa fa-plus" />
</button>
</div>;
export class SequencesList extends export class SequencesList extends
React.Component<SequencesListProps, SequencesListState> { React.Component<SequencesListProps, SequencesListState> {
@ -68,41 +107,28 @@ export class SequencesList extends
onChange = (e: React.SyntheticEvent<HTMLInputElement>) => onChange = (e: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ searchTerm: e.currentTarget.value }); 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() { render() {
const { sequences, dispatch, resourceUsage, sequenceMetas } = this.props; const { sequences, dispatch, resourceUsage, sequenceMetas } = this.props;
const searchTerm = this.state.searchTerm.toLowerCase(); const searchTerm = this.state.searchTerm.toLowerCase();
return <div> return <div>
<button <SequenceListHeader
className="fb-button green add" dispatch={dispatch}
onClick={() => { sequenceCount={this.props.sequences.length}
const newSequence = this.emptySequenceBody(); onChange={this.onChange} />
dispatch(init("Sequence", newSequence));
push("/app/sequences/" + urlFriendly(newSequence.name));
setActiveSequenceByName();
}}>
<i className="fa fa-plus" />
</button>
<input
onChange={this.onChange}
placeholder={t("Search Sequences...")} />
<Row> <Row>
<Col xs={12}> <Col xs={12}>
<div className="sequence-list"> <EmptyStateWrapper
{sortResourcesById(sequences) notEmpty={sequences.length > 0}
.filter(filterFn(searchTerm)) graphic={EmptyStateGraphic.sequences}
.map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))} title={t("No Sequences.")}
</div> text={Content.NO_SEQUENCES}>
{sequences.length > 0 &&
<div className="sequence-list">
{sortResourcesById(sequences)
.filter(filterFn(searchTerm))
.map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))}
</div>}
</EmptyStateWrapper>
</Col> </Col>
</Row> </Row>
</div>; </div>;

View File

@ -11,10 +11,11 @@ import {
} from "../util"; } from "../util";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { getWebAppConfigValue } from "../config_storage/actions"; import { getWebAppConfigValue } from "../config_storage/actions";
import { getFirmwareConfig, getWebAppConfig } from "../resources/getters"; import { getFirmwareConfig } from "../resources/getters";
import { Farmwares } from "../farmware/interfaces"; import { Farmwares } from "../farmware/interfaces";
import { manifestInfo } from "../farmware/generate_manifest_info"; import { manifestInfo } from "../farmware/generate_manifest_info";
import { DevSettings } from "../account/dev/dev_support"; import { DevSettings } from "../account/dev/dev_support";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
export function mapStateToProps(props: Everything): Props { export function mapStateToProps(props: Everything): Props {
const uuid = props.resources.consumers.sequences.current; 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 farmwareNames = Object.values(farmwares).map(fw => fw.name);
const { firstPartyFarmwareNames } = props.resources.consumers.farmware; const { firstPartyFarmwareNames } = props.resources.consumers.farmware;
const conf = getWebAppConfig(props.resources.index); const getConfig = getWebAppConfigValue(() => props);
const showFirstPartyFarmware = !!(conf && conf.body.show_first_party_farmware); const showFirstPartyFarmware =
!!getConfig(BooleanSetting.show_first_party_farmware);
const farmwareConfigs: FarmwareConfigs = {}; const farmwareConfigs: FarmwareConfigs = {};
Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config); Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config);
const installedOsVersion = determineInstalledOsVersion( const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index)); props.bot, maybeGetDevice(props.resources.index));
const confirmStepDeletion = const confirmStepDeletion = !!getConfig(BooleanSetting.confirm_step_deletion);
!!getWebAppConfigValue(() => props)(BooleanSetting.confirm_step_deletion); const showPins = !!getConfig("show_pins" as BooleanConfigKey);
const fbosVersionOverride = DevSettings.overriddenFbosVersion(); const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc( const shouldDisplay = shouldDisplayFunc(
@ -97,5 +99,6 @@ export function mapStateToProps(props: Everything): Props {
confirmStepDeletion, confirmStepDeletion,
menuOpen: props.resources.consumers.sequences.menuOpen, menuOpen: props.resources.consumers.sequences.menuOpen,
stepIndex: props.resources.consumers.sequences.stepIndex, stepIndex: props.resources.consumers.sequences.stepIndex,
showPins,
}; };
} }

View File

@ -2,11 +2,10 @@ import * as React from "react";
import { SequenceBodyItem as Step, TaggedSequence } from "farmbot"; import { SequenceBodyItem as Step, TaggedSequence } from "farmbot";
import { error } from "farmbot-toastr"; import { error } from "farmbot-toastr";
import { StepDragger, NULL_DRAGGER_ID } from "../../draggable/step_dragger"; import { StepDragger, NULL_DRAGGER_ID } from "../../draggable/step_dragger";
import { pushStep } from "../actions"; import { pushStep, closeCommandMenu } from "../actions";
import { StepButtonParams } from "../interfaces"; import { StepButtonParams } from "../interfaces";
import { Col } from "../../ui/index"; import { Col } from "../../ui/index";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { Actions } from "../../constants";
export const stepClick = export const stepClick =
(dispatch: Function, (dispatch: Function,
@ -17,10 +16,7 @@ export const stepClick =
seq seq
? pushStep(step, dispatch, seq, index) ? pushStep(step, dispatch, seq, index)
: error(t("Select a sequence first")); : error(t("Select a sequence first"));
dispatch({ dispatch(closeCommandMenu());
type: Actions.SET_SEQUENCE_STEP_POSITION,
payload: undefined,
});
}; };
export function StepButton({ children, step, color, dispatch, current, index }: export function StepButton({ children, step, color, dispatch, current, index }:

View File

@ -117,7 +117,7 @@ describe("Pin and Peripheral support files", () => {
s.body.label = "not displayed"; s.body.label = "not displayed";
p.body.label = "not displayed"; p.body.label = "not displayed";
const ri = buildResourceIndex([s, p]); 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"); expect(JSON.stringify(result)).not.toContain("not displayed");
}); });
@ -127,7 +127,7 @@ describe("Pin and Peripheral support files", () => {
s.body.label = "not displayed"; s.body.label = "not displayed";
p.body.label = "displayed peripheral"; p.body.label = "displayed peripheral";
const ri = buildResourceIndex([s, p]); 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)).toContain("displayed peripheral");
expect(JSON.stringify(result)).not.toContain("not displayed"); expect(JSON.stringify(result)).not.toContain("not displayed");
}); });
@ -140,7 +140,7 @@ describe("Pin and Peripheral support files", () => {
s.body.label = "not displayed"; s.body.label = "not displayed";
p.body.label = "not displayed"; p.body.label = "not displayed";
const ri = buildResourceIndex([s, p]); 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"); expect(JSON.stringify(result)).not.toContain("not displayed");
}); });
@ -150,7 +150,7 @@ describe("Pin and Peripheral support files", () => {
s.body.label = "displayed sensor"; s.body.label = "displayed sensor";
p.body.label = "displayed peripheral"; p.body.label = "displayed peripheral";
const ri = buildResourceIndex([s, p]); 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 sensor");
expect(JSON.stringify(result)).toContain("displayed peripheral"); expect(JSON.stringify(result)).toContain("displayed peripheral");
}); });

View File

@ -101,18 +101,20 @@ export function pinDropdowns(
return [PIN_HEADING, ...PIN_RANGE.map(pinNumber2DropDown(valueFormat))]; return [PIN_HEADING, ...PIN_RANGE.map(pinNumber2DropDown(valueFormat))];
} }
export const pinsAsDropDownsWritePin = export const pinsAsDropDownsWritePin = (
(input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [ input: ResourceIndex, shouldDisplay: ShouldDisplay, showPins: boolean
): DropDownItem[] => [
...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []), ...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []),
...(shouldDisplay(Feature.rpi_led_control) ? boxLedsAsDropDowns() : []), ...(shouldDisplay(Feature.rpi_led_control) ? boxLedsAsDropDowns() : []),
...pinDropdowns(n => n), ...(showPins ? pinDropdowns(n => n) : []),
]; ];
export const pinsAsDropDownsReadPin = export const pinsAsDropDownsReadPin = (
(input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [ input: ResourceIndex, shouldDisplay: ShouldDisplay, showPins: boolean
): DropDownItem[] => [
...(shouldDisplay(Feature.named_pins) ? sensorsAsDropDowns(input) : []), ...(shouldDisplay(Feature.named_pins) ? sensorsAsDropDowns(input) : []),
...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []), ...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []),
...pinDropdowns(n => n), ...(showPins ? pinDropdowns(n => n) : []),
]; ];
const TYPE_MAPPING: Record<AllowedPinTypes, PinGroupName | BoxLed> = { const TYPE_MAPPING: Record<AllowedPinTypes, PinGroupName | BoxLed> = {

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import { StepInputBox } from "../inputs/step_input_box"; import { StepInputBox } from "../inputs/step_input_box";
import { StepParams } from "../interfaces"; import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
@ -48,7 +47,7 @@ export function TileReadPin(props: StepParams) {
selectedItem={celery2DropDown(pin_number, props.resources)} selectedItem={celery2DropDown(pin_number, props.resources)}
onChange={setArgsDotPinNumber(props)} onChange={setArgsDotPinNumber(props)}
list={pinsAsDropDownsReadPin(props.resources, list={pinsAsDropDownsReadPin(props.resources,
shouldDisplay || (() => false))} /> shouldDisplay || (() => false), !!props.showPins)} />
</Col> </Col>
<PinMode {...props} /> <PinMode {...props} />
<Col xs={6} md={3}> <Col xs={6} md={3}>

View File

@ -56,7 +56,7 @@ export function TileWritePin(props: StepParams) {
selectedItem={celery2DropDown(pin_number, props.resources)} selectedItem={celery2DropDown(pin_number, props.resources)}
onChange={setArgsDotPinNumber(props)} onChange={setArgsDotPinNumber(props)}
list={pinsAsDropDownsWritePin(props.resources, list={pinsAsDropDownsWritePin(props.resources,
shouldDisplay || (() => false))} /> shouldDisplay || (() => false), !!props.showPins)} />
</Col> </Col>
<PinMode {...props} /> <PinMode {...props} />
<Col xs={6} md={3}> <Col xs={6} md={3}>

View File

@ -9,11 +9,13 @@ interface CenterProps {
helpText: string; helpText: string;
width?: number; width?: number;
docPage?: DocSlug; docPage?: DocSlug;
backButton?: React.ReactNode;
} }
export function CenterPanel(props: CenterProps) { export function CenterPanel(props: CenterProps) {
return <Col sm={props.width || 6} lg={6}> return <Col sm={props.width || 6} lg={6}>
<div className={props.className}> <div className={props.className}>
{props.backButton}
<h3> <h3>
<i>{t(props.title)}</i> <i>{t(props.title)}</i>
</h3> </h3>

View File

@ -3,12 +3,14 @@ import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core";
interface HelpProps { interface HelpProps {
text: string; text: string;
requireClick?: boolean;
} }
export function Help(props: HelpProps) { export function Help(props: HelpProps) {
return <Popover return <Popover
position={Position.LEFT_TOP} position={Position.LEFT_TOP}
interactionKind={PopoverInteractionKind.HOVER} interactionKind={props.requireClick
? PopoverInteractionKind.CLICK : PopoverInteractionKind.HOVER}
popoverClassName={"help"} > popoverClassName={"help"} >
<i className="fa fa-question-circle help-icon"></i> <i className="fa fa-question-circle help-icon"></i>
<div>{props.text}</div> <div>{props.text}</div>

View File

@ -9,12 +9,14 @@ interface RightPanelProps {
helpText: string; helpText: string;
show: Boolean | undefined; show: Boolean | undefined;
width?: number; width?: number;
backButton?: React.ReactNode;
} }
export function RightPanel(props: RightPanelProps) { export function RightPanel(props: RightPanelProps) {
return <Col sm={props.width || 3} lg={3}> return <Col sm={props.width || 3} lg={3}>
{props.show && {props.show &&
<div className={props.className}> <div className={props.className}>
{props.backButton}
<h3> <h3>
<i>{t(props.title)}</i> <i>{t(props.title)}</i>
</h3> </h3>