no executables farm event form

pull/1652/head
gabrielburnworth 2020-01-03 12:17:56 -08:00
parent 271884f2d0
commit a10267507b
13 changed files with 534 additions and 420 deletions

View File

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

View File

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

View File

@ -78,13 +78,3 @@
background: $gray;
}
}
.add-farm-event-panel {
.note {
margin-top: 4rem;
}
}
.add-event-repeat-frequency {
min-height: 34px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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} />
&nbsp;{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>;
};

View File

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

View File

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

View File

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