aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/workspaces/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/workspaces/components')
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.js100
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js189
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js246
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js137
-rw-r--r--src/features/workspaces/components/WorkspaceItem.js45
-rw-r--r--src/features/workspaces/components/WorkspaceServiceListItem.js75
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js91
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js195
8 files changed, 1078 insertions, 0 deletions
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js
new file mode 100644
index 000000000..2c00ea63c
--- /dev/null
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -0,0 +1,100 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import { Input, Button } from '@meetfranz/forms';
6import injectSheet from 'react-jss';
7import Form from '../../../lib/Form';
8import { required } from '../../../helpers/validation-helpers';
9import { gaEvent } from '../../../lib/analytics';
10import { GA_CATEGORY_WORKSPACES } from '../index';
11
12const messages = defineMessages({
13 submitButton: {
14 id: 'settings.workspace.add.form.submitButton',
15 defaultMessage: '!!!Create workspace',
16 },
17 name: {
18 id: 'settings.workspace.add.form.name',
19 defaultMessage: '!!!Name',
20 },
21});
22
23const styles = () => ({
24 form: {
25 display: 'flex',
26 },
27 input: {
28 flexGrow: 1,
29 marginRight: '10px',
30 },
31 submitButton: {
32 height: 'inherit',
33 },
34});
35
36@injectSheet(styles) @observer
37class CreateWorkspaceForm extends Component {
38 static contextTypes = {
39 intl: intlShape,
40 };
41
42 static propTypes = {
43 classes: PropTypes.object.isRequired,
44 isSubmitting: PropTypes.bool.isRequired,
45 onSubmit: PropTypes.func.isRequired,
46 };
47
48 form = (() => {
49 const { intl } = this.context;
50 return new Form({
51 fields: {
52 name: {
53 label: intl.formatMessage(messages.name),
54 placeholder: intl.formatMessage(messages.name),
55 value: '',
56 validators: [required],
57 },
58 },
59 });
60 })();
61
62 submitForm() {
63 const { form } = this;
64 form.submit({
65 onSuccess: async (f) => {
66 const { onSubmit } = this.props;
67 const values = f.values();
68 onSubmit(values);
69 gaEvent(GA_CATEGORY_WORKSPACES, 'create', values.name);
70 },
71 });
72 }
73
74 render() {
75 const { intl } = this.context;
76 const { classes, isSubmitting } = this.props;
77 const { form } = this;
78 return (
79 <div className={classes.form}>
80 <Input
81 className={classes.input}
82 {...form.$('name').bind()}
83 showLabel={false}
84 onEnterKey={this.submitForm.bind(this, form)}
85 focus
86 />
87 <Button
88 className={classes.submitButton}
89 type="submit"
90 label={intl.formatMessage(messages.submitButton)}
91 onClick={this.submitForm.bind(this, form)}
92 busy={isSubmitting}
93 buttonType={isSubmitting ? 'secondary' : 'primary'}
94 />
95 </div>
96 );
97 }
98}
99
100export default CreateWorkspaceForm;
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
new file mode 100644
index 000000000..bba4485ff
--- /dev/null
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -0,0 +1,189 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router';
6import { Input, Button } from '@meetfranz/forms';
7import injectSheet from 'react-jss';
8
9import Workspace from '../models/Workspace';
10import Service from '../../../models/Service';
11import Form from '../../../lib/Form';
12import { required } from '../../../helpers/validation-helpers';
13import WorkspaceServiceListItem from './WorkspaceServiceListItem';
14import Request from '../../../stores/lib/Request';
15import { gaEvent } from '../../../lib/analytics';
16import { GA_CATEGORY_WORKSPACES } from '../index';
17
18const messages = defineMessages({
19 buttonDelete: {
20 id: 'settings.workspace.form.buttonDelete',
21 defaultMessage: '!!!Delete workspace',
22 },
23 buttonSave: {
24 id: 'settings.workspace.form.buttonSave',
25 defaultMessage: '!!!Save workspace',
26 },
27 name: {
28 id: 'settings.workspace.form.name',
29 defaultMessage: '!!!Name',
30 },
31 yourWorkspaces: {
32 id: 'settings.workspace.form.yourWorkspaces',
33 defaultMessage: '!!!Your workspaces',
34 },
35 servicesInWorkspaceHeadline: {
36 id: 'settings.workspace.form.servicesInWorkspaceHeadline',
37 defaultMessage: '!!!Services in this Workspace',
38 },
39});
40
41const styles = () => ({
42 nameInput: {
43 height: 'auto',
44 },
45 serviceList: {
46 height: 'auto',
47 },
48});
49
50@injectSheet(styles) @observer
51class EditWorkspaceForm extends Component {
52 static contextTypes = {
53 intl: intlShape,
54 };
55
56 static propTypes = {
57 classes: PropTypes.object.isRequired,
58 onDelete: PropTypes.func.isRequired,
59 onSave: PropTypes.func.isRequired,
60 services: PropTypes.arrayOf(PropTypes.instanceOf(Service)).isRequired,
61 workspace: PropTypes.instanceOf(Workspace).isRequired,
62 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
63 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
64 };
65
66 form = this.prepareWorkspaceForm(this.props.workspace);
67
68 componentWillReceiveProps(nextProps) {
69 const { workspace } = this.props;
70 if (workspace.id !== nextProps.workspace.id) {
71 this.form = this.prepareWorkspaceForm(nextProps.workspace);
72 }
73 }
74
75 prepareWorkspaceForm(workspace) {
76 const { intl } = this.context;
77 return new Form({
78 fields: {
79 name: {
80 label: intl.formatMessage(messages.name),
81 placeholder: intl.formatMessage(messages.name),
82 value: workspace.name,
83 validators: [required],
84 },
85 services: {
86 value: workspace.services.slice(),
87 },
88 },
89 });
90 }
91
92 save(form) {
93 form.submit({
94 onSuccess: async (f) => {
95 const { onSave } = this.props;
96 const values = f.values();
97 onSave(values);
98 gaEvent(GA_CATEGORY_WORKSPACES, 'save');
99 },
100 onError: async () => {},
101 });
102 }
103
104 delete() {
105 const { onDelete } = this.props;
106 onDelete();
107 gaEvent(GA_CATEGORY_WORKSPACES, 'delete');
108 }
109
110 toggleService(service) {
111 const servicesField = this.form.$('services');
112 const serviceIds = servicesField.value;
113 if (serviceIds.includes(service.id)) {
114 serviceIds.splice(serviceIds.indexOf(service.id), 1);
115 } else {
116 serviceIds.push(service.id);
117 }
118 servicesField.set(serviceIds);
119 }
120
121 render() {
122 const { intl } = this.context;
123 const {
124 classes,
125 workspace,
126 services,
127 deleteWorkspaceRequest,
128 updateWorkspaceRequest,
129 } = this.props;
130 const { form } = this;
131 const workspaceServices = form.$('services').value;
132 const isDeleting = deleteWorkspaceRequest.isExecuting;
133 const isSaving = updateWorkspaceRequest.isExecuting;
134 return (
135 <div className="settings__main">
136 <div className="settings__header">
137 <span className="settings__header-item">
138 <Link to="/settings/workspaces">
139 {intl.formatMessage(messages.yourWorkspaces)}
140 </Link>
141 </span>
142 <span className="separator" />
143 <span className="settings__header-item">
144 {workspace.name}
145 </span>
146 </div>
147 <div className="settings__body">
148 <div className={classes.nameInput}>
149 <Input {...form.$('name').bind()} />
150 </div>
151 <h2>{intl.formatMessage(messages.servicesInWorkspaceHeadline)}</h2>
152 <div className={classes.serviceList}>
153 {services.map(s => (
154 <WorkspaceServiceListItem
155 key={s.id}
156 service={s}
157 isInWorkspace={workspaceServices.includes(s.id)}
158 onToggle={() => this.toggleService(s)}
159 />
160 ))}
161 </div>
162 </div>
163 <div className="settings__controls">
164 {/* ===== Delete Button ===== */}
165 <Button
166 label={intl.formatMessage(messages.buttonDelete)}
167 loaded={false}
168 busy={isDeleting}
169 buttonType={isDeleting ? 'secondary' : 'danger'}
170 className="settings__delete-button"
171 disabled={isDeleting}
172 onClick={this.delete.bind(this)}
173 />
174 {/* ===== Save Button ===== */}
175 <Button
176 type="submit"
177 label={intl.formatMessage(messages.buttonSave)}
178 busy={isSaving}
179 buttonType={isSaving ? 'secondary' : 'primary'}
180 onClick={this.save.bind(this, form)}
181 disabled={isSaving}
182 />
183 </div>
184 </div>
185 );
186 }
187}
188
189export default EditWorkspaceForm;
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
new file mode 100644
index 000000000..684e50dd0
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -0,0 +1,246 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, FormattedHTMLMessage, intlShape } from 'react-intl';
6import { H1, Icon, ProBadge } from '@meetfranz/ui';
7import { Button } from '@meetfranz/forms/lib';
8import ReactTooltip from 'react-tooltip';
9
10import WorkspaceDrawerItem from './WorkspaceDrawerItem';
11import { workspaceActions } from '../actions';
12import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index';
13import { gaEvent } from '../../../lib/analytics';
14
15const messages = defineMessages({
16 headline: {
17 id: 'workspaceDrawer.headline',
18 defaultMessage: '!!!Workspaces',
19 },
20 allServices: {
21 id: 'workspaceDrawer.allServices',
22 defaultMessage: '!!!All services',
23 },
24 workspacesSettingsTooltip: {
25 id: 'workspaceDrawer.workspacesSettingsTooltip',
26 defaultMessage: '!!!Workspaces settings',
27 },
28 workspaceFeatureInfo: {
29 id: 'workspaceDrawer.workspaceFeatureInfo',
30 defaultMessage: '!!!Info about workspace feature',
31 },
32 premiumCtaButtonLabel: {
33 id: 'workspaceDrawer.premiumCtaButtonLabel',
34 defaultMessage: '!!!Create your first workspace',
35 },
36 reactivatePremiumAccount: {
37 id: 'workspaceDrawer.reactivatePremiumAccountLabel',
38 defaultMessage: '!!!Reactivate premium account',
39 },
40 addNewWorkspaceLabel: {
41 id: 'workspaceDrawer.addNewWorkspaceLabel',
42 defaultMessage: '!!!add new workspace',
43 },
44 premiumFeatureBadge: {
45 id: 'workspaceDrawer.proFeatureBadge',
46 defaultMessage: '!!!Premium feature',
47 },
48});
49
50const styles = theme => ({
51 drawer: {
52 background: theme.workspaces.drawer.background,
53 width: `${theme.workspaces.drawer.width}px`,
54 },
55 headline: {
56 fontSize: '24px',
57 marginTop: '38px',
58 marginBottom: '25px',
59 marginLeft: theme.workspaces.drawer.padding,
60 },
61 headlineProBadge: {
62 marginRight: 15,
63 },
64 workspacesSettingsButton: {
65 float: 'right',
66 marginRight: theme.workspaces.drawer.padding,
67 marginTop: '2px',
68 },
69 workspacesSettingsButtonIcon: {
70 fill: theme.workspaces.drawer.buttons.color,
71 '&:hover': {
72 fill: theme.workspaces.drawer.buttons.hoverColor,
73 },
74 },
75 workspaces: {
76 height: 'auto',
77 },
78 premiumAnnouncement: {
79 padding: '20px',
80 paddingTop: '0',
81 height: 'auto',
82 },
83 premiumCtaButton: {
84 marginTop: '20px',
85 width: '100%',
86 color: 'white !important',
87 },
88 addNewWorkspaceLabel: {
89 height: 'auto',
90 color: theme.workspaces.drawer.buttons.color,
91 marginTop: 40,
92 textAlign: 'center',
93 '& > svg': {
94 fill: theme.workspaces.drawer.buttons.color,
95 },
96 '& > span': {
97 fontSize: '13px',
98 marginLeft: 10,
99 position: 'relative',
100 top: -3,
101 },
102 '&:hover': {
103 color: theme.workspaces.drawer.buttons.hoverColor,
104 '& > svg': {
105 fill: theme.workspaces.drawer.buttons.hoverColor,
106 },
107 },
108 },
109});
110
111@injectSheet(styles) @observer
112class WorkspaceDrawer extends Component {
113 static propTypes = {
114 classes: PropTypes.object.isRequired,
115 getServicesForWorkspace: PropTypes.func.isRequired,
116 onUpgradeAccountClick: PropTypes.func.isRequired,
117 };
118
119 static contextTypes = {
120 intl: intlShape,
121 };
122
123 componentDidMount() {
124 ReactTooltip.rebuild();
125 }
126
127 render() {
128 const {
129 classes,
130 getServicesForWorkspace,
131 onUpgradeAccountClick,
132 } = this.props;
133 const { intl } = this.context;
134 const {
135 activeWorkspace,
136 isSwitchingWorkspace,
137 nextWorkspace,
138 workspaces,
139 } = workspaceStore;
140 const actualWorkspace = isSwitchingWorkspace ? nextWorkspace : activeWorkspace;
141 return (
142 <div className={classes.drawer}>
143 <H1 className={classes.headline}>
144 {workspaceStore.isPremiumUpgradeRequired && (
145 <span
146 className={classes.headlineProBadge}
147 data-tip={`${intl.formatMessage(messages.premiumFeatureBadge)}`}
148 >
149 <ProBadge />
150 </span>
151 )}
152 {intl.formatMessage(messages.headline)}
153 <span
154 className={classes.workspacesSettingsButton}
155 onClick={() => {
156 workspaceActions.openWorkspaceSettings();
157 gaEvent(GA_CATEGORY_WORKSPACES, 'settings', 'drawerHeadline');
158 }}
159 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`}
160 >
161 <Icon
162 icon="mdiSettings"
163 size={1.5}
164 className={classes.workspacesSettingsButtonIcon}
165 />
166 </span>
167 </H1>
168 {workspaceStore.isPremiumUpgradeRequired ? (
169 <div className={classes.premiumAnnouncement}>
170 <FormattedHTMLMessage {...messages.workspaceFeatureInfo} />
171 {workspaceStore.userHasWorkspaces ? (
172 <Button
173 className={classes.premiumCtaButton}
174 buttonType="primary"
175 label={intl.formatMessage(messages.reactivatePremiumAccount)}
176 icon="mdiStar"
177 onClick={() => {
178 onUpgradeAccountClick();
179 gaEvent('User', 'upgrade', 'workspaceDrawer');
180 }}
181 />
182 ) : (
183 <Button
184 className={classes.premiumCtaButton}
185 buttonType="primary"
186 label={intl.formatMessage(messages.premiumCtaButtonLabel)}
187 icon="mdiPlusBox"
188 onClick={() => {
189 workspaceActions.openWorkspaceSettings();
190 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta');
191 }}
192 />
193 )}
194 </div>
195 ) : (
196 <div className={classes.workspaces}>
197 <WorkspaceDrawerItem
198 name={intl.formatMessage(messages.allServices)}
199 onClick={() => {
200 workspaceActions.deactivate();
201 workspaceActions.toggleWorkspaceDrawer();
202 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
203 }}
204 services={getServicesForWorkspace(null)}
205 isActive={actualWorkspace == null}
206 />
207 {workspaces.map(workspace => (
208 <WorkspaceDrawerItem
209 key={workspace.id}
210 name={workspace.name}
211 isActive={actualWorkspace === workspace}
212 onClick={() => {
213 if (actualWorkspace === workspace) return;
214 workspaceActions.activate({ workspace });
215 workspaceActions.toggleWorkspaceDrawer();
216 gaEvent(GA_CATEGORY_WORKSPACES, 'switch', 'drawer');
217 }}
218 onContextMenuEditClick={() => workspaceActions.edit({ workspace })}
219 services={getServicesForWorkspace(workspace)}
220 />
221 ))}
222 <div
223 className={classes.addNewWorkspaceLabel}
224 onClick={() => {
225 workspaceActions.openWorkspaceSettings();
226 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerAddLabel');
227 }}
228 >
229 <Icon
230 icon="mdiPlusBox"
231 size={1}
232 className={classes.workspacesSettingsButtonIcon}
233 />
234 <span>
235 {intl.formatMessage(messages.addNewWorkspaceLabel)}
236 </span>
237 </div>
238 </div>
239 )}
240 <ReactTooltip place="right" type="dark" effect="solid" />
241 </div>
242 );
243 }
244}
245
246export default WorkspaceDrawer;
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
new file mode 100644
index 000000000..59a2144d3
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -0,0 +1,137 @@
1import { remote } from 'electron';
2import React, { Component } from 'react';
3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7import { defineMessages, intlShape } from 'react-intl';
8
9const { Menu } = remote;
10
11const messages = defineMessages({
12 noServicesAddedYet: {
13 id: 'workspaceDrawer.item.noServicesAddedYet',
14 defaultMessage: '!!!No services added yet',
15 },
16 contextMenuEdit: {
17 id: 'workspaceDrawer.item.contextMenuEdit',
18 defaultMessage: '!!!edit',
19 },
20});
21
22const styles = theme => ({
23 item: {
24 height: '67px',
25 padding: `15px ${theme.workspaces.drawer.padding}px`,
26 borderBottom: `1px solid ${theme.workspaces.drawer.listItem.border}`,
27 transition: 'background-color 300ms ease-out',
28 '&:first-child': {
29 borderTop: `1px solid ${theme.workspaces.drawer.listItem.border}`,
30 },
31 '&:hover': {
32 backgroundColor: theme.workspaces.drawer.listItem.hoverBackground,
33 },
34 },
35 isActiveItem: {
36 backgroundColor: theme.workspaces.drawer.listItem.activeBackground,
37 '&:hover': {
38 backgroundColor: theme.workspaces.drawer.listItem.activeBackground,
39 },
40 },
41 name: {
42 marginTop: '4px',
43 color: theme.workspaces.drawer.listItem.name.color,
44 },
45 activeName: {
46 color: theme.workspaces.drawer.listItem.name.activeColor,
47 },
48 services: {
49 display: 'block',
50 fontSize: '11px',
51 marginTop: '5px',
52 color: theme.workspaces.drawer.listItem.services.color,
53 whiteSpace: 'nowrap',
54 textOverflow: 'ellipsis',
55 overflow: 'hidden',
56 lineHeight: '15px',
57 },
58 activeServices: {
59 color: theme.workspaces.drawer.listItem.services.active,
60 },
61});
62
63@injectSheet(styles) @observer
64class WorkspaceDrawerItem extends Component {
65 static propTypes = {
66 classes: PropTypes.object.isRequired,
67 isActive: PropTypes.bool.isRequired,
68 name: PropTypes.string.isRequired,
69 onClick: PropTypes.func.isRequired,
70 services: PropTypes.arrayOf(PropTypes.string).isRequired,
71 onContextMenuEditClick: PropTypes.func,
72 };
73
74 static defaultProps = {
75 onContextMenuEditClick: null,
76 };
77
78 static contextTypes = {
79 intl: intlShape,
80 };
81
82 render() {
83 const {
84 classes,
85 isActive,
86 name,
87 onClick,
88 onContextMenuEditClick,
89 services,
90 } = this.props;
91 const { intl } = this.context;
92
93 const contextMenuTemplate = [{
94 label: name,
95 enabled: false,
96 }, {
97 type: 'separator',
98 }, {
99 label: intl.formatMessage(messages.contextMenuEdit),
100 click: onContextMenuEditClick,
101 }];
102
103 const contextMenu = Menu.buildFromTemplate(contextMenuTemplate);
104
105 return (
106 <div
107 className={classnames([
108 classes.item,
109 isActive ? classes.isActiveItem : null,
110 ])}
111 onClick={onClick}
112 onContextMenu={() => (
113 onContextMenuEditClick && contextMenu.popup(remote.getCurrentWindow())
114 )}
115 >
116 <span
117 className={classnames([
118 classes.name,
119 isActive ? classes.activeName : null,
120 ])}
121 >
122 {name}
123 </span>
124 <span
125 className={classnames([
126 classes.services,
127 isActive ? classes.activeServices : null,
128 ])}
129 >
130 {services.length ? services.join(', ') : intl.formatMessage(messages.noServicesAddedYet)}
131 </span>
132 </div>
133 );
134 }
135}
136
137export default WorkspaceDrawerItem;
diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js
new file mode 100644
index 000000000..cc4b1a3ba
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceItem.js
@@ -0,0 +1,45 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { intlShape } from 'react-intl';
4import { observer } from 'mobx-react';
5import injectSheet from 'react-jss';
6
7import Workspace from '../models/Workspace';
8
9const styles = theme => ({
10 row: {
11 height: theme.workspaces.settings.listItems.height,
12 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
13 '&:hover': {
14 background: theme.workspaces.settings.listItems.hoverBgColor,
15 },
16 },
17 columnName: {},
18});
19
20@injectSheet(styles) @observer
21class WorkspaceItem extends Component {
22 static propTypes = {
23 classes: PropTypes.object.isRequired,
24 workspace: PropTypes.instanceOf(Workspace).isRequired,
25 onItemClick: PropTypes.func.isRequired,
26 };
27
28 static contextTypes = {
29 intl: intlShape,
30 };
31
32 render() {
33 const { classes, workspace, onItemClick } = this.props;
34
35 return (
36 <tr className={classes.row}>
37 <td onClick={() => onItemClick(workspace)}>
38 {workspace.name}
39 </td>
40 </tr>
41 );
42 }
43}
44
45export default WorkspaceItem;
diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.js b/src/features/workspaces/components/WorkspaceServiceListItem.js
new file mode 100644
index 000000000..e05b21440
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceServiceListItem.js
@@ -0,0 +1,75 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import classnames from 'classnames';
6import { Toggle } from '@meetfranz/forms';
7
8import Service from '../../../models/Service';
9import ServiceIcon from '../../../components/ui/ServiceIcon';
10
11const styles = theme => ({
12 listItem: {
13 height: theme.workspaces.settings.listItems.height,
14 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
15 display: 'flex',
16 alignItems: 'center',
17 },
18 serviceIcon: {
19 padding: theme.workspaces.settings.listItems.padding,
20 },
21 toggle: {
22 height: 'auto',
23 margin: 0,
24 },
25 label: {
26 padding: theme.workspaces.settings.listItems.padding,
27 flexGrow: 1,
28 },
29 disabledLabel: {
30 color: theme.workspaces.settings.listItems.disabled.color,
31 },
32});
33
34@injectSheet(styles) @observer
35class WorkspaceServiceListItem extends Component {
36 static propTypes = {
37 classes: PropTypes.object.isRequired,
38 isInWorkspace: PropTypes.bool.isRequired,
39 onToggle: PropTypes.func.isRequired,
40 service: PropTypes.instanceOf(Service).isRequired,
41 };
42
43 render() {
44 const {
45 classes,
46 isInWorkspace,
47 onToggle,
48 service,
49 } = this.props;
50
51 return (
52 <div className={classes.listItem}>
53 <ServiceIcon
54 className={classes.serviceIcon}
55 service={service}
56 />
57 <span
58 className={classnames([
59 classes.label,
60 service.isEnabled ? null : classes.disabledLabel,
61 ])}
62 >
63 {service.name}
64 </span>
65 <Toggle
66 className={classes.toggle}
67 checked={isInWorkspace}
68 onChange={onToggle}
69 />
70 </div>
71 );
72 }
73}
74
75export default WorkspaceServiceListItem;
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
new file mode 100644
index 000000000..c4a800a7b
--- /dev/null
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -0,0 +1,91 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import classnames from 'classnames';
6import { Loader } from '@meetfranz/ui';
7import { defineMessages, intlShape } from 'react-intl';
8
9import { workspaceStore } from '../index';
10
11const messages = defineMessages({
12 switchingTo: {
13 id: 'workspaces.switchingIndicator.switchingTo',
14 defaultMessage: '!!!Switching to',
15 },
16});
17
18const styles = theme => ({
19 wrapper: {
20 display: 'flex',
21 alignItems: 'flex-start',
22 position: 'absolute',
23 transition: 'width 0.5s ease',
24 width: '100%',
25 marginTop: '20px',
26 },
27 wrapperWhenDrawerIsOpen: {
28 width: `calc(100% - ${theme.workspaces.drawer.width}px)`,
29 },
30 component: {
31 background: 'rgba(20, 20, 20, 0.4)',
32 padding: '10px 20px',
33 display: 'flex',
34 width: 'auto',
35 height: 'auto',
36 margin: [0, 'auto'],
37 borderRadius: 6,
38 alignItems: 'center',
39 zIndex: 200,
40 },
41 spinner: {
42 width: 40,
43 height: 40,
44 marginRight: 10,
45 },
46 message: {
47 fontSize: 16,
48 whiteSpace: 'nowrap',
49 color: theme.colorAppLoaderSpinner,
50 },
51});
52
53@injectSheet(styles) @observer
54class WorkspaceSwitchingIndicator extends Component {
55 static propTypes = {
56 classes: PropTypes.object.isRequired,
57 theme: PropTypes.object.isRequired,
58 };
59
60 static contextTypes = {
61 intl: intlShape,
62 };
63
64 render() {
65 const { classes, theme } = this.props;
66 const { intl } = this.context;
67 const { isSwitchingWorkspace, isWorkspaceDrawerOpen, nextWorkspace } = workspaceStore;
68 if (!isSwitchingWorkspace) return null;
69 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services';
70 return (
71 <div
72 className={classnames([
73 classes.wrapper,
74 isWorkspaceDrawerOpen ? classes.wrapperWhenDrawerIsOpen : null,
75 ])}
76 >
77 <div className={classes.component}>
78 <Loader
79 className={classes.spinner}
80 color={theme.workspaces.switchingIndicator.spinnerColor}
81 />
82 <p className={classes.message}>
83 {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`}
84 </p>
85 </div>
86 </div>
87 );
88 }
89}
90
91export default WorkspaceSwitchingIndicator;
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
new file mode 100644
index 000000000..dd4381a15
--- /dev/null
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -0,0 +1,195 @@
1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6import { Infobox } from '@meetfranz/ui';
7
8import Loader from '../../../components/ui/Loader';
9import WorkspaceItem from './WorkspaceItem';
10import CreateWorkspaceForm from './CreateWorkspaceForm';
11import Request from '../../../stores/lib/Request';
12import Appear from '../../../components/ui/effects/Appear';
13import { workspaceStore } from '../index';
14import PremiumFeatureContainer from '../../../components/ui/PremiumFeatureContainer';
15
16const messages = defineMessages({
17 headline: {
18 id: 'settings.workspaces.headline',
19 defaultMessage: '!!!Your workspaces',
20 },
21 noServicesAdded: {
22 id: 'settings.workspaces.noWorkspacesAdded',
23 defaultMessage: '!!!You haven\'t added any workspaces yet.',
24 },
25 workspacesRequestFailed: {
26 id: 'settings.workspaces.workspacesRequestFailed',
27 defaultMessage: '!!!Could not load your workspaces',
28 },
29 tryReloadWorkspaces: {
30 id: 'settings.workspaces.tryReloadWorkspaces',
31 defaultMessage: '!!!Try again',
32 },
33 updatedInfo: {
34 id: 'settings.workspaces.updatedInfo',
35 defaultMessage: '!!!Your changes have been saved',
36 },
37 deletedInfo: {
38 id: 'settings.workspaces.deletedInfo',
39 defaultMessage: '!!!Workspace has been deleted',
40 },
41 workspaceFeatureInfo: {
42 id: 'settings.workspaces.workspaceFeatureInfo',
43 defaultMessage: '!!!Info about workspace feature',
44 },
45 workspaceFeatureHeadline: {
46 id: 'settings.workspaces.workspaceFeatureHeadline',
47 defaultMessage: '!!!Less is More: Introducing Franz Workspaces',
48 },
49});
50
51const styles = theme => ({
52 table: {
53 width: '100%',
54 '& td': {
55 padding: '10px',
56 },
57 },
58 createForm: {
59 height: 'auto',
60 },
61 appear: {
62 height: 'auto',
63 },
64 premiumAnnouncement: {
65 padding: '20px',
66 backgroundColor: '#3498db',
67 marginLeft: '-20px',
68 marginBottom: '20px',
69 height: 'auto',
70 color: 'white',
71 borderRadius: theme.borderRadius,
72 },
73});
74
75@injectSheet(styles) @observer
76class WorkspacesDashboard extends Component {
77 static propTypes = {
78 classes: PropTypes.object.isRequired,
79 getUserWorkspacesRequest: PropTypes.instanceOf(Request).isRequired,
80 createWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
81 deleteWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
82 updateWorkspaceRequest: PropTypes.instanceOf(Request).isRequired,
83 onCreateWorkspaceSubmit: PropTypes.func.isRequired,
84 onWorkspaceClick: PropTypes.func.isRequired,
85 workspaces: MobxPropTypes.arrayOrObservableArray.isRequired,
86 };
87
88 static contextTypes = {
89 intl: intlShape,
90 };
91
92 render() {
93 const {
94 classes,
95 getUserWorkspacesRequest,
96 createWorkspaceRequest,
97 deleteWorkspaceRequest,
98 updateWorkspaceRequest,
99 onCreateWorkspaceSubmit,
100 onWorkspaceClick,
101 workspaces,
102 } = this.props;
103 const { intl } = this.context;
104 return (
105 <div className="settings__main">
106 <div className="settings__header">
107 <h1>{intl.formatMessage(messages.headline)}</h1>
108 </div>
109 <div className="settings__body">
110
111 {/* ===== Workspace updated info ===== */}
112 {updateWorkspaceRequest.wasExecuted && updateWorkspaceRequest.result && (
113 <Appear className={classes.appear}>
114 <Infobox
115 type="success"
116 icon="mdiCheckboxMarkedCircleOutline"
117 dismissable
118 onUnmount={updateWorkspaceRequest.reset}
119 >
120 {intl.formatMessage(messages.updatedInfo)}
121 </Infobox>
122 </Appear>
123 )}
124
125 {/* ===== Workspace deleted info ===== */}
126 {deleteWorkspaceRequest.wasExecuted && deleteWorkspaceRequest.result && (
127 <Appear className={classes.appear}>
128 <Infobox
129 type="success"
130 icon="mdiCheckboxMarkedCircleOutline"
131 dismissable
132 onUnmount={deleteWorkspaceRequest.reset}
133 >
134 {intl.formatMessage(messages.deletedInfo)}
135 </Infobox>
136 </Appear>
137 )}
138
139 {workspaceStore.isPremiumUpgradeRequired && (
140 <div className={classes.premiumAnnouncement}>
141 <h2>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h2>
142 <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p>
143 </div>
144 )}
145
146 <PremiumFeatureContainer
147 condition={workspaceStore.isPremiumFeature}
148 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'workspaces' }}
149 >
150 {/* ===== Create workspace form ===== */}
151 <div className={classes.createForm}>
152 <CreateWorkspaceForm
153 isSubmitting={createWorkspaceRequest.isExecuting}
154 onSubmit={onCreateWorkspaceSubmit}
155 />
156 </div>
157 {getUserWorkspacesRequest.isExecuting ? (
158 <Loader />
159 ) : (
160 <Fragment>
161 {/* ===== Workspace could not be loaded error ===== */}
162 {getUserWorkspacesRequest.error ? (
163 <Infobox
164 icon="alert"
165 type="danger"
166 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)}
167 ctaLoading={getUserWorkspacesRequest.isExecuting}
168 ctaOnClick={getUserWorkspacesRequest.retry}
169 >
170 {intl.formatMessage(messages.workspacesRequestFailed)}
171 </Infobox>
172 ) : (
173 <table className={classes.table}>
174 {/* ===== Workspaces list ===== */}
175 <tbody>
176 {workspaces.map(workspace => (
177 <WorkspaceItem
178 key={workspace.id}
179 workspace={workspace}
180 onItemClick={w => onWorkspaceClick(w)}
181 />
182 ))}
183 </tbody>
184 </table>
185 )}
186 </Fragment>
187 )}
188 </PremiumFeatureContainer>
189 </div>
190 </div>
191 );
192 }
193}
194
195export default WorkspacesDashboard;