diff --git a/app/components/ScheduleView.tsx b/app/components/ScheduleView.tsx deleted file mode 100644 index ddb78d4..0000000 --- a/app/components/ScheduleView.tsx +++ /dev/null @@ -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 = ; - } else { - dayNode = {dayString}; - } - return {prefix}{dayNode}; -} - -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, 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 ( - - ); - }); - } else { - node = weekdays.map((weekday) => - Weekday[weekday]).join(", "); - } - return ( - - {node} - - ) -} - -export interface ScheduleViewProps { - schedule: Schedule; - editing?: boolean; -} - -@observer -export default class ScheduleView extends React.Component { - 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 ; - }); - } else { - times = schedule.times.map((time) => timeToString(time)) - .join(", "); - } - - const from = formatDateOfYear(schedule.from, , editing); - const to = formatDateOfYear(schedule.to, , editing); - return ( -
- - {times} - - - {from} - {to} -
- ); - } - - private updateWeekdays = (newWeekdays: Weekday[]) => { - this.props.schedule.weekdays = newWeekdays; - } -} diff --git a/app/components/ScheduleView/ScheduleDate.tsx b/app/components/ScheduleView/ScheduleDate.tsx new file mode 100644 index 0000000..834ed22 --- /dev/null +++ b/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 { + static getDerivedStateFromProps(props: ScheduleDateProps, state: ScheduleDateState): Partial { + 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 = ; + } + dayNode = ; + } else { + let dayString: string; + if (date) { + const format = (date.year === 0) ? "M/D" : "l"; + dayString = moment(date).format(format); + } else { + dayString = "N/A"; + } + dayNode = {dayString}; + } + + let labelNode: React.ReactNode = null; + if (typeof label === "string") { + labelNode = + } else if (label != null) { + labelNode = label; + } + + return {labelNode}{dayNode}; + } + + private onChange = (e: React.SyntheticEvent, 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); + } +} diff --git a/app/components/ScheduleView/ScheduleTimes.tsx b/app/components/ScheduleView/ScheduleTimes.tsx new file mode 100644 index 0000000..c8df80c --- /dev/null +++ b/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) => ); + } else { + timesNode = ( + + {times.map((time) => timeToString(time)).join(", ")} + + ); + } + return ( + + {timesNode} + ); + } + private onTimeChange = (newTime: TimeOfDay, index: number) => { + const { times, onChange } = this.props; + const newTimes = times.slice(); + newTimes[index] = newTime; + onChange(newTimes); + } +} diff --git a/app/components/ScheduleView/TimeInput.tsx b/app/components/ScheduleView/TimeInput.tsx new file mode 100644 index 0000000..11f18b7 --- /dev/null +++ b/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 { + static getDerivedStateFromProps(props: TimeInputProps, state: TimeInputState): Partial { + 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 ; + } + private onChange = (e: React.SyntheticEvent, data: InputOnChangeData) => { + this.setState({ + rawValue: data.value, + }); + }; + private onBlur: React.FocusEventHandler = (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) }); + } + }; +} \ No newline at end of file diff --git a/app/components/ScheduleView/WeekdaysView.tsx b/app/components/ScheduleView/WeekdaysView.tsx new file mode 100644 index 0000000..246ac19 --- /dev/null +++ b/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 { + 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 ( + + ); + }); + } else { + node = weekdays.map((weekday) => Weekday[weekday]).join(", "); + } + return ( + {node} + ); + } + private toggleWeekday = (event: React.FormEvent, 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)); + } + } +} diff --git a/app/components/ScheduleView/index.tsx b/app/components/ScheduleView/index.tsx new file mode 100644 index 0000000..dafd9d1 --- /dev/null +++ b/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 { + render() { + const { schedule, label } = this.props; + const editing = this.props.editing || false; + + let labelNode: React.ReactNode; + if (typeof label === "string") { + labelNode = + } else if (label != null) { + labelNode = label; + } + + return ( + + {labelNode} + + + + + + ); + } + + 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; + } +} diff --git a/app/pages/ProgramPage.tsx b/app/pages/ProgramPage.tsx index 3724315..382c213 100644 --- a/app/pages/ProgramPage.tsx +++ b/app/pages/ProgramPage.tsx @@ -147,10 +147,7 @@ class ProgramPage extends React.Component { - - - - + Schedule} /> {this.renderActions(program)} diff --git a/app/styles/ScheduleView.scss b/app/styles/ScheduleView.scss new file mode 100644 index 0000000..01f8e29 --- /dev/null +++ b/app/styles/ScheduleView.scss @@ -0,0 +1,13 @@ +.scheduleView { + >.field, >.fields { + >label { + width: 2rem !important; + } + } +} + +.scheduleTimes { + input { + margin: 0 .5rem; + } +} diff --git a/common/sprinklersRpc/schedule.ts b/common/sprinklersRpc/schedule.ts index c559b52..09d8ba2 100644 --- a/common/sprinklersRpc/schedule.ts +++ b/common/sprinklersRpc/schedule.ts @@ -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; diff --git a/tslint.json b/tslint.json index 8d4daa4..ef71cfa 100644 --- a/tslint.json +++ b/tslint.json @@ -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": [] }