Keep track of editing state with query string; allow deleting of program items
This commit is contained in:
parent
116c803ec7
commit
4a5b5ae7d9
@ -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(() => <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<{
|
||||
}
|
||||
|
||||
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<{
|
||||
}
|
||||
|
||||
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<{
|
||||
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) {
|
||||
|
@ -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 = (
|
||||
<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<{
|
||||
<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>
|
||||
|
@ -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}
|
||||
>
|
||||
<Icon name="play"/>
|
||||
Run
|
||||
</Form.Button>
|
||||
</Form>
|
||||
|
@ -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<{
|
||||
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>
|
||||
|
@ -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";
|
||||
|
||||
interface ProgramPageProps extends RouteComponentProps<{ deviceId: string, programId: string }> {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
@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,
|
||||
};
|
||||
}
|
||||
|
||||
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<{
|
||||
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<{
|
||||
} else {
|
||||
editButtons = (
|
||||
<Button primary onClick={this.startEditing}>
|
||||
<Icon name="edit"/>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
const stopStartButton = (
|
||||
<Button onClick={this.cancelOrRun} positive={!running} negative={running}>
|
||||
<Icon name={running ? "stop" : "play"} />
|
||||
{running ? "Stop" : "Run"}
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Modal.Actions>
|
||||
<Button positive={!running} negative={running} onClick={this.cancelOrRun}>
|
||||
{running ? "Stop" : "Run"}
|
||||
</Button>
|
||||
{stopStartButton}
|
||||
{editButtons}
|
||||
<Button onClick={this.close}>
|
||||
<Icon name="arrow left" />
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
@ -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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
15
yarn.lock
15
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user