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": []
}