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 = "";
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,
};
};

View File

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

View File

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

View File

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

View File

@ -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.`);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", () => {

View File

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

View File

@ -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" },

View File

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

View File

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

View File

@ -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 });
});
});

View File

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

View File

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

View File

@ -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");
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ describe("NavBar", () => {
getConfigValue: jest.fn(),
tour: undefined,
device: fakeDevice(),
autoSync: false,
});
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 { 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");
});
});

View File

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

View File

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

View File

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

View File

@ -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
});

View File

@ -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."));
}

View File

@ -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())} />

View File

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

View File

@ -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");
});
});

View File

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

View File

@ -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);
});
});

View File

@ -27,6 +27,7 @@ describe("<SequenceEditorMiddle/>", () => {
shouldDisplay: jest.fn(),
confirmStepDeletion: 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 { 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");
});

View File

@ -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
});

View File

@ -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,
});

View File

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

View File

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

View File

@ -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)" */

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
};
}

View File

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

View File

@ -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");
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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