diff --git a/client/App.tsx b/client/App.tsx index 5a0ad23..7585682 100644 --- a/client/App.tsx +++ b/client/App.tsx @@ -1,6 +1,6 @@ // import DevTools from "mobx-react-devtools"; import * as React from "react"; -import { Redirect, Route, Switch } from "react-router"; +import { Redirect, Route, Switch, withRouter } from "react-router"; import { Container } from "semantic-ui-react"; import { MessagesView, NavBar } from "@client/components"; @@ -30,7 +30,7 @@ function NavContainer() { ); } -export default function App() { +function App() { return ( @@ -39,3 +39,5 @@ export default function App() { ); } + +export default withRouter(App); diff --git a/client/components/DeviceView.tsx b/client/components/DeviceView.tsx index 4c701c3..ef68f5c 100644 --- a/client/components/DeviceView.tsx +++ b/client/components/DeviceView.tsx @@ -2,7 +2,7 @@ import * as classNames from "classnames"; import { observer } from "mobx-react"; import * as React from "react"; import { Link } from "react-router-dom"; -import { Grid, Header, Icon, Item, SemanticICONS, Dimmer, Segment } from "semantic-ui-react"; +import { Dimmer, Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react"; import { DeviceImage } from "@client/components"; import * as p from "@client/pages"; @@ -62,7 +62,7 @@ const ConnectionState = observer( } ); -interface DeviceViewProps { +interface DeviceViewProps extends RouteComponentProps { deviceId: number; appState: AppState; inList?: boolean; @@ -189,4 +189,4 @@ class DeviceView extends React.Component { } } -export default injectState(observer(DeviceView)); +export default withRouter(injectState(observer(DeviceView))); diff --git a/client/components/MessagesView.tsx b/client/components/MessagesView.tsx index b023ba5..786e4c6 100644 --- a/client/components/MessagesView.tsx +++ b/client/components/MessagesView.tsx @@ -30,7 +30,7 @@ class MessageView extends React.Component<{ if (message.onDismiss) { message.onDismiss(event, data); } - uiStore.messages.remove(message); + uiStore.removeMessage(message); }; } diff --git a/client/components/RunSectionForm.tsx b/client/components/RunSectionForm.tsx index 76fcfe9..fabf9ba 100644 --- a/client/components/RunSectionForm.tsx +++ b/client/components/RunSectionForm.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import * as React from "react"; -import { Form, Header, Icon, Segment, Popup } from "semantic-ui-react"; +import { Form, Header, Icon, Popup, Segment } from "semantic-ui-react"; import { DurationView, SectionChooser } from "@client/components"; import { UiStore } from "@client/state"; diff --git a/client/pages/DevicesPage.tsx b/client/pages/DevicesPage.tsx index 337ce8c..0357a14 100644 --- a/client/pages/DevicesPage.tsx +++ b/client/pages/DevicesPage.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import * as React from "react"; -import { Item, Button } from "semantic-ui-react"; +import { Button, Item } from "semantic-ui-react"; import { DeviceView } from "@client/components"; import { AppState, injectState } from "@client/state"; @@ -12,7 +12,7 @@ class DevicesPage extends React.Component<{ appState: AppState }> { render() { const { appState } = this.props; - const { userData } = appState.userStore; + const userData = appState.userStore.getUserData(); let deviceNodes: React.ReactNode; if (!userData) { deviceNodes = Not logged in; @@ -25,7 +25,7 @@ class DevicesPage extends React.Component<{ appState: AppState }> { } return ( -

Devices

+

Devices

