diff --git a/app/components/ProgramSequenceView.tsx b/app/components/ProgramSequenceView.tsx index ece2ab3..96c0760 100644 --- a/app/components/ProgramSequenceView.tsx +++ b/app/components/ProgramSequenceView.tsx @@ -1,9 +1,8 @@ import classNames = require("classnames"); import { observer } from "mobx-react"; import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { SortableContainer, SortableElement, SortableHandle, SortEnd, arrayMove } from "react-sortable-hoc"; -import { Form, Icon, List } from "semantic-ui-react"; +import { SortableContainer, SortableElement, SortableHandle, SortEnd } from "react-sortable-hoc"; +import { Button, Form, Icon, List } from "semantic-ui-react"; import { DurationView, SectionChooser } from "@app/components/index"; import { Duration } from "@common/Duration"; @@ -11,21 +10,31 @@ import { ProgramItem, Section } from "@common/sprinklersRpc"; import "@app/styles/ProgramSequenceView"; -const Handle = SortableHandle(() => ); +type ItemChangeHandler = (index: number, newItem: ProgramItem) => void; +type ItemRemoveHandler = (index: number) => void; + +const Handle = SortableHandle(() => ); @observer class ProgramSequenceItem extends React.Component<{ - sequenceItem: ProgramItem, sections: Section[], onChange?: (newItem: ProgramItem) => void, + sequenceItem: ProgramItem, + idx: number, + sections: Section[], + editing: boolean, + onChange: ItemChangeHandler, + onRemove: ItemRemoveHandler, }> { renderContent() { - const { sequenceItem, sections } = this.props; - const editing = this.props.onChange != null; + const { editing, sequenceItem, sections } = this.props; const section = sections[sequenceItem.section]; const duration = Duration.fromSeconds(sequenceItem.duration); if (editing) { return ( + {editing ? : } @@ -60,44 +69,43 @@ class ProgramSequenceItem extends React.Component<{ } private onSectionChange = (newSection: Section) => { - if (!this.props.onChange) { - return; - } - this.props.onChange(new ProgramItem({ + this.props.onChange(this.props.idx, new ProgramItem({ ...this.props.sequenceItem, section: newSection.id, })); } private onDurationChange = (newDuration: Duration) => { - if (!this.props.onChange) { - return; - } - this.props.onChange(new ProgramItem({ + this.props.onChange(this.props.idx, new ProgramItem({ ...this.props.sequenceItem, duration: newDuration.toSeconds(), })); } + + private onRemove = () => { + this.props.onRemove(this.props.idx); + } } const ProgramSequenceItemD = SortableElement(ProgramSequenceItem); -type ItemChangeHandler = (newItem: ProgramItem, index: number) => void; - -const ProgramSequenceList = SortableContainer(observer(({ className, list, sections, onChange }: { +const ProgramSequenceList = SortableContainer(observer((props: { className: string, list: ProgramItem[], sections: Section[], - onChange?: ItemChangeHandler, + editing: boolean, + onChange: ItemChangeHandler, + onRemove: ItemRemoveHandler, }) => { + const { className, list, sections, ...rest } = props; const listItems = list.map((item, index) => { - const onChangeHandler = onChange ? (newItem: ProgramItem) => onChange(newItem, index) : undefined; const key = `item-${index}`; return ( ); }); @@ -112,23 +120,28 @@ class ProgramSequenceView extends React.Component<{ const { sequence, sections } = this.props; const editing = this.props.editing || false; const className = classNames("programSequence", { editing }); - const onChange = editing ? this.changeItem : undefined; return ( ); } - private changeItem: ItemChangeHandler = (newItem, index) => { + private changeItem: ItemChangeHandler = (index, newItem) => { this.props.sequence[index] = newItem; } + private removeItem: ItemRemoveHandler = (index) => { + this.props.sequence.splice(index, 1); + } + private onSortEnd = ({oldIndex, newIndex}: SortEnd) => { const { sequence: array } = this.props; if (newIndex >= array.length) { diff --git a/app/components/ProgramTable.tsx b/app/components/ProgramTable.tsx index 29361a0..1b2f82d 100644 --- a/app/components/ProgramTable.tsx +++ b/app/components/ProgramTable.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { RouterStore } from "mobx-react-router"; import * as React from "react"; import { Link } from "react-router-dom"; -import { Button, ButtonProps, Table } from "semantic-ui-react"; +import { Button, ButtonProps, Icon, Table } from "semantic-ui-react"; import { ProgramSequenceView, ScheduleView } from "@app/components"; import * as rp from "@app/routePaths"; @@ -20,9 +20,16 @@ class ProgramRows extends React.Component<{ const { name, running, enabled, schedule, sequence } = program; - const buttonStyle: ButtonProps = { size: "small", compact: true }; + const buttonStyle: ButtonProps = { size: "small", compact: false }; const detailUrl = rp.program(device.id, program.id); + const stopStartButton = ( + + ); + const mainRow = ( {"" + program.id} @@ -32,13 +39,13 @@ class ProgramRows extends React.Component<{ {running ? "Running" : "Not running"} - + {stopStartButton} diff --git a/app/components/RunSectionForm.tsx b/app/components/RunSectionForm.tsx index 2cefeff..c97ac20 100644 --- a/app/components/RunSectionForm.tsx +++ b/app/components/RunSectionForm.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import * as React from "react"; -import { Form, Header, Segment } from "semantic-ui-react"; +import { Form, Header, Icon, Segment } from "semantic-ui-react"; import { DurationView, SectionChooser } from "@app/components"; import { UiStore } from "@app/state"; @@ -47,6 +47,7 @@ export default class RunSectionForm extends React.Component<{ onClick={this.run} disabled={!this.isValid} > + Run diff --git a/app/components/SectionRunnerView.tsx b/app/components/SectionRunnerView.tsx index e2a5d82..6057a97 100644 --- a/app/components/SectionRunnerView.tsx +++ b/app/components/SectionRunnerView.tsx @@ -21,7 +21,7 @@ function PausedState({ paused, togglePaused }: PausedStateProps) { "sectionRunner--pausedState-unpaused": !paused, }); return ( - @@ -137,7 +137,7 @@ export default class SectionRunnerView extends React.Component<{ return (
-

Section Runner Queue

+

Section Runner Queue

diff --git a/app/pages/ProgramPage.tsx b/app/pages/ProgramPage.tsx index 78652f0..4cf5513 100644 --- a/app/pages/ProgramPage.tsx +++ b/app/pages/ProgramPage.tsx @@ -1,8 +1,9 @@ import { observer } from "mobx-react"; import { createViewModel, IViewModel } from "mobx-utils"; +import * as qs from "query-string"; import * as React from "react"; import { RouteComponentProps } from "react-router"; -import { Button, CheckboxProps, Form, Input, InputOnChangeData, Menu, Modal } from "semantic-ui-react"; +import { Button, CheckboxProps, Form, Icon, Input, InputOnChangeData, Menu, Modal } from "semantic-ui-react"; import { ProgramSequenceView, ScheduleView } from "@app/components"; import * as rp from "@app/routePaths"; @@ -10,26 +11,20 @@ import { AppState, injectState } from "@app/state"; import log from "@common/logger"; import { Program, SprinklersDevice } from "@common/sprinklersRpc"; -@observer -class ProgramPage extends React.Component<{ - appState: AppState, -} & RouteComponentProps<{ deviceId: string, programId: number }>, { - programView: Program & IViewModel | undefined, -}> { - private device!: SprinklersDevice; - private program!: Program; - - constructor(p: any) { - super(p); - this.state = { - programView: undefined, - }; - } +interface ProgramPageProps extends RouteComponentProps<{ deviceId: string, programId: string }> { + appState: AppState; +} +@observer +class ProgramPage extends React.Component { get isEditing(): boolean { - return this.state.programView != null; + return qs.parse(this.props.location.search).editing != null; } + device!: SprinklersDevice; + program!: Program; + programView: Program & IViewModel | null = null; + renderName(program: Program) { const { name } = program; if (this.isEditing) { @@ -64,9 +59,11 @@ class ProgramPage extends React.Component<{ editButtons = ( - @@ -74,17 +71,23 @@ class ProgramPage extends React.Component<{ } else { editButtons = ( ); } + const stopStartButton = ( + + ); return ( - + {stopStartButton} {editButtons} @@ -92,17 +95,32 @@ class ProgramPage extends React.Component<{ } render() { - const { deviceId, programId } = this.props.match.params; - const device = this.device = this.props.appState.sprinklersRpc.getDevice(deviceId); - // TODO: check programId - if (device.programs.length <= programId || !device.programs[programId]) { - return null; + const { deviceId, programId: pid } = this.props.match.params; + const programId = Number(pid); + // tslint:disable-next-line:prefer-conditional-expression + if (!this.device || this.device.id !== deviceId) { + this.device = this.props.appState.sprinklersRpc.getDevice(deviceId); + } + // tslint:disable-next-line:prefer-conditional-expression + if (!this.program || this.program.id !== programId) { + if (this.device.programs.length > programId && programId >= 0) { + this.program = this.device.programs[programId]; + } else { + return null; + } + } + if (this.isEditing) { + if (this.programView == null && this.program) { + this.programView = createViewModel(this.program); + } + } else { + if (this.programView != null) { + this.programView = null; + } } - this.program = device.programs[programId]; - const { programView } = this.state; - const program = programView || this.program; - const editing = programView != null; + const program = this.programView || this.program; + const editing = this.isEditing; const { running, enabled, schedule, sequence } = program; @@ -144,37 +162,25 @@ class ProgramPage extends React.Component<{ } private startEditing = () => { - let { programView } = this.state; - if (programView) { // stop editing, so save - programView.submit(); - programView = undefined; - } else { // start editing - programView = createViewModel(this.program); - } - this.setState({ programView }); + this.props.history.push({ search: qs.stringify({ editing: true }) }); } private save = () => { - let { programView } = this.state; - if (programView) { // stop editing, so save - programView.submit(); - programView = undefined; + if (!this.programView || !this.program) { + return; } - this.setState({ programView }); + this.programView.submit(); this.program.update() .then((data) => { log.info({ data }, "Program updated"); }, (err) => { log.error({ err }, "error updating Program"); }); + this.stopEditing(); } - private cancelEditing = () => { - let { programView } = this.state; - if (programView) { - programView = undefined; // stop editing - } - this.setState({ programView }); + private stopEditing = () => { + this.props.history.push({ search: "" }); } private close = () => { @@ -183,16 +189,14 @@ class ProgramPage extends React.Component<{ } private onNameChange = (e: any, p: InputOnChangeData) => { - const { programView } = this.state; - if (programView) { - programView.name = p.value; + if (this.programView) { + this.programView.name = p.value; } } private onEnabledChange = (e: any, p: CheckboxProps) => { - const { programView } = this.state; - if (programView) { - programView.enabled = p.checked!; + if (this.programView) { + this.programView.enabled = p.checked!; } } } diff --git a/app/styles/ProgramSequenceView.scss b/app/styles/ProgramSequenceView.scss index 061f058..288f47e 100644 --- a/app/styles/ProgramSequenceView.scss +++ b/app/styles/ProgramSequenceView.scss @@ -10,8 +10,11 @@ z-index: 2000; display: flex; margin-bottom: .5em; - i.icon { - margin: .5rem; + .fields { + margin: 0em 0em 1em !important; + } + .ui.icon.button { + height: fit-content; } .header { font-weight: bold; diff --git a/package.json b/package.json index 6b00d33..73e8ce5 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@types/object-assign": "^4.0.30", "@types/pino": "^4.16.0", "@types/prop-types": "^15.5.4", + "@types/query-string": "^6.1.0", "@types/react": "16.4.7", "@types/react-dom": "16.0.6", "@types/react-hot-loader": "^4.1.0", @@ -100,6 +101,7 @@ "postcss-preset-env": "^5.2.3", "promise": "^8.0.1", "prop-types": "^15.6.2", + "query-string": "^6.1.0", "react": "16.4.1", "react-dev-utils": "^5.0.1", "react-dom": "16.4.1", diff --git a/yarn.lock b/yarn.lock index 3a7e1ab..f5fa13f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -111,6 +111,10 @@ dependencies: "@types/react" "*" +"@types/query-string@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.1.0.tgz#5f721f9503bdf517d474c66cf4423da5dd2d5698" + "@types/range-parser@*": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d" @@ -5727,6 +5731,13 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +query-string@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.1.0.tgz#01e7d69f6a0940dac67a937d6c6325647aa4532a" + dependencies: + decode-uri-component "^0.2.0" + strict-uri-encode "^2.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -6781,6 +6792,10 @@ stream-to@~0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d" +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"