aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions/index.js6
-rw-r--r--src/actions/lib/actions.js4
-rw-r--r--src/app.js5
-rw-r--r--src/components/layout/AppLayout.js26
-rw-r--r--src/components/layout/Sidebar.js49
-rw-r--r--src/components/services/content/ServiceView.js5
-rw-r--r--src/components/services/tabs/Tabbar.js4
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js29
-rw-r--r--src/components/settings/services/EditServiceForm.js10
-rw-r--r--src/components/settings/services/ServicesDashboard.js2
-rw-r--r--src/components/settings/settings/EditSettingsForm.js1
-rw-r--r--src/components/ui/AppLoader/index.js4
-rw-r--r--src/components/ui/FullscreenLoader/index.js4
-rw-r--r--src/components/ui/Infobox.js17
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js21
-rw-r--r--src/components/ui/PremiumFeatureContainer/styles.js4
-rw-r--r--src/components/ui/ServiceIcon.js67
-rw-r--r--src/components/ui/WebviewLoader/index.js18
-rw-r--r--src/config.js2
-rw-r--r--src/containers/layout/AppLayoutContainer.js15
-rw-r--r--src/containers/settings/SettingsWindow.js2
-rw-r--r--src/environment.js1
-rw-r--r--src/features/delayApp/Component.js2
-rw-r--r--src/features/delayApp/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.js100
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js189
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js246
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js137
-rw-r--r--src/features/workspaces/components/WorkspaceItem.js45
-rw-r--r--src/features/workspaces/components/WorkspaceServiceListItem.js75
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js91
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js195
-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.js37
-rw-r--r--src/features/workspaces/models/Workspace.js25
-rw-r--r--src/features/workspaces/store.js276
-rw-r--r--src/i18n/locales/de.json12
-rw-r--r--src/i18n/locales/defaultMessages.json748
-rw-r--r--src/i18n/locales/en-US.json41
-rw-r--r--src/i18n/messages/src/components/layout/AppLayout.json24
-rw-r--r--src/i18n/messages/src/components/layout/Sidebar.json42
-rw-r--r--src/i18n/messages/src/components/settings/navigation/SettingsNavigation.json41
-rw-r--r--src/i18n/messages/src/components/ui/PremiumFeatureContainer/index.json4
-rw-r--r--src/i18n/messages/src/components/ui/WebviewLoader/index.json15
-rw-r--r--src/i18n/messages/src/features/workspaces/components/CreateWorkspaceForm.json28
-rw-r--r--src/i18n/messages/src/features/workspaces/components/EditWorkspaceForm.json67
-rw-r--r--src/i18n/messages/src/features/workspaces/components/WorkspaceDrawer.json106
-rw-r--r--src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json28
-rw-r--r--src/i18n/messages/src/features/workspaces/components/WorkspaceSwitchingIndicator.json15
-rw-r--r--src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json106
-rw-r--r--src/i18n/messages/src/lib/Menu.json253
-rw-r--r--src/index.js14
-rw-r--r--src/lib/Menu.js94
-rw-r--r--src/lib/analytics.js4
-rw-r--r--src/stores/FeaturesStore.js29
-rw-r--r--src/stores/ServicesStore.js9
-rw-r--r--src/stores/UIStore.js9
-rw-r--r--src/stores/UserStore.js4
-rw-r--r--src/stores/lib/Request.js6
-rw-r--r--src/styles/layout.scss13
-rw-r--r--src/styles/settings.scss7
65 files changed, 3333 insertions, 317 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';
11import news from './news'; 11import news from './news';
12import settings from './settings'; 12import settings from './settings';
13import requests from './requests'; 13import requests from './requests';
14import workspaces from '../features/workspaces/actions';
14 15
15const actions = Object.assign({}, { 16const actions = Object.assign({}, {
16 service, 17 service,
@@ -25,4 +26,7 @@ const actions = Object.assign({}, {
25 requests, 26 requests,
26}); 27});
27 28
28export default defineActions(actions, PropTypes.checkPropTypes); 29export 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 6571e9441..2bc7d2711 100644
--- a/src/actions/lib/actions.js
+++ b/src/actions/lib/actions.js
@@ -9,6 +9,10 @@ export const createActionsFromDefinitions = (actionDefinitions, validate) => {
9 actions[actionName] = action; 9 actions[actionName] = action;
10 action.listeners = []; 10 action.listeners = [];
11 action.listen = listener => action.listeners.push(listener); 11 action.listen = listener => action.listeners.push(listener);
12 action.off = (listener) => {
13 const { listeners } = action;
14 listeners.splice(listeners.indexOf(listener), 1);
15 };
12 action.notify = params => action.listeners.forEach(listener => listener(params)); 16 action.notify = params => action.listeners.forEach(listener => listener(params));
13 }); 17 });
14 return actions; 18 return actions;
diff --git a/src/app.js b/src/app.js
index 96454779e..f6092bf60 100644
--- a/src/app.js
+++ b/src/app.js
@@ -40,6 +40,9 @@ import PricingScreen from './containers/auth/PricingScreen';
40import InviteScreen from './containers/auth/InviteScreen'; 40import InviteScreen from './containers/auth/InviteScreen';
41import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; 41import AuthLayoutContainer from './containers/auth/AuthLayoutContainer';
42import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen'; 42import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen';
43import WorkspacesScreen from './features/workspaces/containers/WorkspacesScreen';
44import EditWorkspaceScreen from './features/workspaces/containers/EditWorkspaceScreen';
45import { WORKSPACES_ROUTES } from './features/workspaces';
43 46
44// Add Polyfills 47// Add Polyfills
45smoothScroll.polyfill(); 48smoothScroll.polyfill();
@@ -76,6 +79,8 @@ window.addEventListener('load', () => {
76 <Route path="/settings/recipes/:filter" component={RecipesScreen} /> 79 <Route path="/settings/recipes/:filter" component={RecipesScreen} />
77 <Route path="/settings/services" component={ServicesScreen} /> 80 <Route path="/settings/services" component={ServicesScreen} />
78 <Route path="/settings/services/:action/:id" component={EditServiceScreen} /> 81 <Route path="/settings/services/:action/:id" component={EditServiceScreen} />
82 <Route path={WORKSPACES_ROUTES.ROOT} component={WorkspacesScreen} />
83 <Route path={WORKSPACES_ROUTES.EDIT} component={EditWorkspaceScreen} />
79 <Route path="/settings/user" component={AccountScreen} /> 84 <Route path="/settings/user" component={AccountScreen} />
80 <Route path="/settings/user/edit" component={EditUserScreen} /> 85 <Route path="/settings/user/edit" component={EditUserScreen} />
81 <Route path="/settings/team" component={TeamScreen} /> 86 <Route path="/settings/team" component={TeamScreen} />
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 593149e72..b7f7722dd 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { TitleBar } from 'electron-react-titlebar'; 5import { TitleBar } from 'electron-react-titlebar';
6import injectSheet from 'react-jss';
6 7
7import InfoBar from '../ui/InfoBar'; 8import InfoBar from '../ui/InfoBar';
8import { Component as DelayApp } from '../../features/delayApp'; 9import { Component as DelayApp } from '../../features/delayApp';
@@ -13,6 +14,8 @@ import ErrorBoundary from '../util/ErrorBoundary';
13// import globalMessages from '../../i18n/globalMessages'; 14// import globalMessages from '../../i18n/globalMessages';
14 15
15import { isWindows } from '../../environment'; 16import { isWindows } from '../../environment';
17import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator';
18import { workspaceStore } from '../../features/workspaces';
16 19
17function createMarkup(HTMLString) { 20function createMarkup(HTMLString) {
18 return { __html: HTMLString }; 21 return { __html: HTMLString };
@@ -45,10 +48,23 @@ const messages = defineMessages({
45 }, 48 },
46}); 49});
47 50
48export default @observer class AppLayout extends Component { 51const styles = theme => ({
52 appContent: {
53 width: `calc(100% + ${theme.workspaces.drawer.width}px)`,
54 transition: 'transform 0.5s ease',
55 transform() {
56 return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`;
57 },
58 },
59});
60
61@injectSheet(styles) @observer
62class AppLayout extends Component {
49 static propTypes = { 63 static propTypes = {
64 classes: PropTypes.object.isRequired,
50 isFullScreen: PropTypes.bool.isRequired, 65 isFullScreen: PropTypes.bool.isRequired,
51 sidebar: PropTypes.element.isRequired, 66 sidebar: PropTypes.element.isRequired,
67 workspacesDrawer: PropTypes.element.isRequired,
52 services: PropTypes.element.isRequired, 68 services: PropTypes.element.isRequired,
53 children: PropTypes.element, 69 children: PropTypes.element,
54 news: MobxPropTypes.arrayOrObservableArray.isRequired, 70 news: MobxPropTypes.arrayOrObservableArray.isRequired,
@@ -76,7 +92,9 @@ export default @observer class AppLayout extends Component {
76 92
77 render() { 93 render() {
78 const { 94 const {
95 classes,
79 isFullScreen, 96 isFullScreen,
97 workspacesDrawer,
80 sidebar, 98 sidebar,
81 services, 99 services,
82 children, 100 children,
@@ -102,9 +120,11 @@ export default @observer class AppLayout extends Component {
102 <div className={(darkMode ? 'theme__dark' : '')}> 120 <div className={(darkMode ? 'theme__dark' : '')}>
103 <div className="app"> 121 <div className="app">
104 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />} 122 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />}
105 <div className="app__content"> 123 <div className={`app__content ${classes.appContent}`}>
124 {workspacesDrawer}
106 {sidebar} 125 {sidebar}
107 <div className="app__service"> 126 <div className="app__service">
127 <WorkspaceSwitchingIndicator />
108 {news.length > 0 && news.map(item => ( 128 {news.length > 0 && news.map(item => (
109 <InfoBar 129 <InfoBar
110 key={item.id} 130 key={item.id}
@@ -176,3 +196,5 @@ export default @observer class AppLayout extends Component {
176 ); 196 );
177 } 197 }
178} 198}
199
200export default AppLayout;
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 609a3b604..36c1f2e39 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -6,6 +6,8 @@ import { observer } from 'mobx-react';
6 6
7import Tabbar from '../services/tabs/Tabbar'; 7import Tabbar from '../services/tabs/Tabbar';
8import { ctrlKey } from '../../environment'; 8import { ctrlKey } from '../../environment';
9import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../../features/workspaces';
10import { gaEvent } from '../../lib/analytics';
9 11
10const messages = defineMessages({ 12const messages = defineMessages({
11 settings: { 13 settings: {
@@ -24,6 +26,14 @@ const messages = defineMessages({
24 id: 'sidebar.unmuteApp', 26 id: 'sidebar.unmuteApp',
25 defaultMessage: '!!!Enable notifications & audio', 27 defaultMessage: '!!!Enable notifications & audio',
26 }, 28 },
29 openWorkspaceDrawer: {
30 id: 'sidebar.openWorkspaceDrawer',
31 defaultMessage: '!!!Open workspace drawer',
32 },
33 closeWorkspaceDrawer: {
34 id: 'sidebar.closeWorkspaceDrawer',
35 defaultMessage: '!!!Close workspace drawer',
36 },
27}); 37});
28 38
29export default @observer class Sidebar extends Component { 39export default @observer class Sidebar extends Component {
@@ -31,7 +41,9 @@ export default @observer class Sidebar extends Component {
31 openSettings: PropTypes.func.isRequired, 41 openSettings: PropTypes.func.isRequired,
32 toggleMuteApp: PropTypes.func.isRequired, 42 toggleMuteApp: PropTypes.func.isRequired,
33 isAppMuted: PropTypes.bool.isRequired, 43 isAppMuted: PropTypes.bool.isRequired,
34 } 44 isWorkspaceDrawerOpen: PropTypes.bool.isRequired,
45 toggleWorkspaceDrawer: PropTypes.func.isRequired,
46 };
35 47
36 static contextTypes = { 48 static contextTypes = {
37 intl: intlShape, 49 intl: intlShape,
@@ -53,9 +65,23 @@ export default @observer class Sidebar extends Component {
53 this.setState({ tooltipEnabled: false }); 65 this.setState({ tooltipEnabled: false });
54 } 66 }
55 67
68 updateToolTip() {
69 this.disableToolTip();
70 setTimeout(this.enableToolTip.bind(this));
71 }
72
56 render() { 73 render() {
57 const { openSettings, toggleMuteApp, isAppMuted } = this.props; 74 const {
75 openSettings,
76 toggleMuteApp,
77 isAppMuted,
78 isWorkspaceDrawerOpen,
79 toggleWorkspaceDrawer,
80 } = this.props;
58 const { intl } = this.context; 81 const { intl } = this.context;
82 const workspaceToggleMessage = (
83 isWorkspaceDrawerOpen ? messages.closeWorkspaceDrawer : messages.openWorkspaceDrawer
84 );
59 85
60 return ( 86 return (
61 <div className="sidebar"> 87 <div className="sidebar">
@@ -64,9 +90,26 @@ export default @observer class Sidebar extends Component {
64 enableToolTip={() => this.enableToolTip()} 90 enableToolTip={() => this.enableToolTip()}
65 disableToolTip={() => this.disableToolTip()} 91 disableToolTip={() => this.disableToolTip()}
66 /> 92 />
93 {workspaceStore.isFeatureEnabled ? (
94 <button
95 type="button"
96 onClick={() => {
97 toggleWorkspaceDrawer();
98 this.updateToolTip();
99 gaEvent(GA_CATEGORY_WORKSPACES, 'toggleDrawer', 'sidebar');
100 }}
101 className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`}
102 data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${ctrlKey}+D)`}
103 >
104 <i className="mdi mdi-view-grid" />
105 </button>
106 ) : null}
67 <button 107 <button
68 type="button" 108 type="button"
69 onClick={toggleMuteApp} 109 onClick={() => {
110 toggleMuteApp();
111 this.updateToolTip();
112 }}
70 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`} 113 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
71 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`} 114 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`}
72 > 115 >
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index 5afc54f9d..13148b9b3 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -35,11 +35,13 @@ export default @observer class ServiceView extends Component {
35 35
36 autorunDisposer = null; 36 autorunDisposer = null;
37 37
38 forceRepaintTimeout = null;
39
38 componentDidMount() { 40 componentDidMount() {
39 this.autorunDisposer = autorun(() => { 41 this.autorunDisposer = autorun(() => {
40 if (this.props.service.isActive) { 42 if (this.props.service.isActive) {
41 this.setState({ forceRepaint: true }); 43 this.setState({ forceRepaint: true });
42 setTimeout(() => { 44 this.forceRepaintTimeout = setTimeout(() => {
43 this.setState({ forceRepaint: false }); 45 this.setState({ forceRepaint: false });
44 }, 100); 46 }, 100);
45 } 47 }
@@ -48,6 +50,7 @@ export default @observer class ServiceView extends Component {
48 50
49 componentWillUnmount() { 51 componentWillUnmount() {
50 this.autorunDisposer(); 52 this.autorunDisposer();
53 clearTimeout(this.forceRepaintTimeout);
51 } 54 }
52 55
53 updateTargetUrl = (event) => { 56 updateTargetUrl = (event) => {
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 0be1a22ba..cab6f23d7 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -2,8 +2,11 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl'; 3import { defineMessages, intlShape } from 'react-intl';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
5import { ProBadge } from '@meetfranz/ui';
5 6
6import Link from '../../ui/Link'; 7import Link from '../../ui/Link';
8import { workspaceStore } from '../../../features/workspaces';
9import UIStore from '../../../stores/UIStore';
7 10
8const messages = defineMessages({ 11const messages = defineMessages({
9 availableServices: { 12 availableServices: {
@@ -14,6 +17,10 @@ const messages = defineMessages({
14 id: 'settings.navigation.yourServices', 17 id: 'settings.navigation.yourServices',
15 defaultMessage: '!!!Your services', 18 defaultMessage: '!!!Your services',
16 }, 19 },
20 yourWorkspaces: {
21 id: 'settings.navigation.yourWorkspaces',
22 defaultMessage: '!!!Your workspaces',
23 },
17 account: { 24 account: {
18 id: 'settings.navigation.account', 25 id: 'settings.navigation.account',
19 defaultMessage: '!!!Account', 26 defaultMessage: '!!!Account',
@@ -38,7 +45,11 @@ const messages = defineMessages({
38 45
39export default @inject('stores') @observer class SettingsNavigation extends Component { 46export default @inject('stores') @observer class SettingsNavigation extends Component {
40 static propTypes = { 47 static propTypes = {
48 stores: PropTypes.shape({
49 ui: PropTypes.instanceOf(UIStore).isRequired,
50 }).isRequired,
41 serviceCount: PropTypes.number.isRequired, 51 serviceCount: PropTypes.number.isRequired,
52 workspaceCount: PropTypes.number.isRequired,
42 }; 53 };
43 54
44 static contextTypes = { 55 static contextTypes = {
@@ -46,7 +57,8 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
46 }; 57 };
47 58
48 render() { 59 render() {
49 const { serviceCount } = this.props; 60 const { serviceCount, workspaceCount, stores } = this.props;
61 const { isDarkThemeActive } = stores.ui;
50 const { intl } = this.context; 62 const { intl } = this.context;
51 63
52 return ( 64 return (
@@ -67,6 +79,21 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
67 {' '} 79 {' '}
68 <span className="badge">{serviceCount}</span> 80 <span className="badge">{serviceCount}</span>
69 </Link> 81 </Link>
82 {workspaceStore.isFeatureEnabled ? (
83 <Link
84 to="/settings/workspaces"
85 className="settings-navigation__link"
86 activeClassName="is-active"
87 >
88 {intl.formatMessage(messages.yourWorkspaces)}
89 {' '}
90 {workspaceStore.isPremiumUpgradeRequired ? (
91 <ProBadge inverted={!isDarkThemeActive && workspaceStore.isSettingsRouteActive} />
92 ) : (
93 <span className="badge">{workspaceCount}</span>
94 )}
95 </Link>
96 ) : null}
70 <Link 97 <Link
71 to="/settings/user" 98 to="/settings/user"
72 className="settings-navigation__link" 99 className="settings-navigation__link"
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 21616b5de..4ba2eb844 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -341,14 +341,20 @@ export default @observer class EditServiceForm extends Component {
341 </div> 341 </div>
342 </div> 342 </div>
343 343
344 <PremiumFeatureContainer condition={isSpellcheckerPremiumFeature}> 344 <PremiumFeatureContainer
345 condition={isSpellcheckerPremiumFeature}
346 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }}
347 >
345 <div className="settings__settings-group"> 348 <div className="settings__settings-group">
346 <Select field={form.$('spellcheckerLanguage')} /> 349 <Select field={form.$('spellcheckerLanguage')} />
347 </div> 350 </div>
348 </PremiumFeatureContainer> 351 </PremiumFeatureContainer>
349 352
350 {isProxyFeatureEnabled && ( 353 {isProxyFeatureEnabled && (
351 <PremiumFeatureContainer condition={isProxyPremiumFeature}> 354 <PremiumFeatureContainer
355 condition={isProxyPremiumFeature}
356 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'proxy' }}
357 >
352 <div className="settings__settings-group"> 358 <div className="settings__settings-group">
353 <h3> 359 <h3>
354 {intl.formatMessage(messages.headlineProxy)} 360 {intl.formatMessage(messages.headlineProxy)}
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
index a12df7372..53bae12df 100644
--- a/src/components/settings/services/ServicesDashboard.js
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -65,7 +65,7 @@ export default @observer class ServicesDashboard extends Component {
65 65
66 static defaultProps = { 66 static defaultProps = {
67 searchNeedle: '', 67 searchNeedle: '',
68 } 68 };
69 69
70 static contextTypes = { 70 static contextTypes = {
71 intl: intlShape, 71 intl: intlShape,
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index a92e559f3..8429d0ecb 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -170,6 +170,7 @@ export default @observer class EditSettingsForm extends Component {
170 <Select field={form.$('locale')} showLabel={false} /> 170 <Select field={form.$('locale')} showLabel={false} />
171 <PremiumFeatureContainer 171 <PremiumFeatureContainer
172 condition={isSpellcheckerPremiumFeature} 172 condition={isSpellcheckerPremiumFeature}
173 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }}
173 > 174 >
174 <Fragment> 175 <Fragment>
175 <Toggle 176 <Toggle
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js
index 61053f6d1..b0c7fed7b 100644
--- a/src/components/ui/AppLoader/index.js
+++ b/src/components/ui/AppLoader/index.js
@@ -23,11 +23,11 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component
23 static propTypes = { 23 static propTypes = {
24 classes: PropTypes.object.isRequired, 24 classes: PropTypes.object.isRequired,
25 theme: PropTypes.object.isRequired, 25 theme: PropTypes.object.isRequired,
26 } 26 };
27 27
28 state = { 28 state = {
29 step: 0, 29 step: 0,
30 } 30 };
31 31
32 interval = null; 32 interval = null;
33 33
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js
index 6ecf4d395..06dab1eb6 100644
--- a/src/components/ui/FullscreenLoader/index.js
+++ b/src/components/ui/FullscreenLoader/index.js
@@ -16,13 +16,13 @@ export default @observer @withTheme @injectSheet(styles) class FullscreenLoader
16 theme: PropTypes.object.isRequired, 16 theme: PropTypes.object.isRequired,
17 spinnerColor: PropTypes.string, 17 spinnerColor: PropTypes.string,
18 children: PropTypes.node, 18 children: PropTypes.node,
19 } 19 };
20 20
21 static defaultProps = { 21 static defaultProps = {
22 className: null, 22 className: null,
23 spinnerColor: null, 23 spinnerColor: null,
24 children: null, 24 children: null,
25 } 25 };
26 26
27 render() { 27 render() {
28 const { 28 const {
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js
index a33c6474a..0917ee9f0 100644
--- a/src/components/ui/Infobox.js
+++ b/src/components/ui/Infobox.js
@@ -13,6 +13,8 @@ export default @observer class Infobox extends Component {
13 ctaLabel: PropTypes.string, 13 ctaLabel: PropTypes.string,
14 ctaLoading: PropTypes.bool, 14 ctaLoading: PropTypes.bool,
15 dismissable: PropTypes.bool, 15 dismissable: PropTypes.bool,
16 onDismiss: PropTypes.func,
17 onSeen: PropTypes.func,
16 }; 18 };
17 19
18 static defaultProps = { 20 static defaultProps = {
@@ -22,12 +24,19 @@ export default @observer class Infobox extends Component {
22 ctaOnClick: () => null, 24 ctaOnClick: () => null,
23 ctaLabel: '', 25 ctaLabel: '',
24 ctaLoading: false, 26 ctaLoading: false,
27 onDismiss: () => null,
28 onSeen: () => null,
25 }; 29 };
26 30
27 state = { 31 state = {
28 dismissed: false, 32 dismissed: false,
29 }; 33 };
30 34
35 componentDidMount() {
36 const { onSeen } = this.props;
37 if (onSeen) onSeen();
38 }
39
31 render() { 40 render() {
32 const { 41 const {
33 children, 42 children,
@@ -37,6 +46,7 @@ export default @observer class Infobox extends Component {
37 ctaLoading, 46 ctaLoading,
38 ctaOnClick, 47 ctaOnClick,
39 dismissable, 48 dismissable,
49 onDismiss,
40 } = this.props; 50 } = this.props;
41 51
42 if (this.state.dismissed) { 52 if (this.state.dismissed) {
@@ -76,9 +86,10 @@ export default @observer class Infobox extends Component {
76 {dismissable && ( 86 {dismissable && (
77 <button 87 <button
78 type="button" 88 type="button"
79 onClick={() => this.setState({ 89 onClick={() => {
80 dismissed: true, 90 this.setState({ dismissed: true });
81 })} 91 if (onDismiss) onDismiss();
92 }}
82 className="infobox__delete mdi mdi-close" 93 className="infobox__delete mdi mdi-close"
83 /> 94 />
84 )} 95 )}
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
index 67cd6af0b..3c1e0fac3 100644
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -9,6 +9,7 @@ import { oneOrManyChildElements } from '../../../prop-types';
9import UserStore from '../../../stores/UserStore'; 9import UserStore from '../../../stores/UserStore';
10 10
11import styles from './styles'; 11import styles from './styles';
12import { gaEvent } from '../../../lib/analytics';
12 13
13const messages = defineMessages({ 14const messages = defineMessages({
14 action: { 15 action: {
@@ -17,14 +18,21 @@ const messages = defineMessages({
17 }, 18 },
18}); 19});
19 20
20export default @inject('stores', 'actions') @injectSheet(styles) @observer class PremiumFeatureContainer extends Component { 21@inject('stores', 'actions') @injectSheet(styles) @observer
22class PremiumFeatureContainer extends Component {
21 static propTypes = { 23 static propTypes = {
22 classes: PropTypes.object.isRequired, 24 classes: PropTypes.object.isRequired,
23 condition: PropTypes.bool, 25 condition: PropTypes.bool,
26 gaEventInfo: PropTypes.shape({
27 category: PropTypes.string.isRequired,
28 event: PropTypes.string.isRequired,
29 label: PropTypes.string,
30 }),
24 }; 31 };
25 32
26 static defaultProps = { 33 static defaultProps = {
27 condition: true, 34 condition: true,
35 gaEventInfo: null,
28 }; 36 };
29 37
30 static contextTypes = { 38 static contextTypes = {
@@ -38,6 +46,7 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class
38 actions, 46 actions,
39 condition, 47 condition,
40 stores, 48 stores,
49 gaEventInfo,
41 } = this.props; 50 } = this.props;
42 51
43 const { intl } = this.context; 52 const { intl } = this.context;
@@ -49,7 +58,13 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class
49 <button 58 <button
50 className={classes.actionButton} 59 className={classes.actionButton}
51 type="button" 60 type="button"
52 onClick={() => actions.ui.openSettings({ path: 'user' })} 61 onClick={() => {
62 actions.ui.openSettings({ path: 'user' });
63 if (gaEventInfo) {
64 const { category, event, label } = gaEventInfo;
65 gaEvent(category, event, label);
66 }
67 }}
53 > 68 >
54 {intl.formatMessage(messages.action)} 69 {intl.formatMessage(messages.action)}
55 </button> 70 </button>
@@ -73,3 +88,5 @@ PremiumFeatureContainer.wrappedComponent.propTypes = {
73 }).isRequired, 88 }).isRequired,
74 }).isRequired, 89 }).isRequired,
75}; 90};
91
92export default PremiumFeatureContainer;
diff --git a/src/components/ui/PremiumFeatureContainer/styles.js b/src/components/ui/PremiumFeatureContainer/styles.js
index 11cbfbb90..41881e044 100644
--- a/src/components/ui/PremiumFeatureContainer/styles.js
+++ b/src/components/ui/PremiumFeatureContainer/styles.js
@@ -20,14 +20,14 @@ export default theme => ({
20 color: theme.colorSubscriptionContainerActionButtonColor, 20 color: theme.colorSubscriptionContainerActionButtonColor,
21 'margin-left': 'auto', 21 'margin-left': 'auto',
22 'border-radius': theme.borderRadiusSmall, 22 'border-radius': theme.borderRadiusSmall,
23 padding: [2, 4], 23 padding: [4, 8],
24 'font-size': 12, 24 'font-size': 12,
25 pointerEvents: 'initial', 25 pointerEvents: 'initial',
26 }, 26 },
27 content: { 27 content: {
28 opacity: 0.5, 28 opacity: 0.5,
29 'margin-top': 20, 29 'margin-top': 20,
30 '& :last-child': { 30 '& > :last-child': {
31 'margin-bottom': 0, 31 'margin-bottom': 0,
32 }, 32 },
33 }, 33 },
diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.js
new file mode 100644
index 000000000..0b9155a4e
--- /dev/null
+++ b/src/components/ui/ServiceIcon.js
@@ -0,0 +1,67 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import classnames from 'classnames';
6
7import ServiceModel from '../../models/Service';
8
9const styles = theme => ({
10 root: {
11 height: 'auto',
12 },
13 icon: {
14 width: theme.serviceIcon.width,
15 },
16 isCustomIcon: {
17 width: theme.serviceIcon.isCustom.width,
18 border: theme.serviceIcon.isCustom.border,
19 borderRadius: theme.serviceIcon.isCustom.borderRadius,
20 },
21 isDisabled: {
22 filter: 'grayscale(100%)',
23 opacity: '.5',
24 },
25});
26
27@injectSheet(styles) @observer
28class ServiceIcon extends Component {
29 static propTypes = {
30 classes: PropTypes.object.isRequired,
31 service: PropTypes.instanceOf(ServiceModel).isRequired,
32 className: PropTypes.string,
33 };
34
35 static defaultProps = {
36 className: '',
37 };
38
39 render() {
40 const {
41 classes,
42 className,
43 service,
44 } = this.props;
45
46 return (
47 <div
48 className={classnames([
49 classes.root,
50 className,
51 ])}
52 >
53 <img
54 src={service.icon}
55 className={classnames([
56 classes.icon,
57 service.isEnabled ? null : classes.isDisabled,
58 service.hasCustomIcon ? classes.isCustomIcon : null,
59 ])}
60 alt=""
61 />
62 </div>
63 );
64 }
65}
66
67export default ServiceIcon;
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js
index 3a3dbbe49..58b6b6f1b 100644
--- a/src/components/ui/WebviewLoader/index.js
+++ b/src/components/ui/WebviewLoader/index.js
@@ -2,23 +2,35 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
5 6
6import FullscreenLoader from '../FullscreenLoader'; 7import FullscreenLoader from '../FullscreenLoader';
7
8import styles from './styles'; 8import styles from './styles';
9 9
10const messages = defineMessages({
11 loading: {
12 id: 'service.webviewLoader.loading',
13 defaultMessage: '!!!Loading',
14 },
15});
16
10export default @observer @injectSheet(styles) class WebviewLoader extends Component { 17export default @observer @injectSheet(styles) class WebviewLoader extends Component {
11 static propTypes = { 18 static propTypes = {
12 name: PropTypes.string.isRequired, 19 name: PropTypes.string.isRequired,
13 classes: PropTypes.object.isRequired, 20 classes: PropTypes.object.isRequired,
14 } 21 };
22
23 static contextTypes = {
24 intl: intlShape,
25 };
15 26
16 render() { 27 render() {
17 const { classes, name } = this.props; 28 const { classes, name } = this.props;
29 const { intl } = this.context;
18 return ( 30 return (
19 <FullscreenLoader 31 <FullscreenLoader
20 className={classes.component} 32 className={classes.component}
21 title={`Loading ${name}`} 33 title={`${intl.formatMessage(messages.loading)} ${name}`}
22 /> 34 />
23 ); 35 );
24 } 36 }
diff --git a/src/config.js b/src/config.js
index 05ee07ee5..0a47aa7d7 100644
--- a/src/config.js
+++ b/src/config.js
@@ -50,6 +50,8 @@ export const DEFAULT_FEATURES_CONFIG = {
50 }, 50 },
51 isServiceProxyEnabled: false, 51 isServiceProxyEnabled: false,
52 isServiceProxyPremiumFeature: true, 52 isServiceProxyPremiumFeature: true,
53 isWorkspacePremiumFeature: true,
54 isWorkspaceEnabled: false,
53}; 55};
54 56
55export const DEFAULT_WINDOW_OPTIONS = { 57export const DEFAULT_WINDOW_OPTIONS = {
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index 5a05ce431..2d855c78f 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -20,6 +20,9 @@ import Services from '../../components/services/content/Services';
20import AppLoader from '../../components/ui/AppLoader'; 20import AppLoader from '../../components/ui/AppLoader';
21 21
22import { state as delayAppState } from '../../features/delayApp'; 22import { state as delayAppState } from '../../features/delayApp';
23import { workspaceActions } from '../../features/workspaces/actions';
24import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer';
25import { workspaceStore } from '../../features/workspaces';
23 26
24export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { 27export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component {
25 static defaultProps = { 28 static defaultProps = {
@@ -82,6 +85,15 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
82 ); 85 );
83 } 86 }
84 87
88 const workspacesDrawer = (
89 <WorkspaceDrawer
90 getServicesForWorkspace={workspace => (
91 workspace ? workspaceStore.getWorkspaceServices(workspace).map(s => s.name) : services.all.map(s => s.name)
92 )}
93 onUpgradeAccountClick={() => openSettings({ path: 'user' })}
94 />
95 );
96
85 const sidebar = ( 97 const sidebar = (
86 <Sidebar 98 <Sidebar
87 services={services.allDisplayed} 99 services={services.allDisplayed}
@@ -96,6 +108,8 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
96 deleteService={deleteService} 108 deleteService={deleteService}
97 updateService={updateService} 109 updateService={updateService}
98 toggleMuteApp={toggleMuteApp} 110 toggleMuteApp={toggleMuteApp}
111 toggleWorkspaceDrawer={workspaceActions.toggleWorkspaceDrawer}
112 isWorkspaceDrawerOpen={workspaceStore.isWorkspaceDrawerOpen}
99 showMessageBadgeWhenMutedSetting={settings.all.app.showMessageBadgeWhenMuted} 113 showMessageBadgeWhenMutedSetting={settings.all.app.showMessageBadgeWhenMuted}
100 showMessageBadgesEvenWhenMuted={ui.showMessageBadgesEvenWhenMuted} 114 showMessageBadgesEvenWhenMuted={ui.showMessageBadgesEvenWhenMuted}
101 /> 115 />
@@ -122,6 +136,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
122 showServicesUpdatedInfoBar={ui.showServicesUpdatedInfoBar} 136 showServicesUpdatedInfoBar={ui.showServicesUpdatedInfoBar}
123 appUpdateIsDownloaded={app.updateStatus === app.updateStatusTypes.DOWNLOADED} 137 appUpdateIsDownloaded={app.updateStatus === app.updateStatusTypes.DOWNLOADED}
124 sidebar={sidebar} 138 sidebar={sidebar}
139 workspacesDrawer={workspacesDrawer}
125 services={servicesContainer} 140 services={servicesContainer}
126 news={news.latest} 141 news={news.latest}
127 removeNewsItem={hide} 142 removeNewsItem={hide}
diff --git a/src/containers/settings/SettingsWindow.js b/src/containers/settings/SettingsWindow.js
index 6d9e0ee77..663b9e2e4 100644
--- a/src/containers/settings/SettingsWindow.js
+++ b/src/containers/settings/SettingsWindow.js
@@ -7,6 +7,7 @@ import ServicesStore from '../../stores/ServicesStore';
7import Layout from '../../components/settings/SettingsLayout'; 7import Layout from '../../components/settings/SettingsLayout';
8import Navigation from '../../components/settings/navigation/SettingsNavigation'; 8import Navigation from '../../components/settings/navigation/SettingsNavigation';
9import ErrorBoundary from '../../components/util/ErrorBoundary'; 9import ErrorBoundary from '../../components/util/ErrorBoundary';
10import { workspaceStore } from '../../features/workspaces';
10 11
11export default @inject('stores', 'actions') @observer class SettingsContainer extends Component { 12export default @inject('stores', 'actions') @observer class SettingsContainer extends Component {
12 render() { 13 render() {
@@ -16,6 +17,7 @@ export default @inject('stores', 'actions') @observer class SettingsContainer ex
16 const navigation = ( 17 const navigation = (
17 <Navigation 18 <Navigation
18 serviceCount={stores.services.all.length} 19 serviceCount={stores.services.all.length}
20 workspaceCount={workspaceStore.workspaces.length}
19 /> 21 />
20 ); 22 );
21 23
diff --git a/src/environment.js b/src/environment.js
index e47b373b0..ae7a67e4d 100644
--- a/src/environment.js
+++ b/src/environment.js
@@ -46,5 +46,6 @@ if (!isDevMode || (isDevMode && useLiveAPI)) {
46} 46}
47 47
48export const API = api; 48export const API = api;
49export const API_VERSION = 'v1';
49export const WS_API = wsApi; 50export const WS_API = wsApi;
50export const WEBSITE = web; 51export const WEBSITE = web;
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/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..2c00ea63c
--- /dev/null
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -0,0 +1,100 @@
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 focus
86 />
87 <Button
88 className={classes.submitButton}
89 type="submit"
90 label={intl.formatMessage(messages.submitButton)}
91 onClick={this.submitForm.bind(this, form)}
92 busy={isSubmitting}
93 buttonType={isSubmitting ? 'secondary' : 'primary'}
94 />
95 </div>
96 );
97 }
98}
99
100export default CreateWorkspaceForm;
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
new file mode 100644
index 000000000..bba4485ff
--- /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 WorkspaceServiceListItem from './WorkspaceServiceListItem';
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 <WorkspaceServiceListItem
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/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
new file mode 100644
index 000000000..684e50dd0
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -0,0 +1,246 @@
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, ProBadge } 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 workspacesSettingsTooltip: {
25 id: 'workspaceDrawer.workspacesSettingsTooltip',
26 defaultMessage: '!!!Workspaces settings',
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 reactivatePremiumAccount: {
37 id: 'workspaceDrawer.reactivatePremiumAccountLabel',
38 defaultMessage: '!!!Reactivate premium account',
39 },
40 addNewWorkspaceLabel: {
41 id: 'workspaceDrawer.addNewWorkspaceLabel',
42 defaultMessage: '!!!add new workspace',
43 },
44 premiumFeatureBadge: {
45 id: 'workspaceDrawer.proFeatureBadge',
46 defaultMessage: '!!!Premium feature',
47 },
48});
49
50const styles = theme => ({
51 drawer: {
52 background: theme.workspaces.drawer.background,
53 width: `${theme.workspaces.drawer.width}px`,
54 },
55 headline: {
56 fontSize: '24px',
57 marginTop: '38px',
58 marginBottom: '25px',
59 marginLeft: theme.workspaces.drawer.padding,
60 },
61 headlineProBadge: {
62 marginRight: 15,
63 },
64 workspacesSettingsButton: {
65 float: 'right',
66 marginRight: theme.workspaces.drawer.padding,
67 marginTop: '2px',
68 },
69 workspacesSettingsButtonIcon: {
70 fill: theme.workspaces.drawer.buttons.color,
71 '&:hover': {
72 fill: theme.workspaces.drawer.buttons.hoverColor,
73 },
74 },
75 workspaces: {
76 height: 'auto',
77 },
78 premiumAnnouncement: {
79 padding: '20px',
80 paddingTop: '0',
81 height: 'auto',
82 },
83 premiumCtaButton: {
84 marginTop: '20px',
85 width: '100%',
86 color: 'white !important',
87 },
88 addNewWorkspaceLabel: {
89 height: 'auto',
90 color: theme.workspaces.drawer.buttons.color,
91 marginTop: 40,
92 textAlign: 'center',
93 '& > svg': {
94 fill: theme.workspaces.drawer.buttons.color,
95 },
96 '& > span': {
97 fontSize: '13px',
98 marginLeft: 10,
99 position: 'relative',
100 top: -3,
101 },
102 '&:hover': {
103 color: theme.workspaces.drawer.buttons.hoverColor,
104 '& > svg': {
105 fill: theme.workspaces.drawer.buttons.hoverColor,
106 },
107 },
108 },
109});
110
111@injectSheet(styles) @observer
112class WorkspaceDrawer extends Component {
113 static propTypes = {
114 classes: PropTypes.object.isRequired,
115 getServicesForWorkspace: PropTypes.func.isRequired,
116 onUpgradeAccountClick: PropTypes.func.isRequired,
117 };
118
119 static contextTypes = {
120 intl: intlShape,
121 };
122
123 componentDidMount() {
124 ReactTooltip.rebuild();
125 }
126
127 render() {
128 const {
129 classes,
130 getServicesForWorkspace,
131 onUpgradeAccountClick,
132 } = this.props;
133 const { intl } = this.context;
134 const {
135 activeWorkspace,
136 isSwitchingWorkspace,
137 nextWorkspace,
138 workspaces,
139 } = workspaceStore;
140 const actualWorkspace = isSwitchingWorkspace ? nextWorkspace : activeWorkspace;
141 return (
142 <div className={classes.drawer}>
143 <H1 className={classes.headline}>
144 {workspaceStore.isPremiumUpgradeRequired && (
145 <span
146 className={classes.headlineProBadge}
147 data-tip={`${intl.formatMessage(messages.premiumFeatureBadge)}`}
148 >
149 <ProBadge />
150 </span>
151 )}
152 {intl.formatMessage(messages.headline)}
153 <span
154 className={classes.workspacesSettingsButton}
155 onClick={() => {
156 workspaceActions.openWorkspaceSettings();
157 gaEvent(GA_CATEGORY_WORKSPACES, 'settings', 'drawerHeadline');
158 }}
159 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`}
160 >
161 <Icon
162 icon="mdiSettings"
163 size={1.5}
164 className={classes.workspacesSettingsButtonIcon}
165 />
166 </span>
167 </H1>
168 {workspaceStore.isPremiumUpgradeRequired ? (
169 <div className={classes.premiumAnnouncement}>
170 <FormattedHTMLMessage {...messages.workspaceFeatureInfo} />
171 {workspaceStore.userHasWorkspaces ? (
172 <Button
173 className={classes.premiumCtaButton}
174 buttonType="primary"
175 label={intl.formatMessage(messages.reactivatePremiumAccount)}
176 icon="mdiStar"
177 onClick={() => {
178 onUpgradeAccountClick();
179 gaEvent('User', 'upgrade', 'workspaceDrawer');
180 }}
181 />
182 ) : (
183 <Button
184 className={classes.premiumCtaButton}
185 buttonType="primary"
186 label={intl.formatMessage(messages.premiumCtaButtonLabel)}
187 icon="mdiPlusBox"
188 onClick={() => {
189 workspaceActions.openWorkspaceSettings();
190 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta');
191 }}
192 />
193 )}
194 </div>
195 ) : (
196 <div className={classes.workspaces}>
197 <WorkspaceDrawerItem
198 name={intl.formatMessage(messages.allServices)}
199 onClick={() => {
200 workspaceActions.deactivate();
201 workspaceActions.toggleWorkspaceDrawer();
202 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
203 }}
204 services={getServicesForWorkspace(null)}
205 isActive={actualWorkspace == null}
206 />
207 {workspaces.map(workspace => (
208 <WorkspaceDrawerItem
209 key={workspace.id}
210 name={workspace.name}
211 isActive={actualWorkspace === workspace}
212 onClick={() => {
213 if (actualWorkspace === workspace) return;
214 workspaceActions.activate({ workspace });
215 workspaceActions.toggleWorkspaceDrawer();
216 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
217 }}
218 onContextMenuEditClick={() => workspaceActions.edit({ workspace })}
219 services={getServicesForWorkspace(workspace)}
220 />
221 ))}
222 <div
223 className={classes.addNewWorkspaceLabel}
224 onClick={() => {
225 workspaceActions.openWorkspaceSettings();
226 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerAddLabel');
227 }}
228 >
229 <Icon
230 icon="mdiPlusBox"
231 size={1}
232 className={classes.workspacesSettingsButtonIcon}
233 />
234 <span>
235 {intl.formatMessage(messages.addNewWorkspaceLabel)}
236 </span>
237 </div>
238 </div>
239 )}
240 <ReactTooltip place="right" type="dark" effect="solid" />
241 </div>
242 );
243 }
244}
245
246export default WorkspaceDrawer;
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
new file mode 100644
index 000000000..59a2144d3
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -0,0 +1,137 @@
1import { remote } from 'electron';
2import React, { Component } from 'react';
3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7import { defineMessages, intlShape } from 'react-intl';
8
9const { Menu } = remote;
10
11const messages = defineMessages({
12 noServicesAddedYet: {
13 id: 'workspaceDrawer.item.noServicesAddedYet',
14 defaultMessage: '!!!No services added yet',
15 },
16 contextMenuEdit: {
17 id: 'workspaceDrawer.item.contextMenuEdit',
18 defaultMessage: '!!!edit',
19 },
20});
21
22const styles = theme => ({
23 item: {
24 height: '67px',
25 padding: `15px ${theme.workspaces.drawer.padding}px`,
26 borderBottom: `1px solid ${theme.workspaces.drawer.listItem.border}`,
27 transition: 'background-color 300ms ease-out',
28 '&:first-child': {
29 borderTop: `1px solid ${theme.workspaces.drawer.listItem.border}`,
30 },
31 '&:hover': {
32 backgroundColor: theme.workspaces.drawer.listItem.hoverBackground,
33 },
34 },
35 isActiveItem: {
36 backgroundColor: theme.workspaces.drawer.listItem.activeBackground,
37 '&:hover': {
38 backgroundColor: theme.workspaces.drawer.listItem.activeBackground,
39 },
40 },
41 name: {
42 marginTop: '4px',
43 color: theme.workspaces.drawer.listItem.name.color,
44 },
45 activeName: {
46 color: theme.workspaces.drawer.listItem.name.activeColor,
47 },
48 services: {
49 display: 'block',
50 fontSize: '11px',
51 marginTop: '5px',
52 color: theme.workspaces.drawer.listItem.services.color,
53 whiteSpace: 'nowrap',
54 textOverflow: 'ellipsis',
55 overflow: 'hidden',
56 lineHeight: '15px',
57 },
58 activeServices: {
59 color: theme.workspaces.drawer.listItem.services.active,
60 },
61});
62
63@injectSheet(styles) @observer
64class WorkspaceDrawerItem extends Component {
65 static propTypes = {
66 classes: PropTypes.object.isRequired,
67 isActive: PropTypes.bool.isRequired,
68 name: PropTypes.string.isRequired,
69 onClick: PropTypes.func.isRequired,
70 services: PropTypes.arrayOf(PropTypes.string).isRequired,
71 onContextMenuEditClick: PropTypes.func,
72 };
73
74 static defaultProps = {
75 onContextMenuEditClick: null,
76 };
77
78 static contextTypes = {
79 intl: intlShape,
80 };
81
82 render() {
83 const {
84 classes,
85 isActive,
86 name,
87 onClick,
88 onContextMenuEditClick,
89 services,
90 } = this.props;
91 const { intl } = this.context;
92
93 const contextMenuTemplate = [{
94 label: name,
95 enabled: false,
96 }, {
97 type: 'separator',
98 }, {
99 label: intl.formatMessage(messages.contextMenuEdit),
100 click: onContextMenuEditClick,
101 }];
102
103 const contextMenu = Menu.buildFromTemplate(contextMenuTemplate);
104
105 return (
106 <div
107 className={classnames([
108 classes.item,
109 isActive ? classes.isActiveItem : null,
110 ])}
111 onClick={onClick}
112 onContextMenu={() => (
113 onContextMenuEditClick && contextMenu.popup(remote.getCurrentWindow())
114 )}
115 >
116 <span
117 className={classnames([
118 classes.name,
119 isActive ? classes.activeName : null,
120 ])}
121 >
122 {name}
123 </span>
124 <span
125 className={classnames([
126 classes.services,
127 isActive ? classes.activeServices : null,
128 ])}
129 >
130 {services.length ? services.join(', ') : intl.formatMessage(messages.noServicesAddedYet)}
131 </span>
132 </div>
133 );
134 }
135}
136
137export default WorkspaceDrawerItem;
diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js
new file mode 100644
index 000000000..cc4b1a3ba
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceItem.js
@@ -0,0 +1,45 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { intlShape } from 'react-intl';
4import { observer } from 'mobx-react';
5import injectSheet from 'react-jss';
6
7import Workspace from '../models/Workspace';
8
9const styles = theme => ({
10 row: {
11 height: theme.workspaces.settings.listItems.height,
12 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
13 '&:hover': {
14 background: theme.workspaces.settings.listItems.hoverBgColor,
15 },
16 },
17 columnName: {},
18});
19
20@injectSheet(styles) @observer
21class WorkspaceItem extends Component {
22 static propTypes = {
23 classes: PropTypes.object.isRequired,
24 workspace: PropTypes.instanceOf(Workspace).isRequired,
25 onItemClick: PropTypes.func.isRequired,
26 };
27
28 static contextTypes = {
29 intl: intlShape,
30 };
31
32 render() {
33 const { classes, workspace, onItemClick } = this.props;
34
35 return (
36 <tr className={classes.row}>
37 <td onClick={() => onItemClick(workspace)}>
38 {workspace.name}
39 </td>
40 </tr>
41 );
42 }
43}
44
45export default WorkspaceItem;
diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.js b/src/features/workspaces/components/WorkspaceServiceListItem.js
new file mode 100644
index 000000000..e05b21440
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceServiceListItem.js
@@ -0,0 +1,75 @@
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 { Toggle } from '@meetfranz/forms';
7
8import Service from '../../../models/Service';
9import ServiceIcon from '../../../components/ui/ServiceIcon';
10
11const styles = theme => ({
12 listItem: {
13 height: theme.workspaces.settings.listItems.height,
14 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
15 display: 'flex',
16 alignItems: 'center',
17 },
18 serviceIcon: {
19 padding: theme.workspaces.settings.listItems.padding,
20 },
21 toggle: {
22 height: 'auto',
23 margin: 0,
24 },
25 label: {
26 padding: theme.workspaces.settings.listItems.padding,
27 flexGrow: 1,
28 },
29 disabledLabel: {
30 color: theme.workspaces.settings.listItems.disabled.color,
31 },
32});
33
34@injectSheet(styles) @observer
35class WorkspaceServiceListItem extends Component {
36 static propTypes = {
37 classes: PropTypes.object.isRequired,
38 isInWorkspace: PropTypes.bool.isRequired,
39 onToggle: PropTypes.func.isRequired,
40 service: PropTypes.instanceOf(Service).isRequired,
41 };
42
43 render() {
44 const {
45 classes,
46 isInWorkspace,
47 onToggle,
48 service,
49 } = this.props;
50
51 return (
52 <div className={classes.listItem}>
53 <ServiceIcon
54 className={classes.serviceIcon}
55 service={service}
56 />
57 <span
58 className={classnames([
59 classes.label,
60 service.isEnabled ? null : classes.disabledLabel,
61 ])}
62 >
63 {service.name}
64 </span>
65 <Toggle
66 className={classes.toggle}
67 checked={isInWorkspace}
68 onChange={onToggle}
69 />
70 </div>
71 );
72 }
73}
74
75export default WorkspaceServiceListItem;
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
new file mode 100644
index 000000000..c4a800a7b
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -0,0 +1,91 @@
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 { Loader } from '@meetfranz/ui';
7import { defineMessages, intlShape } from 'react-intl';
8
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.workspaces.drawer.width}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: 40,
43 height: 40,
44 marginRight: 10,
45 },
46 message: {
47 fontSize: 16,
48 whiteSpace: 'nowrap',
49 color: theme.colorAppLoaderSpinner,
50 },
51});
52
53@injectSheet(styles) @observer
54class WorkspaceSwitchingIndicator extends Component {
55 static propTypes = {
56 classes: PropTypes.object.isRequired,
57 theme: PropTypes.object.isRequired,
58 };
59
60 static contextTypes = {
61 intl: intlShape,
62 };
63
64 render() {
65 const { classes, theme } = this.props;
66 const { intl } = this.context;
67 const { isSwitchingWorkspace, isWorkspaceDrawerOpen, nextWorkspace } = workspaceStore;
68 if (!isSwitchingWorkspace) return null;
69 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services';
70 return (
71 <div
72 className={classnames([
73 classes.wrapper,
74 isWorkspaceDrawerOpen ? classes.wrapperWhenDrawerIsOpen : null,
75 ])}
76 >
77 <div className={classes.component}>
78 <Loader
79 className={classes.spinner}
80 color={theme.workspaces.switchingIndicator.spinnerColor}
81 />
82 <p className={classes.message}>
83 {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`}
84 </p>
85 </div>
86 </div>
87 );
88 }
89}
90
91export default WorkspaceSwitchingIndicator;
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
new file mode 100644
index 000000000..dd4381a15
--- /dev/null
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -0,0 +1,195 @@
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 = theme => ({
52 table: {
53 width: '100%',
54 '& td': {
55 padding: '10px',
56 },
57 },
58 createForm: {
59 height: 'auto',
60 },
61 appear: {
62 height: 'auto',
63 },
64 premiumAnnouncement: {
65 padding: '20px',
66 backgroundColor: '#3498db',
67 marginLeft: '-20px',
68 marginBottom: '20px',
69 height: 'auto',
70 color: 'white',
71 borderRadius: theme.borderRadius,
72 },
73});
74
75@injectSheet(styles) @observer
76class WorkspacesDashboard extends Component {
77 static propTypes = {
78 classes: PropTypes.object.isRequired,
79 getUserWorkspacesRequest: PropTypes.instanceOf(Request).isRequired,
80 createWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
81 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
82 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
83 onCreateWorkspaceSubmit: PropTypes.func.isRequired,
84 onWorkspaceClick: PropTypes.func.isRequired,
85 workspaces: MobxPropTypes.arrayOrObservableArray.isRequired,
86 };
87
88 static contextTypes = {
89 intl: intlShape,
90 };
91
92 render() {
93 const {
94 classes,
95 getUserWorkspacesRequest,
96 createWorkspaceRequest,
97 deleteWorkspaceRequest,
98 updateWorkspaceRequest,
99 onCreateWorkspaceSubmit,
100 onWorkspaceClick,
101 workspaces,
102 } = this.props;
103 const { intl } = this.context;
104 return (
105 <div className="settings__main">
106 <div className="settings__header">
107 <h1>{intl.formatMessage(messages.headline)}</h1>
108 </div>
109 <div className="settings__body">
110
111 {/* ===== Workspace updated info ===== */}
112 {updateWorkspaceRequest.wasExecuted && updateWorkspaceRequest.result && (
113 <Appear className={classes.appear}>
114 <Infobox
115 type="success"
116 icon="mdiCheckboxMarkedCircleOutline"
117 dismissable
118 onUnmount={updateWorkspaceRequest.reset}
119 >
120 {intl.formatMessage(messages.updatedInfo)}
121 </Infobox>
122 </Appear>
123 )}
124
125 {/* ===== Workspace deleted info ===== */}
126 {deleteWorkspaceRequest.wasExecuted && deleteWorkspaceRequest.result && (
127 <Appear className={classes.appear}>
128 <Infobox
129 type="success"
130 icon="mdiCheckboxMarkedCircleOutline"
131 dismissable
132 onUnmount={deleteWorkspaceRequest.reset}
133 >
134 {intl.formatMessage(messages.deletedInfo)}
135 </Infobox>
136 </Appear>
137 )}
138
139 {workspaceStore.isPremiumUpgradeRequired && (
140 <div className={classes.premiumAnnouncement}>
141 <h2>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h2>
142 <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p>
143 </div>
144 )}
145
146 <PremiumFeatureContainer
147 condition={workspaceStore.isPremiumFeature}
148 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'workspaces' }}
149 >
150 {/* ===== Create workspace form ===== */}
151 <div className={classes.createForm}>
152 <CreateWorkspaceForm
153 isSubmitting={createWorkspaceRequest.isExecuting}
154 onSubmit={onCreateWorkspaceSubmit}
155 />
156 </div>
157 {getUserWorkspacesRequest.isExecuting ? (
158 <Loader />
159 ) : (
160 <Fragment>
161 {/* ===== Workspace could not be loaded error ===== */}
162 {getUserWorkspacesRequest.error ? (
163 <Infobox
164 icon="alert"
165 type="danger"
166 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)}
167 ctaLoading={getUserWorkspacesRequest.isExecuting}
168 ctaOnClick={getUserWorkspacesRequest.retry}
169 >
170 {intl.formatMessage(messages.workspacesRequestFailed)}
171 </Infobox>
172 ) : (
173 <table className={classes.table}>
174 {/* ===== Workspaces list ===== */}
175 <tbody>
176 {workspaces.map(workspace => (
177 <WorkspaceItem
178 key={workspace.id}
179 workspace={workspace}
180 onItemClick={w => onWorkspaceClick(w)}
181 />
182 ))}
183 </tbody>
184 </table>
185 )}
186 </Fragment>
187 )}
188 </PremiumFeatureContainer>
189 </div>
190 </div>
191 );
192 }
193}
194
195export 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..ad9023b8b
--- /dev/null
+++ b/src/features/workspaces/index.js
@@ -0,0 +1,37 @@
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 } = stores;
14
15 // Toggle workspace feature
16 reaction(
17 () => features.features.isWorkspaceEnabled,
18 (isEnabled) => {
19 if (isEnabled && !workspaceStore.isFeatureActive) {
20 debug('Initializing `workspaces` feature');
21 workspaceStore.start(stores, actions);
22 } else if (workspaceStore.isFeatureActive) {
23 debug('Disabling `workspaces` feature');
24 workspaceStore.stop();
25 resetApiRequests();
26 }
27 },
28 {
29 fireImmediately: true,
30 },
31 );
32}
33
34export const WORKSPACES_ROUTES = {
35 ROOT: '/settings/workspaces',
36 EDIT: '/settings/workspaces/:action/:id',
37};
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..ea601700e
--- /dev/null
+++ b/src/features/workspaces/store.js
@@ -0,0 +1,276 @@
1import {
2 computed,
3 observable,
4 action,
5} from 'mobx';
6import localStorage from 'mobx-localstorage';
7import { matchRoute } from '../../helpers/routing-helpers';
8import { workspaceActions } from './actions';
9import { FeatureStore } from '../utils/FeatureStore';
10import {
11 createWorkspaceRequest,
12 deleteWorkspaceRequest,
13 getUserWorkspacesRequest,
14 updateWorkspaceRequest,
15} from './api';
16import { WORKSPACES_ROUTES } from './index';
17
18const debug = require('debug')('Franz:feature:workspaces:store');
19
20export default class WorkspacesStore extends FeatureStore {
21 @observable isFeatureEnabled = false;
22
23 @observable isFeatureActive = false;
24
25 @observable isPremiumFeature = true;
26
27 @observable isPremiumUpgradeRequired = true;
28
29 @observable activeWorkspace = null;
30
31 @observable nextWorkspace = null;
32
33 @observable workspaceBeingEdited = null;
34
35 @observable isSwitchingWorkspace = false;
36
37 @observable isWorkspaceDrawerOpen = false;
38
39 @observable isSettingsRouteActive = null;
40
41 @computed get workspaces() {
42 if (!this.isFeatureActive) return [];
43 return getUserWorkspacesRequest.result || [];
44 }
45
46 @computed get settings() {
47 return localStorage.getItem('workspaces') || {};
48 }
49
50 @computed get userHasWorkspaces() {
51 return getUserWorkspacesRequest.wasExecuted && this.workspaces.length > 0;
52 }
53
54 start(stores, actions) {
55 debug('WorkspacesStore::start');
56 this.stores = stores;
57 this.actions = actions;
58
59 this._listenToActions([
60 [workspaceActions.edit, this._edit],
61 [workspaceActions.create, this._create],
62 [workspaceActions.delete, this._delete],
63 [workspaceActions.update, this._update],
64 [workspaceActions.activate, this._setActiveWorkspace],
65 [workspaceActions.deactivate, this._deactivateActiveWorkspace],
66 [workspaceActions.toggleWorkspaceDrawer, this._toggleWorkspaceDrawer],
67 [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings],
68 ]);
69
70 this._startReactions([
71 this._setWorkspaceBeingEditedReaction,
72 this._setActiveServiceOnWorkspaceSwitchReaction,
73 this._setFeatureEnabledReaction,
74 this._setIsPremiumFeatureReaction,
75 this._activateLastUsedWorkspaceReaction,
76 this._openDrawerWithSettingsReaction,
77 this._cleanupInvalidServiceReferences,
78 ]);
79
80 getUserWorkspacesRequest.execute();
81 this.isFeatureActive = true;
82 }
83
84 stop() {
85 debug('WorkspacesStore::stop');
86 this.isFeatureActive = false;
87 this.activeWorkspace = null;
88 this.nextWorkspace = null;
89 this.workspaceBeingEdited = null;
90 this.isSwitchingWorkspace = false;
91 this.isWorkspaceDrawerOpen = false;
92 }
93
94 filterServicesByActiveWorkspace = (services) => {
95 const { activeWorkspace, isFeatureActive } = this;
96 if (isFeatureActive && activeWorkspace) {
97 return this.getWorkspaceServices(activeWorkspace);
98 }
99 return services;
100 };
101
102 getWorkspaceServices(workspace) {
103 const { services } = this.stores;
104 return workspace.services.map(id => services.one(id)).filter(s => !!s);
105 }
106
107 // ========== PRIVATE ========= //
108
109 _wasDrawerOpenBeforeSettingsRoute = null;
110
111 _getWorkspaceById = id => this.workspaces.find(w => w.id === id);
112
113 _updateSettings = (changes) => {
114 localStorage.setItem('workspaces', {
115 ...this.settings,
116 ...changes,
117 });
118 };
119
120 // Actions
121
122 @action _edit = ({ workspace }) => {
123 this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`);
124 };
125
126 @action _create = async ({ name }) => {
127 try {
128 const workspace = await createWorkspaceRequest.execute(name);
129 await getUserWorkspacesRequest.result.push(workspace);
130 this._edit({ workspace });
131 } catch (error) {
132 throw error;
133 }
134 };
135
136 @action _delete = async ({ workspace }) => {
137 try {
138 await deleteWorkspaceRequest.execute(workspace);
139 await getUserWorkspacesRequest.result.remove(workspace);
140 this.stores.router.push('/settings/workspaces');
141 } catch (error) {
142 throw error;
143 }
144 };
145
146 @action _update = async ({ workspace }) => {
147 try {
148 await updateWorkspaceRequest.execute(workspace);
149 // Path local result optimistically
150 const localWorkspace = this._getWorkspaceById(workspace.id);
151 Object.assign(localWorkspace, workspace);
152 this.stores.router.push('/settings/workspaces');
153 } catch (error) {
154 throw error;
155 }
156 };
157
158 @action _setActiveWorkspace = ({ workspace }) => {
159 // Indicate that we are switching to another workspace
160 this.isSwitchingWorkspace = true;
161 this.nextWorkspace = workspace;
162 // Delay switching to next workspace so that the services loading does not drag down UI
163 setTimeout(() => {
164 this.activeWorkspace = workspace;
165 this._updateSettings({ lastActiveWorkspace: workspace.id });
166 }, 100);
167 // Indicate that we are done switching to the next workspace
168 setTimeout(() => {
169 this.isSwitchingWorkspace = false;
170 this.nextWorkspace = null;
171 }, 1000);
172 };
173
174 @action _deactivateActiveWorkspace = () => {
175 // Indicate that we are switching to default workspace
176 this.isSwitchingWorkspace = true;
177 this.nextWorkspace = null;
178 this._updateSettings({ lastActiveWorkspace: null });
179 // Delay switching to next workspace so that the services loading does not drag down UI
180 setTimeout(() => {
181 this.activeWorkspace = null;
182 }, 100);
183 // Indicate that we are done switching to the default workspace
184 setTimeout(() => { this.isSwitchingWorkspace = false; }, 1000);
185 };
186
187 @action _toggleWorkspaceDrawer = () => {
188 this.isWorkspaceDrawerOpen = !this.isWorkspaceDrawerOpen;
189 };
190
191 @action _openWorkspaceSettings = () => {
192 this.actions.ui.openSettings({ path: 'workspaces' });
193 };
194
195 // Reactions
196
197 _setFeatureEnabledReaction = () => {
198 const { isWorkspaceEnabled } = this.stores.features.features;
199 this.isFeatureEnabled = isWorkspaceEnabled;
200 };
201
202 _setIsPremiumFeatureReaction = () => {
203 const { features, user } = this.stores;
204 const { isPremium } = user.data;
205 const { isWorkspacePremiumFeature } = features.features;
206 this.isPremiumFeature = isWorkspacePremiumFeature;
207 this.isPremiumUpgradeRequired = isWorkspacePremiumFeature && !isPremium;
208 };
209
210 _setWorkspaceBeingEditedReaction = () => {
211 const { pathname } = this.stores.router.location;
212 const match = matchRoute('/settings/workspaces/edit/:id', pathname);
213 if (match) {
214 this.workspaceBeingEdited = this._getWorkspaceById(match.id);
215 }
216 };
217
218 _setActiveServiceOnWorkspaceSwitchReaction = () => {
219 if (!this.isFeatureActive) return;
220 if (this.activeWorkspace) {
221 const services = this.stores.services.allDisplayed;
222 const activeService = services.find(s => s.isActive);
223 const workspaceServices = this.getWorkspaceServices(this.activeWorkspace);
224 if (workspaceServices.length <= 0) return;
225 const isActiveServiceInWorkspace = workspaceServices.includes(activeService);
226 if (!isActiveServiceInWorkspace) {
227 this.actions.service.setActive({ serviceId: workspaceServices[0].id });
228 }
229 }
230 };
231
232 _activateLastUsedWorkspaceReaction = () => {
233 if (!this.activeWorkspace && this.userHasWorkspaces) {
234 const { lastActiveWorkspace } = this.settings;
235 if (lastActiveWorkspace) {
236 const workspace = this._getWorkspaceById(lastActiveWorkspace);
237 if (workspace) this._setActiveWorkspace({ workspace });
238 }
239 }
240 };
241
242 _openDrawerWithSettingsReaction = () => {
243 const { router } = this.stores;
244 const isWorkspaceSettingsRoute = router.location.pathname.includes(WORKSPACES_ROUTES.ROOT);
245 const isSwitchingToSettingsRoute = !this.isSettingsRouteActive && isWorkspaceSettingsRoute;
246 const isLeavingSettingsRoute = !isWorkspaceSettingsRoute && this.isSettingsRouteActive;
247
248 if (isSwitchingToSettingsRoute) {
249 this.isSettingsRouteActive = true;
250 this._wasDrawerOpenBeforeSettingsRoute = this.isWorkspaceDrawerOpen;
251 if (!this._wasDrawerOpenBeforeSettingsRoute) {
252 workspaceActions.toggleWorkspaceDrawer();
253 }
254 } else if (isLeavingSettingsRoute) {
255 this.isSettingsRouteActive = false;
256 if (!this._wasDrawerOpenBeforeSettingsRoute && this.isWorkspaceDrawerOpen) {
257 workspaceActions.toggleWorkspaceDrawer();
258 }
259 }
260 };
261
262 _cleanupInvalidServiceReferences = () => {
263 const { services } = this.stores;
264 let invalidServiceReferencesExist = false;
265 this.workspaces.forEach((workspace) => {
266 workspace.services.forEach((serviceId) => {
267 if (!services.one(serviceId)) {
268 invalidServiceReferencesExist = true;
269 }
270 });
271 });
272 if (invalidServiceReferencesExist) {
273 getUserWorkspacesRequest.execute();
274 }
275 };
276}
diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
index 2560a5add..06a03db65 100644
--- a/src/i18n/locales/de.json
+++ b/src/i18n/locales/de.json
@@ -87,6 +87,9 @@
87 "menu.window" : "Fenster", 87 "menu.window" : "Fenster",
88 "menu.window.close" : "Schließen", 88 "menu.window.close" : "Schließen",
89 "menu.window.minimize" : "Minimieren", 89 "menu.window.minimize" : "Minimieren",
90 "menu.workspaces": "Workspaces",
91 "menu.workspaces.defaultWorkspace": "All services",
92 "menu.workspaces.addNewWorkspace": "Add New Workspace",
90 "password.email.label" : "E-Mail Adresse", 93 "password.email.label" : "E-Mail Adresse",
91 "password.headline" : "Passwort zurücksetzen", 94 "password.headline" : "Passwort zurücksetzen",
92 "password.link.login" : "An Deinem Konto anmelden", 95 "password.link.login" : "An Deinem Konto anmelden",
@@ -169,6 +172,7 @@
169 "settings.navigation.logout" : "Abmelden", 172 "settings.navigation.logout" : "Abmelden",
170 "settings.navigation.settings" : "Einstellungen", 173 "settings.navigation.settings" : "Einstellungen",
171 "settings.navigation.yourServices" : "Deine Dienste", 174 "settings.navigation.yourServices" : "Deine Dienste",
175 "settings.navigation.yourWorkspaces": "Deine Workspaces",
172 "settings.recipes.all" : "Alle Dienste", 176 "settings.recipes.all" : "Alle Dienste",
173 "settings.recipes.dev" : "Entwicklung", 177 "settings.recipes.dev" : "Entwicklung",
174 "settings.recipes.headline" : "Verfügbare Dienste", 178 "settings.recipes.headline" : "Verfügbare Dienste",
@@ -226,6 +230,14 @@
226 "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert", 230 "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert",
227 "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert", 231 "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert",
228 "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert", 232 "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert",
233 "settings.workspaces.headline": "Deine Workspaces",
234 "settings.workspace.add.form.submitButton": "Workspace erstellen",
235 "settings.workspace.add.form.name": "Name",
236 "settings.workspace.form.yourWorkspaces": "Deine Workspaces",
237 "settings.workspace.form.name": "Name",
238 "settings.workspace.form.buttonDelete": "Workspace löschen",
239 "settings.workspace.form.buttonSave": "Workspace speichern",
240 "settings.workspace.form.servicesInWorkspaceHeadline": "Services in diesem Workspace",
229 "settings.user.form.accountType.company" : "Firma", 241 "settings.user.form.accountType.company" : "Firma",
230 "settings.user.form.accountType.individual" : "Einzelperson", 242 "settings.user.form.accountType.individual" : "Einzelperson",
231 "settings.user.form.accountType.label" : "Konto-Typ", 243 "settings.user.form.accountType.label" : "Konto-Typ",
diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json
index 650bfc65f..8fe5e8852 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -625,78 +625,78 @@
625 "defaultMessage": "!!!Your services have been updated.", 625 "defaultMessage": "!!!Your services have been updated.",
626 "end": { 626 "end": {
627 "column": 3, 627 "column": 3,
628 "line": 25 628 "line": 28
629 }, 629 },
630 "file": "src/components/layout/AppLayout.js", 630 "file": "src/components/layout/AppLayout.js",
631 "id": "infobar.servicesUpdated", 631 "id": "infobar.servicesUpdated",
632 "start": { 632 "start": {
633 "column": 19, 633 "column": 19,
634 "line": 22 634 "line": 25
635 } 635 }
636 }, 636 },
637 { 637 {
638 "defaultMessage": "!!!A new update for Franz is available.", 638 "defaultMessage": "!!!A new update for Franz is available.",
639 "end": { 639 "end": {
640 "column": 3, 640 "column": 3,
641 "line": 29 641 "line": 32
642 }, 642 },
643 "file": "src/components/layout/AppLayout.js", 643 "file": "src/components/layout/AppLayout.js",
644 "id": "infobar.updateAvailable", 644 "id": "infobar.updateAvailable",
645 "start": { 645 "start": {
646 "column": 19, 646 "column": 19,
647 "line": 26 647 "line": 29
648 } 648 }
649 }, 649 },
650 { 650 {
651 "defaultMessage": "!!!Reload services", 651 "defaultMessage": "!!!Reload services",
652 "end": { 652 "end": {
653 "column": 3, 653 "column": 3,
654 "line": 33 654 "line": 36
655 }, 655 },
656 "file": "src/components/layout/AppLayout.js", 656 "file": "src/components/layout/AppLayout.js",
657 "id": "infobar.buttonReloadServices", 657 "id": "infobar.buttonReloadServices",
658 "start": { 658 "start": {
659 "column": 24, 659 "column": 24,
660 "line": 30 660 "line": 33
661 } 661 }
662 }, 662 },
663 { 663 {
664 "defaultMessage": "!!!Changelog", 664 "defaultMessage": "!!!Changelog",
665 "end": { 665 "end": {
666 "column": 3, 666 "column": 3,
667 "line": 37 667 "line": 40
668 }, 668 },
669 "file": "src/components/layout/AppLayout.js", 669 "file": "src/components/layout/AppLayout.js",
670 "id": "infobar.buttonChangelog", 670 "id": "infobar.buttonChangelog",
671 "start": { 671 "start": {
672 "column": 13, 672 "column": 13,
673 "line": 34 673 "line": 37
674 } 674 }
675 }, 675 },
676 { 676 {
677 "defaultMessage": "!!!Restart & install update", 677 "defaultMessage": "!!!Restart & install update",
678 "end": { 678 "end": {
679 "column": 3, 679 "column": 3,
680 "line": 41 680 "line": 44
681 }, 681 },
682 "file": "src/components/layout/AppLayout.js", 682 "file": "src/components/layout/AppLayout.js",
683 "id": "infobar.buttonInstallUpdate", 683 "id": "infobar.buttonInstallUpdate",
684 "start": { 684 "start": {
685 "column": 23, 685 "column": 23,
686 "line": 38 686 "line": 41
687 } 687 }
688 }, 688 },
689 { 689 {
690 "defaultMessage": "!!!Could not load services and user information", 690 "defaultMessage": "!!!Could not load services and user information",
691 "end": { 691 "end": {
692 "column": 3, 692 "column": 3,
693 "line": 45 693 "line": 48
694 }, 694 },
695 "file": "src/components/layout/AppLayout.js", 695 "file": "src/components/layout/AppLayout.js",
696 "id": "infobar.requiredRequestsFailed", 696 "id": "infobar.requiredRequestsFailed",
697 "start": { 697 "start": {
698 "column": 26, 698 "column": 26,
699 "line": 42 699 "line": 45
700 } 700 }
701 } 701 }
702 ], 702 ],
@@ -708,52 +708,78 @@
708 "defaultMessage": "!!!Settings", 708 "defaultMessage": "!!!Settings",
709 "end": { 709 "end": {
710 "column": 3, 710 "column": 3,
711 "line": 14 711 "line": 16
712 }, 712 },
713 "file": "src/components/layout/Sidebar.js", 713 "file": "src/components/layout/Sidebar.js",
714 "id": "sidebar.settings", 714 "id": "sidebar.settings",
715 "start": { 715 "start": {
716 "column": 12, 716 "column": 12,
717 "line": 11 717 "line": 13
718 } 718 }
719 }, 719 },
720 { 720 {
721 "defaultMessage": "!!!Add new service", 721 "defaultMessage": "!!!Add new service",
722 "end": { 722 "end": {
723 "column": 3, 723 "column": 3,
724 "line": 18 724 "line": 20
725 }, 725 },
726 "file": "src/components/layout/Sidebar.js", 726 "file": "src/components/layout/Sidebar.js",
727 "id": "sidebar.addNewService", 727 "id": "sidebar.addNewService",
728 "start": { 728 "start": {
729 "column": 17, 729 "column": 17,
730 "line": 15 730 "line": 17
731 } 731 }
732 }, 732 },
733 { 733 {
734 "defaultMessage": "!!!Disable notifications & audio", 734 "defaultMessage": "!!!Disable notifications & audio",
735 "end": { 735 "end": {
736 "column": 3, 736 "column": 3,
737 "line": 22 737 "line": 24
738 }, 738 },
739 "file": "src/components/layout/Sidebar.js", 739 "file": "src/components/layout/Sidebar.js",
740 "id": "sidebar.muteApp", 740 "id": "sidebar.muteApp",
741 "start": { 741 "start": {
742 "column": 8, 742 "column": 8,
743 "line": 19 743 "line": 21
744 } 744 }
745 }, 745 },
746 { 746 {
747 "defaultMessage": "!!!Enable notifications & audio", 747 "defaultMessage": "!!!Enable notifications & audio",
748 "end": { 748 "end": {
749 "column": 3, 749 "column": 3,
750 "line": 26 750 "line": 28
751 }, 751 },
752 "file": "src/components/layout/Sidebar.js", 752 "file": "src/components/layout/Sidebar.js",
753 "id": "sidebar.unmuteApp", 753 "id": "sidebar.unmuteApp",
754 "start": { 754 "start": {
755 "column": 10, 755 "column": 10,
756 "line": 23 756 "line": 25
757 }
758 },
759 {
760 "defaultMessage": "!!!Open workspace drawer",
761 "end": {
762 "column": 3,
763 "line": 32
764 },
765 "file": "src/components/layout/Sidebar.js",
766 "id": "sidebar.openWorkspaceDrawer",
767 "start": {
768 "column": 23,
769 "line": 29
770 }
771 },
772 {
773 "defaultMessage": "!!!Close workspace drawer",
774 "end": {
775 "column": 3,
776 "line": 36
777 },
778 "file": "src/components/layout/Sidebar.js",
779 "id": "sidebar.closeWorkspaceDrawer",
780 "start": {
781 "column": 24,
782 "line": 33
757 } 783 }
758 } 784 }
759 ], 785 ],
@@ -1276,46 +1302,59 @@
1276 "defaultMessage": "!!!Available services", 1302 "defaultMessage": "!!!Available services",
1277 "end": { 1303 "end": {
1278 "column": 3, 1304 "column": 3,
1279 "line": 12 1305 "line": 15
1280 }, 1306 },
1281 "file": "src/components/settings/navigation/SettingsNavigation.js", 1307 "file": "src/components/settings/navigation/SettingsNavigation.js",
1282 "id": "settings.navigation.availableServices", 1308 "id": "settings.navigation.availableServices",
1283 "start": { 1309 "start": {
1284 "column": 21, 1310 "column": 21,
1285 "line": 9 1311 "line": 12
1286 } 1312 }
1287 }, 1313 },
1288 { 1314 {
1289 "defaultMessage": "!!!Your services", 1315 "defaultMessage": "!!!Your services",
1290 "end": { 1316 "end": {
1291 "column": 3, 1317 "column": 3,
1292 "line": 16 1318 "line": 19
1293 }, 1319 },
1294 "file": "src/components/settings/navigation/SettingsNavigation.js", 1320 "file": "src/components/settings/navigation/SettingsNavigation.js",
1295 "id": "settings.navigation.yourServices", 1321 "id": "settings.navigation.yourServices",
1296 "start": { 1322 "start": {
1297 "column": 16, 1323 "column": 16,
1298 "line": 13 1324 "line": 16
1299 } 1325 }
1300 }, 1326 },
1301 { 1327 {
1302 "defaultMessage": "!!!Account", 1328 "defaultMessage": "!!!Your workspaces",
1303 "end": { 1329 "end": {
1304 "column": 3, 1330 "column": 3,
1331 "line": 23
1332 },
1333 "file": "src/components/settings/navigation/SettingsNavigation.js",
1334 "id": "settings.navigation.yourWorkspaces",
1335 "start": {
1336 "column": 18,
1305 "line": 20 1337 "line": 20
1338 }
1339 },
1340 {
1341 "defaultMessage": "!!!Account",
1342 "end": {
1343 "column": 3,
1344 "line": 27
1306 }, 1345 },
1307 "file": "src/components/settings/navigation/SettingsNavigation.js", 1346 "file": "src/components/settings/navigation/SettingsNavigation.js",
1308 "id": "settings.navigation.account", 1347 "id": "settings.navigation.account",
1309 "start": { 1348 "start": {
1310 "column": 11, 1349 "column": 11,
1311 "line": 17 1350 "line": 24
1312 } 1351 }
1313 }, 1352 },
1314 { 1353 {
1315 "defaultMessage": "!!!Manage Team", 1354 "defaultMessage": "!!!Manage Team",
1316 "end": { 1355 "end": {
1317 "column": 3, 1356 "column": 3,
1318 "line": 24 1357 "line": 31
1319 }, 1358 },
1320 "file": "src/components/settings/navigation/SettingsNavigation.js", 1359 "file": "src/components/settings/navigation/SettingsNavigation.js",
1321 "id": "settings.navigation.team", 1360 "id": "settings.navigation.team",
@@ -1334,33 +1373,33 @@
1334 "id": "settings.navigation.settings", 1373 "id": "settings.navigation.settings",
1335 "start": { 1374 "start": {
1336 "column": 12, 1375 "column": 12,
1337 "line": 25 1376 "line": 28
1338 } 1377 }
1339 }, 1378 },
1340 { 1379 {
1341 "defaultMessage": "!!!Invite Friends", 1380 "defaultMessage": "!!!Invite Friends",
1342 "end": { 1381 "end": {
1343 "column": 3, 1382 "column": 3,
1344 "line": 32 1383 "line": 35
1345 }, 1384 },
1346 "file": "src/components/settings/navigation/SettingsNavigation.js", 1385 "file": "src/components/settings/navigation/SettingsNavigation.js",
1347 "id": "settings.navigation.inviteFriends", 1386 "id": "settings.navigation.inviteFriends",
1348 "start": { 1387 "start": {
1349 "column": 17, 1388 "column": 17,
1350 "line": 29 1389 "line": 32
1351 } 1390 }
1352 }, 1391 },
1353 { 1392 {
1354 "defaultMessage": "!!!Logout", 1393 "defaultMessage": "!!!Logout",
1355 "end": { 1394 "end": {
1356 "column": 3, 1395 "column": 3,
1357 "line": 36 1396 "line": 39
1358 }, 1397 },
1359 "file": "src/components/settings/navigation/SettingsNavigation.js", 1398 "file": "src/components/settings/navigation/SettingsNavigation.js",
1360 "id": "settings.navigation.logout", 1399 "id": "settings.navigation.logout",
1361 "start": { 1400 "start": {
1362 "column": 10, 1401 "column": 10,
1363 "line": 33 1402 "line": 36
1364 } 1403 }
1365 } 1404 }
1366 ], 1405 ],
@@ -2592,13 +2631,13 @@
2592 "defaultMessage": "!!!Upgrade account", 2631 "defaultMessage": "!!!Upgrade account",
2593 "end": { 2632 "end": {
2594 "column": 3, 2633 "column": 3,
2595 "line": 17 2634 "line": 18
2596 }, 2635 },
2597 "file": "src/components/ui/PremiumFeatureContainer/index.js", 2636 "file": "src/components/ui/PremiumFeatureContainer/index.js",
2598 "id": "premiumFeature.button.upgradeAccount", 2637 "id": "premiumFeature.button.upgradeAccount",
2599 "start": { 2638 "start": {
2600 "column": 10, 2639 "column": 10,
2601 "line": 14 2640 "line": 15
2602 } 2641 }
2603 } 2642 }
2604 ], 2643 ],
@@ -2607,6 +2646,24 @@
2607 { 2646 {
2608 "descriptors": [ 2647 "descriptors": [
2609 { 2648 {
2649 "defaultMessage": "!!!Loading",
2650 "end": {
2651 "column": 3,
2652 "line": 14
2653 },
2654 "file": "src/components/ui/WebviewLoader/index.js",
2655 "id": "service.webviewLoader.loading",
2656 "start": {
2657 "column": 11,
2658 "line": 11
2659 }
2660 }
2661 ],
2662 "path": "src/components/ui/WebviewLoader/index.json"
2663 },
2664 {
2665 "descriptors": [
2666 {
2610 "defaultMessage": "!!!Something went wrong.", 2667 "defaultMessage": "!!!Something went wrong.",
2611 "end": { 2668 "end": {
2612 "column": 3, 2669 "column": 3,
@@ -3261,6 +3318,374 @@
3261 { 3318 {
3262 "descriptors": [ 3319 "descriptors": [
3263 { 3320 {
3321 "defaultMessage": "!!!Create workspace",
3322 "end": {
3323 "column": 3,
3324 "line": 16
3325 },
3326 "file": "src/features/workspaces/components/CreateWorkspaceForm.js",
3327 "id": "settings.workspace.add.form.submitButton",
3328 "start": {
3329 "column": 16,
3330 "line": 13
3331 }
3332 },
3333 {
3334 "defaultMessage": "!!!Name",
3335 "end": {
3336 "column": 3,
3337 "line": 20
3338 },
3339 "file": "src/features/workspaces/components/CreateWorkspaceForm.js",
3340 "id": "settings.workspace.add.form.name",
3341 "start": {
3342 "column": 8,
3343 "line": 17
3344 }
3345 }
3346 ],
3347 "path": "src/features/workspaces/components/CreateWorkspaceForm.json"
3348 },
3349 {
3350 "descriptors": [
3351 {
3352 "defaultMessage": "!!!Delete workspace",
3353 "end": {
3354 "column": 3,
3355 "line": 22
3356 },
3357 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
3358 "id": "settings.workspace.form.buttonDelete",
3359 "start": {
3360 "column": 16,
3361 "line": 19
3362 }
3363 },
3364 {
3365 "defaultMessage": "!!!Save workspace",
3366 "end": {
3367 "column": 3,
3368 "line": 26
3369 },
3370 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
3371 "id": "settings.workspace.form.buttonSave",
3372 "start": {
3373 "column": 14,
3374 "line": 23
3375 }
3376 },
3377 {
3378 "defaultMessage": "!!!Name",
3379 "end": {
3380 "column": 3,
3381 "line": 30
3382 },
3383 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
3384 "id": "settings.workspace.form.name",
3385 "start": {
3386 "column": 8,
3387 "line": 27
3388 }
3389 },
3390 {
3391 "defaultMessage": "!!!Your workspaces",
3392 "end": {
3393 "column": 3,
3394 "line": 34
3395 },
3396 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
3397 "id": "settings.workspace.form.yourWorkspaces",
3398 "start": {
3399 "column": 18,
3400 "line": 31
3401 }
3402 },
3403 {
3404 "defaultMessage": "!!!Services in this Workspace",
3405 "end": {
3406 "column": 3,
3407 "line": 38
3408 },
3409 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
3410 "id": "settings.workspace.form.servicesInWorkspaceHeadline",
3411 "start": {
3412 "column": 31,
3413 "line": 35
3414 }
3415 }
3416 ],
3417 "path": "src/features/workspaces/components/EditWorkspaceForm.json"
3418 },
3419 {
3420 "descriptors": [
3421 {
3422 "defaultMessage": "!!!Workspaces",
3423 "end": {
3424 "column": 3,
3425 "line": 19
3426 },
3427 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3428 "id": "workspaceDrawer.headline",
3429 "start": {
3430 "column": 12,
3431 "line": 16
3432 }
3433 },
3434 {
3435 "defaultMessage": "!!!All services",
3436 "end": {
3437 "column": 3,
3438 "line": 23
3439 },
3440 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3441 "id": "workspaceDrawer.allServices",
3442 "start": {
3443 "column": 15,
3444 "line": 20
3445 }
3446 },
3447 {
3448 "defaultMessage": "!!!Workspaces settings",
3449 "end": {
3450 "column": 3,
3451 "line": 27
3452 },
3453 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3454 "id": "workspaceDrawer.workspacesSettingsTooltip",
3455 "start": {
3456 "column": 29,
3457 "line": 24
3458 }
3459 },
3460 {
3461 "defaultMessage": "!!!Info about workspace feature",
3462 "end": {
3463 "column": 3,
3464 "line": 31
3465 },
3466 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3467 "id": "workspaceDrawer.workspaceFeatureInfo",
3468 "start": {
3469 "column": 24,
3470 "line": 28
3471 }
3472 },
3473 {
3474 "defaultMessage": "!!!Create your first workspace",
3475 "end": {
3476 "column": 3,
3477 "line": 35
3478 },
3479 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3480 "id": "workspaceDrawer.premiumCtaButtonLabel",
3481 "start": {
3482 "column": 25,
3483 "line": 32
3484 }
3485 },
3486 {
3487 "defaultMessage": "!!!Reactivate premium account",
3488 "end": {
3489 "column": 3,
3490 "line": 39
3491 },
3492 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3493 "id": "workspaceDrawer.reactivatePremiumAccountLabel",
3494 "start": {
3495 "column": 28,
3496 "line": 36
3497 }
3498 },
3499 {
3500 "defaultMessage": "!!!add new workspace",
3501 "end": {
3502 "column": 3,
3503 "line": 43
3504 },
3505 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3506 "id": "workspaceDrawer.addNewWorkspaceLabel",
3507 "start": {
3508 "column": 24,
3509 "line": 40
3510 }
3511 },
3512 {
3513 "defaultMessage": "!!!Premium feature",
3514 "end": {
3515 "column": 3,
3516 "line": 47
3517 },
3518 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
3519 "id": "workspaceDrawer.proFeatureBadge",
3520 "start": {
3521 "column": 23,
3522 "line": 44
3523 }
3524 }
3525 ],
3526 "path": "src/features/workspaces/components/WorkspaceDrawer.json"
3527 },
3528 {
3529 "descriptors": [
3530 {
3531 "defaultMessage": "!!!No services added yet",
3532 "end": {
3533 "column": 3,
3534 "line": 15
3535 },
3536 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
3537 "id": "workspaceDrawer.item.noServicesAddedYet",
3538 "start": {
3539 "column": 22,
3540 "line": 12
3541 }
3542 },
3543 {
3544 "defaultMessage": "!!!edit",
3545 "end": {
3546 "column": 3,
3547 "line": 19
3548 },
3549 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
3550 "id": "workspaceDrawer.item.contextMenuEdit",
3551 "start": {
3552 "column": 19,
3553 "line": 16
3554 }
3555 }
3556 ],
3557 "path": "src/features/workspaces/components/WorkspaceDrawerItem.json"
3558 },
3559 {
3560 "descriptors": [
3561 {
3562 "defaultMessage": "!!!Your workspaces",
3563 "end": {
3564 "column": 3,
3565 "line": 20
3566 },
3567 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3568 "id": "settings.workspaces.headline",
3569 "start": {
3570 "column": 12,
3571 "line": 17
3572 }
3573 },
3574 {
3575 "defaultMessage": "!!!You haven't added any workspaces yet.",
3576 "end": {
3577 "column": 3,
3578 "line": 24
3579 },
3580 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3581 "id": "settings.workspaces.noWorkspacesAdded",
3582 "start": {
3583 "column": 19,
3584 "line": 21
3585 }
3586 },
3587 {
3588 "defaultMessage": "!!!Could not load your workspaces",
3589 "end": {
3590 "column": 3,
3591 "line": 28
3592 },
3593 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3594 "id": "settings.workspaces.workspacesRequestFailed",
3595 "start": {
3596 "column": 27,
3597 "line": 25
3598 }
3599 },
3600 {
3601 "defaultMessage": "!!!Try again",
3602 "end": {
3603 "column": 3,
3604 "line": 32
3605 },
3606 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3607 "id": "settings.workspaces.tryReloadWorkspaces",
3608 "start": {
3609 "column": 23,
3610 "line": 29
3611 }
3612 },
3613 {
3614 "defaultMessage": "!!!Your changes have been saved",
3615 "end": {
3616 "column": 3,
3617 "line": 36
3618 },
3619 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3620 "id": "settings.workspaces.updatedInfo",
3621 "start": {
3622 "column": 15,
3623 "line": 33
3624 }
3625 },
3626 {
3627 "defaultMessage": "!!!Workspace has been deleted",
3628 "end": {
3629 "column": 3,
3630 "line": 40
3631 },
3632 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3633 "id": "settings.workspaces.deletedInfo",
3634 "start": {
3635 "column": 15,
3636 "line": 37
3637 }
3638 },
3639 {
3640 "defaultMessage": "!!!Info about workspace feature",
3641 "end": {
3642 "column": 3,
3643 "line": 44
3644 },
3645 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3646 "id": "settings.workspaces.workspaceFeatureInfo",
3647 "start": {
3648 "column": 24,
3649 "line": 41
3650 }
3651 },
3652 {
3653 "defaultMessage": "!!!Less is More: Introducing Franz Workspaces",
3654 "end": {
3655 "column": 3,
3656 "line": 48
3657 },
3658 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
3659 "id": "settings.workspaces.workspaceFeatureHeadline",
3660 "start": {
3661 "column": 28,
3662 "line": 45
3663 }
3664 }
3665 ],
3666 "path": "src/features/workspaces/components/WorkspacesDashboard.json"
3667 },
3668 {
3669 "descriptors": [
3670 {
3671 "defaultMessage": "!!!Switching to",
3672 "end": {
3673 "column": 3,
3674 "line": 15
3675 },
3676 "file": "src/features/workspaces/components/WorkspaceSwitchingIndicator.js",
3677 "id": "workspaces.switchingIndicator.switchingTo",
3678 "start": {
3679 "column": 15,
3680 "line": 12
3681 }
3682 }
3683 ],
3684 "path": "src/features/workspaces/components/WorkspaceSwitchingIndicator.json"
3685 },
3686 {
3687 "descriptors": [
3688 {
3264 "defaultMessage": "!!!Field is required", 3689 "defaultMessage": "!!!Field is required",
3265 "end": { 3690 "end": {
3266 "column": 3, 3691 "column": 3,
@@ -3417,614 +3842,679 @@
3417 "defaultMessage": "!!!Edit", 3842 "defaultMessage": "!!!Edit",
3418 "end": { 3843 "end": {
3419 "column": 3, 3844 "column": 3,
3420 "line": 13 3845 "line": 16
3421 }, 3846 },
3422 "file": "src/lib/Menu.js", 3847 "file": "src/lib/Menu.js",
3423 "id": "menu.edit", 3848 "id": "menu.edit",
3424 "start": { 3849 "start": {
3425 "column": 8, 3850 "column": 8,
3426 "line": 10 3851 "line": 13
3427 } 3852 }
3428 }, 3853 },
3429 { 3854 {
3430 "defaultMessage": "!!!Undo", 3855 "defaultMessage": "!!!Undo",
3431 "end": { 3856 "end": {
3432 "column": 3, 3857 "column": 3,
3433 "line": 17 3858 "line": 20
3434 }, 3859 },
3435 "file": "src/lib/Menu.js", 3860 "file": "src/lib/Menu.js",
3436 "id": "menu.edit.undo", 3861 "id": "menu.edit.undo",
3437 "start": { 3862 "start": {
3438 "column": 8, 3863 "column": 8,
3439 "line": 14 3864 "line": 17
3440 } 3865 }
3441 }, 3866 },
3442 { 3867 {
3443 "defaultMessage": "!!!Redo", 3868 "defaultMessage": "!!!Redo",
3444 "end": { 3869 "end": {
3445 "column": 3, 3870 "column": 3,
3446 "line": 21 3871 "line": 24
3447 }, 3872 },
3448 "file": "src/lib/Menu.js", 3873 "file": "src/lib/Menu.js",
3449 "id": "menu.edit.redo", 3874 "id": "menu.edit.redo",
3450 "start": { 3875 "start": {
3451 "column": 8, 3876 "column": 8,
3452 "line": 18 3877 "line": 21
3453 } 3878 }
3454 }, 3879 },
3455 { 3880 {
3456 "defaultMessage": "!!!Cut", 3881 "defaultMessage": "!!!Cut",
3457 "end": { 3882 "end": {
3458 "column": 3, 3883 "column": 3,
3459 "line": 25 3884 "line": 28
3460 }, 3885 },
3461 "file": "src/lib/Menu.js", 3886 "file": "src/lib/Menu.js",
3462 "id": "menu.edit.cut", 3887 "id": "menu.edit.cut",
3463 "start": { 3888 "start": {
3464 "column": 7, 3889 "column": 7,
3465 "line": 22 3890 "line": 25
3466 } 3891 }
3467 }, 3892 },
3468 { 3893 {
3469 "defaultMessage": "!!!Copy", 3894 "defaultMessage": "!!!Copy",
3470 "end": { 3895 "end": {
3471 "column": 3, 3896 "column": 3,
3472 "line": 29 3897 "line": 32
3473 }, 3898 },
3474 "file": "src/lib/Menu.js", 3899 "file": "src/lib/Menu.js",
3475 "id": "menu.edit.copy", 3900 "id": "menu.edit.copy",
3476 "start": { 3901 "start": {
3477 "column": 8, 3902 "column": 8,
3478 "line": 26 3903 "line": 29
3479 } 3904 }
3480 }, 3905 },
3481 { 3906 {
3482 "defaultMessage": "!!!Paste", 3907 "defaultMessage": "!!!Paste",
3483 "end": { 3908 "end": {
3484 "column": 3, 3909 "column": 3,
3485 "line": 33 3910 "line": 36
3486 }, 3911 },
3487 "file": "src/lib/Menu.js", 3912 "file": "src/lib/Menu.js",
3488 "id": "menu.edit.paste", 3913 "id": "menu.edit.paste",
3489 "start": { 3914 "start": {
3490 "column": 9, 3915 "column": 9,
3491 "line": 30 3916 "line": 33
3492 } 3917 }
3493 }, 3918 },
3494 { 3919 {
3495 "defaultMessage": "!!!Paste And Match Style", 3920 "defaultMessage": "!!!Paste And Match Style",
3496 "end": { 3921 "end": {
3497 "column": 3, 3922 "column": 3,
3498 "line": 37 3923 "line": 40
3499 }, 3924 },
3500 "file": "src/lib/Menu.js", 3925 "file": "src/lib/Menu.js",
3501 "id": "menu.edit.pasteAndMatchStyle", 3926 "id": "menu.edit.pasteAndMatchStyle",
3502 "start": { 3927 "start": {
3503 "column": 22, 3928 "column": 22,
3504 "line": 34 3929 "line": 37
3505 } 3930 }
3506 }, 3931 },
3507 { 3932 {
3508 "defaultMessage": "!!!Delete", 3933 "defaultMessage": "!!!Delete",
3509 "end": { 3934 "end": {
3510 "column": 3, 3935 "column": 3,
3511 "line": 41 3936 "line": 44
3512 }, 3937 },
3513 "file": "src/lib/Menu.js", 3938 "file": "src/lib/Menu.js",
3514 "id": "menu.edit.delete", 3939 "id": "menu.edit.delete",
3515 "start": { 3940 "start": {
3516 "column": 10, 3941 "column": 10,
3517 "line": 38 3942 "line": 41
3518 } 3943 }
3519 }, 3944 },
3520 { 3945 {
3521 "defaultMessage": "!!!Select All", 3946 "defaultMessage": "!!!Select All",
3522 "end": { 3947 "end": {
3523 "column": 3, 3948 "column": 3,
3524 "line": 45 3949 "line": 48
3525 }, 3950 },
3526 "file": "src/lib/Menu.js", 3951 "file": "src/lib/Menu.js",
3527 "id": "menu.edit.selectAll", 3952 "id": "menu.edit.selectAll",
3528 "start": { 3953 "start": {
3529 "column": 13, 3954 "column": 13,
3530 "line": 42 3955 "line": 45
3531 } 3956 }
3532 }, 3957 },
3533 { 3958 {
3534 "defaultMessage": "!!!Speech", 3959 "defaultMessage": "!!!Speech",
3535 "end": { 3960 "end": {
3536 "column": 3, 3961 "column": 3,
3537 "line": 49 3962 "line": 52
3538 }, 3963 },
3539 "file": "src/lib/Menu.js", 3964 "file": "src/lib/Menu.js",
3540 "id": "menu.edit.speech", 3965 "id": "menu.edit.speech",
3541 "start": { 3966 "start": {
3542 "column": 10, 3967 "column": 10,
3543 "line": 46 3968 "line": 49
3544 } 3969 }
3545 }, 3970 },
3546 { 3971 {
3547 "defaultMessage": "!!!Start Speaking", 3972 "defaultMessage": "!!!Start Speaking",
3548 "end": { 3973 "end": {
3549 "column": 3, 3974 "column": 3,
3550 "line": 53 3975 "line": 56
3551 }, 3976 },
3552 "file": "src/lib/Menu.js", 3977 "file": "src/lib/Menu.js",
3553 "id": "menu.edit.startSpeaking", 3978 "id": "menu.edit.startSpeaking",
3554 "start": { 3979 "start": {
3555 "column": 17, 3980 "column": 17,
3556 "line": 50 3981 "line": 53
3557 } 3982 }
3558 }, 3983 },
3559 { 3984 {
3560 "defaultMessage": "!!!Stop Speaking", 3985 "defaultMessage": "!!!Stop Speaking",
3561 "end": { 3986 "end": {
3562 "column": 3, 3987 "column": 3,
3563 "line": 57 3988 "line": 60
3564 }, 3989 },
3565 "file": "src/lib/Menu.js", 3990 "file": "src/lib/Menu.js",
3566 "id": "menu.edit.stopSpeaking", 3991 "id": "menu.edit.stopSpeaking",
3567 "start": { 3992 "start": {
3568 "column": 16, 3993 "column": 16,
3569 "line": 54 3994 "line": 57
3570 } 3995 }
3571 }, 3996 },
3572 { 3997 {
3573 "defaultMessage": "!!!Start Dictation", 3998 "defaultMessage": "!!!Start Dictation",
3574 "end": { 3999 "end": {
3575 "column": 3, 4000 "column": 3,
3576 "line": 61 4001 "line": 64
3577 }, 4002 },
3578 "file": "src/lib/Menu.js", 4003 "file": "src/lib/Menu.js",
3579 "id": "menu.edit.startDictation", 4004 "id": "menu.edit.startDictation",
3580 "start": { 4005 "start": {
3581 "column": 18, 4006 "column": 18,
3582 "line": 58 4007 "line": 61
3583 } 4008 }
3584 }, 4009 },
3585 { 4010 {
3586 "defaultMessage": "!!!Emoji & Symbols", 4011 "defaultMessage": "!!!Emoji & Symbols",
3587 "end": { 4012 "end": {
3588 "column": 3, 4013 "column": 3,
3589 "line": 65 4014 "line": 68
3590 }, 4015 },
3591 "file": "src/lib/Menu.js", 4016 "file": "src/lib/Menu.js",
3592 "id": "menu.edit.emojiSymbols", 4017 "id": "menu.edit.emojiSymbols",
3593 "start": { 4018 "start": {
3594 "column": 16, 4019 "column": 16,
3595 "line": 62 4020 "line": 65
3596 } 4021 }
3597 }, 4022 },
3598 { 4023 {
3599 "defaultMessage": "!!!Actual Size", 4024 "defaultMessage": "!!!Actual Size",
3600 "end": { 4025 "end": {
3601 "column": 3, 4026 "column": 3,
3602 "line": 69 4027 "line": 72
3603 }, 4028 },
3604 "file": "src/lib/Menu.js", 4029 "file": "src/lib/Menu.js",
3605 "id": "menu.view.resetZoom", 4030 "id": "menu.view.resetZoom",
3606 "start": { 4031 "start": {
3607 "column": 13, 4032 "column": 13,
3608 "line": 66 4033 "line": 69
3609 } 4034 }
3610 }, 4035 },
3611 { 4036 {
3612 "defaultMessage": "!!!Zoom In", 4037 "defaultMessage": "!!!Zoom In",
3613 "end": { 4038 "end": {
3614 "column": 3, 4039 "column": 3,
3615 "line": 73 4040 "line": 76
3616 }, 4041 },
3617 "file": "src/lib/Menu.js", 4042 "file": "src/lib/Menu.js",
3618 "id": "menu.view.zoomIn", 4043 "id": "menu.view.zoomIn",
3619 "start": { 4044 "start": {
3620 "column": 10, 4045 "column": 10,
3621 "line": 70 4046 "line": 73
3622 } 4047 }
3623 }, 4048 },
3624 { 4049 {
3625 "defaultMessage": "!!!Zoom Out", 4050 "defaultMessage": "!!!Zoom Out",
3626 "end": { 4051 "end": {
3627 "column": 3, 4052 "column": 3,
3628 "line": 77 4053 "line": 80
3629 }, 4054 },
3630 "file": "src/lib/Menu.js", 4055 "file": "src/lib/Menu.js",
3631 "id": "menu.view.zoomOut", 4056 "id": "menu.view.zoomOut",
3632 "start": { 4057 "start": {
3633 "column": 11, 4058 "column": 11,
3634 "line": 74 4059 "line": 77
3635 } 4060 }
3636 }, 4061 },
3637 { 4062 {
3638 "defaultMessage": "!!!Enter Full Screen", 4063 "defaultMessage": "!!!Enter Full Screen",
3639 "end": { 4064 "end": {
3640 "column": 3, 4065 "column": 3,
3641 "line": 81 4066 "line": 84
3642 }, 4067 },
3643 "file": "src/lib/Menu.js", 4068 "file": "src/lib/Menu.js",
3644 "id": "menu.view.enterFullScreen", 4069 "id": "menu.view.enterFullScreen",
3645 "start": { 4070 "start": {
3646 "column": 19, 4071 "column": 19,
3647 "line": 78 4072 "line": 81
3648 } 4073 }
3649 }, 4074 },
3650 { 4075 {
3651 "defaultMessage": "!!!Exit Full Screen", 4076 "defaultMessage": "!!!Exit Full Screen",
3652 "end": { 4077 "end": {
3653 "column": 3, 4078 "column": 3,
3654 "line": 85 4079 "line": 88
3655 }, 4080 },
3656 "file": "src/lib/Menu.js", 4081 "file": "src/lib/Menu.js",
3657 "id": "menu.view.exitFullScreen", 4082 "id": "menu.view.exitFullScreen",
3658 "start": { 4083 "start": {
3659 "column": 18, 4084 "column": 18,
3660 "line": 82 4085 "line": 85
3661 } 4086 }
3662 }, 4087 },
3663 { 4088 {
3664 "defaultMessage": "!!!Toggle Full Screen", 4089 "defaultMessage": "!!!Toggle Full Screen",
3665 "end": { 4090 "end": {
3666 "column": 3, 4091 "column": 3,
3667 "line": 89 4092 "line": 92
3668 }, 4093 },
3669 "file": "src/lib/Menu.js", 4094 "file": "src/lib/Menu.js",
3670 "id": "menu.view.toggleFullScreen", 4095 "id": "menu.view.toggleFullScreen",
3671 "start": { 4096 "start": {
3672 "column": 20, 4097 "column": 20,
3673 "line": 86 4098 "line": 89
3674 } 4099 }
3675 }, 4100 },
3676 { 4101 {
3677 "defaultMessage": "!!!Toggle Developer Tools", 4102 "defaultMessage": "!!!Toggle Developer Tools",
3678 "end": { 4103 "end": {
3679 "column": 3, 4104 "column": 3,
3680 "line": 93 4105 "line": 96
3681 }, 4106 },
3682 "file": "src/lib/Menu.js", 4107 "file": "src/lib/Menu.js",
3683 "id": "menu.view.toggleDevTools", 4108 "id": "menu.view.toggleDevTools",
3684 "start": { 4109 "start": {
3685 "column": 18, 4110 "column": 18,
3686 "line": 90 4111 "line": 93
3687 } 4112 }
3688 }, 4113 },
3689 { 4114 {
3690 "defaultMessage": "!!!Toggle Service Developer Tools", 4115 "defaultMessage": "!!!Toggle Service Developer Tools",
3691 "end": { 4116 "end": {
3692 "column": 3, 4117 "column": 3,
3693 "line": 97 4118 "line": 100
3694 }, 4119 },
3695 "file": "src/lib/Menu.js", 4120 "file": "src/lib/Menu.js",
3696 "id": "menu.view.toggleServiceDevTools", 4121 "id": "menu.view.toggleServiceDevTools",
3697 "start": { 4122 "start": {
3698 "column": 25, 4123 "column": 25,
3699 "line": 94 4124 "line": 97
3700 } 4125 }
3701 }, 4126 },
3702 { 4127 {
3703 "defaultMessage": "!!!Reload Service", 4128 "defaultMessage": "!!!Reload Service",
3704 "end": { 4129 "end": {
3705 "column": 3, 4130 "column": 3,
3706 "line": 101 4131 "line": 104
3707 }, 4132 },
3708 "file": "src/lib/Menu.js", 4133 "file": "src/lib/Menu.js",
3709 "id": "menu.view.reloadService", 4134 "id": "menu.view.reloadService",
3710 "start": { 4135 "start": {
3711 "column": 17, 4136 "column": 17,
3712 "line": 98 4137 "line": 101
3713 } 4138 }
3714 }, 4139 },
3715 { 4140 {
3716 "defaultMessage": "!!!Reload Franz", 4141 "defaultMessage": "!!!Reload Franz",
3717 "end": { 4142 "end": {
3718 "column": 3, 4143 "column": 3,
3719 "line": 105 4144 "line": 108
3720 }, 4145 },
3721 "file": "src/lib/Menu.js", 4146 "file": "src/lib/Menu.js",
3722 "id": "menu.view.reloadFranz", 4147 "id": "menu.view.reloadFranz",
3723 "start": { 4148 "start": {
3724 "column": 15, 4149 "column": 15,
3725 "line": 102 4150 "line": 105
3726 } 4151 }
3727 }, 4152 },
3728 { 4153 {
3729 "defaultMessage": "!!!Minimize", 4154 "defaultMessage": "!!!Minimize",
3730 "end": { 4155 "end": {
3731 "column": 3, 4156 "column": 3,
3732 "line": 109 4157 "line": 112
3733 }, 4158 },
3734 "file": "src/lib/Menu.js", 4159 "file": "src/lib/Menu.js",
3735 "id": "menu.window.minimize", 4160 "id": "menu.window.minimize",
3736 "start": { 4161 "start": {
3737 "column": 12, 4162 "column": 12,
3738 "line": 106 4163 "line": 109
3739 } 4164 }
3740 }, 4165 },
3741 { 4166 {
3742 "defaultMessage": "!!!Close", 4167 "defaultMessage": "!!!Close",
3743 "end": { 4168 "end": {
3744 "column": 3, 4169 "column": 3,
3745 "line": 113 4170 "line": 116
3746 }, 4171 },
3747 "file": "src/lib/Menu.js", 4172 "file": "src/lib/Menu.js",
3748 "id": "menu.window.close", 4173 "id": "menu.window.close",
3749 "start": { 4174 "start": {
3750 "column": 9, 4175 "column": 9,
3751 "line": 110 4176 "line": 113
3752 } 4177 }
3753 }, 4178 },
3754 { 4179 {
3755 "defaultMessage": "!!!Learn More", 4180 "defaultMessage": "!!!Learn More",
3756 "end": { 4181 "end": {
3757 "column": 3, 4182 "column": 3,
3758 "line": 117 4183 "line": 120
3759 }, 4184 },
3760 "file": "src/lib/Menu.js", 4185 "file": "src/lib/Menu.js",
3761 "id": "menu.help.learnMore", 4186 "id": "menu.help.learnMore",
3762 "start": { 4187 "start": {
3763 "column": 13, 4188 "column": 13,
3764 "line": 114 4189 "line": 117
3765 } 4190 }
3766 }, 4191 },
3767 { 4192 {
3768 "defaultMessage": "!!!Changelog", 4193 "defaultMessage": "!!!Changelog",
3769 "end": { 4194 "end": {
3770 "column": 3, 4195 "column": 3,
3771 "line": 121 4196 "line": 124
3772 }, 4197 },
3773 "file": "src/lib/Menu.js", 4198 "file": "src/lib/Menu.js",
3774 "id": "menu.help.changelog", 4199 "id": "menu.help.changelog",
3775 "start": { 4200 "start": {
3776 "column": 13, 4201 "column": 13,
3777 "line": 118 4202 "line": 121
3778 } 4203 }
3779 }, 4204 },
3780 { 4205 {
3781 "defaultMessage": "!!!Support", 4206 "defaultMessage": "!!!Support",
3782 "end": { 4207 "end": {
3783 "column": 3, 4208 "column": 3,
3784 "line": 125 4209 "line": 128
3785 }, 4210 },
3786 "file": "src/lib/Menu.js", 4211 "file": "src/lib/Menu.js",
3787 "id": "menu.help.support", 4212 "id": "menu.help.support",
3788 "start": { 4213 "start": {
3789 "column": 11, 4214 "column": 11,
3790 "line": 122 4215 "line": 125
3791 } 4216 }
3792 }, 4217 },
3793 { 4218 {
3794 "defaultMessage": "!!!Terms of Service", 4219 "defaultMessage": "!!!Terms of Service",
3795 "end": { 4220 "end": {
3796 "column": 3, 4221 "column": 3,
3797 "line": 129 4222 "line": 132
3798 }, 4223 },
3799 "file": "src/lib/Menu.js", 4224 "file": "src/lib/Menu.js",
3800 "id": "menu.help.tos", 4225 "id": "menu.help.tos",
3801 "start": { 4226 "start": {
3802 "column": 7, 4227 "column": 7,
3803 "line": 126 4228 "line": 129
3804 } 4229 }
3805 }, 4230 },
3806 { 4231 {
3807 "defaultMessage": "!!!Privacy Statement", 4232 "defaultMessage": "!!!Privacy Statement",
3808 "end": { 4233 "end": {
3809 "column": 3, 4234 "column": 3,
3810 "line": 133 4235 "line": 136
3811 }, 4236 },
3812 "file": "src/lib/Menu.js", 4237 "file": "src/lib/Menu.js",
3813 "id": "menu.help.privacy", 4238 "id": "menu.help.privacy",
3814 "start": { 4239 "start": {
3815 "column": 11, 4240 "column": 11,
3816 "line": 130 4241 "line": 133
3817 } 4242 }
3818 }, 4243 },
3819 { 4244 {
3820 "defaultMessage": "!!!File", 4245 "defaultMessage": "!!!File",
3821 "end": { 4246 "end": {
3822 "column": 3, 4247 "column": 3,
3823 "line": 137 4248 "line": 140
3824 }, 4249 },
3825 "file": "src/lib/Menu.js", 4250 "file": "src/lib/Menu.js",
3826 "id": "menu.file", 4251 "id": "menu.file",
3827 "start": { 4252 "start": {
3828 "column": 8, 4253 "column": 8,
3829 "line": 134 4254 "line": 137
3830 } 4255 }
3831 }, 4256 },
3832 { 4257 {
3833 "defaultMessage": "!!!View", 4258 "defaultMessage": "!!!View",
3834 "end": { 4259 "end": {
3835 "column": 3, 4260 "column": 3,
3836 "line": 141 4261 "line": 144
3837 }, 4262 },
3838 "file": "src/lib/Menu.js", 4263 "file": "src/lib/Menu.js",
3839 "id": "menu.view", 4264 "id": "menu.view",
3840 "start": { 4265 "start": {
3841 "column": 8, 4266 "column": 8,
3842 "line": 138 4267 "line": 141
3843 } 4268 }
3844 }, 4269 },
3845 { 4270 {
3846 "defaultMessage": "!!!Services", 4271 "defaultMessage": "!!!Services",
3847 "end": { 4272 "end": {
3848 "column": 3, 4273 "column": 3,
3849 "line": 145 4274 "line": 148
3850 }, 4275 },
3851 "file": "src/lib/Menu.js", 4276 "file": "src/lib/Menu.js",
3852 "id": "menu.services", 4277 "id": "menu.services",
3853 "start": { 4278 "start": {
3854 "column": 12, 4279 "column": 12,
3855 "line": 142 4280 "line": 145
3856 } 4281 }
3857 }, 4282 },
3858 { 4283 {
3859 "defaultMessage": "!!!Window", 4284 "defaultMessage": "!!!Window",
3860 "end": { 4285 "end": {
3861 "column": 3, 4286 "column": 3,
3862 "line": 149 4287 "line": 152
3863 }, 4288 },
3864 "file": "src/lib/Menu.js", 4289 "file": "src/lib/Menu.js",
3865 "id": "menu.window", 4290 "id": "menu.window",
3866 "start": { 4291 "start": {
3867 "column": 10, 4292 "column": 10,
3868 "line": 146 4293 "line": 149
3869 } 4294 }
3870 }, 4295 },
3871 { 4296 {
3872 "defaultMessage": "!!!Help", 4297 "defaultMessage": "!!!Help",
3873 "end": { 4298 "end": {
3874 "column": 3, 4299 "column": 3,
3875 "line": 153 4300 "line": 156
3876 }, 4301 },
3877 "file": "src/lib/Menu.js", 4302 "file": "src/lib/Menu.js",
3878 "id": "menu.help", 4303 "id": "menu.help",
3879 "start": { 4304 "start": {
3880 "column": 8, 4305 "column": 8,
3881 "line": 150 4306 "line": 153
3882 } 4307 }
3883 }, 4308 },
3884 { 4309 {
3885 "defaultMessage": "!!!About Franz", 4310 "defaultMessage": "!!!About Franz",
3886 "end": { 4311 "end": {
3887 "column": 3, 4312 "column": 3,
3888 "line": 157 4313 "line": 160
3889 }, 4314 },
3890 "file": "src/lib/Menu.js", 4315 "file": "src/lib/Menu.js",
3891 "id": "menu.app.about", 4316 "id": "menu.app.about",
3892 "start": { 4317 "start": {
3893 "column": 9, 4318 "column": 9,
3894 "line": 154 4319 "line": 157
3895 } 4320 }
3896 }, 4321 },
3897 { 4322 {
3898 "defaultMessage": "!!!Settings", 4323 "defaultMessage": "!!!Settings",
3899 "end": { 4324 "end": {
3900 "column": 3, 4325 "column": 3,
3901 "line": 161 4326 "line": 164
3902 }, 4327 },
3903 "file": "src/lib/Menu.js", 4328 "file": "src/lib/Menu.js",
3904 "id": "menu.app.settings", 4329 "id": "menu.app.settings",
3905 "start": { 4330 "start": {
3906 "column": 12, 4331 "column": 12,
3907 "line": 158 4332 "line": 161
3908 } 4333 }
3909 }, 4334 },
3910 { 4335 {
3911 "defaultMessage": "!!!Hide", 4336 "defaultMessage": "!!!Hide",
3912 "end": { 4337 "end": {
3913 "column": 3, 4338 "column": 3,
3914 "line": 165 4339 "line": 168
3915 }, 4340 },
3916 "file": "src/lib/Menu.js", 4341 "file": "src/lib/Menu.js",
3917 "id": "menu.app.hide", 4342 "id": "menu.app.hide",
3918 "start": { 4343 "start": {
3919 "column": 8, 4344 "column": 8,
3920 "line": 162 4345 "line": 165
3921 } 4346 }
3922 }, 4347 },
3923 { 4348 {
3924 "defaultMessage": "!!!Hide Others", 4349 "defaultMessage": "!!!Hide Others",
3925 "end": { 4350 "end": {
3926 "column": 3, 4351 "column": 3,
3927 "line": 169 4352 "line": 172
3928 }, 4353 },
3929 "file": "src/lib/Menu.js", 4354 "file": "src/lib/Menu.js",
3930 "id": "menu.app.hideOthers", 4355 "id": "menu.app.hideOthers",
3931 "start": { 4356 "start": {
3932 "column": 14, 4357 "column": 14,
3933 "line": 166 4358 "line": 169
3934 } 4359 }
3935 }, 4360 },
3936 { 4361 {
3937 "defaultMessage": "!!!Unhide", 4362 "defaultMessage": "!!!Unhide",
3938 "end": { 4363 "end": {
3939 "column": 3, 4364 "column": 3,
3940 "line": 173 4365 "line": 176
3941 }, 4366 },
3942 "file": "src/lib/Menu.js", 4367 "file": "src/lib/Menu.js",
3943 "id": "menu.app.unhide", 4368 "id": "menu.app.unhide",
3944 "start": { 4369 "start": {
3945 "column": 10, 4370 "column": 10,
3946 "line": 170 4371 "line": 173
3947 } 4372 }
3948 }, 4373 },
3949 { 4374 {
3950 "defaultMessage": "!!!Quit", 4375 "defaultMessage": "!!!Quit",
3951 "end": { 4376 "end": {
3952 "column": 3, 4377 "column": 3,
3953 "line": 177 4378 "line": 180
3954 }, 4379 },
3955 "file": "src/lib/Menu.js", 4380 "file": "src/lib/Menu.js",
3956 "id": "menu.app.quit", 4381 "id": "menu.app.quit",
3957 "start": { 4382 "start": {
3958 "column": 8, 4383 "column": 8,
3959 "line": 174 4384 "line": 177
3960 } 4385 }
3961 }, 4386 },
3962 { 4387 {
3963 "defaultMessage": "!!!Add New Service...", 4388 "defaultMessage": "!!!Add New Service...",
3964 "end": { 4389 "end": {
3965 "column": 3, 4390 "column": 3,
3966 "line": 181 4391 "line": 184
3967 }, 4392 },
3968 "file": "src/lib/Menu.js", 4393 "file": "src/lib/Menu.js",
3969 "id": "menu.services.addNewService", 4394 "id": "menu.services.addNewService",
3970 "start": { 4395 "start": {
3971 "column": 17, 4396 "column": 17,
3972 "line": 178 4397 "line": 181
3973 } 4398 }
3974 }, 4399 },
3975 { 4400 {
3976 "defaultMessage": "!!!Activate next service...", 4401 "defaultMessage": "!!!Add New Workspace...",
3977 "end": { 4402 "end": {
3978 "column": 3, 4403 "column": 3,
4404 "line": 188
4405 },
4406 "file": "src/lib/Menu.js",
4407 "id": "menu.workspaces.addNewWorkspace",
4408 "start": {
4409 "column": 19,
3979 "line": 185 4410 "line": 185
4411 }
4412 },
4413 {
4414 "defaultMessage": "!!!Open workspace drawer",
4415 "end": {
4416 "column": 3,
4417 "line": 192
4418 },
4419 "file": "src/lib/Menu.js",
4420 "id": "menu.workspaces.openWorkspaceDrawer",
4421 "start": {
4422 "column": 23,
4423 "line": 189
4424 }
4425 },
4426 {
4427 "defaultMessage": "!!!Close workspace drawer",
4428 "end": {
4429 "column": 3,
4430 "line": 196
4431 },
4432 "file": "src/lib/Menu.js",
4433 "id": "menu.workspaces.closeWorkspaceDrawer",
4434 "start": {
4435 "column": 24,
4436 "line": 193
4437 }
4438 },
4439 {
4440 "defaultMessage": "!!!Activate next service...",
4441 "end": {
4442 "column": 3,
4443 "line": 200
3980 }, 4444 },
3981 "file": "src/lib/Menu.js", 4445 "file": "src/lib/Menu.js",
3982 "id": "menu.services.setNextServiceActive", 4446 "id": "menu.services.setNextServiceActive",
3983 "start": { 4447 "start": {
3984 "column": 23, 4448 "column": 23,
3985 "line": 182 4449 "line": 197
3986 } 4450 }
3987 }, 4451 },
3988 { 4452 {
3989 "defaultMessage": "!!!Activate previous service...", 4453 "defaultMessage": "!!!Activate previous service...",
3990 "end": { 4454 "end": {
3991 "column": 3, 4455 "column": 3,
3992 "line": 189 4456 "line": 204
3993 }, 4457 },
3994 "file": "src/lib/Menu.js", 4458 "file": "src/lib/Menu.js",
3995 "id": "menu.services.activatePreviousService", 4459 "id": "menu.services.activatePreviousService",
3996 "start": { 4460 "start": {
3997 "column": 27, 4461 "column": 27,
3998 "line": 186 4462 "line": 201
3999 } 4463 }
4000 }, 4464 },
4001 { 4465 {
4002 "defaultMessage": "!!!Disable notifications & audio", 4466 "defaultMessage": "!!!Disable notifications & audio",
4003 "end": { 4467 "end": {
4004 "column": 3, 4468 "column": 3,
4005 "line": 193 4469 "line": 208
4006 }, 4470 },
4007 "file": "src/lib/Menu.js", 4471 "file": "src/lib/Menu.js",
4008 "id": "sidebar.muteApp", 4472 "id": "sidebar.muteApp",
4009 "start": { 4473 "start": {
4010 "column": 11, 4474 "column": 11,
4011 "line": 190 4475 "line": 205
4012 } 4476 }
4013 }, 4477 },
4014 { 4478 {
4015 "defaultMessage": "!!!Enable notifications & audio", 4479 "defaultMessage": "!!!Enable notifications & audio",
4016 "end": { 4480 "end": {
4017 "column": 3, 4481 "column": 3,
4018 "line": 197 4482 "line": 212
4019 }, 4483 },
4020 "file": "src/lib/Menu.js", 4484 "file": "src/lib/Menu.js",
4021 "id": "sidebar.unmuteApp", 4485 "id": "sidebar.unmuteApp",
4022 "start": { 4486 "start": {
4023 "column": 13, 4487 "column": 13,
4024 "line": 194 4488 "line": 209
4489 }
4490 },
4491 {
4492 "defaultMessage": "!!!Workspaces",
4493 "end": {
4494 "column": 3,
4495 "line": 216
4496 },
4497 "file": "src/lib/Menu.js",
4498 "id": "menu.workspaces",
4499 "start": {
4500 "column": 14,
4501 "line": 213
4502 }
4503 },
4504 {
4505 "defaultMessage": "!!!Default",
4506 "end": {
4507 "column": 3,
4508 "line": 220
4509 },
4510 "file": "src/lib/Menu.js",
4511 "id": "menu.workspaces.defaultWorkspace",
4512 "start": {
4513 "column": 20,
4514 "line": 217
4025 } 4515 }
4026 } 4516 }
4027 ], 4517 ],
4028 "path": "src/lib/Menu.json" 4518 "path": "src/lib/Menu.json"
4029 } 4519 }
4030] \ No newline at end of file 4520]
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 61c25ff98..bcdfb8220 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -87,6 +87,11 @@
87 "menu.window": "Window", 87 "menu.window": "Window",
88 "menu.window.close": "Close", 88 "menu.window.close": "Close",
89 "menu.window.minimize": "Minimize", 89 "menu.window.minimize": "Minimize",
90 "menu.workspaces": "Workspaces",
91 "menu.workspaces.addNewWorkspace": "Add New Workspace...",
92 "menu.workspaces.closeWorkspaceDrawer": "Close workspace drawer",
93 "menu.workspaces.defaultWorkspace": "All services",
94 "menu.workspaces.openWorkspaceDrawer": "Open workspace drawer",
90 "password.email.label": "Email address", 95 "password.email.label": "Email address",
91 "password.headline": "Reset password", 96 "password.headline": "Reset password",
92 "password.link.login": "Sign in to your account", 97 "password.link.login": "Sign in to your account",
@@ -110,6 +115,7 @@
110 "service.errorHandler.headline": "Oh no!", 115 "service.errorHandler.headline": "Oh no!",
111 "service.errorHandler.message": "Error", 116 "service.errorHandler.message": "Error",
112 "service.errorHandler.text": "{name} has failed to load.", 117 "service.errorHandler.text": "{name} has failed to load.",
118 "service.webviewLoader.loading": "Loading",
113 "services.getStarted": "Get started", 119 "services.getStarted": "Get started",
114 "services.welcome": "Welcome to Franz", 120 "services.welcome": "Welcome to Franz",
115 "settings.account.account.editButton": "Edit account", 121 "settings.account.account.editButton": "Edit account",
@@ -170,6 +176,7 @@
170 "settings.navigation.settings": "Settings", 176 "settings.navigation.settings": "Settings",
171 "settings.navigation.team": "Manage Team", 177 "settings.navigation.team": "Manage Team",
172 "settings.navigation.yourServices": "Your services", 178 "settings.navigation.yourServices": "Your services",
179 "settings.navigation.yourWorkspaces": "Your workspaces",
173 "settings.recipes.all": "All services", 180 "settings.recipes.all": "All services",
174 "settings.recipes.dev": "Development", 181 "settings.recipes.dev": "Development",
175 "settings.recipes.headline": "Available services", 182 "settings.recipes.headline": "Available services",
@@ -231,7 +238,7 @@
231 "settings.team.copy": "Franz for Teams gives you the option to invite co-workers to your team by sending them email invitations and manage their subscriptions in your account’s preferences. Don’t waste time setting up subscriptions for every team member individually, forget about multiple invoices and different billing cycles - one team to rule them all!", 238 "settings.team.copy": "Franz for Teams gives you the option to invite co-workers to your team by sending them email invitations and manage their subscriptions in your account’s preferences. Don’t waste time setting up subscriptions for every team member individually, forget about multiple invoices and different billing cycles - one team to rule them all!",
232 "settings.team.headline": "Team", 239 "settings.team.headline": "Team",
233 "settings.team.intro": "You and your team use Franz? You can now manage Premium subscriptions for as many colleagues, friends or family members as you want, all from within one account.", 240 "settings.team.intro": "You and your team use Franz? You can now manage Premium subscriptions for as many colleagues, friends or family members as you want, all from within one account.",
234 "settings.team.manageAction": "Manage your Team", 241 "settings.team.manageAction": "Manage your Team on meetfranz.com",
235 "settings.team.upgradeAction": "Upgrade your Account", 242 "settings.team.upgradeAction": "Upgrade your Account",
236 "settings.user.form.accountType.company": "Company", 243 "settings.user.form.accountType.company": "Company",
237 "settings.user.form.accountType.individual": "Individual", 244 "settings.user.form.accountType.individual": "Individual",
@@ -242,8 +249,25 @@
242 "settings.user.form.firstname": "First Name", 249 "settings.user.form.firstname": "First Name",
243 "settings.user.form.lastname": "Last Name", 250 "settings.user.form.lastname": "Last Name",
244 "settings.user.form.newPassword": "New password", 251 "settings.user.form.newPassword": "New password",
252 "settings.workspace.add.form.name": "Name",
253 "settings.workspace.add.form.submitButton": "Create workspace",
254 "settings.workspace.form.buttonDelete": "Delete workspace",
255 "settings.workspace.form.buttonSave": "Save workspace",
256 "settings.workspace.form.name": "Name",
257 "settings.workspace.form.servicesInWorkspaceHeadline": "Services in this Workspace",
258 "settings.workspace.form.yourWorkspaces": "Your workspaces",
259 "settings.workspaces.deletedInfo": "Workspace has been deleted",
260 "settings.workspaces.headline": "Your workspaces",
261 "settings.workspaces.noWorkspacesAdded": "You haven't added any workspaces yet.",
262 "settings.workspaces.tryReloadWorkspaces": "Try again",
263 "settings.workspaces.updatedInfo": "Your changes have been saved",
264 "settings.workspaces.workspaceFeatureHeadline": "Less is More: Introducing Franz Workspaces",
265 "settings.workspaces.workspaceFeatureInfo": "Franz Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.",
266 "settings.workspaces.workspacesRequestFailed": "Could not load your workspaces",
245 "sidebar.addNewService": "Add new service", 267 "sidebar.addNewService": "Add new service",
268 "sidebar.closeWorkspaceDrawer": "Close workspace drawer",
246 "sidebar.muteApp": "Disable notifications & audio", 269 "sidebar.muteApp": "Disable notifications & audio",
270 "sidebar.openWorkspaceDrawer": "Open workspace drawer",
247 "sidebar.settings": "Settings", 271 "sidebar.settings": "Settings",
248 "sidebar.unmuteApp": "Enable notifications & audio", 272 "sidebar.unmuteApp": "Enable notifications & audio",
249 "signup.company.label": "Company", 273 "signup.company.label": "Company",
@@ -288,5 +312,16 @@
288 "validation.required": "{field} is required", 312 "validation.required": "{field} is required",
289 "validation.url": "{field} is not a valid URL", 313 "validation.url": "{field} is not a valid URL",
290 "welcome.loginButton": "Login to your account", 314 "welcome.loginButton": "Login to your account",
291 "welcome.signupButton": "Create a free account" 315 "welcome.signupButton": "Create a free account",
292} \ No newline at end of file 316 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace",
317 "workspaceDrawer.allServices": "All services",
318 "workspaceDrawer.headline": "Workspaces",
319 "workspaceDrawer.item.contextMenuEdit": "edit",
320 "workspaceDrawer.item.noServicesAddedYet": "No services added yet",
321 "workspaceDrawer.premiumCtaButtonLabel": "Create your first workspace",
322 "workspaceDrawer.proFeatureBadge": "Premium feature",
323 "workspaceDrawer.reactivatePremiumAccountLabel": "Reactivate premium account",
324 "workspaceDrawer.workspaceFeatureInfo": "<p>Franz Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.</p><p>You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.</p>",
325 "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings",
326 "workspaces.switchingIndicator.switchingTo": "Switching to"
327}
diff --git a/src/i18n/messages/src/components/layout/AppLayout.json b/src/i18n/messages/src/components/layout/AppLayout.json
index 07603d062..92593ed5c 100644
--- a/src/i18n/messages/src/components/layout/AppLayout.json
+++ b/src/i18n/messages/src/components/layout/AppLayout.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Your services have been updated.", 4 "defaultMessage": "!!!Your services have been updated.",
5 "file": "src/components/layout/AppLayout.js", 5 "file": "src/components/layout/AppLayout.js",
6 "start": { 6 "start": {
7 "line": 22, 7 "line": 25,
8 "column": 19 8 "column": 19
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 25, 11 "line": 28,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!A new update for Franz is available.", 17 "defaultMessage": "!!!A new update for Franz is available.",
18 "file": "src/components/layout/AppLayout.js", 18 "file": "src/components/layout/AppLayout.js",
19 "start": { 19 "start": {
20 "line": 26, 20 "line": 29,
21 "column": 19 21 "column": 19
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 29, 24 "line": 32,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Reload services", 30 "defaultMessage": "!!!Reload services",
31 "file": "src/components/layout/AppLayout.js", 31 "file": "src/components/layout/AppLayout.js",
32 "start": { 32 "start": {
33 "line": 30, 33 "line": 33,
34 "column": 24 34 "column": 24
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 33, 37 "line": 36,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,11 @@
43 "defaultMessage": "!!!Changelog", 43 "defaultMessage": "!!!Changelog",
44 "file": "src/components/layout/AppLayout.js", 44 "file": "src/components/layout/AppLayout.js",
45 "start": { 45 "start": {
46 "line": 34, 46 "line": 37,
47 "column": 13 47 "column": 13
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 37, 50 "line": 40,
51 "column": 3 51 "column": 3
52 } 52 }
53 }, 53 },
@@ -56,11 +56,11 @@
56 "defaultMessage": "!!!Restart & install update", 56 "defaultMessage": "!!!Restart & install update",
57 "file": "src/components/layout/AppLayout.js", 57 "file": "src/components/layout/AppLayout.js",
58 "start": { 58 "start": {
59 "line": 38, 59 "line": 41,
60 "column": 23 60 "column": 23
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 41, 63 "line": 44,
64 "column": 3 64 "column": 3
65 } 65 }
66 }, 66 },
@@ -69,11 +69,11 @@
69 "defaultMessage": "!!!Could not load services and user information", 69 "defaultMessage": "!!!Could not load services and user information",
70 "file": "src/components/layout/AppLayout.js", 70 "file": "src/components/layout/AppLayout.js",
71 "start": { 71 "start": {
72 "line": 42, 72 "line": 45,
73 "column": 26 73 "column": 26
74 }, 74 },
75 "end": { 75 "end": {
76 "line": 45, 76 "line": 48,
77 "column": 3 77 "column": 3
78 } 78 }
79 } 79 }
diff --git a/src/i18n/messages/src/components/layout/Sidebar.json b/src/i18n/messages/src/components/layout/Sidebar.json
index 7aa00a186..d67adc96e 100644
--- a/src/i18n/messages/src/components/layout/Sidebar.json
+++ b/src/i18n/messages/src/components/layout/Sidebar.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Settings", 4 "defaultMessage": "!!!Settings",
5 "file": "src/components/layout/Sidebar.js", 5 "file": "src/components/layout/Sidebar.js",
6 "start": { 6 "start": {
7 "line": 11, 7 "line": 13,
8 "column": 12 8 "column": 12
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 14, 11 "line": 16,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!Add new service", 17 "defaultMessage": "!!!Add new service",
18 "file": "src/components/layout/Sidebar.js", 18 "file": "src/components/layout/Sidebar.js",
19 "start": { 19 "start": {
20 "line": 15, 20 "line": 17,
21 "column": 17 21 "column": 17
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 18, 24 "line": 20,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Disable notifications & audio", 30 "defaultMessage": "!!!Disable notifications & audio",
31 "file": "src/components/layout/Sidebar.js", 31 "file": "src/components/layout/Sidebar.js",
32 "start": { 32 "start": {
33 "line": 19, 33 "line": 21,
34 "column": 8 34 "column": 8
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 22, 37 "line": 24,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,37 @@
43 "defaultMessage": "!!!Enable notifications & audio", 43 "defaultMessage": "!!!Enable notifications & audio",
44 "file": "src/components/layout/Sidebar.js", 44 "file": "src/components/layout/Sidebar.js",
45 "start": { 45 "start": {
46 "line": 23, 46 "line": 25,
47 "column": 10 47 "column": 10
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 26, 50 "line": 28,
51 "column": 3
52 }
53 },
54 {
55 "id": "sidebar.openWorkspaceDrawer",
56 "defaultMessage": "!!!Open workspace drawer",
57 "file": "src/components/layout/Sidebar.js",
58 "start": {
59 "line": 29,
60 "column": 23
61 },
62 "end": {
63 "line": 32,
64 "column": 3
65 }
66 },
67 {
68 "id": "sidebar.closeWorkspaceDrawer",
69 "defaultMessage": "!!!Close workspace drawer",
70 "file": "src/components/layout/Sidebar.js",
71 "start": {
72 "line": 33,
73 "column": 24
74 },
75 "end": {
76 "line": 36,
51 "column": 3 77 "column": 3
52 } 78 }
53 } 79 }
diff --git a/src/i18n/messages/src/components/settings/navigation/SettingsNavigation.json b/src/i18n/messages/src/components/settings/navigation/SettingsNavigation.json
index 5b854641a..70a989211 100644
--- a/src/i18n/messages/src/components/settings/navigation/SettingsNavigation.json
+++ b/src/i18n/messages/src/components/settings/navigation/SettingsNavigation.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Available services", 4 "defaultMessage": "!!!Available services",
5 "file": "src/components/settings/navigation/SettingsNavigation.js", 5 "file": "src/components/settings/navigation/SettingsNavigation.js",
6 "start": { 6 "start": {
7 "line": 9, 7 "line": 13,
8 "column": 21 8 "column": 21
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 12, 11 "line": 16,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,24 @@
17 "defaultMessage": "!!!Your services", 17 "defaultMessage": "!!!Your services",
18 "file": "src/components/settings/navigation/SettingsNavigation.js", 18 "file": "src/components/settings/navigation/SettingsNavigation.js",
19 "start": { 19 "start": {
20 "line": 13, 20 "line": 17,
21 "column": 16 21 "column": 16
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 16, 24 "line": 20,
25 "column": 3
26 }
27 },
28 {
29 "id": "settings.navigation.yourWorkspaces",
30 "defaultMessage": "!!!Your workspaces",
31 "file": "src/components/settings/navigation/SettingsNavigation.js",
32 "start": {
33 "line": 21,
34 "column": 18
35 },
36 "end": {
37 "line": 24,
25 "column": 3 38 "column": 3
26 } 39 }
27 }, 40 },
@@ -30,11 +43,11 @@
30 "defaultMessage": "!!!Account", 43 "defaultMessage": "!!!Account",
31 "file": "src/components/settings/navigation/SettingsNavigation.js", 44 "file": "src/components/settings/navigation/SettingsNavigation.js",
32 "start": { 45 "start": {
33 "line": 17, 46 "line": 25,
34 "column": 11 47 "column": 11
35 }, 48 },
36 "end": { 49 "end": {
37 "line": 20, 50 "line": 28,
38 "column": 3 51 "column": 3
39 } 52 }
40 }, 53 },
@@ -43,11 +56,11 @@
43 "defaultMessage": "!!!Manage Team", 56 "defaultMessage": "!!!Manage Team",
44 "file": "src/components/settings/navigation/SettingsNavigation.js", 57 "file": "src/components/settings/navigation/SettingsNavigation.js",
45 "start": { 58 "start": {
46 "line": 21, 59 "line": 29,
47 "column": 8 60 "column": 8
48 }, 61 },
49 "end": { 62 "end": {
50 "line": 24, 63 "line": 32,
51 "column": 3 64 "column": 3
52 } 65 }
53 }, 66 },
@@ -56,11 +69,11 @@
56 "defaultMessage": "!!!Settings", 69 "defaultMessage": "!!!Settings",
57 "file": "src/components/settings/navigation/SettingsNavigation.js", 70 "file": "src/components/settings/navigation/SettingsNavigation.js",
58 "start": { 71 "start": {
59 "line": 25, 72 "line": 33,
60 "column": 12 73 "column": 12
61 }, 74 },
62 "end": { 75 "end": {
63 "line": 28, 76 "line": 36,
64 "column": 3 77 "column": 3
65 } 78 }
66 }, 79 },
@@ -69,11 +82,11 @@
69 "defaultMessage": "!!!Invite Friends", 82 "defaultMessage": "!!!Invite Friends",
70 "file": "src/components/settings/navigation/SettingsNavigation.js", 83 "file": "src/components/settings/navigation/SettingsNavigation.js",
71 "start": { 84 "start": {
72 "line": 29, 85 "line": 37,
73 "column": 17 86 "column": 17
74 }, 87 },
75 "end": { 88 "end": {
76 "line": 32, 89 "line": 40,
77 "column": 3 90 "column": 3
78 } 91 }
79 }, 92 },
@@ -82,11 +95,11 @@
82 "defaultMessage": "!!!Logout", 95 "defaultMessage": "!!!Logout",
83 "file": "src/components/settings/navigation/SettingsNavigation.js", 96 "file": "src/components/settings/navigation/SettingsNavigation.js",
84 "start": { 97 "start": {
85 "line": 33, 98 "line": 41,
86 "column": 10 99 "column": 10
87 }, 100 },
88 "end": { 101 "end": {
89 "line": 36, 102 "line": 44,
90 "column": 3 103 "column": 3
91 } 104 }
92 } 105 }
diff --git a/src/i18n/messages/src/components/ui/PremiumFeatureContainer/index.json b/src/i18n/messages/src/components/ui/PremiumFeatureContainer/index.json
index 582d546fa..320d3ca3e 100644
--- a/src/i18n/messages/src/components/ui/PremiumFeatureContainer/index.json
+++ b/src/i18n/messages/src/components/ui/PremiumFeatureContainer/index.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Upgrade account", 4 "defaultMessage": "!!!Upgrade account",
5 "file": "src/components/ui/PremiumFeatureContainer/index.js", 5 "file": "src/components/ui/PremiumFeatureContainer/index.js",
6 "start": { 6 "start": {
7 "line": 14, 7 "line": 15,
8 "column": 10 8 "column": 10
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 17, 11 "line": 18,
12 "column": 3 12 "column": 3
13 } 13 }
14 } 14 }
diff --git a/src/i18n/messages/src/components/ui/WebviewLoader/index.json b/src/i18n/messages/src/components/ui/WebviewLoader/index.json
new file mode 100644
index 000000000..ef3e4b593
--- /dev/null
+++ b/src/i18n/messages/src/components/ui/WebviewLoader/index.json
@@ -0,0 +1,15 @@
1[
2 {
3 "id": "service.webviewLoader.loading",
4 "defaultMessage": "!!!Loading",
5 "file": "src/components/ui/WebviewLoader/index.js",
6 "start": {
7 "line": 11,
8 "column": 11
9 },
10 "end": {
11 "line": 14,
12 "column": 3
13 }
14 }
15] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/CreateWorkspaceForm.json b/src/i18n/messages/src/features/workspaces/components/CreateWorkspaceForm.json
new file mode 100644
index 000000000..f62bac42c
--- /dev/null
+++ b/src/i18n/messages/src/features/workspaces/components/CreateWorkspaceForm.json
@@ -0,0 +1,28 @@
1[
2 {
3 "id": "settings.workspace.add.form.submitButton",
4 "defaultMessage": "!!!Create workspace",
5 "file": "src/features/workspaces/components/CreateWorkspaceForm.js",
6 "start": {
7 "line": 13,
8 "column": 16
9 },
10 "end": {
11 "line": 16,
12 "column": 3
13 }
14 },
15 {
16 "id": "settings.workspace.add.form.name",
17 "defaultMessage": "!!!Name",
18 "file": "src/features/workspaces/components/CreateWorkspaceForm.js",
19 "start": {
20 "line": 17,
21 "column": 8
22 },
23 "end": {
24 "line": 20,
25 "column": 3
26 }
27 }
28] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/EditWorkspaceForm.json b/src/i18n/messages/src/features/workspaces/components/EditWorkspaceForm.json
new file mode 100644
index 000000000..7b0c3e1ce
--- /dev/null
+++ b/src/i18n/messages/src/features/workspaces/components/EditWorkspaceForm.json
@@ -0,0 +1,67 @@
1[
2 {
3 "id": "settings.workspace.form.buttonDelete",
4 "defaultMessage": "!!!Delete workspace",
5 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
6 "start": {
7 "line": 19,
8 "column": 16
9 },
10 "end": {
11 "line": 22,
12 "column": 3
13 }
14 },
15 {
16 "id": "settings.workspace.form.buttonSave",
17 "defaultMessage": "!!!Save workspace",
18 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
19 "start": {
20 "line": 23,
21 "column": 14
22 },
23 "end": {
24 "line": 26,
25 "column": 3
26 }
27 },
28 {
29 "id": "settings.workspace.form.name",
30 "defaultMessage": "!!!Name",
31 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
32 "start": {
33 "line": 27,
34 "column": 8
35 },
36 "end": {
37 "line": 30,
38 "column": 3
39 }
40 },
41 {
42 "id": "settings.workspace.form.yourWorkspaces",
43 "defaultMessage": "!!!Your workspaces",
44 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
45 "start": {
46 "line": 31,
47 "column": 18
48 },
49 "end": {
50 "line": 34,
51 "column": 3
52 }
53 },
54 {
55 "id": "settings.workspace.form.servicesInWorkspaceHeadline",
56 "defaultMessage": "!!!Services in this Workspace",
57 "file": "src/features/workspaces/components/EditWorkspaceForm.js",
58 "start": {
59 "line": 35,
60 "column": 31
61 },
62 "end": {
63 "line": 38,
64 "column": 3
65 }
66 }
67] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawer.json b/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawer.json
new file mode 100644
index 000000000..9f0935620
--- /dev/null
+++ b/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawer.json
@@ -0,0 +1,106 @@
1[
2 {
3 "id": "workspaceDrawer.headline",
4 "defaultMessage": "!!!Workspaces",
5 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
6 "start": {
7 "line": 16,
8 "column": 12
9 },
10 "end": {
11 "line": 19,
12 "column": 3
13 }
14 },
15 {
16 "id": "workspaceDrawer.allServices",
17 "defaultMessage": "!!!All services",
18 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
19 "start": {
20 "line": 20,
21 "column": 15
22 },
23 "end": {
24 "line": 23,
25 "column": 3
26 }
27 },
28 {
29 "id": "workspaceDrawer.workspacesSettingsTooltip",
30 "defaultMessage": "!!!Workspaces settings",
31 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
32 "start": {
33 "line": 24,
34 "column": 29
35 },
36 "end": {
37 "line": 27,
38 "column": 3
39 }
40 },
41 {
42 "id": "workspaceDrawer.workspaceFeatureInfo",
43 "defaultMessage": "!!!Info about workspace feature",
44 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
45 "start": {
46 "line": 28,
47 "column": 24
48 },
49 "end": {
50 "line": 31,
51 "column": 3
52 }
53 },
54 {
55 "id": "workspaceDrawer.premiumCtaButtonLabel",
56 "defaultMessage": "!!!Create your first workspace",
57 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
58 "start": {
59 "line": 32,
60 "column": 25
61 },
62 "end": {
63 "line": 35,
64 "column": 3
65 }
66 },
67 {
68 "id": "workspaceDrawer.reactivatePremiumAccountLabel",
69 "defaultMessage": "!!!Reactivate premium account",
70 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
71 "start": {
72 "line": 36,
73 "column": 28
74 },
75 "end": {
76 "line": 39,
77 "column": 3
78 }
79 },
80 {
81 "id": "workspaceDrawer.addNewWorkspaceLabel",
82 "defaultMessage": "!!!add new workspace",
83 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
84 "start": {
85 "line": 40,
86 "column": 24
87 },
88 "end": {
89 "line": 43,
90 "column": 3
91 }
92 },
93 {
94 "id": "workspaceDrawer.proFeatureBadge",
95 "defaultMessage": "!!!Premium feature",
96 "file": "src/features/workspaces/components/WorkspaceDrawer.js",
97 "start": {
98 "line": 44,
99 "column": 23
100 },
101 "end": {
102 "line": 47,
103 "column": 3
104 }
105 }
106] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json b/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json
new file mode 100644
index 000000000..4ff190606
--- /dev/null
+++ b/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json
@@ -0,0 +1,28 @@
1[
2 {
3 "id": "workspaceDrawer.item.noServicesAddedYet",
4 "defaultMessage": "!!!No services added yet",
5 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
6 "start": {
7 "line": 12,
8 "column": 22
9 },
10 "end": {
11 "line": 15,
12 "column": 3
13 }
14 },
15 {
16 "id": "workspaceDrawer.item.contextMenuEdit",
17 "defaultMessage": "!!!edit",
18 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
19 "start": {
20 "line": 16,
21 "column": 19
22 },
23 "end": {
24 "line": 19,
25 "column": 3
26 }
27 }
28] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/WorkspaceSwitchingIndicator.json b/src/i18n/messages/src/features/workspaces/components/WorkspaceSwitchingIndicator.json
new file mode 100644
index 000000000..4f3e6d55c
--- /dev/null
+++ b/src/i18n/messages/src/features/workspaces/components/WorkspaceSwitchingIndicator.json
@@ -0,0 +1,15 @@
1[
2 {
3 "id": "workspaces.switchingIndicator.switchingTo",
4 "defaultMessage": "!!!Switching to",
5 "file": "src/features/workspaces/components/WorkspaceSwitchingIndicator.js",
6 "start": {
7 "line": 12,
8 "column": 15
9 },
10 "end": {
11 "line": 15,
12 "column": 3
13 }
14 }
15] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json b/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json
new file mode 100644
index 000000000..ef8f1bebc
--- /dev/null
+++ b/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json
@@ -0,0 +1,106 @@
1[
2 {
3 "id": "settings.workspaces.headline",
4 "defaultMessage": "!!!Your workspaces",
5 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
6 "start": {
7 "line": 17,
8 "column": 12
9 },
10 "end": {
11 "line": 20,
12 "column": 3
13 }
14 },
15 {
16 "id": "settings.workspaces.noWorkspacesAdded",
17 "defaultMessage": "!!!You haven't added any workspaces yet.",
18 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
19 "start": {
20 "line": 21,
21 "column": 19
22 },
23 "end": {
24 "line": 24,
25 "column": 3
26 }
27 },
28 {
29 "id": "settings.workspaces.workspacesRequestFailed",
30 "defaultMessage": "!!!Could not load your workspaces",
31 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
32 "start": {
33 "line": 25,
34 "column": 27
35 },
36 "end": {
37 "line": 28,
38 "column": 3
39 }
40 },
41 {
42 "id": "settings.workspaces.tryReloadWorkspaces",
43 "defaultMessage": "!!!Try again",
44 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
45 "start": {
46 "line": 29,
47 "column": 23
48 },
49 "end": {
50 "line": 32,
51 "column": 3
52 }
53 },
54 {
55 "id": "settings.workspaces.updatedInfo",
56 "defaultMessage": "!!!Your changes have been saved",
57 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
58 "start": {
59 "line": 33,
60 "column": 15
61 },
62 "end": {
63 "line": 36,
64 "column": 3
65 }
66 },
67 {
68 "id": "settings.workspaces.deletedInfo",
69 "defaultMessage": "!!!Workspace has been deleted",
70 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
71 "start": {
72 "line": 37,
73 "column": 15
74 },
75 "end": {
76 "line": 40,
77 "column": 3
78 }
79 },
80 {
81 "id": "settings.workspaces.workspaceFeatureInfo",
82 "defaultMessage": "!!!Info about workspace feature",
83 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
84 "start": {
85 "line": 41,
86 "column": 24
87 },
88 "end": {
89 "line": 44,
90 "column": 3
91 }
92 },
93 {
94 "id": "settings.workspaces.workspaceFeatureHeadline",
95 "defaultMessage": "!!!Less is More: Introducing Franz Workspaces",
96 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
97 "start": {
98 "line": 45,
99 "column": 28
100 },
101 "end": {
102 "line": 48,
103 "column": 3
104 }
105 }
106] \ No newline at end of file
diff --git a/src/i18n/messages/src/lib/Menu.json b/src/i18n/messages/src/lib/Menu.json
index 9314f5cce..3889d39e0 100644
--- a/src/i18n/messages/src/lib/Menu.json
+++ b/src/i18n/messages/src/lib/Menu.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Edit", 4 "defaultMessage": "!!!Edit",
5 "file": "src/lib/Menu.js", 5 "file": "src/lib/Menu.js",
6 "start": { 6 "start": {
7 "line": 10, 7 "line": 13,
8 "column": 8 8 "column": 8
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 13, 11 "line": 16,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!Undo", 17 "defaultMessage": "!!!Undo",
18 "file": "src/lib/Menu.js", 18 "file": "src/lib/Menu.js",
19 "start": { 19 "start": {
20 "line": 14, 20 "line": 17,
21 "column": 8 21 "column": 8
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 17, 24 "line": 20,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Redo", 30 "defaultMessage": "!!!Redo",
31 "file": "src/lib/Menu.js", 31 "file": "src/lib/Menu.js",
32 "start": { 32 "start": {
33 "line": 18, 33 "line": 21,
34 "column": 8 34 "column": 8
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 21, 37 "line": 24,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,11 @@
43 "defaultMessage": "!!!Cut", 43 "defaultMessage": "!!!Cut",
44 "file": "src/lib/Menu.js", 44 "file": "src/lib/Menu.js",
45 "start": { 45 "start": {
46 "line": 22, 46 "line": 25,
47 "column": 7 47 "column": 7
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 25, 50 "line": 28,
51 "column": 3 51 "column": 3
52 } 52 }
53 }, 53 },
@@ -56,11 +56,11 @@
56 "defaultMessage": "!!!Copy", 56 "defaultMessage": "!!!Copy",
57 "file": "src/lib/Menu.js", 57 "file": "src/lib/Menu.js",
58 "start": { 58 "start": {
59 "line": 26, 59 "line": 29,
60 "column": 8 60 "column": 8
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 29, 63 "line": 32,
64 "column": 3 64 "column": 3
65 } 65 }
66 }, 66 },
@@ -69,11 +69,11 @@
69 "defaultMessage": "!!!Paste", 69 "defaultMessage": "!!!Paste",
70 "file": "src/lib/Menu.js", 70 "file": "src/lib/Menu.js",
71 "start": { 71 "start": {
72 "line": 30, 72 "line": 33,
73 "column": 9 73 "column": 9
74 }, 74 },
75 "end": { 75 "end": {
76 "line": 33, 76 "line": 36,
77 "column": 3 77 "column": 3
78 } 78 }
79 }, 79 },
@@ -82,11 +82,11 @@
82 "defaultMessage": "!!!Paste And Match Style", 82 "defaultMessage": "!!!Paste And Match Style",
83 "file": "src/lib/Menu.js", 83 "file": "src/lib/Menu.js",
84 "start": { 84 "start": {
85 "line": 34, 85 "line": 37,
86 "column": 22 86 "column": 22
87 }, 87 },
88 "end": { 88 "end": {
89 "line": 37, 89 "line": 40,
90 "column": 3 90 "column": 3
91 } 91 }
92 }, 92 },
@@ -95,11 +95,11 @@
95 "defaultMessage": "!!!Delete", 95 "defaultMessage": "!!!Delete",
96 "file": "src/lib/Menu.js", 96 "file": "src/lib/Menu.js",
97 "start": { 97 "start": {
98 "line": 38, 98 "line": 41,
99 "column": 10 99 "column": 10
100 }, 100 },
101 "end": { 101 "end": {
102 "line": 41, 102 "line": 44,
103 "column": 3 103 "column": 3
104 } 104 }
105 }, 105 },
@@ -108,11 +108,11 @@
108 "defaultMessage": "!!!Select All", 108 "defaultMessage": "!!!Select All",
109 "file": "src/lib/Menu.js", 109 "file": "src/lib/Menu.js",
110 "start": { 110 "start": {
111 "line": 42, 111 "line": 45,
112 "column": 13 112 "column": 13
113 }, 113 },
114 "end": { 114 "end": {
115 "line": 45, 115 "line": 48,
116 "column": 3 116 "column": 3
117 } 117 }
118 }, 118 },
@@ -121,11 +121,11 @@
121 "defaultMessage": "!!!Speech", 121 "defaultMessage": "!!!Speech",
122 "file": "src/lib/Menu.js", 122 "file": "src/lib/Menu.js",
123 "start": { 123 "start": {
124 "line": 46, 124 "line": 49,
125 "column": 10 125 "column": 10
126 }, 126 },
127 "end": { 127 "end": {
128 "line": 49, 128 "line": 52,
129 "column": 3 129 "column": 3
130 } 130 }
131 }, 131 },
@@ -134,11 +134,11 @@
134 "defaultMessage": "!!!Start Speaking", 134 "defaultMessage": "!!!Start Speaking",
135 "file": "src/lib/Menu.js", 135 "file": "src/lib/Menu.js",
136 "start": { 136 "start": {
137 "line": 50, 137 "line": 53,
138 "column": 17 138 "column": 17
139 }, 139 },
140 "end": { 140 "end": {
141 "line": 53, 141 "line": 56,
142 "column": 3 142 "column": 3
143 } 143 }
144 }, 144 },
@@ -147,11 +147,11 @@
147 "defaultMessage": "!!!Stop Speaking", 147 "defaultMessage": "!!!Stop Speaking",
148 "file": "src/lib/Menu.js", 148 "file": "src/lib/Menu.js",
149 "start": { 149 "start": {
150 "line": 54, 150 "line": 57,
151 "column": 16 151 "column": 16
152 }, 152 },
153 "end": { 153 "end": {
154 "line": 57, 154 "line": 60,
155 "column": 3 155 "column": 3
156 } 156 }
157 }, 157 },
@@ -160,11 +160,11 @@
160 "defaultMessage": "!!!Start Dictation", 160 "defaultMessage": "!!!Start Dictation",
161 "file": "src/lib/Menu.js", 161 "file": "src/lib/Menu.js",
162 "start": { 162 "start": {
163 "line": 58, 163 "line": 61,
164 "column": 18 164 "column": 18
165 }, 165 },
166 "end": { 166 "end": {
167 "line": 61, 167 "line": 64,
168 "column": 3 168 "column": 3
169 } 169 }
170 }, 170 },
@@ -173,11 +173,11 @@
173 "defaultMessage": "!!!Emoji & Symbols", 173 "defaultMessage": "!!!Emoji & Symbols",
174 "file": "src/lib/Menu.js", 174 "file": "src/lib/Menu.js",
175 "start": { 175 "start": {
176 "line": 62, 176 "line": 65,
177 "column": 16 177 "column": 16
178 }, 178 },
179 "end": { 179 "end": {
180 "line": 65, 180 "line": 68,
181 "column": 3 181 "column": 3
182 } 182 }
183 }, 183 },
@@ -186,11 +186,11 @@
186 "defaultMessage": "!!!Actual Size", 186 "defaultMessage": "!!!Actual Size",
187 "file": "src/lib/Menu.js", 187 "file": "src/lib/Menu.js",
188 "start": { 188 "start": {
189 "line": 66, 189 "line": 69,
190 "column": 13 190 "column": 13
191 }, 191 },
192 "end": { 192 "end": {
193 "line": 69, 193 "line": 72,
194 "column": 3 194 "column": 3
195 } 195 }
196 }, 196 },
@@ -199,11 +199,11 @@
199 "defaultMessage": "!!!Zoom In", 199 "defaultMessage": "!!!Zoom In",
200 "file": "src/lib/Menu.js", 200 "file": "src/lib/Menu.js",
201 "start": { 201 "start": {
202 "line": 70, 202 "line": 73,
203 "column": 10 203 "column": 10
204 }, 204 },
205 "end": { 205 "end": {
206 "line": 73, 206 "line": 76,
207 "column": 3 207 "column": 3
208 } 208 }
209 }, 209 },
@@ -212,11 +212,11 @@
212 "defaultMessage": "!!!Zoom Out", 212 "defaultMessage": "!!!Zoom Out",
213 "file": "src/lib/Menu.js", 213 "file": "src/lib/Menu.js",
214 "start": { 214 "start": {
215 "line": 74, 215 "line": 77,
216 "column": 11 216 "column": 11
217 }, 217 },
218 "end": { 218 "end": {
219 "line": 77, 219 "line": 80,
220 "column": 3 220 "column": 3
221 } 221 }
222 }, 222 },
@@ -225,11 +225,11 @@
225 "defaultMessage": "!!!Enter Full Screen", 225 "defaultMessage": "!!!Enter Full Screen",
226 "file": "src/lib/Menu.js", 226 "file": "src/lib/Menu.js",
227 "start": { 227 "start": {
228 "line": 78, 228 "line": 81,
229 "column": 19 229 "column": 19
230 }, 230 },
231 "end": { 231 "end": {
232 "line": 81, 232 "line": 84,
233 "column": 3 233 "column": 3
234 } 234 }
235 }, 235 },
@@ -238,11 +238,11 @@
238 "defaultMessage": "!!!Exit Full Screen", 238 "defaultMessage": "!!!Exit Full Screen",
239 "file": "src/lib/Menu.js", 239 "file": "src/lib/Menu.js",
240 "start": { 240 "start": {
241 "line": 82, 241 "line": 85,
242 "column": 18 242 "column": 18
243 }, 243 },
244 "end": { 244 "end": {
245 "line": 85, 245 "line": 88,
246 "column": 3 246 "column": 3
247 } 247 }
248 }, 248 },
@@ -251,11 +251,11 @@
251 "defaultMessage": "!!!Toggle Full Screen", 251 "defaultMessage": "!!!Toggle Full Screen",
252 "file": "src/lib/Menu.js", 252 "file": "src/lib/Menu.js",
253 "start": { 253 "start": {
254 "line": 86, 254 "line": 89,
255 "column": 20 255 "column": 20
256 }, 256 },
257 "end": { 257 "end": {
258 "line": 89, 258 "line": 92,
259 "column": 3 259 "column": 3
260 } 260 }
261 }, 261 },
@@ -264,11 +264,11 @@
264 "defaultMessage": "!!!Toggle Developer Tools", 264 "defaultMessage": "!!!Toggle Developer Tools",
265 "file": "src/lib/Menu.js", 265 "file": "src/lib/Menu.js",
266 "start": { 266 "start": {
267 "line": 90, 267 "line": 93,
268 "column": 18 268 "column": 18
269 }, 269 },
270 "end": { 270 "end": {
271 "line": 93, 271 "line": 96,
272 "column": 3 272 "column": 3
273 } 273 }
274 }, 274 },
@@ -277,11 +277,11 @@
277 "defaultMessage": "!!!Toggle Service Developer Tools", 277 "defaultMessage": "!!!Toggle Service Developer Tools",
278 "file": "src/lib/Menu.js", 278 "file": "src/lib/Menu.js",
279 "start": { 279 "start": {
280 "line": 94, 280 "line": 97,
281 "column": 25 281 "column": 25
282 }, 282 },
283 "end": { 283 "end": {
284 "line": 97, 284 "line": 100,
285 "column": 3 285 "column": 3
286 } 286 }
287 }, 287 },
@@ -290,11 +290,11 @@
290 "defaultMessage": "!!!Reload Service", 290 "defaultMessage": "!!!Reload Service",
291 "file": "src/lib/Menu.js", 291 "file": "src/lib/Menu.js",
292 "start": { 292 "start": {
293 "line": 98, 293 "line": 101,
294 "column": 17 294 "column": 17
295 }, 295 },
296 "end": { 296 "end": {
297 "line": 101, 297 "line": 104,
298 "column": 3 298 "column": 3
299 } 299 }
300 }, 300 },
@@ -303,11 +303,11 @@
303 "defaultMessage": "!!!Reload Franz", 303 "defaultMessage": "!!!Reload Franz",
304 "file": "src/lib/Menu.js", 304 "file": "src/lib/Menu.js",
305 "start": { 305 "start": {
306 "line": 102, 306 "line": 105,
307 "column": 15 307 "column": 15
308 }, 308 },
309 "end": { 309 "end": {
310 "line": 105, 310 "line": 108,
311 "column": 3 311 "column": 3
312 } 312 }
313 }, 313 },
@@ -316,11 +316,11 @@
316 "defaultMessage": "!!!Minimize", 316 "defaultMessage": "!!!Minimize",
317 "file": "src/lib/Menu.js", 317 "file": "src/lib/Menu.js",
318 "start": { 318 "start": {
319 "line": 106, 319 "line": 109,
320 "column": 12 320 "column": 12
321 }, 321 },
322 "end": { 322 "end": {
323 "line": 109, 323 "line": 112,
324 "column": 3 324 "column": 3
325 } 325 }
326 }, 326 },
@@ -329,11 +329,11 @@
329 "defaultMessage": "!!!Close", 329 "defaultMessage": "!!!Close",
330 "file": "src/lib/Menu.js", 330 "file": "src/lib/Menu.js",
331 "start": { 331 "start": {
332 "line": 110, 332 "line": 113,
333 "column": 9 333 "column": 9
334 }, 334 },
335 "end": { 335 "end": {
336 "line": 113, 336 "line": 116,
337 "column": 3 337 "column": 3
338 } 338 }
339 }, 339 },
@@ -342,11 +342,11 @@
342 "defaultMessage": "!!!Learn More", 342 "defaultMessage": "!!!Learn More",
343 "file": "src/lib/Menu.js", 343 "file": "src/lib/Menu.js",
344 "start": { 344 "start": {
345 "line": 114, 345 "line": 117,
346 "column": 13 346 "column": 13
347 }, 347 },
348 "end": { 348 "end": {
349 "line": 117, 349 "line": 120,
350 "column": 3 350 "column": 3
351 } 351 }
352 }, 352 },
@@ -355,11 +355,11 @@
355 "defaultMessage": "!!!Changelog", 355 "defaultMessage": "!!!Changelog",
356 "file": "src/lib/Menu.js", 356 "file": "src/lib/Menu.js",
357 "start": { 357 "start": {
358 "line": 118, 358 "line": 121,
359 "column": 13 359 "column": 13
360 }, 360 },
361 "end": { 361 "end": {
362 "line": 121, 362 "line": 124,
363 "column": 3 363 "column": 3
364 } 364 }
365 }, 365 },
@@ -368,11 +368,11 @@
368 "defaultMessage": "!!!Support", 368 "defaultMessage": "!!!Support",
369 "file": "src/lib/Menu.js", 369 "file": "src/lib/Menu.js",
370 "start": { 370 "start": {
371 "line": 122, 371 "line": 125,
372 "column": 11 372 "column": 11
373 }, 373 },
374 "end": { 374 "end": {
375 "line": 125, 375 "line": 128,
376 "column": 3 376 "column": 3
377 } 377 }
378 }, 378 },
@@ -381,11 +381,11 @@
381 "defaultMessage": "!!!Terms of Service", 381 "defaultMessage": "!!!Terms of Service",
382 "file": "src/lib/Menu.js", 382 "file": "src/lib/Menu.js",
383 "start": { 383 "start": {
384 "line": 126, 384 "line": 129,
385 "column": 7 385 "column": 7
386 }, 386 },
387 "end": { 387 "end": {
388 "line": 129, 388 "line": 132,
389 "column": 3 389 "column": 3
390 } 390 }
391 }, 391 },
@@ -394,11 +394,11 @@
394 "defaultMessage": "!!!Privacy Statement", 394 "defaultMessage": "!!!Privacy Statement",
395 "file": "src/lib/Menu.js", 395 "file": "src/lib/Menu.js",
396 "start": { 396 "start": {
397 "line": 130, 397 "line": 133,
398 "column": 11 398 "column": 11
399 }, 399 },
400 "end": { 400 "end": {
401 "line": 133, 401 "line": 136,
402 "column": 3 402 "column": 3
403 } 403 }
404 }, 404 },
@@ -407,11 +407,11 @@
407 "defaultMessage": "!!!File", 407 "defaultMessage": "!!!File",
408 "file": "src/lib/Menu.js", 408 "file": "src/lib/Menu.js",
409 "start": { 409 "start": {
410 "line": 134, 410 "line": 137,
411 "column": 8 411 "column": 8
412 }, 412 },
413 "end": { 413 "end": {
414 "line": 137, 414 "line": 140,
415 "column": 3 415 "column": 3
416 } 416 }
417 }, 417 },
@@ -420,11 +420,11 @@
420 "defaultMessage": "!!!View", 420 "defaultMessage": "!!!View",
421 "file": "src/lib/Menu.js", 421 "file": "src/lib/Menu.js",
422 "start": { 422 "start": {
423 "line": 138, 423 "line": 141,
424 "column": 8 424 "column": 8
425 }, 425 },
426 "end": { 426 "end": {
427 "line": 141, 427 "line": 144,
428 "column": 3 428 "column": 3
429 } 429 }
430 }, 430 },
@@ -433,11 +433,11 @@
433 "defaultMessage": "!!!Services", 433 "defaultMessage": "!!!Services",
434 "file": "src/lib/Menu.js", 434 "file": "src/lib/Menu.js",
435 "start": { 435 "start": {
436 "line": 142, 436 "line": 145,
437 "column": 12 437 "column": 12
438 }, 438 },
439 "end": { 439 "end": {
440 "line": 145, 440 "line": 148,
441 "column": 3 441 "column": 3
442 } 442 }
443 }, 443 },
@@ -446,11 +446,11 @@
446 "defaultMessage": "!!!Window", 446 "defaultMessage": "!!!Window",
447 "file": "src/lib/Menu.js", 447 "file": "src/lib/Menu.js",
448 "start": { 448 "start": {
449 "line": 146, 449 "line": 149,
450 "column": 10 450 "column": 10
451 }, 451 },
452 "end": { 452 "end": {
453 "line": 149, 453 "line": 152,
454 "column": 3 454 "column": 3
455 } 455 }
456 }, 456 },
@@ -459,11 +459,11 @@
459 "defaultMessage": "!!!Help", 459 "defaultMessage": "!!!Help",
460 "file": "src/lib/Menu.js", 460 "file": "src/lib/Menu.js",
461 "start": { 461 "start": {
462 "line": 150, 462 "line": 153,
463 "column": 8 463 "column": 8
464 }, 464 },
465 "end": { 465 "end": {
466 "line": 153, 466 "line": 156,
467 "column": 3 467 "column": 3
468 } 468 }
469 }, 469 },
@@ -472,11 +472,11 @@
472 "defaultMessage": "!!!About Franz", 472 "defaultMessage": "!!!About Franz",
473 "file": "src/lib/Menu.js", 473 "file": "src/lib/Menu.js",
474 "start": { 474 "start": {
475 "line": 154, 475 "line": 157,
476 "column": 9 476 "column": 9
477 }, 477 },
478 "end": { 478 "end": {
479 "line": 157, 479 "line": 160,
480 "column": 3 480 "column": 3
481 } 481 }
482 }, 482 },
@@ -485,11 +485,11 @@
485 "defaultMessage": "!!!Settings", 485 "defaultMessage": "!!!Settings",
486 "file": "src/lib/Menu.js", 486 "file": "src/lib/Menu.js",
487 "start": { 487 "start": {
488 "line": 158, 488 "line": 161,
489 "column": 12 489 "column": 12
490 }, 490 },
491 "end": { 491 "end": {
492 "line": 161, 492 "line": 164,
493 "column": 3 493 "column": 3
494 } 494 }
495 }, 495 },
@@ -498,11 +498,11 @@
498 "defaultMessage": "!!!Hide", 498 "defaultMessage": "!!!Hide",
499 "file": "src/lib/Menu.js", 499 "file": "src/lib/Menu.js",
500 "start": { 500 "start": {
501 "line": 162, 501 "line": 165,
502 "column": 8 502 "column": 8
503 }, 503 },
504 "end": { 504 "end": {
505 "line": 165, 505 "line": 168,
506 "column": 3 506 "column": 3
507 } 507 }
508 }, 508 },
@@ -511,11 +511,11 @@
511 "defaultMessage": "!!!Hide Others", 511 "defaultMessage": "!!!Hide Others",
512 "file": "src/lib/Menu.js", 512 "file": "src/lib/Menu.js",
513 "start": { 513 "start": {
514 "line": 166, 514 "line": 169,
515 "column": 14 515 "column": 14
516 }, 516 },
517 "end": { 517 "end": {
518 "line": 169, 518 "line": 172,
519 "column": 3 519 "column": 3
520 } 520 }
521 }, 521 },
@@ -524,11 +524,11 @@
524 "defaultMessage": "!!!Unhide", 524 "defaultMessage": "!!!Unhide",
525 "file": "src/lib/Menu.js", 525 "file": "src/lib/Menu.js",
526 "start": { 526 "start": {
527 "line": 170, 527 "line": 173,
528 "column": 10 528 "column": 10
529 }, 529 },
530 "end": { 530 "end": {
531 "line": 173, 531 "line": 176,
532 "column": 3 532 "column": 3
533 } 533 }
534 }, 534 },
@@ -537,11 +537,11 @@
537 "defaultMessage": "!!!Quit", 537 "defaultMessage": "!!!Quit",
538 "file": "src/lib/Menu.js", 538 "file": "src/lib/Menu.js",
539 "start": { 539 "start": {
540 "line": 174, 540 "line": 177,
541 "column": 8 541 "column": 8
542 }, 542 },
543 "end": { 543 "end": {
544 "line": 177, 544 "line": 180,
545 "column": 3 545 "column": 3
546 } 546 }
547 }, 547 },
@@ -550,11 +550,50 @@
550 "defaultMessage": "!!!Add New Service...", 550 "defaultMessage": "!!!Add New Service...",
551 "file": "src/lib/Menu.js", 551 "file": "src/lib/Menu.js",
552 "start": { 552 "start": {
553 "line": 178, 553 "line": 181,
554 "column": 17 554 "column": 17
555 }, 555 },
556 "end": { 556 "end": {
557 "line": 181, 557 "line": 184,
558 "column": 3
559 }
560 },
561 {
562 "id": "menu.workspaces.addNewWorkspace",
563 "defaultMessage": "!!!Add New Workspace...",
564 "file": "src/lib/Menu.js",
565 "start": {
566 "line": 185,
567 "column": 19
568 },
569 "end": {
570 "line": 188,
571 "column": 3
572 }
573 },
574 {
575 "id": "menu.workspaces.openWorkspaceDrawer",
576 "defaultMessage": "!!!Open workspace drawer",
577 "file": "src/lib/Menu.js",
578 "start": {
579 "line": 189,
580 "column": 23
581 },
582 "end": {
583 "line": 192,
584 "column": 3
585 }
586 },
587 {
588 "id": "menu.workspaces.closeWorkspaceDrawer",
589 "defaultMessage": "!!!Close workspace drawer",
590 "file": "src/lib/Menu.js",
591 "start": {
592 "line": 193,
593 "column": 24
594 },
595 "end": {
596 "line": 196,
558 "column": 3 597 "column": 3
559 } 598 }
560 }, 599 },
@@ -563,11 +602,11 @@
563 "defaultMessage": "!!!Activate next service...", 602 "defaultMessage": "!!!Activate next service...",
564 "file": "src/lib/Menu.js", 603 "file": "src/lib/Menu.js",
565 "start": { 604 "start": {
566 "line": 182, 605 "line": 197,
567 "column": 23 606 "column": 23
568 }, 607 },
569 "end": { 608 "end": {
570 "line": 185, 609 "line": 200,
571 "column": 3 610 "column": 3
572 } 611 }
573 }, 612 },
@@ -576,11 +615,11 @@
576 "defaultMessage": "!!!Activate previous service...", 615 "defaultMessage": "!!!Activate previous service...",
577 "file": "src/lib/Menu.js", 616 "file": "src/lib/Menu.js",
578 "start": { 617 "start": {
579 "line": 186, 618 "line": 201,
580 "column": 27 619 "column": 27
581 }, 620 },
582 "end": { 621 "end": {
583 "line": 189, 622 "line": 204,
584 "column": 3 623 "column": 3
585 } 624 }
586 }, 625 },
@@ -589,11 +628,11 @@
589 "defaultMessage": "!!!Disable notifications & audio", 628 "defaultMessage": "!!!Disable notifications & audio",
590 "file": "src/lib/Menu.js", 629 "file": "src/lib/Menu.js",
591 "start": { 630 "start": {
592 "line": 190, 631 "line": 205,
593 "column": 11 632 "column": 11
594 }, 633 },
595 "end": { 634 "end": {
596 "line": 193, 635 "line": 208,
597 "column": 3 636 "column": 3
598 } 637 }
599 }, 638 },
@@ -602,11 +641,37 @@
602 "defaultMessage": "!!!Enable notifications & audio", 641 "defaultMessage": "!!!Enable notifications & audio",
603 "file": "src/lib/Menu.js", 642 "file": "src/lib/Menu.js",
604 "start": { 643 "start": {
605 "line": 194, 644 "line": 209,
606 "column": 13 645 "column": 13
607 }, 646 },
608 "end": { 647 "end": {
609 "line": 197, 648 "line": 212,
649 "column": 3
650 }
651 },
652 {
653 "id": "menu.workspaces",
654 "defaultMessage": "!!!Workspaces",
655 "file": "src/lib/Menu.js",
656 "start": {
657 "line": 213,
658 "column": 14
659 },
660 "end": {
661 "line": 216,
662 "column": 3
663 }
664 },
665 {
666 "id": "menu.workspaces.defaultWorkspace",
667 "defaultMessage": "!!!Default",
668 "file": "src/lib/Menu.js",
669 "start": {
670 "line": 217,
671 "column": 20
672 },
673 "end": {
674 "line": 220,
610 "column": 3 675 "column": 3
611 } 676 }
612 } 677 }
diff --git a/src/index.js b/src/index.js
index 05c793d98..3fe996aa7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -305,6 +305,20 @@ const createWindow = () => {
305 }); 305 });
306}; 306};
307 307
308// Allow passing command line parameters/switches to electron
309// https://electronjs.org/docs/api/chrome-command-line-switches
310// used for Kerberos support
311// Usage e.g. MACOS
312// $ Franz.app/Contents/MacOS/Franz --auth-server-whitelist *.mydomain.com --auth-negotiate-delegate-whitelist *.mydomain.com
313const argv = require('minimist')(process.argv.slice(1));
314
315if (argv['auth-server-whitelist']) {
316 app.commandLine.appendSwitch('auth-server-whitelist', argv['auth-server-whitelist']);
317}
318if (argv['auth-negotiate-delegate-whitelist']) {
319 app.commandLine.appendSwitch('auth-negotiate-delegate-whitelist', argv['auth-negotiate-delegate-whitelist']);
320}
321
308// This method will be called when Electron has finished 322// This method will be called when Electron has finished
309// initialization and is ready to create browser windows. 323// initialization and is ready to create browser windows.
310// Some APIs can only be used after this event occurs. 324// Some APIs can only be used after this event occurs.
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 7a60c448f..a4e41c17c 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -3,6 +3,9 @@ import { observable, autorun } from 'mobx';
3import { defineMessages } from 'react-intl'; 3import { defineMessages } from 'react-intl';
4 4
5import { isMac, ctrlKey, cmdKey } from '../environment'; 5import { isMac, ctrlKey, cmdKey } from '../environment';
6import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../features/workspaces/index';
7import { workspaceActions } from '../features/workspaces/actions';
8import { gaEvent } from './analytics';
6 9
7const { app, Menu, dialog } = remote; 10const { app, Menu, dialog } = remote;
8 11
@@ -179,6 +182,18 @@ const menuItems = defineMessages({
179 id: 'menu.services.addNewService', 182 id: 'menu.services.addNewService',
180 defaultMessage: '!!!Add New Service...', 183 defaultMessage: '!!!Add New Service...',
181 }, 184 },
185 addNewWorkspace: {
186 id: 'menu.workspaces.addNewWorkspace',
187 defaultMessage: '!!!Add New Workspace...',
188 },
189 openWorkspaceDrawer: {
190 id: 'menu.workspaces.openWorkspaceDrawer',
191 defaultMessage: '!!!Open workspace drawer',
192 },
193 closeWorkspaceDrawer: {
194 id: 'menu.workspaces.closeWorkspaceDrawer',
195 defaultMessage: '!!!Close workspace drawer',
196 },
182 activateNextService: { 197 activateNextService: {
183 id: 'menu.services.setNextServiceActive', 198 id: 'menu.services.setNextServiceActive',
184 defaultMessage: '!!!Activate next service...', 199 defaultMessage: '!!!Activate next service...',
@@ -195,6 +210,14 @@ const menuItems = defineMessages({
195 id: 'sidebar.unmuteApp', 210 id: 'sidebar.unmuteApp',
196 defaultMessage: '!!!Enable notifications & audio', 211 defaultMessage: '!!!Enable notifications & audio',
197 }, 212 },
213 workspaces: {
214 id: 'menu.workspaces',
215 defaultMessage: '!!!Workspaces',
216 },
217 defaultWorkspace: {
218 id: 'menu.workspaces.defaultWorkspace',
219 defaultMessage: '!!!Default',
220 },
198}); 221});
199 222
200function getActiveWebview() { 223function getActiveWebview() {
@@ -298,6 +321,11 @@ const _templateFactory = intl => [
298 submenu: [], 321 submenu: [],
299 }, 322 },
300 { 323 {
324 label: intl.formatMessage(menuItems.workspaces),
325 submenu: [],
326 visible: workspaceStore.isFeatureEnabled,
327 },
328 {
301 label: intl.formatMessage(menuItems.window), 329 label: intl.formatMessage(menuItems.window),
302 role: 'window', 330 role: 'window',
303 submenu: [ 331 submenu: [
@@ -669,7 +697,7 @@ export default class FranzMenu {
669 }, 697 },
670 ); 698 );
671 699
672 tpl[4].submenu.unshift(about, { 700 tpl[5].submenu.unshift(about, {
673 type: 'separator', 701 type: 'separator',
674 }); 702 });
675 } else { 703 } else {
@@ -704,6 +732,10 @@ export default class FranzMenu {
704 tpl[3].submenu = serviceTpl; 732 tpl[3].submenu = serviceTpl;
705 } 733 }
706 734
735 if (workspaceStore.isFeatureEnabled) {
736 tpl[4].submenu = this.workspacesMenu();
737 }
738
707 this.currentTemplate = tpl; 739 this.currentTemplate = tpl;
708 const menu = Menu.buildFromTemplate(tpl); 740 const menu = Menu.buildFromTemplate(tpl);
709 Menu.setApplicationMenu(menu); 741 Menu.setApplicationMenu(menu);
@@ -754,6 +786,66 @@ export default class FranzMenu {
754 return menu; 786 return menu;
755 } 787 }
756 788
789 workspacesMenu() {
790 const { workspaces, activeWorkspace, isWorkspaceDrawerOpen } = workspaceStore;
791 const { intl } = window.franz;
792 const menu = [];
793
794 // Add new workspace item:
795 menu.push({
796 label: intl.formatMessage(menuItems.addNewWorkspace),
797 accelerator: `${cmdKey}+Shift+N`,
798 click: () => {
799 workspaceActions.openWorkspaceSettings();
800 },
801 enabled: this.stores.user.isLoggedIn,
802 });
803
804 // Open workspace drawer:
805 const drawerLabel = (
806 isWorkspaceDrawerOpen ? menuItems.closeWorkspaceDrawer : menuItems.openWorkspaceDrawer
807 );
808 menu.push({
809 label: intl.formatMessage(drawerLabel),
810 accelerator: `${cmdKey}+D`,
811 click: () => {
812 workspaceActions.toggleWorkspaceDrawer();
813 gaEvent(GA_CATEGORY_WORKSPACES, 'toggleDrawer', 'menu');
814 },
815 enabled: this.stores.user.isLoggedIn,
816 }, {
817 type: 'separator',
818 });
819
820 // Default workspace
821 menu.push({
822 label: intl.formatMessage(menuItems.defaultWorkspace),
823 accelerator: `${cmdKey}+Alt+0`,
824 type: 'radio',
825 checked: !activeWorkspace,
826 click: () => {
827 workspaceActions.deactivate();
828 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'menu');
829 },
830 });
831
832 // Workspace items
833 if (this.stores.user.isPremium) {
834 workspaces.forEach((workspace, i) => menu.push({
835 label: workspace.name,
836 accelerator: i < 9 ? `${cmdKey}+Alt+${i + 1}` : null,
837 type: 'radio',
838 checked: activeWorkspace ? workspace.id === activeWorkspace.id : false,
839 click: () => {
840 workspaceActions.activate({ workspace });
841 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'menu');
842 },
843 }));
844 }
845
846 return menu;
847 }
848
757 _getServiceName(service) { 849 _getServiceName(service) {
758 if (service.name) { 850 if (service.name) {
759 return service.name; 851 return service.name;
diff --git a/src/lib/analytics.js b/src/lib/analytics.js
index 0519192d1..e7daa9d06 100644
--- a/src/lib/analytics.js
+++ b/src/lib/analytics.js
@@ -28,12 +28,10 @@ ga('send', 'App');
28 28
29export function gaPage(page) { 29export function gaPage(page) {
30 ga('send', 'pageview', page); 30 ga('send', 'pageview', page);
31
32 debug('GA track page', page); 31 debug('GA track page', page);
33} 32}
34 33
35export function gaEvent(category, action, label) { 34export function gaEvent(category, action, label) {
36 ga('send', 'event', category, action, label); 35 ga('send', 'event', category, action, label);
37 36 debug('GA track event', category, action, label);
38 debug('GA track event', category, action);
39} 37}
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index e60d79075..6200c9a16 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -1,4 +1,9 @@
1import { computed, observable, reaction } from 'mobx'; 1import {
2 computed,
3 observable,
4 reaction,
5 runInAction,
6} from 'mobx';
2 7
3import Store from './lib/Store'; 8import Store from './lib/Store';
4import CachedRequest from './lib/CachedRequest'; 9import CachedRequest from './lib/CachedRequest';
@@ -7,6 +12,7 @@ import delayApp from '../features/delayApp';
7import spellchecker from '../features/spellchecker'; 12import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy'; 13import serviceProxy from '../features/serviceProxy';
9import basicAuth from '../features/basicAuth'; 14import basicAuth from '../features/basicAuth';
15import workspaces from '../features/workspaces';
10import shareFranz from '../features/shareFranz'; 16import shareFranz from '../features/shareFranz';
11import settingsWS from '../features/settingsWS'; 17import settingsWS from '../features/settingsWS';
12 18
@@ -17,13 +23,16 @@ export default class FeaturesStore extends Store {
17 23
18 @observable featuresRequest = new CachedRequest(this.api.features, 'features'); 24 @observable featuresRequest = new CachedRequest(this.api.features, 'features');
19 25
26 @observable features = Object.assign({}, DEFAULT_FEATURES_CONFIG);
27
20 async setup() { 28 async setup() {
21 this.registerReactions([ 29 this.registerReactions([
30 this._updateFeatures,
22 this._monitorLoginStatus.bind(this), 31 this._monitorLoginStatus.bind(this),
23 ]); 32 ]);
24 33
25 await this.featuresRequest._promise; 34 await this.featuresRequest._promise;
26 setTimeout(this._enableFeatures.bind(this), 1); 35 setTimeout(this._setupFeatures.bind(this), 1);
27 36
28 // single key reaction 37 // single key reaction
29 reaction(() => this.stores.user.data.isPremium, () => { 38 reaction(() => this.stores.user.data.isPremium, () => {
@@ -37,13 +46,16 @@ export default class FeaturesStore extends Store {
37 return this.defaultFeaturesRequest.execute().result || DEFAULT_FEATURES_CONFIG; 46 return this.defaultFeaturesRequest.execute().result || DEFAULT_FEATURES_CONFIG;
38 } 47 }
39 48
40 @computed get features() { 49 _updateFeatures = () => {
50 const features = Object.assign({}, DEFAULT_FEATURES_CONFIG);
41 if (this.stores.user.isLoggedIn) { 51 if (this.stores.user.isLoggedIn) {
42 return this.featuresRequest.execute().result || DEFAULT_FEATURES_CONFIG; 52 const requestResult = this.featuresRequest.execute().result;
53 Object.assign(features, requestResult);
43 } 54 }
44 55 runInAction('FeaturesStore::_updateFeatures', () => {
45 return DEFAULT_FEATURES_CONFIG; 56 this.features = features;
46 } 57 });
58 };
47 59
48 _monitorLoginStatus() { 60 _monitorLoginStatus() {
49 if (this.stores.user.isLoggedIn) { 61 if (this.stores.user.isLoggedIn) {
@@ -53,11 +65,12 @@ export default class FeaturesStore extends Store {
53 } 65 }
54 } 66 }
55 67
56 _enableFeatures() { 68 _setupFeatures() {
57 delayApp(this.stores, this.actions); 69 delayApp(this.stores, this.actions);
58 spellchecker(this.stores, this.actions); 70 spellchecker(this.stores, this.actions);
59 serviceProxy(this.stores, this.actions); 71 serviceProxy(this.stores, this.actions);
60 basicAuth(this.stores, this.actions); 72 basicAuth(this.stores, this.actions);
73 workspaces(this.stores, this.actions);
61 shareFranz(this.stores, this.actions); 74 shareFranz(this.stores, this.actions);
62 settingsWS(this.stores, this.actions); 75 settingsWS(this.stores, this.actions);
63 } 76 }
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 69e616f0c..0ec6bf550 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -12,6 +12,7 @@ import Request from './lib/Request';
12import CachedRequest from './lib/CachedRequest'; 12import CachedRequest from './lib/CachedRequest';
13import { matchRoute } from '../helpers/routing-helpers'; 13import { matchRoute } from '../helpers/routing-helpers';
14import { gaEvent } from '../lib/analytics'; 14import { gaEvent } from '../lib/analytics';
15import { workspaceStore } from '../features/workspaces';
15 16
16const debug = require('debug')('Franz:ServiceStore'); 17const debug = require('debug')('Franz:ServiceStore');
17 18
@@ -99,7 +100,6 @@ export default class ServicesStore extends Store {
99 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));
100 } 101 }
101 } 102 }
102
103 return []; 103 return [];
104 } 104 }
105 105
@@ -108,13 +108,16 @@ export default class ServicesStore extends Store {
108 } 108 }
109 109
110 @computed get allDisplayed() { 110 @computed get allDisplayed() {
111 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 workspaceStore.filterServicesByActiveWorkspace(services);
112 } 113 }
113 114
114 // 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
115 @computed get allDisplayedUnordered() { 116 @computed get allDisplayedUnordered() {
117 const { showDisabledServices } = this.stores.settings.all.app;
116 const services = this.allServicesRequest.execute().result || []; 118 const services = this.allServicesRequest.execute().result || [];
117 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 workspaceStore.filterServicesByActiveWorkspace(filteredServices);
118 } 121 }
119 122
120 @computed get filtered() { 123 @computed get filtered() {
diff --git a/src/stores/UIStore.js b/src/stores/UIStore.js
index bb7965a4a..a95a8e1e0 100644
--- a/src/stores/UIStore.js
+++ b/src/stores/UIStore.js
@@ -21,11 +21,12 @@ export default class UIStore extends Store {
21 return (settings.app.isAppMuted && settings.app.showMessageBadgeWhenMuted) || !settings.isAppMuted; 21 return (settings.app.isAppMuted && settings.app.showMessageBadgeWhenMuted) || !settings.isAppMuted;
22 } 22 }
23 23
24 @computed get theme() { 24 @computed get isDarkThemeActive() {
25 if (this.stores.settings.all.app.darkMode) { 25 return this.stores.settings.all.app.darkMode;
26 return theme('dark'); 26 }
27 }
28 27
28 @computed get theme() {
29 if (this.isDarkThemeActive) return theme('dark');
29 return theme('default'); 30 return theme('default');
30 } 31 }
31 32
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 77d84afe1..534690fbb 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -142,6 +142,10 @@ export default class UserStore extends Store {
142 return this.getUserInfoRequest.execute().result || {}; 142 return this.getUserInfoRequest.execute().result || {};
143 } 143 }
144 144
145 @computed get isPremium() {
146 return !!this.data.isPremium;
147 }
148
145 @computed get legacyServices() { 149 @computed get legacyServices() {
146 return this.getLegacyServicesRequest.execute() || {}; 150 return this.getLegacyServicesRequest.execute() || {};
147 } 151 }
diff --git a/src/stores/lib/Request.js b/src/stores/lib/Request.js
index 04f528156..486de8a49 100644
--- a/src/stores/lib/Request.js
+++ b/src/stores/lib/Request.js
@@ -85,6 +85,8 @@ export default class Request {
85 return this.execute(...this._currentApiCall.args); 85 return this.execute(...this._currentApiCall.args);
86 } 86 }
87 87
88 retry = () => this.reload();
89
88 isExecutingWithArgs(...args) { 90 isExecutingWithArgs(...args) {
89 return this.isExecuting && this._currentApiCall && isEqual(this._currentApiCall.args, args); 91 return this.isExecuting && this._currentApiCall && isEqual(this._currentApiCall.args, args);
90 } 92 }
@@ -107,7 +109,7 @@ export default class Request {
107 Request._hooks.forEach(hook => hook(this)); 109 Request._hooks.forEach(hook => hook(this));
108 } 110 }
109 111
110 reset() { 112 reset = () => {
111 this.result = null; 113 this.result = null;
112 this.isExecuting = false; 114 this.isExecuting = false;
113 this.isError = false; 115 this.isError = false;
@@ -116,5 +118,5 @@ export default class Request {
116 this._promise = Promise; 118 this._promise = Promise;
117 119
118 return this; 120 return this;
119 } 121 };
120} 122}
diff --git a/src/styles/layout.scss b/src/styles/layout.scss
index 9a003a922..e858b7904 100644
--- a/src/styles/layout.scss
+++ b/src/styles/layout.scss
@@ -18,8 +18,14 @@ html { overflow: hidden; }
18 font-size: 22px; 18 font-size: 22px;
19 19
20 &:hover, 20 &:hover,
21 &:active { color: $dark-theme-gray-smoke; } 21 &:active {
22 &.is-muted { color: $theme-brand-primary; } 22 color: $dark-theme-gray-smoke;
23 }
24
25 &.is-muted,
26 &.is-active {
27 color: $theme-brand-primary;
28 }
23 } 29 }
24 } 30 }
25 31
@@ -33,6 +39,7 @@ html { overflow: hidden; }
33 .app__content { display: flex; } 39 .app__content { display: flex; }
34 40
35 .app__service { 41 .app__service {
42 position: relative;
36 display: flex; 43 display: flex;
37 flex: 1; 44 flex: 1;
38 flex-direction: column; 45 flex-direction: column;
@@ -84,7 +91,7 @@ html { overflow: hidden; }
84 91
85 &:hover, 92 &:hover,
86 &:active { color: lighten($theme-gray-light, 10%); } 93 &:active { color: lighten($theme-gray-light, 10%); }
87 &.is-muted { color: $theme-brand-primary; } 94 &.is-muted, &.is-active { color: $theme-brand-primary; }
88 &--new-service { padding-bottom: 6px; } 95 &--new-service { padding-bottom: 6px; }
89 } 96 }
90 97
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index b286c6f1b..efa0ab942 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -419,6 +419,7 @@
419 419
420 .settings-navigation__link { 420 .settings-navigation__link {
421 align-items: center; 421 align-items: center;
422 justify-content: space-between;
422 color: $theme-text-color; 423 color: $theme-text-color;
423 display: flex; 424 display: flex;
424 flex-shrink: 0; 425 flex-shrink: 0;
@@ -430,7 +431,9 @@
430 &:hover { 431 &:hover {
431 background: darken($theme-gray-lightest, 5%); 432 background: darken($theme-gray-lightest, 5%);
432 433
433 .badge { background: #FFF; } 434 .badge {
435 background: #FFF;
436 }
434 } 437 }
435 438
436 &.is-active { 439 &.is-active {
@@ -447,8 +450,8 @@
447 .settings-navigation__expander { flex: 1; } 450 .settings-navigation__expander { flex: 1; }
448 451
449 .badge { 452 .badge {
453
450 display: initial; 454 display: initial;
451 margin-left: 5px;
452 transition: background $theme-transition-time, color $theme-transition-time; 455 transition: background $theme-transition-time, color $theme-transition-time;
453 } 456 }
454 457