aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions/index.js2
-rw-r--r--src/api/utils/auth.js28
-rw-r--r--src/app.js4
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js12
-rw-r--r--src/config.js2
-rw-r--r--src/environment.js1
-rw-r--r--src/features/delayApp/Component.js2
-rw-r--r--src/features/workspaces/actions.js14
-rw-r--r--src/features/workspaces/api.js28
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.js94
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js142
-rw-r--r--src/features/workspaces/components/WorkspaceItem.js42
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js85
-rw-r--r--src/features/workspaces/containers/EditWorkspaceScreen.js52
-rw-r--r--src/features/workspaces/containers/WorkspacesScreen.js38
-rw-r--r--src/features/workspaces/index.js34
-rw-r--r--src/features/workspaces/models/Workspace.js25
-rw-r--r--src/features/workspaces/state.js13
-rw-r--r--src/features/workspaces/store.js91
-rw-r--r--src/features/workspaces/styles/workspaces-table.scss53
-rw-r--r--src/i18n/locales/de.json8
-rw-r--r--src/i18n/locales/en-US.json8
-rw-r--r--src/stores/FeaturesStore.js4
-rw-r--r--src/styles/main.scss3
24 files changed, 783 insertions, 2 deletions
diff --git a/src/actions/index.js b/src/actions/index.js
index 59acabb0b..45e6da515 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -11,6 +11,7 @@ import payment from './payment';
11import news from './news'; 11import news from './news';
12import settings from './settings'; 12import settings from './settings';
13import requests from './requests'; 13import requests from './requests';
14import workspace from '../features/workspaces/actions';
14 15
15const actions = Object.assign({}, { 16const actions = Object.assign({}, {
16 service, 17 service,
@@ -23,6 +24,7 @@ const actions = Object.assign({}, {
23 news, 24 news,
24 settings, 25 settings,
25 requests, 26 requests,
27 workspace,
26}); 28});
27 29
28export default defineActions(actions, PropTypes.checkPropTypes); 30export default defineActions(actions, PropTypes.checkPropTypes);
diff --git a/src/api/utils/auth.js b/src/api/utils/auth.js
new file mode 100644
index 000000000..d469853a5
--- /dev/null
+++ b/src/api/utils/auth.js
@@ -0,0 +1,28 @@
1import { remote } from 'electron';
2import localStorage from 'mobx-localstorage';
3
4const { app } = remote;
5
6export const prepareAuthRequest = (options, auth = true) => {
7 const request = Object.assign(options, {
8 mode: 'cors',
9 headers: Object.assign({
10 'Content-Type': 'application/json',
11 'X-Franz-Source': 'desktop',
12 'X-Franz-Version': app.getVersion(),
13 'X-Franz-platform': process.platform,
14 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(),
15 'X-Franz-System-Locale': app.getLocale(),
16 }, options.headers),
17 });
18
19 if (auth) {
20 request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
21 }
22
23 return request;
24};
25
26export const sendAuthRequest = (url, options) => (
27 window.fetch(url, prepareAuthRequest(options))
28);
diff --git a/src/app.js b/src/app.js
index 6660feb46..d3b540f62 100644
--- a/src/app.js
+++ b/src/app.js
@@ -39,6 +39,8 @@ import PricingScreen from './containers/auth/PricingScreen';
39import InviteScreen from './containers/auth/InviteScreen'; 39import InviteScreen from './containers/auth/InviteScreen';
40import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; 40import AuthLayoutContainer from './containers/auth/AuthLayoutContainer';
41import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen'; 41import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen';
42import WorkspacesScreen from './features/workspaces/containers/WorkspacesScreen';
43import EditWorkspaceScreen from './features/workspaces/containers/EditWorkspaceScreen';
42 44
43// Add Polyfills 45// Add Polyfills
44smoothScroll.polyfill(); 46smoothScroll.polyfill();
@@ -75,6 +77,8 @@ window.addEventListener('load', () => {
75 <Route path="/settings/recipes/:filter" component={RecipesScreen} /> 77 <Route path="/settings/recipes/:filter" component={RecipesScreen} />
76 <Route path="/settings/services" component={ServicesScreen} /> 78 <Route path="/settings/services" component={ServicesScreen} />
77 <Route path="/settings/services/:action/:id" component={EditServiceScreen} /> 79 <Route path="/settings/services/:action/:id" component={EditServiceScreen} />
80 <Route path="/settings/workspaces" component={WorkspacesScreen} />
81 <Route path="/settings/workspaces/:action/:id" component={EditWorkspaceScreen} />
78 <Route path="/settings/user" component={AccountScreen} /> 82 <Route path="/settings/user" component={AccountScreen} />
79 <Route path="/settings/user/edit" component={EditUserScreen} /> 83 <Route path="/settings/user/edit" component={EditUserScreen} />
80 <Route path="/settings/app" component={EditSettingsScreen} /> 84 <Route path="/settings/app" component={EditSettingsScreen} />
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index 953f702f8..4a80bb126 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -14,6 +14,10 @@ const messages = defineMessages({
14 id: 'settings.navigation.yourServices', 14 id: 'settings.navigation.yourServices',
15 defaultMessage: '!!!Your services', 15 defaultMessage: '!!!Your services',
16 }, 16 },
17 yourWorkspaces: {
18 id: 'settings.navigation.yourWorkspaces',
19 defaultMessage: '!!!Your workspaces',
20 },
17 account: { 21 account: {
18 id: 'settings.navigation.account', 22 id: 'settings.navigation.account',
19 defaultMessage: '!!!Account', 23 defaultMessage: '!!!Account',
@@ -64,6 +68,14 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
64 <span className="badge">{serviceCount}</span> 68 <span className="badge">{serviceCount}</span>
65 </Link> 69 </Link>
66 <Link 70 <Link
71 to="/settings/workspaces"
72 className="settings-navigation__link"
73 activeClassName="is-active"
74 >
75 {intl.formatMessage(messages.yourWorkspaces)}
76 {' '}
77 </Link>
78 <Link
67 to="/settings/user" 79 to="/settings/user"
68 className="settings-navigation__link" 80 className="settings-navigation__link"
69 activeClassName="is-active" 81 activeClassName="is-active"
diff --git a/src/config.js b/src/config.js
index aecba45be..3696354c6 100644
--- a/src/config.js
+++ b/src/config.js
@@ -38,6 +38,8 @@ export const DEFAULT_FEATURES_CONFIG = {
38 }, 38 },
39 isServiceProxyEnabled: false, 39 isServiceProxyEnabled: false,
40 isServiceProxyPremiumFeature: true, 40 isServiceProxyPremiumFeature: true,
41 isWorkspacePremiumFeature: true,
42 isWorkspaceEnabled: true,
41}; 43};
42 44
43export const DEFAULT_WINDOW_OPTIONS = { 45export const DEFAULT_WINDOW_OPTIONS = {
diff --git a/src/environment.js b/src/environment.js
index 73b1c7ab2..d67fd6adb 100644
--- a/src/environment.js
+++ b/src/environment.js
@@ -28,3 +28,4 @@ if (!isDevMode || (isDevMode && useLiveAPI)) {
28} 28}
29 29
30export const API = api; 30export const API = api;
31export const API_VERSION = 'v1';
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js
index ff84510e8..ff0f1f2f8 100644
--- a/src/features/delayApp/Component.js
+++ b/src/features/delayApp/Component.js
@@ -38,7 +38,7 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp
38 38
39 state = { 39 state = {
40 countdown: config.delayDuration, 40 countdown: config.delayDuration,
41 } 41 };
42 42
43 countdownInterval = null; 43 countdownInterval = null;
44 44
diff --git a/src/features/workspaces/actions.js b/src/features/workspaces/actions.js
new file mode 100644
index 000000000..83d3447c3
--- /dev/null
+++ b/src/features/workspaces/actions.js
@@ -0,0 +1,14 @@
1import PropTypes from 'prop-types';
2import Workspace from './models/Workspace';
3
4export default {
5 edit: {
6 workspace: PropTypes.instanceOf(Workspace).isRequired,
7 },
8 create: {
9 name: PropTypes.string.isRequired,
10 },
11 delete: {
12 workspace: PropTypes.instanceOf(Workspace).isRequired,
13 },
14};
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js
new file mode 100644
index 000000000..fabc12455
--- /dev/null
+++ b/src/features/workspaces/api.js
@@ -0,0 +1,28 @@
1import { sendAuthRequest } from '../../api/utils/auth';
2import { API, API_VERSION } from '../../environment';
3
4export default {
5 getUserWorkspaces: async () => {
6 const url = `${API}/${API_VERSION}/workspace`;
7 const request = await sendAuthRequest(url, { method: 'GET' });
8 if (!request.ok) throw request;
9 return request.json();
10 },
11
12 createWorkspace: async (name) => {
13 const url = `${API}/${API_VERSION}/workspace`;
14 const request = await sendAuthRequest(url, {
15 method: 'POST',
16 body: JSON.stringify({ name }),
17 });
18 if (!request.ok) throw request;
19 return request.json();
20 },
21
22 deleteWorkspace: async (workspace) => {
23 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`;
24 const request = await sendAuthRequest(url, { method: 'DELETE' });
25 if (!request.ok) throw request;
26 return request.json();
27 },
28};
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js
new file mode 100644
index 000000000..d440b9bae
--- /dev/null
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -0,0 +1,94 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import { Input, Button } from '@meetfranz/forms';
6import injectSheet from 'react-jss';
7
8import Form from '../../../lib/Form';
9
10const messages = defineMessages({
11 submitButton: {
12 id: 'settings.workspace.add.form.submitButton',
13 defaultMessage: '!!!Save workspace',
14 },
15 name: {
16 id: 'settings.workspace.add.form.name',
17 defaultMessage: '!!!Name',
18 },
19});
20
21const styles = () => ({
22 form: {
23 display: 'flex',
24 },
25 input: {
26 flexGrow: 1,
27 marginRight: '10px',
28 },
29 submitButton: {
30 height: 'inherit',
31 marginTop: '17px',
32 },
33});
34
35@observer @injectSheet(styles)
36class CreateWorkspaceForm extends Component {
37 static contextTypes = {
38 intl: intlShape,
39 };
40
41 static propTypes = {
42 classes: PropTypes.object.isRequired,
43 onSubmit: PropTypes.func.isRequired,
44 };
45
46 prepareForm() {
47 const { intl } = this.context;
48 const config = {
49 fields: {
50 name: {
51 label: intl.formatMessage(messages.name),
52 placeholder: intl.formatMessage(messages.name),
53 value: '',
54 },
55 },
56 };
57 return new Form(config);
58 }
59
60 submitForm(form) {
61 form.submit({
62 onSuccess: async (f) => {
63 const { onSubmit } = this.props;
64 const values = f.values();
65 onSubmit(values);
66 },
67 onError: async () => {},
68 });
69 }
70
71 render() {
72 const { intl } = this.context;
73 const { classes } = this.props;
74 const form = this.prepareForm();
75
76 return (
77 <div className={classes.form}>
78 <Input
79 className={classes.input}
80 {...form.$('name').bind()}
81 onEnterKey={this.submitForm.bind(this, form)}
82 />
83 <Button
84 className={classes.submitButton}
85 type="button"
86 label={intl.formatMessage(messages.submitButton)}
87 onClick={this.submitForm.bind(this, form)}
88 />
89 </div>
90 );
91 }
92}
93
94export default CreateWorkspaceForm;
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
new file mode 100644
index 000000000..9e87b56dd
--- /dev/null
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -0,0 +1,142 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router';
6import { Input, Button } from '@meetfranz/forms';
7
8import Form from '../../../lib/Form';
9import Workspace from '../models/Workspace';
10
11const messages = defineMessages({
12 buttonDelete: {
13 id: 'settings.workspace.form.buttonDelete',
14 defaultMessage: '!!!Delete workspace',
15 },
16 buttonSave: {
17 id: 'settings.workspace.form.buttonSave',
18 defaultMessage: '!!!Save workspace',
19 },
20 name: {
21 id: 'settings.workspace.form.name',
22 defaultMessage: '!!!Name',
23 },
24 yourWorkspaces: {
25 id: 'settings.workspace.form.yourWorkspaces',
26 defaultMessage: '!!!Your workspaces',
27 },
28});
29
30@observer
31class EditWorkspaceForm extends Component {
32 static contextTypes = {
33 intl: intlShape,
34 };
35
36 static propTypes = {
37 workspace: PropTypes.instanceOf(Workspace).isRequired,
38 onSave: PropTypes.func.isRequired,
39 onDelete: PropTypes.func.isRequired,
40 isSaving: PropTypes.bool.isRequired,
41 isDeleting: PropTypes.bool.isRequired,
42 };
43
44 prepareForm(workspace) {
45 const { intl } = this.context;
46 const config = {
47 fields: {
48 name: {
49 label: intl.formatMessage(messages.name),
50 placeholder: intl.formatMessage(messages.name),
51 value: workspace.name,
52 },
53 },
54 };
55 return new Form(config);
56 }
57
58 submitForm(submitEvent, form) {
59 submitEvent.preventDefault();
60 form.submit({
61 onSuccess: async (f) => {
62 const { onSave } = this.props;
63 const values = f.values();
64 onSave(values);
65 },
66 onError: async () => {},
67 });
68 }
69
70 render() {
71 const { intl } = this.context;
72 const {
73 workspace,
74 isDeleting,
75 isSaving,
76 onDelete,
77 } = this.props;
78 if (!workspace) return null;
79
80 const form = this.prepareForm(workspace);
81
82 return (
83 <div className="settings__main">
84 <div className="settings__header">
85 <span className="settings__header-item">
86 <Link to="/settings/workspaces">
87 {intl.formatMessage(messages.yourWorkspaces)}
88 </Link>
89 </span>
90 <span className="separator" />
91 <span className="settings__header-item">
92 {workspace.name}
93 </span>
94 </div>
95 <div className="settings__body">
96 <form onSubmit={e => this.submitForm(e, form)} id="form">
97 <div className="workspace-name">
98 <Input {...form.$('name').bind()} />
99 </div>
100 </form>
101 </div>
102 <div className="settings__controls">
103 {/* ===== Delete Button ===== */}
104 {isDeleting ? (
105 <Button
106 label={intl.formatMessage(messages.buttonDelete)}
107 loaded={false}
108 buttonType="secondary"
109 className="settings__delete-button"
110 disabled
111 />
112 ) : (
113 <Button
114 buttonType="danger"
115 label={intl.formatMessage(messages.buttonDelete)}
116 className="settings__delete-button"
117 onClick={onDelete}
118 />
119 )}
120 {/* ===== Save Button ===== */}
121 {isSaving ? (
122 <Button
123 type="submit"
124 label={intl.formatMessage(messages.buttonSave)}
125 loaded={!isSaving}
126 buttonType="secondary"
127 disabled
128 />
129 ) : (
130 <Button
131 type="submit"
132 label={intl.formatMessage(messages.buttonSave)}
133 htmlForm="form"
134 />
135 )}
136 </div>
137 </div>
138 );
139 }
140}
141
142export default EditWorkspaceForm;
diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js
new file mode 100644
index 000000000..b2c2a4830
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceItem.js
@@ -0,0 +1,42 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { intlShape } from 'react-intl';
4import { observer } from 'mobx-react';
5import classnames from 'classnames';
6import Workspace from '../models/Workspace';
7
8// const messages = defineMessages({});
9
10@observer
11class WorkspaceItem extends Component {
12 static propTypes = {
13 workspace: PropTypes.instanceOf(Workspace).isRequired,
14 onItemClick: PropTypes.func.isRequired,
15 };
16
17 static contextTypes = {
18 intl: intlShape,
19 };
20
21 render() {
22 const { workspace, onItemClick } = this.props;
23 // const { intl } = this.context;
24
25 return (
26 <tr
27 className={classnames({
28 'workspace-table__row': true,
29 })}
30 >
31 <td
32 className="workspace-table__column-name"
33 onClick={() => onItemClick(workspace)}
34 >
35 {workspace.name}
36 </td>
37 </tr>
38 );
39 }
40}
41
42export default WorkspaceItem;
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
new file mode 100644
index 000000000..917807302
--- /dev/null
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -0,0 +1,85 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6
7import Loader from '../../../components/ui/Loader';
8import WorkspaceItem from './WorkspaceItem';
9import CreateWorkspaceForm from './CreateWorkspaceForm';
10
11const messages = defineMessages({
12 headline: {
13 id: 'settings.workspaces.headline',
14 defaultMessage: '!!!Your workspaces',
15 },
16 noServicesAdded: {
17 id: 'settings.workspaces.noWorkspacesAdded',
18 defaultMessage: '!!!You haven\'t added any workspaces yet.',
19 },
20});
21
22const styles = () => ({
23 createForm: {
24 height: 'auto',
25 marginBottom: '20px',
26 },
27});
28
29@observer @injectSheet(styles)
30class WorkspacesDashboard extends Component {
31 static propTypes = {
32 classes: PropTypes.object.isRequired,
33 isLoading: PropTypes.bool.isRequired,
34 onCreateWorkspaceSubmit: PropTypes.func.isRequired,
35 onWorkspaceClick: PropTypes.func.isRequired,
36 workspaces: MobxPropTypes.arrayOrObservableArray.isRequired,
37 };
38
39 static contextTypes = {
40 intl: intlShape,
41 };
42
43 render() {
44 const {
45 workspaces,
46 isLoading,
47 onCreateWorkspaceSubmit,
48 onWorkspaceClick,
49 classes,
50 } = this.props;
51 const { intl } = this.context;
52
53 return (
54 <div className="settings__main">
55 <div className="settings__header">
56 <h1>{intl.formatMessage(messages.headline)}</h1>
57 </div>
58 <div className="settings__body">
59 <div className={classes.body}>
60 <div className={classes.createForm}>
61 <CreateWorkspaceForm onSubmit={onCreateWorkspaceSubmit} />
62 </div>
63 {isLoading ? (
64 <Loader />
65 ) : (
66 <table className="workspace-table">
67 <tbody>
68 {workspaces.map(workspace => (
69 <WorkspaceItem
70 key={workspace.id}
71 workspace={workspace}
72 onItemClick={w => onWorkspaceClick(w)}
73 />
74 ))}
75 </tbody>
76 </table>
77 )}
78 </div>
79 </div>
80 </div>
81 );
82 }
83}
84
85export default WorkspacesDashboard;
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js
new file mode 100644
index 000000000..87b6062fb
--- /dev/null
+++ b/src/features/workspaces/containers/EditWorkspaceScreen.js
@@ -0,0 +1,52 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3
4import ErrorBoundary from '../../../components/util/ErrorBoundary';
5import { gaPage } from '../../../lib/analytics';
6import { state } from '../state';
7import EditWorkspaceForm from '../components/EditWorkspaceForm';
8import PropTypes from 'prop-types';
9
10@inject('stores', 'actions') @observer
11class EditWorkspaceScreen extends Component {
12 static propTypes = {
13 actions: PropTypes.shape({
14 workspace: PropTypes.shape({
15 delete: PropTypes.func.isRequired,
16 }),
17 }).isRequired,
18 };
19
20 componentDidMount() {
21 gaPage('Settings/Workspace/Edit');
22 }
23
24 onDelete = () => {
25 const { workspaceBeingEdited } = state;
26 const { actions } = this.props;
27 if (!workspaceBeingEdited) return null;
28 actions.workspace.delete({ workspace: workspaceBeingEdited });
29 };
30
31 onSave = (values) => {
32 console.log('save workspace', values);
33 };
34
35 render() {
36 const { workspaceBeingEdited } = state;
37 if (!workspaceBeingEdited) return null;
38 return (
39 <ErrorBoundary>
40 <EditWorkspaceForm
41 workspace={workspaceBeingEdited}
42 onDelete={this.onDelete}
43 onSave={this.onSave}
44 isDeleting={false}
45 isSaving={false}
46 />
47 </ErrorBoundary>
48 );
49 }
50}
51
52export default EditWorkspaceScreen;
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js
new file mode 100644
index 000000000..a3876a01a
--- /dev/null
+++ b/src/features/workspaces/containers/WorkspacesScreen.js
@@ -0,0 +1,38 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4import { gaPage } from '../../../lib/analytics';
5import { state } from '../state';
6import WorkspacesDashboard from '../components/WorkspacesDashboard';
7import ErrorBoundary from '../../../components/util/ErrorBoundary';
8
9@inject('actions') @observer
10class WorkspacesScreen extends Component {
11 static propTypes = {
12 actions: PropTypes.shape({
13 workspace: PropTypes.shape({
14 edit: PropTypes.func.isRequired,
15 }),
16 }).isRequired,
17 };
18
19 componentDidMount() {
20 gaPage('Settings/Workspaces Dashboard');
21 }
22
23 render() {
24 const { actions } = this.props;
25 return (
26 <ErrorBoundary>
27 <WorkspacesDashboard
28 workspaces={state.workspaces}
29 isLoading={state.isLoading}
30 onCreateWorkspaceSubmit={data => actions.workspace.create(data)}
31 onWorkspaceClick={w => actions.workspace.edit({ workspace: w })}
32 />
33 </ErrorBoundary>
34 );
35 }
36}
37
38export default WorkspacesScreen;
diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js
new file mode 100644
index 000000000..50ac3b414
--- /dev/null
+++ b/src/features/workspaces/index.js
@@ -0,0 +1,34 @@
1import { reaction } from 'mobx';
2import WorkspacesStore from './store';
3import api from './api';
4import { state, resetState } from './state';
5
6const debug = require('debug')('Franz:feature:workspaces');
7
8let store = null;
9
10export default function initWorkspaces(stores, actions) {
11 const { features, user } = stores;
12 reaction(
13 () => (
14 features.features.isWorkspaceEnabled && (
15 !features.features.isWorkspacePremiumFeature || user.data.isPremium
16 )
17 ),
18 (isEnabled) => {
19 if (isEnabled) {
20 debug('Initializing `workspaces` feature');
21 store = new WorkspacesStore(stores, api, actions, state);
22 store.initialize();
23 } else if (store) {
24 debug('Disabling `workspaces` feature');
25 store.teardown();
26 store = null;
27 resetState(); // Reset state to default
28 }
29 },
30 {
31 fireImmediately: true,
32 },
33 );
34}
diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js
new file mode 100644
index 000000000..ede2710dc
--- /dev/null
+++ b/src/features/workspaces/models/Workspace.js
@@ -0,0 +1,25 @@
1import { observable } from 'mobx';
2
3export default class Workspace {
4 id = null;
5
6 @observable name = null;
7
8 @observable order = null;
9
10 @observable services = [];
11
12 @observable userId = null;
13
14 constructor(data) {
15 if (!data.id) {
16 throw Error('Workspace requires Id');
17 }
18
19 this.id = data.id;
20 this.name = data.name;
21 this.order = data.order;
22 this.services = data.services;
23 this.userId = data.userId;
24 }
25}
diff --git a/src/features/workspaces/state.js b/src/features/workspaces/state.js
new file mode 100644
index 000000000..f938c1470
--- /dev/null
+++ b/src/features/workspaces/state.js
@@ -0,0 +1,13 @@
1import { observable } from 'mobx';
2
3const defaultState = {
4 isLoading: false,
5 workspaces: [],
6 workspaceBeingEdited: null,
7};
8
9export const state = observable(defaultState);
10
11export function resetState() {
12 Object.assign(state, defaultState);
13}
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
new file mode 100644
index 000000000..a9b93f904
--- /dev/null
+++ b/src/features/workspaces/store.js
@@ -0,0 +1,91 @@
1import { observable, reaction } from 'mobx';
2import Store from '../../stores/lib/Store';
3import CachedRequest from '../../stores/lib/CachedRequest';
4import Workspace from './models/Workspace';
5import { matchRoute } from '../../helpers/routing-helpers';
6
7const debug = require('debug')('Franz:feature:workspaces');
8
9export default class WorkspacesStore extends Store {
10 @observable allWorkspacesRequest = new CachedRequest(this.api, 'getUserWorkspaces');
11
12 constructor(stores, api, actions, state) {
13 super(stores, api, actions);
14 this.state = state;
15 }
16
17 setup() {
18 debug('fetching workspaces');
19 this.allWorkspacesRequest.execute();
20
21 /**
22 * Update the state workspaces array when workspaces request has results.
23 */
24 reaction(
25 () => this.allWorkspacesRequest.result,
26 workspaces => this._setWorkspaces(workspaces),
27 );
28 /**
29 * Update the loading state when workspace request is executing.
30 */
31 reaction(
32 () => this.allWorkspacesRequest.isExecuting,
33 isExecuting => this._setIsLoading(isExecuting),
34 );
35 /**
36 * Update the state with the workspace to be edited when route matches.
37 */
38 reaction(
39 () => ({
40 pathname: this.stores.router.location.pathname,
41 workspaces: this.state.workspaces,
42 }),
43 ({ pathname }) => {
44 const match = matchRoute('/settings/workspaces/edit/:id', pathname);
45 if (match) {
46 this.state.workspaceBeingEdited = this._getWorkspaceById(match.id);
47 }
48 },
49 );
50
51 this.actions.workspace.edit.listen(this._edit);
52 this.actions.workspace.create.listen(this._create);
53 this.actions.workspace.delete.listen(this._delete);
54 }
55
56 _setWorkspaces = (workspaces) => {
57 debug('setting user workspaces', workspaces.slice());
58 this.state.workspaces = workspaces.map(data => new Workspace(data));
59 };
60
61 _setIsLoading = (isLoading) => {
62 this.state.isLoading = isLoading;
63 };
64
65 _getWorkspaceById = id => this.state.workspaces.find(w => w.id === id);
66
67 _edit = ({ workspace }) => {
68 this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`);
69 };
70
71 _create = async ({ name }) => {
72 try {
73 const result = await this.api.createWorkspace(name);
74 const workspace = new Workspace(result);
75 this.state.workspaces.push(workspace);
76 this._edit({ workspace });
77 } catch (error) {
78 throw error;
79 }
80 };
81
82 _delete = async ({ workspace }) => {
83 try {
84 await this.api.deleteWorkspace(workspace);
85 this.state.workspaces.remove(workspace);
86 this.stores.router.push('/settings/workspaces');
87 } catch (error) {
88 throw error;
89 }
90 };
91}
diff --git a/src/features/workspaces/styles/workspaces-table.scss b/src/features/workspaces/styles/workspaces-table.scss
new file mode 100644
index 000000000..6d0e7b4f5
--- /dev/null
+++ b/src/features/workspaces/styles/workspaces-table.scss
@@ -0,0 +1,53 @@
1@import '../../../styles/config';
2
3.theme__dark .workspace-table {
4 .workspace-table__column-info .mdi { color: $dark-theme-gray-lightest; }
5
6 .workspace-table__row {
7 border-bottom: 1px solid $dark-theme-gray-darker;
8
9 &:hover { background: $dark-theme-gray-darker; }
10 &.workspace-table__row--disabled { color: $dark-theme-gray; }
11 }
12}
13
14.workspace-table {
15 width: 100%;
16
17 .workspace-table__toggle {
18 width: 60px;
19
20 .franz-form__field {
21 margin-bottom: 0;
22 }
23 }
24
25 .workspace-table__column-action {
26 width: 40px
27 }
28
29 .workspace-table__column-info {
30 width: 40px;
31
32 .mdi {
33 color: $theme-gray-light;
34 display: block;
35 font-size: 18px;
36 }
37 }
38
39 .workspace-table__row {
40 border-bottom: 1px solid $theme-gray-lightest;
41
42 &:hover {
43 cursor: initial;
44 background: $theme-gray-lightest;
45 }
46
47 &.workspace-table__row--disabled {
48 color: $theme-gray-light;
49 }
50 }
51
52 td { padding: 10px; }
53}
diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
index b5abb56d4..e1a955176 100644
--- a/src/i18n/locales/de.json
+++ b/src/i18n/locales/de.json
@@ -158,6 +158,7 @@
158 "settings.navigation.logout" : "Abmelden", 158 "settings.navigation.logout" : "Abmelden",
159 "settings.navigation.settings" : "Einstellungen", 159 "settings.navigation.settings" : "Einstellungen",
160 "settings.navigation.yourServices" : "Deine Dienste", 160 "settings.navigation.yourServices" : "Deine Dienste",
161 "settings.navigation.yourWorkspaces": "Deine Workspaces",
161 "settings.recipes.all" : "Alle Dienste", 162 "settings.recipes.all" : "Alle Dienste",
162 "settings.recipes.dev" : "Entwicklung", 163 "settings.recipes.dev" : "Entwicklung",
163 "settings.recipes.headline" : "Verfügbare Dienste", 164 "settings.recipes.headline" : "Verfügbare Dienste",
@@ -216,6 +217,13 @@
216 "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert", 217 "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert",
217 "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert", 218 "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert",
218 "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert", 219 "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert",
220 "settings.workspaces.headline": "Deine Workspaces",
221 "settings.workspace.add.form.submitButton": "Workspace erstellen",
222 "settings.workspace.add.form.name": "Name",
223 "settings.workspace.form.yourWorkspaces": "Deine Workspaces",
224 "settings.workspace.form.name": "Name",
225 "settings.workspace.form.buttonDelete": "Workspace löschen",
226 "settings.workspace.form.buttonSave": "Workspace speichern",
219 "settings.user.form.accountType.company" : "Firma", 227 "settings.user.form.accountType.company" : "Firma",
220 "settings.user.form.accountType.individual" : "Einzelperson", 228 "settings.user.form.accountType.individual" : "Einzelperson",
221 "settings.user.form.accountType.label" : "Konto-Typ", 229 "settings.user.form.accountType.label" : "Konto-Typ",
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 4e0c5575d..fe16e916f 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -97,6 +97,7 @@
97 "settings.invite.headline": "Invite Friends", 97 "settings.invite.headline": "Invite Friends",
98 "settings.navigation.availableServices": "Available services", 98 "settings.navigation.availableServices": "Available services",
99 "settings.navigation.yourServices": "Your services", 99 "settings.navigation.yourServices": "Your services",
100 "settings.navigation.yourWorkspaces": "Your workspaces",
100 "settings.navigation.account": "Account", 101 "settings.navigation.account": "Account",
101 "settings.navigation.settings": "Settings", 102 "settings.navigation.settings": "Settings",
102 "settings.navigation.inviteFriends": "Invite Friends", 103 "settings.navigation.inviteFriends": "Invite Friends",
@@ -199,6 +200,13 @@
199 "settings.user.form.accountType.individual": "Individual", 200 "settings.user.form.accountType.individual": "Individual",
200 "settings.user.form.accountType.non-profit": "Non-Profit", 201 "settings.user.form.accountType.non-profit": "Non-Profit",
201 "settings.user.form.accountType.company": "Company", 202 "settings.user.form.accountType.company": "Company",
203 "settings.workspaces.headline": "Your workspaces",
204 "settings.workspace.add.form.submitButton": "Create Workspace",
205 "settings.workspace.add.form.name": "Name",
206 "settings.workspace.form.yourWorkspaces": "Your workspaces",
207 "settings.workspace.form.name": "Name",
208 "settings.workspace.form.buttonDelete": "Delete Workspace",
209 "settings.workspace.form.buttonSave": "Save Workspace",
202 "subscription.type.free": "free", 210 "subscription.type.free": "free",
203 "subscription.type.month": "month", 211 "subscription.type.month": "month",
204 "subscription.type.year": "year", 212 "subscription.type.year": "year",
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 0adee6adf..05a620f0b 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -7,6 +7,7 @@ import delayApp from '../features/delayApp';
7import spellchecker from '../features/spellchecker'; 7import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy'; 8import serviceProxy from '../features/serviceProxy';
9import basicAuth from '../features/basicAuth'; 9import basicAuth from '../features/basicAuth';
10import workspaces from '../features/workspaces';
10 11
11import { DEFAULT_FEATURES_CONFIG } from '../config'; 12import { DEFAULT_FEATURES_CONFIG } from '../config';
12 13
@@ -37,7 +38,7 @@ export default class FeaturesStore extends Store {
37 38
38 @computed get features() { 39 @computed get features() {
39 if (this.stores.user.isLoggedIn) { 40 if (this.stores.user.isLoggedIn) {
40 return this.featuresRequest.execute().result || DEFAULT_FEATURES_CONFIG; 41 return Object.assign({}, DEFAULT_FEATURES_CONFIG, this.featuresRequest.execute().result);
41 } 42 }
42 43
43 return DEFAULT_FEATURES_CONFIG; 44 return DEFAULT_FEATURES_CONFIG;
@@ -56,5 +57,6 @@ export default class FeaturesStore extends Store {
56 spellchecker(this.stores, this.actions); 57 spellchecker(this.stores, this.actions);
57 serviceProxy(this.stores, this.actions); 58 serviceProxy(this.stores, this.actions);
58 basicAuth(this.stores, this.actions); 59 basicAuth(this.stores, this.actions);
60 workspaces(this.stores, this.actions);
59 } 61 }
60} 62}
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 784a04d3d..9ba7f5827 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -31,6 +31,9 @@ $mdi-font-path: '../node_modules/mdi/fonts';
31@import './invite.scss'; 31@import './invite.scss';
32@import './title-bar.scss'; 32@import './title-bar.scss';
33 33
34// Workspaces legacy css
35@import '../features/workspaces/styles/workspaces-table';
36
34// form 37// form
35@import './input.scss'; 38@import './input.scss';
36@import './radio.scss'; 39@import './radio.scss';