misc UI fixes
parent
8c053b2388
commit
f303c27ce5
|
@ -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
|
|
@ -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
|
|
@ -2,7 +2,8 @@ jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
|||
|
||||
let mockPath = "";
|
||||
jest.mock("../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
|
@ -39,6 +40,7 @@ const fakeProps = (): AppProps => {
|
|||
getConfigValue: jest.fn(),
|
||||
tour: undefined,
|
||||
resources: buildResourceIndex().index,
|
||||
autoSync: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => {
|
|||
Object.defineProperty(window.location, "reload", { value: jest.fn() });
|
||||
it("basically just initializes stuff", () => {
|
||||
const val = fetchLabFeatures(jest.fn());
|
||||
expect(val.length).toBe(10);
|
||||
expect(val.length).toBe(9);
|
||||
expect(val[0].value).toBeFalsy();
|
||||
const { callback } = val[0];
|
||||
if (callback) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { Content } from "../../constants";
|
||||
import { VirtualTrail } from "../../farm_designer/map/layers/farmbot/bot_trail";
|
||||
|
@ -33,12 +32,6 @@ export const fetchLabFeatures =
|
|||
displayInvert: true,
|
||||
callback: () => window.location.reload()
|
||||
},
|
||||
{
|
||||
name: t("Confirm Sequence step deletion"),
|
||||
description: t(Content.CONFIRM_STEP_DELETION),
|
||||
storageKey: BooleanSetting.confirm_step_deletion,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Hide Webcam widget"),
|
||||
description: t(Content.HIDE_WEBCAM_WIDGET),
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { HotKeys } from "./hotkeys";
|
||||
import { ControlsPopup } from "./controls_popup";
|
||||
import { Content } from "./constants";
|
||||
import { validBotLocationData, validFwConfig } from "./util";
|
||||
import { validBotLocationData, validFwConfig, validFbosConfig } from "./util";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
import {
|
||||
|
@ -22,7 +22,7 @@ import {
|
|||
} from "./config_storage/actions";
|
||||
import { takeSortedLogs } from "./logs/state_to_props";
|
||||
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import { getFirmwareConfig } from "./resources/getters";
|
||||
import { getFirmwareConfig, getFbosConfig } from "./resources/getters";
|
||||
import { intersection } from "lodash";
|
||||
import { t } from "./i18next_wrapper";
|
||||
import { ResourceIndex } from "./resources/interfaces";
|
||||
|
@ -47,10 +47,12 @@ export interface AppProps {
|
|||
getConfigValue: GetWebAppConfigValue;
|
||||
tour: string | undefined;
|
||||
resources: ResourceIndex;
|
||||
autoSync: boolean;
|
||||
}
|
||||
|
||||
export function mapStateToProps(props: Everything): AppProps {
|
||||
const webAppConfigValue = getWebAppConfigValue(() => props);
|
||||
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
|
||||
return {
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
|
@ -70,6 +72,7 @@ export function mapStateToProps(props: Everything): AppProps {
|
|||
getConfigValue: webAppConfigValue,
|
||||
tour: props.resources.consumers.help.currentTour,
|
||||
resources: props.resources.index,
|
||||
autoSync: !!(fbosConfig && fbosConfig.auto_sync),
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
@ -125,6 +128,7 @@ export class App extends React.Component<AppProps, {}> {
|
|||
logs={this.props.logs}
|
||||
getConfigValue={this.props.getConfigValue}
|
||||
tour={this.props.tour}
|
||||
autoSync={this.props.autoSync}
|
||||
device={getDeviceAccountSettings(this.props.resources)} />}
|
||||
{syncLoaded && this.props.children}
|
||||
{!(["controls", "account", "regimens"].includes(currentPage)) &&
|
||||
|
|
|
@ -423,6 +423,9 @@ export namespace Content {
|
|||
trim(`Display time using the 24-hour notation,
|
||||
i.e., 23:00 instead of 11:00pm`);
|
||||
|
||||
export const SHOW_PINS =
|
||||
trim(`Show raw pin lists in Read Sensor and Control Peripheral steps.`);
|
||||
|
||||
// Device
|
||||
export const NOT_HTTPS =
|
||||
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
|
||||
|
@ -561,6 +564,9 @@ export namespace Content {
|
|||
trim(`Click one in the Sequences panel to edit, or click "+" to create
|
||||
a new one.`);
|
||||
|
||||
export const NO_SEQUENCES =
|
||||
trim(`Click "+" to create a new sequence.`);
|
||||
|
||||
export const END_DETECTION_DISABLED =
|
||||
trim(`This command will not execute correctly because you do not have
|
||||
encoders or endstops enabled for the chosen axis. Enable endstops or
|
||||
|
@ -574,6 +580,9 @@ export namespace Content {
|
|||
trim(`Click one in the Regimens panel to edit, or click "+" to create
|
||||
a new one.`);
|
||||
|
||||
export const NO_REGIMENS =
|
||||
trim(`Click "+" to create a new regimen.`);
|
||||
|
||||
// Farm Designer
|
||||
export const OUTSIDE_PLANTING_AREA =
|
||||
trim(`Outside of planting area. Plants must be placed within the grid.`);
|
||||
|
|
|
@ -37,7 +37,7 @@ export class Controls extends React.Component<Props, {}> {
|
|||
bot={this.props.bot}
|
||||
peripherals={this.props.peripherals}
|
||||
dispatch={this.props.dispatch}
|
||||
disabled={this.arduinoBusy} />
|
||||
disabled={this.arduinoBusy || !this.botOnline} />
|
||||
|
||||
webcams = () => <WebcamPanel
|
||||
feeds={this.props.feeds}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { DirectionButton } from "./controls/move/direction_button";
|
||||
import { getDevice } from "./device";
|
||||
import { buildDirectionProps } from "./controls/move/direction_axes_props";
|
||||
import { ControlsPopupProps } from "./controls/move/interfaces";
|
||||
import { commandErr } from "./devices/actions";
|
||||
import { mapPanelClassName } from "./farm_designer/map/util";
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
|
@ -24,7 +24,7 @@ export class ControlsPopup
|
|||
const rightLeft = xySwap ? "y" : "x";
|
||||
const upDown = xySwap ? "x" : "y";
|
||||
return <div
|
||||
className={"controls-popup " + isOpen}>
|
||||
className={`controls-popup ${isOpen} ${mapPanelClassName()}`}>
|
||||
<i className="fa fa-crosshairs"
|
||||
onClick={this.toggle("isOpen")} />
|
||||
<div className="controls-popup-menu-outer">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 1075px) {
|
||||
.all-content-wrapper {
|
||||
padding: 11rem 0 0;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -184,6 +184,7 @@
|
|||
margin-bottom: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
color: $dark_gray;
|
||||
text-align: left;
|
||||
&.active {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid $white;
|
||||
|
|
|
@ -2,6 +2,19 @@
|
|||
position: relative;
|
||||
height: 100vh;
|
||||
overflow-y: hidden;
|
||||
.garden-map-legend {
|
||||
@media screen and (max-width: 450px) {
|
||||
&.panel-open {
|
||||
display: none;
|
||||
}
|
||||
&.short-panel {
|
||||
top: 35rem;
|
||||
}
|
||||
&.panel-closed {
|
||||
top: 15rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.farm-designer-map {
|
||||
|
@ -13,6 +26,9 @@
|
|||
&.panel-open {
|
||||
padding: 11rem 2rem 2rem 31.8rem; // at zoom = 1.0: 110px 20px 20px 318px
|
||||
}
|
||||
&.short-panel {
|
||||
padding: 35rem 2rem 2rem 2rem; // at zoom = 1.0: 350px 20px 20px 20px
|
||||
}
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
|
@ -42,7 +58,6 @@
|
|||
.crop-drag-info-image {
|
||||
width: 100% !important;
|
||||
background-color: $translucent;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.plant-catalog-image {
|
||||
|
@ -104,15 +119,15 @@
|
|||
.text-input-wrapper {
|
||||
position: relative;
|
||||
margin: 1rem;
|
||||
border-bottom: 1px solid #000;
|
||||
border-bottom: 1px solid $dark_gray;
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
background: #000;
|
||||
background: $dark_gray;
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
height: 3px;
|
||||
}
|
||||
&:before {
|
||||
left: 0;
|
||||
|
@ -120,6 +135,9 @@
|
|||
&:after {
|
||||
right: 0;
|
||||
}
|
||||
i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.fa-search {
|
||||
position: absolute;
|
||||
top: 0.8rem;
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
position: fixed;
|
||||
top: 8.9rem;
|
||||
width: 30rem;
|
||||
@media screen and (max-width: 450px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes panel-pullout {
|
||||
|
@ -17,6 +20,12 @@
|
|||
.farm-designer-panels {
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
&.panel-closed {
|
||||
display: none !important;
|
||||
}
|
||||
&.short-panel {
|
||||
height: 24rem;
|
||||
}
|
||||
.panel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -134,33 +143,30 @@
|
|||
|
||||
.panel-title {
|
||||
height: 50px;
|
||||
padding-top: 1.8rem;
|
||||
padding-left: 1.4rem;
|
||||
padding-right: 2rem;
|
||||
.back-arrow {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
color: $off_white;
|
||||
margin-right: 1rem;
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
margin-top: -1.8rem;
|
||||
vertical-align: middle;
|
||||
width: 50px;
|
||||
line-height: 50px;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display: inline-block;
|
||||
color: $white;
|
||||
float: left;
|
||||
color: $off_white;
|
||||
font-size: 1.8rem;
|
||||
margin-top: 0.4rem;
|
||||
white-space: nowrap;
|
||||
width: 10em;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: 2rem;
|
||||
padding: 0.2rem;
|
||||
line-height: 50px;
|
||||
}
|
||||
.right-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
float: right;
|
||||
text-transform: uppercase;
|
||||
font-size: 1rem;
|
||||
|
@ -170,6 +176,8 @@
|
|||
letter-spacing: 1px;
|
||||
border-radius: 4px;
|
||||
color: $off_white;
|
||||
margin-top: 1.25rem;
|
||||
margin-right: 1.5rem;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
|
@ -250,8 +258,15 @@
|
|||
&.with-button {
|
||||
display: flex;
|
||||
margin-top: 5rem;
|
||||
a {
|
||||
.fb-button {
|
||||
margin: 1rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
a {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +283,7 @@
|
|||
input {
|
||||
background: $white;
|
||||
}
|
||||
.is-saved {
|
||||
.save-btn {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -284,7 +299,6 @@
|
|||
.panel-nav {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
|
@ -301,9 +315,6 @@
|
|||
}
|
||||
|
||||
.crop-info-panel {
|
||||
.title {
|
||||
width: 50%;
|
||||
}
|
||||
.panel-header {
|
||||
position: inherit;
|
||||
background-size: 144% !important;
|
||||
|
@ -339,6 +350,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.add-plant-panel,
|
||||
.move-to-panel {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.add-plant-panel {
|
||||
.panel-header {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.move-to-panel-content {
|
||||
&.with-nav {
|
||||
margin-top: 6rem;
|
||||
|
|
|
@ -706,6 +706,11 @@ ul {
|
|||
|
||||
.controls-popup {
|
||||
color: $off_white;
|
||||
@media screen and (max-width: 450px) {
|
||||
&.panel-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
i {
|
||||
position: fixed;
|
||||
bottom: 3rem;
|
||||
|
@ -765,7 +770,7 @@ ul {
|
|||
.empty-state-graphic {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
margin-top: 10%;
|
||||
margin-top: 25%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
|
@ -867,6 +872,9 @@ ul {
|
|||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.fb-button {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-page {
|
||||
|
|
|
@ -23,6 +23,19 @@ nav {
|
|||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.nav-sync {
|
||||
min-width: 90px;
|
||||
&.auto-sync {
|
||||
background: none !important;
|
||||
box-shadow: none;
|
||||
font-style: italic;
|
||||
text-transform: none;
|
||||
}
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: inline-block;
|
||||
a {
|
||||
|
@ -81,6 +94,14 @@ nav {
|
|||
}
|
||||
|
||||
.nav-right {
|
||||
height: 5rem;
|
||||
overflow: hidden;
|
||||
.connection-status-popover {
|
||||
display: inline;
|
||||
.bp3-popover-wrapper {
|
||||
margin: 1.85rem;
|
||||
}
|
||||
}
|
||||
a {
|
||||
font-weight: normal;
|
||||
color: $black;
|
||||
|
@ -92,54 +113,42 @@ nav {
|
|||
margin-right: 0.8rem;
|
||||
}
|
||||
}
|
||||
.connection-status-popover {
|
||||
display: inline;
|
||||
.bp3-popover-wrapper {
|
||||
margin: 1.85rem;
|
||||
}
|
||||
|
||||
.menu-popover {
|
||||
display: inline;
|
||||
.bp3-popover-content {
|
||||
position: relative;
|
||||
width: 22rem;
|
||||
background: $dark_gray;
|
||||
i {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
}
|
||||
.menu-popover {
|
||||
display: inline;
|
||||
.bp3-popover-content {
|
||||
position: relative;
|
||||
width: 22rem;
|
||||
a:not(.app-version) {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.app-version {
|
||||
margin: 1rem -1rem -1rem;
|
||||
background: $dark_gray;
|
||||
color: $white;
|
||||
padding: 0.5rem 0 0 1rem;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
label {
|
||||
color: $white;
|
||||
}
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
color: $gray;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 1.2px;
|
||||
a:not(.app-version) {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
color: $off_white;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.bp3-overlay-content {
|
||||
margin-top: 1.6rem;
|
||||
.app-version {
|
||||
margin: 1rem -1rem -1rem;
|
||||
background: $dark_gray;
|
||||
color: $white;
|
||||
}
|
||||
.bp3-popover-wrapper {
|
||||
padding: 0.5rem 0 0 1rem;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
label {
|
||||
color: $white;
|
||||
}
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
.bp3-popover-arrow-fill {
|
||||
fill: $dark_gray;
|
||||
}
|
||||
.bp3-popover-content {
|
||||
background: $dark_gray;
|
||||
p {
|
||||
display: inline;
|
||||
color: $gray;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,13 +166,13 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 1075px) {
|
||||
.top-menu-container .nav-links {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 975px) {
|
||||
@media screen and (min-width: 1075px) {
|
||||
.mobile-menu-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.bulk-scheduler-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
// Regimen Editor
|
||||
.regimen-day {
|
||||
margin: 1.5rem 0;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
.farmware-input-panel,
|
||||
.sequence-editor-panel,
|
||||
.regimen-editor-panel {
|
||||
margin: -3rem -1.5rem -6rem;
|
||||
margin: -3rem -1.5rem -3rem;
|
||||
height: calc(100vh - 5rem);
|
||||
background: $light_gray;
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
display: none;
|
||||
&.open {
|
||||
display: block;
|
||||
|
@ -22,7 +22,7 @@
|
|||
float: left;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
h3,
|
||||
p {
|
||||
margin-left: 15px;
|
||||
|
@ -33,6 +33,7 @@
|
|||
}
|
||||
.button-group {
|
||||
margin-right: 15px;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
.title-help-text {
|
||||
padding-left: 15px;
|
||||
|
@ -51,7 +52,7 @@
|
|||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
.title-help-text {
|
||||
padding-left: 3rem;
|
||||
padding-right: 3rem;
|
||||
|
@ -63,7 +64,7 @@
|
|||
.sequence-editor-content,
|
||||
.regimen-editor-content {
|
||||
margin-right: -15px;
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
margin-left: 15px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -72,7 +73,7 @@
|
|||
.sequence-editor-tools,
|
||||
.regimen-editor-tools {
|
||||
margin-right: 15px;
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.locals-list {
|
||||
|
@ -146,7 +147,7 @@
|
|||
|
||||
.farmware-info-panel,
|
||||
.step-button-cluster-panel {
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
display: none;
|
||||
&.farmware-info-open,
|
||||
&.inserting-step {
|
||||
|
@ -157,12 +158,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.step-button-cluster {
|
||||
@media screen and (max-width: 767px) {
|
||||
width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
.farmware-info-panel button {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.step-button-cluster {
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -198,12 +205,31 @@
|
|||
padding-top: 0.4rem;
|
||||
margin-bottom: 3rem;
|
||||
margin-right: 5px;
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 1075px) {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.empty-state {
|
||||
display: none;
|
||||
.empty-state-graphic {
|
||||
margin-top: 25%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
&.open {
|
||||
display: none;
|
||||
}
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
.empty-state {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.panel-top {
|
||||
margin: 0;
|
||||
.text-input-wrapper {
|
||||
margin: 0.1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,24 +249,39 @@
|
|||
}
|
||||
|
||||
.farmware-input-panel-contents {
|
||||
@media screen and (max-width: 974px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
padding-right: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sequence-list-panel input,
|
||||
.regimen-list-panel input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.back-to-farmware,
|
||||
.back-to-regimens,
|
||||
.back-to-sequences {
|
||||
display: none;
|
||||
&.open {
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
display: block;
|
||||
margin-top: -2rem;
|
||||
float: left !important;
|
||||
height: 6rem;
|
||||
width: 4rem;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
line-height: 6rem;
|
||||
margin-left: 15px;
|
||||
&.inserting-step,
|
||||
&.inserting-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.back-to-farmware {
|
||||
display: none;
|
||||
&.open {
|
||||
@media screen and (max-width: 767px) {
|
||||
display: block;
|
||||
margin: 4rem;
|
||||
margin-top: 0;
|
||||
|
@ -249,22 +290,19 @@
|
|||
i {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&.inserting-step {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-drop-area {
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.add-command-button-container {
|
||||
display: none;
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
display: block;
|
||||
min-height: 3rem;
|
||||
.add-command {
|
||||
|
@ -278,7 +316,7 @@
|
|||
|
||||
.farmware-info-button {
|
||||
display: none;
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 767px) {
|
||||
&.open {
|
||||
display: block;
|
||||
margin: 4rem;
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
box-shadow: none;
|
||||
color: $dark_gray;
|
||||
font-weight: bold;
|
||||
min-width: 30%;
|
||||
}
|
||||
p {
|
||||
font-size: 1rem;
|
||||
|
|
|
@ -17,7 +17,7 @@ describe("<EStopButton />", () => {
|
|||
bot.hardware.informational_settings.sync_status = undefined;
|
||||
const wrapper = mount(<EStopButton {...fakeProps()} />);
|
||||
expect(wrapper.text()).toEqual("E-STOP");
|
||||
expect(wrapper.find("button").hasClass("gray")).toBeTruthy();
|
||||
expect(wrapper.find("button").hasClass("pseudo-disabled")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("locked", () => {
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { emergencyLock, emergencyUnlock } from "../actions";
|
||||
import { EStopButtonProps } from "../interfaces";
|
||||
import { isBotUp } from "../must_be_online";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
const GRAY = "pseudo-disabled";
|
||||
|
||||
export class EStopButton extends React.Component<EStopButtonProps, {}> {
|
||||
render() {
|
||||
const i = this.props.bot.hardware.informational_settings;
|
||||
const isLocked = !!i.locked;
|
||||
const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock;
|
||||
const color = isLocked ? "yellow" : "red";
|
||||
const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : "gray";
|
||||
const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : GRAY;
|
||||
const emergencyLockStatusText = isLocked ? t("UNLOCK") : "E-STOP";
|
||||
|
||||
return <button
|
||||
|
|
|
@ -93,15 +93,6 @@ describe("<BoardType/>", () => {
|
|||
|
||||
it("displays standard boards", () => {
|
||||
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([
|
||||
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
|
||||
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" },
|
||||
|
|
|
@ -5,7 +5,6 @@ import { FirmwareHardware } from "farmbot";
|
|||
import { ColWidth } from "../farmbot_os_settings";
|
||||
import { updateConfig } from "../../actions";
|
||||
import { BoardTypeProps } from "./interfaces";
|
||||
import { Feature } from "../../interfaces";
|
||||
import { t } from "../../../i18next_wrapper";
|
||||
import { FirmwareHardwareStatus } from "./firmware_hardware_status";
|
||||
|
||||
|
@ -69,12 +68,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
|
|||
return isFwHardwareValue(value) ? value : undefined;
|
||||
}
|
||||
|
||||
get firmwareChoices() {
|
||||
const { shouldDisplay } = this.props;
|
||||
return [ARDUINO, FARMDUINO,
|
||||
...(shouldDisplay(Feature.farmduino_k14) ? [FARMDUINO_K14] : [])
|
||||
];
|
||||
}
|
||||
get firmwareChoices() { return [ARDUINO, FARMDUINO, FARMDUINO_K14]; }
|
||||
|
||||
get firmwareVersion() {
|
||||
return this.props.bot.hardware.informational_settings.firmware_version;
|
||||
|
|
|
@ -7,7 +7,6 @@ import { mapStateToProps } from "./state_to_props";
|
|||
import { Props } from "./interfaces";
|
||||
import { PinBindings } from "./pin_bindings/pin_bindings";
|
||||
import { selectAllDiagnosticDumps } from "../resources/selectors";
|
||||
import { ConnectivityPanel } from "./connectivity";
|
||||
import { getStatus } from "../connectivity/reducer_support";
|
||||
|
||||
@connect(mapStateToProps)
|
||||
|
@ -35,11 +34,6 @@ export class Devices extends React.Component<Props, {}> {
|
|||
isValidFbosConfig={this.props.isValidFbosConfig}
|
||||
env={this.props.env}
|
||||
saveFarmwareEnv={this.props.saveFarmwareEnv} />
|
||||
<ConnectivityPanel
|
||||
status={this.props.deviceAccount.specialStatus}
|
||||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
deviceAccount={this.props.deviceAccount} />
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<HardwareSettings
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../history", () => ({
|
||||
history: {
|
||||
getCurrentLocation: jest.fn(() => { return { pathname: mockPath }; }),
|
||||
},
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); }),
|
||||
history: { getCurrentLocation: jest.fn(() => ({ pathname: mockPath })) },
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
}));
|
||||
|
||||
jest.mock("../../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
|
@ -16,9 +17,14 @@ import { mount } from "enzyme";
|
|||
import { Props } from "../interfaces";
|
||||
import { GardenMapLegendProps } from "../map/interfaces";
|
||||
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 { 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/>", () => {
|
||||
function fakeProps(): Props {
|
||||
|
@ -93,7 +99,7 @@ describe("<FarmDesigner/>", () => {
|
|||
["Map", "Plants", "Events"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -103,7 +109,7 @@ describe("<FarmDesigner/>", () => {
|
|||
["Map", "Plants", "Events"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -113,4 +119,15 @@ describe("<FarmDesigner/>", () => {
|
|||
const wrapper = mount(<FarmDesigner {...p} />);
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -495,7 +495,6 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
<this.RepeatForm />
|
||||
<SaveBtn
|
||||
status={farmEvent.specialStatus || this.state.specialStatusLocal}
|
||||
color="yellow"
|
||||
onClick={() => this.commitViewModel()} />
|
||||
<this.FarmEventDeleteButton />
|
||||
<TzWarning deviceTimezone={this.props.deviceTimezone} />
|
||||
|
|
|
@ -3,13 +3,14 @@ import { connect } from "react-redux";
|
|||
import { GardenMap } from "./map/garden_map";
|
||||
import { Props, State, BotOriginQuadrant, isBotOriginQuadrant } from "./interfaces";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { history } from "../history";
|
||||
import { Plants } from "./plants/plant_inventory";
|
||||
import { GardenMapLegend } from "./map/legend/garden_map_legend";
|
||||
import { NumericSetting, BooleanSetting } from "../session_keys";
|
||||
import { isUndefined, last } from "lodash";
|
||||
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 moment from "moment";
|
||||
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);
|
||||
}
|
||||
|
||||
get mapOnly() {
|
||||
return history.getCurrentLocation().pathname === "/app/designer";
|
||||
}
|
||||
get mapPanelClassName() { return mapPanelClassName(); }
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
@ -134,11 +133,10 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
: 1;
|
||||
const imageAgeInfo = { newestDate, toOldest };
|
||||
|
||||
const displayPanel = this.mapOnly ? "hidden" : "";
|
||||
|
||||
return <div className="farm-designer">
|
||||
|
||||
<GardenMapLegend
|
||||
className={this.mapPanelClassName}
|
||||
zoom={this.updateZoomLevel}
|
||||
toggle={this.toggle}
|
||||
updateBotOriginQuadrant={this.updateBotOriginQuadrant}
|
||||
|
@ -155,13 +153,13 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
getConfigValue={this.props.getConfigValue}
|
||||
imageAgeInfo={imageAgeInfo} />
|
||||
|
||||
<DesignerNavTabs hidden={!this.mapOnly} />
|
||||
<div className={`farm-designer-panels ${displayPanel}`}>
|
||||
<DesignerNavTabs hidden={!(getPanelStatus() === MapPanelStatus.closed)} />
|
||||
<div className={`farm-designer-panels ${this.mapPanelClassName}`}>
|
||||
{this.childComponent(this.props)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`farm-designer-map ${this.mapOnly ? "" : "panel-open"}`}
|
||||
className={`farm-designer-map ${this.mapPanelClassName}`}
|
||||
style={{ zoom: zoom_level }}>
|
||||
<GardenMap
|
||||
showPoints={show_points}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
|
||||
}));
|
||||
|
||||
jest.mock("../../saved_gardens/saved_gardens", () => ({
|
||||
|
@ -16,6 +17,8 @@ import {
|
|||
transformForQuadrant,
|
||||
getMode,
|
||||
getGardenCoordinates,
|
||||
MapPanelStatus,
|
||||
mapPanelClassName,
|
||||
} from "../util";
|
||||
import { McuParams } from "farmbot";
|
||||
import {
|
||||
|
@ -41,7 +44,7 @@ describe("translateScreenToGarden()", () => {
|
|||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 1,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
mapOnly: false,
|
||||
panelStatus: MapPanelStatus.open,
|
||||
});
|
||||
expect(result).toEqual({ x: 180, y: 80 });
|
||||
});
|
||||
|
@ -53,7 +56,7 @@ describe("translateScreenToGarden()", () => {
|
|||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 0.33,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
mapOnly: false,
|
||||
panelStatus: MapPanelStatus.open,
|
||||
});
|
||||
expect(result).toEqual({ x: 2470, y: 840 });
|
||||
});
|
||||
|
@ -65,7 +68,7 @@ describe("translateScreenToGarden()", () => {
|
|||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 1.5,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
mapOnly: false,
|
||||
panelStatus: MapPanelStatus.open,
|
||||
});
|
||||
expect(result).toEqual({ x: 520, y: 150 });
|
||||
});
|
||||
|
@ -80,7 +83,7 @@ describe("translateScreenToGarden()", () => {
|
|||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 0.75,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
mapOnly: false,
|
||||
panelStatus: MapPanelStatus.open,
|
||||
});
|
||||
expect(result).toEqual({ x: 0, y: 130 });
|
||||
});
|
||||
|
@ -96,7 +99,7 @@ describe("translateScreenToGarden()", () => {
|
|||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 0.75,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
mapOnly: false,
|
||||
panelStatus: MapPanelStatus.open,
|
||||
});
|
||||
expect(result).toEqual({ x: 130, y: 0 });
|
||||
});
|
||||
|
@ -108,10 +111,22 @@ describe("translateScreenToGarden()", () => {
|
|||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 1,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
mapOnly: true,
|
||||
panelStatus: MapPanelStatus.closed,
|
||||
});
|
||||
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()", () => {
|
||||
|
@ -365,3 +380,10 @@ describe("getGardenCoordinates()", () => {
|
|||
expect(result).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapPanelClassName()", () => {
|
||||
it("returns correct panel status", () => {
|
||||
mockPath = "/app/designer/move_to";
|
||||
expect(mapPanelClassName()).toEqual("short-panel");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface GardenMapLegendProps {
|
|||
getConfigValue: GetWebAppConfigValue;
|
||||
imageAgeInfo: { newestDate: string, toOldest: number };
|
||||
gardenId?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type MapTransformProps = {
|
||||
|
|
|
@ -111,7 +111,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
|
|||
export function GardenMapLegend(props: GardenMapLegendProps) {
|
||||
const menuClass = props.legendMenuOpen ? "active" : "";
|
||||
return <div
|
||||
className={"garden-map-legend " + menuClass}
|
||||
className={`garden-map-legend ${menuClass} ${props.className}`}
|
||||
style={{ zoom: 1 }}>
|
||||
<div
|
||||
className={"menu-pullout " + menuClass}
|
||||
|
|
|
@ -5,9 +5,8 @@ import {
|
|||
CheckedAxisLength, AxisNumberProperty, BotSize, MapTransformProps, Mode
|
||||
} from "./interfaces";
|
||||
import { trim } from "../../util";
|
||||
import { getPathArray } from "../../history";
|
||||
import { history, getPathArray } from "../../history";
|
||||
import { savedGardenOpen } from "../saved_gardens/saved_gardens";
|
||||
import { last } from "lodash";
|
||||
|
||||
/*
|
||||
* Farm Designer Map Utilities
|
||||
|
@ -51,12 +50,48 @@ export function round(num: number) {
|
|||
*
|
||||
*/
|
||||
|
||||
/** Controlled by .farm-designer-map padding x10 */
|
||||
const paddingWhen = {
|
||||
panelClosed: { left: 20, top: 160 },
|
||||
panelOpen: { left: 318, top: 110 }
|
||||
/** Status of farm designer side panel. */
|
||||
export enum MapPanelStatus {
|
||||
open = "open",
|
||||
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" */
|
||||
const leftOrTop: Record<"x" | "y", "top" | "left"> = { x: "left", y: "top" };
|
||||
|
||||
|
@ -68,7 +103,7 @@ export interface ScreenToGardenParams {
|
|||
zoomLvl: number;
|
||||
mapTransformProps: MapTransformProps;
|
||||
gridOffset: AxisNumberProperty;
|
||||
mapOnly: boolean;
|
||||
panelStatus: MapPanelStatus;
|
||||
}
|
||||
|
||||
/** Transform screen coordinates into garden coordinates */
|
||||
|
@ -76,10 +111,10 @@ export function translateScreenToGarden(
|
|||
params: ScreenToGardenParams
|
||||
): XYCoordinate {
|
||||
const {
|
||||
page, scroll, zoomLvl, mapTransformProps, gridOffset, mapOnly
|
||||
page, scroll, zoomLvl, mapTransformProps, gridOffset, panelStatus
|
||||
} = params;
|
||||
const { xySwap } = mapTransformProps;
|
||||
const mapPadding = mapOnly ? paddingWhen.panelClosed : paddingWhen.panelOpen;
|
||||
const mapPadding = getMapPadding(panelStatus);
|
||||
const screenXY = page;
|
||||
const mapXY = ["x", "y"].reduce<XYCoordinate>(
|
||||
(result: XYCoordinate, axis: "x" | "y") => {
|
||||
|
@ -283,7 +318,7 @@ export const getGardenCoordinates = (props: {
|
|||
mapTransformProps: props.mapTransformProps,
|
||||
gridOffset: props.gridOffset,
|
||||
zoomLvl,
|
||||
mapOnly: last(getPathArray()) === "designer",
|
||||
panelStatus: getPanelStatus(),
|
||||
};
|
||||
return translateScreenToGarden(params);
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { svgToUrl } from "../../open_farm/icons";
|
||||
import {
|
||||
CropInfoProps, CropLiveSearchResult, OpenfarmSearch
|
||||
|
@ -147,7 +146,8 @@ const AddPlantHereButton = (props: {
|
|||
cropName, slug, gardenCoords: botXY, gridSize: undefined,
|
||||
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}}",
|
||||
{ coordinate: botXYLabel })}
|
||||
</button>;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { history as routeHistory } from "../../history";
|
||||
import { last, trim } from "lodash";
|
||||
import { Link } from "../../link";
|
||||
|
@ -81,7 +80,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
|
|||
{props.linkTo &&
|
||||
<Link to={props.linkTo}>
|
||||
<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>
|
||||
</Link>}
|
||||
</div>;
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface PlantPanelProps {
|
|||
export const PLANT_STAGES: DropDownItem[] = [
|
||||
{ value: "planned", label: t("Planned") },
|
||||
{ value: "planted", label: t("Planted") },
|
||||
{ value: "sprouted", label: t("Sprouted") },
|
||||
{ value: "harvested", label: t("Harvested") },
|
||||
];
|
||||
|
||||
|
@ -43,6 +44,10 @@ export const PLANT_STAGES_DDI = {
|
|||
label: PLANT_STAGES[2].label,
|
||||
value: PLANT_STAGES[2].value
|
||||
},
|
||||
[PLANT_STAGES[3].value]: {
|
||||
label: PLANT_STAGES[3].label,
|
||||
value: PLANT_STAGES[3].value
|
||||
},
|
||||
};
|
||||
|
||||
interface EditPlantProperty {
|
||||
|
|
|
@ -23,6 +23,7 @@ describe("NavBar", () => {
|
|||
getConfigValue: jest.fn(),
|
||||
tour: undefined,
|
||||
device: fakeDevice(),
|
||||
autoSync: false,
|
||||
});
|
||||
|
||||
it("has correct parent classname", () => {
|
||||
|
|
|
@ -3,22 +3,22 @@ import { SyncButton } from "../sync_button";
|
|||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { shallow } from "enzyme";
|
||||
import { SyncButtonProps } from "../interfaces";
|
||||
import { SyncStatus } from "farmbot";
|
||||
|
||||
describe("<SyncButton/>", function () {
|
||||
const fakeProps = (): SyncButtonProps => {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
consistent: true,
|
||||
};
|
||||
};
|
||||
const fakeProps = (): SyncButtonProps => ({
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
consistent: true,
|
||||
autoSync: false,
|
||||
});
|
||||
|
||||
it("is gray when inconsistent", () => {
|
||||
const p = fakeProps();
|
||||
p.consistent = false;
|
||||
p.bot.hardware.informational_settings.sync_status = "sync_now";
|
||||
const result = shallow(<SyncButton {...p} />);
|
||||
expect(result.hasClass("gray")).toBeTruthy();
|
||||
expect(result.hasClass("pseudo-disabled")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is gray when disconnected", () => {
|
||||
|
@ -26,16 +26,16 @@ describe("<SyncButton/>", function () {
|
|||
p.consistent = false;
|
||||
p.bot.hardware.informational_settings.sync_status = "unknown";
|
||||
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();
|
||||
// tslint:disable-next-line:no-any
|
||||
p.bot.hardware.informational_settings.sync_status = "new" as any;
|
||||
const result = shallow(<SyncButton {...p} />);
|
||||
expect(result.text()).toContain("new");
|
||||
expect(result.hasClass("gray")).toBeTruthy();
|
||||
expect(result.hasClass("pseudo-disabled")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("syncs when clicked", () => {
|
||||
|
@ -58,4 +58,23 @@ describe("<SyncButton/>", function () {
|
|||
const result = shallow(<SyncButton {...p} />);
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
return <SyncButton
|
||||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
autoSync={this.props.autoSync}
|
||||
consistent={this.props.consistent} />;
|
||||
}
|
||||
|
||||
|
@ -98,10 +99,11 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
<div className="nav-right">
|
||||
<div className="menu-popover">
|
||||
<Popover
|
||||
portalClassName={"nav-right"}
|
||||
popoverClassName={"menu-popover"}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
isOpen={accountMenuOpen}
|
||||
onClose={this.close("accountMenuOpen")}
|
||||
usePortal={false}>
|
||||
onClose={this.close("accountMenuOpen")}>
|
||||
<div className="nav-name"
|
||||
onClick={this.toggle("accountMenuOpen")}>
|
||||
{firstName}
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface SyncButtonProps {
|
|||
bot: BotState;
|
||||
consistent: boolean;
|
||||
onClick?: () => void;
|
||||
autoSync: boolean;
|
||||
}
|
||||
|
||||
export interface NavBarProps {
|
||||
|
@ -20,6 +21,7 @@ export interface NavBarProps {
|
|||
getConfigValue: GetWebAppConfigValue;
|
||||
tour: string | undefined;
|
||||
device: TaggedDevice;
|
||||
autoSync: boolean;
|
||||
}
|
||||
|
||||
export interface NavBarState {
|
||||
|
|
|
@ -1,45 +1,50 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { SyncStatus } from "farmbot/dist";
|
||||
import { SyncButtonProps } from "./interfaces";
|
||||
import { sync } from "../devices/actions";
|
||||
import { t } from "../i18next_wrapper";
|
||||
|
||||
const GRAY = "pseudo-disabled";
|
||||
|
||||
const COLOR_MAPPING: Record<SyncStatus, string> = {
|
||||
"synced": "green",
|
||||
"sync_now": "yellow",
|
||||
"syncing": "yellow",
|
||||
"sync_error": "red",
|
||||
"booting": "gray",
|
||||
"maintenance": "gray",
|
||||
"unknown": "gray"
|
||||
"booting": GRAY,
|
||||
"maintenance": GRAY,
|
||||
"unknown": GRAY
|
||||
};
|
||||
|
||||
const TEXT_MAPPING: () => Record<SyncStatus, string> = () => ({
|
||||
"synced": t("SYNCED"),
|
||||
"sync_now": t("SYNC NOW"),
|
||||
"syncing": t("SYNCING"),
|
||||
"sync_error": t("SYNC ERROR"),
|
||||
"booting": t("UNKNOWN"),
|
||||
"unknown": t("UNKNOWN"),
|
||||
"maintenance": t("UNKNOWN")
|
||||
const TEXT_MAPPING = (autoSync: boolean): Record<SyncStatus, string> => ({
|
||||
"synced": autoSync ? t("Synced") : t("SYNCED"),
|
||||
"sync_now": autoSync ? t("Syncing...") : t("SYNC NOW"),
|
||||
"syncing": autoSync ? t("Syncing...") : t("SYNCING"),
|
||||
"sync_error": autoSync ? t("Sync error") : t("SYNC ERROR"),
|
||||
"booting": autoSync ? t("Sync unknown") : t("UNKNOWN"),
|
||||
"unknown": autoSync ? t("Sync unknown") : t("UNKNOWN"),
|
||||
"maintenance": autoSync ? t("Sync unknown") : t("UNKNOWN")
|
||||
});
|
||||
|
||||
/** Animation during syncing action */
|
||||
const spinner = <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 syncStatus = sync_status || "unknown";
|
||||
const normalColor = COLOR_MAPPING[syncStatus] || "gray";
|
||||
const normalColor = COLOR_MAPPING[syncStatus] || GRAY;
|
||||
const color = (!consistent && (syncStatus === "sync_now"))
|
||||
? "gray"
|
||||
? GRAY
|
||||
: normalColor;
|
||||
const text = TEXT_MAPPING()[syncStatus] || syncStatus.replace("_", " ");
|
||||
const text = TEXT_MAPPING(autoSync)[syncStatus] || syncStatus.replace("_", " ");
|
||||
const spinnerEl = (syncStatus === "syncing") ? spinner : "";
|
||||
const className = autoSync
|
||||
? "nav-sync fb-button auto-sync"
|
||||
: `nav-sync ${color} fb-button`;
|
||||
|
||||
return <button
|
||||
className={`nav-sync ${color} fb-button`}
|
||||
className={className}
|
||||
onClick={() => dispatch(sync())}>
|
||||
{text} {spinnerEl}
|
||||
</button>;
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
jest.mock("../../history", () => ({
|
||||
push: () => jest.fn(),
|
||||
history: {
|
||||
getCurrentLocation: () => ({ pathname: "" })
|
||||
}
|
||||
history: { getCurrentLocation: () => ({ pathname: "" }) }
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Regimens } from "../index";
|
||||
import { Regimens, RegimenBackButtonProps, RegimenBackButton } from "../index";
|
||||
import { Props } from "../interfaces";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { auth } from "../../__test_support__/fake_state/token";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { fakeRegimen } from "../../__test_support__/fake_state/resources";
|
||||
import { clickButton } from "../../__test_support__/helpers";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
describe("<Regimens />", () => {
|
||||
|
@ -43,7 +38,7 @@ describe("<Regimens />", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<Regimens {...fakeProps()} />);
|
||||
["Regimens", "Regimen Editor", "Scheduler"].map(string =>
|
||||
["Regimens", "Edit Regimen", "Scheduler"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
|
@ -60,12 +55,19 @@ describe("<Regimens />", () => {
|
|||
const wrapper = mount(<Regimens {...p} />);
|
||||
expect(wrapper.html()).toContain("inserting-item");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SequenceBackButton />", () => {
|
||||
const fakeProps = (): RegimenBackButtonProps => ({
|
||||
dispatch: jest.fn(),
|
||||
className: "",
|
||||
});
|
||||
|
||||
it("returns to regimen", () => {
|
||||
const p = fakeProps();
|
||||
p.schedulerOpen = true;
|
||||
const wrapper = mount(<Regimens {...p} />);
|
||||
clickButton(wrapper, 0, "back to regimen");
|
||||
p.className = "inserting-item";
|
||||
const wrapper = mount(<RegimenBackButton {...p} />);
|
||||
wrapper.find("i").simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SCHEDULER_STATE, payload: false
|
||||
});
|
||||
|
@ -73,9 +75,9 @@ describe("<Regimens />", () => {
|
|||
|
||||
it("returns to regimen list", () => {
|
||||
const p = fakeProps();
|
||||
p.schedulerOpen = false;
|
||||
const wrapper = mount(<Regimens {...p} />);
|
||||
clickButton(wrapper, 0, "back to regimens");
|
||||
p.className = "";
|
||||
const wrapper = mount(<RegimenBackButton {...p} />);
|
||||
wrapper.find("i").simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_REGIMEN, payload: undefined
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { isNaN, isNumber } from "lodash";
|
||||
|
||||
import { error, warning } from "farmbot-toastr";
|
||||
import { error, warning, success } from "farmbot-toastr";
|
||||
import { ReduxAction, Thunk } from "../../redux/interfaces";
|
||||
import { ToggleDayParams } from "./interfaces";
|
||||
import { findSequence, findRegimen } from "../../resources/selectors";
|
||||
|
@ -93,6 +92,7 @@ export function commitBulkEditor(): Thunk {
|
|||
clonedRegimen.body = mergeDeclarations(varData, regimen.body.body);
|
||||
console.log(JSON.stringify(clonedRegimen.body, undefined, 2));
|
||||
dispatch(overwrite(regimen, clonedRegimen));
|
||||
success(t("Item(s) added."));
|
||||
} else {
|
||||
return error(t("No day(s) selected."));
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
BlurableInput, Row, Col, FBSelect, DropDownItem, NULL_CHOICE
|
||||
} from "../../ui/index";
|
||||
import moment from "moment";
|
||||
|
||||
import { isString } from "lodash";
|
||||
import { betterCompact, bail } from "../../util";
|
||||
import { msToTime, timeToMs } from "./utils";
|
||||
|
@ -69,7 +68,7 @@ export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
|
|||
render() {
|
||||
const { dispatch, weeks, sequences } = this.props;
|
||||
const active = !!(sequences && sequences.length);
|
||||
return <div>
|
||||
return <div className="bulk-scheduler-content">
|
||||
<AddButton
|
||||
active={active}
|
||||
click={() => dispatch(commitBulkEditor())} />
|
||||
|
|
|
@ -12,18 +12,19 @@ import { t } from "../i18next_wrapper";
|
|||
import { ToolTips, Actions } from "../constants";
|
||||
import { unselectRegimen } from "./actions";
|
||||
|
||||
const RegimenBackButton = (props: { dispatch: Function, className: string }) => {
|
||||
export interface RegimenBackButtonProps {
|
||||
dispatch: Function;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const RegimenBackButton = (props: RegimenBackButtonProps) => {
|
||||
const schedulerOpen = props.className.includes("inserting-item");
|
||||
return <Row>
|
||||
<button
|
||||
className={`back-to-regimens fb-button gray ${props.className}`}
|
||||
onClick={() => schedulerOpen
|
||||
? props.dispatch({ type: Actions.SET_SCHEDULER_STATE, payload: false })
|
||||
: props.dispatch(unselectRegimen())}>
|
||||
<i className="fa fa-arrow-left" />
|
||||
{schedulerOpen ? t("back to regimen") : t("back to regimens")}
|
||||
</button>
|
||||
</Row>;
|
||||
return <i
|
||||
className={`back-to-regimens fa fa-arrow-left ${props.className}`}
|
||||
onClick={() => schedulerOpen
|
||||
? props.dispatch({ type: Actions.SET_SCHEDULER_STATE, payload: false })
|
||||
: props.dispatch(unselectRegimen())}
|
||||
title={schedulerOpen ? t("back to regimen") : t("back to regimens")} />;
|
||||
};
|
||||
|
||||
@connect(mapStateToProps)
|
||||
|
@ -39,7 +40,6 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
const insertingItem = this.props.schedulerOpen ? "inserting-item" : "";
|
||||
const activeClasses = [regimenOpen, insertingItem].join(" ");
|
||||
return <Page className="regimen-page">
|
||||
<RegimenBackButton className={activeClasses} dispatch={this.props.dispatch} />
|
||||
<Row>
|
||||
<LeftPanel
|
||||
className={`regimen-list-panel ${activeClasses}`}
|
||||
|
@ -53,7 +53,10 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
</LeftPanel>
|
||||
<CenterPanel
|
||||
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)}
|
||||
width={5}>
|
||||
<RegimenEditor
|
||||
|
@ -66,7 +69,10 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
</CenterPanel>
|
||||
<RightPanel
|
||||
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)}
|
||||
show={!!regimenSelected} width={4}>
|
||||
<BulkScheduler
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { mount } from "enzyme";
|
||||
import { RegimensList } from "../index";
|
||||
import { RegimensListProps } from "../../interfaces";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { inputEvent } from "../../../__test_support__/fake_input_event";
|
||||
|
||||
describe("<RegimensList />", () => {
|
||||
function fakeProps(): RegimensListProps {
|
||||
|
@ -24,9 +25,8 @@ describe("<RegimensList />", () => {
|
|||
});
|
||||
|
||||
it("sets search term", () => {
|
||||
const wrapper = shallow<RegimensList>(<RegimensList {...fakeProps()} />);
|
||||
wrapper.find("input").simulate("change",
|
||||
{ currentTarget: { value: "term" } });
|
||||
expect(wrapper.instance().state.searchTerm).toEqual("term");
|
||||
const wrapper = mount<RegimensList>(<RegimensList {...fakeProps()} />);
|
||||
wrapper.instance().onChange(inputEvent("term"));
|
||||
expect(wrapper.state().searchTerm).toEqual("term");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,31 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { RegimenListItem } from "./regimen_list_item";
|
||||
import { AddRegimen } from "./add_button";
|
||||
import { Row, Col } from "../../ui/index";
|
||||
import { RegimensListProps, RegimensListState } from "../interfaces";
|
||||
import { sortResourcesById } from "../../util";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
|
||||
import { Content } from "../../constants";
|
||||
|
||||
interface RegimenListHeaderProps {
|
||||
onChange(e: React.SyntheticEvent<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
|
||||
React.Component<RegimensListProps, RegimensListState> {
|
||||
|
@ -40,17 +60,22 @@ export class RegimensList extends
|
|||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, regimens } = this.props;
|
||||
|
||||
return <div>
|
||||
<AddRegimen dispatch={dispatch} length={regimens.length} />
|
||||
<input
|
||||
onChange={this.onChange}
|
||||
placeholder={t("Search Regimens...")} />
|
||||
<RegimenListHeader
|
||||
dispatch={this.props.dispatch}
|
||||
regimenCount={this.props.regimens.length}
|
||||
onChange={this.onChange} />
|
||||
<Row>
|
||||
<div className="regimen-list">
|
||||
{this.rows()}
|
||||
</div>
|
||||
<EmptyStateWrapper
|
||||
notEmpty={this.props.regimens.length > 0}
|
||||
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>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
|
|||
shouldDisplay: jest.fn(),
|
||||
confirmStepDeletion: false,
|
||||
menuOpen: false,
|
||||
showPins: true,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -253,9 +254,13 @@ describe("<SequenceSettingsMenu />", () => {
|
|||
it("renders settings", () => {
|
||||
const wrapper = mount(<SequenceSettingsMenu
|
||||
dispatch={jest.fn()}
|
||||
confirmStepDeletion={false} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
confirmStepDeletion={false}
|
||||
showPins={false} />);
|
||||
wrapper.find("button").first().simulate("click");
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
BooleanSetting.confirm_step_deletion, true);
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(setWebAppConfigValue).toHaveBeenCalledWith(
|
||||
"show_pins", true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ describe("<SequenceEditorMiddle/>", () => {
|
|||
shouldDisplay: jest.fn(),
|
||||
confirmStepDeletion: false,
|
||||
menuOpen: false,
|
||||
showPins: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import { buildResourceIndex } from "../../__test_support__/resource_index_builde
|
|||
import { resourceReducer } from "../../resources/reducer";
|
||||
import { resourceReady } from "../../sync/actions";
|
||||
import { setActiveSequenceByName } from "../set_active_sequence_by_name";
|
||||
import { inputEvent } from "../../__test_support__/fake_input_event";
|
||||
|
||||
describe("<SequencesList />", () => {
|
||||
const fakeSequences = () => {
|
||||
|
@ -105,13 +106,8 @@ describe("<SequencesList />", () => {
|
|||
|
||||
it("sets search term", () => {
|
||||
const wrapper = shallow<SequencesList>(<SequencesList {...fakeProps()} />);
|
||||
expect(wrapper.instance().state.searchTerm).toEqual("");
|
||||
const searchField = wrapper.find("input").first();
|
||||
expect(searchField.props().placeholder)
|
||||
.toEqual("Search Sequences...");
|
||||
searchField.simulate("change", {
|
||||
currentTarget: { value: "search this" }
|
||||
});
|
||||
expect(wrapper.state().searchTerm).toEqual("");
|
||||
wrapper.instance().onChange(inputEvent("search this"));
|
||||
expect(wrapper.instance().state.searchTerm).toEqual("search this");
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
jest.mock("../../history", () => ({
|
||||
push: jest.fn(),
|
||||
|
@ -8,7 +6,7 @@ jest.mock("../../history", () => ({
|
|||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { Sequences } from "../sequences";
|
||||
import { Sequences, SequenceBackButtonProps, SequenceBackButton } from "../sequences";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { Props } from "../interfaces";
|
||||
import {
|
||||
|
@ -39,12 +37,13 @@ describe("<Sequences/>", () => {
|
|||
confirmStepDeletion: false,
|
||||
menuOpen: false,
|
||||
stepIndex: undefined,
|
||||
showPins: true,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<Sequences {...fakeProps()} />);
|
||||
expect(wrapper.html()).toContain("Sequences");
|
||||
expect(wrapper.html()).toContain("Sequence Editor");
|
||||
expect(wrapper.html()).toContain("Edit Sequence");
|
||||
expect(wrapper.html()).toContain(ToolTips.SEQUENCE_EDITOR);
|
||||
expect(wrapper.html()).toContain("Commands");
|
||||
});
|
||||
|
@ -62,11 +61,18 @@ describe("<Sequences/>", () => {
|
|||
const wrapper = shallow(<Sequences {...p} />);
|
||||
expect(wrapper.html()).toContain("inserting-step");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SequenceBackButton />", () => {
|
||||
const fakeProps = (): SequenceBackButtonProps => ({
|
||||
dispatch: jest.fn(),
|
||||
className: "",
|
||||
});
|
||||
|
||||
it("goes back", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<Sequences {...p} />);
|
||||
wrapper.find("button").first().simulate("click");
|
||||
const wrapper = mount(<SequenceBackButton {...p} />);
|
||||
wrapper.find("i").first().simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_SEQUENCE, payload: undefined
|
||||
});
|
||||
|
|
|
@ -47,3 +47,8 @@ export const unselectSequence = () => {
|
|||
push("/app/sequences");
|
||||
return { type: Actions.SELECT_SEQUENCE, payload: undefined };
|
||||
};
|
||||
|
||||
export const closeCommandMenu = () => ({
|
||||
type: Actions.SET_SEQUENCE_STEP_POSITION,
|
||||
payload: undefined,
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ interface AllStepsProps {
|
|||
farmwareInfo?: FarmwareInfo;
|
||||
shouldDisplay?: ShouldDisplay;
|
||||
confirmStepDeletion: boolean;
|
||||
showPins?: boolean;
|
||||
}
|
||||
|
||||
export class AllSteps extends React.Component<AllStepsProps, {}> {
|
||||
|
@ -54,6 +55,7 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
|
|||
farmwareInfo,
|
||||
shouldDisplay,
|
||||
confirmStepDeletion: this.props.confirmStepDeletion,
|
||||
showPins: this.props.showPins,
|
||||
})}
|
||||
</div>
|
||||
</StepDragger>
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface Props {
|
|||
confirmStepDeletion: boolean;
|
||||
menuOpen: boolean;
|
||||
stepIndex: number | undefined;
|
||||
showPins: boolean;
|
||||
}
|
||||
|
||||
export interface SequenceEditorMiddleProps {
|
||||
|
@ -59,6 +60,7 @@ export interface SequenceEditorMiddleProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
confirmStepDeletion: boolean;
|
||||
menuOpen: boolean;
|
||||
showPins: boolean;
|
||||
}
|
||||
|
||||
export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
|
||||
|
@ -75,6 +77,7 @@ export interface SequenceHeaderProps {
|
|||
variablesCollapsed: boolean;
|
||||
toggleVarShow: () => void;
|
||||
confirmStepDeletion: boolean;
|
||||
showPins: boolean;
|
||||
}
|
||||
|
||||
export type ChannelName = ALLOWED_CHANNEL_NAMES;
|
||||
|
@ -202,4 +205,5 @@ export interface StepParams {
|
|||
farmwareInfo?: FarmwareInfo;
|
||||
shouldDisplay?: ShouldDisplay;
|
||||
confirmStepDeletion: boolean;
|
||||
showPins?: boolean;
|
||||
}
|
||||
|
|
|
@ -73,15 +73,15 @@ export function locationFormList(resources: ResourceIndex,
|
|||
return [COORDINATE_DDI()]
|
||||
.concat(additionalItems)
|
||||
.concat(heading("Tool"))
|
||||
.concat(toolDDI)
|
||||
.concat(group(everyPointDDI("Tool")))
|
||||
.concat(group(everyPointDDI("ToolSlot")))
|
||||
.concat(toolDDI)
|
||||
.concat(heading("Plant"))
|
||||
.concat(plantDDI)
|
||||
.concat(group(everyPointDDI("Plant")))
|
||||
.concat(plantDDI)
|
||||
.concat(heading("GenericPointer"))
|
||||
.concat(genericPointerDDI)
|
||||
.concat(group(everyPointDDI("GenericPointer")));
|
||||
.concat(group(everyPointDDI("GenericPointer")))
|
||||
.concat(genericPointerDDI);
|
||||
}
|
||||
|
||||
/** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */
|
||||
|
|
|
@ -26,6 +26,7 @@ export class SequenceEditorMiddle
|
|||
farmwareInfo={this.props.farmwareInfo}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
confirmStepDeletion={this.props.confirmStepDeletion}
|
||||
showPins={this.props.showPins}
|
||||
menuOpen={this.props.menuOpen} />}
|
||||
</EmptyStateWrapper>;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import { ToggleButton } from "../controls/toggle_button";
|
|||
import { Content } from "../constants";
|
||||
import { setWebAppConfigValue } from "../config_storage/actions";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
|
||||
export const onDrop =
|
||||
(dispatch1: Function, sequence: TaggedSequence) =>
|
||||
|
@ -49,19 +50,32 @@ export const onDrop =
|
|||
export interface SequenceSettingsMenuProps {
|
||||
dispatch: Function;
|
||||
confirmStepDeletion: boolean;
|
||||
showPins: boolean;
|
||||
}
|
||||
|
||||
export const SequenceSettingsMenu =
|
||||
({ dispatch, confirmStepDeletion }: SequenceSettingsMenuProps) =>
|
||||
({ dispatch, confirmStepDeletion, showPins }: SequenceSettingsMenuProps) =>
|
||||
<div className="sequence-settings-menu">
|
||||
<label>
|
||||
{t("Confirm step deletion")}
|
||||
</label>
|
||||
<Help text={t(Content.CONFIRM_STEP_DELETION)} />
|
||||
<ToggleButton
|
||||
toggleValue={confirmStepDeletion}
|
||||
toggleAction={() => dispatch(setWebAppConfigValue(
|
||||
BooleanSetting.confirm_step_deletion, !confirmStepDeletion))} />
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Confirm step deletion")}
|
||||
</label>
|
||||
<Help text={t(Content.CONFIRM_STEP_DELETION)} requireClick={true} />
|
||||
<ToggleButton
|
||||
toggleValue={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>;
|
||||
|
||||
interface SequenceBtnGroupProps {
|
||||
|
@ -72,11 +86,12 @@ interface SequenceBtnGroupProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
menuOpen: boolean;
|
||||
confirmStepDeletion: boolean;
|
||||
showPins: boolean;
|
||||
}
|
||||
|
||||
const SequenceBtnGroup = ({
|
||||
dispatch, sequence, syncStatus, resources, shouldDisplay, menuOpen,
|
||||
confirmStepDeletion
|
||||
confirmStepDeletion, showPins
|
||||
}: SequenceBtnGroupProps) =>
|
||||
<div className="button-group">
|
||||
<SaveBtn status={sequence.specialStatus}
|
||||
|
@ -104,6 +119,7 @@ const SequenceBtnGroup = ({
|
|||
<i className="fa fa-gear" />
|
||||
<SequenceSettingsMenu
|
||||
dispatch={dispatch}
|
||||
showPins={showPins}
|
||||
confirmStepDeletion={confirmStepDeletion} />
|
||||
</Popover>
|
||||
</div>
|
||||
|
@ -140,6 +156,7 @@ const SequenceHeader = (props: SequenceHeaderProps) => {
|
|||
resources={props.resources}
|
||||
shouldDisplay={props.shouldDisplay}
|
||||
confirmStepDeletion={props.confirmStepDeletion}
|
||||
showPins={props.showPins}
|
||||
menuOpen={props.menuOpen} />
|
||||
<SequenceNameAndColor {...sequenceAndDispatch} />
|
||||
<LocalsList
|
||||
|
@ -190,6 +207,7 @@ export class SequenceEditorMiddleActive extends
|
|||
toggleVarShow={() =>
|
||||
this.setState({ variablesCollapsed: !this.state.variablesCollapsed })}
|
||||
confirmStepDeletion={this.props.confirmStepDeletion}
|
||||
showPins={this.props.showPins}
|
||||
menuOpen={this.props.menuOpen} />
|
||||
<hr />
|
||||
<div className="sequence" id="sequenceDiv"
|
||||
|
|
|
@ -12,18 +12,22 @@ import { setActiveSequenceByName } from "./set_active_sequence_by_name";
|
|||
import { LeftPanel, CenterPanel, RightPanel } from "../ui";
|
||||
import { resourceUsageList } from "../resources/in_use";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { unselectSequence } from "./actions";
|
||||
import { unselectSequence, closeCommandMenu } from "./actions";
|
||||
import { isNumber } from "lodash";
|
||||
|
||||
const SequenceBackButton = (props: { dispatch: Function, className: string }) =>
|
||||
<Row>
|
||||
<button
|
||||
className={`back-to-sequences fb-button gray ${props.className}`}
|
||||
onClick={() => props.dispatch(unselectSequence())}>
|
||||
<i className="fa fa-arrow-left" />
|
||||
{t("back to sequences")}
|
||||
</button>
|
||||
</Row>;
|
||||
export interface SequenceBackButtonProps {
|
||||
dispatch: Function;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const SequenceBackButton = (props: SequenceBackButtonProps) => {
|
||||
const insertingStep = props.className.includes("inserting-step");
|
||||
return <i
|
||||
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)
|
||||
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 activeClasses = [sequenceOpen, insertingStep].join(" ");
|
||||
return <Page className="sequence-page">
|
||||
<SequenceBackButton className={activeClasses} dispatch={this.props.dispatch} />
|
||||
<Row>
|
||||
<LeftPanel
|
||||
className={`sequence-list-panel ${activeClasses}`}
|
||||
|
@ -53,7 +56,10 @@ export class Sequences extends React.Component<Props, {}> {
|
|||
</LeftPanel>
|
||||
<CenterPanel
|
||||
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)}>
|
||||
<SequenceEditorMiddle
|
||||
syncStatus={this.props.syncStatus}
|
||||
|
@ -64,11 +70,15 @@ export class Sequences extends React.Component<Props, {}> {
|
|||
farmwareInfo={this.props.farmwareInfo}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
confirmStepDeletion={this.props.confirmStepDeletion}
|
||||
showPins={this.props.showPins}
|
||||
menuOpen={this.props.menuOpen} />
|
||||
</CenterPanel>
|
||||
<RightPanel
|
||||
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)}
|
||||
show={sequenceSelected}>
|
||||
<StepButtonCluster
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { push } from "../history";
|
||||
import { SequencesListProps, SequencesListState } from "./interfaces";
|
||||
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 { variableList } from "./locals_list/variable_support";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { EmptyStateWrapper, EmptyStateGraphic } from "../ui/empty_state_wrapper";
|
||||
|
||||
const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean => seq
|
||||
.body
|
||||
|
@ -58,6 +58,45 @@ const sequenceList = (props: {
|
|||
</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
|
||||
React.Component<SequencesListProps, SequencesListState> {
|
||||
|
||||
|
@ -68,41 +107,28 @@ export class SequencesList extends
|
|||
onChange = (e: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
this.setState({ searchTerm: e.currentTarget.value });
|
||||
|
||||
emptySequenceBody = (): TaggedSequence["body"] => ({
|
||||
name: t("new sequence {{ num }}", { num: this.props.sequences.length }),
|
||||
args: {
|
||||
version: -999,
|
||||
locals: { kind: "scope_declaration", args: {} },
|
||||
},
|
||||
color: "gray",
|
||||
kind: "sequence",
|
||||
body: []
|
||||
});
|
||||
|
||||
render() {
|
||||
const { sequences, dispatch, resourceUsage, sequenceMetas } = this.props;
|
||||
const searchTerm = this.state.searchTerm.toLowerCase();
|
||||
return <div>
|
||||
<button
|
||||
className="fb-button green add"
|
||||
onClick={() => {
|
||||
const newSequence = this.emptySequenceBody();
|
||||
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...")} />
|
||||
<SequenceListHeader
|
||||
dispatch={dispatch}
|
||||
sequenceCount={this.props.sequences.length}
|
||||
onChange={this.onChange} />
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<div className="sequence-list">
|
||||
{sortResourcesById(sequences)
|
||||
.filter(filterFn(searchTerm))
|
||||
.map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))}
|
||||
</div>
|
||||
<EmptyStateWrapper
|
||||
notEmpty={sequences.length > 0}
|
||||
graphic={EmptyStateGraphic.sequences}
|
||||
title={t("No Sequences.")}
|
||||
text={Content.NO_SEQUENCES}>
|
||||
{sequences.length > 0 &&
|
||||
<div className="sequence-list">
|
||||
{sortResourcesById(sequences)
|
||||
.filter(filterFn(searchTerm))
|
||||
.map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))}
|
||||
</div>}
|
||||
</EmptyStateWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
|
|
|
@ -11,10 +11,11 @@ import {
|
|||
} from "../util";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
import { getFirmwareConfig, getWebAppConfig } from "../resources/getters";
|
||||
import { getFirmwareConfig } from "../resources/getters";
|
||||
import { Farmwares } from "../farmware/interfaces";
|
||||
import { manifestInfo } from "../farmware/generate_manifest_info";
|
||||
import { DevSettings } from "../account/dev/dev_support";
|
||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const uuid = props.resources.consumers.sequences.current;
|
||||
|
@ -61,16 +62,17 @@ export function mapStateToProps(props: Everything): Props {
|
|||
});
|
||||
const farmwareNames = Object.values(farmwares).map(fw => fw.name);
|
||||
const { firstPartyFarmwareNames } = props.resources.consumers.farmware;
|
||||
const conf = getWebAppConfig(props.resources.index);
|
||||
const showFirstPartyFarmware = !!(conf && conf.body.show_first_party_farmware);
|
||||
const getConfig = getWebAppConfigValue(() => props);
|
||||
const showFirstPartyFarmware =
|
||||
!!getConfig(BooleanSetting.show_first_party_farmware);
|
||||
const farmwareConfigs: FarmwareConfigs = {};
|
||||
Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config);
|
||||
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
|
||||
const confirmStepDeletion =
|
||||
!!getWebAppConfigValue(() => props)(BooleanSetting.confirm_step_deletion);
|
||||
const confirmStepDeletion = !!getConfig(BooleanSetting.confirm_step_deletion);
|
||||
const showPins = !!getConfig("show_pins" as BooleanConfigKey);
|
||||
|
||||
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
|
||||
const shouldDisplay = shouldDisplayFunc(
|
||||
|
@ -97,5 +99,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
confirmStepDeletion,
|
||||
menuOpen: props.resources.consumers.sequences.menuOpen,
|
||||
stepIndex: props.resources.consumers.sequences.stepIndex,
|
||||
showPins,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@ import * as React from "react";
|
|||
import { SequenceBodyItem as Step, TaggedSequence } from "farmbot";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { StepDragger, NULL_DRAGGER_ID } from "../../draggable/step_dragger";
|
||||
import { pushStep } from "../actions";
|
||||
import { pushStep, closeCommandMenu } from "../actions";
|
||||
import { StepButtonParams } from "../interfaces";
|
||||
import { Col } from "../../ui/index";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
export const stepClick =
|
||||
(dispatch: Function,
|
||||
|
@ -17,10 +16,7 @@ export const stepClick =
|
|||
seq
|
||||
? pushStep(step, dispatch, seq, index)
|
||||
: error(t("Select a sequence first"));
|
||||
dispatch({
|
||||
type: Actions.SET_SEQUENCE_STEP_POSITION,
|
||||
payload: undefined,
|
||||
});
|
||||
dispatch(closeCommandMenu());
|
||||
};
|
||||
|
||||
export function StepButton({ children, step, color, dispatch, current, index }:
|
||||
|
|
|
@ -117,7 +117,7 @@ describe("Pin and Peripheral support files", () => {
|
|||
s.body.label = "not displayed";
|
||||
p.body.label = "not displayed";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = pinsAsDropDownsWritePin(ri.index, () => false);
|
||||
const result = pinsAsDropDownsWritePin(ri.index, () => false, true);
|
||||
expect(JSON.stringify(result)).not.toContain("not displayed");
|
||||
});
|
||||
|
||||
|
@ -127,7 +127,7 @@ describe("Pin and Peripheral support files", () => {
|
|||
s.body.label = "not displayed";
|
||||
p.body.label = "displayed peripheral";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = pinsAsDropDownsWritePin(ri.index, () => true);
|
||||
const result = pinsAsDropDownsWritePin(ri.index, () => true, true);
|
||||
expect(JSON.stringify(result)).toContain("displayed peripheral");
|
||||
expect(JSON.stringify(result)).not.toContain("not displayed");
|
||||
});
|
||||
|
@ -140,7 +140,7 @@ describe("Pin and Peripheral support files", () => {
|
|||
s.body.label = "not displayed";
|
||||
p.body.label = "not displayed";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = pinsAsDropDownsReadPin(ri.index, () => false);
|
||||
const result = pinsAsDropDownsReadPin(ri.index, () => false, true);
|
||||
expect(JSON.stringify(result)).not.toContain("not displayed");
|
||||
});
|
||||
|
||||
|
@ -150,7 +150,7 @@ describe("Pin and Peripheral support files", () => {
|
|||
s.body.label = "displayed sensor";
|
||||
p.body.label = "displayed peripheral";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = pinsAsDropDownsReadPin(ri.index, () => true);
|
||||
const result = pinsAsDropDownsReadPin(ri.index, () => true, true);
|
||||
expect(JSON.stringify(result)).toContain("displayed sensor");
|
||||
expect(JSON.stringify(result)).toContain("displayed peripheral");
|
||||
});
|
||||
|
|
|
@ -101,18 +101,20 @@ export function pinDropdowns(
|
|||
return [PIN_HEADING, ...PIN_RANGE.map(pinNumber2DropDown(valueFormat))];
|
||||
}
|
||||
|
||||
export const pinsAsDropDownsWritePin =
|
||||
(input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [
|
||||
export const pinsAsDropDownsWritePin = (
|
||||
input: ResourceIndex, shouldDisplay: ShouldDisplay, showPins: boolean
|
||||
): DropDownItem[] => [
|
||||
...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []),
|
||||
...(shouldDisplay(Feature.rpi_led_control) ? boxLedsAsDropDowns() : []),
|
||||
...pinDropdowns(n => n),
|
||||
...(showPins ? pinDropdowns(n => n) : []),
|
||||
];
|
||||
|
||||
export const pinsAsDropDownsReadPin =
|
||||
(input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [
|
||||
export const pinsAsDropDownsReadPin = (
|
||||
input: ResourceIndex, shouldDisplay: ShouldDisplay, showPins: boolean
|
||||
): DropDownItem[] => [
|
||||
...(shouldDisplay(Feature.named_pins) ? sensorsAsDropDowns(input) : []),
|
||||
...(shouldDisplay(Feature.named_pins) ? peripheralsAsDropDowns(input) : []),
|
||||
...pinDropdowns(n => n),
|
||||
...(showPins ? pinDropdowns(n => n) : []),
|
||||
];
|
||||
|
||||
const TYPE_MAPPING: Record<AllowedPinTypes, PinGroupName | BoxLed> = {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { StepInputBox } from "../inputs/step_input_box";
|
||||
import { StepParams } from "../interfaces";
|
||||
import { ToolTips } from "../../constants";
|
||||
|
@ -48,7 +47,7 @@ export function TileReadPin(props: StepParams) {
|
|||
selectedItem={celery2DropDown(pin_number, props.resources)}
|
||||
onChange={setArgsDotPinNumber(props)}
|
||||
list={pinsAsDropDownsReadPin(props.resources,
|
||||
shouldDisplay || (() => false))} />
|
||||
shouldDisplay || (() => false), !!props.showPins)} />
|
||||
</Col>
|
||||
<PinMode {...props} />
|
||||
<Col xs={6} md={3}>
|
||||
|
|
|
@ -56,7 +56,7 @@ export function TileWritePin(props: StepParams) {
|
|||
selectedItem={celery2DropDown(pin_number, props.resources)}
|
||||
onChange={setArgsDotPinNumber(props)}
|
||||
list={pinsAsDropDownsWritePin(props.resources,
|
||||
shouldDisplay || (() => false))} />
|
||||
shouldDisplay || (() => false), !!props.showPins)} />
|
||||
</Col>
|
||||
<PinMode {...props} />
|
||||
<Col xs={6} md={3}>
|
||||
|
|
|
@ -9,11 +9,13 @@ interface CenterProps {
|
|||
helpText: string;
|
||||
width?: number;
|
||||
docPage?: DocSlug;
|
||||
backButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CenterPanel(props: CenterProps) {
|
||||
return <Col sm={props.width || 6} lg={6}>
|
||||
<div className={props.className}>
|
||||
{props.backButton}
|
||||
<h3>
|
||||
<i>{t(props.title)}</i>
|
||||
</h3>
|
||||
|
|
|
@ -3,12 +3,14 @@ import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core";
|
|||
|
||||
interface HelpProps {
|
||||
text: string;
|
||||
requireClick?: boolean;
|
||||
}
|
||||
|
||||
export function Help(props: HelpProps) {
|
||||
return <Popover
|
||||
position={Position.LEFT_TOP}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
interactionKind={props.requireClick
|
||||
? PopoverInteractionKind.CLICK : PopoverInteractionKind.HOVER}
|
||||
popoverClassName={"help"} >
|
||||
<i className="fa fa-question-circle help-icon"></i>
|
||||
<div>{props.text}</div>
|
||||
|
|
|
@ -9,12 +9,14 @@ interface RightPanelProps {
|
|||
helpText: string;
|
||||
show: Boolean | undefined;
|
||||
width?: number;
|
||||
backButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function RightPanel(props: RightPanelProps) {
|
||||
return <Col sm={props.width || 3} lg={3}>
|
||||
{props.show &&
|
||||
<div className={props.className}>
|
||||
{props.backButton}
|
||||
<h3>
|
||||
<i>{t(props.title)}</i>
|
||||
</h3>
|
||||
|
|
Loading…
Reference in New Issue