diff options
Diffstat (limited to 'src/features')
-rw-r--r-- | src/features/delayApp/Component.js | 2 | ||||
-rw-r--r-- | src/features/workspaces/actions.js | 14 | ||||
-rw-r--r-- | src/features/workspaces/api.js | 28 | ||||
-rw-r--r-- | src/features/workspaces/components/CreateWorkspaceForm.js | 94 | ||||
-rw-r--r-- | src/features/workspaces/components/EditWorkspaceForm.js | 142 | ||||
-rw-r--r-- | src/features/workspaces/components/WorkspaceItem.js | 42 | ||||
-rw-r--r-- | src/features/workspaces/components/WorkspacesDashboard.js | 85 | ||||
-rw-r--r-- | src/features/workspaces/containers/EditWorkspaceScreen.js | 52 | ||||
-rw-r--r-- | src/features/workspaces/containers/WorkspacesScreen.js | 38 | ||||
-rw-r--r-- | src/features/workspaces/index.js | 34 | ||||
-rw-r--r-- | src/features/workspaces/models/Workspace.js | 25 | ||||
-rw-r--r-- | src/features/workspaces/state.js | 13 | ||||
-rw-r--r-- | src/features/workspaces/store.js | 91 | ||||
-rw-r--r-- | src/features/workspaces/styles/workspaces-table.scss | 53 |
14 files changed, 712 insertions, 1 deletions
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js index ff84510e8..ff0f1f2f8 100644 --- a/src/features/delayApp/Component.js +++ b/src/features/delayApp/Component.js | |||
@@ -38,7 +38,7 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp | |||
38 | 38 | ||
39 | state = { | 39 | state = { |
40 | countdown: config.delayDuration, | 40 | countdown: config.delayDuration, |
41 | } | 41 | }; |
42 | 42 | ||
43 | countdownInterval = null; | 43 | countdownInterval = null; |
44 | 44 | ||
diff --git a/src/features/workspaces/actions.js b/src/features/workspaces/actions.js new file mode 100644 index 000000000..83d3447c3 --- /dev/null +++ b/src/features/workspaces/actions.js | |||
@@ -0,0 +1,14 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import Workspace from './models/Workspace'; | ||
3 | |||
4 | export default { | ||
5 | edit: { | ||
6 | workspace: PropTypes.instanceOf(Workspace).isRequired, | ||
7 | }, | ||
8 | create: { | ||
9 | name: PropTypes.string.isRequired, | ||
10 | }, | ||
11 | delete: { | ||
12 | workspace: PropTypes.instanceOf(Workspace).isRequired, | ||
13 | }, | ||
14 | }; | ||
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js new file mode 100644 index 000000000..fabc12455 --- /dev/null +++ b/src/features/workspaces/api.js | |||
@@ -0,0 +1,28 @@ | |||
1 | import { sendAuthRequest } from '../../api/utils/auth'; | ||
2 | import { API, API_VERSION } from '../../environment'; | ||
3 | |||
4 | export default { | ||
5 | getUserWorkspaces: async () => { | ||
6 | const url = `${API}/${API_VERSION}/workspace`; | ||
7 | const request = await sendAuthRequest(url, { method: 'GET' }); | ||
8 | if (!request.ok) throw request; | ||
9 | return request.json(); | ||
10 | }, | ||
11 | |||
12 | createWorkspace: async (name) => { | ||
13 | const url = `${API}/${API_VERSION}/workspace`; | ||
14 | const request = await sendAuthRequest(url, { | ||
15 | method: 'POST', | ||
16 | body: JSON.stringify({ name }), | ||
17 | }); | ||
18 | if (!request.ok) throw request; | ||
19 | return request.json(); | ||
20 | }, | ||
21 | |||
22 | deleteWorkspace: async (workspace) => { | ||
23 | const url = `${API}/${API_VERSION}/workspace/${workspace.id}`; | ||
24 | const request = await sendAuthRequest(url, { method: 'DELETE' }); | ||
25 | if (!request.ok) throw request; | ||
26 | return request.json(); | ||
27 | }, | ||
28 | }; | ||
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js new file mode 100644 index 000000000..d440b9bae --- /dev/null +++ b/src/features/workspaces/components/CreateWorkspaceForm.js | |||
@@ -0,0 +1,94 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import { Input, Button } from '@meetfranz/forms'; | ||
6 | import injectSheet from 'react-jss'; | ||
7 | |||
8 | import Form from '../../../lib/Form'; | ||
9 | |||
10 | const messages = defineMessages({ | ||
11 | submitButton: { | ||
12 | id: 'settings.workspace.add.form.submitButton', | ||
13 | defaultMessage: '!!!Save workspace', | ||
14 | }, | ||
15 | name: { | ||
16 | id: 'settings.workspace.add.form.name', | ||
17 | defaultMessage: '!!!Name', | ||
18 | }, | ||
19 | }); | ||
20 | |||
21 | const styles = () => ({ | ||
22 | form: { | ||
23 | display: 'flex', | ||
24 | }, | ||
25 | input: { | ||
26 | flexGrow: 1, | ||
27 | marginRight: '10px', | ||
28 | }, | ||
29 | submitButton: { | ||
30 | height: 'inherit', | ||
31 | marginTop: '17px', | ||
32 | }, | ||
33 | }); | ||
34 | |||
35 | @observer @injectSheet(styles) | ||
36 | class CreateWorkspaceForm extends Component { | ||
37 | static contextTypes = { | ||
38 | intl: intlShape, | ||
39 | }; | ||
40 | |||
41 | static propTypes = { | ||
42 | classes: PropTypes.object.isRequired, | ||
43 | onSubmit: PropTypes.func.isRequired, | ||
44 | }; | ||
45 | |||
46 | prepareForm() { | ||
47 | const { intl } = this.context; | ||
48 | const config = { | ||
49 | fields: { | ||
50 | name: { | ||
51 | label: intl.formatMessage(messages.name), | ||
52 | placeholder: intl.formatMessage(messages.name), | ||
53 | value: '', | ||
54 | }, | ||
55 | }, | ||
56 | }; | ||
57 | return new Form(config); | ||
58 | } | ||
59 | |||
60 | submitForm(form) { | ||
61 | form.submit({ | ||
62 | onSuccess: async (f) => { | ||
63 | const { onSubmit } = this.props; | ||
64 | const values = f.values(); | ||
65 | onSubmit(values); | ||
66 | }, | ||
67 | onError: async () => {}, | ||
68 | }); | ||
69 | } | ||
70 | |||
71 | render() { | ||
72 | const { intl } = this.context; | ||
73 | const { classes } = this.props; | ||
74 | const form = this.prepareForm(); | ||
75 | |||
76 | return ( | ||
77 | <div className={classes.form}> | ||
78 | <Input | ||
79 | className={classes.input} | ||
80 | {...form.$('name').bind()} | ||
81 | onEnterKey={this.submitForm.bind(this, form)} | ||
82 | /> | ||
83 | <Button | ||
84 | className={classes.submitButton} | ||
85 | type="button" | ||
86 | label={intl.formatMessage(messages.submitButton)} | ||
87 | onClick={this.submitForm.bind(this, form)} | ||
88 | /> | ||
89 | </div> | ||
90 | ); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | export default CreateWorkspaceForm; | ||
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js new file mode 100644 index 000000000..9e87b56dd --- /dev/null +++ b/src/features/workspaces/components/EditWorkspaceForm.js | |||
@@ -0,0 +1,142 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import { Link } from 'react-router'; | ||
6 | import { Input, Button } from '@meetfranz/forms'; | ||
7 | |||
8 | import Form from '../../../lib/Form'; | ||
9 | import Workspace from '../models/Workspace'; | ||
10 | |||
11 | const messages = defineMessages({ | ||
12 | buttonDelete: { | ||
13 | id: 'settings.workspace.form.buttonDelete', | ||
14 | defaultMessage: '!!!Delete workspace', | ||
15 | }, | ||
16 | buttonSave: { | ||
17 | id: 'settings.workspace.form.buttonSave', | ||
18 | defaultMessage: '!!!Save workspace', | ||
19 | }, | ||
20 | name: { | ||
21 | id: 'settings.workspace.form.name', | ||
22 | defaultMessage: '!!!Name', | ||
23 | }, | ||
24 | yourWorkspaces: { | ||
25 | id: 'settings.workspace.form.yourWorkspaces', | ||
26 | defaultMessage: '!!!Your workspaces', | ||
27 | }, | ||
28 | }); | ||
29 | |||
30 | @observer | ||
31 | class EditWorkspaceForm extends Component { | ||
32 | static contextTypes = { | ||
33 | intl: intlShape, | ||
34 | }; | ||
35 | |||
36 | static propTypes = { | ||
37 | workspace: PropTypes.instanceOf(Workspace).isRequired, | ||
38 | onSave: PropTypes.func.isRequired, | ||
39 | onDelete: PropTypes.func.isRequired, | ||
40 | isSaving: PropTypes.bool.isRequired, | ||
41 | isDeleting: PropTypes.bool.isRequired, | ||
42 | }; | ||
43 | |||
44 | prepareForm(workspace) { | ||
45 | const { intl } = this.context; | ||
46 | const config = { | ||
47 | fields: { | ||
48 | name: { | ||
49 | label: intl.formatMessage(messages.name), | ||
50 | placeholder: intl.formatMessage(messages.name), | ||
51 | value: workspace.name, | ||
52 | }, | ||
53 | }, | ||
54 | }; | ||
55 | return new Form(config); | ||
56 | } | ||
57 | |||
58 | submitForm(submitEvent, form) { | ||
59 | submitEvent.preventDefault(); | ||
60 | form.submit({ | ||
61 | onSuccess: async (f) => { | ||
62 | const { onSave } = this.props; | ||
63 | const values = f.values(); | ||
64 | onSave(values); | ||
65 | }, | ||
66 | onError: async () => {}, | ||
67 | }); | ||
68 | } | ||
69 | |||
70 | render() { | ||
71 | const { intl } = this.context; | ||
72 | const { | ||
73 | workspace, | ||
74 | isDeleting, | ||
75 | isSaving, | ||
76 | onDelete, | ||
77 | } = this.props; | ||
78 | if (!workspace) return null; | ||
79 | |||
80 | const form = this.prepareForm(workspace); | ||
81 | |||
82 | return ( | ||
83 | <div className="settings__main"> | ||
84 | <div className="settings__header"> | ||
85 | <span className="settings__header-item"> | ||
86 | <Link to="/settings/workspaces"> | ||
87 | {intl.formatMessage(messages.yourWorkspaces)} | ||
88 | </Link> | ||
89 | </span> | ||
90 | <span className="separator" /> | ||
91 | <span className="settings__header-item"> | ||
92 | {workspace.name} | ||
93 | </span> | ||
94 | </div> | ||
95 | <div className="settings__body"> | ||
96 | <form onSubmit={e => this.submitForm(e, form)} id="form"> | ||
97 | <div className="workspace-name"> | ||
98 | <Input {...form.$('name').bind()} /> | ||
99 | </div> | ||
100 | </form> | ||
101 | </div> | ||
102 | <div className="settings__controls"> | ||
103 | {/* ===== Delete Button ===== */} | ||
104 | {isDeleting ? ( | ||
105 | <Button | ||
106 | label={intl.formatMessage(messages.buttonDelete)} | ||
107 | loaded={false} | ||
108 | buttonType="secondary" | ||
109 | className="settings__delete-button" | ||
110 | disabled | ||
111 | /> | ||
112 | ) : ( | ||
113 | <Button | ||
114 | buttonType="danger" | ||
115 | label={intl.formatMessage(messages.buttonDelete)} | ||
116 | className="settings__delete-button" | ||
117 | onClick={onDelete} | ||
118 | /> | ||
119 | )} | ||
120 | {/* ===== Save Button ===== */} | ||
121 | {isSaving ? ( | ||
122 | <Button | ||
123 | type="submit" | ||
124 | label={intl.formatMessage(messages.buttonSave)} | ||
125 | loaded={!isSaving} | ||
126 | buttonType="secondary" | ||
127 | disabled | ||
128 | /> | ||
129 | ) : ( | ||
130 | <Button | ||
131 | type="submit" | ||
132 | label={intl.formatMessage(messages.buttonSave)} | ||
133 | htmlForm="form" | ||
134 | /> | ||
135 | )} | ||
136 | </div> | ||
137 | </div> | ||
138 | ); | ||
139 | } | ||
140 | } | ||
141 | |||
142 | export default EditWorkspaceForm; | ||
diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js new file mode 100644 index 000000000..b2c2a4830 --- /dev/null +++ b/src/features/workspaces/components/WorkspaceItem.js | |||
@@ -0,0 +1,42 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { intlShape } from 'react-intl'; | ||
4 | import { observer } from 'mobx-react'; | ||
5 | import classnames from 'classnames'; | ||
6 | import Workspace from '../models/Workspace'; | ||
7 | |||
8 | // const messages = defineMessages({}); | ||
9 | |||
10 | @observer | ||
11 | class WorkspaceItem extends Component { | ||
12 | static propTypes = { | ||
13 | workspace: PropTypes.instanceOf(Workspace).isRequired, | ||
14 | onItemClick: PropTypes.func.isRequired, | ||
15 | }; | ||
16 | |||
17 | static contextTypes = { | ||
18 | intl: intlShape, | ||
19 | }; | ||
20 | |||
21 | render() { | ||
22 | const { workspace, onItemClick } = this.props; | ||
23 | // const { intl } = this.context; | ||
24 | |||
25 | return ( | ||
26 | <tr | ||
27 | className={classnames({ | ||
28 | 'workspace-table__row': true, | ||
29 | })} | ||
30 | > | ||
31 | <td | ||
32 | className="workspace-table__column-name" | ||
33 | onClick={() => onItemClick(workspace)} | ||
34 | > | ||
35 | {workspace.name} | ||
36 | </td> | ||
37 | </tr> | ||
38 | ); | ||
39 | } | ||
40 | } | ||
41 | |||
42 | export default WorkspaceItem; | ||
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js new file mode 100644 index 000000000..917807302 --- /dev/null +++ b/src/features/workspaces/components/WorkspacesDashboard.js | |||
@@ -0,0 +1,85 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | |||
7 | import Loader from '../../../components/ui/Loader'; | ||
8 | import WorkspaceItem from './WorkspaceItem'; | ||
9 | import CreateWorkspaceForm from './CreateWorkspaceForm'; | ||
10 | |||
11 | const messages = defineMessages({ | ||
12 | headline: { | ||
13 | id: 'settings.workspaces.headline', | ||
14 | defaultMessage: '!!!Your workspaces', | ||
15 | }, | ||
16 | noServicesAdded: { | ||
17 | id: 'settings.workspaces.noWorkspacesAdded', | ||
18 | defaultMessage: '!!!You haven\'t added any workspaces yet.', | ||
19 | }, | ||
20 | }); | ||
21 | |||
22 | const styles = () => ({ | ||
23 | createForm: { | ||
24 | height: 'auto', | ||
25 | marginBottom: '20px', | ||
26 | }, | ||
27 | }); | ||
28 | |||
29 | @observer @injectSheet(styles) | ||
30 | class WorkspacesDashboard extends Component { | ||
31 | static propTypes = { | ||
32 | classes: PropTypes.object.isRequired, | ||
33 | isLoading: PropTypes.bool.isRequired, | ||
34 | onCreateWorkspaceSubmit: PropTypes.func.isRequired, | ||
35 | onWorkspaceClick: PropTypes.func.isRequired, | ||
36 | workspaces: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
37 | }; | ||
38 | |||
39 | static contextTypes = { | ||
40 | intl: intlShape, | ||
41 | }; | ||
42 | |||
43 | render() { | ||
44 | const { | ||
45 | workspaces, | ||
46 | isLoading, | ||
47 | onCreateWorkspaceSubmit, | ||
48 | onWorkspaceClick, | ||
49 | classes, | ||
50 | } = this.props; | ||
51 | const { intl } = this.context; | ||
52 | |||
53 | return ( | ||
54 | <div className="settings__main"> | ||
55 | <div className="settings__header"> | ||
56 | <h1>{intl.formatMessage(messages.headline)}</h1> | ||
57 | </div> | ||
58 | <div className="settings__body"> | ||
59 | <div className={classes.body}> | ||
60 | <div className={classes.createForm}> | ||
61 | <CreateWorkspaceForm onSubmit={onCreateWorkspaceSubmit} /> | ||
62 | </div> | ||
63 | {isLoading ? ( | ||
64 | <Loader /> | ||
65 | ) : ( | ||
66 | <table className="workspace-table"> | ||
67 | <tbody> | ||
68 | {workspaces.map(workspace => ( | ||
69 | <WorkspaceItem | ||
70 | key={workspace.id} | ||
71 | workspace={workspace} | ||
72 | onItemClick={w => onWorkspaceClick(w)} | ||
73 | /> | ||
74 | ))} | ||
75 | </tbody> | ||
76 | </table> | ||
77 | )} | ||
78 | </div> | ||
79 | </div> | ||
80 | </div> | ||
81 | ); | ||
82 | } | ||
83 | } | ||
84 | |||
85 | export default WorkspacesDashboard; | ||
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js new file mode 100644 index 000000000..87b6062fb --- /dev/null +++ b/src/features/workspaces/containers/EditWorkspaceScreen.js | |||
@@ -0,0 +1,52 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { inject, observer } from 'mobx-react'; | ||
3 | |||
4 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
5 | import { gaPage } from '../../../lib/analytics'; | ||
6 | import { state } from '../state'; | ||
7 | import EditWorkspaceForm from '../components/EditWorkspaceForm'; | ||
8 | import PropTypes from 'prop-types'; | ||
9 | |||
10 | @inject('stores', 'actions') @observer | ||
11 | class EditWorkspaceScreen extends Component { | ||
12 | static propTypes = { | ||
13 | actions: PropTypes.shape({ | ||
14 | workspace: PropTypes.shape({ | ||
15 | delete: PropTypes.func.isRequired, | ||
16 | }), | ||
17 | }).isRequired, | ||
18 | }; | ||
19 | |||
20 | componentDidMount() { | ||
21 | gaPage('Settings/Workspace/Edit'); | ||
22 | } | ||
23 | |||
24 | onDelete = () => { | ||
25 | const { workspaceBeingEdited } = state; | ||
26 | const { actions } = this.props; | ||
27 | if (!workspaceBeingEdited) return null; | ||
28 | actions.workspace.delete({ workspace: workspaceBeingEdited }); | ||
29 | }; | ||
30 | |||
31 | onSave = (values) => { | ||
32 | console.log('save workspace', values); | ||
33 | }; | ||
34 | |||
35 | render() { | ||
36 | const { workspaceBeingEdited } = state; | ||
37 | if (!workspaceBeingEdited) return null; | ||
38 | return ( | ||
39 | <ErrorBoundary> | ||
40 | <EditWorkspaceForm | ||
41 | workspace={workspaceBeingEdited} | ||
42 | onDelete={this.onDelete} | ||
43 | onSave={this.onSave} | ||
44 | isDeleting={false} | ||
45 | isSaving={false} | ||
46 | /> | ||
47 | </ErrorBoundary> | ||
48 | ); | ||
49 | } | ||
50 | } | ||
51 | |||
52 | export default EditWorkspaceScreen; | ||
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js new file mode 100644 index 000000000..a3876a01a --- /dev/null +++ b/src/features/workspaces/containers/WorkspacesScreen.js | |||
@@ -0,0 +1,38 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { inject, observer } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { gaPage } from '../../../lib/analytics'; | ||
5 | import { state } from '../state'; | ||
6 | import WorkspacesDashboard from '../components/WorkspacesDashboard'; | ||
7 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
8 | |||
9 | @inject('actions') @observer | ||
10 | class WorkspacesScreen extends Component { | ||
11 | static propTypes = { | ||
12 | actions: PropTypes.shape({ | ||
13 | workspace: PropTypes.shape({ | ||
14 | edit: PropTypes.func.isRequired, | ||
15 | }), | ||
16 | }).isRequired, | ||
17 | }; | ||
18 | |||
19 | componentDidMount() { | ||
20 | gaPage('Settings/Workspaces Dashboard'); | ||
21 | } | ||
22 | |||
23 | render() { | ||
24 | const { actions } = this.props; | ||
25 | return ( | ||
26 | <ErrorBoundary> | ||
27 | <WorkspacesDashboard | ||
28 | workspaces={state.workspaces} | ||
29 | isLoading={state.isLoading} | ||
30 | onCreateWorkspaceSubmit={data => actions.workspace.create(data)} | ||
31 | onWorkspaceClick={w => actions.workspace.edit({ workspace: w })} | ||
32 | /> | ||
33 | </ErrorBoundary> | ||
34 | ); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | export default WorkspacesScreen; | ||
diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js new file mode 100644 index 000000000..50ac3b414 --- /dev/null +++ b/src/features/workspaces/index.js | |||
@@ -0,0 +1,34 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import WorkspacesStore from './store'; | ||
3 | import api from './api'; | ||
4 | import { state, resetState } from './state'; | ||
5 | |||
6 | const debug = require('debug')('Franz:feature:workspaces'); | ||
7 | |||
8 | let store = null; | ||
9 | |||
10 | export default function initWorkspaces(stores, actions) { | ||
11 | const { features, user } = stores; | ||
12 | reaction( | ||
13 | () => ( | ||
14 | features.features.isWorkspaceEnabled && ( | ||
15 | !features.features.isWorkspacePremiumFeature || user.data.isPremium | ||
16 | ) | ||
17 | ), | ||
18 | (isEnabled) => { | ||
19 | if (isEnabled) { | ||
20 | debug('Initializing `workspaces` feature'); | ||
21 | store = new WorkspacesStore(stores, api, actions, state); | ||
22 | store.initialize(); | ||
23 | } else if (store) { | ||
24 | debug('Disabling `workspaces` feature'); | ||
25 | store.teardown(); | ||
26 | store = null; | ||
27 | resetState(); // Reset state to default | ||
28 | } | ||
29 | }, | ||
30 | { | ||
31 | fireImmediately: true, | ||
32 | }, | ||
33 | ); | ||
34 | } | ||
diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js new file mode 100644 index 000000000..ede2710dc --- /dev/null +++ b/src/features/workspaces/models/Workspace.js | |||
@@ -0,0 +1,25 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | |||
3 | export default class Workspace { | ||
4 | id = null; | ||
5 | |||
6 | @observable name = null; | ||
7 | |||
8 | @observable order = null; | ||
9 | |||
10 | @observable services = []; | ||
11 | |||
12 | @observable userId = null; | ||
13 | |||
14 | constructor(data) { | ||
15 | if (!data.id) { | ||
16 | throw Error('Workspace requires Id'); | ||
17 | } | ||
18 | |||
19 | this.id = data.id; | ||
20 | this.name = data.name; | ||
21 | this.order = data.order; | ||
22 | this.services = data.services; | ||
23 | this.userId = data.userId; | ||
24 | } | ||
25 | } | ||
diff --git a/src/features/workspaces/state.js b/src/features/workspaces/state.js new file mode 100644 index 000000000..f938c1470 --- /dev/null +++ b/src/features/workspaces/state.js | |||
@@ -0,0 +1,13 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | |||
3 | const defaultState = { | ||
4 | isLoading: false, | ||
5 | workspaces: [], | ||
6 | workspaceBeingEdited: null, | ||
7 | }; | ||
8 | |||
9 | export const state = observable(defaultState); | ||
10 | |||
11 | export function resetState() { | ||
12 | Object.assign(state, defaultState); | ||
13 | } | ||
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js new file mode 100644 index 000000000..a9b93f904 --- /dev/null +++ b/src/features/workspaces/store.js | |||
@@ -0,0 +1,91 @@ | |||
1 | import { observable, reaction } from 'mobx'; | ||
2 | import Store from '../../stores/lib/Store'; | ||
3 | import CachedRequest from '../../stores/lib/CachedRequest'; | ||
4 | import Workspace from './models/Workspace'; | ||
5 | import { matchRoute } from '../../helpers/routing-helpers'; | ||
6 | |||
7 | const debug = require('debug')('Franz:feature:workspaces'); | ||
8 | |||
9 | export default class WorkspacesStore extends Store { | ||
10 | @observable allWorkspacesRequest = new CachedRequest(this.api, 'getUserWorkspaces'); | ||
11 | |||
12 | constructor(stores, api, actions, state) { | ||
13 | super(stores, api, actions); | ||
14 | this.state = state; | ||
15 | } | ||
16 | |||
17 | setup() { | ||
18 | debug('fetching workspaces'); | ||
19 | this.allWorkspacesRequest.execute(); | ||
20 | |||
21 | /** | ||
22 | * Update the state workspaces array when workspaces request has results. | ||
23 | */ | ||
24 | reaction( | ||
25 | () => this.allWorkspacesRequest.result, | ||
26 | workspaces => this._setWorkspaces(workspaces), | ||
27 | ); | ||
28 | /** | ||
29 | * Update the loading state when workspace request is executing. | ||
30 | */ | ||
31 | reaction( | ||
32 | () => this.allWorkspacesRequest.isExecuting, | ||
33 | isExecuting => this._setIsLoading(isExecuting), | ||
34 | ); | ||
35 | /** | ||
36 | * Update the state with the workspace to be edited when route matches. | ||
37 | */ | ||
38 | reaction( | ||
39 | () => ({ | ||
40 | pathname: this.stores.router.location.pathname, | ||
41 | workspaces: this.state.workspaces, | ||
42 | }), | ||
43 | ({ pathname }) => { | ||
44 | const match = matchRoute('/settings/workspaces/edit/:id', pathname); | ||
45 | if (match) { | ||
46 | this.state.workspaceBeingEdited = this._getWorkspaceById(match.id); | ||
47 | } | ||
48 | }, | ||
49 | ); | ||
50 | |||
51 | this.actions.workspace.edit.listen(this._edit); | ||
52 | this.actions.workspace.create.listen(this._create); | ||
53 | this.actions.workspace.delete.listen(this._delete); | ||
54 | } | ||
55 | |||
56 | _setWorkspaces = (workspaces) => { | ||
57 | debug('setting user workspaces', workspaces.slice()); | ||
58 | this.state.workspaces = workspaces.map(data => new Workspace(data)); | ||
59 | }; | ||
60 | |||
61 | _setIsLoading = (isLoading) => { | ||
62 | this.state.isLoading = isLoading; | ||
63 | }; | ||
64 | |||
65 | _getWorkspaceById = id => this.state.workspaces.find(w => w.id === id); | ||
66 | |||
67 | _edit = ({ workspace }) => { | ||
68 | this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`); | ||
69 | }; | ||
70 | |||
71 | _create = async ({ name }) => { | ||
72 | try { | ||
73 | const result = await this.api.createWorkspace(name); | ||
74 | const workspace = new Workspace(result); | ||
75 | this.state.workspaces.push(workspace); | ||
76 | this._edit({ workspace }); | ||
77 | } catch (error) { | ||
78 | throw error; | ||
79 | } | ||
80 | }; | ||
81 | |||
82 | _delete = async ({ workspace }) => { | ||
83 | try { | ||
84 | await this.api.deleteWorkspace(workspace); | ||
85 | this.state.workspaces.remove(workspace); | ||
86 | this.stores.router.push('/settings/workspaces'); | ||
87 | } catch (error) { | ||
88 | throw error; | ||
89 | } | ||
90 | }; | ||
91 | } | ||
diff --git a/src/features/workspaces/styles/workspaces-table.scss b/src/features/workspaces/styles/workspaces-table.scss new file mode 100644 index 000000000..6d0e7b4f5 --- /dev/null +++ b/src/features/workspaces/styles/workspaces-table.scss | |||
@@ -0,0 +1,53 @@ | |||
1 | @import '../../../styles/config'; | ||
2 | |||
3 | .theme__dark .workspace-table { | ||
4 | .workspace-table__column-info .mdi { color: $dark-theme-gray-lightest; } | ||
5 | |||
6 | .workspace-table__row { | ||
7 | border-bottom: 1px solid $dark-theme-gray-darker; | ||
8 | |||
9 | &:hover { background: $dark-theme-gray-darker; } | ||
10 | &.workspace-table__row--disabled { color: $dark-theme-gray; } | ||
11 | } | ||
12 | } | ||
13 | |||
14 | .workspace-table { | ||
15 | width: 100%; | ||
16 | |||
17 | .workspace-table__toggle { | ||
18 | width: 60px; | ||
19 | |||
20 | .franz-form__field { | ||
21 | margin-bottom: 0; | ||
22 | } | ||
23 | } | ||
24 | |||
25 | .workspace-table__column-action { | ||
26 | width: 40px | ||
27 | } | ||
28 | |||
29 | .workspace-table__column-info { | ||
30 | width: 40px; | ||
31 | |||
32 | .mdi { | ||
33 | color: $theme-gray-light; | ||
34 | display: block; | ||
35 | font-size: 18px; | ||
36 | } | ||
37 | } | ||
38 | |||
39 | .workspace-table__row { | ||
40 | border-bottom: 1px solid $theme-gray-lightest; | ||
41 | |||
42 | &:hover { | ||
43 | cursor: initial; | ||
44 | background: $theme-gray-lightest; | ||
45 | } | ||
46 | |||
47 | &.workspace-table__row--disabled { | ||
48 | color: $theme-gray-light; | ||
49 | } | ||
50 | } | ||
51 | |||
52 | td { padding: 10px; } | ||
53 | } | ||