aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/layout/AppLayout.js4
-rw-r--r--src/components/services/content/ServiceView.js6
-rw-r--r--src/components/ui/AppLoader/index.js4
-rw-r--r--src/containers/layout/AppLayoutContainer.js4
-rw-r--r--src/containers/settings/SettingsWindow.js4
-rw-r--r--src/features/workspaces/api.js51
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js11
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js4
-rw-r--r--src/features/workspaces/containers/EditWorkspaceScreen.js8
-rw-r--r--src/features/workspaces/containers/WorkspacesScreen.js7
-rw-r--r--src/features/workspaces/index.js51
-rw-r--r--src/features/workspaces/state.js18
-rw-r--r--src/features/workspaces/store.js196
-rw-r--r--src/lib/Menu.js6
-rw-r--r--src/stores/ServicesStore.js6
15 files changed, 199 insertions, 181 deletions
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 4dd5ff686..0c72c1413 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -14,8 +14,8 @@ import ErrorBoundary from '../util/ErrorBoundary';
14// import globalMessages from '../../i18n/globalMessages'; 14// import globalMessages from '../../i18n/globalMessages';
15 15
16import { isWindows } from '../../environment'; 16import { isWindows } from '../../environment';
17import { workspacesState } from '../../features/workspaces/state';
18import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; 17import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator';
18import { workspaceStore } from '../../features/workspaces';
19 19
20function createMarkup(HTMLString) { 20function createMarkup(HTMLString) {
21 return { __html: HTMLString }; 21 return { __html: HTMLString };
@@ -53,7 +53,7 @@ const styles = theme => ({
53 width: `calc(100% + ${theme.workspaceDrawerWidth}px)`, 53 width: `calc(100% + ${theme.workspaceDrawerWidth}px)`,
54 transition: 'transform 0.5s ease', 54 transition: 'transform 0.5s ease',
55 transform() { 55 transform() {
56 return workspacesState.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaceDrawerWidth}px)`; 56 return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaceDrawerWidth}px)`;
57 }, 57 },
58 }, 58 },
59}); 59});
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index ada920cb6..13148b9b3 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -35,12 +35,13 @@ export default @observer class ServiceView extends Component {
35 35
36 autorunDisposer = null; 36 autorunDisposer = null;
37 37
38 forceRepaintTimeout = null;
39
38 componentDidMount() { 40 componentDidMount() {
39 this.autorunDisposer = autorun(() => { 41 this.autorunDisposer = autorun(() => {
40 if (!this.isMounted) return;
41 if (this.props.service.isActive) { 42 if (this.props.service.isActive) {
42 this.setState({ forceRepaint: true }); 43 this.setState({ forceRepaint: true });
43 setTimeout(() => { 44 this.forceRepaintTimeout = setTimeout(() => {
44 this.setState({ forceRepaint: false }); 45 this.setState({ forceRepaint: false });
45 }, 100); 46 }, 100);
46 } 47 }
@@ -49,6 +50,7 @@ export default @observer class ServiceView extends Component {
49 50
50 componentWillUnmount() { 51 componentWillUnmount() {
51 this.autorunDisposer(); 52 this.autorunDisposer();
53 clearTimeout(this.forceRepaintTimeout);
52 } 54 }
53 55
54 updateTargetUrl = (event) => { 56 updateTargetUrl = (event) => {
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js
index 61053f6d1..b0c7fed7b 100644
--- a/src/components/ui/AppLoader/index.js
+++ b/src/components/ui/AppLoader/index.js
@@ -23,11 +23,11 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component
23 static propTypes = { 23 static propTypes = {
24 classes: PropTypes.object.isRequired, 24 classes: PropTypes.object.isRequired,
25 theme: PropTypes.object.isRequired, 25 theme: PropTypes.object.isRequired,
26 } 26 };
27 27
28 state = { 28 state = {
29 step: 0, 29 step: 0,
30 } 30 };
31 31
32 interval = null; 32 interval = null;
33 33
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index 772458eab..4329c3097 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -20,9 +20,9 @@ import Services from '../../components/services/content/Services';
20import AppLoader from '../../components/ui/AppLoader'; 20import AppLoader from '../../components/ui/AppLoader';
21 21
22import { state as delayAppState } from '../../features/delayApp'; 22import { state as delayAppState } from '../../features/delayApp';
23import { workspacesState } from '../../features/workspaces/state';
24import { workspaceActions } from '../../features/workspaces/actions'; 23import { workspaceActions } from '../../features/workspaces/actions';
25import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer'; 24import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer';
25import { workspaceStore } from '../../features/workspaces';
26 26
27export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { 27export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component {
28 static defaultProps = { 28 static defaultProps = {
@@ -108,7 +108,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
108 updateService={updateService} 108 updateService={updateService}
109 toggleMuteApp={toggleMuteApp} 109 toggleMuteApp={toggleMuteApp}
110 toggleWorkspaceDrawer={workspaceActions.toggleWorkspaceDrawer} 110 toggleWorkspaceDrawer={workspaceActions.toggleWorkspaceDrawer}
111 isWorkspaceDrawerOpen={workspacesState.isWorkspaceDrawerOpen} 111 isWorkspaceDrawerOpen={workspaceStore.isWorkspaceDrawerOpen}
112 showMessageBadgeWhenMutedSetting={settings.all.app.showMessageBadgeWhenMuted} 112 showMessageBadgeWhenMutedSetting={settings.all.app.showMessageBadgeWhenMuted}
113 showMessageBadgesEvenWhenMuted={ui.showMessageBadgesEvenWhenMuted} 113 showMessageBadgesEvenWhenMuted={ui.showMessageBadgesEvenWhenMuted}
114 /> 114 />
diff --git a/src/containers/settings/SettingsWindow.js b/src/containers/settings/SettingsWindow.js
index 8cbde24c1..663b9e2e4 100644
--- a/src/containers/settings/SettingsWindow.js
+++ b/src/containers/settings/SettingsWindow.js
@@ -7,7 +7,7 @@ import ServicesStore from '../../stores/ServicesStore';
7import Layout from '../../components/settings/SettingsLayout'; 7import Layout from '../../components/settings/SettingsLayout';
8import Navigation from '../../components/settings/navigation/SettingsNavigation'; 8import Navigation from '../../components/settings/navigation/SettingsNavigation';
9import ErrorBoundary from '../../components/util/ErrorBoundary'; 9import ErrorBoundary from '../../components/util/ErrorBoundary';
10import { workspacesState } from '../../features/workspaces/state'; 10import { workspaceStore } from '../../features/workspaces';
11 11
12export default @inject('stores', 'actions') @observer class SettingsContainer extends Component { 12export default @inject('stores', 'actions') @observer class SettingsContainer extends Component {
13 render() { 13 render() {
@@ -17,7 +17,7 @@ export default @inject('stores', 'actions') @observer class SettingsContainer ex
17 const navigation = ( 17 const navigation = (
18 <Navigation 18 <Navigation
19 serviceCount={stores.services.all.length} 19 serviceCount={stores.services.all.length}
20 workspaceCount={workspacesState.workspaces.length} 20 workspaceCount={workspaceStore.workspaces.length}
21 /> 21 />
22 ); 22 );
23 23
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js
index 733cb5593..4e076d233 100644
--- a/src/features/workspaces/api.js
+++ b/src/features/workspaces/api.js
@@ -1,39 +1,60 @@
1import { pick } from 'lodash'; 1import { pick } from 'lodash';
2import { sendAuthRequest } from '../../api/utils/auth'; 2import { sendAuthRequest } from '../../api/utils/auth';
3import { API, API_VERSION } from '../../environment'; 3import { API, API_VERSION } from '../../environment';
4import Request from '../../stores/lib/Request';
5import CachedRequest from '../../stores/lib/CachedRequest';
6import Workspace from './models/Workspace';
4 7
5export default { 8const debug = require('debug')('Franz:feature:workspaces:api');
9
10export const workspaceApi = {
6 getUserWorkspaces: async () => { 11 getUserWorkspaces: async () => {
7 const url = `${API}/${API_VERSION}/workspace`; 12 const url = `${API}/${API_VERSION}/workspace`;
8 const request = await sendAuthRequest(url, { method: 'GET' }); 13 debug('getUserWorkspaces GET', url);
9 if (!request.ok) throw request; 14 const result = await sendAuthRequest(url, { method: 'GET' });
10 return request.json(); 15 debug('getUserWorkspaces RESULT', result);
16 if (!result.ok) throw result;
17 const workspaces = await result.json();
18 return workspaces.map(data => new Workspace(data));
11 }, 19 },
12 20
13 createWorkspace: async (name) => { 21 createWorkspace: async (name) => {
14 const url = `${API}/${API_VERSION}/workspace`; 22 const url = `${API}/${API_VERSION}/workspace`;
15 const request = await sendAuthRequest(url, { 23 const options = {
16 method: 'POST', 24 method: 'POST',
17 body: JSON.stringify({ name }), 25 body: JSON.stringify({ name }),
18 }); 26 };
19 if (!request.ok) throw request; 27 debug('createWorkspace POST', url, options);
20 return request.json(); 28 const result = await sendAuthRequest(url, options);
29 debug('createWorkspace RESULT', result);
30 if (!result.ok) throw result;
31 return new Workspace(await result.json());
21 }, 32 },
22 33
23 deleteWorkspace: async (workspace) => { 34 deleteWorkspace: async (workspace) => {
24 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; 35 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`;
25 const request = await sendAuthRequest(url, { method: 'DELETE' }); 36 debug('deleteWorkspace DELETE', url);
26 if (!request.ok) throw request; 37 const result = await sendAuthRequest(url, { method: 'DELETE' });
27 return request.json(); 38 debug('deleteWorkspace RESULT', result);
39 if (!result.ok) throw result;
40 return (await result.json()).deleted;
28 }, 41 },
29 42
30 updateWorkspace: async (workspace) => { 43 updateWorkspace: async (workspace) => {
31 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; 44 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`;
32 const request = await sendAuthRequest(url, { 45 const options = {
33 method: 'PUT', 46 method: 'PUT',
34 body: JSON.stringify(pick(workspace, ['name', 'services'])), 47 body: JSON.stringify(pick(workspace, ['name', 'services'])),
35 }); 48 };
36 if (!request.ok) throw request; 49 debug('updateWorkspace UPDATE', url, options);
37 return request.json(); 50 const result = await sendAuthRequest(url, options);
51 debug('updateWorkspace RESULT', result);
52 if (!result.ok) throw result;
53 return new Workspace(await result.json());
38 }, 54 },
39}; 55};
56
57export const getUserWorkspacesRequest = new CachedRequest(workspaceApi, 'getUserWorkspaces');
58export const createWorkspaceRequest = new Request(workspaceApi, 'createWorkspace');
59export const deleteWorkspaceRequest = new Request(workspaceApi, 'deleteWorkspace');
60export const updateWorkspaceRequest = new Request(workspaceApi, 'updateWorkspace');
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index c18eb0e11..6dc779be9 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -6,9 +6,9 @@ import { defineMessages, intlShape } from 'react-intl';
6import { H1, Icon } from '@meetfranz/ui'; 6import { H1, Icon } from '@meetfranz/ui';
7import ReactTooltip from 'react-tooltip'; 7import ReactTooltip from 'react-tooltip';
8 8
9import { workspacesState } from '../state';
10import WorkspaceDrawerItem from './WorkspaceDrawerItem'; 9import WorkspaceDrawerItem from './WorkspaceDrawerItem';
11import { workspaceActions } from '../actions'; 10import { workspaceActions } from '../actions';
11import { workspaceStore } from '../index';
12 12
13const messages = defineMessages({ 13const messages = defineMessages({
14 headline: { 14 headline: {
@@ -70,7 +70,12 @@ class WorkspaceDrawer extends Component {
70 getServicesForWorkspace, 70 getServicesForWorkspace,
71 } = this.props; 71 } = this.props;
72 const { intl } = this.context; 72 const { intl } = this.context;
73 const { activeWorkspace, isSwitchingWorkspace, nextWorkspace } = workspacesState; 73 const {
74 activeWorkspace,
75 isSwitchingWorkspace,
76 nextWorkspace,
77 workspaces,
78 } = workspaceStore;
74 const actualWorkspace = isSwitchingWorkspace ? nextWorkspace : activeWorkspace; 79 const actualWorkspace = isSwitchingWorkspace ? nextWorkspace : activeWorkspace;
75 return ( 80 return (
76 <div className={classes.drawer}> 81 <div className={classes.drawer}>
@@ -95,7 +100,7 @@ class WorkspaceDrawer extends Component {
95 services={getServicesForWorkspace(null)} 100 services={getServicesForWorkspace(null)}
96 isActive={actualWorkspace == null} 101 isActive={actualWorkspace == null}
97 /> 102 />
98 {workspacesState.workspaces.map(workspace => ( 103 {workspaces.map(workspace => (
99 <WorkspaceDrawerItem 104 <WorkspaceDrawerItem
100 key={workspace.id} 105 key={workspace.id}
101 name={workspace.name} 106 name={workspace.name}
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
index 68ce6fd87..c012ab008 100644
--- a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -5,8 +5,8 @@ import injectSheet from 'react-jss';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6import { defineMessages, intlShape } from 'react-intl'; 6import { defineMessages, intlShape } from 'react-intl';
7 7
8import { workspacesState } from '../state';
9import LoaderComponent from '../../../components/ui/Loader'; 8import LoaderComponent from '../../../components/ui/Loader';
9import { workspaceStore } from '../index';
10 10
11const messages = defineMessages({ 11const messages = defineMessages({
12 switchingTo: { 12 switchingTo: {
@@ -56,7 +56,7 @@ class WorkspaceSwitchingIndicator extends Component {
56 render() { 56 render() {
57 const { classes } = this.props; 57 const { classes } = this.props;
58 const { intl } = this.context; 58 const { intl } = this.context;
59 const { isSwitchingWorkspace, isWorkspaceDrawerOpen, nextWorkspace } = workspacesState; 59 const { isSwitchingWorkspace, isWorkspaceDrawerOpen, nextWorkspace } = workspaceStore;
60 if (!isSwitchingWorkspace) return null; 60 if (!isSwitchingWorkspace) return null;
61 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services'; 61 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services';
62 return ( 62 return (
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js
index 1b13bc2d4..1c4633e71 100644
--- a/src/features/workspaces/containers/EditWorkspaceScreen.js
+++ b/src/features/workspaces/containers/EditWorkspaceScreen.js
@@ -4,9 +4,9 @@ import PropTypes from 'prop-types';
4 4
5import ErrorBoundary from '../../../components/util/ErrorBoundary'; 5import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import EditWorkspaceForm from '../components/EditWorkspaceForm'; 6import EditWorkspaceForm from '../components/EditWorkspaceForm';
7import { workspacesState } from '../state';
8import ServicesStore from '../../../stores/ServicesStore'; 7import ServicesStore from '../../../stores/ServicesStore';
9import Workspace from '../models/Workspace'; 8import Workspace from '../models/Workspace';
9import { workspaceStore } from '../index';
10 10
11@inject('stores', 'actions') @observer 11@inject('stores', 'actions') @observer
12class EditWorkspaceScreen extends Component { 12class EditWorkspaceScreen extends Component {
@@ -22,14 +22,14 @@ class EditWorkspaceScreen extends Component {
22 }; 22 };
23 23
24 onDelete = () => { 24 onDelete = () => {
25 const { workspaceBeingEdited } = workspacesState; 25 const { workspaceBeingEdited } = workspaceStore;
26 const { actions } = this.props; 26 const { actions } = this.props;
27 if (!workspaceBeingEdited) return null; 27 if (!workspaceBeingEdited) return null;
28 actions.workspaces.delete({ workspace: workspaceBeingEdited }); 28 actions.workspaces.delete({ workspace: workspaceBeingEdited });
29 }; 29 };
30 30
31 onSave = (values) => { 31 onSave = (values) => {
32 const { workspaceBeingEdited } = workspacesState; 32 const { workspaceBeingEdited } = workspaceStore;
33 const { actions } = this.props; 33 const { actions } = this.props;
34 const workspace = new Workspace( 34 const workspace = new Workspace(
35 Object.assign({}, workspaceBeingEdited, values), 35 Object.assign({}, workspaceBeingEdited, values),
@@ -38,7 +38,7 @@ class EditWorkspaceScreen extends Component {
38 }; 38 };
39 39
40 render() { 40 render() {
41 const { workspaceBeingEdited } = workspacesState; 41 const { workspaceBeingEdited } = workspaceStore;
42 const { stores } = this.props; 42 const { stores } = this.props;
43 if (!workspaceBeingEdited) return null; 43 if (!workspaceBeingEdited) return null;
44 return ( 44 return (
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js
index bd1ddcd43..5fdea217e 100644
--- a/src/features/workspaces/containers/WorkspacesScreen.js
+++ b/src/features/workspaces/containers/WorkspacesScreen.js
@@ -1,9 +1,10 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react'; 2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { workspacesState } from '../state';
5import WorkspacesDashboard from '../components/WorkspacesDashboard'; 4import WorkspacesDashboard from '../components/WorkspacesDashboard';
6import ErrorBoundary from '../../../components/util/ErrorBoundary'; 5import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import { workspaceStore } from '../index';
7import { getUserWorkspacesRequest } from '../api';
7 8
8@inject('actions') @observer 9@inject('actions') @observer
9class WorkspacesScreen extends Component { 10class WorkspacesScreen extends Component {
@@ -20,8 +21,8 @@ class WorkspacesScreen extends Component {
20 return ( 21 return (
21 <ErrorBoundary> 22 <ErrorBoundary>
22 <WorkspacesDashboard 23 <WorkspacesDashboard
23 workspaces={workspacesState.workspaces} 24 workspaces={workspaceStore.workspaces}
24 isLoadingWorkspaces={workspacesState.isLoadingWorkspaces} 25 isLoadingWorkspaces={getUserWorkspacesRequest.isExecuting}
25 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)} 26 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)}
26 onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })} 27 onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })}
27 /> 28 />
diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js
index 1644c0e2f..68f82bdee 100644
--- a/src/features/workspaces/index.js
+++ b/src/features/workspaces/index.js
@@ -1,26 +1,9 @@
1import { reaction, runInAction } from 'mobx'; 1import { reaction } from 'mobx';
2import WorkspacesStore from './store'; 2import WorkspacesStore from './store';
3import api from './api';
4import { workspacesState, resetState } from './state';
5 3
6const debug = require('debug')('Franz:feature:workspaces'); 4const debug = require('debug')('Franz:feature:workspaces');
7 5
8let store = null; 6export const workspaceStore = new WorkspacesStore();
9
10export const filterServicesByActiveWorkspace = (services) => {
11 const {
12 activeWorkspace,
13 isFeatureActive,
14 } = workspacesState;
15
16 if (!isFeatureActive) return services;
17 if (activeWorkspace) return services.filter(s => activeWorkspace.services.includes(s.id));
18 return services;
19};
20
21export const getActiveWorkspaceServices = services => (
22 filterServicesByActiveWorkspace(services)
23);
24 7
25export default function initWorkspaces(stores, actions) { 8export default function initWorkspaces(stores, actions) {
26 const { features, user } = stores; 9 const { features, user } = stores;
@@ -33,38 +16,16 @@ export default function initWorkspaces(stores, actions) {
33 ) 16 )
34 ), 17 ),
35 (isEnabled) => { 18 (isEnabled) => {
36 if (isEnabled) { 19 if (isEnabled && !workspaceStore.isFeatureActive) {
37 debug('Initializing `workspaces` feature'); 20 debug('Initializing `workspaces` feature');
38 store = new WorkspacesStore(stores, api, actions, workspacesState); 21 workspaceStore.start(stores, actions);
39 store.initialize(); 22 } else if (workspaceStore.isFeatureActive) {
40 runInAction(() => { workspacesState.isFeatureActive = true; });
41 } else if (store) {
42 debug('Disabling `workspaces` feature'); 23 debug('Disabling `workspaces` feature');
43 runInAction(() => { workspacesState.isFeatureActive = false; }); 24 workspaceStore.stop();
44 store.teardown();
45 store = null;
46 resetState(); // Reset state to default
47 } 25 }
48 }, 26 },
49 { 27 {
50 fireImmediately: true, 28 fireImmediately: true,
51 }, 29 },
52 ); 30 );
53
54 // Update active service on workspace switches
55 reaction(() => ({
56 isFeatureActive: workspacesState.isFeatureActive,
57 activeWorkspace: workspacesState.activeWorkspace,
58 }), ({ isFeatureActive, activeWorkspace }) => {
59 if (!isFeatureActive) return;
60 if (activeWorkspace) {
61 const services = stores.services.allDisplayed;
62 const activeService = services.find(s => s.isActive);
63 const workspaceServices = filterServicesByActiveWorkspace(services);
64 const isActiveServiceInWorkspace = workspaceServices.includes(activeService);
65 if (!isActiveServiceInWorkspace) {
66 actions.service.setActive({ serviceId: workspaceServices[0].id });
67 }
68 }
69 });
70} 31}
diff --git a/src/features/workspaces/state.js b/src/features/workspaces/state.js
deleted file mode 100644
index c916480c0..000000000
--- a/src/features/workspaces/state.js
+++ /dev/null
@@ -1,18 +0,0 @@
1import { observable } from 'mobx';
2
3const defaultState = {
4 activeWorkspace: null,
5 nextWorkspace: null,
6 isLoadingWorkspaces: false,
7 isFeatureActive: false,
8 isSwitchingWorkspace: false,
9 isWorkspaceDrawerOpen: false,
10 workspaces: [],
11 workspaceBeingEdited: null,
12};
13
14export const workspacesState = observable(defaultState);
15
16export function resetState() {
17 Object.assign(workspacesState, defaultState);
18}
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index f6b9b2ff4..883f36ffb 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -1,54 +1,39 @@
1import { observable, reaction, action } from 'mobx'; 1import {
2import Store from '../../stores/lib/Store'; 2 computed,
3import CachedRequest from '../../stores/lib/CachedRequest'; 3 observable,
4import Workspace from './models/Workspace'; 4 action,
5} from 'mobx';
6import Reaction from '../../stores/lib/Reaction';
5import { matchRoute } from '../../helpers/routing-helpers'; 7import { matchRoute } from '../../helpers/routing-helpers';
6import { workspaceActions } from './actions'; 8import { workspaceActions } from './actions';
9import {
10 createWorkspaceRequest,
11 deleteWorkspaceRequest,
12 getUserWorkspacesRequest,
13 updateWorkspaceRequest,
14} from './api';
7 15
8const debug = require('debug')('Franz:feature:workspaces'); 16const debug = require('debug')('Franz:feature:workspaces:store');
9 17
10export default class WorkspacesStore extends Store { 18export default class WorkspacesStore {
11 @observable allWorkspacesRequest = new CachedRequest(this.api, 'getUserWorkspaces'); 19 @observable isFeatureActive = false;
12 20
13 constructor(stores, api, actions, state) { 21 @observable activeWorkspace = null;
14 super(stores, api, actions); 22
15 this.state = state; 23 @observable nextWorkspace = null;
16 } 24
25 @observable workspaceBeingEdited = null;
26
27 @observable isSwitchingWorkspace = false;
17 28
18 setup() { 29 @observable isWorkspaceDrawerOpen = false;
19 debug('fetching workspaces');
20 this.allWorkspacesRequest.execute();
21
22 /**
23 * Update the state workspaces array when workspaces request has results.
24 */
25 reaction(
26 () => this.allWorkspacesRequest.result,
27 workspaces => this._setWorkspaces(workspaces),
28 );
29 /**
30 * Update the loading state when workspace request is executing.
31 */
32 reaction(
33 () => this.allWorkspacesRequest.isExecuting,
34 isExecuting => this._setIsLoadingWorkspaces(isExecuting),
35 );
36 /**
37 * Update the state with the workspace to be edited when route matches.
38 */
39 reaction(
40 () => ({
41 pathname: this.stores.router.location.pathname,
42 workspaces: this.state.workspaces,
43 }),
44 ({ pathname }) => {
45 const match = matchRoute('/settings/workspaces/edit/:id', pathname);
46 if (match) {
47 this.state.workspaceBeingEdited = this._getWorkspaceById(match.id);
48 }
49 },
50 );
51 30
31 @computed get workspaces() {
32 return getUserWorkspacesRequest.execute().result || [];
33 }
34
35 constructor() {
36 // Wire-up action handlers
52 workspaceActions.edit.listen(this._edit); 37 workspaceActions.edit.listen(this._edit);
53 workspaceActions.create.listen(this._create); 38 workspaceActions.create.listen(this._create);
54 workspaceActions.delete.listen(this._delete); 39 workspaceActions.delete.listen(this._delete);
@@ -57,28 +42,62 @@ export default class WorkspacesStore extends Store {
57 workspaceActions.deactivate.listen(this._deactivateActiveWorkspace); 42 workspaceActions.deactivate.listen(this._deactivateActiveWorkspace);
58 workspaceActions.toggleWorkspaceDrawer.listen(this._toggleWorkspaceDrawer); 43 workspaceActions.toggleWorkspaceDrawer.listen(this._toggleWorkspaceDrawer);
59 workspaceActions.openWorkspaceSettings.listen(this._openWorkspaceSettings); 44 workspaceActions.openWorkspaceSettings.listen(this._openWorkspaceSettings);
45
46 // Register and start reactions
47 this._registerReactions([
48 this._updateWorkspaceBeingEdited,
49 this._updateActiveServiceOnWorkspaceSwitch,
50 ]);
60 } 51 }
61 52
62 _getWorkspaceById = id => this.state.workspaces.find(w => w.id === id); 53 start(stores, actions) {
54 debug('WorkspacesStore::start');
55 this.stores = stores;
56 this.actions = actions;
57 this._reactions.forEach(r => r.start());
58 this.isFeatureActive = true;
59 }
63 60
64 @action _setWorkspaces = (workspaces) => { 61 stop() {
65 debug('setting user workspaces', workspaces.slice()); 62 debug('WorkspacesStore::stop');
66 this.state.workspaces = workspaces.map(data => new Workspace(data)); 63 this._reactions.forEach(r => r.stop());
67 }; 64 this.isFeatureActive = false;
65 }
68 66
69 @action _setIsLoadingWorkspaces = (isLoading) => { 67 filterServicesByActiveWorkspace = (services) => {
70 this.state.isLoadingWorkspaces = isLoading; 68 const { activeWorkspace, isFeatureActive } = this;
69
70 if (!isFeatureActive) return services;
71 if (activeWorkspace) {
72 return services.filter(s => (
73 activeWorkspace.services.includes(s.id)
74 ));
75 }
76 return services;
71 }; 77 };
72 78
79 // ========== PRIVATE ========= //
80
81 _reactions = [];
82
83 _registerReactions(reactions) {
84 reactions.forEach(r => this._reactions.push(new Reaction(r)));
85 }
86
87 _getWorkspaceById = id => this.workspaces.find(w => w.id === id);
88
89 // Actions
90
73 @action _edit = ({ workspace }) => { 91 @action _edit = ({ workspace }) => {
74 this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`); 92 this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`);
75 }; 93 };
76 94
77 @action _create = async ({ name }) => { 95 @action _create = async ({ name }) => {
78 try { 96 try {
79 const result = await this.api.createWorkspace(name); 97 const workspace = await createWorkspaceRequest.execute(name);
80 const workspace = new Workspace(result); 98 await getUserWorkspacesRequest.patch((result) => {
81 this.state.workspaces.push(workspace); 99 result.push(workspace);
100 });
82 this._edit({ workspace }); 101 this._edit({ workspace });
83 } catch (error) { 102 } catch (error) {
84 throw error; 103 throw error;
@@ -87,8 +106,10 @@ export default class WorkspacesStore extends Store {
87 106
88 @action _delete = async ({ workspace }) => { 107 @action _delete = async ({ workspace }) => {
89 try { 108 try {
90 await this.api.deleteWorkspace(workspace); 109 await deleteWorkspaceRequest.execute(workspace);
91 this.state.workspaces.remove(workspace); 110 await getUserWorkspacesRequest.patch((result) => {
111 result.remove(workspace);
112 });
92 this.stores.router.push('/settings/workspaces'); 113 this.stores.router.push('/settings/workspaces');
93 } catch (error) { 114 } catch (error) {
94 throw error; 115 throw error;
@@ -97,9 +118,11 @@ export default class WorkspacesStore extends Store {
97 118
98 @action _update = async ({ workspace }) => { 119 @action _update = async ({ workspace }) => {
99 try { 120 try {
100 await this.api.updateWorkspace(workspace); 121 await updateWorkspaceRequest.execute(workspace);
101 const localWorkspace = this.state.workspaces.find(ws => ws.id === workspace.id); 122 await getUserWorkspacesRequest.patch((result) => {
102 Object.assign(localWorkspace, workspace); 123 const localWorkspace = result.find(ws => ws.id === workspace.id);
124 Object.assign(localWorkspace, workspace);
125 });
103 this.stores.router.push('/settings/workspaces'); 126 this.stores.router.push('/settings/workspaces');
104 } catch (error) { 127 } catch (error) {
105 throw error; 128 throw error;
@@ -107,33 +130,56 @@ export default class WorkspacesStore extends Store {
107 }; 130 };
108 131
109 @action _setActiveWorkspace = ({ workspace }) => { 132 @action _setActiveWorkspace = ({ workspace }) => {
110 Object.assign(this.state, { 133 // Indicate that we are switching to another workspace
111 isSwitchingWorkspace: true, 134 this.isSwitchingWorkspace = true;
112 nextWorkspace: workspace, 135 this.nextWorkspace = workspace;
113 }); 136 // Delay switching to next workspace so that the services loading does not drag down UI
114 setTimeout(() => { this.state.activeWorkspace = workspace; }, 100); 137 setTimeout(() => { this.activeWorkspace = workspace; }, 100);
138 // Indicate that we are done switching to the next workspace
115 setTimeout(() => { 139 setTimeout(() => {
116 Object.assign(this.state, { 140 this.isSwitchingWorkspace = false;
117 isSwitchingWorkspace: false, 141 this.nextWorkspace = null;
118 nextWorkspace: null,
119 });
120 }, 1000); 142 }, 1000);
121 }; 143 };
122 144
123 @action _deactivateActiveWorkspace = () => { 145 @action _deactivateActiveWorkspace = () => {
124 Object.assign(this.state, { 146 // Indicate that we are switching to default workspace
125 isSwitchingWorkspace: true, 147 this.isSwitchingWorkspace = true;
126 nextWorkspace: null, 148 this.nextWorkspace = null;
127 }); 149 // Delay switching to next workspace so that the services loading does not drag down UI
128 setTimeout(() => { this.state.activeWorkspace = null; }, 100); 150 setTimeout(() => { this.activeWorkspace = null; }, 100);
129 setTimeout(() => { this.state.isSwitchingWorkspace = false; }, 1000); 151 // Indicate that we are done switching to the default workspace
152 setTimeout(() => { this.isSwitchingWorkspace = false; }, 1000);
130 }; 153 };
131 154
132 @action _toggleWorkspaceDrawer = () => { 155 @action _toggleWorkspaceDrawer = () => {
133 this.state.isWorkspaceDrawerOpen = !this.state.isWorkspaceDrawerOpen; 156 this.isWorkspaceDrawerOpen = !this.isWorkspaceDrawerOpen;
134 }; 157 };
135 158
136 @action _openWorkspaceSettings = () => { 159 @action _openWorkspaceSettings = () => {
137 this.actions.ui.openSettings({ path: 'workspaces' }); 160 this.actions.ui.openSettings({ path: 'workspaces' });
138 }; 161 };
162
163 // Reactions
164
165 _updateWorkspaceBeingEdited = () => {
166 const { pathname } = this.stores.router.location;
167 const match = matchRoute('/settings/workspaces/edit/:id', pathname);
168 if (match) {
169 this.workspaceBeingEdited = this._getWorkspaceById(match.id);
170 }
171 };
172
173 _updateActiveServiceOnWorkspaceSwitch = () => {
174 if (!this.isFeatureActive) return;
175 if (this.activeWorkspace) {
176 const services = this.stores.services.allDisplayed;
177 const activeService = services.find(s => s.isActive);
178 const workspaceServices = this.filterServicesByActiveWorkspace(services);
179 const isActiveServiceInWorkspace = workspaceServices.includes(activeService);
180 if (!isActiveServiceInWorkspace) {
181 this.actions.service.setActive({ serviceId: workspaceServices[0].id });
182 }
183 }
184 };
139} 185}
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 2a88515f4..3435e04f7 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -3,7 +3,7 @@ import { observable, autorun } from 'mobx';
3import { defineMessages } from 'react-intl'; 3import { defineMessages } from 'react-intl';
4 4
5import { isMac, ctrlKey, cmdKey } from '../environment'; 5import { isMac, ctrlKey, cmdKey } from '../environment';
6import { workspacesState } from '../features/workspaces/state'; 6import { workspaceStore } from '../features/workspaces/index';
7import { workspaceActions } from '../features/workspaces/actions'; 7import { workspaceActions } from '../features/workspaces/actions';
8 8
9const { app, Menu, dialog } = remote; 9const { app, Menu, dialog } = remote;
@@ -784,7 +784,7 @@ export default class FranzMenu {
784 } 784 }
785 785
786 workspacesMenu() { 786 workspacesMenu() {
787 const { workspaces, activeWorkspace } = workspacesState; 787 const { workspaces, activeWorkspace, isWorkspaceDrawerOpen } = workspaceStore;
788 const { intl } = window.franz; 788 const { intl } = window.franz;
789 const menu = []; 789 const menu = [];
790 790
@@ -800,7 +800,7 @@ export default class FranzMenu {
800 800
801 // Open workspace drawer: 801 // Open workspace drawer:
802 const drawerLabel = ( 802 const drawerLabel = (
803 workspacesState.isWorkspaceDrawerOpen ? menuItems.closeWorkspaceDrawer : menuItems.openWorkspaceDrawer 803 isWorkspaceDrawerOpen ? menuItems.closeWorkspaceDrawer : menuItems.openWorkspaceDrawer
804 ); 804 );
805 menu.push({ 805 menu.push({
806 label: intl.formatMessage(drawerLabel), 806 label: intl.formatMessage(drawerLabel),
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index cc8eed65b..0ec6bf550 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -12,7 +12,7 @@ import Request from './lib/Request';
12import CachedRequest from './lib/CachedRequest'; 12import CachedRequest from './lib/CachedRequest';
13import { matchRoute } from '../helpers/routing-helpers'; 13import { matchRoute } from '../helpers/routing-helpers';
14import { gaEvent } from '../lib/analytics'; 14import { gaEvent } from '../lib/analytics';
15import { filterServicesByActiveWorkspace, getActiveWorkspaceServices } from '../features/workspaces'; 15import { workspaceStore } from '../features/workspaces';
16 16
17const debug = require('debug')('Franz:ServiceStore'); 17const debug = require('debug')('Franz:ServiceStore');
18 18
@@ -109,7 +109,7 @@ export default class ServicesStore extends Store {
109 109
110 @computed get allDisplayed() { 110 @computed get allDisplayed() {
111 const services = this.stores.settings.all.app.showDisabledServices ? this.all : this.enabled; 111 const services = this.stores.settings.all.app.showDisabledServices ? this.all : this.enabled;
112 return filterServicesByActiveWorkspace(services); 112 return workspaceStore.filterServicesByActiveWorkspace(services);
113 } 113 }
114 114
115 // This is just used to avoid unnecessary rerendering of resource-heavy webviews 115 // This is just used to avoid unnecessary rerendering of resource-heavy webviews
@@ -117,7 +117,7 @@ export default class ServicesStore extends Store {
117 const { showDisabledServices } = this.stores.settings.all.app; 117 const { showDisabledServices } = this.stores.settings.all.app;
118 const services = this.allServicesRequest.execute().result || []; 118 const services = this.allServicesRequest.execute().result || [];
119 const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled); 119 const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled);
120 return getActiveWorkspaceServices(filteredServices); 120 return workspaceStore.filterServicesByActiveWorkspace(filteredServices);
121 } 121 }
122 122
123 @computed get filtered() { 123 @computed get filtered() {