no executables farm event form
parent
271884f2d0
commit
a10267507b
|
@ -820,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 =
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -6,6 +6,7 @@ interface HelpProps {
|
|||
text: string;
|
||||
requireClick?: boolean;
|
||||
position?: PopoverPosition;
|
||||
customIcon?: string;
|
||||
}
|
||||
|
||||
export function Help(props: HelpProps) {
|
||||
|
@ -14,7 +15,7 @@ export function Help(props: HelpProps) {
|
|||
interactionKind={props.requireClick
|
||||
? PopoverInteractionKind.CLICK : PopoverInteractionKind.HOVER}
|
||||
popoverClassName={"help"}>
|
||||
<i className="fa fa-question-circle help-icon"></i>
|
||||
<i className={`fa fa-${props.customIcon || "question-circle"} help-icon`} />
|
||||
<div>{t(props.text)}</div>
|
||||
</Popover>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue