diff options
author | 2017-10-13 12:29:40 +0200 | |
---|---|---|
committer | 2017-10-13 12:29:40 +0200 | |
commit | 58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch) | |
tree | 1211600c2a5d3b5f81c435c6896618111a611720 /src/components/settings/services | |
download | ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.gz ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.zst ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.zip |
initial commit
Diffstat (limited to 'src/components/settings/services')
-rw-r--r-- | src/components/settings/services/EditServiceForm.js | 277 | ||||
-rw-r--r-- | src/components/settings/services/ServiceError.js | 68 | ||||
-rw-r--r-- | src/components/settings/services/ServiceItem.js | 98 | ||||
-rw-r--r-- | src/components/settings/services/ServicesDashboard.js | 155 |
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { Link } from 'react-router'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | import normalizeUrl from 'normalize-url'; | ||
7 | |||
8 | import Form from '../../../lib/Form'; | ||
9 | import User from '../../../models/User'; | ||
10 | import Recipe from '../../../models/Recipe'; | ||
11 | import Service from '../../../models/Service'; | ||
12 | import Tabs, { TabItem } from '../../ui/Tabs'; | ||
13 | import Input from '../../ui/Input'; | ||
14 | import Toggle from '../../ui/Toggle'; | ||
15 | import Button from '../../ui/Button'; | ||
16 | |||
17 | const 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 | ||
69 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { observer } from 'mobx-react'; | ||
3 | import { Link } from 'react-router'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import Infobox from '../../ui/Infobox'; | ||
7 | import Button from '../../ui/Button'; | ||
8 | |||
9 | const 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 | ||
29 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { defineMessages, intlShape } from 'react-intl'; | ||
4 | import ReactTooltip from 'react-tooltip'; | ||
5 | import { observer } from 'mobx-react'; | ||
6 | import classnames from 'classnames'; | ||
7 | |||
8 | import ServiceModel from '../../../models/Service'; | ||
9 | |||
10 | const 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 | ||
22 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { Link } from 'react-router'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | |||
7 | import SearchInput from '../../ui/SearchInput'; | ||
8 | import Infobox from '../../ui/Infobox'; | ||
9 | import Loader from '../../ui/Loader'; | ||
10 | import ServiceItem from './ServiceItem'; | ||
11 | import Appear from '../../ui/effects/Appear'; | ||
12 | |||
13 | const 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 | ||
45 | export 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 | } | ||