clean up regimen form for supported OS versions
parent
95fb9431ee
commit
e19abc2f7e
|
@ -62,6 +62,7 @@ export enum Feature {
|
|||
api_pin_bindings = "api_pin_bindings",
|
||||
farmduino_k14 = "farmduino_k14",
|
||||
jest_feature = "jest_feature", // for tests
|
||||
backscheduled_regimens = "backscheduled_regimens",
|
||||
}
|
||||
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
|
||||
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
|
||||
|
|
|
@ -34,6 +34,7 @@ describe("<AddFarmEvent />", () => {
|
|||
findExecutable: () => sequence,
|
||||
timeOffset: 0,
|
||||
autoSyncEnabled: false,
|
||||
allowRegimenBackscheduling: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ describe("<EditFarmEvent />", () => {
|
|||
findExecutable: () => sequence,
|
||||
timeOffset: 0,
|
||||
autoSyncEnabled: false,
|
||||
allowRegimenBackscheduling: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ describe("<FarmEventForm/>", () => {
|
|||
title: "title",
|
||||
timeOffset: 0,
|
||||
autoSyncEnabled: false,
|
||||
allowRegimenBackscheduling: false,
|
||||
});
|
||||
|
||||
function instance(p: EditFEProps) {
|
||||
|
@ -134,11 +135,26 @@ describe("<FarmEventForm/>", () => {
|
|||
"executable_type": "Regimen",
|
||||
"executable_id": "1",
|
||||
timeOffset: 0
|
||||
});
|
||||
}, { forceRegimensToMidnight: false });
|
||||
expect(result.time_unit).toEqual("never");
|
||||
expect(result.time_unit).not.toEqual("daily");
|
||||
});
|
||||
|
||||
it("sets regimen start_time to `00:00` as needed", () => {
|
||||
const result = recombine({
|
||||
"startDate": "2017-08-01",
|
||||
"startTime": "08:35",
|
||||
"endDate": "2017-08-01",
|
||||
"endTime": "08:33",
|
||||
"repeat": "1",
|
||||
"timeUnit": "daily",
|
||||
"executable_type": "Regimen",
|
||||
"executable_id": "1",
|
||||
timeOffset: 0
|
||||
}, { forceRegimensToMidnight: true });
|
||||
expect(result.start_time).toEqual("2017-08-01T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it(`Recombines local state back into a Partial<TaggedFarmEvent["body"]>`, () => {
|
||||
const result = recombine({
|
||||
"startDate": "2017-08-01",
|
||||
|
@ -150,7 +166,7 @@ describe("<FarmEventForm/>", () => {
|
|||
"executable_type": "Regimen",
|
||||
"executable_id": "1",
|
||||
timeOffset: 0
|
||||
});
|
||||
}, { forceRegimensToMidnight: false });
|
||||
expect(result).toEqual({
|
||||
start_time: "2017-08-01T08:35:00.000Z",
|
||||
end_time: "2017-08-01T08:33:00.000Z",
|
||||
|
@ -180,7 +196,8 @@ describe("<FarmEventForm/>", () => {
|
|||
dispatch={jest.fn()}
|
||||
repeatOptions={repeatOptions}
|
||||
timeOffset={0}
|
||||
autoSyncEnabled={false} />);
|
||||
autoSyncEnabled={false}
|
||||
allowRegimenBackscheduling={false} />);
|
||||
el.update();
|
||||
const txt = el.text().replace(/\s+/g, " ");
|
||||
expect(txt).toContain("Save *");
|
||||
|
@ -210,8 +227,31 @@ describe("<FarmEventForm/>", () => {
|
|||
expect.stringContaining("must first SYNC YOUR DEVICE"));
|
||||
});
|
||||
|
||||
it("displays error message on save", async () => {
|
||||
it("displays error message on save (add): start time has passed", () => {
|
||||
const p = props();
|
||||
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";
|
||||
const i = instance(p);
|
||||
i.commitViewModel();
|
||||
expect(error).toHaveBeenCalledWith("Unable to save farm event.");
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
"FarmEvent start time needs to be in the future, not the past.");
|
||||
});
|
||||
|
||||
it("doesn't display error message on edit: start time has passed", () => {
|
||||
const p = props();
|
||||
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";
|
||||
const i = instance(p);
|
||||
i.commitViewModel();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("displays error message on save: no items", async () => {
|
||||
const p = props();
|
||||
p.allowRegimenBackscheduling = true;
|
||||
p.farmEvent.body.start_time = "2017-05-22T05:00:00.000Z";
|
||||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
const i = instance(p);
|
||||
|
|
|
@ -115,6 +115,7 @@ export class AddFarmEvent
|
|||
title={t("Add Farm Event")}
|
||||
timeOffset={this.props.timeOffset}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
allowRegimenBackscheduling={this.props.allowRegimenBackscheduling}
|
||||
/>;
|
||||
} else {
|
||||
return this
|
||||
|
|
|
@ -24,7 +24,8 @@ export class EditFarmEvent extends React.Component<AddEditFarmEventProps, {}> {
|
|||
title={t("Edit Farm Event")}
|
||||
deleteBtn={true}
|
||||
timeOffset={this.props.timeOffset}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled} />;
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
allowRegimenBackscheduling={this.props.allowRegimenBackscheduling} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -63,14 +63,18 @@ export function destructureFarmEvent(fe: TaggedFarmEvent, timeOffset: number): F
|
|||
}
|
||||
|
||||
type PartialFE = Partial<TaggedFarmEvent["body"]>;
|
||||
type recombineOptions = { forceRegimensToMidnight: boolean };
|
||||
|
||||
/** Take a FormViewModel and recombine the fields into a Partial<FarmEvent>
|
||||
* that can be used to apply updates (such as a PUT request to the API). */
|
||||
export function recombine(vm: FarmEventViewModel): PartialFE {
|
||||
export function recombine(vm: FarmEventViewModel,
|
||||
options: recombineOptions): PartialFE {
|
||||
// Make sure that `repeat` is set to `never` when dealing with regimens.
|
||||
const isReg = vm.executable_type === "Regimen";
|
||||
const startTime = isReg && options.forceRegimensToMidnight
|
||||
? "00:00" : vm.startTime;
|
||||
return {
|
||||
start_time: offsetTime(vm.startDate, vm.startTime, vm.timeOffset),
|
||||
start_time: offsetTime(vm.startDate, startTime, vm.timeOffset),
|
||||
end_time: offsetTime(vm.endDate, vm.endTime, vm.timeOffset),
|
||||
repeat: parseInt(vm.repeat, 10) || 1,
|
||||
time_unit: (isReg ? "never" : vm.timeUnit) as TimeUnit,
|
||||
|
@ -98,6 +102,7 @@ export interface EditFEProps {
|
|||
deleteBtn?: boolean;
|
||||
timeOffset: number;
|
||||
autoSyncEnabled: boolean;
|
||||
allowRegimenBackscheduling: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -179,8 +184,33 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
this.mergeState("timeUnit", (!checked || this.isReg) ? "never" : "daily");
|
||||
};
|
||||
|
||||
/** Validates that start time is not in the past if:
|
||||
* * adding a new event (editing repeat info for ongoing events is allowed)
|
||||
* * is a sequence farm event (backscheduling of regimen events is allowed)
|
||||
* * installed FBOS version supports backscheduling of regimen farm events
|
||||
* (which is the reason this is a frontend validation)
|
||||
*
|
||||
* Once saved, if
|
||||
* - Regimen Farm Event:
|
||||
* * Return to calendar view.
|
||||
* * If scheduled for today, warn about the possibility of missing tasks.
|
||||
* * Display the start time difference from now and maybe prompt to sync.
|
||||
* - Sequence Farm Event:
|
||||
* * Determine the time for the next item to be run.
|
||||
* * Return to calendar view only if more items exist to be run.
|
||||
* * Display the next item run time.
|
||||
* * If auto-sync is disabled, prompt the user to sync.
|
||||
*/
|
||||
commitViewModel = () => {
|
||||
const partial = recombine(betterMerge(this.viewModel, this.state.fe));
|
||||
const partial = recombine(betterMerge(this.viewModel, this.state.fe),
|
||||
{ forceRegimensToMidnight: this.props.allowRegimenBackscheduling });
|
||||
const newEvent = this.props.title.toLowerCase().includes("add");
|
||||
if (newEvent && (moment(partial.start_time) < moment())
|
||||
&& (!this.isReg || !this.props.allowRegimenBackscheduling)) {
|
||||
error(t("Unable to save farm event."));
|
||||
error(t("FarmEvent start time needs to be in the future, not the past."));
|
||||
return;
|
||||
}
|
||||
this.dispatch(edit(this.props.farmEvent, partial));
|
||||
const EditFEPath = window.location.pathname;
|
||||
this
|
||||
|
@ -189,7 +219,11 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
this.setState({ specialStatusLocal: SpecialStatus.SAVED });
|
||||
history.push("/app/designer/farm_events");
|
||||
const frmEvnt = this.props.farmEvent;
|
||||
const nextRun = _.first(scheduleForFarmEvent(frmEvnt.body).items);
|
||||
this.props.dispatch(maybeWarnAboutMissedTasks(frmEvnt, function () {
|
||||
alert(t(Content.REGIMEN_TODAY_SKIPPED_ITEM_RISK));
|
||||
}));
|
||||
const nextRun = _.first(scheduleForFarmEvent(frmEvnt.body).items)
|
||||
|| (this.isReg && moment(frmEvnt.body.start_time));
|
||||
if (nextRun) {
|
||||
const nextRunText = this.props.autoSyncEnabled
|
||||
? t(`This Farm Event will run {{timeFromNow}}.`,
|
||||
|
@ -198,10 +232,7 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
you must first SYNC YOUR DEVICE. If you do not sync, the event will
|
||||
not run.`.replace(/\s+/g, " "), { timeFromNow: nextRun.fromNow() });
|
||||
success(nextRunText);
|
||||
this.props.dispatch(maybeWarnAboutMissedTasks(frmEvnt, function () {
|
||||
alert(t(Content.REGIMEN_TODAY_SKIPPED_ITEM_RISK));
|
||||
}));
|
||||
} else {
|
||||
} else if (!this.isReg) {
|
||||
history.push(EditFEPath);
|
||||
error(t(Content.INVALID_RUN_TIME));
|
||||
}
|
||||
|
@ -219,6 +250,7 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
const fe = this.props.farmEvent;
|
||||
const repeats = this.fieldGet("timeUnit") !== NEVER;
|
||||
const allowRepeat = (!this.isReg && repeats);
|
||||
const forceMidnight = this.isReg && this.props.allowRegimenBackscheduling;
|
||||
return <div className="panel-container magenta-panel add-farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<p className="panel-title">
|
||||
|
@ -258,16 +290,19 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
name="start_time"
|
||||
tzOffset={this.props.timeOffset}
|
||||
value={this.fieldGet("startTime")}
|
||||
onCommit={this.fieldSet("startTime")} />
|
||||
onCommit={this.fieldSet("startTime")}
|
||||
disabled={forceMidnight}
|
||||
hidden={forceMidnight} />
|
||||
</Col>
|
||||
</Row>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={this.toggleRepeat}
|
||||
disabled={this.isReg}
|
||||
checked={repeats && !this.isReg} />
|
||||
{t("Repeats?")}
|
||||
</label>
|
||||
{!this.isReg &&
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={this.toggleRepeat}
|
||||
disabled={this.isReg}
|
||||
checked={repeats && !this.isReg} />
|
||||
{t("Repeats?")}
|
||||
</label>}
|
||||
<FarmEventRepeatForm
|
||||
tzOffset={this.props.timeOffset}
|
||||
disabled={!allowRepeat}
|
||||
|
|
|
@ -8,14 +8,16 @@ interface Props {
|
|||
tzOffset: number;
|
||||
onCommit(ev: React.SyntheticEvent<HTMLInputElement>): void;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
name: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export function EventTimePicker(props: Props) {
|
||||
const { value, onCommit, disabled, name } = props;
|
||||
const { value, onCommit, disabled, hidden, name } = props;
|
||||
return <BlurableInput
|
||||
disabled={!!disabled}
|
||||
hidden={!!hidden}
|
||||
name={name}
|
||||
type="time"
|
||||
className="add-event-start-time"
|
||||
|
|
|
@ -15,7 +15,8 @@ import {
|
|||
findSequenceById,
|
||||
findRegimenById,
|
||||
getDeviceAccountSettings,
|
||||
getFbosConfig
|
||||
getFbosConfig,
|
||||
maybeGetDevice
|
||||
} from "../../resources/selectors";
|
||||
import {
|
||||
TaggedFarmEvent,
|
||||
|
@ -23,8 +24,11 @@ import {
|
|||
TaggedRegimen
|
||||
} from "../../resources/tagged_resources";
|
||||
import { DropDownItem } from "../../ui/index";
|
||||
import { validFbosConfig } from "../../util";
|
||||
import {
|
||||
validFbosConfig, shouldDisplay, determineInstalledOsVersion
|
||||
} from "../../util";
|
||||
import { sourceFbosConfigValue } from "../../devices/components/source_config_value";
|
||||
import { Feature } from "../../devices/interfaces";
|
||||
|
||||
export let formatTime = (input: string, timeOffset: number) => {
|
||||
const iso = new Date(input).toISOString();
|
||||
|
@ -128,6 +132,12 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
|
|||
const autoSyncEnabled =
|
||||
!!sourceFbosConfigValue(fbosConfig, configuration)("auto_sync").value;
|
||||
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
const allowRegimenBackscheduling = shouldDisplay(
|
||||
installedOsVersion, props.bot.minOsFeatureData)(
|
||||
Feature.backscheduled_regimens);
|
||||
|
||||
return {
|
||||
deviceTimezone: dev
|
||||
.body
|
||||
|
@ -144,5 +154,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
|
|||
findExecutable,
|
||||
timeOffset: dev.body.tz_offset_hrs,
|
||||
autoSyncEnabled,
|
||||
allowRegimenBackscheduling,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -137,6 +137,7 @@ export interface AddEditFarmEventProps {
|
|||
findExecutable: ExecutableQuery;
|
||||
timeOffset: number;
|
||||
autoSyncEnabled: boolean;
|
||||
allowRegimenBackscheduling: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue