clean up regimen form for supported OS versions

pull/795/head
gabrielburnworth 2018-04-18 18:00:07 -07:00
parent 95fb9431ee
commit e19abc2f7e
10 changed files with 118 additions and 24 deletions

View File

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

View File

@ -34,6 +34,7 @@ describe("<AddFarmEvent />", () => {
findExecutable: () => sequence,
timeOffset: 0,
autoSyncEnabled: false,
allowRegimenBackscheduling: false,
};
}

View File

@ -32,6 +32,7 @@ describe("<EditFarmEvent />", () => {
findExecutable: () => sequence,
timeOffset: 0,
autoSyncEnabled: false,
allowRegimenBackscheduling: false,
};
}

View File

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

View File

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

View File

@ -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() {

View File

@ -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} />
&nbsp;{t("Repeats?")}
</label>
{!this.isReg &&
<label>
<input type="checkbox"
onChange={this.toggleRepeat}
disabled={this.isReg}
checked={repeats && !this.isReg} />
&nbsp;{t("Repeats?")}
</label>}
<FarmEventRepeatForm
tzOffset={this.props.timeOffset}
disabled={!allowRepeat}

View File

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

View File

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

View File

@ -137,6 +137,7 @@ export interface AddEditFarmEventProps {
findExecutable: ExecutableQuery;
timeOffset: number;
autoSyncEnabled: boolean;
allowRegimenBackscheduling: boolean;
}
/**