aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/accentColor/index.js55
-rw-r--r--src/features/announcements/api.js7
-rw-r--r--src/features/announcements/components/AnnouncementScreen.js7
-rw-r--r--src/features/announcements/index.js9
-rw-r--r--src/features/announcements/store.js6
-rw-r--r--src/features/basicAuth/index.js4
-rw-r--r--src/features/basicAuth/mainIpcHandler.js2
-rw-r--r--src/features/communityRecipes/index.js4
-rw-r--r--src/features/communityRecipes/store.js4
-rw-r--r--src/features/delayApp/Component.js9
-rw-r--r--src/features/delayApp/index.js7
-rw-r--r--src/features/quickSwitch/Component.js337
-rw-r--r--src/features/quickSwitch/index.js24
-rw-r--r--src/features/serviceLimit/components/LimitReachedInfobox.js3
-rw-r--r--src/features/serviceLimit/index.js2
-rw-r--r--src/features/serviceLimit/store.js9
-rw-r--r--src/features/serviceProxy/index.js14
-rwxr-xr-xsrc/features/settingsWS/index.js2
-rwxr-xr-xsrc/features/settingsWS/store.js2
-rw-r--r--src/features/shareFranz/Component.js14
-rw-r--r--src/features/shareFranz/index.js14
-rw-r--r--src/features/spellchecker/index.js20
-rw-r--r--src/features/todos/components/TodosWebview.js12
-rw-r--r--src/features/todos/containers/TodosScreen.js2
-rw-r--r--src/features/todos/index.js2
-rw-r--r--src/features/todos/preload.js4
-rw-r--r--src/features/todos/store.js2
-rw-r--r--src/features/webControls/containers/WebControlsScreen.js1
-rw-r--r--src/features/workspaces/api.js12
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.js4
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js28
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js9
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js2
-rw-r--r--src/features/workspaces/containers/EditWorkspaceScreen.js4
-rw-r--r--src/features/workspaces/index.js2
-rw-r--r--src/features/workspaces/models/Workspace.js14
-rw-r--r--src/features/workspaces/store.js23
37 files changed, 546 insertions, 130 deletions
diff --git a/src/features/accentColor/index.js b/src/features/accentColor/index.js
new file mode 100644
index 000000000..a0f57a2fa
--- /dev/null
+++ b/src/features/accentColor/index.js
@@ -0,0 +1,55 @@
1import { reaction } from 'mobx';
2import themeInfo from '../../assets/themeInfo.json';
3import { DEFAULT_APP_SETTINGS } from '../../config';
4
5const STYLE_ELEMENT_ID = 'accent-color';
6
7function createAccentStyleElement() {
8 const styles = document.createElement('style');
9 styles.id = STYLE_ELEMENT_ID;
10
11 document.querySelector('head').appendChild(styles);
12}
13
14function setAccentStyle(style) {
15 const styleElement = document.getElementById(STYLE_ELEMENT_ID);
16 styleElement.innerHTML = style;
17}
18
19function generateAccentStyle(color) {
20 let style = '';
21
22 Object.keys(themeInfo).forEach((property) => {
23 style += `
24 ${themeInfo[property]} {
25 ${property}: ${color};
26 }
27 `;
28 });
29
30 return style;
31}
32
33export default function initAccentColor(stores) {
34 const { settings } = stores;
35 createAccentStyleElement();
36
37 // Update accent color
38 reaction(
39 () => (
40 settings.all.app.accentColor
41 ),
42 (color) => {
43 if (color === DEFAULT_APP_SETTINGS.accentColor) {
44 // Reset accent style to return to default color scheme
45 setAccentStyle('');
46 } else {
47 const style = generateAccentStyle(color);
48 setAccentStyle(style);
49 }
50 },
51 {
52 fireImmediately: true,
53 },
54 );
55}
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js
index a581bd8de..e5c5a7d6f 100644
--- a/src/features/announcements/api.js
+++ b/src/features/announcements/api.js
@@ -1,8 +1,7 @@
1import { remote } from 'electron'; 1import { remote } from 'electron';
2import Request from '../../stores/lib/Request'; 2import Request from '../../stores/lib/Request';
3import { API, API_VERSION } from '../../environment';
4 3
5const debug = require('debug')('Franz:feature:announcements:api'); 4const debug = require('debug')('Ferdi:feature:announcements:api');
6 5
7export const announcementsApi = { 6export const announcementsApi = {
8 async getCurrentVersion() { 7 async getCurrentVersion() {
@@ -12,7 +11,7 @@ export const announcementsApi = {
12 11
13 async getChangelog(version) { 12 async getChangelog(version) {
14 debug('fetching release changelog from Github'); 13 debug('fetching release changelog from Github');
15 const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`; 14 const url = `https://api.github.com/repos/getferdi/ferdi/releases/tags/v${version}`;
16 const request = await window.fetch(url, { method: 'GET' }); 15 const request = await window.fetch(url, { method: 'GET' });
17 if (!request.ok) return null; 16 if (!request.ok) return null;
18 const data = await request.json(); 17 const data = await request.json();
@@ -21,7 +20,7 @@ export const announcementsApi = {
21 20
22 async getAnnouncement(version) { 21 async getAnnouncement(version) {
23 debug('fetching release announcement from api'); 22 debug('fetching release announcement from api');
24 const url = `${API}/${API_VERSION}/announcements/${version}`; 23 const url = `https://api.getferdi.com/v1/announcements/${version}`;
25 const response = await window.fetch(url, { method: 'GET' }); 24 const response = await window.fetch(url, { method: 'GET' });
26 if (!response.ok) return null; 25 if (!response.ok) return null;
27 return response.json(); 26 return response.json();
diff --git a/src/features/announcements/components/AnnouncementScreen.js b/src/features/announcements/components/AnnouncementScreen.js
index 03bd5ba41..38de2dbc8 100644
--- a/src/features/announcements/components/AnnouncementScreen.js
+++ b/src/features/announcements/components/AnnouncementScreen.js
@@ -8,7 +8,6 @@ import { Button } from '@meetfranz/forms';
8 8
9import { announcementsStore } from '../index'; 9import { announcementsStore } from '../index';
10import UIStore from '../../../stores/UIStore'; 10import UIStore from '../../../stores/UIStore';
11import { gaEvent } from '../../../lib/analytics';
12 11
13const renderer = new marked.Renderer(); 12const renderer = new marked.Renderer();
14 13
@@ -19,7 +18,7 @@ const markedOptions = { sanitize: true, renderer };
19const messages = defineMessages({ 18const messages = defineMessages({
20 headline: { 19 headline: {
21 id: 'feature.announcements.changelog.headline', 20 id: 'feature.announcements.changelog.headline',
22 defaultMessage: '!!!Changes in Franz {version}', 21 defaultMessage: '!!!Changes in Ferdi {version}',
23 }, 22 },
24}); 23});
25 24
@@ -228,9 +227,7 @@ class AnnouncementScreen extends Component {
228 <Button 227 <Button
229 label={announcement.main.cta.label} 228 label={announcement.main.cta.label}
230 onClick={() => { 229 onClick={() => {
231 const { analytics } = announcement.main.cta;
232 window.location.href = `#${announcement.main.cta.href}`; 230 window.location.href = `#${announcement.main.cta.href}`;
233 gaEvent(analytics.category, analytics.action, announcement.main.cta.label);
234 }} 231 }}
235 /> 232 />
236 </div> 233 </div>
@@ -253,9 +250,7 @@ class AnnouncementScreen extends Component {
253 <Button 250 <Button
254 label={announcement.spotlight.cta.label} 251 label={announcement.spotlight.cta.label}
255 onClick={() => { 252 onClick={() => {
256 const { analytics } = announcement.spotlight.cta;
257 window.location.href = `#${announcement.spotlight.cta.href}`; 253 window.location.href = `#${announcement.spotlight.cta.href}`;
258 gaEvent(analytics.category, analytics.action, announcement.spotlight.cta.label);
259 }} 254 }}
260 /> 255 />
261 </div> 256 </div>
diff --git a/src/features/announcements/index.js b/src/features/announcements/index.js
index f14e7c9a5..10f081bb1 100644
--- a/src/features/announcements/index.js
+++ b/src/features/announcements/index.js
@@ -1,7 +1,7 @@
1import { reaction } from 'mobx'; 1import { reaction } from 'mobx';
2import { AnnouncementsStore } from './store'; 2import { AnnouncementsStore } from './store';
3 3
4const debug = require('debug')('Franz:feature:announcements'); 4const debug = require('debug')('Ferdi:feature:announcements');
5 5
6export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements'; 6export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements';
7 7
@@ -12,13 +12,12 @@ export const ANNOUNCEMENTS_ROUTES = {
12}; 12};
13 13
14export default function initAnnouncements(stores, actions) { 14export default function initAnnouncements(stores, actions) {
15 // const { features } = stores; 15 const { features } = stores;
16 16
17 // Toggle workspace feature 17 // Toggle announcement feature
18 reaction( 18 reaction(
19 () => ( 19 () => (
20 true 20 features.features.isAnnouncementsEnabled
21 // features.features.isAnnouncementsEnabled
22 ), 21 ),
23 (isEnabled) => { 22 (isEnabled) => {
24 if (isEnabled) { 23 if (isEnabled) {
diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js
index d58afbc8e..9ec5f67d2 100644
--- a/src/features/announcements/store.js
+++ b/src/features/announcements/store.js
@@ -7,18 +7,17 @@ import semver from 'semver';
7import localStorage from 'mobx-localstorage'; 7import localStorage from 'mobx-localstorage';
8 8
9import { FeatureStore } from '../utils/FeatureStore'; 9import { FeatureStore } from '../utils/FeatureStore';
10import { ANNOUNCEMENTS_ROUTES, GA_CATEGORY_ANNOUNCEMENTS } from '.'; 10import { ANNOUNCEMENTS_ROUTES } from '.';
11import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api'; 11import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api';
12import { announcementActions } from './actions'; 12import { announcementActions } from './actions';
13import { createActionBindings } from '../utils/ActionBinding'; 13import { createActionBindings } from '../utils/ActionBinding';
14import { createReactions } from '../../stores/lib/Reaction'; 14import { createReactions } from '../../stores/lib/Reaction';
15import { gaEvent } from '../../lib/analytics';
16import { matchRoute } from '../../helpers/routing-helpers'; 15import { matchRoute } from '../../helpers/routing-helpers';
17import { DEFAULT_APP_SETTINGS } from '../../config'; 16import { DEFAULT_APP_SETTINGS } from '../../config';
18 17
19const LOCAL_STORAGE_KEY = 'announcements'; 18const LOCAL_STORAGE_KEY = 'announcements';
20 19
21const debug = require('debug')('Franz:feature:announcements:store'); 20const debug = require('debug')('Ferdi:feature:announcements:store');
22 21
23export class AnnouncementsStore extends FeatureStore { 22export class AnnouncementsStore extends FeatureStore {
24 @observable targetVersion = null; 23 @observable targetVersion = null;
@@ -114,7 +113,6 @@ export class AnnouncementsStore extends FeatureStore {
114 if (router.location.pathname !== targetRoute) { 113 if (router.location.pathname !== targetRoute) {
115 this.stores.router.push(targetRoute); 114 this.stores.router.push(targetRoute);
116 } 115 }
117 gaEvent(GA_CATEGORY_ANNOUNCEMENTS, 'show');
118 }; 116 };
119 117
120 // ======= REACTIONS ======== 118 // ======= REACTIONS ========
diff --git a/src/features/basicAuth/index.js b/src/features/basicAuth/index.js
index 89607824b..51625ea55 100644
--- a/src/features/basicAuth/index.js
+++ b/src/features/basicAuth/index.js
@@ -3,7 +3,7 @@ import { observable } from 'mobx';
3 3
4import BasicAuthComponent from './Component'; 4import BasicAuthComponent from './Component';
5 5
6const debug = require('debug')('Franz:feature:basicAuth'); 6const debug = require('debug')('Ferdi:feature:basicAuth');
7 7
8const defaultState = { 8const defaultState = {
9 isModalVisible: true, 9 isModalVisible: true,
@@ -20,7 +20,7 @@ export function resetState() {
20export default function initialize() { 20export default function initialize() {
21 debug('Initialize basicAuth feature'); 21 debug('Initialize basicAuth feature');
22 22
23 window.franz.features.basicAuth = { 23 window.ferdi.features.basicAuth = {
24 state, 24 state,
25 }; 25 };
26 26
diff --git a/src/features/basicAuth/mainIpcHandler.js b/src/features/basicAuth/mainIpcHandler.js
index 87ac0b6df..ae4e7cf93 100644
--- a/src/features/basicAuth/mainIpcHandler.js
+++ b/src/features/basicAuth/mainIpcHandler.js
@@ -1,4 +1,4 @@
1const debug = require('debug')('Franz:feature:basicAuth:main'); 1const debug = require('debug')('Ferdi:feature:basicAuth:main');
2 2
3export default function mainIpcHandler(mainWindow, authInfo) { 3export default function mainIpcHandler(mainWindow, authInfo) {
4 debug('Sending basic auth call', authInfo); 4 debug('Sending basic auth call', authInfo);
diff --git a/src/features/communityRecipes/index.js b/src/features/communityRecipes/index.js
index 4d050f90e..553b423f3 100644
--- a/src/features/communityRecipes/index.js
+++ b/src/features/communityRecipes/index.js
@@ -1,7 +1,7 @@
1import { reaction } from 'mobx'; 1import { reaction } from 'mobx';
2import { CommunityRecipesStore } from './store'; 2import { CommunityRecipesStore } from './store';
3 3
4const debug = require('debug')('Franz:feature:communityRecipes'); 4const debug = require('debug')('Ferdi:feature:communityRecipes');
5 5
6export const DEFAULT_SERVICE_LIMIT = 3; 6export const DEFAULT_SERVICE_LIMIT = 3;
7 7
@@ -19,7 +19,7 @@ export default function initCommunityRecipes(stores, actions) {
19 ), 19 ),
20 (isPremiumFeature) => { 20 (isPremiumFeature) => {
21 debug('Community recipes is premium feature: ', isPremiumFeature); 21 debug('Community recipes is premium feature: ', isPremiumFeature);
22 communityRecipesStore.isCommunityRecipesIncludedInCurrentPlan = isPremiumFeature; 22 communityRecipesStore.isCommunityRecipesIncludedInCurrentPlan = true;
23 }, 23 },
24 { 24 {
25 fireImmediately: true, 25 fireImmediately: true,
diff --git a/src/features/communityRecipes/store.js b/src/features/communityRecipes/store.js
index 4d45c3b33..3a60e5449 100644
--- a/src/features/communityRecipes/store.js
+++ b/src/features/communityRecipes/store.js
@@ -1,10 +1,10 @@
1import { computed, observable } from 'mobx'; 1import { computed, observable } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore'; 2import { FeatureStore } from '../utils/FeatureStore';
3 3
4const debug = require('debug')('Franz:feature:communityRecipes:store'); 4const debug = require('debug')('Ferdi:feature:communityRecipes:store');
5 5
6export class CommunityRecipesStore extends FeatureStore { 6export class CommunityRecipesStore extends FeatureStore {
7 @observable isCommunityRecipesIncludedInCurrentPlan = false; 7 @observable isCommunityRecipesIncludedInCurrentPlan = true;
8 8
9 start(stores, actions) { 9 start(stores, actions) {
10 debug('start'); 10 debug('start');
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js
index fcc27c75c..81f89bc52 100644
--- a/src/features/delayApp/Component.js
+++ b/src/features/delayApp/Component.js
@@ -5,9 +5,6 @@ import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6 6
7import { Button } from '@meetfranz/forms'; 7import { Button } from '@meetfranz/forms';
8import { gaEvent } from '../../lib/analytics';
9
10// import Button from '../../components/ui/Button';
11 8
12import { config } from '.'; 9import { config } from '.';
13import styles from './styles'; 10import styles from './styles';
@@ -32,7 +29,7 @@ const messages = defineMessages({
32 }, 29 },
33 text: { 30 text: {
34 id: 'feature.delayApp.text', 31 id: 'feature.delayApp.text',
35 defaultMessage: '!!!Franz will continue in {seconds} seconds.', 32 defaultMessage: '!!!Ferdi will continue in {seconds} seconds.',
36 }, 33 },
37}); 34});
38 35
@@ -78,12 +75,8 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class
78 75
79 if (!hadSubscription) { 76 if (!hadSubscription) {
80 actions.user.activateTrial({ planId: defaultTrialPlan }); 77 actions.user.activateTrial({ planId: defaultTrialPlan });
81
82 gaEvent('DelayApp', 'subscribe_click', 'Delay App Feature');
83 } else { 78 } else {
84 actions.ui.openSettings({ path: 'user' }); 79 actions.ui.openSettings({ path: 'user' });
85
86 gaEvent('DelayApp', 'subscribe_click', 'Delay App Feature');
87 } 80 }
88 } 81 }
89 82
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js
index 5b28fb7c8..51bd887a2 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -3,10 +3,9 @@ import moment from 'moment';
3import DelayAppComponent from './Component'; 3import DelayAppComponent from './Component';
4 4
5import { DEFAULT_FEATURES_CONFIG } from '../../config'; 5import { DEFAULT_FEATURES_CONFIG } from '../../config';
6import { gaEvent, gaPage } from '../../lib/analytics';
7import { getUserWorkspacesRequest } from '../workspaces/api'; 6import { getUserWorkspacesRequest } from '../workspaces/api';
8 7
9const debug = require('debug')('Franz:feature:delayApp'); 8const debug = require('debug')('Ferdi:feature:delayApp');
10 9
11export const config = { 10export const config = {
12 delayOffset: DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset, 11 delayOffset: DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset,
@@ -29,7 +28,7 @@ export default function init(stores) {
29 let shownAfterLaunch = false; 28 let shownAfterLaunch = false;
30 let timeLastDelay = moment(); 29 let timeLastDelay = moment();
31 30
32 window.franz.features.delayApp = { 31 window.ferdi.features.delayApp = {
33 state, 32 state,
34 }; 33 };
35 34
@@ -64,8 +63,6 @@ export default function init(stores) {
64 debug(`App will be delayed for ${config.delayDuration / 1000}s`); 63 debug(`App will be delayed for ${config.delayDuration / 1000}s`);
65 64
66 setVisibility(true); 65 setVisibility(true);
67 gaPage('/delayApp');
68 gaEvent('DelayApp', 'show', 'Delay App Feature');
69 66
70 67
71 setTimeout(() => { 68 setTimeout(() => {
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
new file mode 100644
index 000000000..797589e9b
--- /dev/null
+++ b/src/features/quickSwitch/Component.js
@@ -0,0 +1,337 @@
1import React, { Component, createRef } from 'react';
2import { remote } from 'electron';
3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react';
5import { reaction } from 'mobx';
6import injectSheet from 'react-jss';
7import { defineMessages, intlShape } from 'react-intl';
8import { Input } from '@meetfranz/forms';
9import { H1 } from '@meetfranz/ui';
10
11import Modal from '../../components/ui/Modal';
12import { state as ModalState } from '.';
13import ServicesStore from '../../stores/ServicesStore';
14
15const messages = defineMessages({
16 title: {
17 id: 'feature.quickSwitch.title',
18 defaultMessage: '!!!QuickSwitch',
19 },
20 search: {
21 id: 'feature.quickSwitch.search',
22 defaultMessage: '!!!Search...',
23 },
24 info: {
25 id: 'feature.quickSwitch.info',
26 defaultMessage: '!!!Select a service with TAB, ↑ and ↓. Open a service with ENTER.',
27 },
28});
29
30const styles = theme => ({
31 modal: {
32 width: '80%',
33 maxWidth: 600,
34 background: theme.styleTypes.primary.contrast,
35 color: theme.styleTypes.primary.accent,
36 paddingTop: 30,
37 },
38 headline: {
39 fontSize: 20,
40 marginBottom: 20,
41 marginTop: -27,
42 },
43 services: {
44 width: '100%',
45 maxHeight: '50vh',
46 overflow: 'scroll',
47 },
48 service: {
49 background: theme.styleTypes.primary.contrast,
50 color: theme.colorText,
51 borderColor: theme.styleTypes.primary.accent,
52 borderStyle: 'solid',
53 borderWidth: 1,
54 borderRadius: 6,
55 padding: '3px 25px',
56 marginBottom: 10,
57 display: 'flex',
58 alignItems: 'center',
59 '&:hover': {
60 background: theme.styleTypes.primary.accent,
61 color: theme.styleTypes.primary.contrast,
62 cursor: 'pointer',
63 },
64 },
65 activeService: {
66 background: theme.styleTypes.primary.accent,
67 color: theme.styleTypes.primary.contrast,
68 cursor: 'pointer',
69 },
70 serviceIcon: {
71 width: 50,
72 height: 50,
73 paddingRight: 20,
74 objectFit: 'contain',
75 },
76});
77
78export default @injectSheet(styles) @inject('stores', 'actions') @observer class QuickSwitchModal extends Component {
79 static propTypes = {
80 classes: PropTypes.object.isRequired,
81 };
82
83 static contextTypes = {
84 intl: intlShape,
85 };
86
87 state = {
88 selected: 0,
89 search: '',
90 wasPrevVisible: false,
91 }
92
93 ARROW_DOWN = 40;
94
95 ARROW_UP = 38;
96
97 ENTER = 13;
98
99 TAB = 9;
100
101 inputRef = createRef();
102
103 serviceElements = {};
104
105 constructor(props) {
106 super(props);
107
108 this._handleKeyDown = this._handleKeyDown.bind(this);
109 this._handleSearchUpdate = this._handleSearchUpdate.bind(this);
110 this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
111 this.openService = this.openService.bind(this);
112
113 reaction(
114 () => ModalState.isModalVisible,
115 this._handleVisibilityChange,
116 );
117 }
118
119 // Add global keydown listener when component mounts
120 componentDidMount() {
121 document.addEventListener('keydown', this._handleKeyDown);
122 }
123
124 // Remove global keydown listener when component unmounts
125 componentWillUnmount() {
126 document.removeEventListener('keydown', this._handleKeyDown);
127 }
128
129 // Get currently shown services
130 services() {
131 let services = [];
132 if (this.state.search) {
133 // Apply simple search algorythm to list of all services
134 services = this.props.stores.services.allDisplayed;
135 services = services.filter(service => service.name.toLowerCase().includes(this.state.search.toLowerCase()));
136 } else {
137 // Add last used services to services array
138 for (const service of this.props.stores.services.lastUsedServices) {
139 if (this.props.stores.services.one(service)) {
140 services.push(
141 this.props.stores.services.one(service),
142 );
143 }
144 }
145
146 // Add all other services in the default order
147 for (const service of this.props.stores.services.allDisplayed) {
148 if (!services.includes(service)) {
149 services.push(service);
150 }
151 }
152 }
153
154 return services;
155 }
156
157 openService(index) {
158 // Open service
159 const service = this.services()[index];
160 this.props.actions.service.setActive({ serviceId: service.id });
161
162 // Reset and close modal
163 this.setState({
164 search: '',
165 });
166 this.close();
167 }
168
169 // Change the selected service
170 // factor should be -1 or 1
171 changeSelected(factor) {
172 this.setState((state) => {
173 let newSelected = state.selected + factor;
174 const services = this.services().length;
175
176 // Roll around when on edge of list
177 if (state.selected < 1 && factor === -1) {
178 newSelected = services - 1;
179 } else if ((state.selected >= (services - 1)) && factor === 1) {
180 newSelected = 0;
181 }
182
183 // Make sure new selection is visible
184 const serviceElement = this.serviceElements[newSelected];
185 if (serviceElement) {
186 serviceElement.scrollIntoViewIfNeeded(false);
187 }
188
189
190 return {
191 selected: newSelected,
192 };
193 });
194 }
195
196 // Handle global key presses to change the selection
197 _handleKeyDown(event) {
198 if (ModalState.isModalVisible) {
199 switch (event.keyCode) {
200 case this.ARROW_DOWN:
201 this.changeSelected(1);
202 break;
203 case this.TAB:
204 this.changeSelected(1);
205 break;
206 case this.ARROW_UP:
207 this.changeSelected(-1);
208 break;
209 case this.ENTER:
210 this.openService(this.state.selected);
211 break;
212 default:
213 break;
214 }
215 }
216 }
217
218 // Handle update of the search query
219 _handleSearchUpdate(evt) {
220 this.setState({
221 search: evt.target.value,
222 });
223 }
224
225 _handleVisibilityChange() {
226 const { isModalVisible } = ModalState;
227
228 if (isModalVisible && !this.state.wasPrevVisible) {
229 // Set focus back on current window if its in a service
230 // TODO: Find a way to gain back focus
231 remote.getCurrentWindow().blurWebView();
232 remote.getCurrentWindow().webContents.focus();
233
234 // The input "focus" attribute will only work on first modal open
235 // Manually add focus to the input element
236 // Wrapped inside timeout to let the modal render first
237 setTimeout(() => {
238 if (this.inputRef.current) {
239 this.inputRef.current.getElementsByTagName('input')[0].focus();
240 }
241 }, 10);
242
243 this.setState({
244 wasPrevVisible: true,
245 });
246 } else if (!isModalVisible && this.state.wasPrevVisible) {
247 // Manually blur focus from the input element to prevent
248 // search query change when modal not visible
249 setTimeout(() => {
250 if (this.inputRef.current) {
251 this.inputRef.current.getElementsByTagName('input')[0].blur();
252 }
253 }, 100);
254
255 this.setState({
256 wasPrevVisible: false,
257 });
258 }
259 }
260
261 // Close this modal
262 close() {
263 ModalState.isModalVisible = false;
264 }
265
266 render() {
267 const { isModalVisible } = ModalState;
268
269 const {
270 openService,
271 } = this;
272
273 const {
274 classes,
275 } = this.props;
276
277 const services = this.services();
278
279 const { intl } = this.context;
280
281 return (
282 <Modal
283 isOpen={isModalVisible}
284 className={`${classes.modal} quick-switch`}
285 shouldCloseOnOverlayClick
286 close={this.close.bind(this)}
287 >
288 <H1 className={classes.headline}>
289 {intl.formatMessage(messages.title)}
290 </H1>
291 <div ref={this.inputRef}>
292 <Input
293 placeholder={intl.formatMessage(messages.search)}
294 focus
295 value={this.state.search}
296 onChange={this._handleSearchUpdate}
297 />
298 </div>
299
300 <div className={classes.services}>
301 { services.map((service, index) => (
302 <div
303 className={`${classes.service} ${this.state.selected === index ? `${classes.activeService} active` : ''} service`}
304 onClick={() => openService(index)}
305 key={service.id}
306 ref={(el) => {
307 this.serviceElements[index] = el;
308 }}
309 >
310 <img
311 src={service.icon}
312 className={classes.serviceIcon}
313 alt={service.recipe.name}
314 />
315 <div>
316 { service.name }
317 </div>
318 </div>
319 ))}
320 </div>
321
322 <p>{intl.formatMessage(messages.info)}</p>
323 </Modal>
324 );
325 }
326}
327
328QuickSwitchModal.wrappedComponent.propTypes = {
329 stores: PropTypes.shape({
330 services: PropTypes.instanceOf(ServicesStore).isRequired,
331 }).isRequired,
332 actions: PropTypes.shape({
333 service: PropTypes.shape({
334 setActive: PropTypes.func.isRequired,
335 }).isRequired,
336 }).isRequired,
337};
diff --git a/src/features/quickSwitch/index.js b/src/features/quickSwitch/index.js
new file mode 100644
index 000000000..c57fad366
--- /dev/null
+++ b/src/features/quickSwitch/index.js
@@ -0,0 +1,24 @@
1import { observable } from 'mobx';
2
3export { default as Component } from './Component';
4
5const debug = require('debug')('Ferdi:feature:quickSwitch');
6
7const defaultState = {
8 isModalVisible: false,
9};
10
11export const state = observable(defaultState);
12
13export default function initialize() {
14 debug('Initialize quickSwitch feature');
15
16 function showModal() {
17 state.isModalVisible = true;
18 }
19
20 window.ferdi.features.quickSwitch = {
21 state,
22 showModal,
23 };
24}
diff --git a/src/features/serviceLimit/components/LimitReachedInfobox.js b/src/features/serviceLimit/components/LimitReachedInfobox.js
index 19285a4eb..83aec4c40 100644
--- a/src/features/serviceLimit/components/LimitReachedInfobox.js
+++ b/src/features/serviceLimit/components/LimitReachedInfobox.js
@@ -5,8 +5,6 @@ import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import { Infobox } from '@meetfranz/ui'; 6import { Infobox } from '@meetfranz/ui';
7 7
8import { gaEvent } from '../../../lib/analytics';
9
10const messages = defineMessages({ 8const messages = defineMessages({
11 limitReached: { 9 limitReached: {
12 id: 'feature.serviceLimit.limitReached', 10 id: 'feature.serviceLimit.limitReached',
@@ -67,7 +65,6 @@ class LimitReachedInfobox extends Component {
67 ctaLabel={intl.formatMessage(messages.action)} 65 ctaLabel={intl.formatMessage(messages.action)}
68 ctaOnClick={() => { 66 ctaOnClick={() => {
69 actions.ui.openSettings({ path: 'user' }); 67 actions.ui.openSettings({ path: 'user' });
70 gaEvent('Service Limit', 'upgrade', 'Upgrade account');
71 }} 68 }}
72 > 69 >
73 {intl.formatMessage(messages.limitReached, { amount: serviceLimit.serviceCount, limit: serviceLimit.serviceLimit })} 70 {intl.formatMessage(messages.limitReached, { amount: serviceLimit.serviceCount, limit: serviceLimit.serviceLimit })}
diff --git a/src/features/serviceLimit/index.js b/src/features/serviceLimit/index.js
index 92ad8bb98..fa93bb615 100644
--- a/src/features/serviceLimit/index.js
+++ b/src/features/serviceLimit/index.js
@@ -1,7 +1,7 @@
1import { reaction } from 'mobx'; 1import { reaction } from 'mobx';
2import { ServiceLimitStore } from './store'; 2import { ServiceLimitStore } from './store';
3 3
4const debug = require('debug')('Franz:feature:serviceLimit'); 4const debug = require('debug')('Ferdi:feature:serviceLimit');
5 5
6export const DEFAULT_SERVICE_LIMIT = 3; 6export const DEFAULT_SERVICE_LIMIT = 3;
7 7
diff --git a/src/features/serviceLimit/store.js b/src/features/serviceLimit/store.js
index 9836c5f51..6510e2872 100644
--- a/src/features/serviceLimit/store.js
+++ b/src/features/serviceLimit/store.js
@@ -2,7 +2,7 @@ import { computed, observable } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore'; 2import { FeatureStore } from '../utils/FeatureStore';
3import { DEFAULT_SERVICE_LIMIT } from '.'; 3import { DEFAULT_SERVICE_LIMIT } from '.';
4 4
5const debug = require('debug')('Franz:feature:serviceLimit:store'); 5const debug = require('debug')('Ferdi:feature:serviceLimit:store');
6 6
7export class ServiceLimitStore extends FeatureStore { 7export class ServiceLimitStore extends FeatureStore {
8 @observable isServiceLimitEnabled = false; 8 @observable isServiceLimitEnabled = false;
@@ -12,7 +12,7 @@ export class ServiceLimitStore extends FeatureStore {
12 this.stores = stores; 12 this.stores = stores;
13 this.actions = actions; 13 this.actions = actions;
14 14
15 this.isServiceLimitEnabled = true; 15 this.isServiceLimitEnabled = false;
16 } 16 }
17 17
18 stop() { 18 stop() {
@@ -22,9 +22,10 @@ export class ServiceLimitStore extends FeatureStore {
22 } 22 }
23 23
24 @computed get userHasReachedServiceLimit() { 24 @computed get userHasReachedServiceLimit() {
25 if (!this.isServiceLimitEnabled) return false; 25 return false;
26 // if (!this.isServiceLimitEnabled) return false;
26 27
27 return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit; 28 // return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit;
28 } 29 }
29 30
30 @computed get serviceLimit() { 31 @computed get serviceLimit() {
diff --git a/src/features/serviceProxy/index.js b/src/features/serviceProxy/index.js
index 55c600de4..e9a01b156 100644
--- a/src/features/serviceProxy/index.js
+++ b/src/features/serviceProxy/index.js
@@ -1,25 +1,25 @@
1import { autorun, observable } from 'mobx'; 1import { autorun, observable } from 'mobx';
2import { remote } from 'electron'; 2import { remote } from 'electron';
3 3
4import { DEFAULT_FEATURES_CONFIG } from '../../config'; 4// import { DEFAULT_FEATURES_CONFIG } from '../../config';
5 5
6const { session } = remote; 6const { session } = remote;
7 7
8const debug = require('debug')('Franz:feature:serviceProxy'); 8const debug = require('debug')('Ferdi:feature:serviceProxy');
9 9
10export const config = observable({ 10export const config = observable({
11 isEnabled: DEFAULT_FEATURES_CONFIG.isServiceProxyEnabled, 11 isEnabled: true,
12 isPremium: DEFAULT_FEATURES_CONFIG.isServiceProxyIncludedInCurrentPlan, 12 isPremium: true,
13}); 13});
14 14
15export default function init(stores) { 15export default function init(stores) {
16 debug('Initializing `serviceProxy` feature'); 16 debug('Initializing `serviceProxy` feature');
17 17
18 autorun(() => { 18 autorun(() => {
19 const { isServiceProxyEnabled, isServiceProxyIncludedInCurrentPlan } = stores.features.features; 19 // const { isServiceProxyEnabled, isServiceProxyIncludedInCurrentPlan } = stores.features.features;
20 20
21 config.isEnabled = isServiceProxyEnabled !== undefined ? isServiceProxyEnabled : DEFAULT_FEATURES_CONFIG.isServiceProxyEnabled; 21 config.isEnabled = true;
22 config.isIncludedInCurrentPlan = isServiceProxyIncludedInCurrentPlan !== undefined ? isServiceProxyIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isServiceProxyIncludedInCurrentPlan; 22 config.isIncludedInCurrentPlan = true;
23 23
24 const services = stores.services.enabled; 24 const services = stores.services.enabled;
25 const isPremiumUser = stores.user.data.isPremium; 25 const isPremiumUser = stores.user.data.isPremium;
diff --git a/src/features/settingsWS/index.js b/src/features/settingsWS/index.js
index 2064d2973..6711296da 100755
--- a/src/features/settingsWS/index.js
+++ b/src/features/settingsWS/index.js
@@ -1,7 +1,7 @@
1import { reaction } from 'mobx'; 1import { reaction } from 'mobx';
2import { SettingsWSStore } from './store'; 2import { SettingsWSStore } from './store';
3 3
4const debug = require('debug')('Franz:feature:settingsWS'); 4const debug = require('debug')('Ferdi:feature:settingsWS');
5 5
6export const settingsStore = new SettingsWSStore(); 6export const settingsStore = new SettingsWSStore();
7 7
diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js
index 167a70d10..9100f33d1 100755
--- a/src/features/settingsWS/store.js
+++ b/src/features/settingsWS/store.js
@@ -6,7 +6,7 @@ import { FeatureStore } from '../utils/FeatureStore';
6import { createReactions } from '../../stores/lib/Reaction'; 6import { createReactions } from '../../stores/lib/Reaction';
7import { WS_API } from '../../environment'; 7import { WS_API } from '../../environment';
8 8
9const debug = require('debug')('Franz:feature:settingsWS:store'); 9const debug = require('debug')('Ferdi:feature:settingsWS:store');
10 10
11export class SettingsWSStore extends FeatureStore { 11export class SettingsWSStore extends FeatureStore {
12 stores = null; 12 stores = null;
diff --git a/src/features/shareFranz/Component.js b/src/features/shareFranz/Component.js
index a33315e17..405fb0ab5 100644
--- a/src/features/shareFranz/Component.js
+++ b/src/features/shareFranz/Component.js
@@ -11,17 +11,16 @@ import {
11} from '@mdi/js'; 11} from '@mdi/js';
12import Modal from '../../components/ui/Modal'; 12import Modal from '../../components/ui/Modal';
13import { state } from '.'; 13import { state } from '.';
14import { gaEvent } from '../../lib/analytics';
15import ServicesStore from '../../stores/ServicesStore'; 14import ServicesStore from '../../stores/ServicesStore';
16 15
17const messages = defineMessages({ 16const messages = defineMessages({
18 headline: { 17 headline: {
19 id: 'feature.shareFranz.headline', 18 id: 'feature.shareFranz.headline',
20 defaultMessage: '!!!Franz is better together!', 19 defaultMessage: '!!!Ferdi is better together!',
21 }, 20 },
22 text: { 21 text: {
23 id: 'feature.shareFranz.text', 22 id: 'feature.shareFranz.text',
24 defaultMessage: '!!!Tell your friends and colleagues how awesome Franz is and help us to spread the word.', 23 defaultMessage: '!!!Tell your friends and colleagues how awesome Ferdi is and help us to spread the word.',
25 }, 24 },
26 actionsEmail: { 25 actionsEmail: {
27 id: 'feature.shareFranz.action.email', 26 id: 'feature.shareFranz.action.email',
@@ -132,9 +131,6 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
132 icon={mdiEmail} 131 icon={mdiEmail}
133 href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareTextEmail, { count: serviceCount })}}`} 132 href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareTextEmail, { count: serviceCount })}}`}
134 target="_blank" 133 target="_blank"
135 onClick={() => {
136 gaEvent('Share Franz', 'share', 'Share via email');
137 }}
138 /> 134 />
139 <Button 135 <Button
140 label={intl.formatMessage(messages.actionsFacebook)} 136 label={intl.formatMessage(messages.actionsFacebook)}
@@ -142,9 +138,6 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
142 icon={mdiFacebookBox} 138 icon={mdiFacebookBox}
143 href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button" 139 href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button"
144 target="_blank" 140 target="_blank"
145 onClick={() => {
146 gaEvent('Share Franz', 'share', 'Share via Facebook');
147 }}
148 /> 141 />
149 <Button 142 <Button
150 label={intl.formatMessage(messages.actionsTwitter)} 143 label={intl.formatMessage(messages.actionsTwitter)}
@@ -152,9 +145,6 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
152 icon={mdiTwitter} 145 icon={mdiTwitter}
153 href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareTextTwitter, { count: serviceCount })}`} 146 href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareTextTwitter, { count: serviceCount })}`}
154 target="_blank" 147 target="_blank"
155 onClick={() => {
156 gaEvent('Share Franz', 'share', 'Share via Twitter');
157 }}
158 /> 148 />
159 </div> 149 </div>
160 </Modal> 150 </Modal>
diff --git a/src/features/shareFranz/index.js b/src/features/shareFranz/index.js
index a39d7a6e6..04e3684ae 100644
--- a/src/features/shareFranz/index.js
+++ b/src/features/shareFranz/index.js
@@ -2,12 +2,11 @@ import { observable, reaction } from 'mobx';
2import ms from 'ms'; 2import ms from 'ms';
3 3
4import { state as delayAppState } from '../delayApp'; 4import { state as delayAppState } from '../delayApp';
5import { gaEvent, gaPage } from '../../lib/analytics';
6import { planSelectionStore } from '../planSelection'; 5import { planSelectionStore } from '../planSelection';
7 6
8export { default as Component } from './Component'; 7export { default as Component } from './Component';
9 8
10const debug = require('debug')('Franz:feature:shareFranz'); 9const debug = require('debug')('Ferdi:feature:shareFranz');
11 10
12const defaultState = { 11const defaultState = {
13 isModalVisible: false, 12 isModalVisible: false,
@@ -17,19 +16,16 @@ const defaultState = {
17export const state = observable(defaultState); 16export const state = observable(defaultState);
18 17
19export default function initialize(stores) { 18export default function initialize(stores) {
20 debug('Initialize shareFranz feature'); 19 debug('Initialize shareFerdi feature');
21 20
22 window.franz.features.shareFranz = { 21 window.ferdi.features.shareFerdi = {
23 state, 22 state,
24 }; 23 };
25 24
26 function showModal() { 25 function showModal() {
27 debug('Showing share window'); 26 debug('Would have showed share window');
28 27
29 state.isModalVisible = true; 28 // state.isModalVisible = true;
30
31 gaEvent('Share Franz', 'show');
32 gaPage('/share-modal');
33 } 29 }
34 30
35 reaction( 31 reaction(
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
index fd8bc738a..6a393e250 100644
--- a/src/features/spellchecker/index.js
+++ b/src/features/spellchecker/index.js
@@ -2,26 +2,26 @@ import { autorun, observable } from 'mobx';
2 2
3import { DEFAULT_FEATURES_CONFIG } from '../../config'; 3import { DEFAULT_FEATURES_CONFIG } from '../../config';
4 4
5const debug = require('debug')('Franz:feature:spellchecker'); 5const debug = require('debug')('Ferdi:feature:spellchecker');
6 6
7export const config = observable({ 7export const config = observable({
8 isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan, 8 isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan,
9}); 9});
10 10
11export default function init(stores) { 11export default function init() {
12 debug('Initializing `spellchecker` feature'); 12 debug('Initializing `spellchecker` feature');
13 13
14 autorun(() => { 14 autorun(() => {
15 const { isSpellcheckerIncludedInCurrentPlan } = stores.features.features; 15 // const { isSpellcheckerIncludedInCurrentPlan } = stores.features.features;
16 16
17 config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan; 17 // config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan;
18 18
19 if (!stores.user.data.isPremium && !config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) { 19 // if (!stores.user.data.isPremium && config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) {
20 debug('Override settings.spellcheckerEnabled flag to false'); 20 // debug('Override settings.spellcheckerEnabled flag to false');
21 21
22 Object.assign(stores.settings.app, { 22 // Object.assign(stores.settings.app, {
23 enableSpellchecking: false, 23 // enableSpellchecking: false,
24 }); 24 // });
25 } 25 // }
26 }); 26 });
27} 27}
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index f24c0b044..e9b1963f7 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -1,12 +1,14 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer, inject } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import Webview from 'react-electron-web-view'; 5import Webview from 'react-electron-web-view';
6import { Icon } from '@meetfranz/ui'; 6import { Icon } from '@meetfranz/ui';
7import { defineMessages, intlShape } from 'react-intl'; 7import { defineMessages, intlShape } from 'react-intl';
8 8
9import { mdiCheckAll } from '@mdi/js'; 9import { mdiCheckAll } from '@mdi/js';
10import SettingsStore from '../../../stores/SettingsStore';
11
10import * as environment from '../../../environment'; 12import * as environment from '../../../environment';
11import Appear from '../../../components/ui/effects/Appear'; 13import Appear from '../../../components/ui/effects/Appear';
12import UpgradeButton from '../../../components/ui/UpgradeButton'; 14import UpgradeButton from '../../../components/ui/UpgradeButton';
@@ -77,7 +79,7 @@ const styles = theme => ({
77 }, 79 },
78}); 80});
79 81
80@injectSheet(styles) @observer 82@injectSheet(styles) @inject('stores') @observer
81class TodosWebview extends Component { 83class TodosWebview extends Component {
82 static propTypes = { 84 static propTypes = {
83 classes: PropTypes.object.isRequired, 85 classes: PropTypes.object.isRequired,
@@ -88,6 +90,9 @@ class TodosWebview extends Component {
88 width: PropTypes.number.isRequired, 90 width: PropTypes.number.isRequired,
89 minWidth: PropTypes.number.isRequired, 91 minWidth: PropTypes.number.isRequired,
90 isTodosIncludedInCurrentPlan: PropTypes.bool.isRequired, 92 isTodosIncludedInCurrentPlan: PropTypes.bool.isRequired,
93 stores: PropTypes.shape({
94 settings: PropTypes.instanceOf(SettingsStore).isRequired,
95 }).isRequired,
91 }; 96 };
92 97
93 state = { 98 state = {
@@ -178,6 +183,7 @@ class TodosWebview extends Component {
178 classes, 183 classes,
179 isVisible, 184 isVisible,
180 isTodosIncludedInCurrentPlan, 185 isTodosIncludedInCurrentPlan,
186 stores,
181 } = this.props; 187 } = this.props;
182 188
183 const { 189 const {
@@ -217,7 +223,7 @@ class TodosWebview extends Component {
217 partition="persist:todos" 223 partition="persist:todos"
218 preload="./features/todos/preload.js" 224 preload="./features/todos/preload.js"
219 ref={(webview) => { this.webview = webview ? webview.view : null; }} 225 ref={(webview) => { this.webview = webview ? webview.view : null; }}
220 src={environment.TODOS_FRONTEND} 226 src={stores.settings.all.app.todoServer || environment.TODOS_FRONTEND}
221 /> 227 />
222 ) : ( 228 ) : (
223 <Appear> 229 <Appear>
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js
index a5da0b014..bc05a587d 100644
--- a/src/features/todos/containers/TodosScreen.js
+++ b/src/features/todos/containers/TodosScreen.js
@@ -25,7 +25,7 @@ class TodosScreen extends Component {
25 width={todosStore.width} 25 width={todosStore.width}
26 minWidth={TODOS_MIN_WIDTH} 26 minWidth={TODOS_MIN_WIDTH}
27 resize={width => todoActions.resize({ width })} 27 resize={width => todoActions.resize({ width })}
28 isTodosIncludedInCurrentPlan={this.props.stores.features.features.isTodosIncludedInCurrentPlan || false} 28 isTodosIncludedInCurrentPlan
29 /> 29 />
30 </ErrorBoundary> 30 </ErrorBoundary>
31 ); 31 );
diff --git a/src/features/todos/index.js b/src/features/todos/index.js
index 7388aebaf..9f355e9ba 100644
--- a/src/features/todos/index.js
+++ b/src/features/todos/index.js
@@ -1,7 +1,7 @@
1import { reaction } from 'mobx'; 1import { reaction } from 'mobx';
2import TodoStore from './store'; 2import TodoStore from './store';
3 3
4const debug = require('debug')('Franz:feature:todos'); 4const debug = require('debug')('Ferdi:feature:todos');
5 5
6export const GA_CATEGORY_TODOS = 'Todos'; 6export const GA_CATEGORY_TODOS = 'Todos';
7 7
diff --git a/src/features/todos/preload.js b/src/features/todos/preload.js
index 6e38a2ef3..d1838e0d6 100644
--- a/src/features/todos/preload.js
+++ b/src/features/todos/preload.js
@@ -1,13 +1,13 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { IPC } from './constants'; 2import { IPC } from './constants';
3 3
4const debug = require('debug')('Franz:feature:todos:preload'); 4const debug = require('debug')('Ferdi:feature:todos:preload');
5 5
6debug('Preloading Todos Webview'); 6debug('Preloading Todos Webview');
7 7
8let hostMessageListener = () => {}; 8let hostMessageListener = () => {};
9 9
10window.franz = { 10window.ferdi = {
11 onInitialize(ipcHostMessageListener) { 11 onInitialize(ipcHostMessageListener) {
12 hostMessageListener = ipcHostMessageListener; 12 hostMessageListener = ipcHostMessageListener;
13 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' }); 13 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' });
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index 4480b2545..a05203a04 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -16,7 +16,7 @@ import {
16import { IPC } from './constants'; 16import { IPC } from './constants';
17import { state as delayAppState } from '../delayApp'; 17import { state as delayAppState } from '../delayApp';
18 18
19const debug = require('debug')('Franz:feature:todos:store'); 19const debug = require('debug')('Ferdi:feature:todos:store');
20 20
21export default class TodoStore extends FeatureStore { 21export default class TodoStore extends FeatureStore {
22 @observable isFeatureEnabled = false; 22 @observable isFeatureEnabled = false;
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js
index cada01a6f..31168a397 100644
--- a/src/features/webControls/containers/WebControlsScreen.js
+++ b/src/features/webControls/containers/WebControlsScreen.js
@@ -32,6 +32,7 @@ class WebControlsScreen extends Component {
32 this.autorunDisposer = autorun(() => { 32 this.autorunDisposer = autorun(() => {
33 if (service.isAttached) { 33 if (service.isAttached) {
34 this.webview = service.webview; 34 this.webview = service.webview;
35 this.url = this.webview.getURL();
35 36
36 URL_EVENTS.forEach((event) => { 37 URL_EVENTS.forEach((event) => {
37 this.webview.addEventListener(event, (e) => { 38 this.webview.addEventListener(event, (e) => {
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js
index 0ec20c9ea..30fbd84be 100644
--- a/src/features/workspaces/api.js
+++ b/src/features/workspaces/api.js
@@ -1,14 +1,14 @@
1import { pick } from 'lodash'; 1import { pick } from 'lodash';
2import { sendAuthRequest } from '../../api/utils/auth'; 2import { sendAuthRequest } from '../../api/utils/auth';
3import { API, API_VERSION } from '../../environment';
4import Request from '../../stores/lib/Request'; 3import Request from '../../stores/lib/Request';
5import Workspace from './models/Workspace'; 4import Workspace from './models/Workspace';
5import apiBase from '../../api/apiBase';
6 6
7const debug = require('debug')('Franz:feature:workspaces:api'); 7const debug = require('debug')('Ferdi:feature:workspaces:api');
8 8
9export const workspaceApi = { 9export const workspaceApi = {
10 getUserWorkspaces: async () => { 10 getUserWorkspaces: async () => {
11 const url = `${API}/${API_VERSION}/workspace`; 11 const url = `${apiBase()}/workspace`;
12 debug('getUserWorkspaces GET', url); 12 debug('getUserWorkspaces GET', url);
13 const result = await sendAuthRequest(url, { method: 'GET' }); 13 const result = await sendAuthRequest(url, { method: 'GET' });
14 debug('getUserWorkspaces RESULT', result); 14 debug('getUserWorkspaces RESULT', result);
@@ -18,7 +18,7 @@ export const workspaceApi = {
18 }, 18 },
19 19
20 createWorkspace: async (name) => { 20 createWorkspace: async (name) => {
21 const url = `${API}/${API_VERSION}/workspace`; 21 const url = `${apiBase()}/workspace`;
22 const options = { 22 const options = {
23 method: 'POST', 23 method: 'POST',
24 body: JSON.stringify({ name }), 24 body: JSON.stringify({ name }),
@@ -31,7 +31,7 @@ export const workspaceApi = {
31 }, 31 },
32 32
33 deleteWorkspace: async (workspace) => { 33 deleteWorkspace: async (workspace) => {
34 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; 34 const url = `${apiBase()}/workspace/${workspace.id}`;
35 debug('deleteWorkspace DELETE', url); 35 debug('deleteWorkspace DELETE', url);
36 const result = await sendAuthRequest(url, { method: 'DELETE' }); 36 const result = await sendAuthRequest(url, { method: 'DELETE' });
37 debug('deleteWorkspace RESULT', result); 37 debug('deleteWorkspace RESULT', result);
@@ -40,7 +40,7 @@ export const workspaceApi = {
40 }, 40 },
41 41
42 updateWorkspace: async (workspace) => { 42 updateWorkspace: async (workspace) => {
43 const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; 43 const url = `${apiBase()}/workspace/${workspace.id}`;
44 const options = { 44 const options = {
45 method: 'PUT', 45 method: 'PUT',
46 body: JSON.stringify(pick(workspace, ['name', 'services'])), 46 body: JSON.stringify(pick(workspace, ['name', 'services'])),
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js
index cddbb2b04..15b97121d 100644
--- a/src/features/workspaces/components/CreateWorkspaceForm.js
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -6,8 +6,7 @@ import { Input, Button } from '@meetfranz/forms';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7import Form from '../../../lib/Form'; 7import Form from '../../../lib/Form';
8import { required } from '../../../helpers/validation-helpers'; 8import { required } from '../../../helpers/validation-helpers';
9import { gaEvent } from '../../../lib/analytics'; 9import { workspaceStore } from '../index';
10import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index';
11 10
12const messages = defineMessages({ 11const messages = defineMessages({
13 submitButton: { 12 submitButton: {
@@ -66,7 +65,6 @@ class CreateWorkspaceForm extends Component {
66 const { onSubmit } = this.props; 65 const { onSubmit } = this.props;
67 const values = f.values(); 66 const values = f.values();
68 onSubmit(values); 67 onSubmit(values);
69 gaEvent(GA_CATEGORY_WORKSPACES, 'create', values.name);
70 }, 68 },
71 }); 69 });
72 } 70 }
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
index e602ebd5a..b3551a7b9 100644
--- a/src/features/workspaces/components/EditWorkspaceForm.js
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -12,8 +12,10 @@ import Form from '../../../lib/Form';
12import { required } from '../../../helpers/validation-helpers'; 12import { required } from '../../../helpers/validation-helpers';
13import WorkspaceServiceListItem from './WorkspaceServiceListItem'; 13import WorkspaceServiceListItem from './WorkspaceServiceListItem';
14import Request from '../../../stores/lib/Request'; 14import Request from '../../../stores/lib/Request';
15import { gaEvent } from '../../../lib/analytics'; 15
16import { GA_CATEGORY_WORKSPACES } from '../index'; 16import { KEEP_WS_LOADED_USID } from '../../../config';
17
18import Toggle from '../../../components/ui/Toggle';
17 19
18const messages = defineMessages({ 20const messages = defineMessages({
19 buttonDelete: { 21 buttonDelete: {
@@ -32,6 +34,14 @@ const messages = defineMessages({
32 id: 'settings.workspace.form.yourWorkspaces', 34 id: 'settings.workspace.form.yourWorkspaces',
33 defaultMessage: '!!!Your workspaces', 35 defaultMessage: '!!!Your workspaces',
34 }, 36 },
37 keepLoaded: {
38 id: 'settings.workspace.form.keepLoaded',
39 defaultMessage: '!!!Keep this workspace loaded*',
40 },
41 keepLoadedInfo: {
42 id: 'settings.workspace.form.keepLoadedInfo',
43 defaultMessage: '!!!*This option will be overwritten by the global "Keep all workspaces loaded" option.',
44 },
35 servicesInWorkspaceHeadline: { 45 servicesInWorkspaceHeadline: {
36 id: 'settings.workspace.form.servicesInWorkspaceHeadline', 46 id: 'settings.workspace.form.servicesInWorkspaceHeadline',
37 defaultMessage: '!!!Services in this Workspace', 47 defaultMessage: '!!!Services in this Workspace',
@@ -53,6 +63,9 @@ const styles = () => ({
53 serviceList: { 63 serviceList: {
54 height: 'auto', 64 height: 'auto',
55 }, 65 },
66 keepLoadedInfo: {
67 marginBottom: '2rem !important',
68 },
56}); 69});
57 70
58@injectSheet(styles) @observer 71@injectSheet(styles) @observer
@@ -90,6 +103,11 @@ class EditWorkspaceForm extends Component {
90 value: workspace.name, 103 value: workspace.name,
91 validators: [required], 104 validators: [required],
92 }, 105 },
106 keepLoaded: {
107 label: intl.formatMessage(messages.keepLoaded),
108 value: workspace.services.includes(KEEP_WS_LOADED_USID),
109 default: false,
110 },
93 services: { 111 services: {
94 value: workspace.services.slice(), 112 value: workspace.services.slice(),
95 }, 113 },
@@ -103,7 +121,6 @@ class EditWorkspaceForm extends Component {
103 const { onSave } = this.props; 121 const { onSave } = this.props;
104 const values = f.values(); 122 const values = f.values();
105 onSave(values); 123 onSave(values);
106 gaEvent(GA_CATEGORY_WORKSPACES, 'save');
107 }, 124 },
108 onError: async () => {}, 125 onError: async () => {},
109 }); 126 });
@@ -112,7 +129,6 @@ class EditWorkspaceForm extends Component {
112 delete() { 129 delete() {
113 const { onDelete } = this.props; 130 const { onDelete } = this.props;
114 onDelete(); 131 onDelete();
115 gaEvent(GA_CATEGORY_WORKSPACES, 'delete');
116 } 132 }
117 133
118 toggleService(service) { 134 toggleService(service) {
@@ -155,6 +171,10 @@ class EditWorkspaceForm extends Component {
155 <div className="settings__body"> 171 <div className="settings__body">
156 <div className={classes.nameInput}> 172 <div className={classes.nameInput}>
157 <Input {...form.$('name').bind()} /> 173 <Input {...form.$('name').bind()} />
174 <Toggle field={form.$('keepLoaded')} />
175 <p className={classes.keepLoadedInfo}>
176 { intl.formatMessage(messages.keepLoadedInfo) }
177 </p>
158 </div> 178 </div>
159 <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2> 179 <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2>
160 <div className={classes.serviceList}> 180 <div className={classes.serviceList}>
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index 07100f5a1..baa94f6b3 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -10,8 +10,7 @@ import ReactTooltip from 'react-tooltip';
10import { mdiPlusBox, mdiSettings, mdiStar } from '@mdi/js'; 10import { mdiPlusBox, mdiSettings, mdiStar } from '@mdi/js';
11import WorkspaceDrawerItem from './WorkspaceDrawerItem'; 11import WorkspaceDrawerItem from './WorkspaceDrawerItem';
12import { workspaceActions } from '../actions'; 12import { workspaceActions } from '../actions';
13import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index'; 13import { workspaceStore } from '../index';
14import { gaEvent } from '../../../lib/analytics';
15 14
16const messages = defineMessages({ 15const messages = defineMessages({
17 headline: { 16 headline: {
@@ -158,7 +157,6 @@ class WorkspaceDrawer extends Component {
158 className={classes.workspacesSettingsButton} 157 className={classes.workspacesSettingsButton}
159 onClick={() => { 158 onClick={() => {
160 workspaceActions.openWorkspaceSettings(); 159 workspaceActions.openWorkspaceSettings();
161 gaEvent(GA_CATEGORY_WORKSPACES, 'settings', 'drawerHeadline');
162 }} 160 }}
163 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`} 161 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`}
164 > 162 >
@@ -180,7 +178,6 @@ class WorkspaceDrawer extends Component {
180 icon={mdiStar} 178 icon={mdiStar}
181 onClick={() => { 179 onClick={() => {
182 onUpgradeAccountClick(); 180 onUpgradeAccountClick();
183 gaEvent('User', 'upgrade', 'workspaceDrawer');
184 }} 181 }}
185 /> 182 />
186 ) : ( 183 ) : (
@@ -191,7 +188,6 @@ class WorkspaceDrawer extends Component {
191 icon={mdiPlusBox} 188 icon={mdiPlusBox}
192 onClick={() => { 189 onClick={() => {
193 workspaceActions.openWorkspaceSettings(); 190 workspaceActions.openWorkspaceSettings();
194 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta');
195 }} 191 }}
196 /> 192 />
197 )} 193 )}
@@ -203,7 +199,6 @@ class WorkspaceDrawer extends Component {
203 onClick={() => { 199 onClick={() => {
204 workspaceActions.deactivate(); 200 workspaceActions.deactivate();
205 workspaceActions.toggleWorkspaceDrawer(); 201 workspaceActions.toggleWorkspaceDrawer();
206 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
207 }} 202 }}
208 services={getServicesForWorkspace(null)} 203 services={getServicesForWorkspace(null)}
209 isActive={actualWorkspace == null} 204 isActive={actualWorkspace == null}
@@ -218,7 +213,6 @@ class WorkspaceDrawer extends Component {
218 if (actualWorkspace === workspace) return; 213 if (actualWorkspace === workspace) return;
219 workspaceActions.activate({ workspace }); 214 workspaceActions.activate({ workspace });
220 workspaceActions.toggleWorkspaceDrawer(); 215 workspaceActions.toggleWorkspaceDrawer();
221 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
222 }} 216 }}
223 onContextMenuEditClick={() => workspaceActions.edit({ workspace })} 217 onContextMenuEditClick={() => workspaceActions.edit({ workspace })}
224 services={getServicesForWorkspace(workspace)} 218 services={getServicesForWorkspace(workspace)}
@@ -229,7 +223,6 @@ class WorkspaceDrawer extends Component {
229 className={classes.addNewWorkspaceLabel} 223 className={classes.addNewWorkspaceLabel}
230 onClick={() => { 224 onClick={() => {
231 workspaceActions.openWorkspaceSettings(); 225 workspaceActions.openWorkspaceSettings();
232 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerAddLabel');
233 }} 226 }}
234 > 227 >
235 <Icon 228 <Icon
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index 4fb302be2..b499e02a4 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -47,7 +47,7 @@ const messages = defineMessages({
47 }, 47 },
48 workspaceFeatureHeadline: { 48 workspaceFeatureHeadline: {
49 id: 'settings.workspaces.workspaceFeatureHeadline', 49 id: 'settings.workspaces.workspaceFeatureHeadline',
50 defaultMessage: '!!!Less is More: Introducing Franz Workspaces', 50 defaultMessage: '!!!Less is More: Introducing Ferdi Workspaces',
51 }, 51 },
52}); 52});
53 53
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js
index 248b40131..7eaabc1ea 100644
--- a/src/features/workspaces/containers/EditWorkspaceScreen.js
+++ b/src/features/workspaces/containers/EditWorkspaceScreen.js
@@ -33,7 +33,9 @@ class EditWorkspaceScreen extends Component {
33 const { workspaceBeingEdited } = workspaceStore; 33 const { workspaceBeingEdited } = workspaceStore;
34 const { actions } = this.props; 34 const { actions } = this.props;
35 const workspace = new Workspace( 35 const workspace = new Workspace(
36 Object.assign({}, workspaceBeingEdited, values), 36 Object.assign({
37 saving: true,
38 }, workspaceBeingEdited, values),
37 ); 39 );
38 actions.workspaces.update({ workspace }); 40 actions.workspaces.update({ workspace });
39 }; 41 };
diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js
index ed3e52096..560b732ab 100644
--- a/src/features/workspaces/index.js
+++ b/src/features/workspaces/index.js
@@ -2,7 +2,7 @@ import { reaction } from 'mobx';
2import WorkspacesStore from './store'; 2import WorkspacesStore from './store';
3import { resetApiRequests } from './api'; 3import { resetApiRequests } from './api';
4 4
5const debug = require('debug')('Franz:feature:workspaces'); 5const debug = require('debug')('Ferdi:feature:workspaces');
6 6
7export const GA_CATEGORY_WORKSPACES = 'Workspaces'; 7export const GA_CATEGORY_WORKSPACES = 'Workspaces';
8export const DEFAULT_SETTING_KEEP_ALL_WORKSPACES_LOADED = false; 8export const DEFAULT_SETTING_KEEP_ALL_WORKSPACES_LOADED = false;
diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js
index 6c73d7095..77c4e05f4 100644
--- a/src/features/workspaces/models/Workspace.js
+++ b/src/features/workspaces/models/Workspace.js
@@ -1,5 +1,7 @@
1import { observable } from 'mobx'; 1import { observable } from 'mobx';
2 2
3import { KEEP_WS_LOADED_USID } from '../../../config';
4
3export default class Workspace { 5export default class Workspace {
4 id = null; 6 id = null;
5 7
@@ -19,7 +21,17 @@ export default class Workspace {
19 this.id = data.id; 21 this.id = data.id;
20 this.name = data.name; 22 this.name = data.name;
21 this.order = data.order; 23 this.order = data.order;
22 this.services.replace(data.services); 24
25 let services = data.services;
26 if (data.saving && data.keepLoaded) {
27 // Keep workspaces loaded
28 services.push(KEEP_WS_LOADED_USID);
29 } else if (data.saving && data.services.includes(KEEP_WS_LOADED_USID)) {
30 // Don't keep loaded
31 services = services.filter(e => e !== KEEP_WS_LOADED_USID);
32 }
33 this.services.replace(services);
34
23 this.userId = data.userId; 35 this.userId = data.userId;
24 } 36 }
25} 37}
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index 13227e033..5c90ff180 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -17,16 +17,18 @@ import { WORKSPACES_ROUTES } from './index';
17import { createReactions } from '../../stores/lib/Reaction'; 17import { createReactions } from '../../stores/lib/Reaction';
18import { createActionBindings } from '../utils/ActionBinding'; 18import { createActionBindings } from '../utils/ActionBinding';
19 19
20const debug = require('debug')('Franz:feature:workspaces:store'); 20import { KEEP_WS_LOADED_USID } from '../../config';
21
22const debug = require('debug')('Ferdi:feature:workspaces:store');
21 23
22export default class WorkspacesStore extends FeatureStore { 24export default class WorkspacesStore extends FeatureStore {
23 @observable isFeatureEnabled = false; 25 @observable isFeatureEnabled = true;
24 26
25 @observable isFeatureActive = false; 27 @observable isFeatureActive = false;
26 28
27 @observable isPremiumFeature = true; 29 @observable isPremiumFeature = false;
28 30
29 @observable isPremiumUpgradeRequired = true; 31 @observable isPremiumUpgradeRequired = false;
30 32
31 @observable activeWorkspace = null; 33 @observable activeWorkspace = null;
32 34
@@ -59,7 +61,8 @@ export default class WorkspacesStore extends FeatureStore {
59 } 61 }
60 62
61 @computed get isUserAllowedToUseFeature() { 63 @computed get isUserAllowedToUseFeature() {
62 return !this.isPremiumUpgradeRequired; 64 return true;
65 // return !this.isPremiumUpgradeRequired;
63 } 66 }
64 67
65 @computed get isAnyWorkspaceActive() { 68 @computed get isAnyWorkspaceActive() {
@@ -263,10 +266,10 @@ export default class WorkspacesStore extends FeatureStore {
263 }; 266 };
264 267
265 _setIsPremiumFeatureReaction = () => { 268 _setIsPremiumFeatureReaction = () => {
266 const { features } = this.stores; 269 // const { features } = this.stores;
267 const { isWorkspaceIncludedInCurrentPlan } = features.features; 270 // const { isWorkspaceIncludedInCurrentPlan } = features.features;
268 this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan; 271 // this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan;
269 this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan; 272 // this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan;
270 }; 273 };
271 274
272 _setWorkspaceBeingEditedReaction = () => { 275 _setWorkspaceBeingEditedReaction = () => {
@@ -331,7 +334,7 @@ export default class WorkspacesStore extends FeatureStore {
331 // Loop through all workspaces and remove invalid service ids (locally) 334 // Loop through all workspaces and remove invalid service ids (locally)
332 this.workspaces.forEach((workspace) => { 335 this.workspaces.forEach((workspace) => {
333 workspace.services.forEach((serviceId) => { 336 workspace.services.forEach((serviceId) => {
334 if (servicesHaveBeenLoaded && !services.one(serviceId)) { 337 if (servicesHaveBeenLoaded && !services.one(serviceId) && serviceId !== KEEP_WS_LOADED_USID) {
335 workspace.services.remove(serviceId); 338 workspace.services.remove(serviceId);
336 } 339 }
337 }); 340 });