aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/settings/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/settings/services')
-rw-r--r--src/components/settings/services/EditServiceForm.js277
-rw-r--r--src/components/settings/services/ServiceError.js68
-rw-r--r--src/components/settings/services/ServiceItem.js98
-rw-r--r--src/components/settings/services/ServicesDashboard.js155
4 files changed, 598 insertions, 0 deletions
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
new file mode 100644
index 000000000..fac0f6b9a
--- /dev/null
+++ b/src/components/settings/services/EditServiceForm.js
@@ -0,0 +1,277 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { Link } from 'react-router';
5import { defineMessages, intlShape } from 'react-intl';
6import normalizeUrl from 'normalize-url';
7
8import Form from '../../../lib/Form';
9import User from '../../../models/User';
10import Recipe from '../../../models/Recipe';
11import Service from '../../../models/Service';
12import Tabs, { TabItem } from '../../ui/Tabs';
13import Input from '../../ui/Input';
14import Toggle from '../../ui/Toggle';
15import Button from '../../ui/Button';
16
17const messages = defineMessages({
18 saveService: {
19 id: 'settings.service.form.saveButton',
20 defaultMessage: '!!!Save service',
21 },
22 deleteService: {
23 id: 'settings.service.form.deleteButton',
24 defaultMessage: '!!!Delete Service',
25 },
26 availableServices: {
27 id: 'settings.service.form.availableServices',
28 defaultMessage: '!!!Available services',
29 },
30 yourServices: {
31 id: 'settings.service.form.yourServices',
32 defaultMessage: '!!!Your services',
33 },
34 addServiceHeadline: {
35 id: 'settings.service.form.addServiceHeadline',
36 defaultMessage: '!!!Add {name}',
37 },
38 editServiceHeadline: {
39 id: 'settings.service.form.editServiceHeadline',
40 defaultMessage: '!!!Edit {name}',
41 },
42 tabHosted: {
43 id: 'settings.service.form.tabHosted',
44 defaultMessage: '!!!Hosted',
45 },
46 tabOnPremise: {
47 id: 'settings.service.form.tabOnPremise',
48 defaultMessage: '!!!Self hosted ⭐️',
49 },
50 customUrlValidationError: {
51 id: 'settings.service.form.customUrlValidationError',
52 defaultMessage: '!!!Could not validate custom {name} server.',
53 },
54 customUrlPremiumInfo: {
55 id: 'settings.service.form.customUrlPremiumInfo',
56 defaultMessage: '!!!To add self hosted services, you need a Franz Premium Supporter Account.',
57 },
58 customUrlUpgradeAccount: {
59 id: 'settings.service.form.customUrlUpgradeAccount',
60 defaultMessage: '!!!Upgrade your account',
61 },
62 indirectMessageInfo: {
63 id: 'settings.service.form.indirectMessageInfo',
64 defaultMessage: '!!!You will be notified about all new messages in a channel, not just @username, @channel, @here, ...', // eslint-disable-line
65 },
66});
67
68@observer
69export default class EditServiceForm extends Component {
70 static propTypes = {
71 recipe: PropTypes.instanceOf(Recipe).isRequired,
72 // service: PropTypes.oneOfType([
73 // PropTypes.object,
74 // PropTypes.instanceOf(Service),
75 // ]),
76 service(props, propName) {
77 if (props.action === 'edit' && !(props[propName] instanceof Service)) {
78 return new Error(`'${propName}'' is expected to be of type 'Service'
79 when editing a Service`);
80 }
81
82 return null;
83 },
84 user: PropTypes.instanceOf(User).isRequired,
85 action: PropTypes.string.isRequired,
86 form: PropTypes.instanceOf(Form).isRequired,
87 onSubmit: PropTypes.func.isRequired,
88 onDelete: PropTypes.func.isRequired,
89 isSaving: PropTypes.bool.isRequired,
90 isDeleting: PropTypes.bool.isRequired,
91 };
92
93 static defaultProps = {
94 service: {},
95 };
96 static contextTypes = {
97 intl: intlShape,
98 };
99
100 state = {
101 isValidatingCustomUrl: false,
102 }
103
104 submit(e) {
105 const { recipe } = this.props;
106
107 e.preventDefault();
108 this.props.form.submit({
109 onSuccess: async (form) => {
110 const values = form.values();
111
112 let isValid = true;
113
114 if (recipe.validateUrl && values.customUrl) {
115 this.setState({ isValidatingCustomUrl: true });
116 try {
117 values.customUrl = normalizeUrl(values.customUrl);
118 isValid = await recipe.validateUrl(values.customUrl);
119 } catch (err) {
120 console.warn('ValidateURL', err);
121 isValid = false;
122 }
123 }
124
125 if (isValid) {
126 this.props.onSubmit(values);
127 } else {
128 form.invalidate('url-validation-error');
129 }
130
131 this.setState({ isValidatingCustomUrl: false });
132 },
133 onError: () => {},
134 });
135 }
136
137 render() {
138 const {
139 recipe,
140 service,
141 action,
142 user,
143 form,
144 isSaving,
145 isDeleting,
146 onDelete,
147 } = this.props;
148 const { intl } = this.context;
149
150 const { isValidatingCustomUrl } = this.state;
151
152 const deleteButton = isDeleting ? (
153 <Button
154 label={intl.formatMessage(messages.deleteService)}
155 loaded={false}
156 buttonType="secondary"
157 className="settings__delete-button"
158 disabled
159 />
160 ) : (
161 <Button
162 buttonType="danger"
163 label={intl.formatMessage(messages.deleteService)}
164 className="settings__delete-button"
165 onClick={onDelete}
166 />
167 );
168
169 return (
170 <div className="settings__main">
171 <div className="settings__header">
172 <span className="settings__header-item">
173 {action === 'add' ? (
174 <Link to="/settings/recipes">
175 {intl.formatMessage(messages.availableServices)}
176 </Link>
177 ) : (
178 <Link to="/settings/services">
179 {intl.formatMessage(messages.yourServices)}
180 </Link>
181 )}
182 </span>
183 <span className="separator" />
184 <span className="settings__header-item">
185 {action === 'add' ? (
186 intl.formatMessage(messages.addServiceHeadline, {
187 name: recipe.name,
188 })
189 ) : (
190 intl.formatMessage(messages.editServiceHeadline, {
191 name: service.name !== '' ? service.name : recipe.name,
192 })
193 )}
194 </span>
195 </div>
196 <div className="settings__body">
197 <form onSubmit={e => this.submit(e)} id="form">
198 <Input field={form.$('name')} focus />
199 {(recipe.hasTeamId || recipe.hasCustomUrl) && (
200 <Tabs
201 active={service.customUrl ? 1 : 0}
202 >
203 {recipe.hasTeamId && (
204 <TabItem title={intl.formatMessage(messages.tabHosted)}>
205 <Input field={form.$('team')} suffix={recipe.urlInputSuffix} />
206 </TabItem>
207 )}
208 {recipe.hasCustomUrl && (
209 <TabItem title={intl.formatMessage(messages.tabOnPremise)}>
210 {user.isPremium ? (
211 <div>
212 <Input field={form.$('customUrl')} />
213 {form.error === 'url-validation-error' && (
214 <p className="franz-form__error">
215 {intl.formatMessage(messages.customUrlValidationError, { name: recipe.name })}
216 </p>
217 )}
218 </div>
219 ) : (
220 <div className="center premium-info">
221 <p>{intl.formatMessage(messages.customUrlPremiumInfo)}</p>
222 <p>
223 <Link to="/settings/user" className="button">
224 {intl.formatMessage(messages.customUrlUpgradeAccount)}
225 </Link>
226 </p>
227 </div>
228 )}
229 </TabItem>
230 )}
231 </Tabs>
232 )}
233 <div className="settings__options">
234 <Toggle field={form.$('isNotificationEnabled')} />
235 {recipe.hasIndirectMessages && (
236 <div>
237 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} />
238 <p className="settings__indirect-message-help">
239 {intl.formatMessage(messages.indirectMessageInfo)}
240 </p>
241 </div>
242 )}
243 <Toggle field={form.$('isEnabled')} />
244 </div>
245 {recipe.message && (
246 <p className="settings__message">
247 <span className="mdi mdi-information" />
248 {recipe.message}
249 </p>
250 )}
251 </form>
252 </div>
253 <div className="settings__controls">
254 {/* Delete Button */}
255 {action === 'edit' && deleteButton}
256
257 {/* Save Button */}
258 {isSaving || isValidatingCustomUrl ? (
259 <Button
260 type="submit"
261 label={intl.formatMessage(messages.saveService)}
262 loaded={false}
263 buttonType="secondary"
264 disabled
265 />
266 ) : (
267 <Button
268 type="submit"
269 label={intl.formatMessage(messages.saveService)}
270 htmlForm="form"
271 />
272 )}
273 </div>
274 </div>
275 );
276 }
277}
diff --git a/src/components/settings/services/ServiceError.js b/src/components/settings/services/ServiceError.js
new file mode 100644
index 000000000..923053296
--- /dev/null
+++ b/src/components/settings/services/ServiceError.js
@@ -0,0 +1,68 @@
1import React, { Component } from 'react';
2import { observer } from 'mobx-react';
3import { Link } from 'react-router';
4import { defineMessages, intlShape } from 'react-intl';
5
6import Infobox from '../../ui/Infobox';
7import Button from '../../ui/Button';
8
9const messages = defineMessages({
10 headline: {
11 id: 'settings.service.error.headline',
12 defaultMessage: '!!!Error',
13 },
14 goBack: {
15 id: 'settings.service.error.goBack',
16 defaultMessage: '!!!Back to services',
17 },
18 availableServices: {
19 id: 'settings.service.form.availableServices',
20 defaultMessage: '!!!Available services',
21 },
22 errorMessage: {
23 id: 'settings.service.error.message',
24 defaultMessage: '!!!Could not load service recipe.',
25 },
26});
27
28@observer
29export default class EditServiceForm extends Component {
30 static contextTypes = {
31 intl: intlShape,
32 };
33
34 render() {
35 const { intl } = this.context;
36
37 return (
38 <div className="settings__main">
39 <div className="settings__header">
40 <span className="settings__header-item">
41 <Link to="/settings/recipes">
42 {intl.formatMessage(messages.availableServices)}
43 </Link>
44 </span>
45 <span className="separator" />
46 <span className="settings__header-item">
47 {intl.formatMessage(messages.headline)}
48 </span>
49 </div>
50 <div className="settings__body">
51 <Infobox
52 type="danger"
53 icon="alert"
54 >
55 {intl.formatMessage(messages.errorMessage)}
56 </Infobox>
57 </div>
58 <div className="settings__controls">
59 <Button
60 label={intl.formatMessage(messages.goBack)}
61 htmlForm="form"
62 onClick={() => window.history.back()}
63 />
64 </div>
65 </div>
66 );
67 }
68}
diff --git a/src/components/settings/services/ServiceItem.js b/src/components/settings/services/ServiceItem.js
new file mode 100644
index 000000000..20d8581d0
--- /dev/null
+++ b/src/components/settings/services/ServiceItem.js
@@ -0,0 +1,98 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4import ReactTooltip from 'react-tooltip';
5import { observer } from 'mobx-react';
6import classnames from 'classnames';
7
8import ServiceModel from '../../../models/Service';
9
10const messages = defineMessages({
11 tooltipIsDisabled: {
12 id: 'settings.services.tooltip.isDisabled',
13 defaultMessage: '!!!Service is disabled',
14 },
15 tooltipNotificationsDisabled: {
16 id: 'settings.services.tooltip.notificationsDisabled',
17 defaultMessage: '!!!Notifications are disabled',
18 },
19});
20
21@observer
22export default class ServiceItem extends Component {
23 static propTypes = {
24 service: PropTypes.instanceOf(ServiceModel).isRequired,
25 goToServiceForm: PropTypes.func.isRequired,
26 };
27 static contextTypes = {
28 intl: intlShape,
29 };
30
31 render() {
32 const {
33 service,
34 // toggleAction,
35 goToServiceForm,
36 } = this.props;
37 const { intl } = this.context;
38
39 return (
40 <tr
41 className={classnames({
42 'service-table__row': true,
43 'service-table__row--disabled': !service.isEnabled,
44 })}
45 >
46 <td
47 className="service-table__column-icon"
48 onClick={goToServiceForm}
49 >
50 <img
51 src={service.icon}
52 className={classnames({
53 'service-table__icon': true,
54 'has-custom-icon': service.hasCustomIcon,
55 })}
56 alt=""
57 />
58 </td>
59 <td
60 className="service-table__column-name"
61 onClick={goToServiceForm}
62 >
63 {service.name !== '' ? service.name : service.recipe.name}
64 </td>
65 <td
66 className="service-table__column-info"
67 onClick={goToServiceForm}
68 >
69 {!service.isEnabled && (
70 <span
71 className="mdi mdi-power"
72 data-tip={intl.formatMessage(messages.tooltipIsDisabled)}
73 />
74 )}
75 </td>
76 <td
77 className="service-table__column-info"
78 onClick={goToServiceForm}
79 >
80 {!service.isNotificationEnabled && (
81 <span
82 className="mdi mdi-message-bulleted-off"
83 data-tip={intl.formatMessage(messages.tooltipNotificationsDisabled)}
84 />
85 )}
86 <ReactTooltip place="top" type="dark" effect="solid" />
87 </td>
88 {/* <td className="service-table__column-action">
89 <input
90 type="checkbox"
91 onChange={toggleAction}
92 checked={service.isEnabled}
93 />
94 </td> */}
95 </tr>
96 );
97 }
98}
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
new file mode 100644
index 000000000..5f146b5f3
--- /dev/null
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -0,0 +1,155 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { Link } from 'react-router';
5import { defineMessages, intlShape } from 'react-intl';
6
7import SearchInput from '../../ui/SearchInput';
8import Infobox from '../../ui/Infobox';
9import Loader from '../../ui/Loader';
10import ServiceItem from './ServiceItem';
11import Appear from '../../ui/effects/Appear';
12
13const messages = defineMessages({
14 headline: {
15 id: 'settings.services.headline',
16 defaultMessage: '!!!Your services',
17 },
18 noServicesAdded: {
19 id: 'settings.services.noServicesAdded',
20 defaultMessage: '!!!You haven\'t added any services yet.',
21 },
22 discoverServices: {
23 id: 'settings.services.discoverServices',
24 defaultMessage: '!!!Discover services',
25 },
26 servicesRequestFailed: {
27 id: 'settings.services.servicesRequestFailed',
28 defaultMessage: '!!!Could not load your services',
29 },
30 tryReloadServices: {
31 id: 'settings.account.tryReloadServices',
32 defaultMessage: '!!!Try again',
33 },
34 updatedInfo: {
35 id: 'settings.services.updatedInfo',
36 defaultMessage: '!!!Your changes have been saved',
37 },
38 deletedInfo: {
39 id: 'settings.services.deletedInfo',
40 defaultMessage: '!!!Service has been deleted',
41 },
42});
43
44@observer
45export default class ServicesDashboard extends Component {
46 static propTypes = {
47 services: MobxPropTypes.arrayOrObservableArray.isRequired,
48 isLoading: PropTypes.bool.isRequired,
49 toggleService: PropTypes.func.isRequired,
50 filterServices: PropTypes.func.isRequired,
51 resetFilter: PropTypes.func.isRequired,
52 goTo: PropTypes.func.isRequired,
53 servicesRequestFailed: PropTypes.bool.isRequired,
54 retryServicesRequest: PropTypes.func.isRequired,
55 status: MobxPropTypes.arrayOrObservableArray.isRequired,
56 };
57 static contextTypes = {
58 intl: intlShape,
59 };
60
61 render() {
62 const {
63 services,
64 isLoading,
65 toggleService,
66 filterServices,
67 resetFilter,
68 goTo,
69 servicesRequestFailed,
70 retryServicesRequest,
71 status,
72 } = this.props;
73 const { intl } = this.context;
74
75 return (
76 <div className="settings__main">
77 <div className="settings__header">
78 <SearchInput
79 className="settings__search-header"
80 defaultValue={intl.formatMessage(messages.headline)}
81 onChange={needle => filterServices({ needle })}
82 onReset={() => resetFilter()}
83 />
84 </div>
85 <div className="settings__body">
86 {!isLoading && servicesRequestFailed && (
87 <div>
88 <Infobox
89 icon="alert"
90 type="danger"
91 ctaLabel={intl.formatMessage(messages.tryReloadServices)}
92 ctaLoading={isLoading}
93 ctaOnClick={retryServicesRequest}
94 >
95 {intl.formatMessage(messages.servicesRequestFailed)}
96 </Infobox>
97 </div>
98 )}
99
100 {status.length > 0 && status.includes('updated') && (
101 <Appear>
102 <Infobox
103 type="success"
104 icon="checkbox-marked-circle-outline"
105 dismissable
106 >
107 {intl.formatMessage(messages.updatedInfo)}
108 </Infobox>
109 </Appear>
110 )}
111
112 {status.length > 0 && status.includes('service-deleted') && (
113 <Appear>
114 <Infobox
115 type="success"
116 icon="checkbox-marked-circle-outline"
117 dismissable
118 >
119 {intl.formatMessage(messages.deletedInfo)}
120 </Infobox>
121 </Appear>
122 )}
123
124 {!isLoading && services.length === 0 && (
125 <div className="align-middle settings__empty-state">
126 <p className="settings__empty-text">
127 <span className="emoji">
128 <img src="./assets/images/emoji/sad.png" alt="" />
129 </span>
130 {intl.formatMessage(messages.noServicesAdded)}
131 </p>
132 <Link to="/settings/recipes" className="button">{intl.formatMessage(messages.discoverServices)}</Link>
133 </div>
134 )}
135 {isLoading ? (
136 <Loader />
137 ) : (
138 <table className="service-table">
139 <tbody>
140 {services.map(service => (
141 <ServiceItem
142 key={service.id}
143 service={service}
144 toggleAction={() => toggleService({ serviceId: service.id })}
145 goToServiceForm={() => goTo(`/settings/services/edit/${service.id}`)}
146 />
147 ))}
148 </tbody>
149 </table>
150 )}
151 </div>
152 </div>
153 );
154 }
155}