From 2871e22a613c93f9c3d7b148e1dce8f7b209a6f9 Mon Sep 17 00:00:00 2001 From: Jannis Mattheis Date: Sun, 21 Oct 2018 13:13:17 +0200 Subject: [PATCH] Migrate AppStore to mobx --- ui/src/actions/AppAction.ts | 53 ------------------------ ui/src/component/Navigation.tsx | 22 ++-------- ui/src/pages/Applications.tsx | 59 +++++++++++---------------- ui/src/stores/AppStore.ts | 72 +++++++++++++++++++-------------- 4 files changed, 68 insertions(+), 138 deletions(-) delete mode 100644 ui/src/actions/AppAction.ts diff --git a/ui/src/actions/AppAction.ts b/ui/src/actions/AppAction.ts deleted file mode 100644 index c482518..0000000 --- a/ui/src/actions/AppAction.ts +++ /dev/null @@ -1,53 +0,0 @@ -import axios, {AxiosResponse} from 'axios'; -import * as config from '../config'; -import dispatcher from '../stores/dispatcher'; -import {snack} from './GlobalAction'; - -/** Fetches all applications. */ -export function fetchApps() { - axios.get(config.get('url') + 'application').then((resp: AxiosResponse) => { - dispatcher.dispatch({type: 'UPDATE_APPS', payload: resp.data}); - }); -} - -/** - * Delete an application by id. - * @param {int} id the application id - */ -export function deleteApp(id: number) { - axios - .delete(config.get('url') + 'application/' + id) - .then(() => { - fetchApps(); - dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id}); - }) - .then(() => snack('Application deleted')); -} - -/** - * Create an application. - * @param {string} name the application name - * @param {string} description the description of the application. - */ -export function createApp(name: string, description: string) { - axios - .post(config.get('url') + 'application', {name, description}) - .then(fetchApps) - .then(() => snack('Application created')); -} - -/** - * Upload an image for an application. - * @param {int} id the application id - * @param {Blob} file the description of the application. - */ -export function uploadImage(id: number, file: Blob) { - const formData = new FormData(); - formData.append('file', file); - axios - .post(config.get('url') + 'application/' + id + '/image', formData, { - headers: {'content-type': 'multipart/form-data'}, - }) - .then(fetchApps) - .then(() => snack('Application image updated')); -} diff --git a/ui/src/component/Navigation.tsx b/ui/src/component/Navigation.tsx index 692b134..4ad4fce 100644 --- a/ui/src/component/Navigation.tsx +++ b/ui/src/component/Navigation.tsx @@ -6,6 +6,7 @@ import {StyleRules, Theme, WithStyles, withStyles} from '@material-ui/core/style import React, {Component} from 'react'; import {Link} from 'react-router-dom'; import AppStore from '../stores/AppStore'; +import {observer} from 'mobx-react'; const styles = (theme: Theme): StyleRules<'drawerPaper' | 'toolbar' | 'link'> => ({ drawerPaper: { @@ -27,24 +28,11 @@ interface IProps { loggedIn: boolean; } -interface IState { - apps: IApplication[]; -} - -class Navigation extends Component { - public state: IState = {apps: []}; - - public componentWillMount() { - AppStore.on('change', this.updateApps); - } - - public componentWillUnmount() { - AppStore.removeListener('change', this.updateApps); - } - +@observer +class Navigation extends Component { public render() { const {classes, loggedIn} = this.props; - const {apps} = this.state; + const apps = AppStore.getItems(); const userApps = apps.length === 0 @@ -88,8 +76,6 @@ class Navigation extends Component { ); } - - private updateApps = () => this.setState({apps: AppStore.get()}); } export default withStyles(styles, {withTheme: true})(Navigation); diff --git a/ui/src/pages/Applications.tsx b/ui/src/pages/Applications.tsx index 91eebbc..4aaea65 100644 --- a/ui/src/pages/Applications.tsx +++ b/ui/src/pages/Applications.tsx @@ -10,42 +10,36 @@ import TableRow from '@material-ui/core/TableRow'; import Delete from '@material-ui/icons/Delete'; import Edit from '@material-ui/icons/Edit'; import React, {ChangeEvent, Component, SFC} from 'react'; -import * as AppAction from '../actions/AppAction'; import ConfirmDialog from '../component/ConfirmDialog'; import DefaultPage from '../component/DefaultPage'; import ToggleVisibility from '../component/ToggleVisibility'; -import AppStore from '../stores/AppStore'; import AddApplicationDialog from './dialog/AddApplicationDialog'; +import AppStore from '../stores/AppStore'; +import {observer} from 'mobx-react'; +import {observable} from 'mobx'; -interface IState { - apps: IApplication[]; - createDialog: boolean; - deleteId: number; -} +@observer +class Applications extends Component { + @observable + private deleteId: number | false = false; + @observable + private createDialog = false; -class Applications extends Component<{}, IState> { - public state = {apps: [], createDialog: false, deleteId: -1}; private uploadId = -1; private upload: HTMLInputElement | null = null; - public componentWillMount() { - AppStore.on('change', this.updateApps); - this.updateApps(); - } - - public componentWillUnmount() { - AppStore.removeListener('change', this.updateApps); - } + public componentDidMount = AppStore.refresh; public render() { - const {apps, createDialog, deleteId} = this.state; + const {createDialog, deleteId} = this; + const apps = AppStore.getItems(); return ( + fButton={() => (this.createDialog = true)}> @@ -68,7 +62,7 @@ class Applications extends Component<{}, IState> { name={app.name} value={app.token} fUpload={() => this.uploadImage(app.id)} - fDelete={() => this.showCloseDialog(app.id)} + fDelete={() => (this.deleteId = app.id)} /> ); })} @@ -84,23 +78,22 @@ class Applications extends Component<{}, IState> { {createDialog && ( (this.createDialog = false)} + fOnSubmit={AppStore.create} /> )} - {deleteId !== -1 && ( + {deleteId !== false && ( AppAction.deleteApp(deleteId)} + text={'Delete ' + AppStore.getByID(deleteId).name + '?'} + fClose={() => (this.deleteId = false)} + fOnSubmit={() => AppStore.remove(deleteId)} /> )} ); } - private updateApps = () => this.setState({...this.state, apps: AppStore.get()}); private uploadImage = (id: number) => { this.uploadId = id; if (this.upload) { @@ -114,17 +107,11 @@ class Applications extends Component<{}, IState> { return; } if (['image/png', 'image/jpeg', 'image/gif'].indexOf(file.type) !== -1) { - AppAction.uploadImage(this.uploadId, file); + AppStore.uploadImage(this.uploadId, file); } else { alert('Uploaded file must be of type png, jpeg or gif.'); } }; - private showCreateDialog = () => this.setState({...this.state, createDialog: true}); - - private hideCreateDialog = () => this.setState({...this.state, createDialog: false}); - private showCloseDialog = (deleteId: number) => this.setState({...this.state, deleteId}); - - private hideCloseDialog = () => this.setState({...this.state, deleteId: -1}); } interface IRowProps { @@ -136,7 +123,7 @@ interface IRowProps { fDelete: VoidFunction; } -const Row: SFC = ({name, value, description, fDelete, fUpload, image}) => ( +const Row: SFC = observer(({name, value, description, fDelete, fUpload, image}) => (
@@ -157,6 +144,6 @@ const Row: SFC = ({name, value, description, fDelete, fUpload, image} -); +)); export default Applications; diff --git a/ui/src/stores/AppStore.ts b/ui/src/stores/AppStore.ts index 6bf57af..8b49a37 100644 --- a/ui/src/stores/AppStore.ts +++ b/ui/src/stores/AppStore.ts @@ -1,38 +1,48 @@ -import {EventEmitter} from 'events'; -import dispatcher, {IEvent} from './dispatcher'; +import {BaseStore} from './BaseStore'; +import axios from 'axios'; +import * as config from '../config'; +import {action} from 'mobx'; +import SnackManager, {SnackReporter} from './SnackManager'; -class AppStore extends EventEmitter { - private apps: IApplication[] = []; - - public get(): IApplication[] { - return this.apps; +class NewAppStore extends BaseStore { + public constructor(private readonly snack: SnackReporter) { + super(); } - public getById(id: number): IApplication { - const app = this.getByIdOrUndefined(id); - if (!app) { - throw new Error('app is required to exist'); - } - return app; - } + protected requestItems = (): Promise => { + return axios + .get(`${config.get('url')}application`) + .then((response) => response.data); + }; - public getName(id: number): string { - const app = this.getByIdOrUndefined(id); + protected requestDelete = (id: number): Promise => { + return axios + .delete(`${config.get('url')}application/${id}`) + .then(() => this.snack('Application deleted')); + }; + + @action + public uploadImage = async (id: number, file: Blob): Promise => { + const formData = new FormData(); + formData.append('file', file); + await axios.post(`${config.get('url')}application/${id}/image`, formData, { + headers: {'content-type': 'multipart/form-data'}, + }); + await this.refresh(); + this.snack('Application image updated'); + }; + + @action + public create = async (name: string, description: string): Promise => { + await axios.post(`${config.get('url')}application`, {name, description}); + await this.refresh(); + this.snack('Application created'); + }; + + public getName = (id: number): string => { + const app = this.getByIDOrUndefined(id); return id === -1 ? 'All Messages' : app !== undefined ? app.name : 'unknown'; - } - - public handle(data: IEvent): void { - if (data.type === 'UPDATE_APPS') { - this.apps = data.payload; - this.emit('change'); - } - } - - private getByIdOrUndefined(id: number): IApplication | undefined { - return this.apps.find((a) => a.id === id); - } + }; } -const store = new AppStore(); -dispatcher.register(store.handle.bind(store)); -export default store; +export default new NewAppStore(SnackManager.snack);