aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/delayApp/Component.js2
-rw-r--r--src/features/delayApp/index.js2
-rw-r--r--src/features/spellchecker/index.js2
-rw-r--r--src/features/utils/FeatureStore.js21
-rw-r--r--src/features/workspaces/actions.js26
-rw-r--r--src/features/workspaces/api.js66
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.js99
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js189
-rw-r--r--src/features/workspaces/components/ServiceListItem.js48
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js167
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js101
-rw-r--r--src/features/workspaces/components/WorkspaceItem.js42
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js87
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js187
-rw-r--r--src/features/workspaces/containers/EditWorkspaceScreen.js60
-rw-r--r--src/features/workspaces/containers/WorkspacesScreen.js42
-rw-r--r--src/features/workspaces/index.js36
-rw-r--r--src/features/workspaces/models/Workspace.js25
-rw-r--r--src/features/workspaces/store.js198
-rw-r--r--src/features/workspaces/styles/workspaces-table.scss53
20 files changed, 1449 insertions, 4 deletions
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/delayApp/index.js b/src/features/delayApp/index.js
index abc8274cf..67f0fc5e6 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -55,7 +55,7 @@ export default function init(stores) {
55 55
56 setVisibility(true); 56 setVisibility(true);
57 gaPage('/delayApp'); 57 gaPage('/delayApp');
58 gaEvent('delayApp', 'show', 'Delay App Feature'); 58 gaEvent('DelayApp', 'show', 'Delay App Feature');
59 59
60 timeLastDelay = moment(); 60 timeLastDelay = moment();
61 shownAfterLaunch = true; 61 shownAfterLaunch = true;
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
index 94883ad17..79a2172b4 100644
--- a/src/features/spellchecker/index.js
+++ b/src/features/spellchecker/index.js
@@ -14,8 +14,6 @@ export default function init(stores) {
14 autorun(() => { 14 autorun(() => {
15 const { isSpellcheckerPremiumFeature } = stores.features.features; 15 const { isSpellcheckerPremiumFeature } = stores.features.features;
16 16
17 console.log('isSpellcheckerPremiumFeature', isSpellcheckerPremiumFeature);
18
19 config.isPremium = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_FEATURES_CONFIG.isSpellcheckerPremiumFeature; 17 config.isPremium = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_FEATURES_CONFIG.isSpellcheckerPremiumFeature;
20 18
21 if (!stores.user.data.isPremium && config.isPremium && stores.settings.app.enableSpellchecking) { 19 if (!stores.user.data.isPremium && config.isPremium && stores.settings.app.enableSpellchecking) {
diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.js
new file mode 100644
index 000000000..66b66a104
--- /dev/null
+++ b/src/features/utils/FeatureStore.js
@@ -0,0 +1,21 @@
1import Reaction from '../../stores/lib/Reaction';
2
3export class FeatureStore {
4 _actions = null;
5
6 _reactions = null;
7
8 _listenToActions(actions) {
9 if (this._actions) this._actions.forEach(a => a[0].off(a[1]));
10 this._actions = [];
11 actions.forEach(a => this._actions.push(a));
12 this._actions.forEach(a => a[0].listen(a[1]));
13 }
14
15 _startReactions(reactions) {
16 if (this._reactions) this._reactions.forEach(r => r.stop());
17 this._reactions = [];
18 reactions.forEach(r => this._reactions.push(new Reaction(r)));
19 this._reactions.forEach(r => r.start());
20 }
21}
diff --git a/src/features/workspaces/actions.js b/src/features/workspaces/actions.js
new file mode 100644
index 000000000..a85f8f57f
--- /dev/null
+++ b/src/features/workspaces/actions.js
@@ -0,0 +1,26 @@
1import PropTypes from 'prop-types';
2import Workspace from './models/Workspace';
3import { createActionsFromDefinitions } from '../../actions/lib/actions';
4
5export const workspaceActions = createActionsFromDefinitions({
6 edit: {
7 workspace: PropTypes.instanceOf(Workspace).isRequired,
8 },
9 create: {
10 name: PropTypes.string.isRequired,
11 },
12 delete: {
13 workspace: PropTypes.instanceOf(Workspace).isRequired,
14 },
15 update: {
16 workspace: PropTypes.instanceOf(Workspace).isRequired,
17 },
18 activate: {
19 workspace: PropTypes.instanceOf(Workspace).isRequired,
20 },
21 deactivate: {},
22 toggleWorkspaceDrawer: {},
23 openWorkspaceSettings: {},
24}, PropTypes.checkPropTypes);
25
26export default workspaceActions;
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js
new file mode 100644
index 000000000..0ec20c9ea
--- /dev/null
+++ b/src/features/workspaces/api.js
@@ -0,0 +1,66 @@
1import { pick } from 'lodash';
2import { sendAuthRequest } from '../../api/utils/auth';
3import { API, API_VERSION } from '../../environment';
4import Request from '../../stores/lib/Request';
5import Workspace from './models/Workspace';
6
7const debug = require('debug')('Franz:feature:workspaces:api');
8
9export const workspaceApi = {
10 getUserWorkspaces: async () => {
11 const url = `${API}/${API_VERSION}/workspace`;
12 debug('getUserWorkspaces GET', url);
13 const result = await sendAuthRequest(url, { method: 'GET' });
14 debug('getUserWorkspaces RESULT', result);
15 if (!result.ok) throw result;
16 const workspaces = await result.json();
17 return workspaces.map(data => new Workspace(data));
18 },
19
20 createWorkspace: async (name) => {
21 const url = `${API}/${API_VERSION}/workspace`;
22 const options = {
23 method: 'POST',
24 body: JSON.stringify({ name }),
25 };
26 debug('createWorkspace POST', url, options);
27 const result = await sendAuthRequest(url, options);
28 debug('createWorkspace RESULT', result);
29 if (!result.ok) throw result;
30 return new Workspace(await result.json());
31 },
32
33 deleteWorkspace: async (workspace) => {
34 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`;
35 debug('deleteWorkspace DELETE', url);
36 const result = await sendAuthRequest(url, { method: 'DELETE' });
37 debug('deleteWorkspace RESULT', result);
38 if (!result.ok) throw result;
39 return true;
40 },
41
42 updateWorkspace: async (workspace) => {
43 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`;
44 const options = {
45 method: 'PUT',
46 body: JSON.stringify(pick(workspace, ['name', 'services'])),
47 };
48 debug('updateWorkspace UPDATE', url, options);
49 const result = await sendAuthRequest(url, options);
50 debug('updateWorkspace RESULT', result);
51 if (!result.ok) throw result;
52 return new Workspace(await result.json());
53 },
54};
55
56export const getUserWorkspacesRequest = new Request(workspaceApi, 'getUserWorkspaces');
57export const createWorkspaceRequest = new Request(workspaceApi, 'createWorkspace');
58export const deleteWorkspaceRequest = new Request(workspaceApi, 'deleteWorkspace');
59export const updateWorkspaceRequest = new Request(workspaceApi, 'updateWorkspace');
60
61export const resetApiRequests = () => {
62 getUserWorkspacesRequest.reset();
63 createWorkspaceRequest.reset();
64 deleteWorkspaceRequest.reset();
65 updateWorkspaceRequest.reset();
66};
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js
new file mode 100644
index 000000000..0be2d528f
--- /dev/null
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -0,0 +1,99 @@
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';
7import Form from '../../../lib/Form';
8import { required } from '../../../helpers/validation-helpers';
9import { gaEvent } from '../../../lib/analytics';
10import { GA_CATEGORY_WORKSPACES } from '../index';
11
12const messages = defineMessages({
13 submitButton: {
14 id: 'settings.workspace.add.form.submitButton',
15 defaultMessage: '!!!Create workspace',
16 },
17 name: {
18 id: 'settings.workspace.add.form.name',
19 defaultMessage: '!!!Name',
20 },
21});
22
23const styles = () => ({
24 form: {
25 display: 'flex',
26 },
27 input: {
28 flexGrow: 1,
29 marginRight: '10px',
30 },
31 submitButton: {
32 height: 'inherit',
33 },
34});
35
36@injectSheet(styles) @observer
37class CreateWorkspaceForm extends Component {
38 static contextTypes = {
39 intl: intlShape,
40 };
41
42 static propTypes = {
43 classes: PropTypes.object.isRequired,
44 isSubmitting: PropTypes.bool.isRequired,
45 onSubmit: PropTypes.func.isRequired,
46 };
47
48 form = (() => {
49 const { intl } = this.context;
50 return new Form({
51 fields: {
52 name: {
53 label: intl.formatMessage(messages.name),
54 placeholder: intl.formatMessage(messages.name),
55 value: '',
56 validators: [required],
57 },
58 },
59 });
60 })();
61
62 submitForm() {
63 const { form } = this;
64 form.submit({
65 onSuccess: async (f) => {
66 const { onSubmit } = this.props;
67 const values = f.values();
68 onSubmit(values);
69 gaEvent(GA_CATEGORY_WORKSPACES, 'create', values.name);
70 },
71 });
72 }
73
74 render() {
75 const { intl } = this.context;
76 const { classes, isSubmitting } = this.props;
77 const { form } = this;
78 return (
79 <div className={classes.form}>
80 <Input
81 className={classes.input}
82 {...form.$('name').bind()}
83 showLabel={false}
84 onEnterKey={this.submitForm.bind(this, form)}
85 />
86 <Button
87 className={classes.submitButton}
88 type="submit"
89 label={intl.formatMessage(messages.submitButton)}
90 onClick={this.submitForm.bind(this, form)}
91 busy={isSubmitting}
92 buttonType={isSubmitting ? 'secondary' : 'primary'}
93 />
94 </div>
95 );
96 }
97}
98
99export default CreateWorkspaceForm;
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
new file mode 100644
index 000000000..e4bf44248
--- /dev/null
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -0,0 +1,189 @@
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';
7import injectSheet from 'react-jss';
8
9import Workspace from '../models/Workspace';
10import Service from '../../../models/Service';
11import Form from '../../../lib/Form';
12import { required } from '../../../helpers/validation-helpers';
13import ServiceListItem from './ServiceListItem';
14import Request from '../../../stores/lib/Request';
15import { gaEvent } from '../../../lib/analytics';
16import { GA_CATEGORY_WORKSPACES } from '../index';
17
18const messages = defineMessages({
19 buttonDelete: {
20 id: 'settings.workspace.form.buttonDelete',
21 defaultMessage: '!!!Delete workspace',
22 },
23 buttonSave: {
24 id: 'settings.workspace.form.buttonSave',
25 defaultMessage: '!!!Save workspace',
26 },
27 name: {
28 id: 'settings.workspace.form.name',
29 defaultMessage: '!!!Name',
30 },
31 yourWorkspaces: {
32 id: 'settings.workspace.form.yourWorkspaces',
33 defaultMessage: '!!!Your workspaces',
34 },
35 servicesInWorkspaceHeadline: {
36 id: 'settings.workspace.form.servicesInWorkspaceHeadline',
37 defaultMessage: '!!!Services in this Workspace',
38 },
39});
40
41const styles = () => ({
42 nameInput: {
43 height: 'auto',
44 },
45 serviceList: {
46 height: 'auto',
47 },
48});
49
50@injectSheet(styles) @observer
51class EditWorkspaceForm extends Component {
52 static contextTypes = {
53 intl: intlShape,
54 };
55
56 static propTypes = {
57 classes: PropTypes.object.isRequired,
58 onDelete: PropTypes.func.isRequired,
59 onSave: PropTypes.func.isRequired,
60 services: PropTypes.arrayOf(PropTypes.instanceOf(Service)).isRequired,
61 workspace: PropTypes.instanceOf(Workspace).isRequired,
62 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
63 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
64 };
65
66 form = this.prepareWorkspaceForm(this.props.workspace);
67
68 componentWillReceiveProps(nextProps) {
69 const { workspace } = this.props;
70 if (workspace.id !== nextProps.workspace.id) {
71 this.form = this.prepareWorkspaceForm(nextProps.workspace);
72 }
73 }
74
75 prepareWorkspaceForm(workspace) {
76 const { intl } = this.context;
77 return new Form({
78 fields: {
79 name: {
80 label: intl.formatMessage(messages.name),
81 placeholder: intl.formatMessage(messages.name),
82 value: workspace.name,
83 validators: [required],
84 },
85 services: {
86 value: workspace.services.slice(),
87 },
88 },
89 });
90 }
91
92 save(form) {
93 form.submit({
94 onSuccess: async (f) => {
95 const { onSave } = this.props;
96 const values = f.values();
97 onSave(values);
98 gaEvent(GA_CATEGORY_WORKSPACES, 'save');
99 },
100 onError: async () => {},
101 });
102 }
103
104 delete() {
105 const { onDelete } = this.props;
106 onDelete();
107 gaEvent(GA_CATEGORY_WORKSPACES, 'delete');
108 }
109
110 toggleService(service) {
111 const servicesField = this.form.$('services');
112 const serviceIds = servicesField.value;
113 if (serviceIds.includes(service.id)) {
114 serviceIds.splice(serviceIds.indexOf(service.id), 1);
115 } else {
116 serviceIds.push(service.id);
117 }
118 servicesField.set(serviceIds);
119 }
120
121 render() {
122 const { intl } = this.context;
123 const {
124 classes,
125 workspace,
126 services,
127 deleteWorkspaceRequest,
128 updateWorkspaceRequest,
129 } = this.props;
130 const { form } = this;
131 const workspaceServices = form.$('services').value;
132 const isDeleting = deleteWorkspaceRequest.isExecuting;
133 const isSaving = updateWorkspaceRequest.isExecuting;
134 return (
135 <div className="settings__main">
136 <div className="settings__header">
137 <span className="settings__header-item">
138 <Link to="/settings/workspaces">
139 {intl.formatMessage(messages.yourWorkspaces)}
140 </Link>
141 </span>
142 <span className="separator" />
143 <span className="settings__header-item">
144 {workspace.name}
145 </span>
146 </div>
147 <div className="settings__body">
148 <div className={classes.nameInput}>
149 <Input {...form.$('name').bind()} />
150 </div>
151 <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2>
152 <div className={classes.serviceList}>
153 {services.map(s => (
154 <ServiceListItem
155 key={s.id}
156 service={s}
157 isInWorkspace={workspaceServices.includes(s.id)}
158 onToggle={() => this.toggleService(s)}
159 />
160 ))}
161 </div>
162 </div>
163 <div className="settings__controls">
164 {/* ===== Delete Button ===== */}
165 <Button
166 label={intl.formatMessage(messages.buttonDelete)}
167 loaded={false}
168 busy={isDeleting}
169 buttonType={isDeleting ? 'secondary' : 'danger'}
170 className="settings__delete-button"
171 disabled={isDeleting}
172 onClick={this.delete.bind(this)}
173 />
174 {/* ===== Save Button ===== */}
175 <Button
176 type="submit"
177 label={intl.formatMessage(messages.buttonSave)}
178 busy={isSaving}
179 buttonType={isSaving ? 'secondary' : 'primary'}
180 onClick={this.save.bind(this, form)}
181 disabled={isSaving}
182 />
183 </div>
184 </div>
185 );
186 }
187}
188
189export default EditWorkspaceForm;
diff --git a/src/features/workspaces/components/ServiceListItem.js b/src/features/workspaces/components/ServiceListItem.js
new file mode 100644
index 000000000..146cc5a36
--- /dev/null
+++ b/src/features/workspaces/components/ServiceListItem.js
@@ -0,0 +1,48 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { Toggle } from '@meetfranz/forms';
6
7import Service from '../../../models/Service';
8
9const styles = () => ({
10 service: {
11 height: 'auto',
12 display: 'flex',
13 },
14 name: {
15 marginTop: '4px',
16 },
17});
18
19@injectSheet(styles) @observer
20class ServiceListItem extends Component {
21 static propTypes = {
22 classes: PropTypes.object.isRequired,
23 isInWorkspace: PropTypes.bool.isRequired,
24 onToggle: PropTypes.func.isRequired,
25 service: PropTypes.instanceOf(Service).isRequired,
26 };
27
28 render() {
29 const {
30 classes,
31 isInWorkspace,
32 onToggle,
33 service,
34 } = this.props;
35
36 return (
37 <div className={classes.service}>
38 <Toggle
39 checked={isInWorkspace}
40 onChange={onToggle}
41 label={service.name}
42 />
43 </div>
44 );
45 }
46}
47
48export default ServiceListItem;
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
new file mode 100644
index 000000000..6eacafa68
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -0,0 +1,167 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, FormattedHTMLMessage, intlShape } from 'react-intl';
6import { H1, Icon } from '@meetfranz/ui';
7import { Button } from '@meetfranz/forms/lib';
8import ReactTooltip from 'react-tooltip';
9
10import WorkspaceDrawerItem from './WorkspaceDrawerItem';
11import { workspaceActions } from '../actions';
12import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index';
13import { gaEvent } from '../../../lib/analytics';
14
15const messages = defineMessages({
16 headline: {
17 id: 'workspaceDrawer.headline',
18 defaultMessage: '!!!Workspaces',
19 },
20 allServices: {
21 id: 'workspaceDrawer.allServices',
22 defaultMessage: '!!!All services',
23 },
24 addWorkspaceTooltip: {
25 id: 'workspaceDrawer.addWorkspaceTooltip',
26 defaultMessage: '!!!Add workspace',
27 },
28 workspaceFeatureInfo: {
29 id: 'workspaceDrawer.workspaceFeatureInfo',
30 defaultMessage: '!!!Info about workspace feature',
31 },
32 premiumCtaButtonLabel: {
33 id: 'workspaceDrawer.premiumCtaButtonLabel',
34 defaultMessage: '!!!Create your first workspace',
35 },
36});
37
38const styles = theme => ({
39 drawer: {
40 backgroundColor: theme.workspaceDrawerBackground,
41 width: `${theme.workspaceDrawerWidth}px`,
42 },
43 headline: {
44 fontSize: '24px',
45 marginTop: '38px',
46 marginBottom: '25px',
47 marginLeft: `${theme.workspaceDrawerPadding}px`,
48 },
49 addWorkspaceButton: {
50 float: 'right',
51 marginRight: `${theme.workspaceDrawerPadding}px`,
52 marginTop: '2px',
53 },
54 addWorkspaceButtonIcon: {
55 fill: theme.workspaceDrawerAddButtonColor,
56 '&:hover': {
57 fill: theme.workspaceDrawerAddButtonHoverColor,
58 },
59 },
60 workspaces: {
61 height: 'auto',
62 },
63 premiumAnnouncement: {
64 padding: '20px',
65 paddingTop: '0',
66 height: 'auto',
67 },
68 premiumCtaButton: {
69 marginTop: '20px',
70 width: '100%',
71 color: 'white !important',
72 },
73});
74
75@injectSheet(styles) @observer
76class WorkspaceDrawer extends Component {
77 static propTypes = {
78 classes: PropTypes.object.isRequired,
79 getServicesForWorkspace: PropTypes.func.isRequired,
80 };
81
82 static contextTypes = {
83 intl: intlShape,
84 };
85
86 componentDidMount() {
87 ReactTooltip.rebuild();
88 }
89
90 render() {
91 const {
92 classes,
93 getServicesForWorkspace,
94 } = this.props;
95 const { intl } = this.context;
96 const {
97 activeWorkspace,
98 isSwitchingWorkspace,
99 nextWorkspace,
100 workspaces,
101 } = workspaceStore;
102 const actualWorkspace = isSwitchingWorkspace ? nextWorkspace : activeWorkspace;
103 return (
104 <div className={classes.drawer}>
105 <H1 className={classes.headline}>
106 {intl.formatMessage(messages.headline)}
107 <span
108 className={classes.addWorkspaceButton}
109 onClick={() => {
110 workspaceActions.openWorkspaceSettings();
111 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerHeadline');
112 }}
113 data-tip={`${intl.formatMessage(messages.addWorkspaceTooltip)}`}
114 >
115 <Icon
116 icon="mdiPlusBox"
117 size={1.5}
118 className={classes.addWorkspaceButtonIcon}
119 />
120 </span>
121 </H1>
122 {workspaceStore.isPremiumUpgradeRequired ? (
123 <div className={classes.premiumAnnouncement}>
124 <FormattedHTMLMessage {...messages.workspaceFeatureInfo} />
125 <Button
126 className={classes.premiumCtaButton}
127 buttonType="primary"
128 label={intl.formatMessage(messages.premiumCtaButtonLabel)}
129 icon="mdiPlusBox"
130 onClick={() => {
131 workspaceActions.openWorkspaceSettings();
132 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta');
133 }}
134 />
135 </div>
136 ) : (
137 <div className={classes.workspaces}>
138 <WorkspaceDrawerItem
139 name={intl.formatMessage(messages.allServices)}
140 onClick={() => {
141 workspaceActions.deactivate();
142 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
143 }}
144 services={getServicesForWorkspace(null)}
145 isActive={actualWorkspace == null}
146 />
147 {workspaces.map(workspace => (
148 <WorkspaceDrawerItem
149 key={workspace.id}
150 name={workspace.name}
151 isActive={actualWorkspace === workspace}
152 onClick={() => {
153 workspaceActions.activate({ workspace });
154 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
155 }}
156 services={getServicesForWorkspace(workspace)}
157 />
158 ))}
159 </div>
160 )}
161 <ReactTooltip place="right" type="dark" effect="solid" />
162 </div>
163 );
164 }
165}
166
167export default WorkspaceDrawer;
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
new file mode 100644
index 000000000..1e28ebea6
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -0,0 +1,101 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import classnames from 'classnames';
6import { defineMessages, intlShape } from 'react-intl';
7
8const messages = defineMessages({
9 noServicesAddedYet: {
10 id: 'workspaceDrawer.item.noServicesAddedYet',
11 defaultMessage: '!!!No services added yet',
12 },
13});
14
15const styles = theme => ({
16 item: {
17 height: '67px',
18 padding: `15px ${theme.workspaceDrawerPadding}px`,
19 borderBottom: `1px solid ${theme.workspaceDrawerItemBorder}`,
20 '&:first-child': {
21 borderTop: `1px solid ${theme.workspaceDrawerItemBorder}`,
22 },
23 },
24 isActiveItem: {
25 backgroundColor: theme.workspaceDrawerItemActiveBackground,
26 },
27 name: {
28 marginTop: '4px',
29 color: theme.workspaceDrawerItemNameColor,
30 },
31 activeName: {
32 color: theme.workspaceDrawerItemNameActiveColor,
33 },
34 services: {
35 display: 'block',
36 fontSize: '11px',
37 marginTop: '5px',
38 color: theme.workspaceDrawerServicesColor,
39 whiteSpace: 'nowrap',
40 textOverflow: 'ellipsis',
41 overflow: 'hidden',
42 lineHeight: '15px',
43 },
44 activeServices: {
45 color: theme.workspaceDrawerServicesActiveColor,
46 },
47});
48
49@injectSheet(styles) @observer
50class WorkspaceDrawerItem extends Component {
51 static propTypes = {
52 classes: PropTypes.object.isRequired,
53 isActive: PropTypes.bool.isRequired,
54 name: PropTypes.string.isRequired,
55 onClick: PropTypes.func.isRequired,
56 services: PropTypes.arrayOf(PropTypes.string).isRequired,
57 };
58
59 static contextTypes = {
60 intl: intlShape,
61 };
62
63 render() {
64 const {
65 classes,
66 isActive,
67 name,
68 onClick,
69 services,
70 } = this.props;
71 const { intl } = this.context;
72 return (
73 <div
74 className={classnames([
75 classes.item,
76 isActive ? classes.isActiveItem : null,
77 ])}
78 onClick={onClick}
79 >
80 <span
81 className={classnames([
82 classes.name,
83 isActive ? classes.activeName : null,
84 ])}
85 >
86 {name}
87 </span>
88 <span
89 className={classnames([
90 classes.services,
91 isActive ? classes.activeServices : null,
92 ])}
93 >
94 {services.length ? services.join(', ') : intl.formatMessage(messages.noServicesAddedYet)}
95 </span>
96 </div>
97 );
98 }
99}
100
101export default WorkspaceDrawerItem;
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/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
new file mode 100644
index 000000000..8aba5bbd9
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -0,0 +1,87 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import classnames from 'classnames';
6import { defineMessages, intlShape } from 'react-intl';
7
8import LoaderComponent from '../../../components/ui/Loader';
9import { workspaceStore } from '../index';
10
11const messages = defineMessages({
12 switchingTo: {
13 id: 'workspaces.switchingIndicator.switchingTo',
14 defaultMessage: '!!!Switching to',
15 },
16});
17
18const styles = theme => ({
19 wrapper: {
20 display: 'flex',
21 alignItems: 'flex-start',
22 position: 'absolute',
23 transition: 'width 0.5s ease',
24 width: '100%',
25 marginTop: '20px',
26 },
27 wrapperWhenDrawerIsOpen: {
28 width: `calc(100% - ${theme.workspaceDrawerWidth}px)`,
29 },
30 component: {
31 background: 'rgba(20, 20, 20, 0.4)',
32 padding: '10px 20px',
33 display: 'flex',
34 width: 'auto',
35 height: 'auto',
36 margin: [0, 'auto'],
37 borderRadius: 6,
38 alignItems: 'center',
39 zIndex: 200,
40 },
41 spinner: {
42 width: '40px',
43 marginRight: '5px',
44 },
45 message: {
46 fontSize: 16,
47 whiteSpace: 'nowrap',
48 },
49});
50
51@injectSheet(styles) @observer
52class WorkspaceSwitchingIndicator extends Component {
53 static propTypes = {
54 classes: PropTypes.object.isRequired,
55 };
56
57 static contextTypes = {
58 intl: intlShape,
59 };
60
61 render() {
62 const { classes } = this.props;
63 const { intl } = this.context;
64 const { isSwitchingWorkspace, isWorkspaceDrawerOpen, nextWorkspace } = workspaceStore;
65 if (!isSwitchingWorkspace) return null;
66 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services';
67 return (
68 <div
69 className={classnames([
70 classes.wrapper,
71 isWorkspaceDrawerOpen ? classes.wrapperWhenDrawerIsOpen : null,
72 ])}
73 >
74 <div className={classes.component}>
75 <div className={classes.spinner}>
76 <LoaderComponent />
77 </div>
78 <p className={classes.message}>
79 {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`}
80 </p>
81 </div>
82 </div>
83 );
84 }
85}
86
87export default WorkspaceSwitchingIndicator;
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
new file mode 100644
index 000000000..b141dc960
--- /dev/null
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -0,0 +1,187 @@
1import React, { Component, Fragment } 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';
6import { Infobox } from '@meetfranz/ui';
7
8import Loader from '../../../components/ui/Loader';
9import WorkspaceItem from './WorkspaceItem';
10import CreateWorkspaceForm from './CreateWorkspaceForm';
11import Request from '../../../stores/lib/Request';
12import Appear from '../../../components/ui/effects/Appear';
13import { workspaceStore } from '../index';
14import PremiumFeatureContainer from '../../../components/ui/PremiumFeatureContainer';
15
16const messages = defineMessages({
17 headline: {
18 id: 'settings.workspaces.headline',
19 defaultMessage: '!!!Your workspaces',
20 },
21 noServicesAdded: {
22 id: 'settings.workspaces.noWorkspacesAdded',
23 defaultMessage: '!!!You haven\'t added any workspaces yet.',
24 },
25 workspacesRequestFailed: {
26 id: 'settings.workspaces.workspacesRequestFailed',
27 defaultMessage: '!!!Could not load your workspaces',
28 },
29 tryReloadWorkspaces: {
30 id: 'settings.workspaces.tryReloadWorkspaces',
31 defaultMessage: '!!!Try again',
32 },
33 updatedInfo: {
34 id: 'settings.workspaces.updatedInfo',
35 defaultMessage: '!!!Your changes have been saved',
36 },
37 deletedInfo: {
38 id: 'settings.workspaces.deletedInfo',
39 defaultMessage: '!!!Workspace has been deleted',
40 },
41 workspaceFeatureInfo: {
42 id: 'settings.workspaces.workspaceFeatureInfo',
43 defaultMessage: '!!!Info about workspace feature',
44 },
45 workspaceFeatureHeadline: {
46 id: 'settings.workspaces.workspaceFeatureHeadline',
47 defaultMessage: '!!!Less is More: Introducing Franz Workspaces',
48 },
49});
50
51const styles = () => ({
52 createForm: {
53 height: 'auto',
54 },
55 appear: {
56 height: 'auto',
57 },
58 premiumAnnouncement: {
59 padding: '20px',
60 backgroundColor: '#3498db',
61 marginLeft: '-20px',
62 marginBottom: '20px',
63 height: 'auto',
64 },
65});
66
67@injectSheet(styles) @observer
68class WorkspacesDashboard extends Component {
69 static propTypes = {
70 classes: PropTypes.object.isRequired,
71 getUserWorkspacesRequest: PropTypes.instanceOf(Request).isRequired,
72 createWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
73 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
74 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
75 onCreateWorkspaceSubmit: PropTypes.func.isRequired,
76 onWorkspaceClick: PropTypes.func.isRequired,
77 workspaces: MobxPropTypes.arrayOrObservableArray.isRequired,
78 };
79
80 static contextTypes = {
81 intl: intlShape,
82 };
83
84 render() {
85 const {
86 classes,
87 getUserWorkspacesRequest,
88 createWorkspaceRequest,
89 deleteWorkspaceRequest,
90 updateWorkspaceRequest,
91 onCreateWorkspaceSubmit,
92 onWorkspaceClick,
93 workspaces,
94 } = this.props;
95 const { intl } = this.context;
96 return (
97 <div className="settings__main">
98 <div className="settings__header">
99 <h1>{intl.formatMessage(messages.headline)}</h1>
100 </div>
101 <div className="settings__body">
102
103 {/* ===== Workspace updated info ===== */}
104 {updateWorkspaceRequest.wasExecuted && updateWorkspaceRequest.result && (
105 <Appear className={classes.appear}>
106 <Infobox
107 type="success"
108 icon="mdiCheckboxMarkedCircleOutline"
109 dismissable
110 onUnmount={updateWorkspaceRequest.reset}
111 >
112 {intl.formatMessage(messages.updatedInfo)}
113 </Infobox>
114 </Appear>
115 )}
116
117 {/* ===== Workspace deleted info ===== */}
118 {deleteWorkspaceRequest.wasExecuted && deleteWorkspaceRequest.result && (
119 <Appear className={classes.appear}>
120 <Infobox
121 type="success"
122 icon="mdiCheckboxMarkedCircleOutline"
123 dismissable
124 onUnmount={deleteWorkspaceRequest.reset}
125 >
126 {intl.formatMessage(messages.deletedInfo)}
127 </Infobox>
128 </Appear>
129 )}
130
131 {workspaceStore.isPremiumUpgradeRequired && (
132 <div className={classes.premiumAnnouncement}>
133 <h2>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h2>
134 <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p>
135 </div>
136 )}
137
138 <PremiumFeatureContainer
139 condition={workspaceStore.isPremiumFeature}
140 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'workspaces' }}
141 >
142 {/* ===== Create workspace form ===== */}
143 <div className={classes.createForm}>
144 <CreateWorkspaceForm
145 isSubmitting={createWorkspaceRequest.isExecuting}
146 onSubmit={onCreateWorkspaceSubmit}
147 />
148 </div>
149 </PremiumFeatureContainer>
150 {getUserWorkspacesRequest.isExecuting ? (
151 <Loader />
152 ) : (
153 <Fragment>
154 {/* ===== Workspace could not be loaded error ===== */}
155 {getUserWorkspacesRequest.error ? (
156 <Infobox
157 icon="alert"
158 type="danger"
159 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)}
160 ctaLoading={getUserWorkspacesRequest.isExecuting}
161 ctaOnClick={getUserWorkspacesRequest.retry}
162 >
163 {intl.formatMessage(messages.workspacesRequestFailed)}
164 </Infobox>
165 ) : (
166 <table className="workspace-table">
167 {/* ===== Workspaces list ===== */}
168 <tbody>
169 {workspaces.map(workspace => (
170 <WorkspaceItem
171 key={workspace.id}
172 workspace={workspace}
173 onItemClick={w => onWorkspaceClick(w)}
174 />
175 ))}
176 </tbody>
177 </table>
178 )}
179 </Fragment>
180 )}
181 </div>
182 </div>
183 );
184 }
185}
186
187export default WorkspacesDashboard;
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js
new file mode 100644
index 000000000..248b40131
--- /dev/null
+++ b/src/features/workspaces/containers/EditWorkspaceScreen.js
@@ -0,0 +1,60 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4
5import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import EditWorkspaceForm from '../components/EditWorkspaceForm';
7import ServicesStore from '../../../stores/ServicesStore';
8import Workspace from '../models/Workspace';
9import { workspaceStore } from '../index';
10import { deleteWorkspaceRequest, updateWorkspaceRequest } from '../api';
11
12@inject('stores', 'actions') @observer
13class EditWorkspaceScreen extends Component {
14 static propTypes = {
15 actions: PropTypes.shape({
16 workspace: PropTypes.shape({
17 delete: PropTypes.func.isRequired,
18 }),
19 }).isRequired,
20 stores: PropTypes.shape({
21 services: PropTypes.instanceOf(ServicesStore).isRequired,
22 }).isRequired,
23 };
24
25 onDelete = () => {
26 const { workspaceBeingEdited } = workspaceStore;
27 const { actions } = this.props;
28 if (!workspaceBeingEdited) return null;
29 actions.workspaces.delete({ workspace: workspaceBeingEdited });
30 };
31
32 onSave = (values) => {
33 const { workspaceBeingEdited } = workspaceStore;
34 const { actions } = this.props;
35 const workspace = new Workspace(
36 Object.assign({}, workspaceBeingEdited, values),
37 );
38 actions.workspaces.update({ workspace });
39 };
40
41 render() {
42 const { workspaceBeingEdited } = workspaceStore;
43 const { stores } = this.props;
44 if (!workspaceBeingEdited) return null;
45 return (
46 <ErrorBoundary>
47 <EditWorkspaceForm
48 workspace={workspaceBeingEdited}
49 services={stores.services.all}
50 onDelete={this.onDelete}
51 onSave={this.onSave}
52 updateWorkspaceRequest={updateWorkspaceRequest}
53 deleteWorkspaceRequest={deleteWorkspaceRequest}
54 />
55 </ErrorBoundary>
56 );
57 }
58}
59
60export default EditWorkspaceScreen;
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js
new file mode 100644
index 000000000..2ab565fa1
--- /dev/null
+++ b/src/features/workspaces/containers/WorkspacesScreen.js
@@ -0,0 +1,42 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4import WorkspacesDashboard from '../components/WorkspacesDashboard';
5import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import { workspaceStore } from '../index';
7import {
8 createWorkspaceRequest,
9 deleteWorkspaceRequest,
10 getUserWorkspacesRequest,
11 updateWorkspaceRequest,
12} from '../api';
13
14@inject('actions') @observer
15class WorkspacesScreen extends Component {
16 static propTypes = {
17 actions: PropTypes.shape({
18 workspace: PropTypes.shape({
19 edit: PropTypes.func.isRequired,
20 }),
21 }).isRequired,
22 };
23
24 render() {
25 const { actions } = this.props;
26 return (
27 <ErrorBoundary>
28 <WorkspacesDashboard
29 workspaces={workspaceStore.workspaces}
30 getUserWorkspacesRequest={getUserWorkspacesRequest}
31 createWorkspaceRequest={createWorkspaceRequest}
32 deleteWorkspaceRequest={deleteWorkspaceRequest}
33 updateWorkspaceRequest={updateWorkspaceRequest}
34 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)}
35 onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })}
36 />
37 </ErrorBoundary>
38 );
39 }
40}
41
42export default WorkspacesScreen;
diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js
new file mode 100644
index 000000000..524a83e3c
--- /dev/null
+++ b/src/features/workspaces/index.js
@@ -0,0 +1,36 @@
1import { reaction } from 'mobx';
2import WorkspacesStore from './store';
3import { resetApiRequests } from './api';
4
5const debug = require('debug')('Franz:feature:workspaces');
6
7export const GA_CATEGORY_WORKSPACES = 'Workspaces';
8
9export const workspaceStore = new WorkspacesStore();
10
11export default function initWorkspaces(stores, actions) {
12 stores.workspaces = workspaceStore;
13 const { features, user } = stores;
14
15 // Toggle workspace feature
16 reaction(
17 () => (
18 features.features.isWorkspaceEnabled && (
19 !features.features.isWorkspacePremiumFeature || user.data.isPremium
20 )
21 ),
22 (isEnabled) => {
23 if (isEnabled && !workspaceStore.isFeatureActive) {
24 debug('Initializing `workspaces` feature');
25 workspaceStore.start(stores, actions);
26 } else if (workspaceStore.isFeatureActive) {
27 debug('Disabling `workspaces` feature');
28 workspaceStore.stop();
29 resetApiRequests();
30 }
31 },
32 {
33 fireImmediately: true,
34 },
35 );
36}
diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js
new file mode 100644
index 000000000..6c73d7095
--- /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.replace(data.services);
23 this.userId = data.userId;
24 }
25}
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
new file mode 100644
index 000000000..712945bdc
--- /dev/null
+++ b/src/features/workspaces/store.js
@@ -0,0 +1,198 @@
1import {
2 computed,
3 observable,
4 action,
5} from 'mobx';
6import { matchRoute } from '../../helpers/routing-helpers';
7import { workspaceActions } from './actions';
8import { FeatureStore } from '../utils/FeatureStore';
9import {
10 createWorkspaceRequest,
11 deleteWorkspaceRequest,
12 getUserWorkspacesRequest,
13 updateWorkspaceRequest,
14} from './api';
15
16const debug = require('debug')('Franz:feature:workspaces:store');
17
18export default class WorkspacesStore extends FeatureStore {
19 @observable isFeatureEnabled = false;
20
21 @observable isPremiumFeature = true;
22
23 @observable isFeatureActive = false;
24
25 @observable activeWorkspace = null;
26
27 @observable nextWorkspace = null;
28
29 @observable workspaceBeingEdited = null;
30
31 @observable isSwitchingWorkspace = false;
32
33 @observable isWorkspaceDrawerOpen = false;
34
35 @computed get workspaces() {
36 if (!this.isFeatureActive) return [];
37 return getUserWorkspacesRequest.result || [];
38 }
39
40 @computed get isPremiumUpgradeRequired() {
41 return this.isFeatureEnabled && !this.isFeatureActive;
42 }
43
44 start(stores, actions) {
45 debug('WorkspacesStore::start');
46 this.stores = stores;
47 this.actions = actions;
48
49 this._listenToActions([
50 [workspaceActions.edit, this._edit],
51 [workspaceActions.create, this._create],
52 [workspaceActions.delete, this._delete],
53 [workspaceActions.update, this._update],
54 [workspaceActions.activate, this._setActiveWorkspace],
55 [workspaceActions.deactivate, this._deactivateActiveWorkspace],
56 [workspaceActions.toggleWorkspaceDrawer, this._toggleWorkspaceDrawer],
57 [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings],
58 ]);
59
60 this._startReactions([
61 this._setWorkspaceBeingEditedReaction,
62 this._setActiveServiceOnWorkspaceSwitchReaction,
63 this._setFeatureEnabledReaction,
64 this._setIsPremiumFeatureReaction,
65 ]);
66
67 getUserWorkspacesRequest.execute();
68 this.isFeatureActive = true;
69 }
70
71 stop() {
72 debug('WorkspacesStore::stop');
73 this.isFeatureActive = false;
74 this.activeWorkspace = null;
75 this.nextWorkspace = null;
76 this.workspaceBeingEdited = null;
77 this.isSwitchingWorkspace = false;
78 this.isWorkspaceDrawerOpen = false;
79 }
80
81 filterServicesByActiveWorkspace = (services) => {
82 const { activeWorkspace, isFeatureActive } = this;
83
84 if (!isFeatureActive) return services;
85 if (activeWorkspace) {
86 return services.filter(s => (
87 activeWorkspace.services.includes(s.id)
88 ));
89 }
90 return services;
91 };
92
93 // ========== PRIVATE ========= //
94
95 _getWorkspaceById = id => this.workspaces.find(w => w.id === id);
96
97 // Actions
98
99 @action _edit = ({ workspace }) => {
100 this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`);
101 };
102
103 @action _create = async ({ name }) => {
104 try {
105 const workspace = await createWorkspaceRequest.execute(name);
106 await getUserWorkspacesRequest.result.push(workspace);
107 this._edit({ workspace });
108 } catch (error) {
109 throw error;
110 }
111 };
112
113 @action _delete = async ({ workspace }) => {
114 try {
115 await deleteWorkspaceRequest.execute(workspace);
116 await getUserWorkspacesRequest.result.remove(workspace);
117 this.stores.router.push('/settings/workspaces');
118 } catch (error) {
119 throw error;
120 }
121 };
122
123 @action _update = async ({ workspace }) => {
124 try {
125 await updateWorkspaceRequest.execute(workspace);
126 // Path local result optimistically
127 const localWorkspace = this._getWorkspaceById(workspace.id);
128 Object.assign(localWorkspace, workspace);
129 this.stores.router.push('/settings/workspaces');
130 } catch (error) {
131 throw error;
132 }
133 };
134
135 @action _setActiveWorkspace = ({ workspace }) => {
136 // Indicate that we are switching to another workspace
137 this.isSwitchingWorkspace = true;
138 this.nextWorkspace = workspace;
139 // Delay switching to next workspace so that the services loading does not drag down UI
140 setTimeout(() => { this.activeWorkspace = workspace; }, 100);
141 // Indicate that we are done switching to the next workspace
142 setTimeout(() => {
143 this.isSwitchingWorkspace = false;
144 this.nextWorkspace = null;
145 }, 1000);
146 };
147
148 @action _deactivateActiveWorkspace = () => {
149 // Indicate that we are switching to default workspace
150 this.isSwitchingWorkspace = true;
151 this.nextWorkspace = null;
152 // Delay switching to next workspace so that the services loading does not drag down UI
153 setTimeout(() => { this.activeWorkspace = null; }, 100);
154 // Indicate that we are done switching to the default workspace
155 setTimeout(() => { this.isSwitchingWorkspace = false; }, 1000);
156 };
157
158 @action _toggleWorkspaceDrawer = () => {
159 this.isWorkspaceDrawerOpen = !this.isWorkspaceDrawerOpen;
160 };
161
162 @action _openWorkspaceSettings = () => {
163 this.actions.ui.openSettings({ path: 'workspaces' });
164 };
165
166 // Reactions
167
168 _setFeatureEnabledReaction = () => {
169 const { isWorkspaceEnabled } = this.stores.features.features;
170 this.isFeatureEnabled = isWorkspaceEnabled;
171 };
172
173 _setIsPremiumFeatureReaction = () => {
174 const { isWorkspacePremiumFeature } = this.stores.features.features;
175 this.isPremiumFeature = isWorkspacePremiumFeature;
176 };
177
178 _setWorkspaceBeingEditedReaction = () => {
179 const { pathname } = this.stores.router.location;
180 const match = matchRoute('/settings/workspaces/edit/:id', pathname);
181 if (match) {
182 this.workspaceBeingEdited = this._getWorkspaceById(match.id);
183 }
184 };
185
186 _setActiveServiceOnWorkspaceSwitchReaction = () => {
187 if (!this.isFeatureActive) return;
188 if (this.activeWorkspace) {
189 const services = this.stores.services.allDisplayed;
190 const activeService = services.find(s => s.isActive);
191 const workspaceServices = this.filterServicesByActiveWorkspace(services);
192 const isActiveServiceInWorkspace = workspaceServices.includes(activeService);
193 if (!isActiveServiceInWorkspace) {
194 this.actions.service.setActive({ serviceId: workspaceServices[0].id });
195 }
196 }
197 };
198}
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}