diff options
Diffstat (limited to 'src')
30 files changed, 1069 insertions, 25 deletions
diff --git a/src/actions/index.js b/src/actions/index.js index 59acabb0b..00f843cd6 100644 --- a/src/actions/index.js +++ b/src/actions/index.js | |||
@@ -11,6 +11,7 @@ import payment from './payment'; | |||
11 | import news from './news'; | 11 | import news from './news'; |
12 | import settings from './settings'; | 12 | import settings from './settings'; |
13 | import requests from './requests'; | 13 | import requests from './requests'; |
14 | import workspaces from '../features/workspaces/actions'; | ||
14 | 15 | ||
15 | const actions = Object.assign({}, { | 16 | const actions = Object.assign({}, { |
16 | service, | 17 | service, |
@@ -25,4 +26,7 @@ const actions = Object.assign({}, { | |||
25 | requests, | 26 | requests, |
26 | }); | 27 | }); |
27 | 28 | ||
28 | export default defineActions(actions, PropTypes.checkPropTypes); | 29 | export default Object.assign( |
30 | defineActions(actions, PropTypes.checkPropTypes), | ||
31 | { workspaces }, | ||
32 | ); | ||
diff --git a/src/actions/lib/actions.js b/src/actions/lib/actions.js index 499018d70..6571e9441 100644 --- a/src/actions/lib/actions.js +++ b/src/actions/lib/actions.js | |||
@@ -1,18 +1,23 @@ | |||
1 | export const createActionsFromDefinitions = (actionDefinitions, validate) => { | ||
2 | const actions = {}; | ||
3 | Object.keys(actionDefinitions).forEach((actionName) => { | ||
4 | const action = (params) => { | ||
5 | const schema = actionDefinitions[actionName]; | ||
6 | validate(schema, params, actionName); | ||
7 | action.notify(params); | ||
8 | }; | ||
9 | actions[actionName] = action; | ||
10 | action.listeners = []; | ||
11 | action.listen = listener => action.listeners.push(listener); | ||
12 | action.notify = params => action.listeners.forEach(listener => listener(params)); | ||
13 | }); | ||
14 | return actions; | ||
15 | }; | ||
16 | |||
1 | export default (definitions, validate) => { | 17 | export default (definitions, validate) => { |
2 | const newActions = {}; | 18 | const newActions = {}; |
3 | Object.keys(definitions).forEach((scopeName) => { | 19 | Object.keys(definitions).forEach((scopeName) => { |
4 | newActions[scopeName] = {}; | 20 | newActions[scopeName] = createActionsFromDefinitions(definitions[scopeName], validate); |
5 | Object.keys(definitions[scopeName]).forEach((actionName) => { | ||
6 | const action = (params) => { | ||
7 | const schema = definitions[scopeName][actionName]; | ||
8 | validate(schema, params, actionName); | ||
9 | action.notify(params); | ||
10 | }; | ||
11 | newActions[scopeName][actionName] = action; | ||
12 | action.listeners = []; | ||
13 | action.listen = listener => action.listeners.push(listener); | ||
14 | action.notify = params => action.listeners.forEach(listener => listener(params)); | ||
15 | }); | ||
16 | }); | 21 | }); |
17 | return newActions; | 22 | return newActions; |
18 | }; | 23 | }; |
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 @@ | |||
1 | import { remote } from 'electron'; | ||
2 | import localStorage from 'mobx-localstorage'; | ||
3 | |||
4 | const { app } = remote; | ||
5 | |||
6 | export 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 | |||
26 | export 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'; | |||
39 | import InviteScreen from './containers/auth/InviteScreen'; | 39 | import InviteScreen from './containers/auth/InviteScreen'; |
40 | import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; | 40 | import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; |
41 | import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen'; | 41 | import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen'; |
42 | import WorkspacesScreen from './features/workspaces/containers/WorkspacesScreen'; | ||
43 | import EditWorkspaceScreen from './features/workspaces/containers/EditWorkspaceScreen'; | ||
42 | 44 | ||
43 | // Add Polyfills | 45 | // Add Polyfills |
44 | smoothScroll.polyfill(); | 46 | smoothScroll.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/layout/Sidebar.js b/src/components/layout/Sidebar.js index 609a3b604..fcc5b0001 100644 --- a/src/components/layout/Sidebar.js +++ b/src/components/layout/Sidebar.js | |||
@@ -31,7 +31,7 @@ export default @observer class Sidebar extends Component { | |||
31 | openSettings: PropTypes.func.isRequired, | 31 | openSettings: PropTypes.func.isRequired, |
32 | toggleMuteApp: PropTypes.func.isRequired, | 32 | toggleMuteApp: PropTypes.func.isRequired, |
33 | isAppMuted: PropTypes.bool.isRequired, | 33 | isAppMuted: PropTypes.bool.isRequired, |
34 | } | 34 | }; |
35 | 35 | ||
36 | static contextTypes = { | 36 | static contextTypes = { |
37 | intl: intlShape, | 37 | intl: intlShape, |
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js index dd5c2140f..5e8260ad0 100644 --- a/src/components/services/tabs/Tabbar.js +++ b/src/components/services/tabs/Tabbar.js | |||
@@ -19,7 +19,7 @@ export default @observer class TabBar extends Component { | |||
19 | updateService: PropTypes.func.isRequired, | 19 | updateService: PropTypes.func.isRequired, |
20 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, | 20 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, |
21 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, | 21 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, |
22 | } | 22 | }; |
23 | 23 | ||
24 | onSortEnd = ({ oldIndex, newIndex }) => { | 24 | onSortEnd = ({ oldIndex, newIndex }) => { |
25 | const { | 25 | const { |
@@ -45,7 +45,7 @@ export default @observer class TabBar extends Component { | |||
45 | redirect: false, | 45 | redirect: false, |
46 | }); | 46 | }); |
47 | } | 47 | } |
48 | } | 48 | }; |
49 | 49 | ||
50 | disableService({ serviceId }) { | 50 | disableService({ serviceId }) { |
51 | this.toggleService({ serviceId, isEnabled: false }); | 51 | this.toggleService({ serviceId, isEnabled: false }); |
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 a782ad667..30a5a5cc0 100644 --- a/src/config.js +++ b/src/config.js | |||
@@ -41,6 +41,8 @@ export const DEFAULT_FEATURES_CONFIG = { | |||
41 | }, | 41 | }, |
42 | isServiceProxyEnabled: false, | 42 | isServiceProxyEnabled: false, |
43 | isServiceProxyPremiumFeature: true, | 43 | isServiceProxyPremiumFeature: true, |
44 | isWorkspacePremiumFeature: true, | ||
45 | isWorkspaceEnabled: true, | ||
44 | }; | 46 | }; |
45 | 47 | ||
46 | export const DEFAULT_WINDOW_OPTIONS = { | 48 | export 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 | ||
30 | export const API = api; | 30 | export const API = api; |
31 | export 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..25246de09 --- /dev/null +++ b/src/features/workspaces/actions.js | |||
@@ -0,0 +1,22 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import Workspace from './models/Workspace'; | ||
3 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
4 | |||
5 | export default 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 | }, PropTypes.checkPropTypes); | ||
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js new file mode 100644 index 000000000..733cb5593 --- /dev/null +++ b/src/features/workspaces/api.js | |||
@@ -0,0 +1,39 @@ | |||
1 | import { pick } from 'lodash'; | ||
2 | import { sendAuthRequest } from '../../api/utils/auth'; | ||
3 | import { API, API_VERSION } from '../../environment'; | ||
4 | |||
5 | export default { | ||
6 | getUserWorkspaces: async () => { | ||
7 | const url = `${API}/${API_VERSION}/workspace`; | ||
8 | const request = await sendAuthRequest(url, { method: 'GET' }); | ||
9 | if (!request.ok) throw request; | ||
10 | return request.json(); | ||
11 | }, | ||
12 | |||
13 | createWorkspace: async (name) => { | ||
14 | const url = `${API}/${API_VERSION}/workspace`; | ||
15 | const request = await sendAuthRequest(url, { | ||
16 | method: 'POST', | ||
17 | body: JSON.stringify({ name }), | ||
18 | }); | ||
19 | if (!request.ok) throw request; | ||
20 | return request.json(); | ||
21 | }, | ||
22 | |||
23 | deleteWorkspace: async (workspace) => { | ||
24 | const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; | ||
25 | const request = await sendAuthRequest(url, { method: 'DELETE' }); | ||
26 | if (!request.ok) throw request; | ||
27 | return request.json(); | ||
28 | }, | ||
29 | |||
30 | updateWorkspace: async (workspace) => { | ||
31 | const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; | ||
32 | const request = await sendAuthRequest(url, { | ||
33 | method: 'PUT', | ||
34 | body: JSON.stringify(pick(workspace, ['name', 'services'])), | ||
35 | }); | ||
36 | if (!request.ok) throw request; | ||
37 | return request.json(); | ||
38 | }, | ||
39 | }; | ||
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js new file mode 100644 index 000000000..83f6e07f7 --- /dev/null +++ b/src/features/workspaces/components/CreateWorkspaceForm.js | |||
@@ -0,0 +1,93 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import { Input, Button } from '@meetfranz/forms'; | ||
6 | import injectSheet from 'react-jss'; | ||
7 | import Form from '../../../lib/Form'; | ||
8 | import { required } from '../../../helpers/validation-helpers'; | ||
9 | |||
10 | const 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 | |||
21 | const styles = () => ({ | ||
22 | form: { | ||
23 | display: 'flex', | ||
24 | }, | ||
25 | input: { | ||
26 | flexGrow: 1, | ||
27 | marginRight: '10px', | ||
28 | }, | ||
29 | submitButton: { | ||
30 | height: 'inherit', | ||
31 | marginTop: '3px', | ||
32 | }, | ||
33 | }); | ||
34 | |||
35 | @injectSheet(styles) @observer | ||
36 | class 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 | form = (() => { | ||
47 | const { intl } = this.context; | ||
48 | return new Form({ | ||
49 | fields: { | ||
50 | name: { | ||
51 | label: intl.formatMessage(messages.name), | ||
52 | placeholder: intl.formatMessage(messages.name), | ||
53 | value: '', | ||
54 | validators: [required], | ||
55 | }, | ||
56 | }, | ||
57 | }); | ||
58 | })(); | ||
59 | |||
60 | submitForm() { | ||
61 | const { form } = this; | ||
62 | form.submit({ | ||
63 | onSuccess: async (f) => { | ||
64 | const { onSubmit } = this.props; | ||
65 | onSubmit(f.values()); | ||
66 | }, | ||
67 | }); | ||
68 | } | ||
69 | |||
70 | render() { | ||
71 | const { intl } = this.context; | ||
72 | const { classes } = this.props; | ||
73 | const { form } = this; | ||
74 | return ( | ||
75 | <div className={classes.form}> | ||
76 | <Input | ||
77 | className={classes.input} | ||
78 | {...form.$('name').bind()} | ||
79 | showLabel={false} | ||
80 | onEnterKey={this.submitForm.bind(this, form)} | ||
81 | /> | ||
82 | <Button | ||
83 | className={classes.submitButton} | ||
84 | type="submit" | ||
85 | label={intl.formatMessage(messages.submitButton)} | ||
86 | onClick={this.submitForm.bind(this, form)} | ||
87 | /> | ||
88 | </div> | ||
89 | ); | ||
90 | } | ||
91 | } | ||
92 | |||
93 | export default CreateWorkspaceForm; | ||
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js new file mode 100644 index 000000000..48090f608 --- /dev/null +++ b/src/features/workspaces/components/EditWorkspaceForm.js | |||
@@ -0,0 +1,192 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import { Link } from 'react-router'; | ||
6 | import { Input, Button } from '@meetfranz/forms'; | ||
7 | import injectSheet from 'react-jss'; | ||
8 | |||
9 | import Workspace from '../models/Workspace'; | ||
10 | import Service from '../../../models/Service'; | ||
11 | import Form from '../../../lib/Form'; | ||
12 | import { required } from '../../../helpers/validation-helpers'; | ||
13 | import ServiceListItem from './ServiceListItem'; | ||
14 | |||
15 | const messages = defineMessages({ | ||
16 | buttonDelete: { | ||
17 | id: 'settings.workspace.form.buttonDelete', | ||
18 | defaultMessage: '!!!Delete workspace', | ||
19 | }, | ||
20 | buttonSave: { | ||
21 | id: 'settings.workspace.form.buttonSave', | ||
22 | defaultMessage: '!!!Save workspace', | ||
23 | }, | ||
24 | name: { | ||
25 | id: 'settings.workspace.form.name', | ||
26 | defaultMessage: '!!!Name', | ||
27 | }, | ||
28 | yourWorkspaces: { | ||
29 | id: 'settings.workspace.form.yourWorkspaces', | ||
30 | defaultMessage: '!!!Your workspaces', | ||
31 | }, | ||
32 | servicesInWorkspaceHeadline: { | ||
33 | id: 'settings.workspace.form.servicesInWorkspaceHeadline', | ||
34 | defaultMessage: '!!!Services in this Workspace', | ||
35 | }, | ||
36 | }); | ||
37 | |||
38 | const styles = () => ({ | ||
39 | nameInput: { | ||
40 | height: 'auto', | ||
41 | }, | ||
42 | serviceList: { | ||
43 | height: 'auto', | ||
44 | }, | ||
45 | }); | ||
46 | |||
47 | @injectSheet(styles) @observer | ||
48 | class EditWorkspaceForm extends Component { | ||
49 | static contextTypes = { | ||
50 | intl: intlShape, | ||
51 | }; | ||
52 | |||
53 | static propTypes = { | ||
54 | classes: PropTypes.object.isRequired, | ||
55 | isDeleting: PropTypes.bool.isRequired, | ||
56 | isSaving: PropTypes.bool.isRequired, | ||
57 | onDelete: PropTypes.func.isRequired, | ||
58 | onSave: PropTypes.func.isRequired, | ||
59 | services: PropTypes.arrayOf(PropTypes.instanceOf(Service)).isRequired, | ||
60 | workspace: PropTypes.instanceOf(Workspace).isRequired, | ||
61 | }; | ||
62 | |||
63 | form = this.prepareWorkspaceForm(this.props.workspace); | ||
64 | |||
65 | componentWillReceiveProps(nextProps) { | ||
66 | const { workspace } = this.props; | ||
67 | if (workspace.id !== nextProps.workspace.id) { | ||
68 | this.form = this.prepareWorkspaceForm(nextProps.workspace); | ||
69 | } | ||
70 | } | ||
71 | |||
72 | prepareWorkspaceForm(workspace) { | ||
73 | const { intl } = this.context; | ||
74 | return new Form({ | ||
75 | fields: { | ||
76 | name: { | ||
77 | label: intl.formatMessage(messages.name), | ||
78 | placeholder: intl.formatMessage(messages.name), | ||
79 | value: workspace.name, | ||
80 | validators: [required], | ||
81 | }, | ||
82 | services: { | ||
83 | value: workspace.services.slice(), | ||
84 | }, | ||
85 | }, | ||
86 | }); | ||
87 | } | ||
88 | |||
89 | submitForm(form) { | ||
90 | form.submit({ | ||
91 | onSuccess: async (f) => { | ||
92 | const { onSave } = this.props; | ||
93 | const values = f.values(); | ||
94 | onSave(values); | ||
95 | }, | ||
96 | onError: async () => {}, | ||
97 | }); | ||
98 | } | ||
99 | |||
100 | toggleService(service) { | ||
101 | const servicesField = this.form.$('services'); | ||
102 | const serviceIds = servicesField.value; | ||
103 | if (serviceIds.includes(service.id)) { | ||
104 | serviceIds.splice(serviceIds.indexOf(service.id), 1); | ||
105 | } else { | ||
106 | serviceIds.push(service.id); | ||
107 | } | ||
108 | servicesField.set(serviceIds); | ||
109 | } | ||
110 | |||
111 | render() { | ||
112 | const { intl } = this.context; | ||
113 | const { | ||
114 | classes, | ||
115 | isDeleting, | ||
116 | isSaving, | ||
117 | onDelete, | ||
118 | workspace, | ||
119 | services, | ||
120 | } = this.props; | ||
121 | const { form } = this; | ||
122 | const workspaceServices = form.$('services').value; | ||
123 | return ( | ||
124 | <div className="settings__main"> | ||
125 | <div className="settings__header"> | ||
126 | <span className="settings__header-item"> | ||
127 | <Link to="/settings/workspaces"> | ||
128 | {intl.formatMessage(messages.yourWorkspaces)} | ||
129 | </Link> | ||
130 | </span> | ||
131 | <span className="separator" /> | ||
132 | <span className="settings__header-item"> | ||
133 | {workspace.name} | ||
134 | </span> | ||
135 | </div> | ||
136 | <div className="settings__body"> | ||
137 | <div className={classes.nameInput}> | ||
138 | <Input {...form.$('name').bind()} /> | ||
139 | </div> | ||
140 | <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2> | ||
141 | <div className={classes.serviceList}> | ||
142 | {services.map(s => ( | ||
143 | <ServiceListItem | ||
144 | key={s.id} | ||
145 | service={s} | ||
146 | isInWorkspace={workspaceServices.includes(s.id)} | ||
147 | onToggle={() => this.toggleService(s)} | ||
148 | /> | ||
149 | ))} | ||
150 | </div> | ||
151 | </div> | ||
152 | <div className="settings__controls"> | ||
153 | {/* ===== Delete Button ===== */} | ||
154 | {isDeleting ? ( | ||
155 | <Button | ||
156 | label={intl.formatMessage(messages.buttonDelete)} | ||
157 | loaded={false} | ||
158 | buttonType="secondary" | ||
159 | className="settings__delete-button" | ||
160 | disabled | ||
161 | /> | ||
162 | ) : ( | ||
163 | <Button | ||
164 | buttonType="danger" | ||
165 | label={intl.formatMessage(messages.buttonDelete)} | ||
166 | className="settings__delete-button" | ||
167 | onClick={onDelete} | ||
168 | /> | ||
169 | )} | ||
170 | {/* ===== Save Button ===== */} | ||
171 | {isSaving ? ( | ||
172 | <Button | ||
173 | type="submit" | ||
174 | label={intl.formatMessage(messages.buttonSave)} | ||
175 | loaded={!isSaving} | ||
176 | buttonType="secondary" | ||
177 | disabled | ||
178 | /> | ||
179 | ) : ( | ||
180 | <Button | ||
181 | type="submit" | ||
182 | label={intl.formatMessage(messages.buttonSave)} | ||
183 | onClick={this.submitForm.bind(this, form)} | ||
184 | /> | ||
185 | )} | ||
186 | </div> | ||
187 | </div> | ||
188 | ); | ||
189 | } | ||
190 | } | ||
191 | |||
192 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | import { Toggle } from '@meetfranz/forms'; | ||
6 | |||
7 | import Service from '../../../models/Service'; | ||
8 | |||
9 | const styles = () => ({ | ||
10 | service: { | ||
11 | height: 'auto', | ||
12 | display: 'flex', | ||
13 | }, | ||
14 | name: { | ||
15 | marginTop: '4px', | ||
16 | }, | ||
17 | }); | ||
18 | |||
19 | @injectSheet(styles) @observer | ||
20 | class 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 | |||
48 | export default ServiceListItem; | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { intlShape } from 'react-intl'; | ||
4 | import { observer } from 'mobx-react'; | ||
5 | import classnames from 'classnames'; | ||
6 | import Workspace from '../models/Workspace'; | ||
7 | |||
8 | // const messages = defineMessages({}); | ||
9 | |||
10 | @observer | ||
11 | class 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 | |||
42 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | |||
7 | import Loader from '../../../components/ui/Loader'; | ||
8 | import WorkspaceItem from './WorkspaceItem'; | ||
9 | import CreateWorkspaceForm from './CreateWorkspaceForm'; | ||
10 | |||
11 | const 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 | |||
22 | const styles = () => ({ | ||
23 | createForm: { | ||
24 | height: 'auto', | ||
25 | marginBottom: '20px', | ||
26 | }, | ||
27 | }); | ||
28 | |||
29 | @observer @injectSheet(styles) | ||
30 | class 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 | |||
85 | export default WorkspacesDashboard; | ||
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js new file mode 100644 index 000000000..1b13bc2d4 --- /dev/null +++ b/src/features/workspaces/containers/EditWorkspaceScreen.js | |||
@@ -0,0 +1,59 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { inject, observer } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | |||
5 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
6 | import EditWorkspaceForm from '../components/EditWorkspaceForm'; | ||
7 | import { workspacesState } from '../state'; | ||
8 | import ServicesStore from '../../../stores/ServicesStore'; | ||
9 | import Workspace from '../models/Workspace'; | ||
10 | |||
11 | @inject('stores', 'actions') @observer | ||
12 | class EditWorkspaceScreen extends Component { | ||
13 | static propTypes = { | ||
14 | actions: PropTypes.shape({ | ||
15 | workspace: PropTypes.shape({ | ||
16 | delete: PropTypes.func.isRequired, | ||
17 | }), | ||
18 | }).isRequired, | ||
19 | stores: PropTypes.shape({ | ||
20 | services: PropTypes.instanceOf(ServicesStore).isRequired, | ||
21 | }).isRequired, | ||
22 | }; | ||
23 | |||
24 | onDelete = () => { | ||
25 | const { workspaceBeingEdited } = workspacesState; | ||
26 | const { actions } = this.props; | ||
27 | if (!workspaceBeingEdited) return null; | ||
28 | actions.workspaces.delete({ workspace: workspaceBeingEdited }); | ||
29 | }; | ||
30 | |||
31 | onSave = (values) => { | ||
32 | const { workspaceBeingEdited } = workspacesState; | ||
33 | const { actions } = this.props; | ||
34 | const workspace = new Workspace( | ||
35 | Object.assign({}, workspaceBeingEdited, values), | ||
36 | ); | ||
37 | actions.workspaces.update({ workspace }); | ||
38 | }; | ||
39 | |||
40 | render() { | ||
41 | const { workspaceBeingEdited } = workspacesState; | ||
42 | const { stores } = this.props; | ||
43 | if (!workspaceBeingEdited) return null; | ||
44 | return ( | ||
45 | <ErrorBoundary> | ||
46 | <EditWorkspaceForm | ||
47 | workspace={workspaceBeingEdited} | ||
48 | services={stores.services.all} | ||
49 | onDelete={this.onDelete} | ||
50 | onSave={this.onSave} | ||
51 | isDeleting={false} | ||
52 | isSaving={false} | ||
53 | /> | ||
54 | </ErrorBoundary> | ||
55 | ); | ||
56 | } | ||
57 | } | ||
58 | |||
59 | export default EditWorkspaceScreen; | ||
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js new file mode 100644 index 000000000..94e714255 --- /dev/null +++ b/src/features/workspaces/containers/WorkspacesScreen.js | |||
@@ -0,0 +1,33 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { inject, observer } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { workspacesState } from '../state'; | ||
5 | import WorkspacesDashboard from '../components/WorkspacesDashboard'; | ||
6 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
7 | |||
8 | @inject('actions') @observer | ||
9 | class WorkspacesScreen extends Component { | ||
10 | static propTypes = { | ||
11 | actions: PropTypes.shape({ | ||
12 | workspace: PropTypes.shape({ | ||
13 | edit: PropTypes.func.isRequired, | ||
14 | }), | ||
15 | }).isRequired, | ||
16 | }; | ||
17 | |||
18 | render() { | ||
19 | const { actions } = this.props; | ||
20 | return ( | ||
21 | <ErrorBoundary> | ||
22 | <WorkspacesDashboard | ||
23 | workspaces={workspacesState.workspaces} | ||
24 | isLoading={workspacesState.isLoading} | ||
25 | onCreateWorkspaceSubmit={data => actions.workspaces.create(data)} | ||
26 | onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })} | ||
27 | /> | ||
28 | </ErrorBoundary> | ||
29 | ); | ||
30 | } | ||
31 | } | ||
32 | |||
33 | export default WorkspacesScreen; | ||
diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js new file mode 100644 index 000000000..8091f49fc --- /dev/null +++ b/src/features/workspaces/index.js | |||
@@ -0,0 +1,68 @@ | |||
1 | import { reaction, runInAction } from 'mobx'; | ||
2 | import WorkspacesStore from './store'; | ||
3 | import api from './api'; | ||
4 | import { workspacesState, resetState } from './state'; | ||
5 | |||
6 | const debug = require('debug')('Franz:feature:workspaces'); | ||
7 | |||
8 | let store = null; | ||
9 | |||
10 | export const filterServicesByActiveWorkspace = (services) => { | ||
11 | const { isFeatureActive, activeWorkspace } = workspacesState; | ||
12 | if (isFeatureActive && activeWorkspace) { | ||
13 | return services.filter(s => activeWorkspace.services.includes(s.id)); | ||
14 | } | ||
15 | return services; | ||
16 | }; | ||
17 | |||
18 | export const getActiveWorkspaceServices = (services) => { | ||
19 | return filterServicesByActiveWorkspace(services); | ||
20 | }; | ||
21 | |||
22 | export default function initWorkspaces(stores, actions) { | ||
23 | const { features, user } = stores; | ||
24 | |||
25 | // Toggle workspace feature | ||
26 | reaction( | ||
27 | () => ( | ||
28 | features.features.isWorkspaceEnabled && ( | ||
29 | !features.features.isWorkspacePremiumFeature || user.data.isPremium | ||
30 | ) | ||
31 | ), | ||
32 | (isEnabled) => { | ||
33 | if (isEnabled) { | ||
34 | debug('Initializing `workspaces` feature'); | ||
35 | store = new WorkspacesStore(stores, api, actions, workspacesState); | ||
36 | store.initialize(); | ||
37 | runInAction(() => { workspacesState.isFeatureActive = true; }); | ||
38 | } else if (store) { | ||
39 | debug('Disabling `workspaces` feature'); | ||
40 | runInAction(() => { workspacesState.isFeatureActive = false; }); | ||
41 | store.teardown(); | ||
42 | store = null; | ||
43 | resetState(); // Reset state to default | ||
44 | } | ||
45 | }, | ||
46 | { | ||
47 | fireImmediately: true, | ||
48 | }, | ||
49 | ); | ||
50 | |||
51 | // Update active service on workspace switches | ||
52 | reaction(() => ({ | ||
53 | isFeatureActive: workspacesState.isFeatureActive, | ||
54 | activeWorkspace: workspacesState.activeWorkspace, | ||
55 | }), ({ isFeatureActive, activeWorkspace }) => { | ||
56 | if (!isFeatureActive) return; | ||
57 | if (activeWorkspace) { | ||
58 | const services = stores.services.allDisplayed; | ||
59 | const activeService = services.find(s => s.isActive); | ||
60 | const workspaceServices = filterServicesByActiveWorkspace(services); | ||
61 | const isActiveServiceInWorkspace = workspaceServices.includes(activeService); | ||
62 | if (!isActiveServiceInWorkspace) { | ||
63 | console.log(workspaceServices[0].id); | ||
64 | actions.service.setActive({ serviceId: workspaceServices[0].id }); | ||
65 | } | ||
66 | } | ||
67 | }); | ||
68 | } | ||
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 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | |||
3 | export 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/state.js b/src/features/workspaces/state.js new file mode 100644 index 000000000..963b96f81 --- /dev/null +++ b/src/features/workspaces/state.js | |||
@@ -0,0 +1,15 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | |||
3 | const defaultState = { | ||
4 | activeWorkspace: null, | ||
5 | isLoading: false, | ||
6 | isFeatureActive: false, | ||
7 | workspaces: [], | ||
8 | workspaceBeingEdited: null, | ||
9 | }; | ||
10 | |||
11 | export const workspacesState = observable(defaultState); | ||
12 | |||
13 | export function resetState() { | ||
14 | Object.assign(workspacesState, defaultState); | ||
15 | } | ||
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js new file mode 100644 index 000000000..a2997a0d2 --- /dev/null +++ b/src/features/workspaces/store.js | |||
@@ -0,0 +1,114 @@ | |||
1 | import { observable, reaction, action } from 'mobx'; | ||
2 | import Store from '../../stores/lib/Store'; | ||
3 | import CachedRequest from '../../stores/lib/CachedRequest'; | ||
4 | import Workspace from './models/Workspace'; | ||
5 | import { matchRoute } from '../../helpers/routing-helpers'; | ||
6 | import workspaceActions from './actions'; | ||
7 | |||
8 | const debug = require('debug')('Franz:feature:workspaces'); | ||
9 | |||
10 | export default class WorkspacesStore extends Store { | ||
11 | @observable allWorkspacesRequest = new CachedRequest(this.api, 'getUserWorkspaces'); | ||
12 | |||
13 | constructor(stores, api, actions, state) { | ||
14 | super(stores, api, actions); | ||
15 | this.state = state; | ||
16 | } | ||
17 | |||
18 | setup() { | ||
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._setIsLoading(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 | |||
52 | workspaceActions.edit.listen(this._edit); | ||
53 | workspaceActions.create.listen(this._create); | ||
54 | workspaceActions.delete.listen(this._delete); | ||
55 | workspaceActions.update.listen(this._update); | ||
56 | workspaceActions.activate.listen(this._setActiveWorkspace); | ||
57 | workspaceActions.deactivate.listen(this._deactivateActiveWorkspace); | ||
58 | } | ||
59 | |||
60 | _getWorkspaceById = id => this.state.workspaces.find(w => w.id === id); | ||
61 | |||
62 | @action _setWorkspaces = (workspaces) => { | ||
63 | debug('setting user workspaces', workspaces.slice()); | ||
64 | this.state.workspaces = workspaces.map(data => new Workspace(data)); | ||
65 | }; | ||
66 | |||
67 | @action _setIsLoading = (isLoading) => { | ||
68 | this.state.isLoading = isLoading; | ||
69 | }; | ||
70 | |||
71 | @action _edit = ({ workspace }) => { | ||
72 | this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`); | ||
73 | }; | ||
74 | |||
75 | @action _create = async ({ name }) => { | ||
76 | try { | ||
77 | const result = await this.api.createWorkspace(name); | ||
78 | const workspace = new Workspace(result); | ||
79 | this.state.workspaces.push(workspace); | ||
80 | this._edit({ workspace }); | ||
81 | } catch (error) { | ||
82 | throw error; | ||
83 | } | ||
84 | }; | ||
85 | |||
86 | @action _delete = async ({ workspace }) => { | ||
87 | try { | ||
88 | await this.api.deleteWorkspace(workspace); | ||
89 | this.state.workspaces.remove(workspace); | ||
90 | this.stores.router.push('/settings/workspaces'); | ||
91 | } catch (error) { | ||
92 | throw error; | ||
93 | } | ||
94 | }; | ||
95 | |||
96 | @action _update = async ({ workspace }) => { | ||
97 | try { | ||
98 | await this.api.updateWorkspace(workspace); | ||
99 | const localWorkspace = this.state.workspaces.find(ws => ws.id === workspace.id); | ||
100 | Object.assign(localWorkspace, workspace); | ||
101 | this.stores.router.push('/settings/workspaces'); | ||
102 | } catch (error) { | ||
103 | throw error; | ||
104 | } | ||
105 | }; | ||
106 | |||
107 | @action _setActiveWorkspace = ({ workspace }) => { | ||
108 | this.state.activeWorkspace = workspace; | ||
109 | }; | ||
110 | |||
111 | @action _deactivateActiveWorkspace = () => { | ||
112 | this.state.activeWorkspace = null; | ||
113 | }; | ||
114 | } | ||
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..0c1fb8aa6 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json | |||
@@ -74,6 +74,9 @@ | |||
74 | "menu.window" : "Fenster", | 74 | "menu.window" : "Fenster", |
75 | "menu.window.close" : "Schließen", | 75 | "menu.window.close" : "Schließen", |
76 | "menu.window.minimize" : "Minimieren", | 76 | "menu.window.minimize" : "Minimieren", |
77 | "menu.workspaces": "Workspaces", | ||
78 | "menu.workspaces.defaultWorkspace": "All services", | ||
79 | "menu.workspaces.addNewWorkspace": "Add New Workspace", | ||
77 | "password.email.label" : "E-Mail Adresse", | 80 | "password.email.label" : "E-Mail Adresse", |
78 | "password.headline" : "Passwort zurücksetzen", | 81 | "password.headline" : "Passwort zurücksetzen", |
79 | "password.link.login" : "An Deinem Konto anmelden", | 82 | "password.link.login" : "An Deinem Konto anmelden", |
@@ -158,6 +161,7 @@ | |||
158 | "settings.navigation.logout" : "Abmelden", | 161 | "settings.navigation.logout" : "Abmelden", |
159 | "settings.navigation.settings" : "Einstellungen", | 162 | "settings.navigation.settings" : "Einstellungen", |
160 | "settings.navigation.yourServices" : "Deine Dienste", | 163 | "settings.navigation.yourServices" : "Deine Dienste", |
164 | "settings.navigation.yourWorkspaces": "Deine Workspaces", | ||
161 | "settings.recipes.all" : "Alle Dienste", | 165 | "settings.recipes.all" : "Alle Dienste", |
162 | "settings.recipes.dev" : "Entwicklung", | 166 | "settings.recipes.dev" : "Entwicklung", |
163 | "settings.recipes.headline" : "Verfügbare Dienste", | 167 | "settings.recipes.headline" : "Verfügbare Dienste", |
@@ -216,6 +220,14 @@ | |||
216 | "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert", | 220 | "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert", |
217 | "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert", | 221 | "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert", |
218 | "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert", | 222 | "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert", |
223 | "settings.workspaces.headline": "Deine Workspaces", | ||
224 | "settings.workspace.add.form.submitButton": "Workspace erstellen", | ||
225 | "settings.workspace.add.form.name": "Name", | ||
226 | "settings.workspace.form.yourWorkspaces": "Deine Workspaces", | ||
227 | "settings.workspace.form.name": "Name", | ||
228 | "settings.workspace.form.buttonDelete": "Workspace löschen", | ||
229 | "settings.workspace.form.buttonSave": "Workspace speichern", | ||
230 | "settings.workspace.form.servicesInWorkspaceHeadline": "Services in diesem Workspace", | ||
219 | "settings.user.form.accountType.company" : "Firma", | 231 | "settings.user.form.accountType.company" : "Firma", |
220 | "settings.user.form.accountType.individual" : "Einzelperson", | 232 | "settings.user.form.accountType.individual" : "Einzelperson", |
221 | "settings.user.form.accountType.label" : "Konto-Typ", | 233 | "settings.user.form.accountType.label" : "Konto-Typ", |
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 25ec027d8..2a51662a2 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,14 @@ | |||
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", | ||
210 | "settings.workspace.form.servicesInWorkspaceHeadline": "Services in this Workspace", | ||
202 | "subscription.type.free": "free", | 211 | "subscription.type.free": "free", |
203 | "subscription.type.month": "month", | 212 | "subscription.type.month": "month", |
204 | "subscription.type.year": "year", | 213 | "subscription.type.year": "year", |
@@ -267,7 +276,10 @@ | |||
267 | "menu.app.hideOthers": "Hide Others", | 276 | "menu.app.hideOthers": "Hide Others", |
268 | "menu.app.unhide": "Unhide", | 277 | "menu.app.unhide": "Unhide", |
269 | "menu.app.quit": "Quit", | 278 | "menu.app.quit": "Quit", |
270 | "menu.services.addNewService": "Add New Service...", | 279 | "menu.services.addNewService": "Add New Service", |
280 | "menu.workspaces": "Workspaces", | ||
281 | "menu.workspaces.defaultWorkspace": "All services", | ||
282 | "menu.workspaces.addNewWorkspace": "Add New Workspace", | ||
271 | "validation.required": "{field} is required", | 283 | "validation.required": "{field} is required", |
272 | "validation.email": "{field} is not valid", | 284 | "validation.email": "{field} is not valid", |
273 | "validation.url": "{field} is not a valid URL", | 285 | "validation.url": "{field} is not a valid URL", |
diff --git a/src/lib/Menu.js b/src/lib/Menu.js index c378619ad..1560dd285 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js | |||
@@ -3,6 +3,8 @@ import { observable, autorun, computed } from 'mobx'; | |||
3 | import { defineMessages } from 'react-intl'; | 3 | import { defineMessages } from 'react-intl'; |
4 | 4 | ||
5 | import { isMac, ctrlKey, cmdKey } from '../environment'; | 5 | import { isMac, ctrlKey, cmdKey } from '../environment'; |
6 | import { workspacesState } from '../features/workspaces/state'; | ||
7 | import workspaceActions from '../features/workspaces/actions'; | ||
6 | 8 | ||
7 | const { app, Menu, dialog } = remote; | 9 | const { app, Menu, dialog } = remote; |
8 | 10 | ||
@@ -179,6 +181,18 @@ const menuItems = defineMessages({ | |||
179 | id: 'menu.services.addNewService', | 181 | id: 'menu.services.addNewService', |
180 | defaultMessage: '!!!Add New Service...', | 182 | defaultMessage: '!!!Add New Service...', |
181 | }, | 183 | }, |
184 | workspaces: { | ||
185 | id: 'menu.workspaces', | ||
186 | defaultMessage: '!!!Workspaces', | ||
187 | }, | ||
188 | defaultWorkspace: { | ||
189 | id: 'menu.workspaces.defaultWorkspace', | ||
190 | defaultMessage: '!!!Default', | ||
191 | }, | ||
192 | addNewWorkspace: { | ||
193 | id: 'menu.workspaces.addNewWorkspace', | ||
194 | defaultMessage: '!!!Add New Workspace...', | ||
195 | }, | ||
182 | }); | 196 | }); |
183 | 197 | ||
184 | function getActiveWebview() { | 198 | function getActiveWebview() { |
@@ -266,6 +280,10 @@ const _templateFactory = intl => [ | |||
266 | submenu: [], | 280 | submenu: [], |
267 | }, | 281 | }, |
268 | { | 282 | { |
283 | label: intl.formatMessage(menuItems.workspaces), | ||
284 | submenu: [], | ||
285 | }, | ||
286 | { | ||
269 | label: intl.formatMessage(menuItems.window), | 287 | label: intl.formatMessage(menuItems.window), |
270 | role: 'window', | 288 | role: 'window', |
271 | submenu: [ | 289 | submenu: [ |
@@ -499,7 +517,9 @@ export default class FranzMenu { | |||
499 | } | 517 | } |
500 | 518 | ||
501 | _build() { | 519 | _build() { |
502 | const serviceTpl = Object.assign([], this.serviceTpl); // need to clone object so we don't modify computed (cached) object | 520 | // need to clone object so we don't modify computed (cached) object |
521 | const serviceTpl = Object.assign([], this.serviceTpl); | ||
522 | const workspacesMenu = Object.assign([], this.workspacesMenu); | ||
503 | 523 | ||
504 | if (window.franz === undefined) { | 524 | if (window.franz === undefined) { |
505 | return; | 525 | return; |
@@ -632,7 +652,7 @@ export default class FranzMenu { | |||
632 | }, | 652 | }, |
633 | ); | 653 | ); |
634 | 654 | ||
635 | tpl[4].submenu.unshift(about, { | 655 | tpl[5].submenu.unshift(about, { |
636 | type: 'separator', | 656 | type: 'separator', |
637 | }); | 657 | }); |
638 | } else { | 658 | } else { |
@@ -678,6 +698,8 @@ export default class FranzMenu { | |||
678 | tpl[3].submenu = serviceTpl; | 698 | tpl[3].submenu = serviceTpl; |
679 | } | 699 | } |
680 | 700 | ||
701 | tpl[4].submenu = workspacesMenu; | ||
702 | |||
681 | this.currentTemplate = tpl; | 703 | this.currentTemplate = tpl; |
682 | const menu = Menu.buildFromTemplate(tpl); | 704 | const menu = Menu.buildFromTemplate(tpl); |
683 | Menu.setApplicationMenu(menu); | 705 | Menu.setApplicationMenu(menu); |
@@ -701,6 +723,51 @@ export default class FranzMenu { | |||
701 | return []; | 723 | return []; |
702 | } | 724 | } |
703 | 725 | ||
726 | @computed get workspacesMenu() { | ||
727 | const { workspaces, activeWorkspace } = workspacesState; | ||
728 | const { intl } = window.franz; | ||
729 | const menu = []; | ||
730 | |||
731 | // Add new workspace item: | ||
732 | menu.push({ | ||
733 | label: intl.formatMessage(menuItems.addNewWorkspace), | ||
734 | accelerator: `${cmdKey}+Shift+N`, | ||
735 | click: () => { | ||
736 | this.actions.ui.openSettings({ path: 'workspaces' }); | ||
737 | }, | ||
738 | enabled: this.stores.user.isLoggedIn, | ||
739 | }, { | ||
740 | type: 'separator', | ||
741 | }); | ||
742 | |||
743 | // Default workspace | ||
744 | menu.push({ | ||
745 | label: intl.formatMessage(menuItems.defaultWorkspace), | ||
746 | accelerator: `${cmdKey}+Alt+1`, | ||
747 | type: 'radio', | ||
748 | checked: !activeWorkspace, | ||
749 | click: () => { | ||
750 | workspaceActions.deactivate(); | ||
751 | }, | ||
752 | }); | ||
753 | |||
754 | // Workspace items | ||
755 | if (this.stores.user.isLoggedIn) { | ||
756 | workspaces.forEach((workspace, i) => menu.push({ | ||
757 | label: workspace.name, | ||
758 | accelerator: i < 9 ? `${cmdKey}+Alt+${i + 2}` : null, | ||
759 | type: 'radio', | ||
760 | checked: activeWorkspace ? workspace.id === activeWorkspace.id : false, | ||
761 | click: () => { | ||
762 | workspaceActions.activate({ workspace }); | ||
763 | }, | ||
764 | })); | ||
765 | } | ||
766 | |||
767 | console.log(menu); | ||
768 | return menu; | ||
769 | } | ||
770 | |||
704 | _getServiceName(service) { | 771 | _getServiceName(service) { |
705 | if (service.name) { | 772 | if (service.name) { |
706 | return service.name; | 773 | return service.name; |
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index d2842083c..b7130904b 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js | |||
@@ -7,6 +7,7 @@ import delayApp from '../features/delayApp'; | |||
7 | import spellchecker from '../features/spellchecker'; | 7 | import spellchecker from '../features/spellchecker'; |
8 | import serviceProxy from '../features/serviceProxy'; | 8 | import serviceProxy from '../features/serviceProxy'; |
9 | import basicAuth from '../features/basicAuth'; | 9 | import basicAuth from '../features/basicAuth'; |
10 | import workspaces from '../features/workspaces'; | ||
10 | import shareFranz from '../features/shareFranz'; | 11 | import shareFranz from '../features/shareFranz'; |
11 | 12 | ||
12 | import { DEFAULT_FEATURES_CONFIG } from '../config'; | 13 | import { DEFAULT_FEATURES_CONFIG } from '../config'; |
@@ -38,7 +39,7 @@ export default class FeaturesStore extends Store { | |||
38 | 39 | ||
39 | @computed get features() { | 40 | @computed get features() { |
40 | if (this.stores.user.isLoggedIn) { | 41 | if (this.stores.user.isLoggedIn) { |
41 | return this.featuresRequest.execute().result || DEFAULT_FEATURES_CONFIG; | 42 | return Object.assign({}, DEFAULT_FEATURES_CONFIG, this.featuresRequest.execute().result); |
42 | } | 43 | } |
43 | 44 | ||
44 | return DEFAULT_FEATURES_CONFIG; | 45 | return DEFAULT_FEATURES_CONFIG; |
@@ -57,6 +58,7 @@ export default class FeaturesStore extends Store { | |||
57 | spellchecker(this.stores, this.actions); | 58 | spellchecker(this.stores, this.actions); |
58 | serviceProxy(this.stores, this.actions); | 59 | serviceProxy(this.stores, this.actions); |
59 | basicAuth(this.stores, this.actions); | 60 | basicAuth(this.stores, this.actions); |
61 | workspaces(this.stores, this.actions); | ||
60 | shareFranz(this.stores, this.actions); | 62 | shareFranz(this.stores, this.actions); |
61 | } | 63 | } |
62 | } | 64 | } |
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index c63bef196..a86db8103 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -2,7 +2,7 @@ import { | |||
2 | action, | 2 | action, |
3 | reaction, | 3 | reaction, |
4 | computed, | 4 | computed, |
5 | observable, | 5 | observable, runInAction, |
6 | } from 'mobx'; | 6 | } from 'mobx'; |
7 | import { debounce, remove } from 'lodash'; | 7 | import { debounce, remove } from 'lodash'; |
8 | import ms from 'ms'; | 8 | import ms from 'ms'; |
@@ -12,6 +12,8 @@ import Request from './lib/Request'; | |||
12 | import CachedRequest from './lib/CachedRequest'; | 12 | import CachedRequest from './lib/CachedRequest'; |
13 | import { matchRoute } from '../helpers/routing-helpers'; | 13 | import { matchRoute } from '../helpers/routing-helpers'; |
14 | import { gaEvent } from '../lib/analytics'; | 14 | import { gaEvent } from '../lib/analytics'; |
15 | import { workspacesState } from '../features/workspaces/state'; | ||
16 | import { filterServicesByActiveWorkspace, getActiveWorkspaceServices } from '../features/workspaces'; | ||
15 | 17 | ||
16 | const debug = require('debug')('Franz:ServiceStore'); | 18 | const debug = require('debug')('Franz:ServiceStore'); |
17 | 19 | ||
@@ -98,7 +100,6 @@ export default class ServicesStore extends Store { | |||
98 | return observable(services.slice().slice().sort((a, b) => a.order - b.order)); | 100 | return observable(services.slice().slice().sort((a, b) => a.order - b.order)); |
99 | } | 101 | } |
100 | } | 102 | } |
101 | |||
102 | return []; | 103 | return []; |
103 | } | 104 | } |
104 | 105 | ||
@@ -107,13 +108,16 @@ export default class ServicesStore extends Store { | |||
107 | } | 108 | } |
108 | 109 | ||
109 | @computed get allDisplayed() { | 110 | @computed get allDisplayed() { |
110 | return 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); | ||
111 | } | 113 | } |
112 | 114 | ||
113 | // 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 |
114 | @computed get allDisplayedUnordered() { | 116 | @computed get allDisplayedUnordered() { |
117 | const { showDisabledServices } = this.stores.settings.all.app; | ||
115 | const services = this.allServicesRequest.execute().result || []; | 118 | const services = this.allServicesRequest.execute().result || []; |
116 | return this.stores.settings.all.app.showDisabledServices ? services : services.filter(service => service.isEnabled); | 119 | const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled); |
120 | return getActiveWorkspaceServices(filteredServices); | ||
117 | } | 121 | } |
118 | 122 | ||
119 | @computed get filtered() { | 123 | @computed get filtered() { |
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'; |