Alex Mikhalev
7 years ago
10 changed files with 314 additions and 112 deletions
@ -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; |
|
||||||
} |
|
||||||
} |
|
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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) }); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
@ -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)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
.scheduleView { |
||||||
|
>.field, >.fields { |
||||||
|
>label { |
||||||
|
width: 2rem !important; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.scheduleTimes { |
||||||
|
input { |
||||||
|
margin: 0 .5rem; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue