diff options
author | Stefan <stefan@adlk.io> | 2019-06-04 14:26:56 +0200 |
---|---|---|
committer | Stefan <stefan@adlk.io> | 2019-06-04 14:26:56 +0200 |
commit | fd6a1e9ba24c7947af08a30829c860cad4726f67 (patch) | |
tree | 0b6276700ced8d13eaf0a1e3b706a69a5a47b1da /src/features | |
parent | fix(Windows): Replace tray icon with high-res version (diff) | |
parent | Merge pull request #1443 from meetfranz/feature/multilingual-announcements (diff) | |
download | ferdium-app-fd6a1e9ba24c7947af08a30829c860cad4726f67.tar.gz ferdium-app-fd6a1e9ba24c7947af08a30829c860cad4726f67.tar.zst ferdium-app-fd6a1e9ba24c7947af08a30829c860cad4726f67.zip |
Merge branch 'develop' of https://github.com/meetfranz/franz into develop
Diffstat (limited to 'src/features')
-rw-r--r-- | src/features/announcements/actions.js | 10 | ||||
-rw-r--r-- | src/features/announcements/api.js | 33 | ||||
-rw-r--r-- | src/features/announcements/components/AnnouncementScreen.js | 286 | ||||
-rw-r--r-- | src/features/announcements/index.js | 36 | ||||
-rw-r--r-- | src/features/announcements/store.js | 144 | ||||
-rwxr-xr-x | src/features/settingsWS/actions.js | 10 | ||||
-rwxr-xr-x | src/features/settingsWS/index.js | 29 | ||||
-rwxr-xr-x | src/features/settingsWS/state.js | 13 | ||||
-rwxr-xr-x | src/features/settingsWS/store.js | 130 | ||||
-rw-r--r-- | src/features/shareFranz/index.js | 2 | ||||
-rw-r--r-- | src/features/utils/ActionBinding.js | 29 | ||||
-rw-r--r-- | src/features/utils/FeatureStore.js | 47 | ||||
-rw-r--r-- | src/features/utils/FeatureStore.test.js | 92 | ||||
-rw-r--r-- | src/features/workspaces/components/CreateWorkspaceForm.js | 4 | ||||
-rw-r--r-- | src/features/workspaces/components/EditWorkspaceForm.js | 41 | ||||
-rw-r--r-- | src/features/workspaces/components/WorkspacesDashboard.js | 38 | ||||
-rw-r--r-- | src/features/workspaces/store.js | 89 |
17 files changed, 981 insertions, 52 deletions
diff --git a/src/features/announcements/actions.js b/src/features/announcements/actions.js new file mode 100644 index 000000000..bab496314 --- /dev/null +++ b/src/features/announcements/actions.js | |||
@@ -0,0 +1,10 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const announcementActions = createActionsFromDefinitions({ | ||
5 | show: { | ||
6 | targetVersion: PropTypes.string, | ||
7 | }, | ||
8 | }, PropTypes.checkPropTypes); | ||
9 | |||
10 | export default announcementActions; | ||
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js new file mode 100644 index 000000000..a581bd8de --- /dev/null +++ b/src/features/announcements/api.js | |||
@@ -0,0 +1,33 @@ | |||
1 | import { remote } from 'electron'; | ||
2 | import Request from '../../stores/lib/Request'; | ||
3 | import { API, API_VERSION } from '../../environment'; | ||
4 | |||
5 | const debug = require('debug')('Franz:feature:announcements:api'); | ||
6 | |||
7 | export const announcementsApi = { | ||
8 | async getCurrentVersion() { | ||
9 | debug('getting current version of electron app'); | ||
10 | return Promise.resolve(remote.app.getVersion()); | ||
11 | }, | ||
12 | |||
13 | async getChangelog(version) { | ||
14 | debug('fetching release changelog from Github'); | ||
15 | const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`; | ||
16 | const request = await window.fetch(url, { method: 'GET' }); | ||
17 | if (!request.ok) return null; | ||
18 | const data = await request.json(); | ||
19 | return data.body; | ||
20 | }, | ||
21 | |||
22 | async getAnnouncement(version) { | ||
23 | debug('fetching release announcement from api'); | ||
24 | const url = `${API}/${API_VERSION}/announcements/${version}`; | ||
25 | const response = await window.fetch(url, { method: 'GET' }); | ||
26 | if (!response.ok) return null; | ||
27 | return response.json(); | ||
28 | }, | ||
29 | }; | ||
30 | |||
31 | export const getCurrentVersionRequest = new Request(announcementsApi, 'getCurrentVersion'); | ||
32 | export const getChangelogRequest = new Request(announcementsApi, 'getChangelog'); | ||
33 | export const getAnnouncementRequest = new Request(announcementsApi, 'getAnnouncement'); | ||
diff --git a/src/features/announcements/components/AnnouncementScreen.js b/src/features/announcements/components/AnnouncementScreen.js new file mode 100644 index 000000000..e7c5fe395 --- /dev/null +++ b/src/features/announcements/components/AnnouncementScreen.js | |||
@@ -0,0 +1,286 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import marked from 'marked'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | import injectSheet from 'react-jss'; | ||
7 | import { Button } from '@meetfranz/forms'; | ||
8 | |||
9 | import { announcementsStore } from '../index'; | ||
10 | import UIStore from '../../../stores/UIStore'; | ||
11 | import { gaEvent } from '../../../lib/analytics'; | ||
12 | |||
13 | const renderer = new marked.Renderer(); | ||
14 | |||
15 | renderer.link = (href, title, text) => `<a target="_blank" href="${href}" title="${title}">${text}</a>`; | ||
16 | |||
17 | const markedOptions = { sanitize: true, renderer }; | ||
18 | |||
19 | const messages = defineMessages({ | ||
20 | headline: { | ||
21 | id: 'feature.announcements.changelog.headline', | ||
22 | defaultMessage: '!!!Changes in Franz {version}', | ||
23 | }, | ||
24 | }); | ||
25 | |||
26 | const smallScreen = '1000px'; | ||
27 | |||
28 | const styles = theme => ({ | ||
29 | container: { | ||
30 | background: theme.colorBackground, | ||
31 | position: 'absolute', | ||
32 | top: 0, | ||
33 | zIndex: 140, | ||
34 | width: '100%', | ||
35 | height: '100%', | ||
36 | overflowY: 'auto', | ||
37 | }, | ||
38 | headline: { | ||
39 | color: theme.colorHeadline, | ||
40 | margin: [25, 0, 40], | ||
41 | // 'max-width': 500, | ||
42 | 'text-align': 'center', | ||
43 | 'line-height': '1.3em', | ||
44 | }, | ||
45 | announcement: { | ||
46 | height: 'auto', | ||
47 | |||
48 | [`@media(min-width: ${smallScreen})`]: { | ||
49 | display: 'flex', | ||
50 | flexDirection: 'column', | ||
51 | justifyContent: 'center', | ||
52 | height: '100vh', | ||
53 | }, | ||
54 | }, | ||
55 | main: { | ||
56 | display: 'flex', | ||
57 | flexDirection: 'column', | ||
58 | flexGrow: 1, | ||
59 | justifyContent: 'center', | ||
60 | |||
61 | '& h1': { | ||
62 | margin: [40, 0, 15], | ||
63 | fontSize: 70, | ||
64 | color: theme.styleTypes.primary.accent, | ||
65 | textAlign: 'center', | ||
66 | |||
67 | [`@media(min-width: ${smallScreen})`]: { | ||
68 | marginTop: 0, | ||
69 | }, | ||
70 | }, | ||
71 | '& h2': { | ||
72 | fontSize: 30, | ||
73 | fontWeight: 300, | ||
74 | color: theme.colorText, | ||
75 | textAlign: 'center', | ||
76 | marginBottom: 60, | ||
77 | }, | ||
78 | }, | ||
79 | mainBody: { | ||
80 | display: 'flex', | ||
81 | flexDirection: 'column', | ||
82 | alignItems: 'center', | ||
83 | width: 'calc(100% - 80px)', | ||
84 | height: 'auto', | ||
85 | margin: '0 auto', | ||
86 | [`@media(min-width: ${smallScreen})`]: { | ||
87 | flexDirection: 'row', | ||
88 | justifyContent: 'center', | ||
89 | }, | ||
90 | }, | ||
91 | mainImage: { | ||
92 | minWidth: 250, | ||
93 | maxWidth: 400, | ||
94 | margin: '0 auto', | ||
95 | marginBottom: 40, | ||
96 | '& img': { | ||
97 | width: '100%', | ||
98 | }, | ||
99 | [`@media(min-width: ${smallScreen})`]: { | ||
100 | margin: 0, | ||
101 | }, | ||
102 | }, | ||
103 | mainText: { | ||
104 | height: 'auto', | ||
105 | maxWidth: 600, | ||
106 | textAlign: 'center', | ||
107 | '& p': { | ||
108 | lineHeight: '1.5em', | ||
109 | }, | ||
110 | [`@media(min-width: ${smallScreen})`]: { | ||
111 | textAlign: 'left', | ||
112 | }, | ||
113 | }, | ||
114 | mainCtaButton: { | ||
115 | textAlign: 'center', | ||
116 | marginTop: 40, | ||
117 | [`@media(min-width: ${smallScreen})`]: { | ||
118 | textAlign: 'left', | ||
119 | }, | ||
120 | }, | ||
121 | spotlight: { | ||
122 | height: 'auto', | ||
123 | background: theme.announcements.spotlight.background, | ||
124 | padding: [40, 0], | ||
125 | marginTop: 80, | ||
126 | [`@media(min-width: ${smallScreen})`]: { | ||
127 | marginTop: 0, | ||
128 | justifyContent: 'center', | ||
129 | alignItems: 'flex-start', | ||
130 | display: 'flex', | ||
131 | flexDirection: 'row', | ||
132 | }, | ||
133 | }, | ||
134 | spotlightTopicContainer: { | ||
135 | textAlign: 'center', | ||
136 | marginBottom: 20, | ||
137 | |||
138 | [`@media(min-width: ${smallScreen})`]: { | ||
139 | marginBottom: 0, | ||
140 | minWidth: 250, | ||
141 | maxWidth: 330, | ||
142 | width: '100%', | ||
143 | textAlign: 'right', | ||
144 | marginRight: 60, | ||
145 | }, | ||
146 | }, | ||
147 | spotlightContentContainer: { | ||
148 | textAlign: 'center', | ||
149 | [`@media(min-width: ${smallScreen})`]: { | ||
150 | height: 'auto', | ||
151 | maxWidth: 600, | ||
152 | paddingRight: 40, | ||
153 | textAlign: 'left', | ||
154 | }, | ||
155 | '& p': { | ||
156 | lineHeight: '1.5em', | ||
157 | }, | ||
158 | }, | ||
159 | spotlightTopic: { | ||
160 | fontSize: 20, | ||
161 | marginBottom: 5, | ||
162 | letterSpacing: 0, | ||
163 | fontWeight: 100, | ||
164 | }, | ||
165 | spotlightSubject: { | ||
166 | fontSize: 20, | ||
167 | }, | ||
168 | changelog: { | ||
169 | padding: [0, 60], | ||
170 | maxWidth: 700, | ||
171 | margin: [100, 'auto'], | ||
172 | height: 'auto', | ||
173 | |||
174 | '& h3': { | ||
175 | fontSize: '24px', | ||
176 | margin: '1.5em 0 1em 0', | ||
177 | }, | ||
178 | '& li': { | ||
179 | marginBottom: '1em', | ||
180 | lineHeight: '1.4em', | ||
181 | }, | ||
182 | '& div': { | ||
183 | height: 'auto', | ||
184 | }, | ||
185 | }, | ||
186 | }); | ||
187 | |||
188 | |||
189 | @inject('stores', 'actions') @injectSheet(styles) @observer | ||
190 | class AnnouncementScreen extends Component { | ||
191 | static propTypes = { | ||
192 | classes: PropTypes.object.isRequired, | ||
193 | stores: PropTypes.shape({ | ||
194 | ui: PropTypes.instanceOf(UIStore).isRequired, | ||
195 | }).isRequired, | ||
196 | }; | ||
197 | |||
198 | static contextTypes = { | ||
199 | intl: intlShape, | ||
200 | }; | ||
201 | |||
202 | render() { | ||
203 | const { classes, stores } = this.props; | ||
204 | const { intl } = this.context; | ||
205 | const { changelog, announcement } = announcementsStore; | ||
206 | const themeImage = stores.ui.isDarkThemeActive ? 'dark' : 'light'; | ||
207 | return ( | ||
208 | <div className={classes.container}> | ||
209 | {announcement && ( | ||
210 | <div className={classes.announcement}> | ||
211 | <div className={classes.main}> | ||
212 | <h1>{announcement.main.headline}</h1> | ||
213 | <h2>{announcement.main.subHeadline}</h2> | ||
214 | <div className={classes.mainBody}> | ||
215 | <div className={classes.mainImage}> | ||
216 | <img | ||
217 | src={announcement.main.image[themeImage]} | ||
218 | alt="" | ||
219 | /> | ||
220 | </div> | ||
221 | <div className={classes.mainText}> | ||
222 | <div | ||
223 | dangerouslySetInnerHTML={{ | ||
224 | __html: marked(announcement.main.text, markedOptions), | ||
225 | }} | ||
226 | /> | ||
227 | <div className={classes.mainCtaButton}> | ||
228 | <Button | ||
229 | label={announcement.main.cta.label} | ||
230 | onClick={() => { | ||
231 | const { analytics } = announcement.main.cta; | ||
232 | window.location.href = `#${announcement.main.cta.href}`; | ||
233 | gaEvent(analytics.category, analytics.action, announcement.main.cta.label); | ||
234 | }} | ||
235 | /> | ||
236 | </div> | ||
237 | </div> | ||
238 | </div> | ||
239 | </div> | ||
240 | {announcement.spotlight && ( | ||
241 | <div className={classes.spotlight}> | ||
242 | <div className={classes.spotlightTopicContainer}> | ||
243 | <h2 className={classes.spotlightTopic}>{announcement.spotlight.title}</h2> | ||
244 | <h3 className={classes.spotlightSubject}>{announcement.spotlight.subject}</h3> | ||
245 | </div> | ||
246 | <div className={classes.spotlightContentContainer}> | ||
247 | <div | ||
248 | dangerouslySetInnerHTML={{ | ||
249 | __html: marked(announcement.spotlight.text, markedOptions), | ||
250 | }} | ||
251 | /> | ||
252 | <div className={classes.mainCtaButton}> | ||
253 | <Button | ||
254 | label={announcement.spotlight.cta.label} | ||
255 | onClick={() => { | ||
256 | const { analytics } = announcement.spotlight.cta; | ||
257 | window.location.href = `#${announcement.spotlight.cta.href}`; | ||
258 | gaEvent(analytics.category, analytics.action, announcement.spotlight.cta.label); | ||
259 | }} | ||
260 | /> | ||
261 | </div> | ||
262 | </div> | ||
263 | </div> | ||
264 | )} | ||
265 | </div> | ||
266 | )} | ||
267 | {changelog && ( | ||
268 | <div className={classes.changelog}> | ||
269 | <h1 className={classes.headline}> | ||
270 | {intl.formatMessage(messages.headline, { | ||
271 | version: announcementsStore.targetVersion, | ||
272 | })} | ||
273 | </h1> | ||
274 | <div | ||
275 | dangerouslySetInnerHTML={{ | ||
276 | __html: marked(changelog, markedOptions), | ||
277 | }} | ||
278 | /> | ||
279 | </div> | ||
280 | )} | ||
281 | </div> | ||
282 | ); | ||
283 | } | ||
284 | } | ||
285 | |||
286 | export default AnnouncementScreen; | ||
diff --git a/src/features/announcements/index.js b/src/features/announcements/index.js new file mode 100644 index 000000000..f14e7c9a5 --- /dev/null +++ b/src/features/announcements/index.js | |||
@@ -0,0 +1,36 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import { AnnouncementsStore } from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:announcements'); | ||
5 | |||
6 | export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements'; | ||
7 | |||
8 | export const announcementsStore = new AnnouncementsStore(); | ||
9 | |||
10 | export const ANNOUNCEMENTS_ROUTES = { | ||
11 | TARGET: '/announcements/:id', | ||
12 | }; | ||
13 | |||
14 | export default function initAnnouncements(stores, actions) { | ||
15 | // const { features } = stores; | ||
16 | |||
17 | // Toggle workspace feature | ||
18 | reaction( | ||
19 | () => ( | ||
20 | true | ||
21 | // features.features.isAnnouncementsEnabled | ||
22 | ), | ||
23 | (isEnabled) => { | ||
24 | if (isEnabled) { | ||
25 | debug('Initializing `announcements` feature'); | ||
26 | announcementsStore.start(stores, actions); | ||
27 | } else if (announcementsStore.isFeatureActive) { | ||
28 | debug('Disabling `announcements` feature'); | ||
29 | announcementsStore.stop(); | ||
30 | } | ||
31 | }, | ||
32 | { | ||
33 | fireImmediately: true, | ||
34 | }, | ||
35 | ); | ||
36 | } | ||
diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js new file mode 100644 index 000000000..ad78a0979 --- /dev/null +++ b/src/features/announcements/store.js | |||
@@ -0,0 +1,144 @@ | |||
1 | import { | ||
2 | action, | ||
3 | computed, | ||
4 | observable, | ||
5 | } from 'mobx'; | ||
6 | import semver from 'semver'; | ||
7 | import localStorage from 'mobx-localstorage'; | ||
8 | |||
9 | import { FeatureStore } from '../utils/FeatureStore'; | ||
10 | import { ANNOUNCEMENTS_ROUTES, GA_CATEGORY_ANNOUNCEMENTS } from '.'; | ||
11 | import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api'; | ||
12 | import { announcementActions } from './actions'; | ||
13 | import { createActionBindings } from '../utils/ActionBinding'; | ||
14 | import { createReactions } from '../../stores/lib/Reaction'; | ||
15 | import { gaEvent } from '../../lib/analytics'; | ||
16 | import { matchRoute } from '../../helpers/routing-helpers'; | ||
17 | |||
18 | const LOCAL_STORAGE_KEY = 'announcements'; | ||
19 | |||
20 | const debug = require('debug')('Franz:feature:announcements:store'); | ||
21 | |||
22 | export class AnnouncementsStore extends FeatureStore { | ||
23 | @observable targetVersion = null; | ||
24 | |||
25 | @observable isFeatureActive = false; | ||
26 | |||
27 | @computed get changelog() { | ||
28 | return getChangelogRequest.result; | ||
29 | } | ||
30 | |||
31 | @computed get announcement() { | ||
32 | if (!this.stores || !getAnnouncementRequest.result) return null; | ||
33 | const { locale, defaultLocale } = this.stores.app; | ||
34 | const announcement = getAnnouncementRequest.result; | ||
35 | // User locale | ||
36 | if (announcement[locale]) return announcement[locale]; | ||
37 | // Default locale | ||
38 | if (announcement[defaultLocale]) return announcement[defaultLocale]; | ||
39 | // No locales specified | ||
40 | return announcement; | ||
41 | } | ||
42 | |||
43 | @computed get areNewsAvailable() { | ||
44 | const isChangelogAvailable = getChangelogRequest.wasExecuted && !!this.changelog; | ||
45 | const isAnnouncementAvailable = getAnnouncementRequest.wasExecuted && !!this.announcement; | ||
46 | return isChangelogAvailable || isAnnouncementAvailable; | ||
47 | } | ||
48 | |||
49 | @computed get settings() { | ||
50 | return localStorage.getItem(LOCAL_STORAGE_KEY) || {}; | ||
51 | } | ||
52 | |||
53 | @computed get lastSeenAnnouncementVersion() { | ||
54 | return this.settings.lastSeenAnnouncementVersion || null; | ||
55 | } | ||
56 | |||
57 | @computed get currentVersion() { | ||
58 | return getCurrentVersionRequest.result; | ||
59 | } | ||
60 | |||
61 | @computed get isNewUser() { | ||
62 | return this.stores.settings.stats.appStarts <= 1; | ||
63 | } | ||
64 | |||
65 | async start(stores, actions) { | ||
66 | debug('AnnouncementsStore::start'); | ||
67 | this.stores = stores; | ||
68 | this.actions = actions; | ||
69 | getCurrentVersionRequest.execute(); | ||
70 | |||
71 | this._registerActions(createActionBindings([ | ||
72 | [announcementActions.show, this._showAnnouncement], | ||
73 | ])); | ||
74 | |||
75 | this._reactions = createReactions([ | ||
76 | this._showAnnouncementOnRouteMatch, | ||
77 | this._showAnnouncementToUsersWhoUpdatedApp, | ||
78 | this._fetchAnnouncements, | ||
79 | ]); | ||
80 | this._registerReactions(this._reactions); | ||
81 | this.isFeatureActive = true; | ||
82 | } | ||
83 | |||
84 | stop() { | ||
85 | super.stop(); | ||
86 | debug('AnnouncementsStore::stop'); | ||
87 | this.isFeatureActive = false; | ||
88 | } | ||
89 | |||
90 | // ======= HELPERS ======= // | ||
91 | |||
92 | _updateSettings = (changes) => { | ||
93 | localStorage.setItem(LOCAL_STORAGE_KEY, { | ||
94 | ...this.settings, | ||
95 | ...changes, | ||
96 | }); | ||
97 | }; | ||
98 | |||
99 | // ======= ACTIONS ======= // | ||
100 | |||
101 | @action _showAnnouncement = ({ targetVersion } = {}) => { | ||
102 | const { router } = this.stores; | ||
103 | this.targetVersion = targetVersion || this.currentVersion; | ||
104 | this._updateSettings({ | ||
105 | lastSeenAnnouncementVersion: this.currentVersion, | ||
106 | }); | ||
107 | const targetRoute = `/announcements/${this.targetVersion}`; | ||
108 | if (router.location.pathname !== targetRoute) { | ||
109 | this.stores.router.push(targetRoute); | ||
110 | } | ||
111 | gaEvent(GA_CATEGORY_ANNOUNCEMENTS, 'show'); | ||
112 | }; | ||
113 | |||
114 | // ======= REACTIONS ======== | ||
115 | |||
116 | _showAnnouncementToUsersWhoUpdatedApp = () => { | ||
117 | const { announcement, isNewUser } = this; | ||
118 | // Check if there is an announcement and don't show announcements to new users | ||
119 | if (!announcement || isNewUser) return; | ||
120 | |||
121 | // Check if the user has already used current version (= has seen the announcement) | ||
122 | const { currentVersion, lastSeenAnnouncementVersion } = this; | ||
123 | if (semver.gt(currentVersion, lastSeenAnnouncementVersion || '0.0.0')) { | ||
124 | debug(`${currentVersion} > ${lastSeenAnnouncementVersion}: announcement is shown`); | ||
125 | this._showAnnouncement(); | ||
126 | } | ||
127 | }; | ||
128 | |||
129 | _fetchAnnouncements = () => { | ||
130 | const targetVersion = this.targetVersion || this.currentVersion; | ||
131 | if (!targetVersion) return; | ||
132 | getChangelogRequest.reset().execute(targetVersion); | ||
133 | getAnnouncementRequest.reset().execute(targetVersion); | ||
134 | }; | ||
135 | |||
136 | _showAnnouncementOnRouteMatch = () => { | ||
137 | const { router } = this.stores; | ||
138 | const match = matchRoute(ANNOUNCEMENTS_ROUTES.TARGET, router.location.pathname); | ||
139 | if (match) { | ||
140 | const targetVersion = match.id; | ||
141 | this._showAnnouncement({ targetVersion }); | ||
142 | } | ||
143 | } | ||
144 | } | ||
diff --git a/src/features/settingsWS/actions.js b/src/features/settingsWS/actions.js new file mode 100755 index 000000000..631670c8a --- /dev/null +++ b/src/features/settingsWS/actions.js | |||
@@ -0,0 +1,10 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const settingsWSActions = createActionsFromDefinitions({ | ||
5 | greet: { | ||
6 | name: PropTypes.string.isRequired, | ||
7 | }, | ||
8 | }, PropTypes.checkPropTypes); | ||
9 | |||
10 | export default settingsWSActions; | ||
diff --git a/src/features/settingsWS/index.js b/src/features/settingsWS/index.js new file mode 100755 index 000000000..2064d2973 --- /dev/null +++ b/src/features/settingsWS/index.js | |||
@@ -0,0 +1,29 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import { SettingsWSStore } from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:settingsWS'); | ||
5 | |||
6 | export const settingsStore = new SettingsWSStore(); | ||
7 | |||
8 | export default function initSettingsWebSocket(stores, actions) { | ||
9 | const { features } = stores; | ||
10 | |||
11 | // Toggle SettingsWebSocket feature | ||
12 | reaction( | ||
13 | () => ( | ||
14 | features.features.isSettingsWSEnabled | ||
15 | ), | ||
16 | (isEnabled) => { | ||
17 | if (isEnabled) { | ||
18 | debug('Initializing `settingsWS` feature'); | ||
19 | settingsStore.start(stores, actions); | ||
20 | } else if (settingsStore) { | ||
21 | debug('Disabling `settingsWS` feature'); | ||
22 | settingsStore.stop(); | ||
23 | } | ||
24 | }, | ||
25 | { | ||
26 | fireImmediately: true, | ||
27 | }, | ||
28 | ); | ||
29 | } | ||
diff --git a/src/features/settingsWS/state.js b/src/features/settingsWS/state.js new file mode 100755 index 000000000..7b16b2b6e --- /dev/null +++ b/src/features/settingsWS/state.js | |||
@@ -0,0 +1,13 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | |||
3 | const defaultState = { | ||
4 | isFeatureActive: false, | ||
5 | }; | ||
6 | |||
7 | export const settingsWSState = observable(defaultState); | ||
8 | |||
9 | export function resetState() { | ||
10 | Object.assign(settingsWSState, defaultState); | ||
11 | } | ||
12 | |||
13 | export default settingsWSState; | ||
diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js new file mode 100755 index 000000000..167a70d10 --- /dev/null +++ b/src/features/settingsWS/store.js | |||
@@ -0,0 +1,130 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | import WebSocket from 'ws'; | ||
3 | import ms from 'ms'; | ||
4 | |||
5 | import { FeatureStore } from '../utils/FeatureStore'; | ||
6 | import { createReactions } from '../../stores/lib/Reaction'; | ||
7 | import { WS_API } from '../../environment'; | ||
8 | |||
9 | const debug = require('debug')('Franz:feature:settingsWS:store'); | ||
10 | |||
11 | export class SettingsWSStore extends FeatureStore { | ||
12 | stores = null; | ||
13 | |||
14 | actions = null; | ||
15 | |||
16 | ws = null; | ||
17 | |||
18 | pingTimeout = null; | ||
19 | |||
20 | reconnectTimeout = null; | ||
21 | |||
22 | @observable connected = false; | ||
23 | |||
24 | start(stores, actions) { | ||
25 | this.stores = stores; | ||
26 | this.actions = actions; | ||
27 | |||
28 | this._registerReactions(createReactions([ | ||
29 | this._initialize.bind(this), | ||
30 | this._reconnect.bind(this), | ||
31 | this._close.bind(this), | ||
32 | ])); | ||
33 | } | ||
34 | |||
35 | connect() { | ||
36 | try { | ||
37 | const wsURL = `${WS_API}/ws/${this.stores.user.data.id}`; | ||
38 | debug('Setting up WebSocket to', wsURL); | ||
39 | |||
40 | this.ws = new WebSocket(wsURL); | ||
41 | |||
42 | this.ws.on('open', () => { | ||
43 | debug('Opened WebSocket'); | ||
44 | this.send({ | ||
45 | action: 'authorize', | ||
46 | token: this.stores.user.authToken, | ||
47 | }); | ||
48 | |||
49 | this.connected = true; | ||
50 | |||
51 | this.heartbeat(); | ||
52 | }); | ||
53 | |||
54 | this.ws.on('message', (data) => { | ||
55 | const resp = JSON.parse(data); | ||
56 | debug('Received message', resp); | ||
57 | |||
58 | if (resp.id) { | ||
59 | this.stores.user.getUserInfoRequest.patch((result) => { | ||
60 | if (!result) return; | ||
61 | |||
62 | debug('Patching user object with new values'); | ||
63 | Object.assign(result, resp); | ||
64 | }); | ||
65 | } | ||
66 | }); | ||
67 | |||
68 | this.ws.on('ping', this.heartbeat.bind(this)); | ||
69 | } catch (err) { | ||
70 | console.err(err); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | heartbeat() { | ||
75 | debug('Heartbeat'); | ||
76 | clearTimeout(this.pingTimeout); | ||
77 | |||
78 | this.pingTimeout = setTimeout(() => { | ||
79 | debug('Terminating connection, reconnecting in 35'); | ||
80 | this.ws.terminate(); | ||
81 | |||
82 | this.connected = false; | ||
83 | }, ms('35s')); | ||
84 | } | ||
85 | |||
86 | send(data) { | ||
87 | if (this.ws && this.ws.readyState === 1) { | ||
88 | this.ws.send(JSON.stringify(data)); | ||
89 | debug('Sending data', data); | ||
90 | } else { | ||
91 | debug('WebSocket is not initialized'); | ||
92 | } | ||
93 | } | ||
94 | |||
95 | // Reactions | ||
96 | |||
97 | _initialize() { | ||
98 | if (this.stores.user.data.id && !this.ws) { | ||
99 | this.connect(); | ||
100 | } | ||
101 | } | ||
102 | |||
103 | _reconnect() { | ||
104 | if (!this.connected) { | ||
105 | debug('Trying to reconnect in 30s'); | ||
106 | this.reconnectTimeout = setInterval(() => { | ||
107 | debug('Trying to reconnect'); | ||
108 | this.connect(); | ||
109 | }, ms('30s')); | ||
110 | } else { | ||
111 | debug('Clearing reconnect interval'); | ||
112 | clearInterval(this.reconnectTimeout); | ||
113 | } | ||
114 | } | ||
115 | |||
116 | _close() { | ||
117 | if (!this.stores.user.isLoggedIn) { | ||
118 | debug('Stopping reactions'); | ||
119 | this._stopReactions(); | ||
120 | |||
121 | if (this.ws) { | ||
122 | debug('Terminating connection'); | ||
123 | this.ws.terminate(); | ||
124 | this.ws = null; | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | export default SettingsWSStore; | ||
diff --git a/src/features/shareFranz/index.js b/src/features/shareFranz/index.js index 3a8ec95d3..87deacef4 100644 --- a/src/features/shareFranz/index.js +++ b/src/features/shareFranz/index.js | |||
@@ -35,7 +35,7 @@ export default function initialize(stores) { | |||
35 | () => stores.user.isLoggedIn, | 35 | () => stores.user.isLoggedIn, |
36 | () => { | 36 | () => { |
37 | setTimeout(() => { | 37 | setTimeout(() => { |
38 | if (stores.settings.stats.appStarts % 30 === 0) { | 38 | if (stores.settings.stats.appStarts % 50 === 0) { |
39 | if (delayAppState.isDelayAppScreenVisible) { | 39 | if (delayAppState.isDelayAppScreenVisible) { |
40 | debug('Delaying share modal by 5 minutes'); | 40 | debug('Delaying share modal by 5 minutes'); |
41 | setTimeout(() => showModal(), ms('5m')); | 41 | setTimeout(() => showModal(), ms('5m')); |
diff --git a/src/features/utils/ActionBinding.js b/src/features/utils/ActionBinding.js new file mode 100644 index 000000000..497aa071b --- /dev/null +++ b/src/features/utils/ActionBinding.js | |||
@@ -0,0 +1,29 @@ | |||
1 | export default class ActionBinding { | ||
2 | action; | ||
3 | |||
4 | isActive = false; | ||
5 | |||
6 | constructor(action) { | ||
7 | this.action = action; | ||
8 | } | ||
9 | |||
10 | start() { | ||
11 | if (!this.isActive) { | ||
12 | const { action } = this; | ||
13 | action[0].listen(action[1]); | ||
14 | this.isActive = true; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | stop() { | ||
19 | if (this.isActive) { | ||
20 | const { action } = this; | ||
21 | action[0].off(action[1]); | ||
22 | this.isActive = false; | ||
23 | } | ||
24 | } | ||
25 | } | ||
26 | |||
27 | export const createActionBindings = actions => ( | ||
28 | actions.map(a => new ActionBinding(a)) | ||
29 | ); | ||
diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.js index 66b66a104..0bc10e176 100644 --- a/src/features/utils/FeatureStore.js +++ b/src/features/utils/FeatureStore.js | |||
@@ -1,21 +1,40 @@ | |||
1 | import Reaction from '../../stores/lib/Reaction'; | ||
2 | |||
3 | export class FeatureStore { | 1 | export class FeatureStore { |
4 | _actions = null; | 2 | _actions = []; |
3 | |||
4 | _reactions = []; | ||
5 | |||
6 | stop() { | ||
7 | this._stopActions(); | ||
8 | this._stopReactions(); | ||
9 | } | ||
10 | |||
11 | // ACTIONS | ||
12 | |||
13 | _registerActions(actions) { | ||
14 | this._actions = actions; | ||
15 | this._startActions(); | ||
16 | } | ||
5 | 17 | ||
6 | _reactions = null; | 18 | _startActions(actions = this._actions) { |
19 | actions.forEach(a => a.start()); | ||
20 | } | ||
21 | |||
22 | _stopActions(actions = this._actions) { | ||
23 | actions.forEach(a => a.stop()); | ||
24 | } | ||
25 | |||
26 | // REACTIONS | ||
27 | |||
28 | _registerReactions(reactions) { | ||
29 | this._reactions = reactions; | ||
30 | this._startReactions(); | ||
31 | } | ||
7 | 32 | ||
8 | _listenToActions(actions) { | 33 | _startReactions(reactions = this._reactions) { |
9 | if (this._actions) this._actions.forEach(a => a[0].off(a[1])); | 34 | reactions.forEach(r => r.start()); |
10 | this._actions = []; | ||
11 | actions.forEach(a => this._actions.push(a)); | ||
12 | this._actions.forEach(a => a[0].listen(a[1])); | ||
13 | } | 35 | } |
14 | 36 | ||
15 | _startReactions(reactions) { | 37 | _stopReactions(reactions = this._reactions) { |
16 | if (this._reactions) this._reactions.forEach(r => r.stop()); | 38 | reactions.forEach(r => r.stop()); |
17 | this._reactions = []; | ||
18 | reactions.forEach(r => this._reactions.push(new Reaction(r))); | ||
19 | this._reactions.forEach(r => r.start()); | ||
20 | } | 39 | } |
21 | } | 40 | } |
diff --git a/src/features/utils/FeatureStore.test.js b/src/features/utils/FeatureStore.test.js new file mode 100644 index 000000000..92308bf52 --- /dev/null +++ b/src/features/utils/FeatureStore.test.js | |||
@@ -0,0 +1,92 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { observable } from 'mobx'; | ||
3 | import { FeatureStore } from './FeatureStore'; | ||
4 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
5 | import { createActionBindings } from './ActionBinding'; | ||
6 | import { createReactions } from '../../stores/lib/Reaction'; | ||
7 | |||
8 | const actions = createActionsFromDefinitions({ | ||
9 | countUp: {}, | ||
10 | }, PropTypes.checkPropTypes); | ||
11 | |||
12 | class TestFeatureStore extends FeatureStore { | ||
13 | @observable count = 0; | ||
14 | |||
15 | reactionInvokedCount = 0; | ||
16 | |||
17 | start() { | ||
18 | this._registerActions(createActionBindings([ | ||
19 | [actions.countUp, this._countUp], | ||
20 | ])); | ||
21 | this._registerReactions(createReactions([ | ||
22 | this._countReaction, | ||
23 | ])); | ||
24 | } | ||
25 | |||
26 | _countUp = () => { | ||
27 | this.count += 1; | ||
28 | }; | ||
29 | |||
30 | _countReaction = () => { | ||
31 | this.reactionInvokedCount += 1; | ||
32 | } | ||
33 | } | ||
34 | |||
35 | describe('FeatureStore', () => { | ||
36 | let store = null; | ||
37 | |||
38 | beforeEach(() => { | ||
39 | store = new TestFeatureStore(); | ||
40 | }); | ||
41 | |||
42 | describe('registering actions', () => { | ||
43 | it('starts the actions', () => { | ||
44 | store.start(); | ||
45 | actions.countUp(); | ||
46 | expect(store.count).toBe(1); | ||
47 | }); | ||
48 | it('starts the reactions', () => { | ||
49 | store.start(); | ||
50 | actions.countUp(); | ||
51 | expect(store.reactionInvokedCount).toBe(1); | ||
52 | }); | ||
53 | }); | ||
54 | |||
55 | describe('stopping the store', () => { | ||
56 | it('stops the actions', () => { | ||
57 | store.start(); | ||
58 | actions.countUp(); | ||
59 | store.stop(); | ||
60 | actions.countUp(); | ||
61 | expect(store.count).toBe(1); | ||
62 | }); | ||
63 | it('stops the reactions', () => { | ||
64 | store.start(); | ||
65 | actions.countUp(); | ||
66 | store.stop(); | ||
67 | store.count += 1; | ||
68 | expect(store.reactionInvokedCount).toBe(1); | ||
69 | }); | ||
70 | }); | ||
71 | |||
72 | describe('toggling the store', () => { | ||
73 | it('restarts the actions correctly', () => { | ||
74 | store.start(); | ||
75 | actions.countUp(); | ||
76 | store.stop(); | ||
77 | actions.countUp(); | ||
78 | store.start(); | ||
79 | actions.countUp(); | ||
80 | expect(store.count).toBe(2); | ||
81 | }); | ||
82 | it('restarts the reactions correctly', () => { | ||
83 | store.start(); | ||
84 | actions.countUp(); | ||
85 | store.stop(); | ||
86 | actions.countUp(); | ||
87 | store.start(); | ||
88 | actions.countUp(); | ||
89 | expect(store.count).toBe(2); | ||
90 | }); | ||
91 | }); | ||
92 | }); | ||
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js index 2c00ea63c..cddbb2b04 100644 --- a/src/features/workspaces/components/CreateWorkspaceForm.js +++ b/src/features/workspaces/components/CreateWorkspaceForm.js | |||
@@ -7,7 +7,7 @@ import injectSheet from 'react-jss'; | |||
7 | import Form from '../../../lib/Form'; | 7 | import Form from '../../../lib/Form'; |
8 | import { required } from '../../../helpers/validation-helpers'; | 8 | import { required } from '../../../helpers/validation-helpers'; |
9 | import { gaEvent } from '../../../lib/analytics'; | 9 | import { gaEvent } from '../../../lib/analytics'; |
10 | import { GA_CATEGORY_WORKSPACES } from '../index'; | 10 | import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index'; |
11 | 11 | ||
12 | const messages = defineMessages({ | 12 | const messages = defineMessages({ |
13 | submitButton: { | 13 | submitButton: { |
@@ -82,7 +82,7 @@ class CreateWorkspaceForm extends Component { | |||
82 | {...form.$('name').bind()} | 82 | {...form.$('name').bind()} |
83 | showLabel={false} | 83 | showLabel={false} |
84 | onEnterKey={this.submitForm.bind(this, form)} | 84 | onEnterKey={this.submitForm.bind(this, form)} |
85 | focus | 85 | focus={workspaceStore.isUserAllowedToUseFeature} |
86 | /> | 86 | /> |
87 | <Button | 87 | <Button |
88 | className={classes.submitButton} | 88 | className={classes.submitButton} |
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js index bba4485ff..e602ebd5a 100644 --- a/src/features/workspaces/components/EditWorkspaceForm.js +++ b/src/features/workspaces/components/EditWorkspaceForm.js | |||
@@ -1,4 +1,4 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component, Fragment } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { observer } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
@@ -36,6 +36,14 @@ const messages = defineMessages({ | |||
36 | id: 'settings.workspace.form.servicesInWorkspaceHeadline', | 36 | id: 'settings.workspace.form.servicesInWorkspaceHeadline', |
37 | defaultMessage: '!!!Services in this Workspace', | 37 | defaultMessage: '!!!Services in this Workspace', |
38 | }, | 38 | }, |
39 | noServicesAdded: { | ||
40 | id: 'settings.services.noServicesAdded', | ||
41 | defaultMessage: '!!!You haven\'t added any services yet.', | ||
42 | }, | ||
43 | discoverServices: { | ||
44 | id: 'settings.services.discoverServices', | ||
45 | defaultMessage: '!!!Discover services', | ||
46 | }, | ||
39 | }); | 47 | }); |
40 | 48 | ||
41 | const styles = () => ({ | 49 | const styles = () => ({ |
@@ -150,14 +158,29 @@ class EditWorkspaceForm extends Component { | |||
150 | </div> | 158 | </div> |
151 | <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2> | 159 | <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2> |
152 | <div className={classes.serviceList}> | 160 | <div className={classes.serviceList}> |
153 | {services.map(s => ( | 161 | {services.length === 0 ? ( |
154 | <WorkspaceServiceListItem | 162 | <div className="align-middle settings__empty-state"> |
155 | key={s.id} | 163 | {/* ===== Empty state ===== */} |
156 | service={s} | 164 | <p className="settings__empty-text"> |
157 | isInWorkspace={workspaceServices.includes(s.id)} | 165 | <span className="emoji"> |
158 | onToggle={() => this.toggleService(s)} | 166 | <img src="./assets/images/emoji/sad.png" alt="" /> |
159 | /> | 167 | </span> |
160 | ))} | 168 | {intl.formatMessage(messages.noServicesAdded)} |
169 | </p> | ||
170 | <Link to="/settings/recipes" className="button">{intl.formatMessage(messages.discoverServices)}</Link> | ||
171 | </div> | ||
172 | ) : ( | ||
173 | <Fragment> | ||
174 | {services.map(s => ( | ||
175 | <WorkspaceServiceListItem | ||
176 | key={s.id} | ||
177 | service={s} | ||
178 | isInWorkspace={workspaceServices.includes(s.id)} | ||
179 | onToggle={() => this.toggleService(s)} | ||
180 | /> | ||
181 | ))} | ||
182 | </Fragment> | ||
183 | )} | ||
161 | </div> | 184 | </div> |
162 | </div> | 185 | </div> |
163 | <div className="settings__controls"> | 186 | <div className="settings__controls"> |
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js index dd4381a15..09c98ab8c 100644 --- a/src/features/workspaces/components/WorkspacesDashboard.js +++ b/src/features/workspaces/components/WorkspacesDashboard.js | |||
@@ -170,18 +170,32 @@ class WorkspacesDashboard extends Component { | |||
170 | {intl.formatMessage(messages.workspacesRequestFailed)} | 170 | {intl.formatMessage(messages.workspacesRequestFailed)} |
171 | </Infobox> | 171 | </Infobox> |
172 | ) : ( | 172 | ) : ( |
173 | <table className={classes.table}> | 173 | <Fragment> |
174 | {/* ===== Workspaces list ===== */} | 174 | {workspaces.length === 0 ? ( |
175 | <tbody> | 175 | <div className="align-middle settings__empty-state"> |
176 | {workspaces.map(workspace => ( | 176 | {/* ===== Workspaces empty state ===== */} |
177 | <WorkspaceItem | 177 | <p className="settings__empty-text"> |
178 | key={workspace.id} | 178 | <span className="emoji"> |
179 | workspace={workspace} | 179 | <img src="./assets/images/emoji/sad.png" alt="" /> |
180 | onItemClick={w => onWorkspaceClick(w)} | 180 | </span> |
181 | /> | 181 | {intl.formatMessage(messages.noServicesAdded)} |
182 | ))} | 182 | </p> |
183 | </tbody> | 183 | </div> |
184 | </table> | 184 | ) : ( |
185 | <table className={classes.table}> | ||
186 | {/* ===== Workspaces list ===== */} | ||
187 | <tbody> | ||
188 | {workspaces.map(workspace => ( | ||
189 | <WorkspaceItem | ||
190 | key={workspace.id} | ||
191 | workspace={workspace} | ||
192 | onItemClick={w => onWorkspaceClick(w)} | ||
193 | /> | ||
194 | ))} | ||
195 | </tbody> | ||
196 | </table> | ||
197 | )} | ||
198 | </Fragment> | ||
185 | )} | 199 | )} |
186 | </Fragment> | 200 | </Fragment> |
187 | )} | 201 | )} |
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js index ea601700e..51a7f3651 100644 --- a/src/features/workspaces/store.js +++ b/src/features/workspaces/store.js | |||
@@ -14,6 +14,8 @@ import { | |||
14 | updateWorkspaceRequest, | 14 | updateWorkspaceRequest, |
15 | } from './api'; | 15 | } from './api'; |
16 | import { WORKSPACES_ROUTES } from './index'; | 16 | import { WORKSPACES_ROUTES } from './index'; |
17 | import { createReactions } from '../../stores/lib/Reaction'; | ||
18 | import { createActionBindings } from '../utils/ActionBinding'; | ||
17 | 19 | ||
18 | const debug = require('debug')('Franz:feature:workspaces:store'); | 20 | const debug = require('debug')('Franz:feature:workspaces:store'); |
19 | 21 | ||
@@ -51,37 +53,78 @@ export default class WorkspacesStore extends FeatureStore { | |||
51 | return getUserWorkspacesRequest.wasExecuted && this.workspaces.length > 0; | 53 | return getUserWorkspacesRequest.wasExecuted && this.workspaces.length > 0; |
52 | } | 54 | } |
53 | 55 | ||
56 | @computed get isUserAllowedToUseFeature() { | ||
57 | return !this.isPremiumUpgradeRequired; | ||
58 | } | ||
59 | |||
60 | @computed get isAnyWorkspaceActive() { | ||
61 | return !!this.activeWorkspace; | ||
62 | } | ||
63 | |||
64 | // ========== PRIVATE PROPERTIES ========= // | ||
65 | |||
66 | _wasDrawerOpenBeforeSettingsRoute = null; | ||
67 | |||
68 | _freeUserActions = []; | ||
69 | |||
70 | _premiumUserActions = []; | ||
71 | |||
72 | _allActions = []; | ||
73 | |||
74 | _freeUserReactions = []; | ||
75 | |||
76 | _premiumUserReactions = []; | ||
77 | |||
78 | _allReactions = []; | ||
79 | |||
80 | // ========== PUBLIC API ========= // | ||
81 | |||
54 | start(stores, actions) { | 82 | start(stores, actions) { |
55 | debug('WorkspacesStore::start'); | 83 | debug('WorkspacesStore::start'); |
56 | this.stores = stores; | 84 | this.stores = stores; |
57 | this.actions = actions; | 85 | this.actions = actions; |
58 | 86 | ||
59 | this._listenToActions([ | 87 | // ACTIONS |
88 | |||
89 | this._freeUserActions = createActionBindings([ | ||
90 | [workspaceActions.toggleWorkspaceDrawer, this._toggleWorkspaceDrawer], | ||
91 | [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings], | ||
92 | ]); | ||
93 | this._premiumUserActions = createActionBindings([ | ||
60 | [workspaceActions.edit, this._edit], | 94 | [workspaceActions.edit, this._edit], |
61 | [workspaceActions.create, this._create], | 95 | [workspaceActions.create, this._create], |
62 | [workspaceActions.delete, this._delete], | 96 | [workspaceActions.delete, this._delete], |
63 | [workspaceActions.update, this._update], | 97 | [workspaceActions.update, this._update], |
64 | [workspaceActions.activate, this._setActiveWorkspace], | 98 | [workspaceActions.activate, this._setActiveWorkspace], |
65 | [workspaceActions.deactivate, this._deactivateActiveWorkspace], | 99 | [workspaceActions.deactivate, this._deactivateActiveWorkspace], |
66 | [workspaceActions.toggleWorkspaceDrawer, this._toggleWorkspaceDrawer], | ||
67 | [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings], | ||
68 | ]); | 100 | ]); |
101 | this._allActions = this._freeUserActions.concat(this._premiumUserActions); | ||
102 | this._registerActions(this._allActions); | ||
69 | 103 | ||
70 | this._startReactions([ | 104 | // REACTIONS |
71 | this._setWorkspaceBeingEditedReaction, | 105 | |
72 | this._setActiveServiceOnWorkspaceSwitchReaction, | 106 | this._freeUserReactions = createReactions([ |
107 | this._stopPremiumActionsAndReactions, | ||
108 | this._openDrawerWithSettingsReaction, | ||
73 | this._setFeatureEnabledReaction, | 109 | this._setFeatureEnabledReaction, |
74 | this._setIsPremiumFeatureReaction, | 110 | this._setIsPremiumFeatureReaction, |
75 | this._activateLastUsedWorkspaceReaction, | ||
76 | this._openDrawerWithSettingsReaction, | ||
77 | this._cleanupInvalidServiceReferences, | 111 | this._cleanupInvalidServiceReferences, |
78 | ]); | 112 | ]); |
113 | this._premiumUserReactions = createReactions([ | ||
114 | this._setActiveServiceOnWorkspaceSwitchReaction, | ||
115 | this._activateLastUsedWorkspaceReaction, | ||
116 | this._setWorkspaceBeingEditedReaction, | ||
117 | ]); | ||
118 | this._allReactions = this._freeUserReactions.concat(this._premiumUserReactions); | ||
119 | |||
120 | this._registerReactions(this._allReactions); | ||
79 | 121 | ||
80 | getUserWorkspacesRequest.execute(); | 122 | getUserWorkspacesRequest.execute(); |
81 | this.isFeatureActive = true; | 123 | this.isFeatureActive = true; |
82 | } | 124 | } |
83 | 125 | ||
84 | stop() { | 126 | stop() { |
127 | super.stop(); | ||
85 | debug('WorkspacesStore::stop'); | 128 | debug('WorkspacesStore::stop'); |
86 | this.isFeatureActive = false; | 129 | this.isFeatureActive = false; |
87 | this.activeWorkspace = null; | 130 | this.activeWorkspace = null; |
@@ -104,9 +147,7 @@ export default class WorkspacesStore extends FeatureStore { | |||
104 | return workspace.services.map(id => services.one(id)).filter(s => !!s); | 147 | return workspace.services.map(id => services.one(id)).filter(s => !!s); |
105 | } | 148 | } |
106 | 149 | ||
107 | // ========== PRIVATE ========= // | 150 | // ========== PRIVATE METHODS ========= // |
108 | |||
109 | _wasDrawerOpenBeforeSettingsRoute = null; | ||
110 | 151 | ||
111 | _getWorkspaceById = id => this.workspaces.find(w => w.id === id); | 152 | _getWorkspaceById = id => this.workspaces.find(w => w.id === id); |
112 | 153 | ||
@@ -192,6 +233,14 @@ export default class WorkspacesStore extends FeatureStore { | |||
192 | this.actions.ui.openSettings({ path: 'workspaces' }); | 233 | this.actions.ui.openSettings({ path: 'workspaces' }); |
193 | }; | 234 | }; |
194 | 235 | ||
236 | @action reorderServicesOfActiveWorkspace = async ({ oldIndex, newIndex }) => { | ||
237 | const { activeWorkspace } = this; | ||
238 | const { services } = activeWorkspace; | ||
239 | // Move services from the old to the new position | ||
240 | services.splice(newIndex, 0, services.splice(oldIndex, 1)[0]); | ||
241 | await updateWorkspaceRequest.execute(activeWorkspace); | ||
242 | }; | ||
243 | |||
195 | // Reactions | 244 | // Reactions |
196 | 245 | ||
197 | _setFeatureEnabledReaction = () => { | 246 | _setFeatureEnabledReaction = () => { |
@@ -218,13 +267,15 @@ export default class WorkspacesStore extends FeatureStore { | |||
218 | _setActiveServiceOnWorkspaceSwitchReaction = () => { | 267 | _setActiveServiceOnWorkspaceSwitchReaction = () => { |
219 | if (!this.isFeatureActive) return; | 268 | if (!this.isFeatureActive) return; |
220 | if (this.activeWorkspace) { | 269 | if (this.activeWorkspace) { |
221 | const services = this.stores.services.allDisplayed; | 270 | const activeService = this.stores.services.active; |
222 | const activeService = services.find(s => s.isActive); | ||
223 | const workspaceServices = this.getWorkspaceServices(this.activeWorkspace); | 271 | const workspaceServices = this.getWorkspaceServices(this.activeWorkspace); |
224 | if (workspaceServices.length <= 0) return; | 272 | if (workspaceServices.length <= 0) return; |
225 | const isActiveServiceInWorkspace = workspaceServices.includes(activeService); | 273 | const isActiveServiceInWorkspace = workspaceServices.includes(activeService); |
226 | if (!isActiveServiceInWorkspace) { | 274 | if (!isActiveServiceInWorkspace) { |
227 | this.actions.service.setActive({ serviceId: workspaceServices[0].id }); | 275 | this.actions.service.setActive({ |
276 | serviceId: workspaceServices[0].id, | ||
277 | keepActiveRoute: true, | ||
278 | }); | ||
228 | } | 279 | } |
229 | } | 280 | } |
230 | }; | 281 | }; |
@@ -273,4 +324,14 @@ export default class WorkspacesStore extends FeatureStore { | |||
273 | getUserWorkspacesRequest.execute(); | 324 | getUserWorkspacesRequest.execute(); |
274 | } | 325 | } |
275 | }; | 326 | }; |
327 | |||
328 | _stopPremiumActionsAndReactions = () => { | ||
329 | if (!this.isUserAllowedToUseFeature) { | ||
330 | this._stopActions(this._premiumUserActions); | ||
331 | this._stopReactions(this._premiumUserReactions); | ||
332 | } else { | ||
333 | this._startActions(this._premiumUserActions); | ||
334 | this._startReactions(this._premiumUserReactions); | ||
335 | } | ||
336 | } | ||
276 | } | 337 | } |