aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/announcements/api.js6
-rw-r--r--src/features/announcements/components/AnnouncementScreen.js7
-rw-r--r--src/features/announcements/index.js2
-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.js294
-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/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
35 files changed, 444 insertions, 125 deletions
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js
index a581bd8de..a7995d6db 100644
--- a/src/features/announcements/api.js
+++ b/src/features/announcements/api.js
@@ -1,8 +1,8 @@
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'; 3import apiBase from '../../api/apiBase';
4 4
5const debug = require('debug')('Franz:feature:announcements:api'); 5const debug = require('debug')('Ferdi:feature:announcements:api');
6 6
7export const announcementsApi = { 7export const announcementsApi = {
8 async getCurrentVersion() { 8 async getCurrentVersion() {
@@ -21,7 +21,7 @@ export const announcementsApi = {
21 21
22 async getAnnouncement(version) { 22 async getAnnouncement(version) {
23 debug('fetching release announcement from api'); 23 debug('fetching release announcement from api');
24 const url = `${API}/${API_VERSION}/announcements/${version}`; 24 const url = `${apiBase()}/announcements/${version}`;
25 const response = await window.fetch(url, { method: 'GET' }); 25 const response = await window.fetch(url, { method: 'GET' });
26 if (!response.ok) return null; 26 if (!response.ok) return null;
27 return response.json(); 27 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..42823e74c 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
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 6344edb89..c61cb06c9 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 bd0395376..5cc6c9506 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -3,9 +3,8 @@ 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';
7 6
8const debug = require('debug')('Franz:feature:delayApp'); 7const debug = require('debug')('Ferdi:feature:delayApp');
9 8
10export const config = { 9export const config = {
11 delayOffset: DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset, 10 delayOffset: DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset,
@@ -28,7 +27,7 @@ export default function init(stores) {
28 let shownAfterLaunch = false; 27 let shownAfterLaunch = false;
29 let timeLastDelay = moment(); 28 let timeLastDelay = moment();
30 29
31 window.franz.features.delayApp = { 30 window.ferdi.features.delayApp = {
32 state, 31 state,
33 }; 32 };
34 33
@@ -57,8 +56,6 @@ export default function init(stores) {
57 debug(`App will be delayed for ${config.delayDuration / 1000}s`); 56 debug(`App will be delayed for ${config.delayDuration / 1000}s`);
58 57
59 setVisibility(true); 58 setVisibility(true);
60 gaPage('/delayApp');
61 gaEvent('DelayApp', 'show', 'Delay App Feature');
62 59
63 60
64 setTimeout(() => { 61 setTimeout(() => {
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
new file mode 100644
index 000000000..ddbdbe304
--- /dev/null
+++ b/src/features/quickSwitch/Component.js
@@ -0,0 +1,294 @@
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';
9
10import Modal from '../../components/ui/Modal';
11import { state as ModalState } from '.';
12import ServicesStore from '../../stores/ServicesStore';
13
14const messages = defineMessages({
15 search: {
16 id: 'feature.quickSwitch.search',
17 defaultMessage: '!!!Search...',
18 },
19 info: {
20 id: 'feature.quickSwitch.info',
21 defaultMessage: '!!!Select a service with TAB, ↑ and ↓. Open a service with ENTER.',
22 },
23});
24
25const styles = theme => ({
26 modal: {
27 width: '80%',
28 maxWidth: 600,
29 background: theme.styleTypes.primary.contrast,
30 color: theme.styleTypes.primary.accent,
31 paddingTop: 30,
32 },
33 services: {
34 width: '100%',
35 marginTop: 30,
36 },
37 service: {
38 background: theme.styleTypes.primary.contrast,
39 color: theme.colorText,
40 borderColor: theme.styleTypes.primary.accent,
41 borderStyle: 'solid',
42 borderWidth: 1,
43 borderRadius: 6,
44 padding: '3px 25px',
45 marginBottom: 10,
46 display: 'flex',
47 alignItems: 'center',
48 '&:hover': {
49 background: theme.styleTypes.primary.accent,
50 color: theme.styleTypes.primary.contrast,
51 cursor: 'pointer',
52 },
53 },
54 activeService: {
55 background: theme.styleTypes.primary.accent,
56 color: theme.styleTypes.primary.contrast,
57 cursor: 'pointer',
58 },
59 serviceIcon: {
60 width: 50,
61 height: 50,
62 paddingRight: 20,
63 objectFit: 'contain',
64 },
65});
66
67export default @injectSheet(styles) @inject('stores', 'actions') @observer class QuickSwitchModal extends Component {
68 static propTypes = {
69 classes: PropTypes.object.isRequired,
70 };
71
72 static contextTypes = {
73 intl: intlShape,
74 };
75
76 state = {
77 selected: 0,
78 search: '',
79 wasPrevVisible: false,
80 }
81
82 ARROW_DOWN = 40;
83
84 ARROW_UP = 38;
85
86 ENTER = 13;
87
88 TAB = 9;
89
90 inputRef = createRef();
91
92 constructor(props) {
93 super(props);
94
95 this._handleKeyDown = this._handleKeyDown.bind(this);
96 this._handleSearchUpdate = this._handleSearchUpdate.bind(this);
97 this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
98 this.openService = this.openService.bind(this);
99
100 reaction(
101 () => ModalState.isModalVisible,
102 this._handleVisibilityChange,
103 );
104 }
105
106 // Add global keydown listener when component mounts
107 componentDidMount() {
108 document.addEventListener('keydown', this._handleKeyDown);
109 }
110
111 // Remove global keydown listener when component unmounts
112 componentWillUnmount() {
113 document.removeEventListener('keydown', this._handleKeyDown);
114 }
115
116 // Get currently shown services
117 services() {
118 let services = this.props.stores.services.allDisplayed;
119 if (this.state.search) {
120 // Apply simple search algorythm
121 services = services.filter(service => service.name.toLowerCase().includes(this.state.search.toLowerCase()));
122 }
123
124 return services;
125 }
126
127 openService(index) {
128 // Open service
129 const service = this.services()[index];
130 this.props.actions.service.setActive({ serviceId: service.id });
131
132 // Reset and close modal
133 this.setState({
134 search: '',
135 });
136 this.close();
137 }
138
139 // Change the selected service
140 // factor should be -1 or 1
141 changeSelected(factor) {
142 this.setState((state) => {
143 let newSelected = state.selected + factor;
144 const services = this.services().length;
145
146 // Roll around when on edge of list
147 if (state.selected < 1 && factor === -1) {
148 newSelected = services - 1;
149 } else if ((state.selected >= (services - 1)) && factor === 1) {
150 newSelected = 0;
151 }
152
153 return {
154 selected: newSelected,
155 };
156 });
157 }
158
159 // Handle global key presses to change the selection
160 _handleKeyDown(event) {
161 if (ModalState.isModalVisible) {
162 switch (event.keyCode) {
163 case this.ARROW_DOWN:
164 this.changeSelected(1);
165 break;
166 case this.TAB:
167 this.changeSelected(1);
168 break;
169 case this.ARROW_UP:
170 this.changeSelected(-1);
171 break;
172 case this.ENTER:
173 this.openService(this.state.selected);
174 break;
175 default:
176 break;
177 }
178 }
179 }
180
181 // Handle update of the search query
182 _handleSearchUpdate(evt) {
183 this.setState({
184 search: evt.target.value,
185 });
186 }
187
188 _handleVisibilityChange() {
189 const { isModalVisible } = ModalState;
190
191 if (isModalVisible && !this.state.wasPrevVisible) {
192 // Set focus back on current window if its in a service
193 // TODO: Find a way to gain back focus
194 remote.getCurrentWindow().blurWebView();
195 remote.getCurrentWindow().webContents.focus();
196
197 // The input "focus" attribute will only work on first modal open
198 // Manually add focus to the input element
199 // Wrapped inside timeout to let the modal render first
200 setTimeout(() => {
201 if (this.inputRef.current) {
202 this.inputRef.current.getElementsByTagName('input')[0].focus();
203 }
204 }, 10);
205
206 this.setState({
207 wasPrevVisible: true,
208 });
209 } else if (!isModalVisible && this.state.wasPrevVisible) {
210 // Manually blur focus from the input element to prevent
211 // search query change when modal not visible
212 setTimeout(() => {
213 if (this.inputRef.current) {
214 this.inputRef.current.getElementsByTagName('input')[0].blur();
215 }
216 }, 100);
217
218 this.setState({
219 wasPrevVisible: false,
220 });
221 }
222 }
223
224 // Close this modal
225 close() {
226 ModalState.isModalVisible = false;
227 }
228
229 render() {
230 const { isModalVisible } = ModalState;
231
232 const {
233 openService,
234 } = this;
235
236 const {
237 classes,
238 } = this.props;
239
240 const services = this.services();
241
242 const { intl } = this.context;
243
244 return (
245 <Modal
246 isOpen={isModalVisible}
247 className={classes.modal}
248 shouldCloseOnOverlayClick
249 close={this.close.bind(this)}
250 >
251 <div ref={this.inputRef}>
252 <Input
253 placeholder={intl.formatMessage(messages.search)}
254 focus
255 value={this.state.search}
256 onChange={this._handleSearchUpdate}
257 />
258 </div>
259
260 <div className={classes.services}>
261 { services.map((service, index) => (
262 <div
263 className={`${classes.service} ${this.state.selected === index ? classes.activeService : ''}`}
264 onClick={() => openService(index)}
265 key={service.id}
266 >
267 <img
268 src={service.icon}
269 className={classes.serviceIcon}
270 alt={service.recipe.name}
271 />
272 <div>
273 { service.name }
274 </div>
275 </div>
276 ))}
277 </div>
278
279 <p>{intl.formatMessage(messages.info)}</p>
280 </Modal>
281 );
282 }
283}
284
285QuickSwitchModal.wrappedComponent.propTypes = {
286 stores: PropTypes.shape({
287 services: PropTypes.instanceOf(ServicesStore).isRequired,
288 }).isRequired,
289 actions: PropTypes.shape({
290 service: PropTypes.shape({
291 setActive: PropTypes.func.isRequired,
292 }).isRequired,
293 }).isRequired,
294};
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 87deacef4..217e926f9 100644
--- a/src/features/shareFranz/index.js
+++ b/src/features/shareFranz/index.js
@@ -2,11 +2,10 @@ 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';
6 5
7export { default as Component } from './Component'; 6export { default as Component } from './Component';
8 7
9const debug = require('debug')('Franz:feature:shareFranz'); 8const debug = require('debug')('Ferdi:feature:shareFranz');
10 9
11const defaultState = { 10const defaultState = {
12 isModalVisible: false, 11 isModalVisible: false,
@@ -16,19 +15,16 @@ const defaultState = {
16export const state = observable(defaultState); 15export const state = observable(defaultState);
17 16
18export default function initialize(stores) { 17export default function initialize(stores) {
19 debug('Initialize shareFranz feature'); 18 debug('Initialize shareFerdi feature');
20 19
21 window.franz.features.shareFranz = { 20 window.ferdi.features.shareFerdi = {
22 state, 21 state,
23 }; 22 };
24 23
25 function showModal() { 24 function showModal() {
26 debug('Showing share window'); 25 debug('Would have showed share window');
27 26
28 state.isModalVisible = true; 27 // state.isModalVisible = true;
29
30 gaEvent('Share Franz', 'show');
31 gaPage('/share-modal');
32 } 28 }
33 29
34 reaction( 30 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..35c102220 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) @observer @inject('stores')
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/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 ee6f8416c..e991b9909 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 } from '@mdi/js'; 10import { mdiPlusBox, mdiSettings } 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: {
@@ -155,7 +154,6 @@ class WorkspaceDrawer extends Component {
155 className={classes.workspacesSettingsButton} 154 className={classes.workspacesSettingsButton}
156 onClick={() => { 155 onClick={() => {
157 workspaceActions.openWorkspaceSettings(); 156 workspaceActions.openWorkspaceSettings();
158 gaEvent(GA_CATEGORY_WORKSPACES, 'settings', 'drawerHeadline');
159 }} 157 }}
160 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`} 158 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`}
161 > 159 >
@@ -177,7 +175,6 @@ class WorkspaceDrawer extends Component {
177 icon="mdiStar" 175 icon="mdiStar"
178 onClick={() => { 176 onClick={() => {
179 onUpgradeAccountClick(); 177 onUpgradeAccountClick();
180 gaEvent('User', 'upgrade', 'workspaceDrawer');
181 }} 178 }}
182 /> 179 />
183 ) : ( 180 ) : (
@@ -188,7 +185,6 @@ class WorkspaceDrawer extends Component {
188 icon={mdiPlusBox} 185 icon={mdiPlusBox}
189 onClick={() => { 186 onClick={() => {
190 workspaceActions.openWorkspaceSettings(); 187 workspaceActions.openWorkspaceSettings();
191 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta');
192 }} 188 }}
193 /> 189 />
194 )} 190 )}
@@ -200,7 +196,6 @@ class WorkspaceDrawer extends Component {
200 onClick={() => { 196 onClick={() => {
201 workspaceActions.deactivate(); 197 workspaceActions.deactivate();
202 workspaceActions.toggleWorkspaceDrawer(); 198 workspaceActions.toggleWorkspaceDrawer();
203 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
204 }} 199 }}
205 services={getServicesForWorkspace(null)} 200 services={getServicesForWorkspace(null)}
206 isActive={actualWorkspace == null} 201 isActive={actualWorkspace == null}
@@ -215,7 +210,6 @@ class WorkspaceDrawer extends Component {
215 if (actualWorkspace === workspace) return; 210 if (actualWorkspace === workspace) return;
216 workspaceActions.activate({ workspace }); 211 workspaceActions.activate({ workspace });
217 workspaceActions.toggleWorkspaceDrawer(); 212 workspaceActions.toggleWorkspaceDrawer();
218 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
219 }} 213 }}
220 onContextMenuEditClick={() => workspaceActions.edit({ workspace })} 214 onContextMenuEditClick={() => workspaceActions.edit({ workspace })}
221 services={getServicesForWorkspace(workspace)} 215 services={getServicesForWorkspace(workspace)}
@@ -226,7 +220,6 @@ class WorkspaceDrawer extends Component {
226 className={classes.addNewWorkspaceLabel} 220 className={classes.addNewWorkspaceLabel}
227 onClick={() => { 221 onClick={() => {
228 workspaceActions.openWorkspaceSettings(); 222 workspaceActions.openWorkspaceSettings();
229 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerAddLabel');
230 }} 223 }}
231 > 224 >
232 <Icon 225 <Icon
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index 70e213912..977b23999 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -46,7 +46,7 @@ const messages = defineMessages({
46 }, 46 },
47 workspaceFeatureHeadline: { 47 workspaceFeatureHeadline: {
48 id: 'settings.workspaces.workspaceFeatureHeadline', 48 id: 'settings.workspaces.workspaceFeatureHeadline',
49 defaultMessage: '!!!Less is More: Introducing Franz Workspaces', 49 defaultMessage: '!!!Less is More: Introducing Ferdi Workspaces',
50 }, 50 },
51}); 51});
52 52
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 7f41cfc88..949f8a792 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
@@ -54,7 +56,8 @@ export default class WorkspacesStore extends FeatureStore {
54 } 56 }
55 57
56 @computed get isUserAllowedToUseFeature() { 58 @computed get isUserAllowedToUseFeature() {
57 return !this.isPremiumUpgradeRequired; 59 return true;
60 // return !this.isPremiumUpgradeRequired;
58 } 61 }
59 62
60 @computed get isAnyWorkspaceActive() { 63 @computed get isAnyWorkspaceActive() {
@@ -258,10 +261,10 @@ export default class WorkspacesStore extends FeatureStore {
258 }; 261 };
259 262
260 _setIsPremiumFeatureReaction = () => { 263 _setIsPremiumFeatureReaction = () => {
261 const { features } = this.stores; 264 // const { features } = this.stores;
262 const { isWorkspaceIncludedInCurrentPlan } = features.features; 265 // const { isWorkspaceIncludedInCurrentPlan } = features.features;
263 this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan; 266 // this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan;
264 this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan; 267 // this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan;
265 }; 268 };
266 269
267 _setWorkspaceBeingEditedReaction = () => { 270 _setWorkspaceBeingEditedReaction = () => {
@@ -326,7 +329,7 @@ export default class WorkspacesStore extends FeatureStore {
326 // Loop through all workspaces and remove invalid service ids (locally) 329 // Loop through all workspaces and remove invalid service ids (locally)
327 this.workspaces.forEach((workspace) => { 330 this.workspaces.forEach((workspace) => {
328 workspace.services.forEach((serviceId) => { 331 workspace.services.forEach((serviceId) => {
329 if (servicesHaveBeenLoaded && !services.one(serviceId)) { 332 if (servicesHaveBeenLoaded && !services.one(serviceId) && serviceId !== KEEP_WS_LOADED_USID) {
330 workspace.services.remove(serviceId); 333 workspace.services.remove(serviceId);
331 } 334 }
332 }); 335 });