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>
|
||||
<ProgramSequenceView sequence={sequence} sections={this.device.sections} editing={editing}/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label><h4>Schedule</h4></label>
|
||||
<ScheduleView schedule={schedule} editing={editing}/>
|
||||
</Form.Field>
|
||||
<ScheduleView schedule={schedule} editing={editing} label={<h4>Schedule</h4>} />
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
{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());
|
||||
}
|
||||
|
||||
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 minute: number;
|
||||
readonly second: number;
|
||||
@ -47,6 +54,17 @@ export enum Month {
|
||||
}
|
||||
|
||||
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 month: Month;
|
||||
readonly year: number;
|
||||
|
@ -32,7 +32,8 @@
|
||||
],
|
||||
"no-submodule-imports": false,
|
||||
"jsx-boolean-value": [ true, "never" ],
|
||||
"no-implicit-dependencies": false
|
||||
"no-implicit-dependencies": false,
|
||||
"curly": [ true, "ignore-same-line" ]
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user