Browse Source

Lots of work on schedule editing

update-deps
Alex Mikhalev 7 years ago
parent
commit
03f317c1ba
  1. 107
      app/components/ScheduleView.tsx
  2. 79
      app/components/ScheduleView/ScheduleDate.tsx
  3. 41
      app/components/ScheduleView/ScheduleTimes.tsx
  4. 51
      app/components/ScheduleView/TimeInput.tsx
  5. 52
      app/components/ScheduleView/WeekdaysView.tsx
  6. 57
      app/components/ScheduleView/index.tsx
  7. 5
      app/pages/ProgramPage.tsx
  8. 13
      app/styles/ScheduleView.scss
  9. 18
      common/sprinklersRpc/schedule.ts
  10. 3
      tslint.json

107
app/components/ScheduleView.tsx

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

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

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

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

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

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

5
app/pages/ProgramPage.tsx

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

@ -0,0 +1,13 @@
.scheduleView {
>.field, >.fields {
>label {
width: 2rem !important;
}
}
}
.scheduleTimes {
input {
margin: 0 .5rem;
}
}

18
common/sprinklersRpc/schedule.ts

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

3
tslint.json

@ -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…
Cancel
Save