Merge pull request #1652 from gabrielburnworth/staging
Form updates and refactoringlog_problems_iii
commit
37d4d27dce
|
@ -26,6 +26,7 @@ module CeleryScript
|
|||
FRIENDLY_ERRORS = {
|
||||
nothing: {
|
||||
write_pin: "You must select a Peripheral in the Control Peripheral step.",
|
||||
toggle_pin: "You must select a Peripheral in the Toggle Peripheral step.",
|
||||
variable_declaration: MISSING_VAR,
|
||||
parameter_declaration: MISSING_PARAM,
|
||||
read_pin: "You must select a Sensor in the Read Sensor step.",
|
||||
|
|
|
@ -7,11 +7,11 @@ export function clickButton(
|
|||
position: number,
|
||||
text: string,
|
||||
options?: { partial_match?: boolean, button_tag?: string }) {
|
||||
const btnTag = options && options.button_tag ? options.button_tag : "button";
|
||||
const btnTag = options?.button_tag ? options.button_tag : "button";
|
||||
const button = wrapper.find(btnTag).at(position);
|
||||
const expectedText = text.toLowerCase();
|
||||
const actualText = button.text().toLowerCase();
|
||||
options && options.partial_match
|
||||
options?.partial_match
|
||||
? expect(actualText).toContain(expectedText)
|
||||
: expect(actualText).toEqual(expectedText);
|
||||
button.simulate("click");
|
||||
|
|
|
@ -45,29 +45,29 @@ const fakeProps = (): AppProps => ({
|
|||
});
|
||||
|
||||
describe("<App />: Controls Pop-Up", () => {
|
||||
function controlsPopUp(page: string, exists: boolean) {
|
||||
it(`doesn't render controls pop-up on ${page} page`, () => {
|
||||
mockPath = "/app/" + page;
|
||||
const wrapper = mount(<App {...fakeProps()} />);
|
||||
if (exists) {
|
||||
expect(wrapper.html()).toContain("controls-popup");
|
||||
} else {
|
||||
expect(wrapper.html()).not.toContain("controls-popup");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
controlsPopUp("designer", true);
|
||||
controlsPopUp("designer/plants", true);
|
||||
controlsPopUp("controls", false);
|
||||
controlsPopUp("device", true);
|
||||
controlsPopUp("sequences", true);
|
||||
controlsPopUp("sequences/for_regimens", true);
|
||||
controlsPopUp("regimens", false);
|
||||
controlsPopUp("tools", true);
|
||||
controlsPopUp("farmware", true);
|
||||
controlsPopUp("account", false);
|
||||
|
||||
it.each<["renders" | "doesn't render", string]>([
|
||||
["renders", "designer"],
|
||||
["renders", "designer/plants"],
|
||||
["doesn't render", "controls"],
|
||||
["renders", "device"],
|
||||
["renders", "sequences"],
|
||||
["renders", "sequences/for_regimens"],
|
||||
["doesn't render", "regimens"],
|
||||
["renders", "tools"],
|
||||
["renders", "farmware"],
|
||||
["renders", "messages"],
|
||||
["renders", "logs"],
|
||||
["renders", "help"],
|
||||
["doesn't render", "account"],
|
||||
])("%s controls pop-up on %s page", (expected, page) => {
|
||||
mockPath = "/app/" + page;
|
||||
const wrapper = mount(<App {...fakeProps()} />);
|
||||
if (expected == "renders") {
|
||||
expect(wrapper.html()).toContain("controls-popup");
|
||||
} else {
|
||||
expect(wrapper.html()).not.toContain("controls-popup");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("<App />: Loading", () => {
|
||||
|
|
|
@ -32,7 +32,7 @@ test("buildResourceIndex - add a FarmEvent", () => {
|
|||
const key = Object.keys(db.index.byKind.FarmEvent)[0];
|
||||
const fe = db.index.references[key];
|
||||
expect(fe).toBeTruthy();
|
||||
if (fe && fe.kind === "FarmEvent") {
|
||||
if (fe?.kind === "FarmEvent") {
|
||||
const { body } = fe;
|
||||
expect(body).toEqual(STUB_RESOURCE.body);
|
||||
} else {
|
||||
|
|
|
@ -300,7 +300,9 @@ export namespace ToolTips {
|
|||
trim(`Power cycle FarmBot's onboard computer or microcontroller.`);
|
||||
|
||||
export const SET_SERVO_ANGLE =
|
||||
trim(`Move a servo to the provided angle.`);
|
||||
trim(`Move a servo to the provided angle. An angle of 90 degrees
|
||||
corresponds to the servo midpoint (or, for a continuous rotation
|
||||
servo, no movement).`);
|
||||
|
||||
export const TOGGLE_PIN =
|
||||
trim(`Toggle a digital pin on or off.`);
|
||||
|
@ -818,11 +820,9 @@ export namespace Content {
|
|||
Doing so will limit the functionality of your FarmBot and
|
||||
may cause unexpected behavior.`);
|
||||
|
||||
export const SET_TIMEZONE_HEADER =
|
||||
trim(`You must set a timezone before using the event feature.`);
|
||||
|
||||
export const SET_TIMEZONE_BODY =
|
||||
trim(`Set device timezone here.`);
|
||||
export const MISSING_EXECUTABLE =
|
||||
trim(`You haven't made any sequences or regimens yet. To add an event,
|
||||
first create a sequence or regimen.`);
|
||||
|
||||
// Farmware
|
||||
export const NO_IMAGES_YET =
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import * as React from "react";
|
||||
import { AxisInputBoxProps } from "./interfaces";
|
||||
import { Col, BlurableInput } from "../ui/index";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export const AxisInputBox = ({ onChange, value, axis }: AxisInputBoxProps) => {
|
||||
return <Col xs={3}>
|
||||
<BlurableInput
|
||||
value={(isUndefined(value) ? "" : value)}
|
||||
value={value ?? ""}
|
||||
type="number"
|
||||
allowEmpty={true}
|
||||
onCommit={e => {
|
||||
|
|
|
@ -431,12 +431,32 @@
|
|||
.location-form {
|
||||
width: 100% !important;
|
||||
}
|
||||
.note {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
.bp3-popover-wrapper {
|
||||
display: inline;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.farm-event-form {
|
||||
.farm-event-repeat-options {
|
||||
input[type=checkbox] {
|
||||
margin-right: 0.5rem;
|
||||
margin-top: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.farm-event-repeat-form {
|
||||
.add-event-repeat-frequency {
|
||||
min-height: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-farm-event-panel button.red,
|
||||
.edit-farm-event-panel button.red,
|
||||
.add-farm-event-panel button.magenta,
|
||||
.edit-farm-event-panel button.magenta {
|
||||
.edit-farm-event-panel button.red {
|
||||
margin-top: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
|
|
@ -78,13 +78,3 @@
|
|||
background: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
.add-farm-event-panel {
|
||||
.note {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.add-event-repeat-frequency {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,12 @@
|
|||
margin-left: 1rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
input[type="radio"] {
|
||||
margin-right: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +113,7 @@
|
|||
background: $blue;
|
||||
}
|
||||
&.toggle-pin-step {
|
||||
background: $yellow;
|
||||
background: $orange;
|
||||
}
|
||||
&.set-zero-step {
|
||||
background: $blue;
|
||||
|
@ -124,6 +130,12 @@
|
|||
&.system-action-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.emergency-stop-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.reboot-step {
|
||||
background: $brown;
|
||||
}
|
||||
&.unknown-step {
|
||||
background: $gray;
|
||||
}
|
||||
|
@ -221,7 +233,7 @@
|
|||
background: $light_blue;
|
||||
}
|
||||
&.toggle-pin-step {
|
||||
background: $light_yellow;
|
||||
background: $light_orange;
|
||||
}
|
||||
&.set-zero-step {
|
||||
background: $light_blue;
|
||||
|
@ -238,6 +250,12 @@
|
|||
&.system-action-step {
|
||||
background: $light_brown;
|
||||
}
|
||||
&.emergency-stop-step {
|
||||
background: $light_brown;
|
||||
}
|
||||
&.reboot-step {
|
||||
background: $light_brown;
|
||||
}
|
||||
&.unknown-step {
|
||||
background: $light_gray;
|
||||
}
|
||||
|
|
|
@ -383,21 +383,20 @@ describe("fetchReleases()", () => {
|
|||
});
|
||||
|
||||
describe("fetchLatestGHBetaRelease()", () => {
|
||||
const testFetchBeta = (tag_name: string, version: string) =>
|
||||
it(`fetches latest beta OS release version: ${tag_name}`, async () => {
|
||||
mockGetRelease = Promise.resolve({ data: [{ tag_name }] });
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchLatestGHBetaRelease("url/001")(dispatch);
|
||||
expect(axios.get).toHaveBeenCalledWith("url");
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: { version, commit: undefined },
|
||||
type: Actions.FETCH_BETA_OS_UPDATE_INFO_OK
|
||||
});
|
||||
it.each<[string, string]>([
|
||||
["v1.0.0-beta", "1.0.0-beta"],
|
||||
["v1.0.0-rc1", "1.0.0-rc1"],
|
||||
])("fetches latest beta OS release version: %s", async (tag_name, version) => {
|
||||
mockGetRelease = Promise.resolve({ data: [{ tag_name }] });
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchLatestGHBetaRelease("url/001")(dispatch);
|
||||
expect(axios.get).toHaveBeenCalledWith("url");
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: { version, commit: undefined },
|
||||
type: Actions.FETCH_BETA_OS_UPDATE_INFO_OK
|
||||
});
|
||||
|
||||
testFetchBeta("v1.0.0-beta", "1.0.0-beta");
|
||||
testFetchBeta("v1.0.0-rc1", "1.0.0-rc1");
|
||||
});
|
||||
|
||||
it("fails to fetches latest beta OS release version", async () => {
|
||||
mockGetRelease = Promise.reject("error");
|
||||
|
|
|
@ -36,8 +36,8 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
|
|||
</Col>
|
||||
<Col xs={2} className={"centered-button-div"}>
|
||||
<ToggleButton
|
||||
grayscale={grayscale && grayscale.x}
|
||||
disabled={disable && disable.x}
|
||||
grayscale={grayscale?.x}
|
||||
disabled={disable?.x}
|
||||
dim={!xParam.consistent}
|
||||
toggleValue={xParam.value}
|
||||
toggleAction={() =>
|
||||
|
@ -45,8 +45,8 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
|
|||
</Col>
|
||||
<Col xs={2} className={"centered-button-div"}>
|
||||
<ToggleButton
|
||||
grayscale={grayscale && grayscale.y}
|
||||
disabled={disable && disable.y}
|
||||
grayscale={grayscale?.y}
|
||||
disabled={disable?.y}
|
||||
dim={!yParam.consistent}
|
||||
toggleValue={yParam.value}
|
||||
toggleAction={() =>
|
||||
|
@ -54,8 +54,8 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
|
|||
</Col>
|
||||
<Col xs={2} className={"centered-button-div"}>
|
||||
<ToggleButton
|
||||
grayscale={grayscale && grayscale.z}
|
||||
disabled={disable && disable.z}
|
||||
grayscale={grayscale?.z}
|
||||
disabled={disable?.z}
|
||||
dim={!zParam.consistent}
|
||||
toggleValue={zParam.value}
|
||||
toggleAction={() =>
|
||||
|
|
|
@ -355,21 +355,20 @@ describe("<OsUpdateButton/>", () => {
|
|||
expect(osUpdateButton.text()).toEqual("UP TO DATE");
|
||||
});
|
||||
|
||||
function bytesProgressTest(unit: string, progress: number, text: string) {
|
||||
it(`shows update progress: ${unit}`, () => {
|
||||
bot.hardware.jobs = {
|
||||
"FBOS_OTA": { status: "working", bytes: progress, unit: "bytes" }
|
||||
};
|
||||
const buttons = mount(<OsUpdateButton {...fakeProps()} />);
|
||||
const osUpdateButton = buttons.find("button").first();
|
||||
expect(osUpdateButton.text()).toBe(text);
|
||||
});
|
||||
}
|
||||
bytesProgressTest("bytes", 300, "300B");
|
||||
bytesProgressTest("kilobytes", 30000, "29kB");
|
||||
bytesProgressTest("megabytes", 3e6, "3MB");
|
||||
it.each<[string, number]>([
|
||||
["300B", 300],
|
||||
["29kB", 30000],
|
||||
["3MB", 3e6],
|
||||
])("shows bytes update progress: %s", (expected, progress) => {
|
||||
bot.hardware.jobs = {
|
||||
"FBOS_OTA": { status: "working", bytes: progress, unit: "bytes" }
|
||||
};
|
||||
const buttons = mount(<OsUpdateButton {...fakeProps()} />);
|
||||
const osUpdateButton = buttons.find("button").first();
|
||||
expect(osUpdateButton.text()).toBe(expected);
|
||||
});
|
||||
|
||||
it("shows update progress: percent", () => {
|
||||
it("shows percent update progress: 10%", () => {
|
||||
bot.hardware.jobs = {
|
||||
"FBOS_OTA": { status: "working", percent: 10, unit: "percent" }
|
||||
};
|
||||
|
|
|
@ -8,15 +8,13 @@ import { Dictionary } from "farmbot";
|
|||
|
||||
describe("<EncodersAndEndStops />", () => {
|
||||
const mockFeatures: Dictionary<boolean> = {};
|
||||
const fakeProps = (): EncodersProps => {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
controlPanelState: panelState(),
|
||||
sourceFwConfig: x =>
|
||||
({ value: bot.hardware.mcu_params[x], consistent: true }),
|
||||
shouldDisplay: jest.fn(key => mockFeatures[key]),
|
||||
};
|
||||
};
|
||||
const fakeProps = (): EncodersProps => ({
|
||||
dispatch: jest.fn(),
|
||||
controlPanelState: panelState(),
|
||||
sourceFwConfig: x =>
|
||||
({ value: bot.hardware.mcu_params[x], consistent: true }),
|
||||
shouldDisplay: jest.fn(key => mockFeatures[key]),
|
||||
});
|
||||
|
||||
it("shows new inversion param", () => {
|
||||
mockFeatures.endstop_invert = true;
|
||||
|
@ -24,16 +22,15 @@ describe("<EncodersAndEndStops />", () => {
|
|||
expect(wrapper.text().toLowerCase()).not.toContain("invert endstops");
|
||||
});
|
||||
|
||||
const intSizeTest = (size: "short" | "long") =>
|
||||
it(`uses ${size} int scaling factor`, () => {
|
||||
mockFeatures.long_scaling_factor = size === "short" ? false : true;
|
||||
const wrapper = shallow(<EncodersAndEndStops {...fakeProps()} />);
|
||||
const sfProps = wrapper.find("NumericMCUInputGroup").at(2)
|
||||
.props() as NumericMCUInputGroupProps;
|
||||
expect(sfProps.name).toEqual("Encoder Scaling");
|
||||
expect(sfProps.intSize).toEqual(size);
|
||||
});
|
||||
|
||||
intSizeTest("short");
|
||||
intSizeTest("long");
|
||||
it.each<["short" | "long"]>([
|
||||
["short"],
|
||||
["long"],
|
||||
])("uses %s int scaling factor", (size) => {
|
||||
mockFeatures.long_scaling_factor = size === "short" ? false : true;
|
||||
const wrapper = shallow(<EncodersAndEndStops {...fakeProps()} />);
|
||||
const sfProps = wrapper.find("NumericMCUInputGroup").at(2)
|
||||
.props() as NumericMCUInputGroupProps;
|
||||
expect(sfProps.name).toEqual("Encoder Scaling");
|
||||
expect(sfProps.intSize).toEqual(size);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,7 +115,7 @@ describe("<PinBindingInputGroup/>", () => {
|
|||
const p = fakeProps();
|
||||
const key = Object.keys(p.resources.byKind.Sequence)[0];
|
||||
const s = p.resources.references[key];
|
||||
const id = s && s.body.id;
|
||||
const id = s?.body.id;
|
||||
const wrapper = mount<PinBindingInputGroup>(<PinBindingInputGroup {...p} />);
|
||||
expect(wrapper.instance().state.sequenceIdInput).toEqual(undefined);
|
||||
wrapper.instance().setSequenceIdInput({ label: "label", value: "" + id });
|
||||
|
|
|
@ -79,8 +79,8 @@ export const mcuParamValidator =
|
|||
(ok: () => void, no?: (message: string) => void): void => {
|
||||
const validator = edgeCases[key];
|
||||
const result = validator && validator(key, val, state);
|
||||
if (result && result.outcome === "NO") {
|
||||
return (no && no(result.errorMessage));
|
||||
if (result?.outcome === "NO") {
|
||||
return (no?.(result.errorMessage));
|
||||
} else {
|
||||
return ok();
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ describe("draggableReducer", () => {
|
|||
const dt = nextState.dataTransfer;
|
||||
expect(Object.keys(dt)).toContain(payload.uuid);
|
||||
const entry = dt[payload.uuid];
|
||||
expect(entry && entry.uuid).toEqual(payload.uuid);
|
||||
expect(entry?.uuid).toEqual(payload.uuid);
|
||||
});
|
||||
|
||||
it("drops a step", () => {
|
||||
|
|
|
@ -56,7 +56,7 @@ export const DesignerPanelHeader = (props: DesignerPanelHeaderProps) => {
|
|||
title={t("go back") + backToText(props.backTo)}
|
||||
onClick={() => {
|
||||
props.backTo ? routeHistory.push(props.backTo) : history.back();
|
||||
props.onBack && props.onBack();
|
||||
props.onBack?.();
|
||||
}} />
|
||||
{props.title &&
|
||||
<span className={`title ${textColor}-text`}>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
jest.mock("../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
destroy: jest.fn(),
|
||||
init: jest.fn(() => ({ payload: { uuid: "fakeUuid" } })),
|
||||
}));
|
||||
|
||||
jest.mock("../../../resources/actions", () => ({ destroyOK: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { RawAddFarmEvent as AddFarmEvent } from "../add_farm_event";
|
||||
import { AddEditFarmEventProps } from "../../interfaces";
|
||||
import {
|
||||
|
@ -10,8 +17,13 @@ import {
|
|||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { Actions } from "../../../constants";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { destroyOK } from "../../../resources/actions";
|
||||
import { init, destroy } from "../../../api/crud";
|
||||
import { DesignerPanelHeader } from "../../designer_panel";
|
||||
import { Content } from "../../../constants";
|
||||
import { error } from "../../../toast/toast";
|
||||
import { FarmEventForm } from "../edit_fe_form";
|
||||
|
||||
describe("<AddFarmEvent />", () => {
|
||||
function fakeProps(): AddEditFarmEventProps {
|
||||
|
@ -49,21 +61,24 @@ describe("<AddFarmEvent />", () => {
|
|||
expect(deleteBtn.props().hidden).toBeTruthy();
|
||||
});
|
||||
|
||||
it("redirects", () => {
|
||||
const p = fakeProps();
|
||||
p.findFarmEventByUuid = jest.fn();
|
||||
const wrapper = mount(<AddFarmEvent {...p} />);
|
||||
expect(wrapper.text()).toContain("Loading");
|
||||
});
|
||||
|
||||
it("renders with no executables", () => {
|
||||
const p = fakeProps();
|
||||
p.findFarmEventByUuid = jest.fn();
|
||||
p.sequencesById = {};
|
||||
p.regimensById = {};
|
||||
const wrapper = mount(<AddFarmEvent {...p} />);
|
||||
expect(wrapper.text())
|
||||
.toContain("You haven't made any regimens or sequences yet.");
|
||||
expect(wrapper.html()).toContain("fa-exclamation-triangle");
|
||||
});
|
||||
|
||||
it("changes temporary values", () => {
|
||||
const p = fakeProps();
|
||||
p.findFarmEventByUuid = jest.fn();
|
||||
p.sequencesById = {};
|
||||
p.regimensById = {};
|
||||
const wrapper = mount<AddFarmEvent>(<AddFarmEvent {...p} />);
|
||||
expect(wrapper.instance().getField("repeat")).toEqual("1");
|
||||
wrapper.instance().setField("repeat", "2");
|
||||
expect(wrapper.state().temporaryValues.repeat).toEqual("2");
|
||||
});
|
||||
|
||||
it("renders with no sequences", () => {
|
||||
|
@ -74,27 +89,57 @@ describe("<AddFarmEvent />", () => {
|
|||
p.regimensById = { "1": regimen };
|
||||
const wrapper = mount(<AddFarmEvent {...p} />);
|
||||
wrapper.mount();
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.INIT_RESOURCE,
|
||||
payload: expect.objectContaining({
|
||||
kind: "FarmEvent",
|
||||
body: expect.objectContaining({ executable_type: "Regimen" })
|
||||
})
|
||||
});
|
||||
expect(init).toHaveBeenCalledWith("FarmEvent",
|
||||
expect.objectContaining({ executable_type: "Regimen" }));
|
||||
});
|
||||
|
||||
it("cleans up when unmounting", () => {
|
||||
const props = fakeProps();
|
||||
const wrapper = mount(<AddFarmEvent {...props} />);
|
||||
wrapper.update();
|
||||
const uuid: string = wrapper.state("uuid");
|
||||
props.farmEvents[0].uuid = uuid;
|
||||
props.farmEvents[0].body.id = undefined;
|
||||
wrapper.setProps(props);
|
||||
wrapper.update();
|
||||
jest.resetAllMocks();
|
||||
const p = fakeProps();
|
||||
const farmEvent = fakeFarmEvent("Sequence", 1);
|
||||
farmEvent.body.id = 0;
|
||||
p.findFarmEventByUuid = () => farmEvent;
|
||||
const wrapper = mount(<AddFarmEvent {...p} />);
|
||||
wrapper.unmount();
|
||||
expect(props.dispatch).toHaveBeenCalled();
|
||||
expect(props.dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(destroy).toHaveBeenCalledWith(farmEvent.uuid, true);
|
||||
});
|
||||
|
||||
it("doesn't delete saved farm events when unmounting", () => {
|
||||
const p = fakeProps();
|
||||
const farmEvent = fakeFarmEvent("Sequence", 1);
|
||||
farmEvent.body.id = 1;
|
||||
p.findFarmEventByUuid = () => farmEvent;
|
||||
const wrapper = mount(<AddFarmEvent {...p} />);
|
||||
wrapper.unmount();
|
||||
expect(destroy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cleans up on back", () => {
|
||||
const p = fakeProps();
|
||||
const farmEvent = fakeFarmEvent("Sequence", 1);
|
||||
farmEvent.body.id = 0;
|
||||
p.findFarmEventByUuid = () => farmEvent;
|
||||
const wrapper = shallow(<AddFarmEvent {...p} />);
|
||||
wrapper.find(DesignerPanelHeader).simulate("back");
|
||||
expect(destroyOK).toHaveBeenCalledWith(farmEvent);
|
||||
});
|
||||
|
||||
it("doesn't delete saved farm events on back", () => {
|
||||
const p = fakeProps();
|
||||
const farmEvent = fakeFarmEvent("Sequence", 1);
|
||||
farmEvent.body.id = 1;
|
||||
p.findFarmEventByUuid = () => farmEvent;
|
||||
const wrapper = shallow(<AddFarmEvent {...p} />);
|
||||
wrapper.find(DesignerPanelHeader).simulate("back");
|
||||
expect(destroyOK).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows error on save", () => {
|
||||
const p = fakeProps();
|
||||
p.findFarmEventByUuid = jest.fn();
|
||||
p.sequencesById = {};
|
||||
p.regimensById = {};
|
||||
const wrapper = shallow(<AddFarmEvent {...p} />);
|
||||
wrapper.find(FarmEventForm).simulate("save");
|
||||
expect(error).toHaveBeenCalledWith(Content.MISSING_EXECUTABLE);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ describe("<EditFarmEvent />", () => {
|
|||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<EditFarmEvent {...fakeProps()} />);
|
||||
["Edit Event", "Sequence or Regimen", "fake", "Save"]
|
||||
["Sequence or Regimen", "fake", "Save"]
|
||||
.map(string => expect(wrapper.text()).toContain(string));
|
||||
const deleteBtn = wrapper.find("button").last();
|
||||
expect(deleteBtn.text()).toEqual("Delete");
|
||||
|
|
|
@ -17,7 +17,14 @@ import {
|
|||
FarmEventViewModel,
|
||||
recombine,
|
||||
destructureFarmEvent,
|
||||
offsetTime
|
||||
offsetTime,
|
||||
FarmEventDeleteButtonProps,
|
||||
FarmEventDeleteButton,
|
||||
RepeatForm,
|
||||
RepeatFormProps,
|
||||
StartTimeForm,
|
||||
StartTimeFormProps,
|
||||
FarmEventForm
|
||||
} from "../edit_fe_form";
|
||||
import { isString, isFunction } from "lodash";
|
||||
import { repeatOptions } from "../map_state_to_props_add_edit";
|
||||
|
@ -28,15 +35,14 @@ import {
|
|||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { fakeVariableNameSet } from "../../../__test_support__/fake_variables";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { destroy, save } from "../../../api/crud";
|
||||
import { save, destroy } from "../../../api/crud";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
import { error, success } from "../../../toast/toast";
|
||||
|
||||
const mockSequence = fakeSequence();
|
||||
|
||||
describe("<FarmEventForm/>", () => {
|
||||
const props = (): EditFEProps => ({
|
||||
describe("<EditFEForm />", () => {
|
||||
const fakeProps = (): EditFEProps => ({
|
||||
deviceTimezone: undefined,
|
||||
executableOptions: [],
|
||||
repeatOptions: [],
|
||||
|
@ -53,34 +59,18 @@ describe("<FarmEventForm/>", () => {
|
|||
function instance(p: EditFEProps) {
|
||||
return mount(<EditFEForm {...p} />).instance() as EditFEForm;
|
||||
}
|
||||
const context = { form: new EditFEForm(props()) };
|
||||
const context = { form: new EditFEForm(fakeProps()) };
|
||||
|
||||
beforeEach(() => {
|
||||
context.form = new EditFEForm(props());
|
||||
context.form = new EditFEForm(fakeProps());
|
||||
});
|
||||
|
||||
it("sets defaults", () => {
|
||||
expect(context.form.state.fe).toMatchObject({});
|
||||
});
|
||||
|
||||
it("determines if it is a one time event", () => {
|
||||
const i = instance(props());
|
||||
expect(i.repeats).toBe(false);
|
||||
i.mergeState("timeUnit", "daily");
|
||||
i.forceUpdate();
|
||||
expect(i.repeats).toBe(true);
|
||||
});
|
||||
|
||||
it("has a dispatch", () => {
|
||||
const p = props();
|
||||
const i = instance(p);
|
||||
expect(i.dispatch).toBe(p.dispatch);
|
||||
i.dispatch();
|
||||
expect(p.dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("has a view model", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const i = instance(p);
|
||||
i.forceUpdate();
|
||||
const vm = i.viewModel;
|
||||
|
@ -100,7 +90,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("has an executable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const i = instance(p);
|
||||
i.forceUpdate();
|
||||
expect(i.executableGet().value).toEqual(mockSequence.body.id);
|
||||
|
@ -108,7 +98,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("sets the executable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const i = instance(p);
|
||||
i.forceUpdate();
|
||||
i.executableSet({ value: "wow", label: "hey", headingId: "Sequence" });
|
||||
|
@ -118,7 +108,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("allows proper changes to the executable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.farmEvent.body.id = 0;
|
||||
p.farmEvent.body.executable_type = "Sequence";
|
||||
const i = instance(p);
|
||||
|
@ -128,7 +118,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("doesn't allow improper changes to the executable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.farmEvent.body.id = 1;
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
const i = instance(p);
|
||||
|
@ -139,7 +129,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("gets executable info", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const i = instance(p);
|
||||
i.forceUpdate();
|
||||
const exe = i.executableGet();
|
||||
|
@ -149,13 +139,12 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("sets a subfield of state.fe", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const i = instance(p);
|
||||
i.forceUpdate();
|
||||
// tslint:disable-next-line:no-any
|
||||
i.fieldSet("repeat")(({ currentTarget: { value: "4" } } as any));
|
||||
i.fieldSet("executable_id", "1");
|
||||
i.forceUpdate();
|
||||
expect(i.state.fe.repeat).toEqual("4");
|
||||
expect(i.state.fe.executable_id).toEqual("1");
|
||||
});
|
||||
|
||||
it("sets regimen repeat to `never` as needed", () => {
|
||||
|
@ -245,7 +234,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("displays success message on save: manual sync", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.autoSyncEnabled = false;
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
|
@ -256,7 +245,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("displays success message on save: auto sync", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.autoSyncEnabled = true;
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
const regimen = fakeRegimen();
|
||||
|
@ -273,7 +262,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("warns about missed regimen items", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
const regimen = fakeRegimen();
|
||||
regimen.body.regimen_items = [
|
||||
|
@ -293,7 +282,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("sends toast with regimen start time", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
const regimen = fakeRegimen();
|
||||
regimen.body.regimen_items = [{ sequence_id: -1, time_offset: 1000000000 }];
|
||||
|
@ -308,7 +297,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("sends toast with next sequence run time", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.farmEvent.body.executable_type = "Sequence";
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-06-22T06:00:00.000Z";
|
||||
|
@ -328,7 +317,7 @@ describe("<FarmEventForm/>", () => {
|
|||
};
|
||||
|
||||
it("displays error message on save (add): start time has passed", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.title = "add";
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
|
@ -338,7 +327,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("displays error message on edit: start time has passed", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.title = "edit";
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
|
@ -349,7 +338,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("displays error message on save: no items", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
|
@ -360,7 +349,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("displays error message on save: save error", async () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn()
|
||||
.mockResolvedValueOnce("")
|
||||
.mockRejectedValueOnce("error");
|
||||
|
@ -374,7 +363,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("allows start time: edit with unsupported OS", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => false;
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
p.farmEvent.body.start_time = "2017-06-01T01:00:00.000Z";
|
||||
|
@ -385,7 +374,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("allows start time: add with supported OS", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.title = "add";
|
||||
p.shouldDisplay = () => true;
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
|
@ -396,7 +385,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("rejects start time: add sequence event", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.title = "add";
|
||||
p.farmEvent.body.executable_type = "Sequence";
|
||||
p.farmEvent.body.start_time = "2017-06-01T01:00:00.000Z";
|
||||
|
@ -406,7 +395,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("allows start time: edit sequence event", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.farmEvent.body.executable_type = "Sequence";
|
||||
p.farmEvent.body.start_time = "2017-06-01T01:00:00.000Z";
|
||||
const fakeNow = moment("2017-06-01T02:00:00.000Z");
|
||||
|
@ -416,7 +405,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("allows start time in the future", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.title = "add";
|
||||
p.farmEvent.body.executable_type = "Sequence";
|
||||
p.farmEvent.body.start_time = "2017-06-01T01:00:00.000Z";
|
||||
|
@ -426,7 +415,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("edits a variable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const oldVariable: ParameterApplication = {
|
||||
kind: "parameter_application",
|
||||
args: {
|
||||
|
@ -455,7 +444,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("saves an updated variable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const oldVariable: ParameterApplication = {
|
||||
kind: "parameter_application",
|
||||
args: {
|
||||
|
@ -481,7 +470,7 @@ describe("<FarmEventForm/>", () => {
|
|||
});
|
||||
|
||||
it("saves the current variable", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
const sequence = fakeSequence();
|
||||
p.findExecutable = () => sequence;
|
||||
const plant = fakePlant();
|
||||
|
@ -504,44 +493,11 @@ describe("<FarmEventForm/>", () => {
|
|||
expect(inst.updatedFarmEvent.body).toEqual([oldVariable]);
|
||||
});
|
||||
|
||||
it("deletes a farmEvent", async () => {
|
||||
const p = props();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const inst = instance(p);
|
||||
const wrapper = shallow(<inst.FarmEventDeleteButton />);
|
||||
clickButton(wrapper, 0, "delete");
|
||||
await expect(destroy).toHaveBeenCalledWith(p.farmEvent.uuid);
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/events");
|
||||
expect(success).toHaveBeenCalledWith("Deleted event.", "Deleted");
|
||||
});
|
||||
|
||||
it("sets repeat", () => {
|
||||
const p = props();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const e = {
|
||||
currentTarget: { checked: true }
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
const inst = instance(p);
|
||||
inst.toggleRepeat(e);
|
||||
expect(inst.state).toEqual({
|
||||
fe: { timeUnit: "daily" },
|
||||
specialStatusLocal: SpecialStatus.DIRTY
|
||||
});
|
||||
});
|
||||
|
||||
it("sets repeat: regimen", () => {
|
||||
const p = props();
|
||||
p.farmEvent.body.executable_type = "Regimen";
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const e = {
|
||||
currentTarget: { checked: true }
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
const inst = instance(p);
|
||||
inst.toggleRepeat(e);
|
||||
expect(inst.state).toEqual({
|
||||
fe: { timeUnit: "never" },
|
||||
specialStatusLocal: SpecialStatus.DIRTY
|
||||
});
|
||||
it("shows error message upon farm event save", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<EditFEForm {...p} />);
|
||||
wrapper.find(FarmEventForm).simulate("save");
|
||||
expect(error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -557,3 +513,75 @@ describe("destructureFarmEvent", () => {
|
|||
expect(endTime).toBe("23:32");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<StartTimeForm />", () => {
|
||||
const fakeProps = (): StartTimeFormProps => ({
|
||||
isRegimen: false,
|
||||
fieldGet: jest.fn(),
|
||||
fieldSet: jest.fn(),
|
||||
timeSettings: fakeTimeSettings(),
|
||||
});
|
||||
|
||||
it("changes start date", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<StartTimeForm {...p} />);
|
||||
wrapper.find("BlurableInput").first().simulate("commit", {
|
||||
currentTarget: { value: "2017-07-26" }
|
||||
});
|
||||
expect(p.fieldSet).toHaveBeenCalledWith("startDate", "2017-07-26");
|
||||
});
|
||||
|
||||
it("changes start time", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<StartTimeForm {...p} />);
|
||||
wrapper.find("EventTimePicker").simulate("commit", {
|
||||
currentTarget: { value: "08:57" }
|
||||
});
|
||||
expect(p.fieldSet).toHaveBeenCalledWith("startTime", "08:57");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<RepeatForm />", () => {
|
||||
const fakeProps = (): RepeatFormProps => ({
|
||||
isRegimen: false,
|
||||
fieldGet: jest.fn(key =>
|
||||
"" + ({ endDate: "2017-07-26" } as FarmEventViewModel)[key]),
|
||||
fieldSet: jest.fn(),
|
||||
timeSettings: fakeTimeSettings(),
|
||||
});
|
||||
|
||||
it("toggles repeat on", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<RepeatForm {...p} />);
|
||||
wrapper.find("input").first().simulate("change", {
|
||||
currentTarget: { checked: true }
|
||||
});
|
||||
expect(p.fieldSet).toHaveBeenCalledWith("timeUnit", "daily");
|
||||
});
|
||||
|
||||
it("toggles repeat off", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<RepeatForm {...p} />);
|
||||
wrapper.find("input").first().simulate("change", {
|
||||
currentTarget: { checked: false }
|
||||
});
|
||||
expect(p.fieldSet).toHaveBeenCalledWith("timeUnit", "never");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<FarmEventDeleteButton />", () => {
|
||||
const fakeProps = (): FarmEventDeleteButtonProps => ({
|
||||
hidden: false,
|
||||
farmEvent: fakeFarmEvent("Sequence", 1),
|
||||
dispatch: jest.fn(() => Promise.resolve()),
|
||||
});
|
||||
|
||||
it("deletes farm event", async () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<FarmEventDeleteButton {...p} />);
|
||||
await wrapper.find("button").simulate("click");
|
||||
expect(destroy).toHaveBeenCalledWith(p.farmEvent.uuid);
|
||||
expect(history.push).toHaveBeenCalledWith("/app/designer/events");
|
||||
expect(success).toHaveBeenCalledWith("Deleted event.", "Deleted");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import * as React from "react";
|
||||
import { RepeatFormProps, FarmEventRepeatForm } from "../farm_event_repeat_form";
|
||||
import { betterMerge } from "../../../util";
|
||||
import {
|
||||
FarmEventRepeatFormProps, FarmEventRepeatForm
|
||||
} from "../farm_event_repeat_form";
|
||||
import { shallow, ShallowWrapper, render } from "enzyme";
|
||||
import { get } from "lodash";
|
||||
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
|
||||
|
||||
const DEFAULTS: RepeatFormProps = {
|
||||
const fakeProps = (): FarmEventRepeatFormProps => ({
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
onChange: jest.fn(),
|
||||
fieldSet: jest.fn(),
|
||||
timeUnit: "daily",
|
||||
repeat: "1",
|
||||
endDate: "2017-07-26",
|
||||
endTime: "08:57",
|
||||
timeSettings: fakeTimeSettings(),
|
||||
};
|
||||
});
|
||||
|
||||
enum Selectors {
|
||||
REPEAT = "BlurableInput[name=\"repeat\"]",
|
||||
|
@ -23,10 +24,6 @@ enum Selectors {
|
|||
TIME_UNIT = "FBSelect"
|
||||
}
|
||||
|
||||
function props(i?: Partial<RepeatFormProps>): RepeatFormProps {
|
||||
return betterMerge(DEFAULTS, i || {});
|
||||
}
|
||||
|
||||
function formVal(el: ShallowWrapper<{}, {}>, query: string) {
|
||||
return getProp(el, query, "value");
|
||||
}
|
||||
|
@ -37,8 +34,8 @@ function getProp(el: ShallowWrapper<{}, {}>, query: string, prop: string) {
|
|||
|
||||
describe("<FarmEventRepeatForm/>", () => {
|
||||
it("shows proper values", () => {
|
||||
const p = props();
|
||||
const el = shallow<RepeatFormProps>(<FarmEventRepeatForm {...p} />);
|
||||
const p = fakeProps();
|
||||
const el = shallow<FarmEventRepeatFormProps>(<FarmEventRepeatForm {...p} />);
|
||||
expect(formVal(el, Selectors.REPEAT)).toEqual(p.repeat);
|
||||
expect(formVal(el, Selectors.END_DATE)).toEqual(p.endDate);
|
||||
expect(formVal(el, Selectors.END_TIME)).toEqual(p.endTime);
|
||||
|
@ -47,7 +44,7 @@ describe("<FarmEventRepeatForm/>", () => {
|
|||
});
|
||||
|
||||
it("defaults to `daily` when a bad input it passed", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.timeUnit = "never";
|
||||
const el = shallow(<FarmEventRepeatForm {...p} />);
|
||||
expect(formVal(el, Selectors.REPEAT)).toEqual(p.repeat);
|
||||
|
@ -55,7 +52,7 @@ describe("<FarmEventRepeatForm/>", () => {
|
|||
});
|
||||
|
||||
it("disables all inputs via the `disabled` prop", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.disabled = true;
|
||||
const el = shallow(<FarmEventRepeatForm {...p} />);
|
||||
expect(getProp(el, Selectors.END_DATE, "disabled")).toBeTruthy();
|
||||
|
@ -65,9 +62,37 @@ describe("<FarmEventRepeatForm/>", () => {
|
|||
});
|
||||
|
||||
it("hides", () => {
|
||||
const p = props();
|
||||
const p = fakeProps();
|
||||
p.hidden = true;
|
||||
const el = render(<FarmEventRepeatForm {...p} />);
|
||||
expect(el.text()).toEqual("");
|
||||
});
|
||||
|
||||
const testBlurable = (input: string, field: string, value: string) => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<FarmEventRepeatForm {...p} />);
|
||||
wrapper.find(input).simulate("commit", {
|
||||
currentTarget: { value }
|
||||
});
|
||||
expect(p.fieldSet).toHaveBeenCalledWith(field, value);
|
||||
};
|
||||
|
||||
it("changes repeat frequency", () => {
|
||||
testBlurable(Selectors.REPEAT, "repeat", "1");
|
||||
});
|
||||
|
||||
it("changes time unit", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<FarmEventRepeatForm {...p} />);
|
||||
wrapper.find(Selectors.TIME_UNIT).simulate("change", { value: "daily" });
|
||||
expect(p.fieldSet).toHaveBeenCalledWith("timeUnit", "daily");
|
||||
});
|
||||
|
||||
it("changes end date", () => {
|
||||
testBlurable(Selectors.END_DATE, "endDate", "2017-07-26");
|
||||
});
|
||||
|
||||
it("changes end time", () => {
|
||||
testBlurable(Selectors.END_TIME, "endTime", "08:57");
|
||||
});
|
||||
});
|
|
@ -5,7 +5,6 @@ import {
|
|||
} from "../../../__test_support__/farm_event_calendar_support";
|
||||
import { render, shallow, mount } from "enzyme";
|
||||
import { get } from "lodash";
|
||||
import { Content } from "../../../constants";
|
||||
import { defensiveClone } from "../../../util";
|
||||
import { FarmEventProps } from "../../interfaces";
|
||||
|
||||
|
@ -29,15 +28,6 @@ describe("<PureFarmEvents/>", () => {
|
|||
expect(rows[2]).toEqual("02:00pm");
|
||||
});
|
||||
|
||||
it("warns about unset timezones", () => {
|
||||
const p = fakeProps();
|
||||
p.timezoneIsSet = false;
|
||||
const results = render(<PureFarmEvents {...p} />);
|
||||
const txt = results.text();
|
||||
expect(txt).toContain(Content.SET_TIMEZONE_HEADER);
|
||||
expect(txt).toContain(Content.SET_TIMEZONE_BODY);
|
||||
});
|
||||
|
||||
it("renders FarmEvent lacking a subheading", () => {
|
||||
const p = fakeProps();
|
||||
const row = [defensiveClone(calendarRows[0])];
|
||||
|
|
|
@ -1,36 +1,53 @@
|
|||
import * as React from "react";
|
||||
import moment from "moment";
|
||||
import { connect } from "react-redux";
|
||||
import { mapStateToPropsAddEdit, } from "./map_state_to_props_add_edit";
|
||||
import {
|
||||
mapStateToPropsAddEdit, formatDate, formatTime,
|
||||
} from "./map_state_to_props_add_edit";
|
||||
import { init, destroy } from "../../api/crud";
|
||||
import { EditFEForm } from "./edit_fe_form";
|
||||
import { betterCompact } from "../../util";
|
||||
import {
|
||||
EditFEForm, FarmEventForm, FarmEventViewModel, NEVER
|
||||
} from "./edit_fe_form";
|
||||
import { betterCompact, betterMerge } from "../../util";
|
||||
import { entries } from "../../resources/util";
|
||||
import {
|
||||
AddEditFarmEventProps,
|
||||
TaggedExecutable
|
||||
} from "../interfaces";
|
||||
import { ExecutableType } from "farmbot/dist/resources/api_resources";
|
||||
import { Link } from "../../link";
|
||||
import {
|
||||
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
|
||||
} from "../designer_panel";
|
||||
import { variableList } from "../../sequences/locals_list/variable_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Panel } from "../panel_header";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { destroyOK } from "../../resources/actions";
|
||||
import { Content } from "../../constants";
|
||||
import { error } from "../../toast/toast";
|
||||
|
||||
interface State {
|
||||
uuid: string;
|
||||
temporaryValues: Partial<FarmEventViewModel>;
|
||||
}
|
||||
|
||||
export class RawAddFarmEvent
|
||||
extends React.Component<AddEditFarmEventProps, Partial<State>> {
|
||||
|
||||
constructor(props: AddEditFarmEventProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
extends React.Component<AddEditFarmEventProps, State> {
|
||||
temporaryValueDefaults = () => {
|
||||
const now = new Date();
|
||||
const later = new Date(now.getTime() + 60000);
|
||||
return {
|
||||
startDate: formatDate(now.toString(), this.props.timeSettings),
|
||||
startTime: formatTime(now.toString(), this.props.timeSettings),
|
||||
endDate: formatDate(now.toString(), this.props.timeSettings),
|
||||
endTime: formatTime(later.toString(), this.props.timeSettings),
|
||||
repeat: "1",
|
||||
timeUnit: NEVER,
|
||||
};
|
||||
}
|
||||
|
||||
state: State = { uuid: "", temporaryValues: this.temporaryValueDefaults() };
|
||||
|
||||
get sequences() { return betterCompact(entries(this.props.sequencesById)); }
|
||||
|
||||
get regimens() { return betterCompact(entries(this.props.regimensById)); }
|
||||
|
@ -71,55 +88,51 @@ export class RawAddFarmEvent
|
|||
if (fe && unsaved) { this.props.dispatch(destroy(fe.uuid, true)); }
|
||||
}
|
||||
|
||||
/** No executables. Can't load form. */
|
||||
none() {
|
||||
return <p>
|
||||
{t("You haven't made any regimens or sequences yet. Please create a ")}
|
||||
<Link to="/app/sequences">{t("sequence")}</Link> {t(" or ")}
|
||||
<Link to="/app/regimens">{t("regimen")}</Link> {t(" first.")}
|
||||
</p>;
|
||||
}
|
||||
|
||||
/** User has executables to create FarmEvents with, has not loaded yet. */
|
||||
loading() {
|
||||
return <p>{t("Loading")}...</p>;
|
||||
}
|
||||
|
||||
placeholderTemplate(children: React.ReactChild | React.ReactChild[]) {
|
||||
return <DesignerPanel panelName={"add-farm-event"} panel={Panel.FarmEvents}>
|
||||
<DesignerPanelHeader
|
||||
panelName={"add-farm-event"}
|
||||
panel={Panel.FarmEvents}
|
||||
title={t("No Executables")} />
|
||||
<DesignerPanelContent panelName={"add-farm-event"}>
|
||||
<label>
|
||||
{children}
|
||||
</label>
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
}
|
||||
getField = (field: keyof State["temporaryValues"]): string =>
|
||||
"" + this.state.temporaryValues[field];
|
||||
setField = (field: keyof State["temporaryValues"], value: string) =>
|
||||
this.setState(betterMerge(this.state, {
|
||||
temporaryValues: { [field]: value }
|
||||
}))
|
||||
|
||||
render() {
|
||||
const { uuid } = this.state;
|
||||
const fe = this.props.findFarmEventByUuid(uuid);
|
||||
if (fe) {
|
||||
return <EditFEForm
|
||||
farmEvent={fe}
|
||||
deviceTimezone={this.props.deviceTimezone}
|
||||
repeatOptions={this.props.repeatOptions}
|
||||
executableOptions={this.props.executableOptions}
|
||||
dispatch={this.props.dispatch}
|
||||
findExecutable={this.props.findExecutable}
|
||||
const farmEvent = this.props.findFarmEventByUuid(this.state.uuid);
|
||||
const panelName = "add-farm-event";
|
||||
return <DesignerPanel panelName={panelName} panel={Panel.FarmEvents}>
|
||||
<DesignerPanelHeader
|
||||
panelName={panelName}
|
||||
panel={Panel.FarmEvents}
|
||||
title={t("Add Event")}
|
||||
timeSettings={this.props.timeSettings}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
resources={this.props.resources}
|
||||
shouldDisplay={this.props.shouldDisplay}
|
||||
/>;
|
||||
} else {
|
||||
return this
|
||||
.placeholderTemplate(this.executable ? this.loading() : this.none());
|
||||
}
|
||||
onBack={(farmEvent && !farmEvent.body.id)
|
||||
? () => this.props.dispatch(destroyOK(farmEvent))
|
||||
: undefined} />
|
||||
<DesignerPanelContent panelName={panelName}>
|
||||
{farmEvent
|
||||
? <EditFEForm
|
||||
farmEvent={farmEvent}
|
||||
deviceTimezone={this.props.deviceTimezone}
|
||||
repeatOptions={this.props.repeatOptions}
|
||||
executableOptions={this.props.executableOptions}
|
||||
dispatch={this.props.dispatch}
|
||||
findExecutable={this.props.findExecutable}
|
||||
title={t("Add Event")}
|
||||
timeSettings={this.props.timeSettings}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
resources={this.props.resources}
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
: <FarmEventForm
|
||||
isRegimen={false}
|
||||
fieldGet={this.getField}
|
||||
fieldSet={this.setField}
|
||||
timeSettings={this.props.timeSettings}
|
||||
executableOptions={[]}
|
||||
executableSet={() => { }}
|
||||
executableGet={() => undefined}
|
||||
dispatch={this.props.dispatch}
|
||||
specialStatus={SpecialStatus.DIRTY}
|
||||
onSave={() => error(t(Content.MISSING_EXECUTABLE))} />}
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,10 @@ export function occurrence(
|
|||
CalendarOccurrence {
|
||||
const normalHeading = fe.executable.name || fe.executable_type;
|
||||
const heading = () => {
|
||||
if (modifiers && modifiers.empty) {
|
||||
if (modifiers?.empty) {
|
||||
return "*Empty*";
|
||||
}
|
||||
if (modifiers && modifiers.numHidden) {
|
||||
if (modifiers?.numHidden) {
|
||||
return `+ ${modifiers.numHidden} more: ` + normalHeading;
|
||||
}
|
||||
return normalHeading;
|
||||
|
|
|
@ -3,7 +3,7 @@ import moment from "moment";
|
|||
import { success, error } from "../../toast/toast";
|
||||
import {
|
||||
TaggedFarmEvent, SpecialStatus, TaggedSequence, TaggedRegimen,
|
||||
ParameterApplication
|
||||
ParameterApplication,
|
||||
} from "farmbot";
|
||||
import { ExecutableQuery } from "../interfaces";
|
||||
import { formatTime, formatDate } from "./map_state_to_props_add_edit";
|
||||
|
@ -12,18 +12,17 @@ import {
|
|||
Col, Row,
|
||||
SaveBtn,
|
||||
FBSelect,
|
||||
DropDownItem
|
||||
DropDownItem,
|
||||
Help,
|
||||
} from "../../ui";
|
||||
import { destroy, save, overwrite } from "../../api/crud";
|
||||
import { history } from "../../history";
|
||||
// TIL: https://stackoverflow.com/a/24900248/1064917
|
||||
import { betterMerge, parseIntInput } from "../../util";
|
||||
import { maybeWarnAboutMissedTasks } from "./util";
|
||||
import { FarmEventRepeatForm } from "./farm_event_repeat_form";
|
||||
import { scheduleForFarmEvent } from "./calendar/scheduler";
|
||||
import { executableType } from "../util";
|
||||
import { Content } from "../../constants";
|
||||
import { destroyOK } from "../../resources/actions";
|
||||
import { EventTimePicker } from "./event_time_picker";
|
||||
import { TzWarning } from "./tz_warning";
|
||||
import { nextRegItemTimes } from "./map_state_to_props";
|
||||
|
@ -31,9 +30,6 @@ import { first } from "lodash";
|
|||
import {
|
||||
TimeUnit, ExecutableType, FarmEvent
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
import {
|
||||
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
|
||||
} from "../designer_panel";
|
||||
import { LocalsList } from "../../sequences/locals_list/locals_list";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
import { ShouldDisplay } from "../../devices/interfaces";
|
||||
|
@ -41,13 +37,11 @@ import {
|
|||
addOrEditParamApps, variableList, getRegimenVariableData
|
||||
} from "../../sequences/locals_list/variable_support";
|
||||
import {
|
||||
AllowedVariableNodes
|
||||
AllowedVariableNodes,
|
||||
} from "../../sequences/locals_list/locals_list_support";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
import { Panel } from "../panel_header";
|
||||
|
||||
type FormEvent = React.SyntheticEvent<HTMLInputElement>;
|
||||
export const NEVER: TimeUnit = "never";
|
||||
/** Separate each of the form fields into their own interface. Recombined later
|
||||
* on save.
|
||||
|
@ -66,6 +60,8 @@ export interface FarmEventViewModel {
|
|||
body?: ParameterApplication[];
|
||||
}
|
||||
|
||||
export type FarmEventViewModelKey = keyof FarmEventViewModel;
|
||||
|
||||
/** Breaks up a TaggedFarmEvent into a structure that can easily be used
|
||||
* by the edit form.
|
||||
* USE CASE EXAMPLE: We have a "date" and "time" field that are created from
|
||||
|
@ -148,7 +144,7 @@ export interface EditFEProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
interface State {
|
||||
export interface EditFEFormState {
|
||||
/**
|
||||
* Hold a partial FarmEvent locally containing only updates made by the form.
|
||||
*/
|
||||
|
@ -162,12 +158,8 @@ interface State {
|
|||
specialStatusLocal: SpecialStatus;
|
||||
}
|
||||
|
||||
export class EditFEForm extends React.Component<EditFEProps, State> {
|
||||
state: State = { fe: {}, specialStatusLocal: SpecialStatus.SAVED };
|
||||
|
||||
get repeats() { return this.fieldGet("timeUnit") !== NEVER; }
|
||||
|
||||
get dispatch() { return this.props.dispatch; }
|
||||
export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
|
||||
state: EditFEFormState = { fe: {}, specialStatusLocal: SpecialStatus.SAVED };
|
||||
|
||||
/** API data for the FarmEvent to which form updates can be applied. */
|
||||
get viewModel() {
|
||||
|
@ -240,7 +232,7 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
const { uuid } = this.props.findExecutable(
|
||||
next_executable_type, parseInt("" + ddi.value));
|
||||
const varData = this.props.resources.sequenceMetas[uuid];
|
||||
const update: State = {
|
||||
const update: EditFEFormState = {
|
||||
fe: {
|
||||
executable_type: next_executable_type,
|
||||
executable_id: (ddi.value || "").toString(),
|
||||
|
@ -263,28 +255,15 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
};
|
||||
}
|
||||
|
||||
fieldSet = (name: keyof State["fe"]) => (e: FormEvent) => {
|
||||
fieldSet = (name: FarmEventViewModelKey, value: string) =>
|
||||
// A merge is required to not overwrite `fe`.
|
||||
this.setState(betterMerge(this.state, {
|
||||
fe: { [name]: e.currentTarget.value },
|
||||
fe: { [name]: value },
|
||||
specialStatusLocal: SpecialStatus.DIRTY
|
||||
}));
|
||||
}
|
||||
}))
|
||||
|
||||
fieldGet = (name: keyof State["fe"]): string => {
|
||||
return (this.state.fe[name] || this.viewModel[name] || "").toString();
|
||||
}
|
||||
|
||||
mergeState = (k: keyof FarmEventViewModel, v: string) => {
|
||||
this.setState(betterMerge(this.state, {
|
||||
fe: { [k]: v },
|
||||
specialStatusLocal: SpecialStatus.DIRTY
|
||||
}));
|
||||
}
|
||||
|
||||
toggleRepeat = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { checked } = e.currentTarget;
|
||||
this.mergeState("timeUnit", (!checked || this.isReg) ? "never" : "daily");
|
||||
};
|
||||
fieldGet = (name: FarmEventViewModelKey): string =>
|
||||
(this.state.fe[name] || this.viewModel[name] || "").toString()
|
||||
|
||||
nextItemTime = (fe: FarmEvent, now: moment.Moment
|
||||
): moment.Moment | undefined => {
|
||||
|
@ -362,11 +341,12 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
if (!this.nextItemTime(this.updatedFarmEvent, now)) {
|
||||
return nothingToRunWarning();
|
||||
}
|
||||
this.dispatch(overwrite(this.props.farmEvent, this.updatedFarmEvent));
|
||||
this.dispatch(save(this.props.farmEvent.uuid))
|
||||
const { dispatch } = this.props;
|
||||
dispatch(overwrite(this.props.farmEvent, this.updatedFarmEvent));
|
||||
dispatch(save(this.props.farmEvent.uuid))
|
||||
.then(() => {
|
||||
this.setState({ specialStatusLocal: SpecialStatus.SAVED });
|
||||
this.dispatch(maybeWarnAboutMissedTasks(this.props.farmEvent,
|
||||
dispatch(maybeWarnAboutMissedTasks(this.props.farmEvent,
|
||||
() => alert(t(Content.REGIMEN_TODAY_SKIPPED_ITEM_RISK)), now));
|
||||
this.nextRunTimeActions(now);
|
||||
history.push("/app/designer/events");
|
||||
|
@ -377,123 +357,184 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
});
|
||||
}
|
||||
|
||||
StartTimeForm = () => {
|
||||
const forceMidnight = this.isReg;
|
||||
return <div>
|
||||
<label>
|
||||
{t("Starts")}
|
||||
</label>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<BlurableInput
|
||||
type="date"
|
||||
className="add-event-start-date"
|
||||
name="start_date"
|
||||
value={this.fieldGet("startDate")}
|
||||
onCommit={this.fieldSet("startDate")} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<EventTimePicker
|
||||
className="add-event-start-time"
|
||||
name="start_time"
|
||||
timeSettings={this.props.timeSettings}
|
||||
value={this.fieldGet("startTime")}
|
||||
onCommit={this.fieldSet("startTime")}
|
||||
disabled={forceMidnight}
|
||||
hidden={forceMidnight} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
}
|
||||
|
||||
RepeatCheckbox = ({ allowRepeat }: { allowRepeat: boolean }) =>
|
||||
!this.isReg ?
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={this.toggleRepeat}
|
||||
disabled={this.isReg}
|
||||
checked={allowRepeat} />
|
||||
{t("Repeats?")}
|
||||
</label> : <div />
|
||||
|
||||
dateCheck = (): string | undefined => {
|
||||
const startDate = this.fieldGet("startDate");
|
||||
const endDate = this.fieldGet("endDate");
|
||||
if (!moment(endDate).isSameOrAfter(moment(startDate))) {
|
||||
return t("End date must not be before start date.");
|
||||
}
|
||||
}
|
||||
|
||||
timeCheck = (): string | undefined => {
|
||||
const startDate = this.fieldGet("startDate");
|
||||
const startTime = this.fieldGet("startTime");
|
||||
const endDate = this.fieldGet("endDate");
|
||||
const endTime = this.fieldGet("endTime");
|
||||
const start = offsetTime(startDate, startTime, this.props.timeSettings);
|
||||
const end = offsetTime(endDate, endTime, this.props.timeSettings);
|
||||
if (moment(start).isSameOrAfter(moment(end))) {
|
||||
return t("End time must be after start time.");
|
||||
}
|
||||
}
|
||||
|
||||
RepeatForm = () => {
|
||||
const allowRepeat = !this.isReg && this.repeats;
|
||||
return <div>
|
||||
<this.RepeatCheckbox allowRepeat={allowRepeat} />
|
||||
<FarmEventRepeatForm
|
||||
timeSettings={this.props.timeSettings}
|
||||
disabled={!allowRepeat}
|
||||
hidden={!allowRepeat}
|
||||
onChange={this.mergeState}
|
||||
timeUnit={this.fieldGet("timeUnit") as TimeUnit}
|
||||
repeat={this.fieldGet("repeat")}
|
||||
endDate={this.fieldGet("endDate")}
|
||||
endTime={this.fieldGet("endTime")}
|
||||
dateError={this.dateCheck()}
|
||||
timeError={this.timeCheck()} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
FarmEventDeleteButton = () =>
|
||||
<button className="fb-button red" hidden={!this.props.deleteBtn}
|
||||
onClick={() => {
|
||||
this.dispatch(destroy(this.props.farmEvent.uuid))
|
||||
.then(() => {
|
||||
history.push("/app/designer/events");
|
||||
success(t("Deleted event."), t("Deleted"));
|
||||
});
|
||||
}}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
|
||||
render() {
|
||||
const { farmEvent } = this.props;
|
||||
return <DesignerPanel panelName={"add-farm-event"} panel={Panel.FarmEvents}>
|
||||
<DesignerPanelHeader
|
||||
panelName={"add-farm-event"}
|
||||
panel={Panel.FarmEvents}
|
||||
title={this.props.title}
|
||||
onBack={!farmEvent.body.id ? () =>
|
||||
// Throw out unsaved farmevents.
|
||||
this.props.dispatch(destroyOK(farmEvent))
|
||||
: undefined} />
|
||||
<DesignerPanelContent panelName={"add-farm-event"}>
|
||||
<label>
|
||||
{t("Sequence or Regimen")}
|
||||
</label>
|
||||
<FBSelect
|
||||
list={this.props.executableOptions}
|
||||
onChange={this.executableSet}
|
||||
selectedItem={this.executableGet()} />
|
||||
return <div className="edit-farm-event-form">
|
||||
<FarmEventForm
|
||||
isRegimen={this.isReg}
|
||||
fieldGet={this.fieldGet}
|
||||
fieldSet={this.fieldSet}
|
||||
timeSettings={this.props.timeSettings}
|
||||
executableOptions={this.props.executableOptions}
|
||||
executableSet={this.executableSet}
|
||||
executableGet={this.executableGet}
|
||||
dispatch={this.props.dispatch}
|
||||
specialStatus={farmEvent.specialStatus || this.state.specialStatusLocal}
|
||||
onSave={() => this.commitViewModel()}>
|
||||
<this.LocalsList />
|
||||
<this.StartTimeForm />
|
||||
<this.RepeatForm />
|
||||
<SaveBtn
|
||||
status={farmEvent.specialStatus || this.state.specialStatusLocal}
|
||||
onClick={() => this.commitViewModel()} />
|
||||
<this.FarmEventDeleteButton />
|
||||
<TzWarning deviceTimezone={this.props.deviceTimezone} />
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel>;
|
||||
</FarmEventForm>
|
||||
<FarmEventDeleteButton
|
||||
hidden={!this.props.deleteBtn}
|
||||
farmEvent={this.props.farmEvent}
|
||||
dispatch={this.props.dispatch} />
|
||||
<TzWarning deviceTimezone={this.props.deviceTimezone} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface StartTimeFormProps {
|
||||
isRegimen: boolean;
|
||||
fieldGet(name: FarmEventViewModelKey): string;
|
||||
fieldSet(name: FarmEventViewModelKey, value: string): void;
|
||||
timeSettings: TimeSettings;
|
||||
}
|
||||
|
||||
export const StartTimeForm = (props: StartTimeFormProps) => {
|
||||
const forceMidnight = props.isRegimen;
|
||||
return <div className="start-time-form">
|
||||
<label>
|
||||
{t("Starts")}
|
||||
</label>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<BlurableInput
|
||||
type="date"
|
||||
className="add-event-start-date"
|
||||
name="start_date"
|
||||
value={props.fieldGet("startDate")}
|
||||
onCommit={e => props.fieldSet("startDate", e.currentTarget.value)} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<EventTimePicker
|
||||
className="add-event-start-time"
|
||||
name="start_time"
|
||||
timeSettings={props.timeSettings}
|
||||
value={props.fieldGet("startTime")}
|
||||
onCommit={e => props.fieldSet("startTime", e.currentTarget.value)}
|
||||
disabled={forceMidnight}
|
||||
hidden={forceMidnight} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export interface RepeatFormProps {
|
||||
isRegimen: boolean;
|
||||
fieldGet(name: FarmEventViewModelKey): string;
|
||||
fieldSet(name: FarmEventViewModelKey, value: string): void;
|
||||
timeSettings: TimeSettings;
|
||||
}
|
||||
|
||||
export const RepeatForm = (props: RepeatFormProps) => {
|
||||
const allowRepeat = !props.isRegimen && props.fieldGet("timeUnit") !== NEVER;
|
||||
return <div className="farm-event-repeat-options">
|
||||
{!props.isRegimen
|
||||
? <label>
|
||||
<input type="checkbox"
|
||||
onChange={e => props.fieldSet("timeUnit",
|
||||
(!e.currentTarget.checked || props.isRegimen) ? "never" : "daily")}
|
||||
disabled={props.isRegimen}
|
||||
checked={allowRepeat} />
|
||||
{t("Repeats?")}
|
||||
</label>
|
||||
: <div />}
|
||||
<FarmEventRepeatForm
|
||||
timeSettings={props.timeSettings}
|
||||
disabled={!allowRepeat}
|
||||
hidden={!allowRepeat}
|
||||
fieldSet={props.fieldSet}
|
||||
timeUnit={props.fieldGet("timeUnit") as TimeUnit}
|
||||
repeat={props.fieldGet("repeat")}
|
||||
endDate={props.fieldGet("endDate")}
|
||||
endTime={props.fieldGet("endTime")}
|
||||
dateError={dateCheck(props.fieldGet)}
|
||||
timeError={timeCheck(props.fieldGet, props.timeSettings)} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const dateCheck = (
|
||||
fieldGet: (name: FarmEventViewModelKey) => string
|
||||
): string | undefined => {
|
||||
const startDate = fieldGet("startDate");
|
||||
const endDate = fieldGet("endDate");
|
||||
if (!moment(endDate).isSameOrAfter(moment(startDate))) {
|
||||
return t("End date must not be before start date.");
|
||||
}
|
||||
};
|
||||
|
||||
export const timeCheck = (
|
||||
fieldGet: (name: FarmEventViewModelKey) => string,
|
||||
timeSettings: TimeSettings
|
||||
): string | undefined => {
|
||||
const startDate = fieldGet("startDate");
|
||||
const startTime = fieldGet("startTime");
|
||||
const endDate = fieldGet("endDate");
|
||||
const endTime = fieldGet("endTime");
|
||||
const start = offsetTime(startDate, startTime, timeSettings);
|
||||
const end = offsetTime(endDate, endTime, timeSettings);
|
||||
if (moment(start).isSameOrAfter(moment(end))) {
|
||||
return t("End time must be after start time.");
|
||||
}
|
||||
};
|
||||
|
||||
export interface FarmEventDeleteButtonProps {
|
||||
hidden: boolean;
|
||||
farmEvent: TaggedFarmEvent;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export const FarmEventDeleteButton = (props: FarmEventDeleteButtonProps) =>
|
||||
<button className="fb-button red" hidden={props.hidden}
|
||||
onClick={() =>
|
||||
props.dispatch(destroy(props.farmEvent.uuid))
|
||||
.then(() => {
|
||||
history.push("/app/designer/events");
|
||||
success(t("Deleted event."), t("Deleted"));
|
||||
})}>
|
||||
{t("Delete")}
|
||||
</button>;
|
||||
|
||||
export interface FarmEventFormProps {
|
||||
isRegimen: boolean;
|
||||
fieldGet(name: FarmEventViewModelKey): string;
|
||||
fieldSet(name: FarmEventViewModelKey, value: string): void;
|
||||
timeSettings: TimeSettings;
|
||||
executableOptions: DropDownItem[];
|
||||
executableSet(ddi: DropDownItem): void;
|
||||
executableGet(): DropDownItem | undefined;
|
||||
dispatch: Function;
|
||||
specialStatus: SpecialStatus;
|
||||
onSave(): void;
|
||||
children?: React.ReactChild;
|
||||
}
|
||||
|
||||
export const FarmEventForm = (props: FarmEventFormProps) => {
|
||||
const { isRegimen, fieldGet, fieldSet, timeSettings } = props;
|
||||
return <div className="farm-event-form">
|
||||
<label>
|
||||
{t("Sequence or Regimen")}
|
||||
</label>
|
||||
{props.executableOptions.length < 1 &&
|
||||
<Help
|
||||
text={Content.MISSING_EXECUTABLE}
|
||||
customIcon={"exclamation-triangle"} />}
|
||||
<FBSelect
|
||||
list={props.executableOptions}
|
||||
onChange={props.executableSet}
|
||||
selectedItem={props.executableGet()} />
|
||||
{props.children}
|
||||
<StartTimeForm
|
||||
isRegimen={isRegimen}
|
||||
fieldGet={fieldGet}
|
||||
fieldSet={fieldSet}
|
||||
timeSettings={timeSettings} />
|
||||
<RepeatForm
|
||||
isRegimen={isRegimen}
|
||||
fieldGet={fieldGet}
|
||||
fieldSet={fieldSet}
|
||||
timeSettings={props.timeSettings} />
|
||||
<SaveBtn
|
||||
status={props.specialStatus}
|
||||
onClick={props.onSave} />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -10,15 +10,12 @@ import { TimeUnit } from "farmbot/dist/resources/api_resources";
|
|||
import { t } from "../../i18next_wrapper";
|
||||
import { TimeSettings } from "../../interfaces";
|
||||
|
||||
type Ev = React.SyntheticEvent<HTMLInputElement>;
|
||||
type Key = keyof FarmEventViewModel;
|
||||
|
||||
export interface RepeatFormProps {
|
||||
export interface FarmEventRepeatFormProps {
|
||||
/** Should the form controls be grayed out? */
|
||||
disabled: boolean;
|
||||
/** Should the form be shown _at all_? */
|
||||
hidden: boolean;
|
||||
onChange(key: Key, value: string): void;
|
||||
fieldSet(name: keyof FarmEventViewModel, value: string): void;
|
||||
timeUnit: TimeUnit;
|
||||
repeat: string;
|
||||
endDate: string;
|
||||
|
@ -31,11 +28,9 @@ export interface RepeatFormProps {
|
|||
const indexKey: keyof DropDownItem = "value";
|
||||
const OPTN_LOOKUP = keyBy(repeatOptions, indexKey);
|
||||
|
||||
export function FarmEventRepeatForm(props: RepeatFormProps) {
|
||||
const { disabled, onChange, repeat, endDate, endTime, timeUnit } = props;
|
||||
const changeHandler =
|
||||
(key: Key) => (e: Ev) => onChange(key, e.currentTarget.value);
|
||||
return props.hidden ? <div /> : <div>
|
||||
export function FarmEventRepeatForm(props: FarmEventRepeatFormProps) {
|
||||
const { disabled, fieldSet, repeat, endDate, endTime, timeUnit } = props;
|
||||
return props.hidden ? <div /> : <div className="farm-event-repeat-form">
|
||||
<label>
|
||||
{t("Every")}
|
||||
</label>
|
||||
|
@ -48,13 +43,13 @@ export function FarmEventRepeatForm(props: RepeatFormProps) {
|
|||
className="add-event-repeat-frequency"
|
||||
name="repeat"
|
||||
value={repeat}
|
||||
onCommit={changeHandler("repeat")}
|
||||
onCommit={e => fieldSet("repeat", e.currentTarget.value)}
|
||||
min={1} />
|
||||
</Col>
|
||||
<Col xs={8}>
|
||||
<FBSelect
|
||||
list={repeatOptions}
|
||||
onChange={(e) => onChange("timeUnit", "" + e.value)}
|
||||
onChange={ddi => fieldSet("timeUnit", "" + ddi.value)}
|
||||
selectedItem={OPTN_LOOKUP[timeUnit] || OPTN_LOOKUP["daily"]} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -69,7 +64,7 @@ export function FarmEventRepeatForm(props: RepeatFormProps) {
|
|||
className="add-event-end-date"
|
||||
name="endDate"
|
||||
value={endDate}
|
||||
onCommit={changeHandler("endDate")}
|
||||
onCommit={e => fieldSet("endDate", e.currentTarget.value)}
|
||||
error={props.dateError} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
|
@ -79,7 +74,7 @@ export function FarmEventRepeatForm(props: RepeatFormProps) {
|
|||
name="endTime"
|
||||
timeSettings={props.timeSettings}
|
||||
value={endTime}
|
||||
onCommit={changeHandler("endTime")}
|
||||
onCommit={e => fieldSet("endTime", e.currentTarget.value)}
|
||||
error={props.timeError} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Row } from "../../ui/index";
|
||||
import { mapStateToProps } from "./map_state_to_props";
|
||||
import {
|
||||
FarmEventProps, CalendarOccurrence, FarmEventState
|
||||
|
@ -100,28 +99,9 @@ export class PureFarmEvents
|
|||
});
|
||||
}
|
||||
|
||||
/** FarmEvents will generate some very unexpected results if the user has
|
||||
* not set a timezone for the bot (defaults to 0 UTC offset, which could be
|
||||
* far from user's local time). */
|
||||
tzwarning = () => {
|
||||
return <DesignerPanelContent panelName={"farm-event"}>
|
||||
<Row>
|
||||
</Row>
|
||||
|
||||
<div className="farm-events">
|
||||
<h2>Timezone Required</h2>
|
||||
<p>
|
||||
{t(Content.SET_TIMEZONE_HEADER)}
|
||||
</p>
|
||||
<p>
|
||||
<Link to="/app/device">{t(Content.SET_TIMEZONE_BODY)}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</DesignerPanelContent>;
|
||||
};
|
||||
|
||||
normalContent = () => {
|
||||
return <div className="farm-event-panel-normal-content">
|
||||
render() {
|
||||
return <DesignerPanel panelName={"farm-event"} panel={Panel.FarmEvents}>
|
||||
<DesignerNavTabs />
|
||||
<DesignerPanelTop
|
||||
panel={Panel.FarmEvents}
|
||||
linkTo={"/app/designer/events/add"}
|
||||
|
@ -134,7 +114,6 @@ export class PureFarmEvents
|
|||
placeholder={t("Search events...")} />
|
||||
</DesignerPanelTop>
|
||||
<DesignerPanelContent panelName={"farm-event"}>
|
||||
|
||||
<div className="farm-events">
|
||||
<EmptyStateWrapper
|
||||
notEmpty={this.props.calendarRows.length > 0}
|
||||
|
@ -146,19 +125,8 @@ export class PureFarmEvents
|
|||
</EmptyStateWrapper>
|
||||
</div>
|
||||
</DesignerPanelContent>
|
||||
</div>;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <DesignerPanel panelName={"farm-event"} panel={Panel.FarmEvents}>
|
||||
<DesignerNavTabs />
|
||||
{this.props.timezoneIsSet ? this.normalContent() : this.tzwarning()}
|
||||
</DesignerPanel>;
|
||||
}
|
||||
}
|
||||
|
||||
/** This is intentional. It is not a hack or a work around.
|
||||
* It avoids mocking `connect` in unit tests.
|
||||
* See testing pattern noted here: https://github.com/airbnb/enzyme/issues/98
|
||||
*/
|
||||
export const FarmEvents = connect(mapStateToProps)(PureFarmEvents);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../../history", () => ({
|
||||
history: { push: jest.fn() },
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
getPathArray: jest.fn(() => mockPath.split("/")),
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
|
@ -32,11 +32,12 @@ import {
|
|||
} from "../../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("movePlant", () => {
|
||||
function movePlantTest(
|
||||
caseDescription: string,
|
||||
attempted: { x: number, y: number },
|
||||
expected: { x: number, y: number }) {
|
||||
it(`restricts plant to grid area: ${caseDescription}`, () => {
|
||||
it.each<[string, Record<"x" | "y", number>, Record<"x" | "y", number>]>([
|
||||
["within bounds", { x: 1, y: 2 }, { x: 101, y: 202 }],
|
||||
["too high", { x: 10000, y: 10000 }, { x: 3000, y: 1500 }],
|
||||
["too low", { x: -10000, y: -10000 }, { x: 0, y: 0 }],
|
||||
])("restricts plant to grid area: %s",
|
||||
(_test_description, attempted, expected) => {
|
||||
const payload: MovePlantProps = {
|
||||
deltaX: attempted.x,
|
||||
deltaY: attempted.y,
|
||||
|
@ -57,10 +58,6 @@ describe("movePlant", () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
movePlantTest("within bounds", { x: 1, y: 2 }, { x: 101, y: 202 });
|
||||
movePlantTest("too high", { x: 10000, y: 10000 }, { x: 3000, y: 1500 });
|
||||
movePlantTest("too low", { x: -10000, y: -10000 }, { x: 0, y: 0 });
|
||||
});
|
||||
|
||||
describe("closePlantInfo()", () => {
|
||||
|
|
|
@ -36,7 +36,7 @@ const addOrRemoveFromGroup =
|
|||
const group = fetchGroupFromUrl(resources);
|
||||
const point =
|
||||
resources.references[clickedPlantUuid] as TaggedPoint | undefined;
|
||||
if (group && point && point.body.id) {
|
||||
if (group && point?.body.id) {
|
||||
type Body = (typeof group)["body"];
|
||||
const nextGroup: Body = ({
|
||||
...group.body,
|
||||
|
@ -54,7 +54,7 @@ const addOrRemoveFromSelection =
|
|||
(clickedPlantUuid: UUID, selectedPlants: UUID[] | undefined) => {
|
||||
const nextSelected =
|
||||
(selectedPlants || []).filter(uuid => uuid !== clickedPlantUuid);
|
||||
if (!(selectedPlants && selectedPlants.includes(clickedPlantUuid))) {
|
||||
if (!(selectedPlants?.includes(clickedPlantUuid))) {
|
||||
nextSelected.push(clickedPlantUuid);
|
||||
}
|
||||
return selectPlant(nextSelected);
|
||||
|
|
|
@ -8,22 +8,27 @@ import {
|
|||
} from "../../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<BotFigure/>", () => {
|
||||
function fakeProps(): BotFigureProps {
|
||||
return {
|
||||
name: "",
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 }
|
||||
};
|
||||
}
|
||||
const fakeProps = (): BotFigureProps => ({
|
||||
name: "",
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
});
|
||||
|
||||
function checkPositionForQuadrant(
|
||||
quadrant: BotOriginQuadrant,
|
||||
xySwap: boolean,
|
||||
expected: { x: number, y: number },
|
||||
name: string,
|
||||
opacity: number) {
|
||||
it(`shows ${name} in correct location for quadrant ${quadrant}`, () => {
|
||||
it.each<[
|
||||
string, BotOriginQuadrant, Record<"x" | "y", number>, boolean, number
|
||||
]>([
|
||||
["motors", 1, { x: 3000, y: 0 }, false, 0.75],
|
||||
["motors", 2, { x: 0, y: 0 }, false, 0.75],
|
||||
["motors", 3, { x: 0, y: 1500 }, false, 0.75],
|
||||
["motors", 4, { x: 3000, y: 1500 }, false, 0.75],
|
||||
["motors", 1, { x: 0, y: 1500 }, true, 0.75],
|
||||
["motors", 2, { x: 0, y: 0 }, true, 0.75],
|
||||
["motors", 3, { x: 3000, y: 0 }, true, 0.75],
|
||||
["motors", 4, { x: 3000, y: 1500 }, true, 0.75],
|
||||
["encoders", 2, { x: 0, y: 0 }, false, 0.25],
|
||||
])("shows %s in correct location for quadrant %i",
|
||||
(name, quadrant, expected, xySwap, opacity) => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.quadrant = quadrant;
|
||||
p.mapTransformProps.xySwap = xySwap;
|
||||
|
@ -53,17 +58,6 @@ describe("<BotFigure/>", () => {
|
|||
const UTMProps = result.find("circle").props();
|
||||
expect(UTMProps).toEqual(expectedUTMProps);
|
||||
});
|
||||
}
|
||||
|
||||
checkPositionForQuadrant(1, false, { x: 3000, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, false, { x: 0, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(3, false, { x: 0, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(4, false, { x: 3000, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(1, true, { x: 0, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, true, { x: 0, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(3, true, { x: 3000, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(4, true, { x: 3000, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, false, { x: 0, y: 0 }, "encoders", 0.25);
|
||||
|
||||
it("changes location", () => {
|
||||
const p = fakeProps();
|
||||
|
|
|
@ -6,25 +6,25 @@ import {
|
|||
} from "../../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<BotPeripherals/>", () => {
|
||||
function fakeProps(): BotPeripheralsProps {
|
||||
return {
|
||||
peripherals: [{ label: "", value: false }],
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
}
|
||||
const fakeProps = (): BotPeripheralsProps => ({
|
||||
peripherals: [{ label: "", value: false }],
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
getConfigValue: jest.fn(),
|
||||
});
|
||||
|
||||
function notDisplayed(name: string) {
|
||||
it(`doesn't display ${name}`, () => {
|
||||
const p = fakeProps();
|
||||
p.peripherals[0].label = name;
|
||||
p.peripherals[0].value = false;
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find(`#${name}`).length).toEqual(0);
|
||||
});
|
||||
}
|
||||
it.each<[string]>([
|
||||
["lights"],
|
||||
["vacuum"],
|
||||
["water"],
|
||||
])("doesn't display %s", (peripheralName) => {
|
||||
const p = fakeProps();
|
||||
p.peripherals[0].label = peripheralName;
|
||||
p.peripherals[0].value = false;
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find(`#${peripheralName}`).length).toEqual(0);
|
||||
});
|
||||
|
||||
function animationToggle(
|
||||
props: BotPeripheralsProps, enabled: number, disabled: number) {
|
||||
|
@ -101,8 +101,4 @@ describe("<BotPeripherals/>", () => {
|
|||
});
|
||||
animationToggle(p, 3, 1);
|
||||
});
|
||||
|
||||
notDisplayed("lights");
|
||||
notDisplayed("vacuum");
|
||||
notDisplayed("water");
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ function getNewTrailArray(update: TrailRecord, watering: boolean): TrailRecord[]
|
|||
const arr: TrailRecord[] = JSON.parse(get(sessionStorage, key, "[]"));
|
||||
if (arr.length > (trailLength - 1)) { arr.shift(); } // max length reached
|
||||
const last = arr[arr.length - 1]; // most recent item in array
|
||||
if (update && update.coord &&
|
||||
if (update?.coord &&
|
||||
(!last || !isEqual(last.coord, update.coord))) { // coordinate comparison
|
||||
arr.push(update); // unique addition
|
||||
} else { // nothing new to add, increase water circle size if watering
|
||||
|
|
|
@ -11,31 +11,25 @@ import {
|
|||
} from "../../../../../__test_support__/fake_time_settings";
|
||||
|
||||
const mockConfig = fakeWebAppConfig();
|
||||
jest.mock("../../../../../resources/selectors", () => {
|
||||
return {
|
||||
getWebAppConfig: () => mockConfig,
|
||||
assertUuid: jest.fn()
|
||||
};
|
||||
});
|
||||
jest.mock("../../../../../resources/selectors", () => ({
|
||||
getWebAppConfig: () => mockConfig,
|
||||
assertUuid: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../config_storage/actions", () => {
|
||||
return {
|
||||
setWebAppConfigValue: jest.fn()
|
||||
};
|
||||
});
|
||||
jest.mock("../../../../../config_storage/actions", () => ({
|
||||
setWebAppConfigValue: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<ImageFilterMenu />", () => {
|
||||
mockConfig.body.photo_filter_begin = "";
|
||||
mockConfig.body.photo_filter_end = "";
|
||||
|
||||
const fakeProps = (): ImageFilterMenuProps => {
|
||||
return {
|
||||
timeSettings: fakeTimeSettings(),
|
||||
dispatch: jest.fn(),
|
||||
getConfigValue: jest.fn(x => mockConfig.body[x as StringConfigKey]),
|
||||
imageAgeInfo: { newestDate: "", toOldest: 1 }
|
||||
};
|
||||
};
|
||||
const fakeProps = (): ImageFilterMenuProps => ({
|
||||
timeSettings: fakeTimeSettings(),
|
||||
dispatch: jest.fn(),
|
||||
getConfigValue: jest.fn(x => mockConfig.body[x as StringConfigKey]),
|
||||
imageAgeInfo: { newestDate: "", toOldest: 1 },
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const p = fakeProps();
|
||||
|
@ -44,44 +38,38 @@ describe("<ImageFilterMenu />", () => {
|
|||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
const testFilterSetDate =
|
||||
(filter: "beginDate" | "endDate",
|
||||
key: "photo_filter_begin" | "photo_filter_end",
|
||||
i: number) => {
|
||||
it(`sets filter: ${filter}`, () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "2001-01-03" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("2001-01-03");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(key, "2001-01-03T00:00:00.000Z");
|
||||
});
|
||||
};
|
||||
it.each<[
|
||||
"beginDate" | "endDate", "photo_filter_begin" | "photo_filter_end", number
|
||||
]>([
|
||||
["beginDate", "photo_filter_begin", 0],
|
||||
["endDate", "photo_filter_end", 2],
|
||||
])("sets filter: %s", (filter, key, i) => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "2001-01-03" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("2001-01-03");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(key, "2001-01-03T00:00:00.000Z");
|
||||
});
|
||||
|
||||
testFilterSetDate("beginDate", "photo_filter_begin", 0);
|
||||
testFilterSetDate("endDate", "photo_filter_end", 2);
|
||||
|
||||
const testFilterSetTime =
|
||||
(filter: "beginTime" | "endTime",
|
||||
key: "photo_filter_begin" | "photo_filter_end",
|
||||
i: number) => {
|
||||
it(`sets filter: ${filter}`, () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "05:00" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("05:00");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(key, "2001-01-03T05:00:00.000Z");
|
||||
});
|
||||
};
|
||||
|
||||
testFilterSetTime("beginTime", "photo_filter_begin", 1);
|
||||
testFilterSetTime("endTime", "photo_filter_end", 3);
|
||||
it.each<[
|
||||
"beginTime" | "endTime", "photo_filter_begin" | "photo_filter_end", number
|
||||
]>([
|
||||
["beginTime", "photo_filter_begin", 1],
|
||||
["endTime", "photo_filter_end", 3],
|
||||
])("sets filter: %s", (filter, key, i) => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<ImageFilterMenu>(<ImageFilterMenu {...p} />);
|
||||
wrapper.setState({ beginDate: "2001-01-03", endDate: "2001-01-03" });
|
||||
wrapper.find("BlurableInput").at(i).simulate("commit", {
|
||||
currentTarget: { value: "05:00" }
|
||||
});
|
||||
expect(wrapper.instance().state[filter]).toEqual("05:00");
|
||||
expect(setWebAppConfigValue)
|
||||
.toHaveBeenCalledWith(key, "2001-01-03T05:00:00.000Z");
|
||||
});
|
||||
|
||||
it("loads values from config", () => {
|
||||
mockConfig.body.photo_filter_begin = "2001-01-03T05:00:00.000Z";
|
||||
|
|
|
@ -17,44 +17,39 @@ describe("<ToolbaySlot />", () => {
|
|||
xySwap: false,
|
||||
});
|
||||
|
||||
const checkSlotDirection =
|
||||
(direction: number,
|
||||
quadrant: BotOriginQuadrant,
|
||||
xySwap: boolean,
|
||||
expected: string) => {
|
||||
it(`renders slot, pullout: ${direction} quad: ${quadrant} yx: ${xySwap}`,
|
||||
() => {
|
||||
const p = fakeProps();
|
||||
p.pulloutDirection = direction;
|
||||
p.quadrant = quadrant;
|
||||
p.xySwap = xySwap;
|
||||
const wrapper = svgMount(<ToolbaySlot {...p} />);
|
||||
expect(wrapper.find("use").props().transform).toEqual(expected);
|
||||
});
|
||||
};
|
||||
checkSlotDirection(0, 2, false, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 1, false, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(1, 2, false, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 3, false, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 4, false, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(2, 3, false, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(3, 1, false, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 2, false, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 3, false, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(3, 4, false, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(4, 3, false, "rotate(90, 10, 20)");
|
||||
it.each<[number, BotOriginQuadrant, boolean, string]>([
|
||||
[0, 2, false, "rotate(0, 10, 20)"],
|
||||
[1, 1, false, "rotate(180, 10, 20)"],
|
||||
[1, 2, false, "rotate(0, 10, 20)"],
|
||||
[1, 3, false, "rotate(0, 10, 20)"],
|
||||
[1, 4, false, "rotate(180, 10, 20)"],
|
||||
[2, 3, false, "rotate(180, 10, 20)"],
|
||||
[3, 1, false, "rotate(90, 10, 20)"],
|
||||
[3, 2, false, "rotate(90, 10, 20)"],
|
||||
[3, 3, false, "rotate(270, 10, 20)"],
|
||||
[3, 4, false, "rotate(270, 10, 20)"],
|
||||
[4, 3, false, "rotate(90, 10, 20)"],
|
||||
|
||||
checkSlotDirection(0, 2, true, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(1, 1, true, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(1, 2, true, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(1, 3, true, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(1, 4, true, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(2, 3, true, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 1, true, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(3, 2, true, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(3, 3, true, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(3, 4, true, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(4, 3, true, "rotate(180, 10, 20)");
|
||||
[0, 2, true, "rotate(180, 10, 20)"],
|
||||
[1, 1, true, "rotate(90, 10, 20)"],
|
||||
[1, 2, true, "rotate(90, 10, 20)"],
|
||||
[1, 3, true, "rotate(270, 10, 20)"],
|
||||
[1, 4, true, "rotate(270, 10, 20)"],
|
||||
[2, 3, true, "rotate(90, 10, 20)"],
|
||||
[3, 1, true, "rotate(180, 10, 20)"],
|
||||
[3, 2, true, "rotate(0, 10, 20)"],
|
||||
[3, 3, true, "rotate(0, 10, 20)"],
|
||||
[3, 4, true, "rotate(180, 10, 20)"],
|
||||
[4, 3, true, "rotate(180, 10, 20)"],
|
||||
])("renders slot, pullout: %s quad: %s yx: %s",
|
||||
(direction, quadrant, xySwap, expected) => {
|
||||
const p = fakeProps();
|
||||
p.pulloutDirection = direction;
|
||||
p.quadrant = quadrant;
|
||||
p.xySwap = xySwap;
|
||||
const wrapper = svgMount(<ToolbaySlot {...p} />);
|
||||
expect(wrapper.find("use").props().transform).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Tool/>", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
let mockDev = false;
|
||||
jest.mock("../../../../../account/dev/dev_support", () => ({
|
||||
DevSettings: { futureFeaturesEnabled: () => mockDev, }
|
||||
DevSettings: { futureFeaturesEnabled: () => mockDev }
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../history", () => ({ history: { push: jest.fn() } }));
|
||||
|
@ -29,21 +29,20 @@ describe("<ToolSlotPoint/>", () => {
|
|||
hoveredToolSlot: undefined,
|
||||
});
|
||||
|
||||
const testToolSlotGraphics = (tool: 0 | 1, slot: 0 | 1) => {
|
||||
it(`renders ${tool ? "" : "no"} tool and ${slot ? "" : "no"} slot`, () => {
|
||||
if (!tool && !slot) { tool = 1; }
|
||||
const p = fakeProps();
|
||||
if (!tool) { p.slot.tool = undefined; }
|
||||
p.slot.toolSlot.body.pullout_direction = slot;
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
expect(wrapper.find("circle").length).toEqual(tool);
|
||||
expect(wrapper.find("use").length).toEqual(slot);
|
||||
});
|
||||
};
|
||||
testToolSlotGraphics(0, 0);
|
||||
testToolSlotGraphics(0, 1);
|
||||
testToolSlotGraphics(1, 0);
|
||||
testToolSlotGraphics(1, 1);
|
||||
it.each<[0 | 1, 0 | 1]>([
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
])("renders %s tool and %s slot", (tool, slot) => {
|
||||
if (!tool && !slot) { tool = 1; }
|
||||
const p = fakeProps();
|
||||
if (!tool) { p.slot.tool = undefined; }
|
||||
p.slot.toolSlot.body.pullout_direction = slot;
|
||||
const wrapper = svgMount(<ToolSlotPoint {...p} />);
|
||||
expect(wrapper.find("circle").length).toEqual(tool);
|
||||
expect(wrapper.find("use").length).toEqual(slot);
|
||||
});
|
||||
|
||||
it("opens tool info", () => {
|
||||
const p = fakeProps();
|
||||
|
|
|
@ -4,7 +4,8 @@ jest.mock("../../../history", () => ({
|
|||
getPathArray: jest.fn(() => mockPath.split("/"))
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({ destroy: jest.fn() }));
|
||||
let mockDestroy = jest.fn(() => Promise.resolve());
|
||||
jest.mock("../../../api/crud", () => ({ destroy: mockDestroy }));
|
||||
|
||||
let mockDev = false;
|
||||
jest.mock("../../../account/dev/dev_support", () => ({
|
||||
|
@ -41,7 +42,7 @@ describe("<SelectPlants />", () => {
|
|||
return {
|
||||
selected: ["plant.1"],
|
||||
plants: [plant1, plant2],
|
||||
dispatch: jest.fn(),
|
||||
dispatch: jest.fn(x => x),
|
||||
gardenOpen: undefined,
|
||||
};
|
||||
}
|
||||
|
@ -118,7 +119,7 @@ describe("<SelectPlants />", () => {
|
|||
|
||||
it("deletes selected plants", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
mockDestroy = jest.fn(() => Promise.resolve());
|
||||
p.selected = ["plant.1", "plant.2"];
|
||||
const wrapper = mount(<SelectPlants {...p} />);
|
||||
expect(wrapper.text()).toContain("Delete");
|
||||
|
@ -130,7 +131,7 @@ describe("<SelectPlants />", () => {
|
|||
|
||||
it("does not delete if selection is empty", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
mockDestroy = jest.fn(() => Promise.resolve());
|
||||
p.selected = undefined;
|
||||
const wrapper = mount(<SelectPlants {...p} />);
|
||||
expect(wrapper.text()).toContain("Delete");
|
||||
|
@ -140,7 +141,7 @@ describe("<SelectPlants />", () => {
|
|||
|
||||
it("errors when deleting selected plants", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.reject());
|
||||
mockDestroy = jest.fn(() => Promise.reject());
|
||||
p.selected = ["plant.1", "plant.2"];
|
||||
const wrapper = mount(<SelectPlants {...p} />);
|
||||
expect(wrapper.text()).toContain("Delete");
|
||||
|
|
|
@ -7,7 +7,7 @@ import { TaggedPlant } from "../map/interfaces";
|
|||
import { DesignerPanel, DesignerPanelHeader } from "../designer_panel";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { EditPlantInfoProps, PlantOptions } from "../interfaces";
|
||||
import { isString, isUndefined } from "lodash";
|
||||
import { isString } from "lodash";
|
||||
import { history, getPathArray } from "../../history";
|
||||
import { destroy, edit, save } from "../../api/crud";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
|
@ -20,7 +20,7 @@ export class RawPlantInfo extends React.Component<EditPlantInfoProps, {}> {
|
|||
get confirmDelete() {
|
||||
const confirmSetting = this.props.getConfigValue(
|
||||
BooleanSetting.confirm_plant_deletion);
|
||||
return isUndefined(confirmSetting) ? true : confirmSetting;
|
||||
return confirmSetting ?? true;
|
||||
}
|
||||
|
||||
destroy = (plantUUID: string) => {
|
||||
|
|
|
@ -51,7 +51,7 @@ describe("", () => {
|
|||
|
||||
const sort = (sortType: PointGroupSortType): string[] => {
|
||||
const array = SORT_OPTIONS[sortType](plants as TaggedPlant[]);
|
||||
return array.map(x => x && x.body && (x.body.name || "NA"));
|
||||
return array.map(x => x?.body?.name || "NA");
|
||||
};
|
||||
|
||||
it("sorts randomly", () => {
|
||||
|
|
|
@ -66,7 +66,7 @@ export const mapStateToProps = (props: Everything): EditGardenProps => {
|
|||
const savedGarden = findSavedGardenByUrl(props.resources.index);
|
||||
return {
|
||||
savedGarden,
|
||||
gardenIsOpen: !!(savedGarden && savedGarden.uuid === openedSavedGarden),
|
||||
gardenIsOpen: !!(savedGarden?.uuid === openedSavedGarden),
|
||||
dispatch: props.dispatch,
|
||||
plantPointerCount: selectAllPlantPointers(props.resources.index).length,
|
||||
};
|
||||
|
|
|
@ -71,7 +71,7 @@ const Setting = (props: SettingProps) => {
|
|||
toggleValue={props.invert ? !value : value}
|
||||
toggleAction={() => {
|
||||
props.dispatch(setWebAppConfigValue(setting, !value));
|
||||
callback && callback();
|
||||
callback?.();
|
||||
}}
|
||||
title={`${t("toggle")} ${title}`}
|
||||
customText={{ textFalse: t("off"), textTrue: t("on") }} />}
|
||||
|
|
|
@ -100,7 +100,7 @@ export class RawAddToolSlot
|
|||
: "initializing"}
|
||||
<SaveBtn onClick={this.save} status={SpecialStatus.DIRTY} />
|
||||
</DesignerPanelContent>
|
||||
</DesignerPanel >;
|
||||
</DesignerPanel>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,24 +67,21 @@ describe("<FarmwarePage />", () => {
|
|||
expect(wrapper.text()).toContain("Take Photo");
|
||||
});
|
||||
|
||||
const TEST_DATA = {
|
||||
"Photos": ["Take Photo"],
|
||||
"take-photo": ["Take Photo"],
|
||||
"Weed Detector": ["detect weeds", "CLEAR WEEDS", "Color Range"],
|
||||
"plant-detection": ["detect weeds", "CLEAR WEEDS", "Color Range"],
|
||||
"Camera Calibration": ["Calibrate", "Color Range", "Invert Hue Range Selection"],
|
||||
"camera-calibration": ["Calibrate", "Color Range", "Invert Hue Range Selection"],
|
||||
};
|
||||
it.each<[string, string[]]>([
|
||||
["Photos", ["Take Photo"]],
|
||||
["take-photo", ["Take Photo"]],
|
||||
["Weed Detector", ["detect weeds", "CLEAR WEEDS", "Color Range"]],
|
||||
["plant-detection", ["detect weeds", "CLEAR WEEDS", "Color Range"]],
|
||||
["Camera Calibration", ["Calibrate", "Color Range", "Invert Hue Range Selection"]],
|
||||
["camera-calibration", ["Calibrate", "Color Range", "Invert Hue Range Selection"]],
|
||||
])("renders %s Farmware page", (farmware, expectedText) => {
|
||||
const p = fakeProps();
|
||||
p.currentFarmware = farmware;
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
expectedText.map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
Object.entries(TEST_DATA).map(([farmware, expectedText]) =>
|
||||
it(`renders ${farmware} Farmware page`, () => {
|
||||
const p = fakeProps();
|
||||
p.currentFarmware = farmware;
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
expectedText.map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
})
|
||||
);
|
||||
it("renders installed Farmware page", () => {
|
||||
const p = fakeProps();
|
||||
const farmware = fakeFarmware();
|
||||
|
|
|
@ -24,7 +24,7 @@ export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
|
|||
className="fb-button gray fa fa-download"
|
||||
onClick={() => {
|
||||
const p = getDevice().installFirstPartyFarmware();
|
||||
p && p.catch(commandErr("Farmware installation"));
|
||||
p?.catch(commandErr("Farmware installation"));
|
||||
}}
|
||||
disabled={props.firstPartyFwsInstalled} />
|
||||
</fieldset>
|
||||
|
|
|
@ -108,6 +108,6 @@ export function FarmwareForm(props: FarmwareFormProps): JSX.Element {
|
|||
|
||||
/** Determine if a Farmware has requested inputs. */
|
||||
export function needsFarmwareForm(farmware: FarmwareManifestInfo): Boolean {
|
||||
const needsWidget = farmware.config && farmware.config.length > 0;
|
||||
const needsWidget = farmware.config?.length > 0;
|
||||
return needsWidget;
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ const PendingInstallNameError =
|
|||
}): JSX.Element => {
|
||||
const installation: TaggedFarmwareInstallation | undefined =
|
||||
installations.filter(x => x.body.url === url)[0];
|
||||
const packageError = installation && installation.body.package_error;
|
||||
const packageError = installation?.body.package_error;
|
||||
return (url && installation && packageError)
|
||||
? <div className="error-with-button">
|
||||
<label>{t("Could not fetch package name")}</label>
|
||||
|
|
|
@ -116,7 +116,7 @@ export class Photos extends React.Component<PhotosProps, {}> {
|
|||
|
||||
deletePhoto = () => {
|
||||
const img = this.props.currentImage || this.props.images[0];
|
||||
if (img && img.uuid) {
|
||||
if (img?.uuid) {
|
||||
this.props.dispatch(destroy(img.uuid))
|
||||
.then(() => success(t("Image Deleted.")))
|
||||
.catch(() => error(t("Could not delete image.")));
|
||||
|
|
|
@ -61,7 +61,7 @@ export class ImageWorkspace extends React.Component<ImageWorkspaceProps, {}> {
|
|||
|
||||
maybeProcessPhoto = () => {
|
||||
const img = this.props.currentImage || this.props.images[0];
|
||||
if (img && img.body.id) {
|
||||
if (img?.body.id) {
|
||||
this.props.onProcessPhoto(img.body.id);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ jest.mock("../../draggable/actions", () => ({
|
|||
stepGet: jest.fn(() => () => mockStepGetResult),
|
||||
}));
|
||||
|
||||
import { FolderNode } from "../constants";
|
||||
import { FolderNode } from "../interfaces";
|
||||
import { ingest } from "../data_transfer";
|
||||
import {
|
||||
collapseAll,
|
||||
|
@ -240,11 +240,11 @@ describe("deleteFolder", () => {
|
|||
|
||||
describe("updateSearchTerm", () => {
|
||||
it("updates a search term", () => {
|
||||
const argss =
|
||||
const args =
|
||||
(payload: string | undefined) => ({ type: "FOLDER_SEARCH", payload });
|
||||
[undefined, "foo"].map(term => {
|
||||
updateSearchTerm(term);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(argss(term));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(args(term));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,21 @@ jest.mock("../../history", () => ({
|
|||
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
|
||||
}));
|
||||
|
||||
jest.mock("@blueprintjs/core", () => ({
|
||||
Popover: jest.fn(p => <div>{p.children}</div>),
|
||||
Position: jest.fn(),
|
||||
PopoverInteractionKind: jest.fn(),
|
||||
Button: jest.fn(p => <button>{p.text}</button>),
|
||||
Classes: jest.fn(),
|
||||
MenuItem: jest.fn(),
|
||||
Alignment: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@blueprintjs/select", () => ({
|
||||
Select: { ofType: jest.fn() },
|
||||
ItemRenderer: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
|
@ -30,7 +45,7 @@ import {
|
|||
FolderNameInputProps,
|
||||
FolderNodeMedial,
|
||||
FolderNodeTerminal,
|
||||
} from "../constants";
|
||||
} from "../interfaces";
|
||||
import {
|
||||
updateSearchTerm, toggleAll, moveSequence, dropSequence,
|
||||
sequenceEditMaybeSave,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FolderNode } from "../constants";
|
||||
import { FolderNode } from "../interfaces";
|
||||
import { ingest } from "../data_transfer";
|
||||
import { climb } from "../climb";
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TEST_GRAPH } from "./actions_test";
|
||||
import { searchFolderTree, FolderSearchProps } from "../search_folder_tree";
|
||||
import { TaggedResource } from "farmbot";
|
||||
import { FolderUnion } from "../constants";
|
||||
import { FolderUnion } from "../interfaces";
|
||||
|
||||
describe("searchFolderTree", () => {
|
||||
const searchFor = (input: string) => searchFolderTree({
|
||||
|
@ -19,7 +19,7 @@ describe("searchFolderTree", () => {
|
|||
expect(before).toEqual(after); // Prevent mutation of original data.
|
||||
});
|
||||
|
||||
it("finds an `inital` folder", () => {
|
||||
it("finds an `initial` folder", () => {
|
||||
const results = searchFor("one").map(x => x.name);
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results).toContain("One");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RootFolderNode as Tree } from "./constants";
|
||||
import { RootFolderNode as Tree } from "./interfaces";
|
||||
import { cloneAndClimb } from "./climb";
|
||||
import { Color, SpecialStatus, TaggedSequence } from "farmbot";
|
||||
import { store } from "../redux/store";
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { RootFolderNode, FolderUnion, FolderNodeMedial, FolderNodeInitial } from "./constants";
|
||||
import {
|
||||
RootFolderNode, FolderUnion, FolderNodeMedial, FolderNodeInitial
|
||||
} from "./interfaces";
|
||||
import { defensiveClone } from "../util";
|
||||
|
||||
interface TreeClimberState {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
FolderButtonClusterProps,
|
||||
FolderNameInputProps,
|
||||
SequenceDropAreaState,
|
||||
} from "./constants";
|
||||
} from "./interfaces";
|
||||
import {
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
|
@ -80,7 +80,7 @@ export const FolderListItem = (props: FolderItemProps) => {
|
|||
onMouseUp={() => props.toggleSequenceMove(sequence.uuid)} />
|
||||
</div>
|
||||
</li>
|
||||
</StepDragger >;
|
||||
</StepDragger>;
|
||||
};
|
||||
|
||||
const ToggleFolderBtn = (props: ToggleFolderBtnProps) => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
FolderNodeTerminal,
|
||||
RootFolderNode,
|
||||
FolderMeta,
|
||||
} from "./constants";
|
||||
} from "./interfaces";
|
||||
import { sortBy } from "lodash";
|
||||
|
||||
type FoldersIndexedByParentId = Record<number, FolderNode[]>;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FolderProps } from "./constants";
|
||||
import { FolderProps } from "./interfaces";
|
||||
import { selectAllSequences } from "../resources/selectors";
|
||||
import { TaggedSequence } from "farmbot";
|
||||
import { resourceUsageList } from "../resources/in_use";
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { TaggedResource, TaggedSequence } from "farmbot";
|
||||
|
||||
import { RootFolderNode, FolderUnion } from "./constants";
|
||||
import { RootFolderNode, FolderUnion } from "./interfaces";
|
||||
|
||||
export interface FolderSearchProps {
|
||||
references: Record<string, TaggedResource | undefined>;
|
||||
|
|
|
@ -18,7 +18,7 @@ export const clickHandler =
|
|||
/** BEGIN LEGACY SHIMS */
|
||||
const { onClick, to } = props;
|
||||
navigate(maybeStripLegacyUrl(to));
|
||||
onClick && onClick(e);
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
export class Link extends React.Component<LinkProps, {}> {
|
||||
|
|
|
@ -180,7 +180,7 @@ const FirmwareChoiceTable = () =>
|
|||
export const changeFirmwareHardware = (dispatch: Function | undefined) =>
|
||||
(ddi: DropDownItem) => {
|
||||
if (isFwHardwareValue(ddi.value)) {
|
||||
dispatch && dispatch(updateConfig({ firmware_hardware: ddi.value }));
|
||||
dispatch?.(updateConfig({ firmware_hardware: ddi.value }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const computeEditorUrlFromState =
|
|||
: resources.consumers.regimens.currentRegimen;
|
||||
const r = resources.index.references[current || ""];
|
||||
const base = `/app/${resource === "Sequence" ? "sequences" : "regimens"}/`;
|
||||
if (r && r.kind == resource) {
|
||||
if (r?.kind == resource) {
|
||||
return base + urlFriendly(r.body.name);
|
||||
} else {
|
||||
return base;
|
||||
|
|
|
@ -59,7 +59,7 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
BooleanSetting.disable_emergency_unlock_confirmation)} />
|
||||
|
||||
AccountMenu = () => {
|
||||
const hasName = this.props.user && this.props.user.body.name;
|
||||
const hasName = this.props.user?.body.name;
|
||||
const firstName = hasName ?
|
||||
`${hasName.split(" ")[0].slice(0, 9)} ▾` : `${t("Menu")} ▾`;
|
||||
return <div className="menu-popover">
|
||||
|
|
|
@ -43,10 +43,7 @@ const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
|
|||
|
||||
const cacheTheIcon = (slug: string) =>
|
||||
(resp: AxiosResponse<OFCropResponse>): OFIcon => {
|
||||
if (resp
|
||||
&& resp.data
|
||||
&& resp.data.data
|
||||
&& resp.data.data.attributes) {
|
||||
if (resp?.data?.data?.attributes) {
|
||||
const icon = {
|
||||
slug: resp.data.data.attributes.slug,
|
||||
spread: resp.data.data.attributes.spread,
|
||||
|
|
|
@ -35,7 +35,7 @@ export function getMiddleware(env: EnvName) {
|
|||
.map((mwc) => mwc.fn);
|
||||
// tslint:disable-next-line:no-any
|
||||
const wow = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
|
||||
const dtCompose = wow && wow({
|
||||
const dtCompose = wow?.({
|
||||
actionsBlacklist: [
|
||||
Actions.NETWORK_EDGE_CHANGE,
|
||||
Actions.PING_NO,
|
||||
|
|
|
@ -12,10 +12,8 @@ const WEB_APP_CONFIG: ResourceName = "WebAppConfig";
|
|||
* resources, downloading the filtered log list as required from the API. */
|
||||
// tslint:disable-next-line:no-any
|
||||
export const fn: Middleware = () => (dispatch) => (action: any) => {
|
||||
const needsRefresh = action
|
||||
&& action.payload
|
||||
&& action.type === Actions.SAVE_RESOURCE_OK
|
||||
&& action.payload.kind === WEB_APP_CONFIG;
|
||||
const needsRefresh = action?.payload?.kind === WEB_APP_CONFIG
|
||||
&& action.type === Actions.SAVE_RESOURCE_OK;
|
||||
|
||||
needsRefresh && throttledLogRefresh(dispatch);
|
||||
|
||||
|
|
|
@ -23,16 +23,11 @@ const WEB_APP_CONFIG: ResourceName = "WebAppConfig";
|
|||
// tslint:disable-next-line:no-any
|
||||
const fn: Middleware = () => (dispatch) => (action: any) => {
|
||||
const x: DeepPartial<SyncResponse<TaggedWebAppConfig>> = action;
|
||||
if (x
|
||||
&& x.type === Actions.RESOURCE_READY
|
||||
&& x.payload
|
||||
&& x.payload.body
|
||||
if (x?.type === Actions.RESOURCE_READY
|
||||
&& x.payload?.body
|
||||
&& x.payload.kind === WEB_APP_CONFIG) {
|
||||
const conf = arrayUnwrap(x.payload.body);
|
||||
conf
|
||||
&& conf.body
|
||||
&& conf.body.disable_i18n
|
||||
&& revertToEnglish();
|
||||
conf?.body?.disable_i18n && revertToEnglish();
|
||||
}
|
||||
|
||||
return dispatch(action);
|
||||
|
|
|
@ -12,10 +12,9 @@ export function dontStopThem() { }
|
|||
const shouldStop =
|
||||
(allResources: TaggedResource[], config: TaggedWebAppConfig | undefined) => {
|
||||
const loggedIn = !!localStorage.getItem("session");
|
||||
const discardUnsaved = config && config.body.discard_unsaved;
|
||||
const discardUnsaved = config?.body.discard_unsaved;
|
||||
const sequenceResources = allResources.filter(r => r.kind === "Sequence");
|
||||
const discardUnsavedSequences =
|
||||
config && config.body.discard_unsaved_sequences;
|
||||
const discardUnsavedSequences = config?.body.discard_unsaved_sequences;
|
||||
|
||||
/**
|
||||
* For the unsaved notification to show, a user must:
|
||||
|
@ -59,6 +58,6 @@ export function registerSubscribers(store: Store) {
|
|||
subscriptions.forEach(function (s) {
|
||||
ENV_LIST.includes &&
|
||||
ENV_LIST.includes(s.env) &&
|
||||
store.subscribe(() => s.fn && s.fn(store.getState()));
|
||||
store.subscribe(() => s.fn?.(store.getState()));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const fn: MW =
|
|||
// tslint:disable-next-line:no-any
|
||||
(action: any) => {
|
||||
const fbos = getVersionFromState(store.getState());
|
||||
window.Rollbar && window.Rollbar.configure({ payload: { fbos } });
|
||||
window.Rollbar?.configure({ payload: { fbos } });
|
||||
return dispatch(action);
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const BAD_UUID = "WARNING: Not a sequence UUID.";
|
|||
export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
|
||||
selected = (): DropDownItem => {
|
||||
const s = this.props.selectedSequence;
|
||||
return (s && s.body.id)
|
||||
return (s?.body.id)
|
||||
? { label: s.body.name, value: s.uuid }
|
||||
: NULL_CHOICE;
|
||||
};
|
||||
|
@ -66,7 +66,7 @@ export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
|
|||
|
||||
render() {
|
||||
const { dispatch, weeks, sequences } = this.props;
|
||||
const active = !!(sequences && sequences.length);
|
||||
const active = !!(sequences?.length);
|
||||
return <div className="bulk-scheduler-content">
|
||||
<AddButton
|
||||
active={active}
|
||||
|
|
|
@ -66,7 +66,7 @@ describe("<RegimenEditor />", () => {
|
|||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const wrapper = mount(<RegimenEditor {...p} />);
|
||||
clickButton(wrapper, 2, "delete");
|
||||
const expectedUuid = p.current && p.current.uuid;
|
||||
const expectedUuid = p.current?.uuid;
|
||||
expect(destroy).toHaveBeenCalledWith(expectedUuid);
|
||||
});
|
||||
|
||||
|
@ -74,7 +74,7 @@ describe("<RegimenEditor />", () => {
|
|||
const p = fakeProps();
|
||||
const wrapper = mount(<RegimenEditor {...p} />);
|
||||
clickButton(wrapper, 0, "save", { partial_match: true });
|
||||
const expectedUuid = p.current && p.current.uuid;
|
||||
const expectedUuid = p.current?.uuid;
|
||||
expect(save).toHaveBeenCalledWith(expectedUuid);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -177,7 +177,7 @@ describe("maybeGetSequence", () => {
|
|||
const i = buildResourceIndex([s]);
|
||||
const result = Selector.maybeGetSequence(i.index, s.uuid);
|
||||
expect(result).toBeTruthy();
|
||||
result && expect(result.uuid).toBe(s.uuid);
|
||||
expect(result?.uuid).toBe(s.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { HelpState } from "../help/reducer";
|
|||
import { UsageIndex } from "./in_use";
|
||||
import { SequenceMeta } from "./sequence_meta";
|
||||
import { AlertReducerState } from "../messages/interfaces";
|
||||
import { RootFolderNode, FolderMeta } from "../folders/constants";
|
||||
import { RootFolderNode, FolderMeta } from "../folders/interfaces";
|
||||
|
||||
export type UUID = string;
|
||||
export type VariableNameSet = Record<string, SequenceMeta | undefined>;
|
||||
|
|
|
@ -33,7 +33,9 @@ import { get } from "lodash";
|
|||
import { Actions } from "../constants";
|
||||
import { getFbosConfig } from "./getters";
|
||||
import { ingest, PARENTLESS as NO_PARENT } from "../folders/data_transfer";
|
||||
import { FolderNode, FolderMeta, FolderNodeTerminal, FolderNodeMedial } from "../folders/constants";
|
||||
import {
|
||||
FolderNode, FolderMeta, FolderNodeTerminal, FolderNodeMedial
|
||||
} from "../folders/interfaces";
|
||||
import { climb } from "../folders/climb";
|
||||
|
||||
export function findByUuid(index: ResourceIndex, uuid: string): TaggedResource {
|
||||
|
@ -63,10 +65,12 @@ export const reindexFolders = (i: ResourceIndex) => {
|
|||
const allSequences = selectAllSequences(i);
|
||||
|
||||
const oldMeta = i.sequenceFolders.localMetaAttributes;
|
||||
/** Open folder edit mode when adding a new folder (& not during init all). */
|
||||
const editing = !!oldMeta[-1];
|
||||
const localMetaAttributes: Record<number, FolderMeta> = {};
|
||||
folders.map(x => {
|
||||
localMetaAttributes[x.id] = {
|
||||
...(oldMeta[x.id] || {}),
|
||||
...(oldMeta[x.id] || { editing }),
|
||||
sequences: [], // Clobber and re-init
|
||||
};
|
||||
});
|
||||
|
@ -188,7 +192,7 @@ const reindexAllSequences = (i: ResourceIndex) => {
|
|||
const mapper = reindexSequences(i);
|
||||
betterCompact(Object.keys(i.byKind["Sequence"]).map(uuid => {
|
||||
const resource = i.references[uuid];
|
||||
return (resource && resource.kind == "Sequence") ? resource : undefined;
|
||||
return (resource?.kind == "Sequence") ? resource : undefined;
|
||||
})).map(mapper);
|
||||
};
|
||||
|
||||
|
@ -283,7 +287,7 @@ const AFTER_HOOKS: IndexerHook = {
|
|||
FbosConfig: (i) => {
|
||||
const conf = getFbosConfig(i);
|
||||
|
||||
if (conf && conf.body.boot_sequence_id) {
|
||||
if (conf?.body.boot_sequence_id) {
|
||||
const { boot_sequence_id } = conf.body;
|
||||
const tracker = i.inUse["Sequence.FbosConfig"];
|
||||
const uuid = i.byKindAndId[joinKindAndId("Sequence", boot_sequence_id)];
|
||||
|
@ -345,7 +349,7 @@ export const indexUpsert: IndexUpsert = (db, resources, strategy) => {
|
|||
const { kind } = arrayUnwrap(resources);
|
||||
// Clean up indexes (if needed)
|
||||
const before = BEFORE_HOOKS[kind];
|
||||
before && before(db, strategy);
|
||||
before?.(db, strategy);
|
||||
|
||||
// Run indexers
|
||||
ups.map(callback => {
|
||||
|
@ -354,14 +358,14 @@ export const indexUpsert: IndexUpsert = (db, resources, strategy) => {
|
|||
|
||||
// Finalize indexing (if needed)
|
||||
const after = AFTER_HOOKS[kind];
|
||||
after && after(db, strategy);
|
||||
after?.(db, strategy);
|
||||
};
|
||||
|
||||
export function indexRemove(db: ResourceIndex, resource: TaggedResource) {
|
||||
downs.map(callback => arrayWrap(resource).map(r => callback(r, db)));
|
||||
// Finalize indexing (if needed)
|
||||
const after = AFTER_HOOKS[resource.kind];
|
||||
after && after(db, "ongoing");
|
||||
after?.(db, "ongoing");
|
||||
}
|
||||
|
||||
export const beforeEach = (state: RestResources,
|
||||
|
|
|
@ -77,7 +77,7 @@ export function findPointerByTypeAndId(index: ResourceIndex,
|
|||
const uuid = "" + index.byKindAndId[pni];
|
||||
const resource = index.references[uuid];
|
||||
|
||||
if (resource && resource.kind === "Point") {
|
||||
if (resource?.kind === "Point") {
|
||||
return resource;
|
||||
} else {
|
||||
// We might have a sequence dependency leak if this exception is ever
|
||||
|
@ -204,7 +204,7 @@ export function maybeGetTimeSettings(index: ResourceIndex): TimeSettings {
|
|||
|
||||
export function maybeGetDevice(index: ResourceIndex): TaggedDevice | undefined {
|
||||
const dev = index.references[Object.keys(index.byKind.Device)[0] || "nope"];
|
||||
return (dev && dev.kind === "Device") ?
|
||||
return (dev?.kind === "Device") ?
|
||||
dev : undefined;
|
||||
}
|
||||
|
||||
|
@ -227,7 +227,7 @@ export function maybeFetchUser(index: ResourceIndex):
|
|||
if (user && sanityCheck(user) && list.length > 1) {
|
||||
throw new Error("PROBLEM: Expected 1 user. Got: " + list.length);
|
||||
}
|
||||
if ((list.length === 1) && user && user.kind === "User") {
|
||||
if ((list.length === 1) && user?.kind === "User") {
|
||||
return user;
|
||||
} else {
|
||||
return undefined;
|
||||
|
|
|
@ -50,7 +50,7 @@ export const determineVector =
|
|||
return ts ? ts.body : undefined;
|
||||
case "identifier":
|
||||
const variable = maybeFindVariable(node.args.label, resources, uuid);
|
||||
return variable && variable.vector;
|
||||
return variable?.vector;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
@ -127,7 +127,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
|
|||
p.dispatch = dispatch;
|
||||
const wrapper = mount(<SequenceEditorMiddleActive {...p} />);
|
||||
const props = wrapper.find("DropArea").props() as DropAreaProps;
|
||||
props.callback && props.callback("key");
|
||||
props.callback?.("key");
|
||||
dispatch.mock.calls[0][0](() =>
|
||||
({ value: 1, intent: "step_splice", draggerId: 2 }));
|
||||
expect(splice).toHaveBeenCalledWith(expect.objectContaining({
|
||||
|
|
|
@ -148,7 +148,7 @@ const SequenceBtnGroup = ({
|
|||
onClick={() => {
|
||||
const confirm = getWebAppConfigValue(
|
||||
BooleanSetting.confirm_sequence_deletion);
|
||||
const force = isUndefined(confirm) ? false : !confirm;
|
||||
const force = !(confirm ?? true);
|
||||
dispatch(destroy(sequence.uuid, force))
|
||||
.then(() => push("/app/sequences/"));
|
||||
}}>
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
export function mapStateToProps(props: Everything): Props {
|
||||
const uuid = props.resources.consumers.sequences.current;
|
||||
const sequence = uuid ? findSequence(props.resources.index, uuid) : undefined;
|
||||
sequence && (sequence.body.body || []).map(x => getStepTag(x));
|
||||
(sequence?.body.body || []).map(x => getStepTag(x));
|
||||
|
||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
const { mcu_params } = props.bot.hardware;
|
||||
|
|
|
@ -52,7 +52,7 @@ export function StepButtonCluster(props: StepButtonProps) {
|
|||
step={{
|
||||
kind: "toggle_pin",
|
||||
args: {
|
||||
pin_number: 4
|
||||
pin_number: NOTHING_SELECTED
|
||||
}
|
||||
}}
|
||||
color="orange">
|
||||
|
|
|
@ -97,61 +97,6 @@ describe("renderCeleryNode()", () => {
|
|||
}
|
||||
|
||||
const TEST_DATA: TestData[] = [
|
||||
{
|
||||
expected: "MarkPlantasx = 300",
|
||||
node: {
|
||||
kind: "resource_update",
|
||||
args: {
|
||||
resource_id: 23,
|
||||
resource_type: "Plant",
|
||||
label: "x",
|
||||
value: 300
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
expected: "System",
|
||||
node: {
|
||||
kind: "check_updates",
|
||||
args: {
|
||||
package: "farmbot_os"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
expected: "System",
|
||||
node: {
|
||||
kind: "factory_reset",
|
||||
args: {
|
||||
package: "farmbot_os"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
expected: "Unlocking a device requires user intervention",
|
||||
node: {
|
||||
kind: "emergency_lock",
|
||||
args: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
expected: "Unable to properly display this step",
|
||||
node: {
|
||||
kind: "power_off",
|
||||
args: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
expected: "Unable to properly display this step",
|
||||
node: { kind: "read_status", args: {} }
|
||||
},
|
||||
{
|
||||
expected: "",
|
||||
node: {
|
||||
kind: "install_first_party_farmware",
|
||||
args: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "_if",
|
||||
|
@ -166,31 +111,13 @@ describe("renderCeleryNode()", () => {
|
|||
expected: "Then Execute"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "execute_script",
|
||||
args: { label: "farmware-to-execute" }
|
||||
},
|
||||
node: { kind: "execute_script", args: { label: "farmware-to-execute" } },
|
||||
expected: "Manual Input"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "execute",
|
||||
args: {
|
||||
sequence_id: 0
|
||||
}
|
||||
},
|
||||
node: { kind: "execute", args: { sequence_id: 0 } },
|
||||
expected: "Select a sequence"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "find_home",
|
||||
args: {
|
||||
speed: 100,
|
||||
axis: "all"
|
||||
}
|
||||
},
|
||||
expected: "Find x"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "move_absolute",
|
||||
|
@ -213,40 +140,37 @@ describe("renderCeleryNode()", () => {
|
|||
expected: "Message"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "take_photo",
|
||||
args: {}
|
||||
},
|
||||
node: { kind: "take_photo", args: {} },
|
||||
expected: "Photo"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "wait",
|
||||
args: {
|
||||
milliseconds: 100
|
||||
}
|
||||
},
|
||||
node: { kind: "wait", args: { milliseconds: 100 } },
|
||||
expected: "milliseconds"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "set_servo_angle",
|
||||
kind: "resource_update",
|
||||
args: {
|
||||
pin_number: 4,
|
||||
pin_value: 90,
|
||||
resource_id: 23,
|
||||
resource_type: "Plant",
|
||||
label: "x",
|
||||
value: 300
|
||||
}
|
||||
},
|
||||
expected: "MarkPlantasx = 300"
|
||||
},
|
||||
{
|
||||
node: { kind: "set_servo_angle", args: { pin_number: 4, pin_value: 90, } },
|
||||
expected: "Servo"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "toggle_pin",
|
||||
args: {
|
||||
pin_number: 13
|
||||
}
|
||||
},
|
||||
node: { kind: "toggle_pin", args: { pin_number: 13 } },
|
||||
expected: "Pin"
|
||||
},
|
||||
{
|
||||
node: { kind: "find_home", args: { speed: 100, axis: "all" } },
|
||||
expected: "Find x"
|
||||
},
|
||||
{
|
||||
node: { kind: "zero", args: { axis: "all" } },
|
||||
expected: "Zero x"
|
||||
|
@ -259,52 +183,36 @@ describe("renderCeleryNode()", () => {
|
|||
node: { kind: "home", args: { axis: "all", speed: 100, } },
|
||||
expected: "Home x"
|
||||
},
|
||||
{
|
||||
node: { kind: "reboot", args: { package: "farmbot_os" } },
|
||||
expected: "System"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "check_updates", args: { package: "farmbot_os" }
|
||||
},
|
||||
expected: "System"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "factory_reset", args: { package: "farmbot_os" }
|
||||
},
|
||||
expected: "System"
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "sync",
|
||||
args: {}
|
||||
},
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "dump_info",
|
||||
args: {}
|
||||
},
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "power_off",
|
||||
args: {}
|
||||
},
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "read_status",
|
||||
args: {}
|
||||
},
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: { kind: "emergency_lock", args: {} },
|
||||
expected: "Unlocking a device requires user intervention"
|
||||
},
|
||||
{
|
||||
node: { kind: "reboot", args: { package: "farmbot_os" } },
|
||||
expected: "entire system"
|
||||
},
|
||||
{
|
||||
node: { kind: "check_updates", args: { package: "farmbot_os" } },
|
||||
expected: "System"
|
||||
},
|
||||
{
|
||||
node: { kind: "factory_reset", args: { package: "farmbot_os" } },
|
||||
expected: "System"
|
||||
},
|
||||
{
|
||||
node: { kind: "sync", args: {} },
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: { kind: "dump_info", args: {} },
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: { kind: "power_off", args: {} },
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: { kind: "read_status", args: {} },
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
|
@ -312,19 +220,12 @@ describe("renderCeleryNode()", () => {
|
|||
expected: ""
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "install_first_party_farmware", args: {}
|
||||
},
|
||||
node: { kind: "install_first_party_farmware", args: {} },
|
||||
expected: ""
|
||||
},
|
||||
{
|
||||
node: {
|
||||
kind: "unknown",
|
||||
args: {
|
||||
unknown: 0
|
||||
}
|
||||
// tslint:disable-next-line:no-any
|
||||
} as any,
|
||||
// tslint:disable-next-line: no-any
|
||||
node: { kind: "unknown", args: { unknown: 0 } } as any,
|
||||
expected: "unknown"
|
||||
},
|
||||
];
|
||||
|
|
|
@ -205,21 +205,20 @@ describe("Pin and Peripheral support files", () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
Object.values(BoxLed).map(boxLed => {
|
||||
it(`converts ${boxLed} named pin to DropDownItem`, () => {
|
||||
const ri = buildResourceIndex([]).index;
|
||||
const namedPin: NamedPin = {
|
||||
kind: "named_pin",
|
||||
args: { pin_type: boxLed, pin_id: -1 }
|
||||
};
|
||||
const result = namedPin2DropDown(ri, namedPin);
|
||||
const expected: DropDownItem = {
|
||||
label: expect.stringContaining("LED"),
|
||||
value: boxLed,
|
||||
headingId: PinGroupName.BoxLed
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
it.each<[BoxLed]>(Object.values(BoxLed).map(x => [x])
|
||||
)("converts %s named pin to DropDownItem", (boxLed) => {
|
||||
const ri = buildResourceIndex([]).index;
|
||||
const namedPin: NamedPin = {
|
||||
kind: "named_pin",
|
||||
args: { pin_type: boxLed, pin_id: -1 }
|
||||
};
|
||||
const result = namedPin2DropDown(ri, namedPin);
|
||||
const expected: DropDownItem = {
|
||||
label: expect.stringContaining("LED"),
|
||||
value: boxLed,
|
||||
headingId: PinGroupName.BoxLed
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("converts nothing to DropDownItems", () => {
|
||||
|
|
|
@ -83,7 +83,7 @@ describe("<TileExecuteScript/>", () => {
|
|||
it("shows special 1st-party Farmware name", () => {
|
||||
const p = fakeProps();
|
||||
(p.currentStep as ExecuteScript).args.label = "plant-detection";
|
||||
p.farmwareData && p.farmwareData.farmwareNames.push("plant-detection");
|
||||
p.farmwareData?.farmwareNames.push("plant-detection");
|
||||
const wrapper = mount(<TileExecuteScript {...p} />);
|
||||
expect(wrapper.find("label").length).toEqual(1);
|
||||
expect(wrapper.text()).toContain("Weed Detector");
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
jest.mock("../../api/crud", () => {
|
||||
return { editStep: jest.fn() };
|
||||
});
|
||||
import { TileReboot, editTheRebootStep, rebootExecutor, MultiChoiceRadio } from "../step_tiles/tile_reboot";
|
||||
import { render, mount } from "enzyme";
|
||||
jest.mock("../../../api/crud", () => ({ editStep: jest.fn() }));
|
||||
|
||||
import { TileReboot, editTheRebootStep, rebootExecutor } from "../tile_reboot";
|
||||
import { render } from "enzyme";
|
||||
import React from "react";
|
||||
import { StepParams } from "../interfaces";
|
||||
import { fakeSequence } from "../../__test_support__/fake_state/resources";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { editStep } from "../../api/crud";
|
||||
import { StepParams } from "../../interfaces";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { editStep } from "../../../api/crud";
|
||||
import { Reboot } from "farmbot";
|
||||
|
||||
const fakeProps = (): StepParams => {
|
||||
|
@ -66,21 +65,3 @@ describe("<TileReboot/>", () => {
|
|||
expect(step.args.package).toBe("arduino_firmware");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MultiChoiceRadio", () => {
|
||||
it("triggers callbacks", () => {
|
||||
const PACKAGE_CHOICES = {
|
||||
"a": "1",
|
||||
"b": "2"
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
const el = mount(<MultiChoiceRadio
|
||||
uuid={"WHATEVER"}
|
||||
choices={PACKAGE_CHOICES}
|
||||
currentChoice={"a"}
|
||||
onChange={onChange} />);
|
||||
const radio = el.find("input[type='radio']").first();
|
||||
radio.simulate("change", "a");
|
||||
expect(onChange).toHaveBeenCalledWith("a");
|
||||
});
|
||||
});
|
|
@ -1,6 +1,13 @@
|
|||
let mockStep = {} as SendMessage;
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
editStep: jest.fn(x => x.executor(mockStep)),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { TileSendMessage, RefactoredSendMessage } from "../tile_send_message";
|
||||
import { mount } from "enzyme";
|
||||
import {
|
||||
TileSendMessage, RefactoredSendMessage, SendMessageParams
|
||||
} from "../tile_send_message";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { SendMessage, Channel } from "farmbot/dist";
|
||||
import { channel } from "../tile_send_message_support";
|
||||
|
@ -8,7 +15,7 @@ import { emptyState } from "../../../resources/reducer";
|
|||
import { MessageType } from "../../interfaces";
|
||||
|
||||
describe("<TileSendMessage/>", () => {
|
||||
function props() {
|
||||
const fakeProps = (): SendMessageParams => {
|
||||
const currentStep: SendMessage = {
|
||||
kind: "send_message",
|
||||
args: {
|
||||
|
@ -31,16 +38,17 @@ describe("<TileSendMessage/>", () => {
|
|||
resources: emptyState().index,
|
||||
confirmStepDeletion: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function bootstrapTest() {
|
||||
return {
|
||||
component: mount(<TileSendMessage {...props()} />)
|
||||
};
|
||||
}
|
||||
it("throws error upon wrong step type", () => {
|
||||
const p = fakeProps();
|
||||
p.currentStep.kind = "nope" as SendMessage["kind"];
|
||||
expect(() => shallow(<TileSendMessage {...p} />))
|
||||
.toThrowError("TileSendMessage expects send_message");
|
||||
});
|
||||
|
||||
it("renders inputs", () => {
|
||||
const block = bootstrapTest().component;
|
||||
const block = mount(<TileSendMessage {...fakeProps()} />);
|
||||
const inputs = block.find("input");
|
||||
const labels = block.find("label");
|
||||
const buttons = block.find("button");
|
||||
|
@ -72,14 +80,37 @@ describe("<TileSendMessage/>", () => {
|
|||
});
|
||||
|
||||
it("adds and removes channels", () => {
|
||||
const i = new RefactoredSendMessage(props());
|
||||
const i = new RefactoredSendMessage(fakeProps());
|
||||
const addEmail = i.add("email");
|
||||
const removeEmail = i.remove("email");
|
||||
const { currentStep } = i.props;
|
||||
currentStep.body = [];
|
||||
addEmail(currentStep);
|
||||
expect(currentStep.body).toContainEqual(channel("email"));
|
||||
removeEmail(currentStep);
|
||||
expect(currentStep.body).not.toContainEqual(channel("email"));
|
||||
});
|
||||
|
||||
it("adds and removes channels via toggle", () => {
|
||||
const i = new RefactoredSendMessage(fakeProps());
|
||||
delete i.props.currentStep.body;
|
||||
mockStep = i.props.currentStep;
|
||||
i.toggle("email")();
|
||||
expect(mockStep.body).toContainEqual(channel("email"));
|
||||
i.toggle("email")();
|
||||
expect(mockStep.body).not.toContainEqual(channel("email"));
|
||||
});
|
||||
|
||||
it("sets message type", () => {
|
||||
const i = new RefactoredSendMessage(fakeProps());
|
||||
mockStep = i.props.currentStep;
|
||||
i.setMsgType({ label: "", value: "fun" });
|
||||
expect(mockStep.args.message_type).toEqual("fun");
|
||||
});
|
||||
|
||||
it("doesn't set incorrect message type", () => {
|
||||
const i = new RefactoredSendMessage(fakeProps());
|
||||
mockStep = i.props.currentStep;
|
||||
expect(() => i.setMsgType({ label: "", value: "nope" }))
|
||||
.toThrowError("message_type must be one of ALLOWED_MESSAGE_TYPES.");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
jest.mock("../../../api/crud", () => ({ editStep: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { TileSetServoAngle, pinNumberChanger, createServoEditFn, ServoPinSelection } from "../tile_set_servo_angle";
|
||||
import {
|
||||
TileSetServoAngle, pinNumberChanger, createServoEditFn, ServoPinSelection
|
||||
} from "../tile_set_servo_angle";
|
||||
import { mount } from "enzyme";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { SetServoAngle } from "farmbot";
|
||||
|
@ -33,8 +35,8 @@ describe("<TileSetServoAngle/>", () => {
|
|||
const inputs = block.find("input");
|
||||
const labels = block.find("label");
|
||||
const stepArgs = props.currentStep.args as SetServoAngle["args"];
|
||||
expect(inputs.length).toEqual(4);
|
||||
expect(labels.length).toEqual(4);
|
||||
expect(inputs.length).toEqual(6);
|
||||
expect(labels.length).toEqual(6);
|
||||
expect(inputs.first().props().placeholder).toEqual("Control Servo");
|
||||
expect(labels.at(0).text()).toContain("Servo angle (0-180)");
|
||||
expect(inputs.at(1).props().value).toEqual(stepArgs.pin_value);
|
||||
|
@ -53,7 +55,7 @@ describe("<TileSetServoAngle/>", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("dissallows named_pins", () => {
|
||||
it("disallows named_pins", () => {
|
||||
const p = fakeProps();
|
||||
const step = p.currentStep;
|
||||
if (step.kind === "set_servo_angle") {
|
||||
|
|
|
@ -157,10 +157,11 @@ export function renderCeleryNode(props: StepParams) {
|
|||
case "home": return <TileMoveHome {...props} />;
|
||||
case "reboot": return <TileReboot {...props} />;
|
||||
case "emergency_lock": return <TileEmergencyStop {...props} />;
|
||||
case "install_first_party_farmware": return <TileSystemAction {...props} />;
|
||||
case "assertion": return <TileAssertion {...props} />;
|
||||
case "check_updates":
|
||||
case "factory_reset":
|
||||
case "sync": case "dump_info": case "power_off": case "read_status":
|
||||
case "emergency_unlock": case "install_first_party_farmware":
|
||||
return <TileSystemAction {...props} />;
|
||||
case "check_updates": case "factory_reset":
|
||||
return <TileFirmwareAction {...props} />;
|
||||
default:
|
||||
return <TileUnknown {...props} />;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ToolTips, Content } from "../../constants";
|
|||
import {
|
||||
StepWrapper, StepHeader, StepContent, conflictsString, StepWarning
|
||||
} from "../step_ui/index";
|
||||
import { StepRadio } from "../step_ui/step_radio";
|
||||
import { AxisStepRadio } from "../step_ui/step_radio";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Xyz, Calibrate, TaggedSequence } from "farmbot";
|
||||
import { some } from "lodash";
|
||||
|
@ -81,7 +81,7 @@ class InnerTileCalibrate extends React.Component<CalibrateParams, {}> {
|
|||
conflicts={this.settingConflicts} />}
|
||||
</StepHeader>
|
||||
<StepContent className={className}>
|
||||
<StepRadio
|
||||
<AxisStepRadio
|
||||
currentSequence={currentSequence}
|
||||
currentStep={currentStep}
|
||||
dispatch={dispatch}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { t } from "../../i18next_wrapper";
|
|||
|
||||
export function TileEmergencyStop(props: StepParams) {
|
||||
const { dispatch, currentStep, index, currentSequence } = props;
|
||||
const className = "take-photo-step";
|
||||
const className = "emergency-stop-step";
|
||||
return <StepWrapper>
|
||||
<StepHeader
|
||||
className={className}
|
||||
|
|
|
@ -54,7 +54,7 @@ export function TileExecuteScript(props: StepParams) {
|
|||
|
||||
/** Configs (inputs) from Farmware manifest for <FarmwareInputs />. */
|
||||
const currentFarmwareConfigDefaults = (fwName: string): FarmwareConfig[] => {
|
||||
return farmwareData && farmwareData.farmwareConfigs[fwName]
|
||||
return farmwareData?.farmwareConfigs[fwName]
|
||||
? farmwareData.farmwareConfigs[fwName]
|
||||
: [];
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
StepWrapper, StepHeader, StepContent, StepWarning, conflictsString
|
||||
} from "../step_ui/index";
|
||||
import { some } from "lodash";
|
||||
import { StepRadio } from "../step_ui/step_radio";
|
||||
import { AxisStepRadio } from "../step_ui/step_radio";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
export function TileFindHome(props: StepParams) {
|
||||
|
@ -80,7 +80,7 @@ class InnerFindHome extends React.Component<FindHomeParams, {}> {
|
|||
conflicts={this.settingConflicts} />}
|
||||
</StepHeader>
|
||||
<StepContent className={className}>
|
||||
<StepRadio
|
||||
<AxisStepRadio
|
||||
currentSequence={currentSequence}
|
||||
currentStep={currentStep}
|
||||
dispatch={dispatch}
|
||||
|
|
|
@ -24,8 +24,8 @@ import {
|
|||
const fakeResourceIndex = buildResourceIndex(FAKE_RESOURCES).index;
|
||||
const fakeTaggedSequence = fakeResourceIndex
|
||||
.references[Object.keys(fakeResourceIndex.byKind.Sequence)[0]] as TaggedSequence;
|
||||
const fakeId = fakeTaggedSequence && fakeTaggedSequence.body.id || 0;
|
||||
const fakeName = fakeTaggedSequence && fakeTaggedSequence.body.name || "";
|
||||
const fakeId = fakeTaggedSequence.body.id || 0;
|
||||
const fakeName = fakeTaggedSequence.body.name || "";
|
||||
const expectedItem = { label: fakeName, value: fakeId };
|
||||
|
||||
function fakeProps(): IfParams {
|
||||
|
|
|
@ -2,10 +2,11 @@ import * as React from "react";
|
|||
import { StepParams } from "../interfaces";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
|
||||
import { StepRadio } from "../step_ui/step_radio";
|
||||
import { AxisStepRadio } from "../step_ui/step_radio";
|
||||
import { StepInputBox } from "../inputs/step_input_box";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Home } from "farmbot";
|
||||
|
||||
export function TileMoveHome(props: StepParams) {
|
||||
const { dispatch, currentStep, index, currentSequence } = props;
|
||||
|
@ -20,9 +21,9 @@ export function TileMoveHome(props: StepParams) {
|
|||
index={index}
|
||||
confirmStepDeletion={props.confirmStepDeletion} />
|
||||
<StepContent className={className}>
|
||||
<StepRadio
|
||||
<AxisStepRadio
|
||||
currentSequence={currentSequence}
|
||||
currentStep={currentStep}
|
||||
currentStep={currentStep as Home}
|
||||
dispatch={dispatch}
|
||||
index={index}
|
||||
label={t("Home")} />
|
||||
|
|
|
@ -3,52 +3,18 @@ import { StepParams } from "../interfaces";
|
|||
import { ToolTips } from "../../constants";
|
||||
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { ALLOWED_PACKAGES, SequenceBodyItem, Reboot } from "farmbot";
|
||||
import { editStep } from "../../api/crud";
|
||||
import { StepRadio } from "../step_ui/step_radio";
|
||||
|
||||
type StringMap = Record<string, string>;
|
||||
|
||||
interface MultiChoiceRadioProps<T extends StringMap> {
|
||||
uuid: string;
|
||||
choices: T;
|
||||
currentChoice: keyof T;
|
||||
onChange(key: keyof T): void;
|
||||
}
|
||||
|
||||
export const MultiChoiceRadio =
|
||||
<T extends StringMap>(props: MultiChoiceRadioProps<T>) => {
|
||||
const choices = Object.keys(props.choices);
|
||||
return <Row>
|
||||
<Col xs={12}>
|
||||
<div className="bottom-content">
|
||||
<div className="channel-fields">
|
||||
<form>
|
||||
{choices.map((choice, i) =>
|
||||
<div key={`${props.uuid} ${i}`} style={{ display: "inline" }}>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value={choice}
|
||||
onChange={() => props.onChange(choice)}
|
||||
checked={props.currentChoice === choice} />
|
||||
{t(props.choices[choice])}
|
||||
</label>
|
||||
</div>)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>;
|
||||
};
|
||||
|
||||
const PACKAGE_CHOICES: Record<ALLOWED_PACKAGES, string> = {
|
||||
"arduino_firmware": "Just the Arduino",
|
||||
"farmbot_os": "Entire system"
|
||||
};
|
||||
const PACKAGE_CHOICES = (): Record<ALLOWED_PACKAGES, string> => ({
|
||||
"arduino_firmware": t("Just the Arduino"),
|
||||
"farmbot_os": t("Entire system")
|
||||
});
|
||||
|
||||
function assertReboot(x: SequenceBodyItem): asserts x is Reboot {
|
||||
if (x.kind !== "reboot") {
|
||||
throw new Error("Impossible");
|
||||
throw new Error(`${x.kind} is not "reboot"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +40,7 @@ export const editTheRebootStep =
|
|||
|
||||
export function TileReboot(props: StepParams) {
|
||||
const { dispatch, currentStep, index, currentSequence } = props;
|
||||
const className = "take-photo-step";
|
||||
const className = "reboot-step";
|
||||
assertReboot(currentStep);
|
||||
return <StepWrapper>
|
||||
<StepHeader
|
||||
|
@ -86,9 +52,9 @@ export function TileReboot(props: StepParams) {
|
|||
index={index}
|
||||
confirmStepDeletion={props.confirmStepDeletion} />
|
||||
<StepContent className={className}>
|
||||
<MultiChoiceRadio
|
||||
uuid={currentSequence.uuid + index}
|
||||
choices={PACKAGE_CHOICES}
|
||||
<StepRadio
|
||||
choices={Object.keys(PACKAGE_CHOICES())}
|
||||
choiceLabelLookup={PACKAGE_CHOICES()}
|
||||
currentChoice={currentStep.args.package as ALLOWED_PACKAGES}
|
||||
onChange={editTheRebootStep(props)} />
|
||||
</StepContent>
|
||||
|
|
|
@ -29,7 +29,7 @@ export function TileSendMessage(props: StepParams) {
|
|||
}
|
||||
}
|
||||
|
||||
interface SendMessageParams {
|
||||
export interface SendMessageParams {
|
||||
currentStep: SendMessage;
|
||||
currentSequence: TaggedSequence;
|
||||
dispatch: Function;
|
||||
|
@ -40,19 +40,12 @@ interface SendMessageParams {
|
|||
|
||||
export class RefactoredSendMessage
|
||||
extends React.Component<SendMessageParams, {}> {
|
||||
get args() { return this.props.currentStep.args; }
|
||||
get message() { return this.args.message; }
|
||||
get message_type() { return this.args.message_type; }
|
||||
get step() { return this.props.currentStep; }
|
||||
get dispatch() { return this.props.dispatch; }
|
||||
get sequence() { return this.props.currentSequence; }
|
||||
get index() { return this.props.index; }
|
||||
get currentSelection() {
|
||||
return MESSAGE_STATUSES_DDI[this.message_type];
|
||||
return MESSAGE_STATUSES_DDI[this.props.currentStep.args.message_type];
|
||||
}
|
||||
|
||||
get channels() {
|
||||
return (this.step.body || []).map(x => x.args.channel_name);
|
||||
return (this.props.currentStep.body || []).map(x => x.args.channel_name);
|
||||
}
|
||||
|
||||
hasChannel = (name: ChannelName) => {
|
||||
|
@ -69,19 +62,19 @@ export class RefactoredSendMessage
|
|||
}
|
||||
|
||||
toggle = (n: ChannelName) => () => {
|
||||
this.dispatch(editStep({
|
||||
sequence: this.sequence,
|
||||
step: this.step,
|
||||
index: this.index,
|
||||
this.props.dispatch(editStep({
|
||||
sequence: this.props.currentSequence,
|
||||
step: this.props.currentStep,
|
||||
index: this.props.index,
|
||||
executor: this.hasChannel(n) ? this.remove(n) : this.add(n)
|
||||
}));
|
||||
}
|
||||
|
||||
setMsgType = (x: DropDownItem) => {
|
||||
this.dispatch(editStep({
|
||||
sequence: this.sequence,
|
||||
step: this.step,
|
||||
index: this.index,
|
||||
this.props.dispatch(editStep({
|
||||
sequence: this.props.currentSequence,
|
||||
step: this.props.currentStep,
|
||||
index: this.props.index,
|
||||
executor: (step: SendMessage) => {
|
||||
if (isMessageType(x.value)) {
|
||||
step.args.message_type = x.value;
|
||||
|
@ -110,7 +103,7 @@ export class RefactoredSendMessage
|
|||
<Col xs={12}>
|
||||
<label>{t("Message")}</label>
|
||||
<span className="char-limit">
|
||||
{this.message.length}/300
|
||||
{this.props.currentStep.args.message.length}/300
|
||||
</span>
|
||||
<StepInputBox dispatch={dispatch}
|
||||
step={currentStep}
|
||||
|
|
|
@ -2,17 +2,18 @@ import * as React from "react";
|
|||
import { StepInputBox } from "../inputs/step_input_box";
|
||||
import { StepParams } from "../interfaces";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
|
||||
import { StepWrapper, StepHeader, StepContent } from "../step_ui";
|
||||
import { Row, Col } from "../../ui/index";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { MultiChoiceRadio } from "./tile_reboot";
|
||||
import { SetServoAngle } from "farmbot";
|
||||
import { editStep } from "../../api/crud";
|
||||
import { StepRadio } from "../step_ui/step_radio";
|
||||
|
||||
const PACKAGE_CHOICES: Record<string, string> = {
|
||||
"4": "Pin 4",
|
||||
"5": "Pin 5",
|
||||
};
|
||||
const PIN_CHOICES = ["4", "5", "6", "11"];
|
||||
const CHOICE_LABELS = () => PIN_CHOICES.reduce((acc, pinNumber) => {
|
||||
acc[pinNumber] = `${t("Pin")} ${pinNumber}`;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
type Keys =
|
||||
| "dispatch"
|
||||
|
@ -35,14 +36,14 @@ export const pinNumberChanger = (props: Props) => (y: string) => {
|
|||
};
|
||||
|
||||
export function ServoPinSelection(props: Props) {
|
||||
const { currentSequence, index, currentStep } = props;
|
||||
const { currentStep } = props;
|
||||
const num = (currentStep as SetServoAngle).args.pin_number;
|
||||
if (typeof num !== "number") { throw new Error("NO!"); }
|
||||
const onChange = pinNumberChanger(props);
|
||||
|
||||
return <MultiChoiceRadio
|
||||
uuid={currentSequence.uuid + index}
|
||||
choices={PACKAGE_CHOICES}
|
||||
return <StepRadio
|
||||
choices={PIN_CHOICES}
|
||||
choiceLabelLookup={CHOICE_LABELS()}
|
||||
currentChoice={"" + num}
|
||||
onChange={onChange} />;
|
||||
}
|
||||
|
@ -79,5 +80,4 @@ export function TileSetServoAngle(props: StepParams) {
|
|||
</Row>
|
||||
</StepContent>
|
||||
</StepWrapper>;
|
||||
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@ import * as React from "react";
|
|||
import { StepParams } from "../interfaces";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
|
||||
import { StepRadio } from "../step_ui/step_radio";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { AxisStepRadio } from "../step_ui/step_radio";
|
||||
import { Zero } from "farmbot";
|
||||
|
||||
export function TileSetZero(props: StepParams) {
|
||||
const { dispatch, currentStep, index, currentSequence } = props;
|
||||
|
@ -18,9 +19,9 @@ export function TileSetZero(props: StepParams) {
|
|||
index={index}
|
||||
confirmStepDeletion={props.confirmStepDeletion} />
|
||||
<StepContent className={className}>
|
||||
<StepRadio
|
||||
<AxisStepRadio
|
||||
currentSequence={currentSequence}
|
||||
currentStep={currentStep}
|
||||
currentStep={currentStep as Zero}
|
||||
dispatch={dispatch}
|
||||
index={index}
|
||||
label={t("Zero")} />
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
|
||||
let mockStep = {};
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
editStep: jest.fn(x => x.executor(mockStep)),
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { StepRadio, StepRadioProps } from "../step_radio";
|
||||
import { AxisStepRadio, AxisStepRadioProps } from "../step_radio";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { FindHome, Calibrate, Zero } from "farmbot";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
|
||||
describe("<StepRadio />", () => {
|
||||
const currentStep: FindHome = {
|
||||
const findHomeStep: FindHome = {
|
||||
kind: "find_home",
|
||||
args: {
|
||||
speed: 100,
|
||||
|
@ -16,55 +18,55 @@ describe("<StepRadio />", () => {
|
|||
}
|
||||
};
|
||||
|
||||
const fakeProps = (): StepRadioProps => ({
|
||||
const fakeProps = (): AxisStepRadioProps => ({
|
||||
currentSequence: fakeSequence(),
|
||||
currentStep,
|
||||
currentStep: findHomeStep,
|
||||
dispatch: jest.fn(),
|
||||
index: 0,
|
||||
label: "Find",
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<StepRadio {...fakeProps()} />);
|
||||
const wrapper = mount(<AxisStepRadio {...fakeProps()} />);
|
||||
expect(wrapper.find("input").length).toEqual(4);
|
||||
expect(wrapper.text()).toContain("Find");
|
||||
});
|
||||
|
||||
it("handles update for find_home", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<StepRadio {...p} />);
|
||||
mockStep = p.currentStep;
|
||||
const wrapper = mount(<AxisStepRadio {...p} />);
|
||||
wrapper.find("input").last().simulate("change");
|
||||
const expectedStep: FindHome = {
|
||||
kind: "find_home",
|
||||
args: { speed: 100, axis: "all" }
|
||||
};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.currentSequence,
|
||||
expect.objectContaining({ body: [expectedStep] }));
|
||||
expect(mockStep).toEqual(expectedStep);
|
||||
});
|
||||
|
||||
it("handles update for calibrate", () => {
|
||||
const p = fakeProps();
|
||||
p.currentStep = { kind: "calibrate", args: { axis: "x" } };
|
||||
const wrapper = mount(<StepRadio {...p} />);
|
||||
mockStep = p.currentStep;
|
||||
const wrapper = mount(<AxisStepRadio {...p} />);
|
||||
wrapper.find("input").last().simulate("change");
|
||||
const expectedStep: Calibrate = {
|
||||
kind: "calibrate",
|
||||
args: { axis: "all" }
|
||||
};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.currentSequence,
|
||||
expect.objectContaining({ body: [expectedStep] }));
|
||||
expect(mockStep).toEqual(expectedStep);
|
||||
});
|
||||
|
||||
it("handles update for zero", () => {
|
||||
const p = fakeProps();
|
||||
p.currentStep = { kind: "zero", args: { axis: "x" } };
|
||||
const wrapper = mount(<StepRadio {...p} />);
|
||||
mockStep = p.currentStep;
|
||||
const wrapper = mount(<AxisStepRadio {...p} />);
|
||||
wrapper.find("input").last().simulate("change");
|
||||
const expectedStep: Zero = {
|
||||
kind: "zero",
|
||||
args: { axis: "all" }
|
||||
};
|
||||
expect(overwrite).toHaveBeenCalledWith(p.currentSequence,
|
||||
expect.objectContaining({ body: [expectedStep] }));
|
||||
expect(mockStep).toEqual(expectedStep);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,57 +1,32 @@
|
|||
import * as React from "react";
|
||||
import { Row, Col } from "../../ui/index";
|
||||
import {
|
||||
TaggedSequence, SequenceBodyItem, ALLOWED_AXIS
|
||||
} from "farmbot";
|
||||
import { overwrite } from "../../api/crud";
|
||||
import { defensiveClone } from "../../util";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import {
|
||||
TaggedSequence, ALLOWED_AXIS, FindHome, Home, Calibrate, Zero
|
||||
} from "farmbot";
|
||||
import { editStep } from "../../api/crud";
|
||||
|
||||
export interface StepRadioProps {
|
||||
currentSequence: TaggedSequence;
|
||||
currentStep: SequenceBodyItem;
|
||||
dispatch: Function;
|
||||
index: number;
|
||||
label: string;
|
||||
export interface StepRadioProps<T extends string> {
|
||||
choices: T[];
|
||||
choiceLabelLookup: Record<T, string>;
|
||||
currentChoice: T;
|
||||
onChange(key: T): void;
|
||||
}
|
||||
|
||||
const AXIS_CHOICES: ALLOWED_AXIS[] = ["x", "y", "z", "all"];
|
||||
|
||||
export function StepRadio(props: StepRadioProps) {
|
||||
const isSelected = (choice: ALLOWED_AXIS) => {
|
||||
if (props.currentStep.kind === "find_home"
|
||||
|| props.currentStep.kind === "calibrate"
|
||||
|| props.currentStep.kind === "zero") {
|
||||
return props.currentStep.args.axis === choice;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (choice: ALLOWED_AXIS) => {
|
||||
const update = defensiveClone(props.currentStep);
|
||||
if (update.kind === "find_home"
|
||||
|| update.kind === "calibrate"
|
||||
|| update.kind === "zero") {
|
||||
const nextSequence = defensiveClone(props.currentSequence).body;
|
||||
update.args.axis = choice;
|
||||
(nextSequence.body || [])[props.index] = update;
|
||||
props.dispatch(overwrite(props.currentSequence, nextSequence));
|
||||
}
|
||||
};
|
||||
|
||||
return <Row>
|
||||
export const StepRadio = <T extends string>(props: StepRadioProps<T>) =>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<div className="bottom-content">
|
||||
<div className="channel-fields">
|
||||
<form>
|
||||
{AXIS_CHOICES.map((choice, i) =>
|
||||
{props.choices.map((choice, i) =>
|
||||
<div key={i} style={{ display: "inline" }}>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value={choice}
|
||||
onChange={e =>
|
||||
handleUpdate(e.currentTarget.value as typeof choice)}
|
||||
checked={isSelected(choice)} />
|
||||
{` ${t(props.label)} ${choice}`}
|
||||
onChange={() => props.onChange(choice)}
|
||||
checked={props.currentChoice === choice} />
|
||||
{t(props.choiceLabelLookup[choice])}
|
||||
</label>
|
||||
</div>)}
|
||||
</form>
|
||||
|
@ -59,4 +34,37 @@ export function StepRadio(props: StepRadioProps) {
|
|||
</div>
|
||||
</Col>
|
||||
</Row>;
|
||||
|
||||
type AxisStep = FindHome | Home | Calibrate | Zero;
|
||||
|
||||
export interface AxisStepRadioProps {
|
||||
currentSequence: TaggedSequence;
|
||||
currentStep: AxisStep;
|
||||
dispatch: Function;
|
||||
index: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const AxisStepRadio = (props: AxisStepRadioProps) => {
|
||||
const AXIS_CHOICES: ALLOWED_AXIS[] = ["x", "y", "z", "all"];
|
||||
const CHOICE_LABELS = AXIS_CHOICES.reduce((acc, axis) => {
|
||||
acc[axis] = `${t(props.label)} ${axis}`;
|
||||
return acc;
|
||||
}, {} as Record<ALLOWED_AXIS, string>);
|
||||
|
||||
const handleUpdate = (axis: ALLOWED_AXIS) => {
|
||||
const { currentStep, index, currentSequence } = props;
|
||||
props.dispatch(editStep({
|
||||
step: currentStep,
|
||||
index,
|
||||
sequence: currentSequence,
|
||||
executor: (step: AxisStep) => step.args.axis = axis,
|
||||
}));
|
||||
};
|
||||
|
||||
return <StepRadio
|
||||
choices={AXIS_CHOICES}
|
||||
choiceLabelLookup={CHOICE_LABELS}
|
||||
currentChoice={props.currentStep.args.axis}
|
||||
onChange={handleUpdate} />;
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ export const newTaggedResource = <T extends TR>(kind: T["kind"],
|
|||
return {
|
||||
kind: kind as TaggedResource["kind"],
|
||||
body: body as TaggedResource["body"],
|
||||
uuid: generateUuid(body && body.id ? body.id : undefined, kind),
|
||||
uuid: generateUuid(body?.id, kind),
|
||||
specialStatus
|
||||
} as T;
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue