mobile panel navigation
parent
b6eff330ab
commit
ab715c1bba
|
@ -796,15 +796,18 @@ export enum Actions {
|
|||
SELECT_REGIMEN = "SELECT_REGIMEN",
|
||||
SET_SEQUENCE = "SET_SEQUENCE",
|
||||
SET_TIME_OFFSET = "SET_TIME_OFFSET",
|
||||
SET_SCHEDULER_STATE = "SET_SCHEDULER_STATE",
|
||||
|
||||
// Sequences
|
||||
SELECT_SEQUENCE = "SELECT_SEQUENCE",
|
||||
SET_SEQUENCE_POPUP_STATE = "SET_SEQUENCE_POPUP_STATE",
|
||||
SET_SEQUENCE_STEP_POSITION = "SET_SEQUENCE_STEP_POSITION",
|
||||
|
||||
// Farmware
|
||||
SELECT_FARMWARE = "SELECT_FARMWARE",
|
||||
SELECT_IMAGE = "SELECT_IMAGE",
|
||||
FETCH_FIRST_PARTY_FARMWARE_NAMES_OK = "FETCH_FIRST_PARTY_FARMWARE_NAMES_OK",
|
||||
SET_FARMWARE_INFO_STATE = "SET_FARMWARE_INFO_STATE",
|
||||
|
||||
// App
|
||||
START_TOUR = "START_TOUR",
|
||||
|
|
|
@ -267,18 +267,20 @@ a {
|
|||
}
|
||||
|
||||
.drag-drop-area {
|
||||
margin: 0.75rem 0;
|
||||
margin-right: 15px;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
border-color: $light_gray;
|
||||
color: $gray;
|
||||
font-weight: bold;
|
||||
padding: 1.25rem;
|
||||
background: $off_white;
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
font-weight: bold;
|
||||
&.visible {
|
||||
margin: 0.75rem 0;
|
||||
margin-right: 15px;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
border-color: $light_gray;
|
||||
color: $gray;
|
||||
font-weight: bold;
|
||||
padding: 1.25rem;
|
||||
background: $off_white;
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.hardware-widget,
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
// Bulk Scheduler
|
||||
.bulk-scheduler {
|
||||
@media screen and (max-width: 768px) {
|
||||
display: none;
|
||||
margin-bottom: 3rem;
|
||||
&.open {
|
||||
&.inserting-item {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.week-grid-meta-buttons {
|
||||
margin-top: 1rem;
|
||||
|
@ -61,4 +67,13 @@
|
|||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.open-bulk-scheduler-btn {
|
||||
display: none;
|
||||
@media screen and (max-width: 768px) {
|
||||
display: block;
|
||||
margin: auto;
|
||||
float: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Regimen List styles in sequences.scss
|
||||
|
|
|
@ -5,6 +5,15 @@
|
|||
height: calc(100vh - 5rem);
|
||||
background: $light_gray;
|
||||
@media screen and (max-width: 768px) {
|
||||
display: none;
|
||||
&.open {
|
||||
display: block;
|
||||
&.farmware-info-open,
|
||||
&.inserting-item,
|
||||
&.inserting-step {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
padding: 3rem 1.5rem 8rem;
|
||||
|
@ -138,6 +147,11 @@
|
|||
.farmware-info-panel,
|
||||
.step-button-cluster-panel {
|
||||
@media screen and (max-width: 974px) {
|
||||
display: none;
|
||||
&.farmware-info-open,
|
||||
&.inserting-step {
|
||||
display: block;
|
||||
}
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
@ -185,6 +199,9 @@
|
|||
margin-bottom: 3rem;
|
||||
margin-right: 5px;
|
||||
@media screen and (max-width: 974px) {
|
||||
&.open {
|
||||
display: none;
|
||||
}
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
@ -217,3 +234,59 @@
|
|||
.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) {
|
||||
display: block;
|
||||
margin: 4rem;
|
||||
margin-top: 0;
|
||||
margin-left: 2rem;
|
||||
float: left !important;
|
||||
i {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&.inserting-step {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-drop-area {
|
||||
@media screen and (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.add-command-button-container {
|
||||
display: none;
|
||||
@media screen and (max-width: 768px) {
|
||||
display: block;
|
||||
min-height: 3rem;
|
||||
.add-command {
|
||||
display: block;
|
||||
margin: auto;
|
||||
margin-top: 1rem;
|
||||
float: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.farmware-info-button {
|
||||
display: none;
|
||||
@media screen and (max-width: 768px) {
|
||||
&.open {
|
||||
display: block;
|
||||
margin: 4rem;
|
||||
margin-top: 0;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
&.farmware-info-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -227,6 +227,7 @@ export interface FarmwareProps {
|
|||
saveFarmwareEnv: SaveFarmwareEnv;
|
||||
taggedFarmwareInstallations: TaggedFarmwareInstallation[];
|
||||
imageJobs: JobProgress[];
|
||||
infoOpen: boolean;
|
||||
}
|
||||
|
||||
export interface HardwareSettingsProps {
|
||||
|
|
|
@ -26,9 +26,9 @@ export class DropArea extends React.Component<DropAreaProps, DropAreaState> {
|
|||
|
||||
render() {
|
||||
const isVisible = this.props.isLocked || this.state.isHovered;
|
||||
const klass = isVisible ? "drag-drop-area" : "";
|
||||
const visible = isVisible ? "visible" : "";
|
||||
return <div
|
||||
className={klass}
|
||||
className={`drag-drop-area ${visible}`}
|
||||
onDragLeave={this.toggle}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -16,6 +16,7 @@ import { FarmwarePage, BasicFarmwarePage } from "../index";
|
|||
import { FarmwareProps } from "../../devices/interfaces";
|
||||
import { fakeFarmware, fakeFarmwares } from "../../__test_support__/fake_farmwares";
|
||||
import { clickButton } from "../../__test_support__/helpers";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
describe("<FarmwarePage />", () => {
|
||||
const fakeProps = (): FarmwareProps => {
|
||||
|
@ -36,6 +37,7 @@ describe("<FarmwarePage />", () => {
|
|||
saveFarmwareEnv: jest.fn(),
|
||||
taggedFarmwareInstallations: [],
|
||||
imageJobs: [],
|
||||
infoOpen: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -102,9 +104,50 @@ describe("<FarmwarePage />", () => {
|
|||
p.farmwares["My Fake Test Farmware"] = farmware;
|
||||
p.currentFarmware = "My Fake Test Farmware";
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
clickButton(wrapper, 1, "Run");
|
||||
clickButton(wrapper, 3, "Run");
|
||||
expect(mockDevice.execScript).toHaveBeenCalledWith("My Fake Test Farmware");
|
||||
});
|
||||
|
||||
it("shows Farmware info", () => {
|
||||
const p = fakeProps();
|
||||
p.botToMqttStatus = "up";
|
||||
p.infoOpen = true;
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
expect(wrapper.html()).toContain("farmware-info-open");
|
||||
});
|
||||
|
||||
it("opens Farmware list", () => {
|
||||
const p = fakeProps();
|
||||
p.botToMqttStatus = "up";
|
||||
p.infoOpen = false;
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
clickButton(wrapper, 0, "farmware list");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_FARMWARE, payload: undefined
|
||||
});
|
||||
});
|
||||
|
||||
it("closes Farmware info", () => {
|
||||
const p = fakeProps();
|
||||
p.botToMqttStatus = "up";
|
||||
p.infoOpen = true;
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
clickButton(wrapper, 0, "back");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_FARMWARE_INFO_STATE, payload: false
|
||||
});
|
||||
});
|
||||
|
||||
it("opens Farmware info", () => {
|
||||
const p = fakeProps();
|
||||
p.botToMqttStatus = "up";
|
||||
p.infoOpen = false;
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
clickButton(wrapper, 1, "farmware info");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_FARMWARE_INFO_STATE, payload: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<BasicFarmwarePage />", () => {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
|
||||
import { farmwareReducer } from "../reducer";
|
||||
import { FarmwareState } from "../interfaces";
|
||||
import { Actions } from "../../constants";
|
||||
import { fakeImage, fakeFarmwareInstallation } from "../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeImage, fakeFarmwareInstallation
|
||||
} from "../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("farmwareReducer", () => {
|
||||
const fakeState = (): FarmwareState => {
|
||||
return {
|
||||
currentFarmware: undefined,
|
||||
currentImage: undefined,
|
||||
firstPartyFarmwareNames: []
|
||||
firstPartyFarmwareNames: [],
|
||||
infoOpen: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -82,4 +84,14 @@ describe("farmwareReducer", () => {
|
|||
.not.toEqual(newState.firstPartyFarmwareNames);
|
||||
expect(newState.firstPartyFarmwareNames).toEqual(FARMWARE_NAMES);
|
||||
});
|
||||
|
||||
it("sets the farmware info panel state", () => {
|
||||
const oldState = fakeState();
|
||||
const newState = farmwareReducer(oldState, {
|
||||
type: Actions.SET_FARMWARE_INFO_STATE,
|
||||
payload: true
|
||||
});
|
||||
expect(oldState.infoOpen).toBeFalsy();
|
||||
expect(newState.infoOpen).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
Page, Row, LeftPanel, CenterPanel, RightPanel, DocSlug
|
||||
Page, Row, LeftPanel, CenterPanel, RightPanel, DocSlug, Col
|
||||
} from "../ui/index";
|
||||
import { mapStateToProps, isPendingInstallation } from "./state_to_props";
|
||||
import { Photos } from "./images/photos";
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
} from "./farmware_forms";
|
||||
import { urlFriendly } from "../util";
|
||||
import { history } from "../history";
|
||||
import { ToolTips } from "../constants";
|
||||
import { ToolTips, Actions } from "../constants";
|
||||
import { FarmwareInfo } from "./farmware_info";
|
||||
import { Farmwares, FarmwareManifestInfo } from "./interfaces";
|
||||
import { commandErr } from "../devices/actions";
|
||||
|
@ -112,6 +112,10 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.dispatch({
|
||||
type: Actions.SELECT_FARMWARE,
|
||||
payload: "Photos"
|
||||
});
|
||||
if (!this.current && Object.values(this.props.farmwares).length > 0) {
|
||||
const farmwareNames = Object.values(this.props.farmwares).map(x => x.name);
|
||||
setActiveFarmwareByName(farmwareNames);
|
||||
|
@ -173,13 +177,54 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
FarmwareBackButton = (props: { className: string }) => {
|
||||
const infoOpen = props.className.includes("farmware-info-open");
|
||||
return <Row>
|
||||
<button
|
||||
className={`back-to-farmware fb-button gray ${props.className}`}
|
||||
onClick={() => infoOpen
|
||||
? this.props.dispatch({
|
||||
type: Actions.SET_FARMWARE_INFO_STATE, payload: false
|
||||
})
|
||||
: this.props.dispatch({
|
||||
type: Actions.SELECT_FARMWARE, payload: undefined
|
||||
})}>
|
||||
{infoOpen ? t("back") : t("farmware list")}
|
||||
</button>
|
||||
</Row>;
|
||||
};
|
||||
|
||||
FarmwareInfoButton = (props: { className: string, online: boolean }) =>
|
||||
<Row>
|
||||
<button
|
||||
className={`farmware-info-button fb-button gray ${props.className}`}
|
||||
disabled={!props.online}
|
||||
onClick={() => this.props.dispatch({
|
||||
type: Actions.SET_FARMWARE_INFO_STATE, payload: true
|
||||
})}>
|
||||
{t("farmware info")}
|
||||
</button>
|
||||
</Row>;
|
||||
|
||||
render() {
|
||||
const farmware = getFarmwareByName(
|
||||
this.props.farmwares, this.current || "take-photo");
|
||||
const farmwareOpen = this.current ? "open" : "";
|
||||
const online = this.props.botToMqttStatus === "up";
|
||||
const infoOpen = (this.props.infoOpen && online) ? "farmware-info-open" : "";
|
||||
const activeClasses = [farmwareOpen, infoOpen].join(" ");
|
||||
return <Page className="farmware-page">
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<this.FarmwareBackButton className={activeClasses} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<this.FarmwareInfoButton className={activeClasses} online={online} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<LeftPanel
|
||||
className="farmware-list-panel"
|
||||
className={`farmware-list-panel ${activeClasses}`}
|
||||
title={t("Farmware")}
|
||||
helpText={ToolTips.FARMWARE_LIST}>
|
||||
<FarmwareList
|
||||
|
@ -192,7 +237,7 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
showFirstParty={!!this.props.webAppConfig.show_first_party_farmware} />
|
||||
</LeftPanel>
|
||||
<CenterPanel
|
||||
className="farmware-input-panel"
|
||||
className={`farmware-input-panel ${activeClasses}`}
|
||||
title={this.current || t("Photos")}
|
||||
helpText={getToolTipByFarmware(this.props.farmwares, this.current)
|
||||
|| ToolTips.PHOTOS}
|
||||
|
@ -202,7 +247,7 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
</div>}
|
||||
</CenterPanel>
|
||||
<RightPanel
|
||||
className="farmware-info-panel"
|
||||
className={`farmware-info-panel ${activeClasses}`}
|
||||
title={t("Information")}
|
||||
helpText={ToolTips.FARMWARE_INFO}
|
||||
show={!!farmware}>
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface FarmwareState {
|
|||
currentFarmware: string | undefined;
|
||||
currentImage: string | undefined;
|
||||
firstPartyFarmwareNames: string[];
|
||||
infoOpen: boolean;
|
||||
}
|
||||
|
||||
export type FarmwareManifestEntry = Record<"name" | "manifest", string>;
|
||||
|
|
|
@ -6,7 +6,8 @@ import { Actions } from "../constants";
|
|||
export let farmwareState: FarmwareState = {
|
||||
currentFarmware: undefined,
|
||||
currentImage: undefined,
|
||||
firstPartyFarmwareNames: []
|
||||
firstPartyFarmwareNames: [],
|
||||
infoOpen: false,
|
||||
};
|
||||
|
||||
export let farmwareReducer = generateReducer<FarmwareState>(farmwareState)
|
||||
|
@ -33,4 +34,8 @@ export let farmwareReducer = generateReducer<FarmwareState>(farmwareState)
|
|||
const thisUUID = s.currentImage;
|
||||
if (thisUUID === thatUUID) { s.currentImage = undefined; }
|
||||
return s;
|
||||
})
|
||||
.add<boolean>(Actions.SET_FARMWARE_INFO_STATE, (s, { payload }) => {
|
||||
s.infoOpen = payload;
|
||||
return s;
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
|||
|| firstImage;
|
||||
const botStateFarmwares = props.bot.hardware.process_info.farmwares;
|
||||
const conf = getWebAppConfig(props.resources.index);
|
||||
const { currentFarmware, firstPartyFarmwareNames } =
|
||||
const { currentFarmware, firstPartyFarmwareNames, infoOpen } =
|
||||
props.resources.consumers.farmware;
|
||||
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
|
@ -129,5 +129,6 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
|||
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
|
||||
taggedFarmwareInstallations,
|
||||
imageJobs,
|
||||
infoOpen,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ describe("<WeedDetector />", () => {
|
|||
saveFarmwareEnv: jest.fn(),
|
||||
taggedFarmwareInstallations: [],
|
||||
imageJobs: [],
|
||||
infoOpen: false,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { editRegimen, selectRegimen } from "../actions";
|
||||
import { editRegimen, selectRegimen, unselectRegimen } from "../actions";
|
||||
import { fakeRegimen } from "../../__test_support__/fake_state/resources";
|
||||
import { Actions } from "../../constants";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
|
@ -40,3 +40,11 @@ describe("selectRegimen()", () => {
|
|||
expect(() => selectRegimen("wrong")).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe("unselectRegimen()", () => {
|
||||
it("deselects regimen", () => {
|
||||
expect(unselectRegimen()).toEqual({
|
||||
type: Actions.SELECT_REGIMEN, payload: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,8 @@ 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 />", () => {
|
||||
function fakeProps(): Props {
|
||||
|
@ -35,6 +37,7 @@ describe("<Regimens />", () => {
|
|||
regimenUsageStats: {},
|
||||
shouldDisplay: () => false,
|
||||
variableData: {},
|
||||
schedulerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -50,4 +53,31 @@ describe("<Regimens />", () => {
|
|||
const wrapper = mount(<Regimens {...p} />);
|
||||
expect(wrapper.text()).not.toContain("Scheduler");
|
||||
});
|
||||
|
||||
it("shows scheduler", () => {
|
||||
const p = fakeProps();
|
||||
p.schedulerOpen = true;
|
||||
const wrapper = mount(<Regimens {...p} />);
|
||||
expect(wrapper.html()).toContain("inserting-item");
|
||||
});
|
||||
|
||||
it("returns to regimen", () => {
|
||||
const p = fakeProps();
|
||||
p.schedulerOpen = true;
|
||||
const wrapper = mount(<Regimens {...p} />);
|
||||
clickButton(wrapper, 0, "back to regimen");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SCHEDULER_STATE, payload: false
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to regimen list", () => {
|
||||
const p = fakeProps();
|
||||
p.schedulerOpen = false;
|
||||
const wrapper = mount(<Regimens {...p} />);
|
||||
clickButton(wrapper, 0, "back to regimens");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_REGIMEN, payload: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,10 +5,10 @@ import { popWeek, pushWeek, selectDays, deselectDays } from "../bulk_scheduler/a
|
|||
import { defensiveClone } from "../../util";
|
||||
|
||||
const STATE: RegimenState = {
|
||||
"dailyOffsetMs": 300000,
|
||||
"selectedSequenceUUID": "Sequence.71.167",
|
||||
"currentRegimen": "Regimen.4.56",
|
||||
"weeks": [
|
||||
dailyOffsetMs: 300000,
|
||||
selectedSequenceUUID: "Sequence.71.167",
|
||||
currentRegimen: "Regimen.4.56",
|
||||
weeks: [
|
||||
{
|
||||
"days": {
|
||||
"day1": true,
|
||||
|
@ -20,7 +20,8 @@ const STATE: RegimenState = {
|
|||
"day7": false
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
schedulerOpen: false,
|
||||
};
|
||||
|
||||
describe("Regimens reducer", () => {
|
||||
|
@ -123,3 +124,16 @@ describe("SET_TIME_OFFSET", () => {
|
|||
expect(nextState.dailyOffsetMs).toBe(action.payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_SCHEDULER_STATE", () => {
|
||||
it("sets schedulerOpen", () => {
|
||||
const state = defensiveClone(STATE);
|
||||
state.schedulerOpen = false;
|
||||
const action = {
|
||||
type: Actions.SET_SCHEDULER_STATE,
|
||||
payload: true
|
||||
};
|
||||
const nextState = regimensReducer(STATE, action);
|
||||
expect(nextState.schedulerOpen).toBe(action.payload);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,3 +19,7 @@ export function selectRegimen(payload: string): SelectRegimen {
|
|||
throw new Error("Not a regimen.");
|
||||
}
|
||||
}
|
||||
|
||||
export const unselectRegimen = () => ({
|
||||
type: Actions.SELECT_REGIMEN, payload: undefined
|
||||
});
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
import { VariableDeclaration } from "farmbot";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("<ActiveEditor />", () => {
|
||||
const fakeProps = (): ActiveEditorProps => ({
|
||||
|
@ -53,6 +55,15 @@ describe("<ActiveEditor />", () => {
|
|||
expect(overwrite).toHaveBeenCalledWith(expect.any(Object),
|
||||
expect.objectContaining({ regimen_items: [keptItem] }));
|
||||
});
|
||||
|
||||
it("opens scheduler", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<ActiveEditor {...p} />);
|
||||
clickButton(wrapper, 3, "Schedule item");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SCHEDULER_STATE, payload: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("editRegimenVariables()", () => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { RegimenNameInput } from "./regimen_name_input";
|
||||
import { ActiveEditorProps } from "./interfaces";
|
||||
|
||||
import { push } from "../../history";
|
||||
import {
|
||||
RegimenItem, CalendarRow, RegimenItemCalendarRow, RegimenProps
|
||||
|
@ -17,6 +16,7 @@ import {
|
|||
} from "../../sequences/locals_list/locals_list_support";
|
||||
import { addOrEditBodyVariables } from "../../sequences/locals_list/handle_select";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
/**
|
||||
* The bottom half of the regimen editor panel (when there's something to
|
||||
|
@ -39,10 +39,19 @@ export function ActiveEditor(props: ActiveEditorProps) {
|
|||
shouldDisplay={props.shouldDisplay} />
|
||||
<hr />
|
||||
</div>
|
||||
<OpenSchedulerButton dispatch={props.dispatch} />
|
||||
<RegimenRows calendar={props.calendar} dispatch={props.dispatch} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
export const OpenSchedulerButton = (props: { dispatch: Function }) =>
|
||||
<button className="open-bulk-scheduler-btn fb-button gray"
|
||||
onClick={() => props.dispatch({
|
||||
type: Actions.SET_SCHEDULER_STATE, payload: true
|
||||
})}>
|
||||
{t("Schedule item")}
|
||||
</button>;
|
||||
|
||||
export const editRegimenVariables = (props: RegimenProps) =>
|
||||
(bodyVariables: VariableNode[]) =>
|
||||
(variable: ScopeDeclarationBodyItem) => {
|
||||
|
|
|
@ -8,9 +8,23 @@ import { Page, Row, LeftPanel, CenterPanel, RightPanel } from "../ui/index";
|
|||
import { mapStateToProps } from "./state_to_props";
|
||||
import { isTaggedRegimen } from "../resources/tagged_resources";
|
||||
import { setActiveRegimenByName } from "./set_active_regimen_by_name";
|
||||
|
||||
import { ToolTips } from "../constants";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { ToolTips, Actions } from "../constants";
|
||||
import { unselectRegimen } from "./actions";
|
||||
|
||||
const RegimenBackButton = (props: { dispatch: Function, className: string }) => {
|
||||
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>;
|
||||
};
|
||||
|
||||
@connect(mapStateToProps)
|
||||
export class Regimens extends React.Component<Props, {}> {
|
||||
|
@ -21,10 +35,14 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
render() {
|
||||
const { current, calendar } = this.props;
|
||||
const regimenSelected = current && isTaggedRegimen(current) && calendar;
|
||||
const regimenOpen = regimenSelected ? "open" : "";
|
||||
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"
|
||||
className={`regimen-list-panel ${activeClasses}`}
|
||||
title={t("Regimens")}
|
||||
helpText={t(ToolTips.REGIMEN_LIST)}>
|
||||
<RegimensList
|
||||
|
@ -34,7 +52,7 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
regimen={this.props.current} />
|
||||
</LeftPanel>
|
||||
<CenterPanel
|
||||
className="regimen-editor-panel"
|
||||
className={`regimen-editor-panel ${activeClasses}`}
|
||||
title={t("Regimen Editor")}
|
||||
helpText={t(ToolTips.REGIMEN_EDITOR)}
|
||||
width={5}>
|
||||
|
@ -47,7 +65,7 @@ export class Regimens extends React.Component<Props, {}> {
|
|||
shouldDisplay={this.props.shouldDisplay} />
|
||||
</CenterPanel>
|
||||
<RightPanel
|
||||
className="bulk-scheduler"
|
||||
className={`bulk-scheduler ${activeClasses}`}
|
||||
title={t("Scheduler")}
|
||||
helpText={t(ToolTips.BULK_SCHEDULER)}
|
||||
show={!!regimenSelected} width={4}>
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface Props {
|
|||
calendar: CalendarRow[];
|
||||
regimenUsageStats: Record<UUID, boolean | undefined>;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
schedulerOpen: boolean;
|
||||
}
|
||||
|
||||
export interface RegimenItemCalendarRow {
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface RegimenState {
|
|||
weeks: Week[];
|
||||
selectedSequenceUUID?: string | undefined;
|
||||
currentRegimen?: string | undefined;
|
||||
schedulerOpen: boolean;
|
||||
}
|
||||
|
||||
function newWeek() {
|
||||
|
@ -30,7 +31,8 @@ function newState(): RegimenState {
|
|||
dailyOffsetMs: 300000,
|
||||
weeks: times(10, newWeek),
|
||||
selectedSequenceUUID: undefined,
|
||||
currentRegimen: undefined
|
||||
currentRegimen: undefined,
|
||||
schedulerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -80,4 +82,8 @@ export let regimensReducer = generateReducer<RegimenState>(initialState)
|
|||
.add<number>(Actions.SET_TIME_OFFSET, (s, { payload }) => {
|
||||
s.dailyOffsetMs = payload;
|
||||
return s;
|
||||
})
|
||||
.add<boolean>(Actions.SET_SCHEDULER_STATE, (s, { payload }) => {
|
||||
s.schedulerOpen = payload;
|
||||
return s;
|
||||
});
|
||||
|
|
|
@ -25,8 +25,9 @@ import { DevSettings } from "../account/dev/dev_support";
|
|||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const { resources, dispatch, bot } = props;
|
||||
const { weeks, dailyOffsetMs, selectedSequenceUUID, currentRegimen } =
|
||||
resources.consumers.regimens;
|
||||
const {
|
||||
weeks, dailyOffsetMs, selectedSequenceUUID, currentRegimen, schedulerOpen
|
||||
} = resources.consumers.regimens;
|
||||
const { index } = resources;
|
||||
const current = maybeGetRegimen(index, currentRegimen);
|
||||
const calendar = current ?
|
||||
|
@ -67,6 +68,7 @@ export function mapStateToProps(props: Everything): Props {
|
|||
calendar,
|
||||
regimenUsageStats: resourceUsageList(props.resources.index.inUse),
|
||||
shouldDisplay,
|
||||
schedulerOpen,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ jest.mock("../../history", () => ({ push: jest.fn() }));
|
|||
|
||||
jest.mock("../../api/crud", () => ({
|
||||
init: jest.fn(),
|
||||
edit: jest.fn()
|
||||
edit: jest.fn(),
|
||||
overwrite: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../set_active_sequence_by_name", () => ({
|
||||
|
@ -10,13 +11,14 @@ jest.mock("../set_active_sequence_by_name", () => ({
|
|||
}));
|
||||
|
||||
import {
|
||||
copySequence, editCurrentSequence, selectSequence
|
||||
copySequence, editCurrentSequence, selectSequence, pushStep
|
||||
} from "../actions";
|
||||
import { fakeSequence } from "../../__test_support__/fake_state/resources";
|
||||
import { init, edit } from "../../api/crud";
|
||||
import { init, edit, overwrite } from "../../api/crud";
|
||||
import { push } from "../../history";
|
||||
import { Actions } from "../../constants";
|
||||
import { setActiveSequenceByName } from "../set_active_sequence_by_name";
|
||||
import { TakePhoto, Wait } from "farmbot";
|
||||
|
||||
describe("copySequence()", () => {
|
||||
it("copies sequence", () => {
|
||||
|
@ -57,3 +59,44 @@ describe("selectSequence()", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pushStep()", () => {
|
||||
const step = (n: number): Wait => ({ kind: "wait", args: { milliseconds: n } });
|
||||
const NEW_STEP: TakePhoto = { kind: "take_photo", args: {} };
|
||||
|
||||
it("adds step at 2", () => {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.body = [
|
||||
step(1),
|
||||
step(2),
|
||||
step(3),
|
||||
];
|
||||
pushStep(NEW_STEP, jest.fn(), sequence, 2);
|
||||
expect(overwrite).toHaveBeenCalledWith(sequence, expect.objectContaining({
|
||||
body: [
|
||||
step(1),
|
||||
step(2),
|
||||
NEW_STEP,
|
||||
step(3),
|
||||
]
|
||||
}));
|
||||
});
|
||||
|
||||
it("adds step at end", () => {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.body = [
|
||||
step(1),
|
||||
step(2),
|
||||
step(3),
|
||||
];
|
||||
pushStep(NEW_STEP, jest.fn(), sequence);
|
||||
expect(overwrite).toHaveBeenCalledWith(sequence, expect.objectContaining({
|
||||
body: [
|
||||
step(1),
|
||||
step(2),
|
||||
step(3),
|
||||
NEW_STEP,
|
||||
]
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,9 @@ describe("sequence reducer", () => {
|
|||
after: string | undefined) {
|
||||
const sequence = fakeSequence();
|
||||
sequence.uuid = "sequence";
|
||||
const state: SequenceReducerState = { current: before, menuOpen: false };
|
||||
const state: SequenceReducerState = {
|
||||
current: before, menuOpen: false, stepIndex: undefined
|
||||
};
|
||||
const action = { type: actionType, payload: sequence };
|
||||
const stateAfter = sequenceReducer(state, action);
|
||||
expect(stateAfter.current).toBe(after);
|
||||
|
@ -21,9 +23,20 @@ describe("sequence reducer", () => {
|
|||
});
|
||||
|
||||
it("sets current sequence with string", () => {
|
||||
const state: SequenceReducerState = { current: undefined, menuOpen: false };
|
||||
const state: SequenceReducerState = {
|
||||
current: undefined, menuOpen: false, stepIndex: undefined
|
||||
};
|
||||
const action = { type: Actions.SELECT_SEQUENCE, payload: "sequence" };
|
||||
const stateAfter = sequenceReducer(state, action);
|
||||
expect(stateAfter.current).toBe("sequence");
|
||||
});
|
||||
|
||||
it("sets step position", () => {
|
||||
const state: SequenceReducerState = {
|
||||
current: undefined, menuOpen: false, stepIndex: undefined
|
||||
};
|
||||
const action = { type: Actions.SET_SEQUENCE_STEP_POSITION, payload: 1 };
|
||||
const stateAfter = sequenceReducer(state, action);
|
||||
expect(stateAfter.stepIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ jest.mock("../locals_list/locals_list", () => ({
|
|||
|
||||
import * as React from "react";
|
||||
import {
|
||||
SequenceEditorMiddleActive, onDrop, SequenceNameAndColor
|
||||
SequenceEditorMiddleActive, onDrop, SequenceNameAndColor, AddCommandButton
|
||||
} from "../sequence_editor_middle_active";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { ActiveMiddleProps, SequenceHeaderProps } from "../interfaces";
|
||||
|
@ -45,6 +45,7 @@ import { execSequence } from "../../devices/actions";
|
|||
import { clickButton } from "../../__test_support__/helpers";
|
||||
import { fakeVariableNameSet } from "../../__test_support__/fake_variables";
|
||||
import { DropAreaProps } from "../../draggable/interfaces";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
describe("<SequenceEditorMiddleActive/>", () => {
|
||||
const fakeProps = (): ActiveMiddleProps => {
|
||||
|
@ -228,3 +229,15 @@ describe("<SequenceNameAndColor />", () => {
|
|||
{ color: "red" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AddCommandButton />", () => {
|
||||
it("dispatches new step position", () => {
|
||||
const dispatch = jest.fn();
|
||||
const wrapper = shallow(<AddCommandButton dispatch={dispatch} index={1} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SEQUENCE_STEP_POSITION,
|
||||
payload: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,39 +2,44 @@ jest.mock("react-redux", () => ({
|
|||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../history", () => ({
|
||||
push: jest.fn(),
|
||||
history: { getCurrentLocation: () => "" },
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { Sequences } from "../sequences";
|
||||
import { shallow } from "enzyme";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { Props } from "../interfaces";
|
||||
import {
|
||||
FAKE_RESOURCES, buildResourceIndex
|
||||
} from "../../__test_support__/resource_index_builder";
|
||||
import { fakeSequence } from "../../__test_support__/fake_state/resources";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { ToolTips, Actions } from "../../constants";
|
||||
import {
|
||||
fakeHardwareFlags
|
||||
} from "../../__test_support__/sequence_hardware_settings";
|
||||
import { push } from "../../history";
|
||||
|
||||
describe("<Sequences/>", () => {
|
||||
function fakeProps(): Props {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
sequence: fakeSequence(),
|
||||
sequences: [],
|
||||
resources: buildResourceIndex(FAKE_RESOURCES).index,
|
||||
syncStatus: "synced",
|
||||
hardwareFlags: fakeHardwareFlags(),
|
||||
farmwareInfo: {
|
||||
farmwareNames: [],
|
||||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false,
|
||||
farmwareConfigs: {},
|
||||
},
|
||||
shouldDisplay: jest.fn(),
|
||||
confirmStepDeletion: false,
|
||||
menuOpen: false,
|
||||
};
|
||||
}
|
||||
const fakeProps = (): Props => ({
|
||||
dispatch: jest.fn(),
|
||||
sequence: fakeSequence(),
|
||||
sequences: [],
|
||||
resources: buildResourceIndex(FAKE_RESOURCES).index,
|
||||
syncStatus: "synced",
|
||||
hardwareFlags: fakeHardwareFlags(),
|
||||
farmwareInfo: {
|
||||
farmwareNames: [],
|
||||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false,
|
||||
farmwareConfigs: {},
|
||||
},
|
||||
shouldDisplay: jest.fn(),
|
||||
confirmStepDeletion: false,
|
||||
menuOpen: false,
|
||||
stepIndex: undefined,
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<Sequences {...fakeProps()} />);
|
||||
|
@ -50,4 +55,21 @@ describe("<Sequences/>", () => {
|
|||
const wrapper = shallow(<Sequences {...p} />);
|
||||
expect(wrapper.text()).not.toContain("Commands");
|
||||
});
|
||||
|
||||
it("makes inserting step mode active", () => {
|
||||
const p = fakeProps();
|
||||
p.stepIndex = 2;
|
||||
const wrapper = shallow(<Sequences {...p} />);
|
||||
expect(wrapper.html()).toContain("inserting-step");
|
||||
});
|
||||
|
||||
it("goes back", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<Sequences {...p} />);
|
||||
wrapper.find("button").first().simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_SEQUENCE, payload: undefined
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith("/app/sequences");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ describe("<StepButtonCluster />", () => {
|
|||
dispatch: jest.fn(),
|
||||
current: undefined,
|
||||
shouldDisplay: () => false,
|
||||
stepIndex: undefined,
|
||||
});
|
||||
|
||||
it("renders sequence commands", () => {
|
||||
|
|
|
@ -5,16 +5,17 @@ import { defensiveClone } from "../util";
|
|||
import { push } from "../history";
|
||||
import { urlFriendly } from "../util";
|
||||
import { Actions } from "../constants";
|
||||
|
||||
import { setActiveSequenceByName } from "./set_active_sequence_by_name";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { isNumber } from "lodash";
|
||||
|
||||
export function pushStep(step: SequenceBodyItem,
|
||||
dispatch: Function,
|
||||
sequence: TaggedSequence) {
|
||||
sequence: TaggedSequence,
|
||||
index?: number | undefined) {
|
||||
const next = defensiveClone(sequence);
|
||||
next.body.body = next.body.body || [];
|
||||
next.body.body.push(defensiveClone(step));
|
||||
next.body.body.splice(isNumber(index) ? index : Infinity, 0, defensiveClone(step));
|
||||
dispatch(overwrite(sequence, next.body));
|
||||
}
|
||||
|
||||
|
@ -41,3 +42,8 @@ export function selectSequence(uuid: string): SelectSequence {
|
|||
payload: uuid
|
||||
};
|
||||
}
|
||||
|
||||
export const unselectSequence = () => {
|
||||
push("/app/sequences");
|
||||
return { type: Actions.SELECT_SEQUENCE, payload: undefined };
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ResourceIndex } from "../resources/interfaces";
|
|||
import { getStepTag } from "../resources/sequence_tagging";
|
||||
import { HardwareFlags, FarmwareInfo } from "./interfaces";
|
||||
import { ShouldDisplay } from "../devices/interfaces";
|
||||
import { AddCommandButton } from "./sequence_editor_middle_active";
|
||||
|
||||
interface AllStepsProps {
|
||||
sequence: TaggedSequence;
|
||||
|
@ -35,6 +36,7 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
|
|||
const readThatCommentAbove = getStepTag(currentStep);
|
||||
return <div className="sequence-steps"
|
||||
key={readThatCommentAbove}>
|
||||
<AddCommandButton dispatch={dispatch} index={index} />
|
||||
<DropArea callback={(key) => onDrop(index, key)} />
|
||||
<StepDragger
|
||||
dispatch={dispatch}
|
||||
|
|
|
@ -46,6 +46,7 @@ export interface Props {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
confirmStepDeletion: boolean;
|
||||
menuOpen: boolean;
|
||||
stepIndex: number | undefined;
|
||||
}
|
||||
|
||||
export interface SequenceEditorMiddleProps {
|
||||
|
@ -109,6 +110,7 @@ export interface Sequence extends CeleryScriptSequence {
|
|||
export interface SequenceReducerState {
|
||||
current: string | undefined;
|
||||
menuOpen: boolean;
|
||||
stepIndex: number | undefined;
|
||||
}
|
||||
|
||||
export interface SequencesListProps {
|
||||
|
@ -141,6 +143,7 @@ export interface StepButtonParams {
|
|||
| "purple"
|
||||
| "pink"
|
||||
| "gray";
|
||||
index?: number | undefined;
|
||||
}
|
||||
|
||||
export interface CopyParams {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Actions } from "../constants";
|
|||
export const initialState: SequenceReducerState = {
|
||||
current: undefined,
|
||||
menuOpen: false,
|
||||
stepIndex: undefined,
|
||||
};
|
||||
|
||||
export let sequenceReducer = generateReducer<SequenceReducerState>(initialState)
|
||||
|
@ -24,4 +25,8 @@ export let sequenceReducer = generateReducer<SequenceReducerState>(initialState)
|
|||
.add<boolean>(Actions.SET_SEQUENCE_POPUP_STATE, function (s, { payload }) {
|
||||
s.menuOpen = payload;
|
||||
return s;
|
||||
})
|
||||
.add<number | undefined>(Actions.SET_SEQUENCE_STEP_POSITION, function (s, { payload }) {
|
||||
s.stepIndex = payload;
|
||||
return s;
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import * as React from "react";
|
|||
import { ActiveMiddleProps, SequenceHeaderProps } from "./interfaces";
|
||||
import { editCurrentSequence } from "./actions";
|
||||
import { splice, move } from "./step_tiles";
|
||||
|
||||
import { push } from "../history";
|
||||
import { BlurableInput, Row, Col, SaveBtn, ColorPicker } from "../ui";
|
||||
import { DropArea } from "../draggable/drop_area";
|
||||
|
@ -19,6 +18,7 @@ import { ResourceIndex } from "../resources/interfaces";
|
|||
import { ShouldDisplay } from "../devices/interfaces";
|
||||
import { isScopeDeclarationBodyItem } from "./locals_list/handle_select";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { Actions } from "../constants";
|
||||
|
||||
export const onDrop =
|
||||
(dispatch1: Function, sequence: TaggedSequence) =>
|
||||
|
@ -168,9 +168,22 @@ export class SequenceEditorMiddleActive extends
|
|||
callback={key => onDrop(dispatch, sequence)(Infinity, key)}>
|
||||
{t("DRAG COMMAND HERE")}
|
||||
</DropArea>
|
||||
<AddCommandButton dispatch={dispatch} index={99999999} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const AddCommandButton = (props: { dispatch: Function, index: number }) =>
|
||||
<div className="add-command-button-container">
|
||||
<button
|
||||
className="add-command fb-button gray"
|
||||
onClick={() => props.dispatch({
|
||||
type: Actions.SET_SEQUENCE_STEP_POSITION,
|
||||
payload: props.index,
|
||||
})}>
|
||||
{t("Add command")}
|
||||
</button>
|
||||
</div>;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { SequencesList } from "./sequences_list";
|
||||
import { StepButtonCluster } from "./step_button_cluster";
|
||||
|
@ -13,6 +12,18 @@ 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 { 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>;
|
||||
|
||||
@connect(mapStateToProps)
|
||||
export class Sequences extends React.Component<Props, {}> {
|
||||
|
@ -23,10 +34,14 @@ export class Sequences extends React.Component<Props, {}> {
|
|||
render() {
|
||||
const { sequence } = this.props;
|
||||
const sequenceSelected = sequence && isTaggedSequence(sequence);
|
||||
const sequenceOpen = sequenceSelected ? "open" : "";
|
||||
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"
|
||||
className={`sequence-list-panel ${activeClasses}`}
|
||||
title={t("Sequences")}
|
||||
helpText={t(ToolTips.SEQUENCE_LIST)}>
|
||||
<SequencesList
|
||||
|
@ -37,7 +52,7 @@ export class Sequences extends React.Component<Props, {}> {
|
|||
sequences={this.props.sequences} />
|
||||
</LeftPanel>
|
||||
<CenterPanel
|
||||
className="sequence-editor-panel"
|
||||
className={`sequence-editor-panel ${activeClasses}`}
|
||||
title={t("Sequence Editor")}
|
||||
helpText={t(ToolTips.SEQUENCE_EDITOR)}>
|
||||
<SequenceEditorMiddle
|
||||
|
@ -52,14 +67,15 @@ export class Sequences extends React.Component<Props, {}> {
|
|||
menuOpen={this.props.menuOpen} />
|
||||
</CenterPanel>
|
||||
<RightPanel
|
||||
className="step-button-cluster-panel"
|
||||
className={`step-button-cluster-panel ${activeClasses}`}
|
||||
title={t("Commands")}
|
||||
helpText={t(ToolTips.SEQUENCE_COMMANDS)}
|
||||
show={sequenceSelected}>
|
||||
<StepButtonCluster
|
||||
current={this.props.sequence}
|
||||
dispatch={this.props.dispatch}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
stepIndex={this.props.stepIndex} />
|
||||
</RightPanel>
|
||||
</Row>
|
||||
</Page>;
|
||||
|
|
|
@ -96,5 +96,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
shouldDisplay,
|
||||
confirmStepDeletion,
|
||||
menuOpen: props.resources.consumers.sequences.menuOpen,
|
||||
stepIndex: props.resources.consumers.sequences.stepIndex,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { StepButton } from "./step_buttons/index";
|
||||
|
||||
import { scrollToBottom } from "../util";
|
||||
import { Row } from "../ui/index";
|
||||
import { TaggedSequence } from "farmbot";
|
||||
|
@ -13,36 +12,26 @@ export interface StepButtonProps {
|
|||
dispatch: Function;
|
||||
current: TaggedSequence | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
stepIndex: number | undefined;
|
||||
}
|
||||
|
||||
export function StepButtonCluster(props: StepButtonProps) {
|
||||
const { dispatch, current, shouldDisplay } = props;
|
||||
const { dispatch, current, shouldDisplay, stepIndex } = props;
|
||||
const commonStepProps = { dispatch, current, index: stepIndex };
|
||||
const ALL_THE_BUTTONS = [
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 }
|
||||
},
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
},
|
||||
location: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
speed: CONFIG_DEFAULTS.speed
|
||||
}
|
||||
}}
|
||||
color="blue">
|
||||
{t("MOVE ABSOLUTE")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "move_relative",
|
||||
args: { x: 0, y: 0, z: 0, speed: CONFIG_DEFAULTS.speed }
|
||||
|
@ -50,8 +39,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="green">
|
||||
{t("MOVE RELATIVE")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "write_pin",
|
||||
args: { pin_number: 0, pin_value: 0, pin_mode: 0 }
|
||||
|
@ -59,8 +47,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="orange">
|
||||
{t("WRITE PIN")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "read_pin",
|
||||
args: {
|
||||
|
@ -72,8 +59,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="yellow">
|
||||
{t("READ PIN")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "wait",
|
||||
args: { milliseconds: 0 }
|
||||
|
@ -81,8 +67,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="brown">
|
||||
{t("WAIT")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "send_message",
|
||||
args: {
|
||||
|
@ -93,8 +78,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="red">
|
||||
{t("SEND MESSAGE")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton{...commonStepProps}
|
||||
step={{
|
||||
kind: "find_home",
|
||||
args: {
|
||||
|
@ -105,8 +89,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="blue">
|
||||
{t("Find Home")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "_if",
|
||||
args: {
|
||||
|
@ -120,8 +103,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="purple">
|
||||
{t("IF STATEMENT")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "execute",
|
||||
args: { sequence_id: 0 }
|
||||
|
@ -129,8 +111,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
color="gray">
|
||||
{t("EXECUTE SEQUENCE")}
|
||||
</StepButton>,
|
||||
<StepButton dispatch={dispatch}
|
||||
current={current}
|
||||
<StepButton {...commonStepProps}
|
||||
step={{
|
||||
kind: "execute_script",
|
||||
args: { label: "plant-detection" }
|
||||
|
@ -139,8 +120,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
{t("Run Farmware")}
|
||||
</StepButton>,
|
||||
<StepButton
|
||||
dispatch={dispatch}
|
||||
current={current}
|
||||
{...commonStepProps}
|
||||
color="brown"
|
||||
step={{ kind: "take_photo", args: {} }} >
|
||||
{t("TAKE PHOTO")}
|
||||
|
@ -148,8 +128,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
];
|
||||
|
||||
shouldDisplay(Feature.mark_as_step) && ALL_THE_BUTTONS.push(<StepButton
|
||||
dispatch={dispatch}
|
||||
current={current}
|
||||
{...commonStepProps}
|
||||
step={{
|
||||
kind: "resource_update",
|
||||
args: {
|
||||
|
|
|
@ -16,7 +16,8 @@ function props(): StepButtonParams {
|
|||
},
|
||||
},
|
||||
dispatch: jest.fn(),
|
||||
color: "blue"
|
||||
color: "blue",
|
||||
index: 1,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -24,7 +25,7 @@ describe("<StepButton/>", () => {
|
|||
|
||||
it("clicks it", () => {
|
||||
const p = props();
|
||||
const el = shallow(<StepButton {...p } />);
|
||||
const el = shallow(<StepButton {...p} />);
|
||||
el.find("button").simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
payload: expect.objectContaining({
|
||||
|
@ -34,6 +35,10 @@ describe("<StepButton/>", () => {
|
|||
}),
|
||||
type: Actions.OVERWRITE_RESOURCE
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SET_SEQUENCE_STEP_POSITION,
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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";
|
||||
|
@ -7,15 +6,24 @@ import { pushStep } 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, step: Step, seq: TaggedSequence | undefined) =>
|
||||
(dispatch: Function,
|
||||
step: Step,
|
||||
seq: TaggedSequence | undefined,
|
||||
index?: number | undefined) =>
|
||||
() => {
|
||||
(seq) ?
|
||||
pushStep(step, dispatch, seq) : error(t("Select a sequence first"));
|
||||
seq
|
||||
? pushStep(step, dispatch, seq, index)
|
||||
: error(t("Select a sequence first"));
|
||||
dispatch({
|
||||
type: Actions.SET_SEQUENCE_STEP_POSITION,
|
||||
payload: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export function StepButton({ children, step, color, dispatch, current }:
|
||||
export function StepButton({ children, step, color, dispatch, current, index }:
|
||||
StepButtonParams) {
|
||||
return <Col xs={6} sm={12}>
|
||||
<div className="block">
|
||||
|
@ -26,7 +34,7 @@ export function StepButton({ children, step, color, dispatch, current }:
|
|||
draggerId={NULL_DRAGGER_ID} >
|
||||
<button draggable={true}
|
||||
className={`fb-button full-width block ${color}`}
|
||||
onClick={stepClick(dispatch, step, current)} >
|
||||
onClick={stepClick(dispatch, step, current, index)} >
|
||||
{children}
|
||||
<i className="fa fa-arrows block-control" />
|
||||
</button>
|
||||
|
|
Loading…
Reference in New Issue