Browse Source

Keep track of editing state with query string; allow deleting of program items

update-deps
Alex Mikhalev 7 years ago
parent
commit
4a5b5ae7d9
  1. 67
      app/components/ProgramSequenceView.tsx
  2. 17
      app/components/ProgramTable.tsx
  3. 3
      app/components/RunSectionForm.tsx
  4. 4
      app/components/SectionRunnerView.tsx
  5. 110
      app/pages/ProgramPage.tsx
  6. 7
      app/styles/ProgramSequenceView.scss
  7. 2
      package.json
  8. 15
      yarn.lock

67
app/components/ProgramSequenceView.tsx

@ -1,9 +1,8 @@ @@ -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"; @@ -11,21 +10,31 @@ import { ProgramItem, Section } from "@common/sprinklersRpc";
import "@app/styles/ProgramSequenceView";
const Handle = SortableHandle(() => <Icon name="bars"/>);
type ItemChangeHandler = (index: number, newItem: ProgramItem) => void;
type ItemRemoveHandler = (index: number) => void;
const Handle = SortableHandle(() => <Button basic icon><Icon name="bars"/></Button>);
@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 (
<Form.Group>
<Button icon negative onClick={this.onRemove}>
<Icon name="cancel" />
</Button>
<SectionChooser
label="Section"
sections={sections}
@ -50,7 +59,7 @@ class ProgramSequenceItem extends React.Component<{ @@ -50,7 +59,7 @@ class ProgramSequenceItem extends React.Component<{
}
render() {
const editing = this.props.onChange != null;
const { editing }= this.props;
return (
<li className="programSequence-item ui form">
{editing ? <Handle /> : <List.Icon name="caret right"/>}
@ -60,44 +69,43 @@ class ProgramSequenceItem extends React.Component<{ @@ -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 (
<ProgramSequenceItemD
sequenceItem={item}
sections={sections}
{...rest}
key={key}
sequenceItem={item}
index={index}
onChange={onChangeHandler}
idx={index}
sections={sections}
/>
);
});
@ -112,23 +120,28 @@ class ProgramSequenceView extends React.Component<{ @@ -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 (
<ProgramSequenceList
className={className}
useDragHandle
list={sequence}
sections={sections}
onChange={onChange}
editing={editing}
onChange={this.changeItem}
onRemove={this.removeItem}
onSortEnd={this.onSortEnd}
/>
);
}
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) {

17
app/components/ProgramTable.tsx

@ -2,7 +2,7 @@ import { observer } from "mobx-react"; @@ -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<{ @@ -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 = (
<Button onClick={this.cancelOrRun} {...buttonStyle} positive={!running} negative={running}>
<Icon name={running ? "stop" : "play"} />
{running ? "Stop" : "Run"}
</Button>
);
const mainRow = (
<Table.Row>
<Table.Cell className="program--number">{"" + program.id}</Table.Cell>
@ -32,13 +39,13 @@ class ProgramRows extends React.Component<{ @@ -32,13 +39,13 @@ class ProgramRows extends React.Component<{
<span>{running ? "Running" : "Not running"}</span>
</Table.Cell>
<Table.Cell>
<Button onClick={this.cancelOrRun} {...buttonStyle} positive={!running} negative={running}>
{running ? "Stop" : "Run"}
</Button>
{stopStartButton}
<Button as={Link} to={detailUrl} {...buttonStyle} primary>
<Icon name="edit"/>
Open
</Button>
<Button onClick={this.toggleExpanded} {...buttonStyle}>
<Icon name="list"/>
{expanded ? "Hide Details" : "Show Details"}
</Button>
</Table.Cell>

3
app/components/RunSectionForm.tsx

@ -1,6 +1,6 @@ @@ -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<{ @@ -47,6 +47,7 @@ export default class RunSectionForm extends React.Component<{
onClick={this.run}
disabled={!this.isValid}
>
<Icon name="play"/>
Run
</Form.Button>
</Form>

4
app/components/SectionRunnerView.tsx

@ -21,7 +21,7 @@ function PausedState({ paused, togglePaused }: PausedStateProps) { @@ -21,7 +21,7 @@ function PausedState({ paused, togglePaused }: PausedStateProps) {
"sectionRunner--pausedState-unpaused": !paused,
});
return (
<Button className={classes} size="tiny" onClick={togglePaused}>
<Button className={classes} size="medium" onClick={togglePaused}>
<Icon name={paused ? "pause" : "play"}/>
{paused ? "Paused" : "Processing"}
</Button>
@ -137,7 +137,7 @@ export default class SectionRunnerView extends React.Component<{ @@ -137,7 +137,7 @@ export default class SectionRunnerView extends React.Component<{
return (
<Segment className="sectionRunner">
<div style={{ display: "flex", alignContent: "baseline" }}>
<h4 style={{ marginBottom: 0 }}>Section Runner Queue</h4>
<h3 style={{ marginBottom: 0 }}>Section Runner Queue</h3>
<div className="flex-spacer"/>
<PausedState paused={paused} togglePaused={this.togglePaused}/>
</div>

110
app/pages/ProgramPage.tsx

@ -1,8 +1,9 @@ @@ -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"; @@ -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<Program> | 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<ProgramPageProps> {
get isEditing(): boolean {
return this.state.programView != null;
return qs.parse(this.props.location.search).editing != null;
}
device!: SprinklersDevice;
program!: Program;
programView: Program & IViewModel<Program> | null = null;
renderName(program: Program) {
const { name } = program;
if (this.isEditing) {
@ -64,9 +59,11 @@ class ProgramPage extends React.Component<{ @@ -64,9 +59,11 @@ class ProgramPage extends React.Component<{
editButtons = (
<React.Fragment>
<Button primary onClick={this.save}>
<Icon name="save" />
Save
</Button>
<Button negative onClick={this.cancelEditing}>
<Button negative onClick={this.stopEditing}>
<Icon name="cancel" />
Cancel
</Button>
</React.Fragment>
@ -74,17 +71,23 @@ class ProgramPage extends React.Component<{ @@ -74,17 +71,23 @@ class ProgramPage extends React.Component<{
} else {
editButtons = (
<Button primary onClick={this.startEditing}>
<Icon name="edit"/>
Edit
</Button>
);
}
return (
<Modal.Actions>
<Button positive={!running} negative={running} onClick={this.cancelOrRun}>
const stopStartButton = (
<Button onClick={this.cancelOrRun} positive={!running} negative={running}>
<Icon name={running ? "stop" : "play"} />
{running ? "Stop" : "Run"}
</Button>
);
return (
<Modal.Actions>
{stopStartButton}
{editButtons}
<Button onClick={this.close}>
<Icon name="arrow left" />
Close
</Button>
</Modal.Actions>
@ -92,17 +95,32 @@ class ProgramPage extends React.Component<{ @@ -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]) {
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;
}
this.program = device.programs[programId];
}
if (this.isEditing) {
if (this.programView == null && this.program) {
this.programView = createViewModel(this.program);
}
} else {
if (this.programView != null) {
this.programView = null;
}
}
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<{ @@ -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<{ @@ -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!;
}
}
}

7
app/styles/ProgramSequenceView.scss

@ -10,8 +10,11 @@ @@ -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;

2
package.json

@ -68,6 +68,7 @@ @@ -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 @@ @@ -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",

15
yarn.lock

@ -111,6 +111,10 @@ @@ -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: @@ -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: @@ -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"

Loading…
Cancel
Save