aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/settings
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2017-10-13 12:29:40 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2017-10-13 12:29:40 +0200
commit58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch)
tree1211600c2a5d3b5f81c435c6896618111a611720 /src/components/settings
downloadferdium-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.js56
-rw-r--r--src/components/settings/account/AccountDashboard.js286
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js84
-rw-r--r--src/components/settings/recipes/RecipeItem.js34
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js151
-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
-rw-r--r--src/components/settings/settings/EditSettingsForm.js148
-rw-r--r--src/components/settings/user/EditUserForm.js145
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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4
5import { oneOrManyChildElements } from '../../prop-types';
6import Appear from '../ui/effects/Appear';
7
8@observer
9export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape, FormattedMessage } from 'react-intl';
5import ReactTooltip from 'react-tooltip';
6import moment from 'moment';
7
8import Loader from '../../ui/Loader';
9import Button from '../../ui/Button';
10import Infobox from '../../ui/Infobox';
11import Link from '../../ui/Link';
12import SubscriptionForm from '../../../containers/ui/SubscriptionFormScreen';
13
14const 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
78export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4
5import Link from '../../ui/Link';
6
7const 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
30export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4
5import RecipePreviewModel from '../../../models/RecipePreview';
6
7@observer
8export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router';
6
7import SearchInput from '../../ui/SearchInput';
8import Infobox from '../../ui/Infobox';
9import RecipeItem from './RecipeItem';
10import Loader from '../../ui/Loader';
11import Appear from '../../ui/effects/Appear';
12
13const 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
41export 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 @@
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}
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 @@
1import { remote } from 'electron';
2import React, { Component } from 'react';
3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react';
5import { defineMessages, intlShape } from 'react-intl';
6
7import Form from '../../../lib/Form';
8import Button from '../../ui/Button';
9import Toggle from '../../ui/Toggle';
10import Select from '../../ui/Select';
11
12const 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
56export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router';
6
7// import { Link } from 'react-router';
8
9import Form from '../../../lib/Form';
10import Input from '../../ui/Input';
11import Button from '../../ui/Button';
12import Radio from '../../ui/Radio';
13import Infobox from '../../ui/Infobox';
14
15const 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
43export 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}