diff options
author | Stefan Malzner <stefan@adlk.io> | 2017-10-13 12:29:40 +0200 |
---|---|---|
committer | Stefan Malzner <stefan@adlk.io> | 2017-10-13 12:29:40 +0200 |
commit | 58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch) | |
tree | 1211600c2a5d3b5f81c435c6896618111a611720 /src/components/settings | |
download | ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.gz ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.zst ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.zip |
initial commit
Diffstat (limited to 'src/components/settings')
-rw-r--r-- | src/components/settings/SettingsLayout.js | 56 | ||||
-rw-r--r-- | src/components/settings/account/AccountDashboard.js | 286 | ||||
-rw-r--r-- | src/components/settings/navigation/SettingsNavigation.js | 84 | ||||
-rw-r--r-- | src/components/settings/recipes/RecipeItem.js | 34 | ||||
-rw-r--r-- | src/components/settings/recipes/RecipesDashboard.js | 151 | ||||
-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 | ||||
-rw-r--r-- | src/components/settings/settings/EditSettingsForm.js | 148 | ||||
-rw-r--r-- | src/components/settings/user/EditUserForm.js | 145 |
11 files changed, 1502 insertions, 0 deletions
diff --git a/src/components/settings/SettingsLayout.js b/src/components/settings/SettingsLayout.js new file mode 100644 index 000000000..d5392ddba --- /dev/null +++ b/src/components/settings/SettingsLayout.js | |||
@@ -0,0 +1,56 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | |||
5 | import { oneOrManyChildElements } from '../../prop-types'; | ||
6 | import Appear from '../ui/effects/Appear'; | ||
7 | |||
8 | @observer | ||
9 | export default class SettingsLayout extends Component { | ||
10 | static propTypes = { | ||
11 | navigation: PropTypes.element.isRequired, | ||
12 | children: oneOrManyChildElements.isRequired, | ||
13 | closeSettings: PropTypes.func.isRequired, | ||
14 | }; | ||
15 | |||
16 | componentWillMount() { | ||
17 | document.addEventListener('keydown', this.handleKeyDown.bind(this), false); | ||
18 | } | ||
19 | |||
20 | componentWillUnmount() { | ||
21 | document.removeEventListener('keydown', this.handleKeyDown.bind(this), false); | ||
22 | } | ||
23 | |||
24 | handleKeyDown(e) { | ||
25 | if (e.keyCode === 27) { // escape key | ||
26 | this.props.closeSettings(); | ||
27 | } | ||
28 | } | ||
29 | |||
30 | render() { | ||
31 | const { | ||
32 | navigation, | ||
33 | children, | ||
34 | closeSettings, | ||
35 | } = this.props; | ||
36 | |||
37 | return ( | ||
38 | <Appear transitionName="fadeIn-fast"> | ||
39 | <div className="settings-wrapper"> | ||
40 | <button | ||
41 | className="settings-wrapper__action" | ||
42 | onClick={closeSettings} | ||
43 | /> | ||
44 | <div className="settings franz-form"> | ||
45 | {navigation} | ||
46 | {children} | ||
47 | <button | ||
48 | className="settings__close mdi mdi-close" | ||
49 | onClick={closeSettings} | ||
50 | /> | ||
51 | </div> | ||
52 | </div> | ||
53 | </Appear> | ||
54 | ); | ||
55 | } | ||
56 | } | ||
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js new file mode 100644 index 000000000..75dbdef49 --- /dev/null +++ b/src/components/settings/account/AccountDashboard.js | |||
@@ -0,0 +1,286 @@ | |||
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, FormattedMessage } from 'react-intl'; | ||
5 | import ReactTooltip from 'react-tooltip'; | ||
6 | import moment from 'moment'; | ||
7 | |||
8 | import Loader from '../../ui/Loader'; | ||
9 | import Button from '../../ui/Button'; | ||
10 | import Infobox from '../../ui/Infobox'; | ||
11 | import Link from '../../ui/Link'; | ||
12 | import SubscriptionForm from '../../../containers/ui/SubscriptionFormScreen'; | ||
13 | |||
14 | const messages = defineMessages({ | ||
15 | headline: { | ||
16 | id: 'settings.account.headline', | ||
17 | defaultMessage: '!!!Account', | ||
18 | }, | ||
19 | headlineSubscription: { | ||
20 | id: 'settings.account.headlineSubscription', | ||
21 | defaultMessage: '!!!Your Subscription', | ||
22 | }, | ||
23 | headlineUpgrade: { | ||
24 | id: 'settings.account.headlineUpgrade', | ||
25 | defaultMessage: '!!!Upgrade your Account', | ||
26 | }, | ||
27 | headlineInvoices: { | ||
28 | id: 'settings.account.headlineInvoices', | ||
29 | defaultMessage: '!!Invoices', | ||
30 | }, | ||
31 | manageSubscriptionButtonLabel: { | ||
32 | id: 'settings.account.manageSubscription.label', | ||
33 | defaultMessage: '!!!Manage your subscription', | ||
34 | }, | ||
35 | accountTypeBasic: { | ||
36 | id: 'settings.account.accountType.basic', | ||
37 | defaultMessage: '!!!Basic Account', | ||
38 | }, | ||
39 | accountTypePremium: { | ||
40 | id: 'settings.account.accountType.premium', | ||
41 | defaultMessage: '!!!Premium Supporter Account', | ||
42 | }, | ||
43 | accountEditButton: { | ||
44 | id: 'settings.account.account.editButton', | ||
45 | defaultMessage: '!!!Edit Account', | ||
46 | }, | ||
47 | invoiceDownload: { | ||
48 | id: 'settings.account.invoiceDownload', | ||
49 | defaultMessage: '!!!Download', | ||
50 | }, | ||
51 | userInfoRequestFailed: { | ||
52 | id: 'settings.account.userInfoRequestFailed', | ||
53 | defaultMessage: '!!!Could not load user information', | ||
54 | }, | ||
55 | tryReloadUserInfoRequest: { | ||
56 | id: 'settings.account.tryReloadUserInfoRequest', | ||
57 | defaultMessage: '!!!Try again', | ||
58 | }, | ||
59 | miningActive: { | ||
60 | id: 'settings.account.mining.active', | ||
61 | defaultMessage: '!!!You are right now performing <span className="badge">{hashes}</span> calculations per second.', | ||
62 | }, | ||
63 | miningThankYou: { | ||
64 | id: 'settings.account.mining.thankyou', | ||
65 | defaultMessage: '!!!Thank you for supporting Franz with your processing power.', | ||
66 | }, | ||
67 | miningMoreInfo: { | ||
68 | id: 'settings.account.mining.moreInformation', | ||
69 | defaultMessage: '!!!Get more information', | ||
70 | }, | ||
71 | cancelMining: { | ||
72 | id: 'settings.account.mining.cancel', | ||
73 | defaultMessage: '!!!Cancel mining', | ||
74 | }, | ||
75 | }); | ||
76 | |||
77 | @observer | ||
78 | export default class AccountDashboard extends Component { | ||
79 | static propTypes = { | ||
80 | user: MobxPropTypes.observableObject.isRequired, | ||
81 | orders: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
82 | hashrate: PropTypes.number.isRequired, | ||
83 | isLoading: PropTypes.bool.isRequired, | ||
84 | isLoadingOrdersInfo: PropTypes.bool.isRequired, | ||
85 | isLoadingPlans: PropTypes.bool.isRequired, | ||
86 | isCreatingPaymentDashboardUrl: PropTypes.bool.isRequired, | ||
87 | userInfoRequestFailed: PropTypes.bool.isRequired, | ||
88 | retryUserInfoRequest: PropTypes.func.isRequired, | ||
89 | openDashboard: PropTypes.func.isRequired, | ||
90 | openExternalUrl: PropTypes.func.isRequired, | ||
91 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | ||
92 | stopMiner: PropTypes.func.isRequired, | ||
93 | }; | ||
94 | |||
95 | static contextTypes = { | ||
96 | intl: intlShape, | ||
97 | }; | ||
98 | |||
99 | render() { | ||
100 | const { | ||
101 | user, | ||
102 | orders, | ||
103 | hashrate, | ||
104 | isLoading, | ||
105 | isCreatingPaymentDashboardUrl, | ||
106 | openDashboard, | ||
107 | openExternalUrl, | ||
108 | isLoadingOrdersInfo, | ||
109 | isLoadingPlans, | ||
110 | userInfoRequestFailed, | ||
111 | retryUserInfoRequest, | ||
112 | onCloseSubscriptionWindow, | ||
113 | stopMiner, | ||
114 | } = this.props; | ||
115 | const { intl } = this.context; | ||
116 | |||
117 | return ( | ||
118 | <div className="settings__main"> | ||
119 | <div className="settings__header"> | ||
120 | <span className="settings__header-item"> | ||
121 | {intl.formatMessage(messages.headline)} | ||
122 | </span> | ||
123 | </div> | ||
124 | <div className="settings__body"> | ||
125 | {isLoading && ( | ||
126 | <Loader /> | ||
127 | )} | ||
128 | |||
129 | {!isLoading && userInfoRequestFailed && ( | ||
130 | <div> | ||
131 | <Infobox | ||
132 | icon="alert" | ||
133 | type="danger" | ||
134 | ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)} | ||
135 | ctaLoading={isLoading} | ||
136 | ctaOnClick={retryUserInfoRequest} | ||
137 | > | ||
138 | {intl.formatMessage(messages.userInfoRequestFailed)} | ||
139 | </Infobox> | ||
140 | </div> | ||
141 | )} | ||
142 | |||
143 | {!userInfoRequestFailed && ( | ||
144 | <div> | ||
145 | {!isLoading && ( | ||
146 | <div className="account"> | ||
147 | <div className="account__box account__box--flex"> | ||
148 | <div className="account__avatar"> | ||
149 | <img | ||
150 | src="./assets/images/logo.svg" | ||
151 | alt="" | ||
152 | /> | ||
153 | {user.isPremium && ( | ||
154 | <span | ||
155 | className="account__avatar-premium emoji" | ||
156 | data-tip="Premium Supporter Account" | ||
157 | > | ||
158 | <img src="./assets/images/emoji/star.png" alt="" /> | ||
159 | </span> | ||
160 | )} | ||
161 | </div> | ||
162 | <div className="account__info"> | ||
163 | <h2> | ||
164 | {`${user.firstname} ${user.lastname}`} | ||
165 | </h2> | ||
166 | {user.organization && `${user.organization}, `} | ||
167 | {user.email}<br /> | ||
168 | {!user.isPremium && ( | ||
169 | <span className="badge badge">{intl.formatMessage(messages.accountTypeBasic)}</span> | ||
170 | )} | ||
171 | {user.isPremium && ( | ||
172 | <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span> | ||
173 | )} | ||
174 | </div> | ||
175 | <Link to="/settings/user/edit" className="button"> | ||
176 | {intl.formatMessage(messages.accountEditButton)} | ||
177 | </Link> | ||
178 | |||
179 | {user.emailValidated} | ||
180 | </div> | ||
181 | </div> | ||
182 | )} | ||
183 | |||
184 | {user.isSubscriptionOwner && ( | ||
185 | isLoadingOrdersInfo ? ( | ||
186 | <Loader /> | ||
187 | ) : ( | ||
188 | <div className="account franz-form"> | ||
189 | {orders.length > 0 && ( | ||
190 | <div> | ||
191 | <div className="account__box"> | ||
192 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> | ||
193 | <div className="account__subscription"> | ||
194 | {orders[0].name} | ||
195 | <span className="badge">{orders[0].price}</span> | ||
196 | <Button | ||
197 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} | ||
198 | className="account__subscription-button franz-form__button--inverted" | ||
199 | loaded={!isCreatingPaymentDashboardUrl} | ||
200 | onClick={() => openDashboard()} | ||
201 | /> | ||
202 | </div> | ||
203 | </div> | ||
204 | <div className="account__box account__box--last"> | ||
205 | <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> | ||
206 | <table className="invoices"> | ||
207 | <tbody> | ||
208 | {orders.map(order => ( | ||
209 | <tr key={order.id}> | ||
210 | <td className="invoices__date"> | ||
211 | {moment(order.date).format('DD.MM.YYYY')} | ||
212 | </td> | ||
213 | <td className="invoices__action"> | ||
214 | <button | ||
215 | onClick={() => openExternalUrl(order.invoiceUrl)} | ||
216 | > | ||
217 | {intl.formatMessage(messages.invoiceDownload)} | ||
218 | </button> | ||
219 | </td> | ||
220 | </tr> | ||
221 | ))} | ||
222 | </tbody> | ||
223 | </table> | ||
224 | </div> | ||
225 | </div> | ||
226 | )} | ||
227 | </div> | ||
228 | ) | ||
229 | )} | ||
230 | |||
231 | {user.isMiner && ( | ||
232 | <div className="account franz-form"> | ||
233 | <div className="account__box"> | ||
234 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> | ||
235 | <div className="account__subscription"> | ||
236 | <div> | ||
237 | <p>{intl.formatMessage(messages.miningThankYou)}</p> | ||
238 | <FormattedMessage | ||
239 | {...messages.miningActive} | ||
240 | values={{ | ||
241 | hashes: <span className="badge">{hashrate.toFixed(2)}</span>, | ||
242 | }} | ||
243 | tagName="p" | ||
244 | /> | ||
245 | <p> | ||
246 | <Link | ||
247 | to="http://meetfranz.com/mining" | ||
248 | target="_blank" | ||
249 | className="link" | ||
250 | > | ||
251 | {intl.formatMessage(messages.miningMoreInfo)} | ||
252 | </Link> | ||
253 | </p> | ||
254 | </div> | ||
255 | <Button | ||
256 | label={intl.formatMessage(messages.cancelMining)} | ||
257 | className="account__subscription-button franz-form__button--inverted" | ||
258 | onClick={() => stopMiner()} | ||
259 | /> | ||
260 | </div> | ||
261 | </div> | ||
262 | </div> | ||
263 | )} | ||
264 | |||
265 | {!user.isPremium && !user.isMiner && ( | ||
266 | isLoadingPlans ? ( | ||
267 | <Loader /> | ||
268 | ) : ( | ||
269 | <div className="account franz-form"> | ||
270 | <div className="account__box account__box--last"> | ||
271 | <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> | ||
272 | <SubscriptionForm | ||
273 | onCloseWindow={onCloseSubscriptionWindow} | ||
274 | /> | ||
275 | </div> | ||
276 | </div> | ||
277 | ) | ||
278 | )} | ||
279 | </div> | ||
280 | )} | ||
281 | </div> | ||
282 | <ReactTooltip place="right" type="dark" effect="solid" /> | ||
283 | </div> | ||
284 | ); | ||
285 | } | ||
286 | } | ||
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js new file mode 100644 index 000000000..3b21a7765 --- /dev/null +++ b/src/components/settings/navigation/SettingsNavigation.js | |||
@@ -0,0 +1,84 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { defineMessages, intlShape } from 'react-intl'; | ||
4 | |||
5 | import Link from '../../ui/Link'; | ||
6 | |||
7 | const messages = defineMessages({ | ||
8 | availableServices: { | ||
9 | id: 'settings.navigation.availableServices', | ||
10 | defaultMessage: '!!!Available services', | ||
11 | }, | ||
12 | yourServices: { | ||
13 | id: 'settings.navigation.yourServices', | ||
14 | defaultMessage: '!!!Your services', | ||
15 | }, | ||
16 | account: { | ||
17 | id: 'settings.navigation.account', | ||
18 | defaultMessage: '!!!Account', | ||
19 | }, | ||
20 | settings: { | ||
21 | id: 'settings.navigation.settings', | ||
22 | defaultMessage: '!!!Settings', | ||
23 | }, | ||
24 | logout: { | ||
25 | id: 'settings.navigation.logout', | ||
26 | defaultMessage: '!!!Logout', | ||
27 | }, | ||
28 | }); | ||
29 | |||
30 | export default class SettingsNavigation extends Component { | ||
31 | static propTypes = { | ||
32 | serviceCount: PropTypes.number.isRequired, | ||
33 | }; | ||
34 | |||
35 | static contextTypes = { | ||
36 | intl: intlShape, | ||
37 | }; | ||
38 | |||
39 | render() { | ||
40 | const { serviceCount } = this.props; | ||
41 | const { intl } = this.context; | ||
42 | |||
43 | return ( | ||
44 | <div className="settings-navigation"> | ||
45 | <Link | ||
46 | to="/settings/recipes" | ||
47 | className="settings-navigation__link" | ||
48 | activeClassName="is-active" | ||
49 | > | ||
50 | {intl.formatMessage(messages.availableServices)} | ||
51 | </Link> | ||
52 | <Link | ||
53 | to="/settings/services" | ||
54 | className="settings-navigation__link" | ||
55 | activeClassName="is-active" | ||
56 | > | ||
57 | {intl.formatMessage(messages.yourServices)} <span className="badge">{serviceCount}</span> | ||
58 | </Link> | ||
59 | <Link | ||
60 | to="/settings/user" | ||
61 | className="settings-navigation__link" | ||
62 | activeClassName="is-active" | ||
63 | > | ||
64 | {intl.formatMessage(messages.account)} | ||
65 | </Link> | ||
66 | <Link | ||
67 | to="/settings/app" | ||
68 | className="settings-navigation__link" | ||
69 | activeClassName="is-active" | ||
70 | > | ||
71 | {intl.formatMessage(messages.settings)} | ||
72 | </Link> | ||
73 | <span className="settings-navigation__expander" /> | ||
74 | <Link | ||
75 | to="/auth/logout" | ||
76 | className="settings-navigation__link" | ||
77 | activeClassName="is-active" | ||
78 | > | ||
79 | {intl.formatMessage(messages.logout)} | ||
80 | </Link> | ||
81 | </div> | ||
82 | ); | ||
83 | } | ||
84 | } | ||
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.js new file mode 100644 index 000000000..7b2f64d26 --- /dev/null +++ b/src/components/settings/recipes/RecipeItem.js | |||
@@ -0,0 +1,34 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | |||
5 | import RecipePreviewModel from '../../../models/RecipePreview'; | ||
6 | |||
7 | @observer | ||
8 | export default class RecipeItem extends Component { | ||
9 | static propTypes = { | ||
10 | recipe: PropTypes.instanceOf(RecipePreviewModel).isRequired, | ||
11 | onClick: PropTypes.func.isRequired, | ||
12 | }; | ||
13 | |||
14 | render() { | ||
15 | const { recipe, onClick } = this.props; | ||
16 | |||
17 | return ( | ||
18 | <button | ||
19 | className="recipe-teaser" | ||
20 | onClick={onClick} | ||
21 | > | ||
22 | {recipe.local && ( | ||
23 | <span className="recipe-teaser__dev-badge">dev</span> | ||
24 | )} | ||
25 | <img | ||
26 | src={recipe.icons.svg} | ||
27 | className="recipe-teaser__icon" | ||
28 | alt="" | ||
29 | /> | ||
30 | <span className="recipe-teaser__label">{recipe.name}</span> | ||
31 | </button> | ||
32 | ); | ||
33 | } | ||
34 | } | ||
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js new file mode 100644 index 000000000..02ea04e35 --- /dev/null +++ b/src/components/settings/recipes/RecipesDashboard.js | |||
@@ -0,0 +1,151 @@ | |||
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 { Link } from 'react-router'; | ||
6 | |||
7 | import SearchInput from '../../ui/SearchInput'; | ||
8 | import Infobox from '../../ui/Infobox'; | ||
9 | import RecipeItem from './RecipeItem'; | ||
10 | import Loader from '../../ui/Loader'; | ||
11 | import Appear from '../../ui/effects/Appear'; | ||
12 | |||
13 | const messages = defineMessages({ | ||
14 | headline: { | ||
15 | id: 'settings.recipes.headline', | ||
16 | defaultMessage: '!!!Available Services', | ||
17 | }, | ||
18 | mostPopularRecipes: { | ||
19 | id: 'settings.recipes.mostPopular', | ||
20 | defaultMessage: '!!!Most popular', | ||
21 | }, | ||
22 | allRecipes: { | ||
23 | id: 'settings.recipes.all', | ||
24 | defaultMessage: '!!!All services', | ||
25 | }, | ||
26 | devRecipes: { | ||
27 | id: 'settings.recipes.dev', | ||
28 | defaultMessage: '!!!Development', | ||
29 | }, | ||
30 | nothingFound: { | ||
31 | id: 'settings.recipes.nothingFound', | ||
32 | defaultMessage: '!!!Sorry, but no service matched your search term.', | ||
33 | }, | ||
34 | servicesSuccessfulAddedInfo: { | ||
35 | id: 'settings.recipes.servicesSuccessfulAddedInfo', | ||
36 | defaultMessage: '!!!Service successfully added', | ||
37 | }, | ||
38 | }); | ||
39 | |||
40 | @observer | ||
41 | export default class RecipesDashboard extends Component { | ||
42 | static propTypes = { | ||
43 | recipes: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
44 | isLoading: PropTypes.bool.isRequired, | ||
45 | hasLoadedRecipes: PropTypes.bool.isRequired, | ||
46 | showAddServiceInterface: PropTypes.func.isRequired, | ||
47 | searchRecipes: PropTypes.func.isRequired, | ||
48 | resetSearch: PropTypes.func.isRequired, | ||
49 | serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
50 | devRecipesCount: PropTypes.number.isRequired, | ||
51 | searchNeedle: PropTypes.string, | ||
52 | }; | ||
53 | |||
54 | static defaultProps = { | ||
55 | searchNeedle: '', | ||
56 | } | ||
57 | |||
58 | static contextTypes = { | ||
59 | intl: intlShape, | ||
60 | }; | ||
61 | |||
62 | render() { | ||
63 | const { | ||
64 | recipes, | ||
65 | isLoading, | ||
66 | hasLoadedRecipes, | ||
67 | showAddServiceInterface, | ||
68 | searchRecipes, | ||
69 | resetSearch, | ||
70 | serviceStatus, | ||
71 | devRecipesCount, | ||
72 | searchNeedle, | ||
73 | } = this.props; | ||
74 | const { intl } = this.context; | ||
75 | |||
76 | return ( | ||
77 | <div className="settings__main"> | ||
78 | <div className="settings__header"> | ||
79 | <SearchInput | ||
80 | className="settings__search-header" | ||
81 | defaultValue={intl.formatMessage(messages.headline)} | ||
82 | onChange={e => searchRecipes(e)} | ||
83 | onReset={() => resetSearch()} | ||
84 | throttle | ||
85 | /> | ||
86 | </div> | ||
87 | <div className="settings__body recipes"> | ||
88 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( | ||
89 | <Appear> | ||
90 | <Infobox | ||
91 | type="success" | ||
92 | icon="checkbox-marked-circle-outline" | ||
93 | dismissable | ||
94 | > | ||
95 | {intl.formatMessage(messages.servicesSuccessfulAddedInfo)} | ||
96 | </Infobox> | ||
97 | </Appear> | ||
98 | )} | ||
99 | {!searchNeedle && ( | ||
100 | <div className="recipes__navigation"> | ||
101 | <Link | ||
102 | to="/settings/recipes" | ||
103 | className="badge" | ||
104 | activeClassName="badge--primary" | ||
105 | > | ||
106 | {intl.formatMessage(messages.mostPopularRecipes)} | ||
107 | </Link> | ||
108 | <Link | ||
109 | to="/settings/recipes/all" | ||
110 | className="badge" | ||
111 | activeClassName="badge--primary" | ||
112 | > | ||
113 | {intl.formatMessage(messages.allRecipes)} | ||
114 | </Link> | ||
115 | {devRecipesCount > 0 && ( | ||
116 | <Link | ||
117 | to="/settings/recipes/dev" | ||
118 | className="badge" | ||
119 | activeClassName="badge--primary" | ||
120 | > | ||
121 | {intl.formatMessage(messages.devRecipes)} ({devRecipesCount}) | ||
122 | </Link> | ||
123 | )} | ||
124 | </div> | ||
125 | )} | ||
126 | {isLoading ? ( | ||
127 | <Loader /> | ||
128 | ) : ( | ||
129 | <div className="recipes__list"> | ||
130 | {hasLoadedRecipes && recipes.length === 0 && ( | ||
131 | <p className="align-middle settings__empty-state"> | ||
132 | <span className="emoji"> | ||
133 | <img src="./assets/images/emoji/dontknow.png" alt="" /> | ||
134 | </span> | ||
135 | {intl.formatMessage(messages.nothingFound)} | ||
136 | </p> | ||
137 | )} | ||
138 | {recipes.map(recipe => ( | ||
139 | <RecipeItem | ||
140 | key={recipe.id} | ||
141 | recipe={recipe} | ||
142 | onClick={() => showAddServiceInterface({ recipeId: recipe.id })} | ||
143 | /> | ||
144 | ))} | ||
145 | </div> | ||
146 | )} | ||
147 | </div> | ||
148 | </div> | ||
149 | ); | ||
150 | } | ||
151 | } | ||
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 | } | ||
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js new file mode 100644 index 000000000..02736dbb9 --- /dev/null +++ b/src/components/settings/settings/EditSettingsForm.js | |||
@@ -0,0 +1,148 @@ | |||
1 | import { remote } from 'electron'; | ||
2 | import React, { Component } from 'react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { observer } from 'mobx-react'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | |||
7 | import Form from '../../../lib/Form'; | ||
8 | import Button from '../../ui/Button'; | ||
9 | import Toggle from '../../ui/Toggle'; | ||
10 | import Select from '../../ui/Select'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | headline: { | ||
14 | id: 'settings.app.headline', | ||
15 | defaultMessage: '!!!Settings', | ||
16 | }, | ||
17 | headlineGeneral: { | ||
18 | id: 'settings.app.headlineGeneral', | ||
19 | defaultMessage: '!!!General', | ||
20 | }, | ||
21 | headlineLanguage: { | ||
22 | id: 'settings.app.headlineLanguage', | ||
23 | defaultMessage: '!!!Language', | ||
24 | }, | ||
25 | headlineUpdates: { | ||
26 | id: 'settings.app.headlineUpdates', | ||
27 | defaultMessage: '!!!Updates', | ||
28 | }, | ||
29 | buttonSearchForUpdate: { | ||
30 | id: 'settings.app.buttonSearchForUpdate', | ||
31 | defaultMessage: '!!!Check for updates', | ||
32 | }, | ||
33 | buttonInstallUpdate: { | ||
34 | id: 'settings.app.buttonInstallUpdate', | ||
35 | defaultMessage: '!!!Restart & install update', | ||
36 | }, | ||
37 | updateStatusSearching: { | ||
38 | id: 'settings.app.updateStatusSearching', | ||
39 | defaultMessage: '!!!Is searching for update', | ||
40 | }, | ||
41 | updateStatusAvailable: { | ||
42 | id: 'settings.app.updateStatusAvailable', | ||
43 | defaultMessage: '!!!Update available, downloading...', | ||
44 | }, | ||
45 | updateStatusUpToDate: { | ||
46 | id: 'settings.app.updateStatusUpToDate', | ||
47 | defaultMessage: '!!!You are using the latest version of Franz', | ||
48 | }, | ||
49 | currentVersion: { | ||
50 | id: 'settings.app.currentVersion', | ||
51 | defaultMessage: '!!!Current version:', | ||
52 | }, | ||
53 | }); | ||
54 | |||
55 | @observer | ||
56 | export default class EditSettingsForm extends Component { | ||
57 | static propTypes = { | ||
58 | checkForUpdates: PropTypes.func.isRequired, | ||
59 | installUpdate: PropTypes.func.isRequired, | ||
60 | form: PropTypes.instanceOf(Form).isRequired, | ||
61 | onSubmit: PropTypes.func.isRequired, | ||
62 | isCheckingForUpdates: PropTypes.bool.isRequired, | ||
63 | isUpdateAvailable: PropTypes.bool.isRequired, | ||
64 | noUpdateAvailable: PropTypes.bool.isRequired, | ||
65 | updateIsReadyToInstall: PropTypes.bool.isRequired, | ||
66 | }; | ||
67 | |||
68 | static contextTypes = { | ||
69 | intl: intlShape, | ||
70 | }; | ||
71 | |||
72 | submit(e) { | ||
73 | e.preventDefault(); | ||
74 | this.props.form.submit({ | ||
75 | onSuccess: (form) => { | ||
76 | const values = form.values(); | ||
77 | this.props.onSubmit(values); | ||
78 | }, | ||
79 | onError: () => {}, | ||
80 | }); | ||
81 | } | ||
82 | |||
83 | render() { | ||
84 | const { | ||
85 | checkForUpdates, | ||
86 | installUpdate, | ||
87 | form, | ||
88 | isCheckingForUpdates, | ||
89 | isUpdateAvailable, | ||
90 | noUpdateAvailable, | ||
91 | updateIsReadyToInstall, | ||
92 | } = this.props; | ||
93 | const { intl } = this.context; | ||
94 | |||
95 | let updateButtonLabelMessage = messages.buttonSearchForUpdate; | ||
96 | if (isCheckingForUpdates) { | ||
97 | updateButtonLabelMessage = messages.updateStatusSearching; | ||
98 | } else if (isUpdateAvailable) { | ||
99 | updateButtonLabelMessage = messages.updateStatusAvailable; | ||
100 | } else { | ||
101 | updateButtonLabelMessage = messages.buttonSearchForUpdate; | ||
102 | } | ||
103 | |||
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 | <form | ||
111 | onSubmit={e => this.submit(e)} | ||
112 | onChange={e => this.submit(e)} | ||
113 | id="form" | ||
114 | > | ||
115 | <h2>{intl.formatMessage(messages.headlineGeneral)}</h2> | ||
116 | <Toggle field={form.$('autoLaunchOnStart')} /> | ||
117 | <Toggle field={form.$('runInBackground')} /> | ||
118 | {process.platform === 'win32' && ( | ||
119 | <Toggle field={form.$('minimizeToSystemTray')} /> | ||
120 | )} | ||
121 | <h2>{intl.formatMessage(messages.headlineLanguage)}</h2> | ||
122 | <Select field={form.$('locale')} showLabel={false} /> | ||
123 | <h2>{intl.formatMessage(messages.headlineUpdates)}</h2> | ||
124 | {updateIsReadyToInstall ? ( | ||
125 | <Button | ||
126 | label={intl.formatMessage(messages.buttonInstallUpdate)} | ||
127 | onClick={installUpdate} | ||
128 | /> | ||
129 | ) : ( | ||
130 | <Button | ||
131 | label={intl.formatMessage(updateButtonLabelMessage)} | ||
132 | onClick={checkForUpdates} | ||
133 | disabled={isCheckingForUpdates || isUpdateAvailable} | ||
134 | loaded={!isCheckingForUpdates || !isUpdateAvailable} | ||
135 | /> | ||
136 | )} | ||
137 | {noUpdateAvailable && ( | ||
138 | <p>{intl.formatMessage(messages.updateStatusUpToDate)}</p> | ||
139 | )} | ||
140 | <br /> | ||
141 | <Toggle field={form.$('beta')} /> | ||
142 | {intl.formatMessage(messages.currentVersion)} {remote.app.getVersion()} | ||
143 | </form> | ||
144 | </div> | ||
145 | </div> | ||
146 | ); | ||
147 | } | ||
148 | } | ||
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js new file mode 100644 index 000000000..f36887fc2 --- /dev/null +++ b/src/components/settings/user/EditUserForm.js | |||
@@ -0,0 +1,145 @@ | |||
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 { Link } from 'react-router'; | ||
6 | |||
7 | // import { Link } from 'react-router'; | ||
8 | |||
9 | import Form from '../../../lib/Form'; | ||
10 | import Input from '../../ui/Input'; | ||
11 | import Button from '../../ui/Button'; | ||
12 | import Radio from '../../ui/Radio'; | ||
13 | import Infobox from '../../ui/Infobox'; | ||
14 | |||
15 | const messages = defineMessages({ | ||
16 | headline: { | ||
17 | id: 'settings.account.headline', | ||
18 | defaultMessage: '!!!Account', | ||
19 | }, | ||
20 | headlineProfile: { | ||
21 | id: 'settings.account.headlineProfile', | ||
22 | defaultMessage: '!!!Update Profile', | ||
23 | }, | ||
24 | headlineAccount: { | ||
25 | id: 'settings.account.headlineAccount', | ||
26 | defaultMessage: '!!!Account Information', | ||
27 | }, | ||
28 | headlinePassword: { | ||
29 | id: 'settings.account.headlinePassword', | ||
30 | defaultMessage: '!!!Change Password', | ||
31 | }, | ||
32 | successInfo: { | ||
33 | id: 'settings.account.successInfo', | ||
34 | defaultMessage: '!!!Your changes have been saved', | ||
35 | }, | ||
36 | buttonSave: { | ||
37 | id: 'settings.account.buttonSave', | ||
38 | defaultMessage: '!!!Update profile', | ||
39 | }, | ||
40 | }); | ||
41 | |||
42 | @observer | ||
43 | export default class EditServiceForm extends Component { | ||
44 | static propTypes = { | ||
45 | status: MobxPropTypes.observableArray.isRequired, | ||
46 | form: PropTypes.instanceOf(Form).isRequired, | ||
47 | onSubmit: PropTypes.func.isRequired, | ||
48 | isSaving: PropTypes.bool.isRequired, | ||
49 | }; | ||
50 | |||
51 | static defaultProps = { | ||
52 | service: {}, | ||
53 | }; | ||
54 | |||
55 | static contextTypes = { | ||
56 | intl: intlShape, | ||
57 | }; | ||
58 | |||
59 | submit(e) { | ||
60 | e.preventDefault(); | ||
61 | this.props.form.submit({ | ||
62 | onSuccess: (form) => { | ||
63 | const values = form.values(); | ||
64 | this.props.onSubmit(values); | ||
65 | }, | ||
66 | onError: () => {}, | ||
67 | }); | ||
68 | } | ||
69 | |||
70 | render() { | ||
71 | const { | ||
72 | // user, | ||
73 | status, | ||
74 | form, | ||
75 | isSaving, | ||
76 | } = this.props; | ||
77 | const { intl } = this.context; | ||
78 | |||
79 | return ( | ||
80 | <div className="settings__main"> | ||
81 | <div className="settings__header"> | ||
82 | <span className="settings__header-item"> | ||
83 | <Link to="/settings/user"> | ||
84 | {intl.formatMessage(messages.headline)} | ||
85 | </Link> | ||
86 | </span> | ||
87 | <span className="separator" /> | ||
88 | <span className="settings__header-item"> | ||
89 | {intl.formatMessage(messages.headlineProfile)} | ||
90 | </span> | ||
91 | </div> | ||
92 | <div className="settings__body"> | ||
93 | <form onSubmit={e => this.submit(e)} id="form"> | ||
94 | {status.length > 0 && status.includes('data-updated') && ( | ||
95 | <Infobox | ||
96 | type="success" | ||
97 | icon="checkbox-marked-circle-outline" | ||
98 | > | ||
99 | {intl.formatMessage(messages.successInfo)} | ||
100 | </Infobox> | ||
101 | )} | ||
102 | <h2>{intl.formatMessage(messages.headlineAccount)}</h2> | ||
103 | <div className="grid__row"> | ||
104 | <Input field={form.$('firstname')} focus /> | ||
105 | <Input field={form.$('lastname')} /> | ||
106 | </div> | ||
107 | <Input field={form.$('email')} /> | ||
108 | <Radio field={form.$('accountType')} /> | ||
109 | {form.$('accountType').value === 'company' && ( | ||
110 | <Input field={form.$('organization')} /> | ||
111 | )} | ||
112 | <h2>{intl.formatMessage(messages.headlinePassword)}</h2> | ||
113 | <Input | ||
114 | field={form.$('oldPassword')} | ||
115 | showPasswordToggle | ||
116 | /> | ||
117 | <Input | ||
118 | field={form.$('newPassword')} | ||
119 | showPasswordToggle | ||
120 | scorePassword | ||
121 | /> | ||
122 | </form> | ||
123 | </div> | ||
124 | <div className="settings__controls"> | ||
125 | {/* Save Button */} | ||
126 | {isSaving ? ( | ||
127 | <Button | ||
128 | type="submit" | ||
129 | label={intl.formatMessage(messages.buttonSave)} | ||
130 | loaded={!isSaving} | ||
131 | buttonType="secondary" | ||
132 | disabled | ||
133 | /> | ||
134 | ) : ( | ||
135 | <Button | ||
136 | type="submit" | ||
137 | label={intl.formatMessage(messages.buttonSave)} | ||
138 | htmlForm="form" | ||
139 | /> | ||
140 | )} | ||
141 | </div> | ||
142 | </div> | ||
143 | ); | ||
144 | } | ||
145 | } | ||