{deviceNodes}
); diff --git a/client/pages/ProgramPage.tsx b/client/pages/ProgramPage.tsx index eb68da7..168468b 100644 --- a/client/pages/ProgramPage.tsx +++ b/client/pages/ProgramPage.tsx @@ -2,7 +2,7 @@ import { assign } from "lodash"; import { observer } from "mobx-react"; import * as qs from "query-string"; import * as React from "react"; -import { RouteComponentProps } from "react-router"; +import { RouteComponentProps, withRouter } from "react-router"; import { Button, CheckboxProps, @@ -252,8 +252,8 @@ class ProgramPage extends React.Component { @action.bound private close() { - const { deviceId } = this.props.match.params; - this.props.history.push({ pathname: route.device(deviceId), search: "" }); + // this.props.history.goBack(); + this.props.appState.history.goBack(); } @action.bound @@ -271,5 +271,5 @@ class ProgramPage extends React.Component { } } -const DecoratedProgramPage = injectState(observer(ProgramPage)); +const DecoratedProgramPage = injectState(withRouter(observer(ProgramPage))); export default DecoratedProgramPage; diff --git a/client/sprinklersRpc/WebSocketRpcClient.ts b/client/sprinklersRpc/WebSocketRpcClient.ts index 1125d77..3c8ddc8 100644 --- a/client/sprinklersRpc/WebSocketRpcClient.ts +++ b/client/sprinklersRpc/WebSocketRpcClient.ts @@ -191,11 +191,16 @@ export class WebSocketRpcClient extends s.SprinklersRPC { const id = this.nextRequestId++; return new Promise((resolve, reject) => { let timeoutHandle: number; - this.responseCallbacks[id] = response => { + this.responseCallbacks[id] = (response: ws.ServerResponse) => { clearTimeout(timeoutHandle); delete this.responseCallbacks[id]; if (response.result === "success") { - resolve(response.data); + if (response.method === method) { + resolve(response.data as ws.IServerResponseTypes[Method]); + } else { + reject(new s.RpcError("Response method does not match request method", ErrorCode.Internal, + { requestMethod: method, responseMethod: response.method })); + } } else { const { error } = response; reject(new s.RpcError(error.message, error.code, error.data)); diff --git a/client/state/AppState.ts b/client/state/AppState.ts index 5b3e6e8..089f3c7 100644 --- a/client/state/AppState.ts +++ b/client/state/AppState.ts @@ -47,7 +47,7 @@ export default class AppState extends TypedEventEmitter { async start() { configure({ - enforceActions: true + enforceActions: "observed" }); syncHistoryWithStore(this.history, this.routerStore); diff --git a/client/state/UserStore.ts b/client/state/UserStore.ts index 7b02cc5..8ad84d3 100644 --- a/client/state/UserStore.ts +++ b/client/state/UserStore.ts @@ -1,20 +1,24 @@ import { ISprinklersDevice, IUser } from "@common/httpApi"; -import { action, observable } from "mobx"; +import { action, IObservableValue, observable } from "mobx"; export class UserStore { - @observable - userData: IUser | null = null; + userData: IObservableValue = observable.box(null); @action.bound receiveUserData(userData: IUser) { - this.userData = userData; + this.userData.set(userData); + } + + getUserData(): IUser | null { + return this.userData.get(); } findDevice(id: number): ISprinklersDevice | null { + const userData = this.userData.get(); return ( - (this.userData && - this.userData.devices && - this.userData.devices.find(dev => dev.id === id)) || + (userData && + userData.devices && + userData.devices.find(dev => dev.id === id)) || null ); } diff --git a/client/state/reactContext.tsx b/client/state/reactContext.tsx index 77ecb6a..b41cc94 100644 --- a/client/state/reactContext.tsx +++ b/client/state/reactContext.tsx @@ -31,12 +31,6 @@ export function ConsumeState({ children }: ConsumeStateProps) { return {consumeState}; } -type Diff< - T extends string | number | symbol, - U extends string | number | symbol -> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]; -type Omit = { [P in Diff]: T[P] }; - export function injectState

( Component: React.ComponentType

): React.ComponentClass> { @@ -48,7 +42,9 @@ export function injectState

( "Component with injectState must be mounted inside ProvideState" ); } - return ; + // tslint:disable-next-line:no-object-literal-type-assertion + const allProps: Readonly

= {...this.props, appState: state} as Readonly

; + return ; }; return {consumeState}; } diff --git a/client/styles/DeviceView.scss b/client/styles/DeviceView.scss index 878a383..5dc6aed 100644 --- a/client/styles/DeviceView.scss +++ b/client/styles/DeviceView.scss @@ -81,3 +81,10 @@ $connected-color: #13d213; .ui.modal.programEditor > .header > .header.item .inline.fields { margin-bottom: 0; } + +.runSectionForm-runButton { + display: inline-block; + &, .ui.disabled.button { + pointer-events: auto !important; + } +} diff --git a/client/styles/DurationView.scss b/client/styles/DurationView.scss index 1dc7db9..91e4cef 100644 --- a/client/styles/DurationView.scss +++ b/client/styles/DurationView.scss @@ -4,7 +4,7 @@ $durationInput-labelWidth: 2.5em; .field .durationInputs { display: flex; // max-width: 100%; - justify-content: start; + justify-content: flex-start; flex-wrap: wrap; margin: -$durationInput-spacing / 2; diff --git a/client/styles/ProgramSequenceView.scss b/client/styles/ProgramSequenceView.scss index 16c8120..50fd59a 100644 --- a/client/styles/ProgramSequenceView.scss +++ b/client/styles/ProgramSequenceView.scss @@ -8,12 +8,16 @@ .programSequence-item { list-style-type: none; display: flex; + align-items: center; margin-bottom: 0.5em; &.dragging { z-index: 1010; } .fields { - margin: 0em 0em 1em !important; + display: flex; + align-items: center; + margin: 0em !important; + padding: 0em !important; } .ui.icon.button { height: fit-content; diff --git a/common/jsonRpc/index.ts b/common/jsonRpc/index.ts index 5dd80c5..3845055 100644 --- a/common/jsonRpc/index.ts +++ b/common/jsonRpc/index.ts @@ -133,7 +133,7 @@ export type IResponseHandler< ResponseTypes, ErrorType, Method extends keyof ResponseTypes = keyof ResponseTypes -> = (response: ResponseData) => void; +> = (response: Response) => void; export interface ResponseHandlers< ResponseTypes = DefaultResponseTypes, diff --git a/common/sprinklersRpc/mqtt/index.ts b/common/sprinklersRpc/mqtt/index.ts index 18dc534..74c7434 100644 --- a/common/sprinklersRpc/mqtt/index.ts +++ b/common/sprinklersRpc/mqtt/index.ts @@ -218,7 +218,7 @@ class MqttSprinklersDevice extends s.SprinklersDevice { } doUnsubscribe() { - this.apiClient.client.unsubscribe(this.subscriptions, err => { + this.apiClient.client.unsubscribe(this.subscriptions, (err: Error | undefined) => { if (err) { log.error({ err, id: this.id }, "error unsubscribing to device"); } else { diff --git a/common/sprinklersRpc/schema/requests.ts b/common/sprinklersRpc/schema/requests.ts index 2e2c7b5..2cfe916 100644 --- a/common/sprinklersRpc/schema/requests.ts +++ b/common/sprinklersRpc/schema/requests.ts @@ -36,8 +36,8 @@ export const updateProgram: ModelSchema< deserializer: (json, done) => { done(null, json); }, - beforeDeserialize: () => {}, - afterDeserialize: () => {}, + beforeDeserialize: undefined as any, + afterDeserialize: undefined as any, } }); diff --git a/server/Database.ts b/server/Database.ts index 98ca307..cf3f861 100644 --- a/server/Database.ts +++ b/server/Database.ts @@ -47,7 +47,7 @@ export class Database { const users: User[] = []; for (let i = 0; i < NUM; i++) { const username = "alex" + i; - let user = await this.users.findByUsername(username); + let user = await this.users.findByUsername(username, { devices: true }); if (!user) { user = await this.users.create({ name: "Alex Mikhalev" + i, @@ -74,7 +74,10 @@ export class Database { for (let j = 0; j < 5; j++) { const userIdx = (i + j * 10) % NUM; const user = users[userIdx]; - user.devices = (user.devices || []).concat([device]); + if (!user.devices) { + user.devices = []; + } + user.devices.push(device); } } await this.sprinklersDevices.save(devices); diff --git a/server/commands/device.ts b/server/commands/device.ts index c114a74..59443bf 100644 --- a/server/commands/device.ts +++ b/server/commands/device.ts @@ -70,7 +70,7 @@ export default class DeviceCommand extends ManageCommand { } getFindConditions(flags: DeviceFlags, action: Action): FindConditions { - let whereClause: FindConditions = {}; + const whereClause: FindConditions = {}; if (flags.id) { whereClause.id = flags.id; } @@ -125,7 +125,7 @@ export default class DeviceCommand extends ManageCommand { query = query.where("user.username = :username", { username: flags.username }); } const devices = await query.getMany(); - if (devices.length == 0) { + if (devices.length === 0) { this.log("No sprinklers devices found"); return 1; } @@ -146,7 +146,7 @@ export default class DeviceCommand extends ManageCommand { if (!user) { return this.error(`Could not find user with username '${flags.username}'`); } - let query = this.database.sprinklersDevices.createQueryBuilder() + const query = this.database.sprinklersDevices.createQueryBuilder() .relation("users") .of(device); if (flags["add-user"]) { diff --git a/server/express/api/devices.ts b/server/express/api/devices.ts index ec3fdc6..a19d13b 100644 --- a/server/express/api/devices.ts +++ b/server/express/api/devices.ts @@ -108,6 +108,12 @@ export function devices(state: ServerState) { async (req, res) => { const token: DeviceToken = req.token! as any; const deviceId = token.aud; + const devs = await state.database.sprinklersDevices.count({ + deviceId + }); + if (devs === 0) { + throw new ApiError("deviceId not found", ErrorCode.NotFound); + } const clientId = `device-${deviceId}`; res.send({ mqttUrl: state.mqttUrl,