Lots of work on schedule editing
This commit is contained in:
parent
117bb7cfa6
commit
03f317c1ba
@ -1,107 +0,0 @@
|
|||||||
import { observer } from "mobx-react";
|
|
||||||
import * as moment from "moment";
|
|
||||||
import * as React from "react";
|
|
||||||
import { Checkbox, Form, Input, InputOnChangeData, CheckboxProps } from "semantic-ui-react";
|
|
||||||
|
|
||||||
import { DateOfYear, Schedule, TimeOfDay, Weekday, WEEKDAYS } from "@common/sprinklersRpc";
|
|
||||||
|
|
||||||
function timeToString(time: TimeOfDay) {
|
|
||||||
return moment(time).format("LTS");
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateOfYear(day: DateOfYear | null, prefix: React.ReactNode, editing: boolean) {
|
|
||||||
if (day == null && !editing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const format = (day && day.year === 0) ? "M/D" : "l";
|
|
||||||
const dayString = moment(day || "").format(format);
|
|
||||||
let dayNode: React.ReactNode;
|
|
||||||
if (editing) {
|
|
||||||
dayNode = <Input value={dayString} />;
|
|
||||||
} else {
|
|
||||||
dayNode = <span>{dayString}</span>;
|
|
||||||
}
|
|
||||||
return <Form.Field inline>{prefix}{dayNode}</Form.Field>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WeekdaysViewProps {
|
|
||||||
weekdays: Weekday[];
|
|
||||||
editing: boolean;
|
|
||||||
onChange?: (newWeekdays: Weekday[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function WeekdaysView({weekdays, editing, onChange}: WeekdaysViewProps) {
|
|
||||||
let node: React.ReactNode;
|
|
||||||
if (editing) {
|
|
||||||
node = WEEKDAYS.map((weekday) => {
|
|
||||||
const checked = weekdays.find((wd) => wd === weekday) != null;
|
|
||||||
const name = Weekday[weekday];
|
|
||||||
const toggleWeekday = (event: React.FormEvent<HTMLInputElement>, data: CheckboxProps) => {
|
|
||||||
if (!onChange) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.checked && !checked) {
|
|
||||||
onChange(weekdays.concat([weekday]));
|
|
||||||
} else if (!data.checked && checked) {
|
|
||||||
onChange(weekdays.filter((wd) => wd !== weekday));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Form.Field control={Checkbox} label={name} checked={checked} key={weekday} onChange={toggleWeekday} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
node = weekdays.map((weekday) =>
|
|
||||||
Weekday[weekday]).join(", ");
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Form.Group inline>
|
|
||||||
<label>On</label> {node}
|
|
||||||
</Form.Group>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScheduleViewProps {
|
|
||||||
schedule: Schedule;
|
|
||||||
editing?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export default class ScheduleView extends React.Component<ScheduleViewProps> {
|
|
||||||
render() {
|
|
||||||
const { schedule } = this.props;
|
|
||||||
const editing = this.props.editing != null ? this.props.editing : false;
|
|
||||||
|
|
||||||
let times: React.ReactNode;
|
|
||||||
if (editing) {
|
|
||||||
times = schedule.times
|
|
||||||
.map((time, i) => {
|
|
||||||
const onChange = (event: React.SyntheticEvent, d: InputOnChangeData) => {
|
|
||||||
const m = moment(d.value, ["LTS"]);
|
|
||||||
schedule.times[i] = TimeOfDay.fromMoment(m);
|
|
||||||
};
|
|
||||||
return <Input value={timeToString(time)} key={i} onChange={onChange} />;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
times = schedule.times.map((time) => timeToString(time))
|
|
||||||
.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const from = formatDateOfYear(schedule.from, <label>From </label>, editing);
|
|
||||||
const to = formatDateOfYear(schedule.to, <label>To </label>, editing);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Field inline>
|
|
||||||
<label>At</label> {times}
|
|
||||||
</Form.Field>
|
|
||||||
<WeekdaysView weekdays={schedule.weekdays} editing={editing} onChange={this.updateWeekdays}/>
|
|
||||||
{from}
|
|
||||||
{to}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateWeekdays = (newWeekdays: Weekday[]) => {
|
|
||||||
this.props.schedule.weekdays = newWeekdays;
|
|
||||||
}
|
|
||||||
}
|
|
79
app/components/ScheduleView/ScheduleDate.tsx
Normal file
79
app/components/ScheduleView/ScheduleDate.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import * as moment from "moment";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Form, Icon, Input, InputOnChangeData } from "semantic-ui-react";
|
||||||
|
|
||||||
|
import { DateOfYear } from "@common/sprinklersRpc";
|
||||||
|
|
||||||
|
const HTML_DATE_INPUT_FORMAT = "YYYY-MM-DD";
|
||||||
|
|
||||||
|
export interface ScheduleDateProps {
|
||||||
|
date: DateOfYear | null | undefined;
|
||||||
|
label: string | React.ReactNode | undefined;
|
||||||
|
editing: boolean | undefined;
|
||||||
|
onChange: (newDate: DateOfYear | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleDateState {
|
||||||
|
rawValue: string | "";
|
||||||
|
lastDate: DateOfYear | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ScheduleDate extends React.Component<ScheduleDateProps, ScheduleDateState> {
|
||||||
|
static getDerivedStateFromProps(props: ScheduleDateProps, state: ScheduleDateState): Partial<ScheduleDateState> {
|
||||||
|
if (!DateOfYear.equals(props.date, state.lastDate)) {
|
||||||
|
const rawValue = props.date == null ? "" :
|
||||||
|
moment(props.date).format(HTML_DATE_INPUT_FORMAT);
|
||||||
|
return { lastDate: props.date, rawValue };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(p: ScheduleDateProps) {
|
||||||
|
super(p);
|
||||||
|
this.state = { rawValue: "", lastDate: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { date, label, editing } = this.props;
|
||||||
|
|
||||||
|
let dayNode: React.ReactNode;
|
||||||
|
if (editing) { // tslint:disable-line:prefer-conditional-expression
|
||||||
|
let clearIcon: React.ReactNode | undefined;
|
||||||
|
if (date) {
|
||||||
|
clearIcon = <Icon name="ban" link onClick={this.onClear} />;
|
||||||
|
}
|
||||||
|
dayNode = <Input type="date" icon={clearIcon} value={this.state.rawValue} onChange={this.onChange}/>;
|
||||||
|
} else {
|
||||||
|
let dayString: string;
|
||||||
|
if (date) {
|
||||||
|
const format = (date.year === 0) ? "M/D" : "l";
|
||||||
|
dayString = moment(date).format(format);
|
||||||
|
} else {
|
||||||
|
dayString = "N/A";
|
||||||
|
}
|
||||||
|
dayNode = <span>{dayString}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelNode: React.ReactNode = null;
|
||||||
|
if (typeof label === "string") {
|
||||||
|
labelNode = <label>{label}</label>
|
||||||
|
} else if (label != null) {
|
||||||
|
labelNode = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Form.Field inline>{labelNode}{dayNode}</Form.Field>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onChange = (e: React.SyntheticEvent<HTMLInputElement>, data: InputOnChangeData) => {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (!onChange) return;
|
||||||
|
const m = moment(data.value, HTML_DATE_INPUT_FORMAT);
|
||||||
|
onChange(DateOfYear.fromMoment(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClear = () => {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (!onChange) return;
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
}
|
41
app/components/ScheduleView/ScheduleTimes.tsx
Normal file
41
app/components/ScheduleView/ScheduleTimes.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import * as moment from "moment";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Form } from "semantic-ui-react";
|
||||||
|
|
||||||
|
import { TimeOfDay } from "@common/sprinklersRpc";
|
||||||
|
import TimeInput from "./TimeInput";
|
||||||
|
|
||||||
|
function timeToString(time: TimeOfDay) {
|
||||||
|
return moment(time).format("LTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ScheduleTimes extends React.Component<{
|
||||||
|
times: TimeOfDay[];
|
||||||
|
onChange: (newTimes: TimeOfDay[]) => void;
|
||||||
|
editing: boolean;
|
||||||
|
}> {
|
||||||
|
render() {
|
||||||
|
const { times, editing } = this.props;
|
||||||
|
let timesNode: React.ReactNode;
|
||||||
|
if (editing) {
|
||||||
|
timesNode = times
|
||||||
|
.map((time, i) => <TimeInput value={time} key={i} index={i} onChange={this.onTimeChange} />);
|
||||||
|
} else {
|
||||||
|
timesNode = (
|
||||||
|
<span>
|
||||||
|
{times.map((time) => timeToString(time)).join(", ")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (<Form.Field inline className="scheduleTimes">
|
||||||
|
<label>At</label>
|
||||||
|
{timesNode}
|
||||||
|
</Form.Field>);
|
||||||
|
}
|
||||||
|
private onTimeChange = (newTime: TimeOfDay, index: number) => {
|
||||||
|
const { times, onChange } = this.props;
|
||||||
|
const newTimes = times.slice();
|
||||||
|
newTimes[index] = newTime;
|
||||||
|
onChange(newTimes);
|
||||||
|
}
|
||||||
|
}
|
51
app/components/ScheduleView/TimeInput.tsx
Normal file
51
app/components/ScheduleView/TimeInput.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as moment from "moment";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Input, InputOnChangeData } from "semantic-ui-react";
|
||||||
|
|
||||||
|
import { TimeOfDay } from "@common/sprinklersRpc";
|
||||||
|
|
||||||
|
const HTML_TIME_INPUT_FORMAT = "HH:mm";
|
||||||
|
|
||||||
|
function timeOfDayToHtmlDateInput(tod: TimeOfDay): string {
|
||||||
|
return moment(tod).format(HTML_TIME_INPUT_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeInputProps {
|
||||||
|
value: TimeOfDay;
|
||||||
|
index: number;
|
||||||
|
onChange: (newValue: TimeOfDay, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeInputState {
|
||||||
|
rawValue: string;
|
||||||
|
lastTime: TimeOfDay | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TimeInput extends React.Component<TimeInputProps, TimeInputState> {
|
||||||
|
static getDerivedStateFromProps(props: TimeInputProps, state: TimeInputState): Partial<TimeInputState> {
|
||||||
|
if (!TimeOfDay.equals(props.value, state.lastTime)) {
|
||||||
|
return { lastTime: props.value, rawValue: timeOfDayToHtmlDateInput(props.value) };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
constructor(p: any) {
|
||||||
|
super(p);
|
||||||
|
this.state = { rawValue: "", lastTime: null };
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return <Input type="time" value={this.state.rawValue} onChange={this.onChange} onBlur={this.onBlur} />;
|
||||||
|
}
|
||||||
|
private onChange = (e: React.SyntheticEvent<HTMLInputElement>, data: InputOnChangeData) => {
|
||||||
|
this.setState({
|
||||||
|
rawValue: data.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
private onBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
const m = moment(this.state.rawValue, HTML_TIME_INPUT_FORMAT);
|
||||||
|
if (m.isValid()) {
|
||||||
|
this.props.onChange(TimeOfDay.fromMoment(m), this.props.index);
|
||||||
|
} else {
|
||||||
|
this.setState({ rawValue: timeOfDayToHtmlDateInput(this.props.value) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
52
app/components/ScheduleView/WeekdaysView.tsx
Normal file
52
app/components/ScheduleView/WeekdaysView.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Checkbox, CheckboxProps, Form } from "semantic-ui-react";
|
||||||
|
|
||||||
|
import { Weekday, WEEKDAYS } from "@common/sprinklersRpc";
|
||||||
|
|
||||||
|
export interface WeekdaysViewProps {
|
||||||
|
weekdays: Weekday[];
|
||||||
|
editing: boolean;
|
||||||
|
onChange?: (newWeekdays: Weekday[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WeekdaysView extends React.Component<WeekdaysViewProps> {
|
||||||
|
render() {
|
||||||
|
const { weekdays, editing } = this.props;
|
||||||
|
let node: React.ReactNode;
|
||||||
|
if (editing) {
|
||||||
|
node = WEEKDAYS.map((weekday) => {
|
||||||
|
const checked = weekdays.find((wd) => wd === weekday) != null;
|
||||||
|
const name = Weekday[weekday];
|
||||||
|
return (
|
||||||
|
<Form.Field
|
||||||
|
control={Checkbox}
|
||||||
|
x-weekday={weekday}
|
||||||
|
label={name}
|
||||||
|
checked={checked}
|
||||||
|
key={weekday}
|
||||||
|
onChange={this.toggleWeekday}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
node = weekdays.map((weekday) => Weekday[weekday]).join(", ");
|
||||||
|
}
|
||||||
|
return (<Form.Group inline>
|
||||||
|
<label>On</label> {node}
|
||||||
|
</Form.Group>);
|
||||||
|
}
|
||||||
|
private toggleWeekday = (event: React.FormEvent<HTMLInputElement>, data: CheckboxProps) => {
|
||||||
|
const { weekdays, onChange } = this.props;
|
||||||
|
if (!onChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const weekday: Weekday = Number(event.currentTarget.getAttribute("x-weekday"));
|
||||||
|
if (data.checked) {
|
||||||
|
const newWeekdays = weekdays.concat([weekday]);
|
||||||
|
newWeekdays.sort();
|
||||||
|
onChange(newWeekdays);
|
||||||
|
} else {
|
||||||
|
onChange(weekdays.filter((wd) => wd !== weekday));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
app/components/ScheduleView/index.tsx
Normal file
57
app/components/ScheduleView/index.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Form } from "semantic-ui-react";
|
||||||
|
|
||||||
|
import { DateOfYear, Schedule, TimeOfDay, Weekday } from "@common/sprinklersRpc";
|
||||||
|
import ScheduleDate from "./ScheduleDate";
|
||||||
|
import ScheduleTimes from "./ScheduleTimes";
|
||||||
|
import WeekdaysView from "./WeekdaysView";
|
||||||
|
|
||||||
|
import "@app/styles/ScheduleView";
|
||||||
|
|
||||||
|
export interface ScheduleViewProps {
|
||||||
|
label?: string | React.ReactNode | undefined;
|
||||||
|
schedule: Schedule;
|
||||||
|
editing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export default class ScheduleView extends React.Component<ScheduleViewProps> {
|
||||||
|
render() {
|
||||||
|
const { schedule, label } = this.props;
|
||||||
|
const editing = this.props.editing || false;
|
||||||
|
|
||||||
|
let labelNode: React.ReactNode;
|
||||||
|
if (typeof label === "string") {
|
||||||
|
labelNode = <label>{label}</label>
|
||||||
|
} else if (label != null) {
|
||||||
|
labelNode = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Field className="scheduleView">
|
||||||
|
{labelNode}
|
||||||
|
<ScheduleTimes times={schedule.times} editing={editing} onChange={this.updateTimes}/>
|
||||||
|
<WeekdaysView weekdays={schedule.weekdays} editing={editing} onChange={this.updateWeekdays}/>
|
||||||
|
<ScheduleDate label="From" date={schedule.from} editing={editing} onChange={this.updateFromDate}/>
|
||||||
|
<ScheduleDate label="To" date={schedule.to} editing={editing} onChange={this.updateToDate}/>
|
||||||
|
</Form.Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTimes = (newTimes: TimeOfDay[]) => {
|
||||||
|
this.props.schedule.times = newTimes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWeekdays = (newWeekdays: Weekday[]) => {
|
||||||
|
this.props.schedule.weekdays = newWeekdays;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFromDate = (newFromDate: DateOfYear | null) => {
|
||||||
|
this.props.schedule.from = newFromDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateToDate = (newToDate: DateOfYear | null) => {
|
||||||
|
this.props.schedule.to = newToDate;
|
||||||
|
}
|
||||||
|
}
|
@ -147,10 +147,7 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
<label><h4>Sequence</h4></label>
|
<label><h4>Sequence</h4></label>
|
||||||
<ProgramSequenceView sequence={sequence} sections={this.device.sections} editing={editing}/>
|
<ProgramSequenceView sequence={sequence} sections={this.device.sections} editing={editing}/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<ScheduleView schedule={schedule} editing={editing} label={<h4>Schedule</h4>} />
|
||||||
<label><h4>Schedule</h4></label>
|
|
||||||
<ScheduleView schedule={schedule} editing={editing}/>
|
|
||||||
</Form.Field>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
{this.renderActions(program)}
|
{this.renderActions(program)}
|
||||||
|
13
app/styles/ScheduleView.scss
Normal file
13
app/styles/ScheduleView.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.scheduleView {
|
||||||
|
>.field, >.fields {
|
||||||
|
>label {
|
||||||
|
width: 2rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheduleTimes {
|
||||||
|
input {
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,13 @@ export class TimeOfDay {
|
|||||||
return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static equals(a: TimeOfDay | null | undefined, b: TimeOfDay | null | undefined): boolean {
|
||||||
|
return (a === b) || ((a != null && b != null) && a.hour === b.hour &&
|
||||||
|
a.minute === b.minute &&
|
||||||
|
a.second === b.second &&
|
||||||
|
a.millisecond === b.millisecond);
|
||||||
|
}
|
||||||
|
|
||||||
readonly hour: number;
|
readonly hour: number;
|
||||||
readonly minute: number;
|
readonly minute: number;
|
||||||
readonly second: number;
|
readonly second: number;
|
||||||
@ -47,6 +54,17 @@ export enum Month {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DateOfYear {
|
export class DateOfYear {
|
||||||
|
static equals(a: DateOfYear | null | undefined, b: DateOfYear | null | undefined): boolean {
|
||||||
|
return (a === b) || ((a instanceof DateOfYear && b instanceof DateOfYear) &&
|
||||||
|
a.day === b.day &&
|
||||||
|
a.month === b.month &&
|
||||||
|
a.year === b.year);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMoment(m: Moment): DateOfYear {
|
||||||
|
return new DateOfYear(m.date(), m.month(), m.year());
|
||||||
|
}
|
||||||
|
|
||||||
readonly day: number;
|
readonly day: number;
|
||||||
readonly month: Month;
|
readonly month: Month;
|
||||||
readonly year: number;
|
readonly year: number;
|
||||||
|
@ -32,7 +32,8 @@
|
|||||||
],
|
],
|
||||||
"no-submodule-imports": false,
|
"no-submodule-imports": false,
|
||||||
"jsx-boolean-value": [ true, "never" ],
|
"jsx-boolean-value": [ true, "never" ],
|
||||||
"no-implicit-dependencies": false
|
"no-implicit-dependencies": false,
|
||||||
|
"curly": [ true, "ignore-same-line" ]
|
||||||
},
|
},
|
||||||
"rulesDirectory": []
|
"rulesDirectory": []
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user