aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-12-13 14:01:33 +0100
committerLibravatar Stefan Malzner <stefan@adlk.io>2018-12-13 14:01:33 +0100
commitb5937bd427e02d64b675c9c4719ec8148c68c224 (patch)
tree3e67ca01935eb36ab6d40a1e197d7c2c0f38b587 /src/components
parentUpdate CHANGELOG.md (diff)
parentIgnore clicks on premium elements (diff)
downloadferdium-app-b5937bd427e02d64b675c9c4719ec8148c68c224.tar.gz
ferdium-app-b5937bd427e02d64b675c9c4719ec8148c68c224.tar.zst
ferdium-app-b5937bd427e02d64b675c9c4719ec8148c68c224.zip
Merge branch 'develop'v5.0.0-beta.21
Diffstat (limited to 'src/components')
-rw-r--r--src/components/auth/AuthLayout.js27
-rw-r--r--src/components/auth/Invite.js32
-rw-r--r--src/components/auth/Login.js9
-rw-r--r--src/components/auth/Pricing.js23
-rw-r--r--src/components/auth/Signup.js3
-rw-r--r--src/components/layout/AppLayout.js136
-rw-r--r--src/components/layout/Sidebar.js3
-rw-r--r--src/components/services/content/ErrorHandlers/WebviewErrorHandler.js84
-rw-r--r--src/components/services/content/ErrorHandlers/styles.js25
-rw-r--r--src/components/services/content/ServiceDisabled.js1
-rw-r--r--src/components/services/content/ServiceWebview.js65
-rw-r--r--src/components/services/content/Services.js6
-rw-r--r--src/components/services/content/WebviewCrashHandler.js24
-rw-r--r--src/components/settings/SettingsLayout.js23
-rw-r--r--src/components/settings/account/AccountDashboard.js48
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js5
-rw-r--r--src/components/settings/recipes/RecipeItem.js1
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js10
-rw-r--r--src/components/settings/services/EditServiceForm.js53
-rw-r--r--src/components/settings/services/ServiceItem.js1
-rw-r--r--src/components/settings/services/ServicesDashboard.js20
-rw-r--r--src/components/settings/settings/EditSettingsForm.js14
-rw-r--r--src/components/settings/user/EditUserForm.js4
-rw-r--r--src/components/subscription/SubscriptionForm.js49
-rw-r--r--src/components/subscription/SubscriptionPopup.js4
-rw-r--r--src/components/ui/AppLoader.js15
-rw-r--r--src/components/ui/AppLoader/index.js70
-rw-r--r--src/components/ui/AppLoader/styles.js16
-rw-r--r--src/components/ui/Button.js3
-rw-r--r--src/components/ui/FullscreenLoader/index.js56
-rw-r--r--src/components/ui/FullscreenLoader/styles.js23
-rw-r--r--src/components/ui/ImageUpload.js10
-rw-r--r--src/components/ui/InfoBar.js4
-rw-r--r--src/components/ui/Infobox.js2
-rw-r--r--src/components/ui/Input.js6
-rw-r--r--src/components/ui/Link.js1
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js1
-rw-r--r--src/components/ui/PremiumFeatureContainer/styles.js2
-rw-r--r--src/components/ui/Radio.js4
-rw-r--r--src/components/ui/SearchInput.js27
-rw-r--r--src/components/ui/Select.js8
-rw-r--r--src/components/ui/StatusBarTargetUrl.js3
-rw-r--r--src/components/ui/Tabs/TabItem.js4
-rw-r--r--src/components/ui/WebviewLoader/index.js25
-rw-r--r--src/components/ui/WebviewLoader/styles.js9
-rw-r--r--src/components/util/ErrorBoundary/index.js60
-rw-r--r--src/components/util/ErrorBoundary/styles.js13
47 files changed, 736 insertions, 296 deletions
diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js
index 4fb0e6a59..ac8fdbe5b 100644
--- a/src/components/auth/AuthLayout.js
+++ b/src/components/auth/AuthLayout.js
@@ -1,7 +1,6 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { RouteTransition } from 'react-router-transition';
5import { intlShape } from 'react-intl'; 4import { intlShape } from 'react-intl';
6import { TitleBar } from 'electron-react-titlebar'; 5import { TitleBar } from 'electron-react-titlebar';
7 6
@@ -16,7 +15,6 @@ import { isWindows } from '../../environment';
16export default @observer class AuthLayout extends Component { 15export default @observer class AuthLayout extends Component {
17 static propTypes = { 16 static propTypes = {
18 children: oneOrManyChildElements.isRequired, 17 children: oneOrManyChildElements.isRequired,
19 pathname: PropTypes.string.isRequired,
20 error: globalErrorPropType.isRequired, 18 error: globalErrorPropType.isRequired,
21 isOnline: PropTypes.bool.isRequired, 19 isOnline: PropTypes.bool.isRequired,
22 isAPIHealthy: PropTypes.bool.isRequired, 20 isAPIHealthy: PropTypes.bool.isRequired,
@@ -33,7 +31,6 @@ export default @observer class AuthLayout extends Component {
33 render() { 31 render() {
34 const { 32 const {
35 children, 33 children,
36 pathname,
37 error, 34 error,
38 isOnline, 35 isOnline,
39 isAPIHealthy, 36 isAPIHealthy,
@@ -46,8 +43,8 @@ export default @observer class AuthLayout extends Component {
46 43
47 return ( 44 return (
48 <div className={darkMode ? 'theme__dark' : ''}> 45 <div className={darkMode ? 'theme__dark' : ''}>
49 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon={'assets/images/logo.svg'} />} 46 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />}
50 <div className={'auth'}> 47 <div className="auth">
51 {!isOnline && ( 48 {!isOnline && (
52 <InfoBar 49 <InfoBar
53 type="warning" 50 type="warning"
@@ -69,22 +66,10 @@ export default @observer class AuthLayout extends Component {
69 </InfoBar> 66 </InfoBar>
70 )} 67 )}
71 <div className="auth__layout"> 68 <div className="auth__layout">
72 <RouteTransition 69 {/* Inject globalError into children */}
73 pathname={pathname} 70 {React.cloneElement(children, {
74 atEnter={{ opacity: 0 }} 71 error,
75 atLeave={{ opacity: 0 }} 72 })}
76 atActive={{ opacity: 1 }}
77 mapStyles={styles => ({
78 transform: `translateX(${styles.translateX}%)`,
79 opacity: styles.opacity,
80 })}
81 component="span"
82 >
83 {/* Inject globalError into children */}
84 {React.cloneElement(children, {
85 error,
86 })}
87 </RouteTransition>
88 </div> 73 </div>
89 {/* </div> */} 74 {/* </div> */}
90 <Link to="https://adlk.io" className="auth__adlk" target="_blank"> 75 <Link to="https://adlk.io" className="auth__adlk" target="_blank">
diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js
index 96821a61a..fd957ee73 100644
--- a/src/components/auth/Invite.js
+++ b/src/components/auth/Invite.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
@@ -127,7 +127,7 @@ export default @observer class Invite extends Component {
127 }); 127 });
128 128
129 const renderForm = ( 129 const renderForm = (
130 <div> 130 <Fragment>
131 {this.state.showSuccessInfo && isInviteSuccessful && ( 131 {this.state.showSuccessInfo && isInviteSuccessful && (
132 <Appear> 132 <Appear>
133 <Infobox 133 <Infobox
@@ -141,11 +141,13 @@ export default @observer class Invite extends Component {
141 )} 141 )}
142 142
143 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 143 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}>
144 {!embed && (<img 144 {!embed && (
145 src="./assets/images/logo.svg" 145 <img
146 className="auth__logo" 146 src="./assets/images/logo.svg"
147 alt="" 147 className="auth__logo"
148 />)} 148 alt=""
149 />
150 )}
149 <h1 className={embed && 'invite__embed'}> 151 <h1 className={embed && 'invite__embed'}>
150 {intl.formatMessage(messages.headline)} 152 {intl.formatMessage(messages.headline)}
151 </h1> 153 </h1>
@@ -164,14 +166,16 @@ export default @observer class Invite extends Component {
164 label={intl.formatMessage(messages.submitButtonLabel)} 166 label={intl.formatMessage(messages.submitButtonLabel)}
165 loaded={!isLoadingInvite} 167 loaded={!isLoadingInvite}
166 /> 168 />
167 {!embed && (<Link 169 {!embed && (
168 to="/" 170 <Link
169 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" 171 to="/"
170 > 172 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip"
171 {intl.formatMessage(messages.skipButtonLabel)} 173 >
172 </Link>)} 174 {intl.formatMessage(messages.skipButtonLabel)}
175 </Link>
176 )}
173 </form> 177 </form>
174 </div> 178 </Fragment>
175 ); 179 );
176 180
177 return ( 181 return (
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index f465b35a5..5d21f8b60 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -11,11 +11,8 @@ import Button from '../ui/Button';
11import Link from '../ui/Link'; 11import Link from '../ui/Link';
12import Infobox from '../ui/Infobox'; 12import Infobox from '../ui/Infobox';
13 13
14
15import { globalError as globalErrorPropType } from '../../prop-types'; 14import { globalError as globalErrorPropType } from '../../prop-types';
16 15
17// import Appear from '../ui/effects/Appear';
18
19const messages = defineMessages({ 16const messages = defineMessages({
20 headline: { 17 headline: {
21 id: 'login.headline', 18 id: 'login.headline',
@@ -86,18 +83,18 @@ export default @observer class Login extends Component {
86 }, 83 },
87 }, this.context.intl); 84 }, this.context.intl);
88 85
86 emailField = null;
87
89 submit(e) { 88 submit(e) {
90 e.preventDefault(); 89 e.preventDefault();
91 this.form.submit({ 90 this.form.submit({
92 onSuccess: (form) => { 91 onSuccess: (form) => {
93 this.props.onSubmit(form.values()); 92 this.props.onSubmit(form.values());
94 }, 93 },
95 onError: () => {}, 94 onError: () => { },
96 }); 95 });
97 } 96 }
98 97
99 emailField = null;
100
101 render() { 98 render() {
102 const { form } = this; 99 const { form } = this;
103 const { intl } = this.context; 100 const { intl } = this.context;
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index f08129568..7ab14f429 100644
--- a/src/components/auth/Pricing.js
+++ b/src/components/auth/Pricing.js
@@ -69,18 +69,29 @@ export default @observer class Signup extends Component {
69 donor.amount ? ( 69 donor.amount ? (
70 <span> 70 <span>
71 <p> 71 <p>
72 Thank you so much for your previous donation of <strong>$ {donor.amount}</strong>. 72 Thank you so much for your previous donation of
73 {' '}
74 <strong>
75 $
76 {donor.amount}
77 </strong>
78 .
73 <br /> 79 <br />
74 Your support allowed us to get where we are today. 80 Your support allowed us to get where we are today.
75 <br /> 81 <br />
76 </p> 82 </p>
77 <p> 83 <p>
78 As an early supporter, you get <strong>a lifetime premium supporter license</strong> without any 84 As an early supporter, you get
85 {' '}
86 <strong>a lifetime premium supporter license</strong>
87 {' '}
88 without any
79 additional charges. 89 additional charges.
80 </p> 90 </p>
81 <p> 91 <p>
82 However, If you want to keep supporting us, you are more than welcome to subscribe to a plan. 92 However, If you want to keep supporting us, you are more than welcome to subscribe to a plan.
83 <br /><br /> 93 <br />
94 <br />
84 </p> 95 </p>
85 </span> 96 </span>
86 ) : ( 97 ) : (
@@ -113,12 +124,6 @@ export default @observer class Signup extends Component {
113 hideInfo={Boolean(donor.amount)} 124 hideInfo={Boolean(donor.amount)}
114 skipButtonLabel={intl.formatMessage(messages.skipPayment)} 125 skipButtonLabel={intl.formatMessage(messages.skipPayment)}
115 /> 126 />
116 {/* <Link
117 to={inviteRoute}
118 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip"
119 >
120 {intl.formatMessage(messages.skipPayment)}
121 </Link> */}
122 </Appear> 127 </Appear>
123 </Loader> 128 </Loader>
124 </form> 129 </form>
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index bbcad8b67..d9b83eeb8 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -199,7 +199,8 @@ export default @observer class Signup extends Component {
199 className="link" 199 className="link"
200 > 200 >
201 {intl.formatMessage(messages.privacy)} 201 {intl.formatMessage(messages.privacy)}
202 </Link>. 202 </Link>
203 .
203 </p> 204 </p>
204 </form> 205 </form>
205 <div className="auth__links"> 206 <div className="auth__links">
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 3ababe54a..dbe0bb4b6 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -6,6 +6,8 @@ import { TitleBar } from 'electron-react-titlebar';
6 6
7import InfoBar from '../ui/InfoBar'; 7import InfoBar from '../ui/InfoBar';
8import { Component as DelayApp } from '../../features/delayApp'; 8import { Component as DelayApp } from '../../features/delayApp';
9import ErrorBoundary from '../util/ErrorBoundary';
10
9import globalMessages from '../../i18n/globalMessages'; 11import globalMessages from '../../i18n/globalMessages';
10 12
11import { isWindows } from '../../environment'; 13import { isWindows } from '../../environment';
@@ -94,74 +96,78 @@ export default @observer class AppLayout extends Component {
94 const { intl } = this.context; 96 const { intl } = this.context;
95 97
96 return ( 98 return (
97 <div className={(darkMode ? 'theme__dark' : '')}> 99 <ErrorBoundary>
98 <div className="app"> 100 <div className={(darkMode ? 'theme__dark' : '')}>
99 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon={'assets/images/logo.svg'} />} 101 <div className="app">
100 <div className="app__content"> 102 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />}
101 {sidebar} 103 <div className="app__content">
102 <div className="app__service"> 104 {sidebar}
103 {news.length > 0 && news.map(item => ( 105 <div className="app__service">
104 <InfoBar 106 {news.length > 0 && news.map(item => (
105 key={item.id} 107 <InfoBar
106 position="top" 108 key={item.id}
107 type={item.type} 109 position="top"
108 sticky={item.sticky} 110 type={item.type}
109 onHide={() => removeNewsItem({ newsId: item.id })} 111 sticky={item.sticky}
110 > 112 onHide={() => removeNewsItem({ newsId: item.id })}
111 <span dangerouslySetInnerHTML={createMarkup(item.message)} /> 113 >
112 </InfoBar> 114 <span dangerouslySetInnerHTML={createMarkup(item.message)} />
113 ))} 115 </InfoBar>
114 {!isOnline && ( 116 ))}
115 <InfoBar 117 {!isOnline && (
116 type="danger" 118 <InfoBar
117 > 119 type="danger"
118 <span className="mdi mdi-flash" /> 120 >
119 {intl.formatMessage(globalMessages.notConnectedToTheInternet)} 121 <span className="mdi mdi-flash" />
120 </InfoBar> 122 {intl.formatMessage(globalMessages.notConnectedToTheInternet)}
121 )} 123 </InfoBar>
122 {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( 124 )}
123 <InfoBar 125 {!areRequiredRequestsSuccessful && showRequiredRequestsError && (
124 type="danger" 126 <InfoBar
125 ctaLabel="Try again" 127 type="danger"
126 ctaLoading={areRequiredRequestsLoading} 128 ctaLabel="Try again"
127 sticky 129 ctaLoading={areRequiredRequestsLoading}
128 onClick={retryRequiredRequests} 130 sticky
129 > 131 onClick={retryRequiredRequests}
130 <span className="mdi mdi-flash" /> 132 >
131 {intl.formatMessage(messages.requiredRequestsFailed)} 133 <span className="mdi mdi-flash" />
132 </InfoBar> 134 {intl.formatMessage(messages.requiredRequestsFailed)}
133 )} 135 </InfoBar>
134 {showServicesUpdatedInfoBar && ( 136 )}
135 <InfoBar 137 {showServicesUpdatedInfoBar && (
136 type="primary" 138 <InfoBar
137 ctaLabel={intl.formatMessage(messages.buttonReloadServices)} 139 type="primary"
138 onClick={reloadServicesAfterUpdate} 140 ctaLabel={intl.formatMessage(messages.buttonReloadServices)}
139 sticky 141 onClick={reloadServicesAfterUpdate}
140 > 142 sticky
141 <span className="mdi mdi-power-plug" /> 143 >
142 {intl.formatMessage(messages.servicesUpdated)} 144 <span className="mdi mdi-power-plug" />
143 </InfoBar> 145 {intl.formatMessage(messages.servicesUpdated)}
144 )} 146 </InfoBar>
145 {appUpdateIsDownloaded && ( 147 )}
146 <InfoBar 148 {appUpdateIsDownloaded && (
147 type="primary" 149 <InfoBar
148 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)} 150 type="primary"
149 onClick={installAppUpdate} 151 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
150 sticky 152 onClick={installAppUpdate}
151 > 153 sticky
152 <span className="mdi mdi-information" /> 154 >
153 {intl.formatMessage(messages.updateAvailable)} <a href="https://meetfranz.com/changelog" target="_blank"> 155 <span className="mdi mdi-information" />
154 <u>{intl.formatMessage(messages.changelog)}</u> 156 {intl.formatMessage(messages.updateAvailable)}
155 </a> 157 {' '}
156 </InfoBar> 158 <a href="https://meetfranz.com/changelog" target="_blank">
157 )} 159 <u>{intl.formatMessage(messages.changelog)}</u>
158 {isDelayAppScreenVisible && (<DelayApp />)} 160 </a>
159 {services} 161 </InfoBar>
162 )}
163 {isDelayAppScreenVisible && (<DelayApp />)}
164 {services}
165 </div>
160 </div> 166 </div>
161 </div> 167 </div>
168 {children}
162 </div> 169 </div>
163 {children} 170 </ErrorBoundary>
164 </div>
165 ); 171 );
166 } 172 }
167} 173}
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 6ea95bf88..609a3b604 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -65,6 +65,7 @@ export default @observer class Sidebar extends Component {
65 disableToolTip={() => this.disableToolTip()} 65 disableToolTip={() => this.disableToolTip()}
66 /> 66 />
67 <button 67 <button
68 type="button"
68 onClick={toggleMuteApp} 69 onClick={toggleMuteApp}
69 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`} 70 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
70 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`} 71 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`}
@@ -72,6 +73,7 @@ export default @observer class Sidebar extends Component {
72 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} /> 73 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} />
73 </button> 74 </button>
74 <button 75 <button
76 type="button"
75 onClick={() => openSettings({ path: 'recipes' })} 77 onClick={() => openSettings({ path: 'recipes' })}
76 className="sidebar__button sidebar__button--new-service" 78 className="sidebar__button sidebar__button--new-service"
77 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`} 79 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`}
@@ -79,6 +81,7 @@ export default @observer class Sidebar extends Component {
79 <i className="mdi mdi-plus-box" /> 81 <i className="mdi mdi-plus-box" />
80 </button> 82 </button>
81 <button 83 <button
84 type="button"
82 onClick={() => openSettings({ path: 'app' })} 85 onClick={() => openSettings({ path: 'app' })}
83 className="sidebar__button sidebar__button--settings" 86 className="sidebar__button sidebar__button--settings"
84 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+,)`} 87 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+,)`}
diff --git a/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js b/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js
new file mode 100644
index 000000000..415a8d1b5
--- /dev/null
+++ b/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js
@@ -0,0 +1,84 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6
7import Button from '../../../ui/Button';
8
9import styles from './styles';
10
11const messages = defineMessages({
12 headline: {
13 id: 'service.errorHandler.headline',
14 defaultMessage: '!!!Oh no!',
15 },
16 text: {
17 id: 'service.errorHandler.text',
18 defaultMessage: '!!!{name} has failed to load.',
19 },
20 action: {
21 id: 'service.errorHandler.action',
22 defaultMessage: '!!!Reload {name}',
23 },
24 editAction: {
25 id: 'service.errorHandler.editAction',
26 defaultMessage: '!!!Edit {name}',
27 },
28 errorMessage: {
29 id: 'service.errorHandler.message',
30 defaultMessage: '!!!Error:',
31 },
32});
33
34export default @injectSheet(styles) @observer class WebviewCrashHandler extends Component {
35 static propTypes = {
36 name: PropTypes.string.isRequired,
37 reload: PropTypes.func.isRequired,
38 edit: PropTypes.func.isRequired,
39 errorMessage: PropTypes.string.isRequired,
40 classes: PropTypes.object.isRequired,
41 };
42
43 static contextTypes = {
44 intl: intlShape,
45 };
46
47 render() {
48 const {
49 name,
50 reload,
51 edit,
52 errorMessage,
53 classes,
54 } = this.props;
55 const { intl } = this.context;
56
57 return (
58 <div className={classes.component}>
59 <h1>{intl.formatMessage(messages.headline)}</h1>
60 <p>{intl.formatMessage(messages.text, { name })}</p>
61 <p>
62 <strong>
63 {intl.formatMessage(messages.errorMessage)}
64:
65 </strong>
66 {' '}
67 {errorMessage}
68 </p>
69 <div className={classes.buttonContainer}>
70 <Button
71 label={intl.formatMessage(messages.editAction, { name })}
72 buttonType="inverted"
73 onClick={() => edit()}
74 />
75 <Button
76 label={intl.formatMessage(messages.action, { name })}
77 buttonType="inverted"
78 onClick={() => reload()}
79 />
80 </div>
81 </div>
82 );
83 }
84}
diff --git a/src/components/services/content/ErrorHandlers/styles.js b/src/components/services/content/ErrorHandlers/styles.js
new file mode 100644
index 000000000..f11386798
--- /dev/null
+++ b/src/components/services/content/ErrorHandlers/styles.js
@@ -0,0 +1,25 @@
1export default {
2 component: {
3 left: 0,
4 position: 'absolute',
5 top: 0,
6 width: '100%',
7 zIndex: 0,
8 alignItems: 'center',
9 // background: $theme-gray-lighter;
10 display: 'flex',
11 flexDirection: 'column',
12 justifyContent: 'center',
13 textAlign: 'center',
14 },
15 buttonContainer: {
16 display: 'flex',
17 flexDirection: 'row',
18 height: 'auto',
19 margin: [40, 0, 20],
20
21 '& button': {
22 margin: [0, 10, 0, 10],
23 },
24 },
25};
diff --git a/src/components/services/content/ServiceDisabled.js b/src/components/services/content/ServiceDisabled.js
index 58fb38d8c..d0f12256e 100644
--- a/src/components/services/content/ServiceDisabled.js
+++ b/src/components/services/content/ServiceDisabled.js
@@ -27,6 +27,7 @@ export default @observer class ServiceDisabled extends Component {
27 }; 27 };
28 28
29 countdownInterval = null; 29 countdownInterval = null;
30
30 countdownIntervalTimeout = 1000; 31 countdownIntervalTimeout = 1000;
31 32
32 render() { 33 render() {
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 7163209ee..b1a2c0207 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { autorun } from 'mobx'; 3import { autorun } from 'mobx';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
@@ -7,7 +7,9 @@ import classnames from 'classnames';
7 7
8import ServiceModel from '../../../models/Service'; 8import ServiceModel from '../../../models/Service';
9import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl'; 9import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl';
10import WebviewLoader from '../../ui/WebviewLoader';
10import WebviewCrashHandler from './WebviewCrashHandler'; 11import WebviewCrashHandler from './WebviewCrashHandler';
12import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
11import ServiceDisabled from './ServiceDisabled'; 13import ServiceDisabled from './ServiceDisabled';
12 14
13export default @observer class ServiceWebview extends Component { 15export default @observer class ServiceWebview extends Component {
@@ -15,8 +17,10 @@ export default @observer class ServiceWebview extends Component {
15 service: PropTypes.instanceOf(ServiceModel).isRequired, 17 service: PropTypes.instanceOf(ServiceModel).isRequired,
16 setWebviewReference: PropTypes.func.isRequired, 18 setWebviewReference: PropTypes.func.isRequired,
17 reload: PropTypes.func.isRequired, 19 reload: PropTypes.func.isRequired,
20 edit: PropTypes.func.isRequired,
18 isAppMuted: PropTypes.bool.isRequired, 21 isAppMuted: PropTypes.bool.isRequired,
19 enable: PropTypes.func.isRequired, 22 enable: PropTypes.func.isRequired,
23 isActive: PropTypes.bool,
20 }; 24 };
21 25
22 static defaultProps = { 26 static defaultProps = {
@@ -29,8 +33,12 @@ export default @observer class ServiceWebview extends Component {
29 statusBarVisible: false, 33 statusBarVisible: false,
30 }; 34 };
31 35
36 autorunDisposer = null;
37
38 webview = null;
39
32 componentDidMount() { 40 componentDidMount() {
33 autorun(() => { 41 this.autorunDisposer = autorun(() => {
34 if (this.props.service.isActive) { 42 if (this.props.service.isActive) {
35 this.setState({ forceRepaint: true }); 43 this.setState({ forceRepaint: true });
36 setTimeout(() => { 44 setTimeout(() => {
@@ -40,6 +48,10 @@ export default @observer class ServiceWebview extends Component {
40 }); 48 });
41 } 49 }
42 50
51 componentWillUnmount() {
52 this.autorunDisposer();
53 }
54
43 updateTargetUrl = (event) => { 55 updateTargetUrl = (event) => {
44 let visible = true; 56 let visible = true;
45 if (event.url === '' || event.url === '#') { 57 if (event.url === '' || event.url === '#') {
@@ -51,13 +63,12 @@ export default @observer class ServiceWebview extends Component {
51 }); 63 });
52 } 64 }
53 65
54 webview = null;
55
56 render() { 66 render() {
57 const { 67 const {
58 service, 68 service,
59 setWebviewReference, 69 setWebviewReference,
60 reload, 70 reload,
71 edit,
61 isAppMuted, 72 isAppMuted,
62 enable, 73 enable,
63 } = this.props; 74 } = this.props;
@@ -78,25 +89,47 @@ export default @observer class ServiceWebview extends Component {
78 89
79 return ( 90 return (
80 <div className={webviewClasses}> 91 <div className={webviewClasses}>
81 {service.hasCrashed && ( 92 {service.isActive && service.isEnabled && (
82 <WebviewCrashHandler 93 <Fragment>
83 name={service.recipe.name} 94 {service.hasCrashed && (
84 webview={service.webview} 95 <WebviewCrashHandler
85 reload={reload} 96 name={service.recipe.name}
86 /> 97 webview={service.webview}
98 reload={reload}
99 />
100 )}
101 {service.isEnabled && service.isLoading && service.isFirstLoad && (
102 <WebviewLoader
103 loaded={false}
104 name={service.name}
105 />
106 )}
107 {service.isError && (
108 <WebviewErrorHandler
109 name={service.recipe.name}
110 errorMessage={service.errorMessage}
111 reload={reload}
112 edit={edit}
113 />
114 )}
115 </Fragment>
87 )} 116 )}
88 {!service.isEnabled ? ( 117 {!service.isEnabled ? (
89 <ServiceDisabled 118 <Fragment>
90 name={service.recipe.name} 119 {service.isActive && (
91 webview={service.webview} 120 <ServiceDisabled
92 enable={enable} 121 name={service.recipe.name}
93 /> 122 webview={service.webview}
123 enable={enable}
124 />
125 )}
126 </Fragment>
94 ) : ( 127 ) : (
95 <Webview 128 <Webview
96 ref={(element) => { this.webview = element; }} 129 ref={(element) => { this.webview = element; }}
97 autosize 130 autosize
98 src={service.url} 131 src={service.url}
99 preload="./webview/plugin.js" 132 preload="./webview/recipe.js"
100 partition={`persist:service-${service.id}`} 133 partition={`persist:service-${service.id}`}
101 onDidAttach={() => setWebviewReference({ 134 onDidAttach={() => setWebviewReference({
102 serviceId: service.id, 135 serviceId: service.id,
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index 4cbd51043..1aeb17e03 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -20,18 +20,18 @@ const messages = defineMessages({
20 20
21export default @observer class Services extends Component { 21export default @observer class Services extends Component {
22 static propTypes = { 22 static propTypes = {
23 services: MobxPropTypes.arrayOrObservableArray.isRequired, 23 services: MobxPropTypes.arrayOrObservableArray,
24 setWebviewReference: PropTypes.func.isRequired, 24 setWebviewReference: PropTypes.func.isRequired,
25 handleIPCMessage: PropTypes.func.isRequired, 25 handleIPCMessage: PropTypes.func.isRequired,
26 openWindow: PropTypes.func.isRequired, 26 openWindow: PropTypes.func.isRequired,
27 reload: PropTypes.func.isRequired, 27 reload: PropTypes.func.isRequired,
28 openSettings: PropTypes.func.isRequired,
28 isAppMuted: PropTypes.bool.isRequired, 29 isAppMuted: PropTypes.bool.isRequired,
29 update: PropTypes.func.isRequired, 30 update: PropTypes.func.isRequired,
30 }; 31 };
31 32
32 static defaultProps = { 33 static defaultProps = {
33 services: [], 34 services: [],
34 activeService: '',
35 }; 35 };
36 36
37 static contextTypes = { 37 static contextTypes = {
@@ -45,6 +45,7 @@ export default @observer class Services extends Component {
45 setWebviewReference, 45 setWebviewReference,
46 openWindow, 46 openWindow,
47 reload, 47 reload,
48 openSettings,
48 isAppMuted, 49 isAppMuted,
49 update, 50 update,
50 } = this.props; 51 } = this.props;
@@ -79,6 +80,7 @@ export default @observer class Services extends Component {
79 setWebviewReference={setWebviewReference} 80 setWebviewReference={setWebviewReference}
80 openWindow={openWindow} 81 openWindow={openWindow}
81 reload={() => reload({ serviceId: service.id })} 82 reload={() => reload({ serviceId: service.id })}
83 edit={() => openSettings({ path: `services/edit/${service.id}` })}
82 isAppMuted={isAppMuted} 84 isAppMuted={isAppMuted}
83 enable={() => update({ 85 enable={() => update({
84 serviceId: service.id, 86 serviceId: service.id,
diff --git a/src/components/services/content/WebviewCrashHandler.js b/src/components/services/content/WebviewCrashHandler.js
index 3be1fccf4..42bc3c877 100644
--- a/src/components/services/content/WebviewCrashHandler.js
+++ b/src/components/services/content/WebviewCrashHandler.js
@@ -38,13 +38,18 @@ export default @observer class WebviewCrashHandler extends Component {
38 countdown: 10000, 38 countdown: 10000,
39 } 39 }
40 40
41 countdownInterval = null;
42
43 countdownIntervalTimeout = 1000;
44
45
41 componentDidMount() { 46 componentDidMount() {
42 const { reload } = this.props; 47 const { reload } = this.props;
43 48
44 this.countdownInterval = setInterval(() => { 49 this.countdownInterval = setInterval(() => {
45 this.setState({ 50 this.setState(prevState => ({
46 countdown: this.state.countdown - this.countdownIntervalTimeout, 51 countdown: prevState.countdown - this.countdownIntervalTimeout,
47 }); 52 }));
48 53
49 if (this.state.countdown <= 0) { 54 if (this.state.countdown <= 0) {
50 reload(); 55 reload();
@@ -53,9 +58,6 @@ export default @observer class WebviewCrashHandler extends Component {
53 }, this.countdownIntervalTimeout); 58 }, this.countdownIntervalTimeout);
54 } 59 }
55 60
56 countdownInterval = null;
57 countdownIntervalTimeout = 1000;
58
59 render() { 61 render() {
60 const { name, reload } = this.props; 62 const { name, reload } = this.props;
61 const { intl } = this.context; 63 const { intl } = this.context;
@@ -70,10 +72,12 @@ export default @observer class WebviewCrashHandler extends Component {
70 buttonType="inverted" 72 buttonType="inverted"
71 onClick={() => reload()} 73 onClick={() => reload()}
72 /> 74 />
73 <p className="footnote">{intl.formatMessage(messages.autoReload, { 75 <p className="footnote">
74 name, 76 {intl.formatMessage(messages.autoReload, {
75 seconds: this.state.countdown / 1000, 77 name,
76 })}</p> 78 seconds: this.state.countdown / 1000,
79 })}
80 </p>
77 </div> 81 </div>
78 ); 82 );
79 } 83 }
diff --git a/src/components/settings/SettingsLayout.js b/src/components/settings/SettingsLayout.js
index 3cb08feb1..72ba7b2e3 100644
--- a/src/components/settings/SettingsLayout.js
+++ b/src/components/settings/SettingsLayout.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4 4
5import ErrorBoundary from '../util/ErrorBoundary';
5import { oneOrManyChildElements } from '../../prop-types'; 6import { oneOrManyChildElements } from '../../prop-types';
6import Appear from '../ui/effects/Appear'; 7import Appear from '../ui/effects/Appear';
7 8
@@ -36,18 +37,22 @@ export default @observer class SettingsLayout extends Component {
36 return ( 37 return (
37 <Appear transitionName="fadeIn-fast"> 38 <Appear transitionName="fadeIn-fast">
38 <div className="settings-wrapper"> 39 <div className="settings-wrapper">
39 <button 40 <ErrorBoundary>
40 className="settings-wrapper__action"
41 onClick={closeSettings}
42 />
43 <div className="settings franz-form">
44 {navigation}
45 {children}
46 <button 41 <button
47 className="settings__close mdi mdi-close" 42 type="button"
43 className="settings-wrapper__action"
48 onClick={closeSettings} 44 onClick={closeSettings}
49 /> 45 />
50 </div> 46 <div className="settings franz-form">
47 {navigation}
48 {children}
49 <button
50 type="button"
51 className="settings__close mdi mdi-close"
52 onClick={closeSettings}
53 />
54 </div>
55 </ErrorBoundary>
51 </div> 56 </div>
52 </Appear> 57 </Appear>
53 ); 58 );
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 06c7074dd..9c9543749 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
@@ -132,21 +132,19 @@ export default @observer class AccountDashboard extends Component {
132 )} 132 )}
133 133
134 {!isLoading && userInfoRequestFailed && ( 134 {!isLoading && userInfoRequestFailed && (
135 <div> 135 <Infobox
136 <Infobox 136 icon="alert"
137 icon="alert" 137 type="danger"
138 type="danger" 138 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)}
139 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)} 139 ctaLoading={isLoading}
140 ctaLoading={isLoading} 140 ctaOnClick={retryUserInfoRequest}
141 ctaOnClick={retryUserInfoRequest} 141 >
142 > 142 {intl.formatMessage(messages.userInfoRequestFailed)}
143 {intl.formatMessage(messages.userInfoRequestFailed)} 143 </Infobox>
144 </Infobox>
145 </div>
146 )} 144 )}
147 145
148 {!userInfoRequestFailed && ( 146 {!userInfoRequestFailed && (
149 <div> 147 <Fragment>
150 {!isLoading && ( 148 {!isLoading && (
151 <div className="account"> 149 <div className="account">
152 <div className="account__box account__box--flex"> 150 <div className="account__box account__box--flex">
@@ -169,7 +167,8 @@ export default @observer class AccountDashboard extends Component {
169 {`${user.firstname} ${user.lastname}`} 167 {`${user.firstname} ${user.lastname}`}
170 </h2> 168 </h2>
171 {user.organization && `${user.organization}, `} 169 {user.organization && `${user.organization}, `}
172 {user.email}<br /> 170 {user.email}
171 <br />
173 {!user.isEnterprise && !user.isPremium && ( 172 {!user.isEnterprise && !user.isPremium && (
174 <span className="badge badge">{intl.formatMessage(messages.accountTypeBasic)}</span> 173 <span className="badge badge">{intl.formatMessage(messages.accountTypeBasic)}</span>
175 )} 174 )}
@@ -194,7 +193,7 @@ export default @observer class AccountDashboard extends Component {
194 ) : ( 193 ) : (
195 <div className="account franz-form"> 194 <div className="account franz-form">
196 {orders.length > 0 && ( 195 {orders.length > 0 && (
197 <div> 196 <Fragment>
198 <div className="account__box"> 197 <div className="account__box">
199 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> 198 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2>
200 <div className="account__subscription"> 199 <div className="account__subscription">
@@ -219,6 +218,7 @@ export default @observer class AccountDashboard extends Component {
219 </td> 218 </td>
220 <td className="invoices__action"> 219 <td className="invoices__action">
221 <button 220 <button
221 type="button"
222 onClick={() => openExternalUrl(order.invoiceUrl)} 222 onClick={() => openExternalUrl(order.invoiceUrl)}
223 > 223 >
224 {intl.formatMessage(messages.invoiceDownload)} 224 {intl.formatMessage(messages.invoiceDownload)}
@@ -229,7 +229,7 @@ export default @observer class AccountDashboard extends Component {
229 </tbody> 229 </tbody>
230 </table> 230 </table>
231 </div> 231 </div>
232 </div> 232 </Fragment>
233 )} 233 )}
234 </div> 234 </div>
235 ) 235 )
@@ -262,20 +262,6 @@ export default @observer class AccountDashboard extends Component {
262 </div> 262 </div>
263 )} 263 )}
264 264
265 {user.isMiner && (
266 <div className="account franz-form">
267 <div className="account__box account__box">
268 <h2>Miner Info</h2>
269 <div className="account__subscription">
270 <div>
271 <p>To maintain a high security level for all our Franz users, we had to remove the miner. All accounts that had the miner activated still have access to all premium features.</p>
272 <p>Every financial support is still much appreciated.</p>
273 </div>
274 </div>
275 </div>
276 </div>
277 )}
278
279 {!user.isEnterprise && !user.isPremium && ( 265 {!user.isEnterprise && !user.isPremium && (
280 isLoadingPlans ? ( 266 isLoadingPlans ? (
281 <Loader /> 267 <Loader />
@@ -312,7 +298,7 @@ export default @observer class AccountDashboard extends Component {
312 </div> 298 </div>
313 </div> 299 </div>
314 )} 300 )}
315 </div> 301 </Fragment>
316 )} 302 )}
317 </div> 303 </div>
318 <ReactTooltip place="right" type="dark" effect="solid" /> 304 <ReactTooltip place="right" type="dark" effect="solid" />
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index b86d94ac7..953f702f8 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -59,7 +59,9 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
59 className="settings-navigation__link" 59 className="settings-navigation__link"
60 activeClassName="is-active" 60 activeClassName="is-active"
61 > 61 >
62 {intl.formatMessage(messages.yourServices)} <span className="badge">{serviceCount}</span> 62 {intl.formatMessage(messages.yourServices)}
63 {' '}
64 <span className="badge">{serviceCount}</span>
63 </Link> 65 </Link>
64 <Link 66 <Link
65 to="/settings/user" 67 to="/settings/user"
@@ -93,4 +95,3 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
93 ); 95 );
94 } 96 }
95} 97}
96
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.js
index dae8891b3..3bb0852b2 100644
--- a/src/components/settings/recipes/RecipeItem.js
+++ b/src/components/settings/recipes/RecipeItem.js
@@ -15,6 +15,7 @@ export default @observer class RecipeItem extends Component {
15 15
16 return ( 16 return (
17 <button 17 <button
18 type="button"
18 className="recipe-teaser" 19 className="recipe-teaser"
19 onClick={onClick} 20 onClick={onClick}
20 > 21 >
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js
index cd783200f..00cd725cf 100644
--- a/src/components/settings/recipes/RecipesDashboard.js
+++ b/src/components/settings/recipes/RecipesDashboard.js
@@ -129,11 +129,17 @@ export default @observer class RecipesDashboard extends Component {
129 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`} 129 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`}
130 onClick={() => resetSearch()} 130 onClick={() => resetSearch()}
131 > 131 >
132 {intl.formatMessage(messages.devRecipes)} ({devRecipesCount}) 132 {intl.formatMessage(messages.devRecipes)}
133 {' '}
134(
135 {devRecipesCount}
136)
133 </Link> 137 </Link>
134 )} 138 )}
135 <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request"> 139 <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request">
136 {intl.formatMessage(messages.missingService)} <i className="mdi mdi-open-in-new" /> 140 {intl.formatMessage(messages.missingService)}
141 {' '}
142 <i className="mdi mdi-open-in-new" />
137 </a> 143 </a>
138 </div> 144 </div>
139 {/* )} */} 145 {/* )} */}
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index d16ec35b8..468d85c45 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
@@ -14,6 +14,7 @@ import Input from '../../ui/Input';
14import Toggle from '../../ui/Toggle'; 14import Toggle from '../../ui/Toggle';
15import Button from '../../ui/Button'; 15import Button from '../../ui/Button';
16import ImageUpload from '../../ui/ImageUpload'; 16import ImageUpload from '../../ui/ImageUpload';
17import Select from '../../ui/Select';
17 18
18import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; 19import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
19 20
@@ -96,7 +97,11 @@ const messages = defineMessages({
96 }, 97 },
97 headlineProxy: { 98 headlineProxy: {
98 id: 'settings.service.form.proxy.headline', 99 id: 'settings.service.form.proxy.headline',
99 defaultMessage: '!!!Proxy Settings', 100 defaultMessage: '!!!HTTP/HTTPS Proxy Settings',
101 },
102 proxyRestartInfo: {
103 id: 'settings.service.form.proxy.restartInfo',
104 defaultMessage: '!!!Please restart Franz after changing proxy Settings.',
100 }, 105 },
101 proxyInfo: { 106 proxyInfo: {
102 id: 'settings.service.form.proxy.info', 107 id: 'settings.service.form.proxy.info',
@@ -129,6 +134,7 @@ export default @observer class EditServiceForm extends Component {
129 static defaultProps = { 134 static defaultProps = {
130 service: {}, 135 service: {},
131 }; 136 };
137
132 static contextTypes = { 138 static contextTypes = {
133 intl: intlShape, 139 intl: intlShape,
134 }; 140 };
@@ -270,14 +276,14 @@ export default @observer class EditServiceForm extends Component {
270 {recipe.hasCustomUrl && ( 276 {recipe.hasCustomUrl && (
271 <TabItem title={intl.formatMessage(messages.tabOnPremise)}> 277 <TabItem title={intl.formatMessage(messages.tabOnPremise)}>
272 {user.isPremium || recipe.author.find(a => a.email === user.email) ? ( 278 {user.isPremium || recipe.author.find(a => a.email === user.email) ? (
273 <div> 279 <Fragment>
274 <Input field={form.$('customUrl')} /> 280 <Input field={form.$('customUrl')} />
275 {form.error === 'url-validation-error' && ( 281 {form.error === 'url-validation-error' && (
276 <p className="franz-form__error"> 282 <p className="franz-form__error">
277 {intl.formatMessage(messages.customUrlValidationError, { name: recipe.name })} 283 {intl.formatMessage(messages.customUrlValidationError, { name: recipe.name })}
278 </p> 284 </p>
279 )} 285 )}
280 </div> 286 </Fragment>
281 ) : ( 287 ) : (
282 <div className="center premium-info"> 288 <div className="center premium-info">
283 <p>{intl.formatMessage(messages.customUrlPremiumInfo)}</p> 289 <p>{intl.formatMessage(messages.customUrlPremiumInfo)}</p>
@@ -307,12 +313,12 @@ export default @observer class EditServiceForm extends Component {
307 <h3>{intl.formatMessage(messages.headlineBadges)}</h3> 313 <h3>{intl.formatMessage(messages.headlineBadges)}</h3>
308 <Toggle field={form.$('isBadgeEnabled')} /> 314 <Toggle field={form.$('isBadgeEnabled')} />
309 {recipe.hasIndirectMessages && form.$('isBadgeEnabled').value && ( 315 {recipe.hasIndirectMessages && form.$('isBadgeEnabled').value && (
310 <div> 316 <Fragment>
311 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> 317 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} />
312 <p className="settings__help"> 318 <p className="settings__help">
313 {intl.formatMessage(messages.indirectMessageInfo)} 319 {intl.formatMessage(messages.indirectMessageInfo)}
314 </p> 320 </p>
315 </div> 321 </Fragment>
316 )} 322 )}
317 </div> 323 </div>
318 324
@@ -333,6 +339,12 @@ export default @observer class EditServiceForm extends Component {
333 </div> 339 </div>
334 </div> 340 </div>
335 341
342 <PremiumFeatureContainer>
343 <div className="settings__settings-group">
344 <Select field={form.$('spellcheckerLanguage')} />
345 </div>
346 </PremiumFeatureContainer>
347
336 {isProxyFeatureEnabled && ( 348 {isProxyFeatureEnabled && (
337 <PremiumFeatureContainer condition={isProxyFeaturePremiumFeature}> 349 <PremiumFeatureContainer condition={isProxyFeaturePremiumFeature}>
338 <div className="settings__settings-group"> 350 <div className="settings__settings-group">
@@ -342,18 +354,31 @@ export default @observer class EditServiceForm extends Component {
342 </h3> 354 </h3>
343 <Toggle field={form.$('proxy.isEnabled')} /> 355 <Toggle field={form.$('proxy.isEnabled')} />
344 {form.$('proxy.isEnabled').value && ( 356 {form.$('proxy.isEnabled').value && (
345 <div> 357 <Fragment>
346 <Input field={form.$('proxy.host')} /> 358 <div className="grid">
347 <Input field={form.$('proxy.user')} /> 359 <div className="grid__row">
348 <Input 360 <Input field={form.$('proxy.host')} className="proxyHost" />
349 field={form.$('proxy.password')} 361 <Input field={form.$('proxy.port')} />
350 showPasswordToggle 362 </div>
351 /> 363 </div>
364 <div className="grid">
365 <div className="grid__row">
366 <Input field={form.$('proxy.user')} />
367 <Input
368 field={form.$('proxy.password')}
369 showPasswordToggle
370 />
371 </div>
372 </div>
373 <p>
374 <span className="mdi mdi-information" />
375 {intl.formatMessage(messages.proxyRestartInfo)}
376 </p>
352 <p> 377 <p>
353 <span className="mdi mdi-information" /> 378 <span className="mdi mdi-information" />
354 {intl.formatMessage(messages.proxyInfo)} 379 {intl.formatMessage(messages.proxyInfo)}
355 </p> 380 </p>
356 </div> 381 </Fragment>
357 )} 382 )}
358 </div> 383 </div>
359 </PremiumFeatureContainer> 384 </PremiumFeatureContainer>
diff --git a/src/components/settings/services/ServiceItem.js b/src/components/settings/services/ServiceItem.js
index 84080519b..ebc618a00 100644
--- a/src/components/settings/services/ServiceItem.js
+++ b/src/components/settings/services/ServiceItem.js
@@ -27,6 +27,7 @@ export default @observer class ServiceItem extends Component {
27 service: PropTypes.instanceOf(ServiceModel).isRequired, 27 service: PropTypes.instanceOf(ServiceModel).isRequired,
28 goToServiceForm: PropTypes.func.isRequired, 28 goToServiceForm: PropTypes.func.isRequired,
29 }; 29 };
30
30 static contextTypes = { 31 static contextTypes = {
31 intl: intlShape, 32 intl: intlShape,
32 }; 33 };
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
index e7dfaf106..a12df7372 100644
--- a/src/components/settings/services/ServicesDashboard.js
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -101,17 +101,15 @@ export default @observer class ServicesDashboard extends Component {
101 /> 101 />
102 )} 102 )}
103 {!isLoading && servicesRequestFailed && ( 103 {!isLoading && servicesRequestFailed && (
104 <div> 104 <Infobox
105 <Infobox 105 icon="alert"
106 icon="alert" 106 type="danger"
107 type="danger" 107 ctaLabel={intl.formatMessage(messages.tryReloadServices)}
108 ctaLabel={intl.formatMessage(messages.tryReloadServices)} 108 ctaLoading={isLoading}
109 ctaLoading={isLoading} 109 ctaOnClick={retryServicesRequest}
110 ctaOnClick={retryServicesRequest} 110 >
111 > 111 {intl.formatMessage(messages.servicesRequestFailed)}
112 {intl.formatMessage(messages.servicesRequestFailed)} 112 </Infobox>
113 </Infobox>
114 </div>
115 )} 113 )}
116 114
117 {status.length > 0 && status.includes('updated') && ( 115 {status.length > 0 && status.includes('updated') && (
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 1ec2ab614..a92e559f3 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -1,5 +1,5 @@
1import { remote } from 'electron'; 1import { remote } from 'electron';
2import React, { Component } from 'react'; 2import React, { Component, Fragment } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
@@ -171,21 +171,23 @@ export default @observer class EditSettingsForm extends Component {
171 <PremiumFeatureContainer 171 <PremiumFeatureContainer
172 condition={isSpellcheckerPremiumFeature} 172 condition={isSpellcheckerPremiumFeature}
173 > 173 >
174 <div> 174 <Fragment>
175 <Toggle 175 <Toggle
176 field={form.$('enableSpellchecking')} 176 field={form.$('enableSpellchecking')}
177 /> 177 />
178 {form.$('enableSpellchecking').value && ( 178 {form.$('enableSpellchecking').value && (
179 <Select field={form.$('spellcheckerLanguage')} /> 179 <Select field={form.$('spellcheckerLanguage')} />
180 )} 180 )}
181 </div> 181 </Fragment>
182 </PremiumFeatureContainer> 182 </PremiumFeatureContainer>
183 <a 183 <a
184 href={FRANZ_TRANSLATION} 184 href={FRANZ_TRANSLATION}
185 target="_blank" 185 target="_blank"
186 className="link" 186 className="link"
187 > 187 >
188 {intl.formatMessage(messages.translationHelp)} <i className="mdi mdi-open-in-new" /> 188 {intl.formatMessage(messages.translationHelp)}
189 {' '}
190 <i className="mdi mdi-open-in-new" />
189 </a> 191 </a>
190 192
191 {/* Advanced */} 193 {/* Advanced */}
@@ -233,7 +235,9 @@ export default @observer class EditSettingsForm extends Component {
233 )} 235 )}
234 <br /> 236 <br />
235 <Toggle field={form.$('beta')} /> 237 <Toggle field={form.$('beta')} />
236 {intl.formatMessage(messages.currentVersion)} {remote.app.getVersion()} 238 {intl.formatMessage(messages.currentVersion)}
239 {' '}
240 {remote.app.getVersion()}
237 </form> 241 </form>
238 </div> 242 </div>
239 </div> 243 </div>
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js
index b825f844a..0e3ac6b10 100644
--- a/src/components/settings/user/EditUserForm.js
+++ b/src/components/settings/user/EditUserForm.js
@@ -48,10 +48,6 @@ export default @observer class EditServiceForm extends Component {
48 isEnterprise: PropTypes.bool.isRequired, 48 isEnterprise: PropTypes.bool.isRequired,
49 }; 49 };
50 50
51 static defaultProps = {
52 service: {},
53 };
54
55 static contextTypes = { 51 static contextTypes = {
56 intl: intlShape, 52 intl: intlShape,
57 }; 53 };
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index 12e8471ff..90da8ddc3 100644
--- a/src/components/subscription/SubscriptionForm.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
@@ -81,8 +81,7 @@ export default @observer class SubscriptionForm extends Component {
81 hideInfo: PropTypes.bool.isRequired, 81 hideInfo: PropTypes.bool.isRequired,
82 }; 82 };
83 83
84 static defaultProps ={ 84 static defaultProps = {
85 content: '',
86 showSkipOption: false, 85 showSkipOption: false,
87 skipAction: () => null, 86 skipAction: () => null,
88 skipButtonLabel: '', 87 skipButtonLabel: '',
@@ -158,35 +157,33 @@ export default @observer class SubscriptionForm extends Component {
158 <Radio field={this.form.$('paymentTier')} showLabel={false} className="paymentTiers" /> 157 <Radio field={this.form.$('paymentTier')} showLabel={false} className="paymentTiers" />
159 {!hideInfo && ( 158 {!hideInfo && (
160 <div className="subscription__premium-info"> 159 <div className="subscription__premium-info">
161 <div> 160 <p>
162 <p> 161 <strong>{intl.formatMessage(messages.includedFeatures)}</strong>
163 <strong>{intl.formatMessage(messages.includedFeatures)}</strong> 162 </p>
164 </p> 163 <div className="subscription">
165 <div className="subscription"> 164 <ul className="subscription__premium-features">
166 <ul className="subscription__premium-features"> 165 <li>{intl.formatMessage(messages.features.onpremise)}</li>
167 <li>{intl.formatMessage(messages.features.onpremise)}</li> 166 <li>
168 <li> 167 {intl.formatMessage(messages.features.noInterruptions)}
169 {intl.formatMessage(messages.features.noInterruptions)} 168 </li>
170 </li> 169 <li>
171 <li> 170 {intl.formatMessage(messages.features.spellchecker)}
172 {intl.formatMessage(messages.features.spellchecker)} 171 </li>
173 </li> 172 <li>
174 <li> 173 {intl.formatMessage(messages.features.proxy)}
175 {intl.formatMessage(messages.features.proxy)} 174 </li>
176 </li> 175 <li>
177 <li> 176 {intl.formatMessage(messages.features.ads)}
178 {intl.formatMessage(messages.features.ads)} 177 </li>
179 </li> 178 </ul>
180 </ul>
181 </div>
182 </div> 179 </div>
183 </div> 180 </div>
184 )} 181 )}
185 <div> 182 <Fragment>
186 {error.code === 'no-payment-session' && ( 183 {error.code === 'no-payment-session' && (
187 <p className="error-message center">{intl.formatMessage(messages.paymentSessionError)}</p> 184 <p className="error-message center">{intl.formatMessage(messages.paymentSessionError)}</p>
188 )} 185 )}
189 </div> 186 </Fragment>
190 {showSkipOption && this.form.$('paymentTier').value === 'skip' ? ( 187 {showSkipOption && this.form.$('paymentTier').value === 'skip' ? (
191 <Button 188 <Button
192 label={skipButtonLabel} 189 label={skipButtonLabel}
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js
index f3c63e7ee..b5d7c4b2d 100644
--- a/src/components/subscription/SubscriptionPopup.js
+++ b/src/components/subscription/SubscriptionPopup.js
@@ -46,7 +46,9 @@ export default @observer class SubscriptionPopup extends Component {
46 } 46 }
47 47
48 render() { 48 render() {
49 const { url, closeWindow, completeCheck, isCompleted } = this.props; 49 const {
50 url, closeWindow, completeCheck, isCompleted,
51 } = this.props;
50 const { intl } = this.context; 52 const { intl } = this.context;
51 53
52 return ( 54 return (
diff --git a/src/components/ui/AppLoader.js b/src/components/ui/AppLoader.js
deleted file mode 100644
index ac3cdcb05..000000000
--- a/src/components/ui/AppLoader.js
+++ /dev/null
@@ -1,15 +0,0 @@
1import React from 'react';
2
3import Appear from '../../components/ui/effects/Appear';
4import Loader from '../../components/ui/Loader';
5
6export default function () {
7 return (
8 <div className="app-loader">
9 <Appear>
10 <h1 className="app-loader__title">Franz</h1>
11 <Loader color="#FFF" />
12 </Appear>
13 </div>
14 );
15}
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js
new file mode 100644
index 000000000..61053f6d1
--- /dev/null
+++ b/src/components/ui/AppLoader/index.js
@@ -0,0 +1,70 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import injectSheet, { withTheme } from 'react-jss';
4import classnames from 'classnames';
5
6import FullscreenLoader from '../FullscreenLoader';
7import { shuffleArray } from '../../../helpers/array-helpers';
8
9import styles from './styles';
10
11const textList = shuffleArray([
12 'Looking for Sisi',
13 'Contacting the herald',
14 'Saddling the unicorn',
15 'Learning the Waltz',
16 'Visiting Horst & Grete',
17 'Twisting my moustache',
18 'Playing the trumpet',
19 'Traveling through space & time',
20]);
21
22export default @injectSheet(styles) @withTheme class AppLoader extends Component {
23 static propTypes = {
24 classes: PropTypes.object.isRequired,
25 theme: PropTypes.object.isRequired,
26 }
27
28 state = {
29 step: 0,
30 }
31
32 interval = null;
33
34 componentDidMount() {
35 this.interval = setInterval(() => {
36 this.setState(prevState => ({
37 step: prevState.step === textList.length - 1 ? 0 : prevState.step + 1,
38 }));
39 }, 2500);
40 }
41
42 componentWillUnmount() {
43 clearInterval(this.interval);
44 }
45
46 render() {
47 const { classes, theme } = this.props;
48 const { step } = this.state;
49
50 return (
51 <FullscreenLoader
52 title="Franz"
53 className={classes.component}
54 spinnerColor={theme.colorAppLoaderSpinner}
55 >
56 {textList.map((text, i) => (
57 <span
58 key={text}
59 className={classnames({
60 [`${classes.slogan}`]: true,
61 [`${classes.visible}`]: step === i,
62 })}
63 >
64 {text}
65 </span>
66 ))}
67 </FullscreenLoader>
68 );
69 }
70}
diff --git a/src/components/ui/AppLoader/styles.js b/src/components/ui/AppLoader/styles.js
new file mode 100644
index 000000000..755a56b40
--- /dev/null
+++ b/src/components/ui/AppLoader/styles.js
@@ -0,0 +1,16 @@
1export default {
2 component: {
3 color: '#FFF',
4 },
5 slogan: {
6 display: 'block',
7 opacity: 0,
8 transition: 'opacity 1s ease',
9 position: 'absolute',
10 textAlign: 'center',
11 width: '100%',
12 },
13 visible: {
14 opacity: 1,
15 },
16};
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js
index 309e05bb4..ffc7f7051 100644
--- a/src/components/ui/Button.js
+++ b/src/components/ui/Button.js
@@ -62,6 +62,8 @@ export default @observer class Button extends Component {
62 } 62 }
63 63
64 return ( 64 return (
65 // disabling rule as button has type defined in `buttonProps`
66 /* eslint-disable react/button-has-type */
65 <button {...buttonProps}> 67 <button {...buttonProps}>
66 <Loader 68 <Loader
67 loaded={loaded} 69 loaded={loaded}
@@ -72,6 +74,7 @@ export default @observer class Button extends Component {
72 /> 74 />
73 {label} 75 {label}
74 </button> 76 </button>
77 /* eslint-enable react/button-has-type */
75 ); 78 );
76 } 79 }
77} 80}
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js
new file mode 100644
index 000000000..6ecf4d395
--- /dev/null
+++ b/src/components/ui/FullscreenLoader/index.js
@@ -0,0 +1,56 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet, { withTheme } from 'react-jss';
5import classnames from 'classnames';
6
7import Loader from '../Loader';
8
9import styles from './styles';
10
11export default @observer @withTheme @injectSheet(styles) class FullscreenLoader extends Component {
12 static propTypes = {
13 className: PropTypes.string,
14 title: PropTypes.string.isRequired,
15 classes: PropTypes.object.isRequired,
16 theme: PropTypes.object.isRequired,
17 spinnerColor: PropTypes.string,
18 children: PropTypes.node,
19 }
20
21 static defaultProps = {
22 className: null,
23 spinnerColor: null,
24 children: null,
25 }
26
27 render() {
28 const {
29 classes,
30 title,
31 children,
32 spinnerColor,
33 className,
34 theme,
35 } = this.props;
36
37 return (
38 <div className={classes.wrapper}>
39 <div
40 className={classnames({
41 [`${classes.component}`]: true,
42 [`${className}`]: className,
43 })}
44 >
45 <h1 className={classes.title}>{title}</h1>
46 <Loader color={spinnerColor || theme.colorFullscreenLoaderSpinner} />
47 {children && (
48 <div className={classes.content}>
49 {children}
50 </div>
51 )}
52 </div>
53 </div>
54 );
55 }
56}
diff --git a/src/components/ui/FullscreenLoader/styles.js b/src/components/ui/FullscreenLoader/styles.js
new file mode 100644
index 000000000..64d24e4ce
--- /dev/null
+++ b/src/components/ui/FullscreenLoader/styles.js
@@ -0,0 +1,23 @@
1export default {
2 wrapper: {
3 display: 'flex',
4 alignItems: 'center',
5 position: 'absolute',
6 width: '100%',
7 },
8 component: {
9 width: '100%',
10 display: 'flex',
11 flexDirection: 'column',
12 alignItems: 'center',
13 textAlign: 'center',
14 height: 'auto',
15 },
16 title: {
17 fontSize: 35,
18 },
19 content: {
20 marginTop: 20,
21 width: '100%',
22 },
23};
diff --git a/src/components/ui/ImageUpload.js b/src/components/ui/ImageUpload.js
index 76f77d631..83a05554b 100644
--- a/src/components/ui/ImageUpload.js
+++ b/src/components/ui/ImageUpload.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form'; 4import { Field } from 'mobx-react-form';
@@ -23,6 +23,8 @@ export default @observer class ImageUpload extends Component {
23 path: null, 23 path: null,
24 } 24 }
25 25
26 dropzoneRef = null;
27
26 onDrop(acceptedFiles) { 28 onDrop(acceptedFiles) {
27 const { field } = this.props; 29 const { field } = this.props;
28 30
@@ -36,8 +38,6 @@ export default @observer class ImageUpload extends Component {
36 field.set(''); 38 field.set('');
37 } 39 }
38 40
39 dropzoneRef = null;
40
41 render() { 41 render() {
42 const { 42 const {
43 field, 43 field,
@@ -57,7 +57,7 @@ export default @observer class ImageUpload extends Component {
57 <label className="franz-form__label" htmlFor="iconUpload">{field.label}</label> 57 <label className="franz-form__label" htmlFor="iconUpload">{field.label}</label>
58 <div className="image-upload"> 58 <div className="image-upload">
59 {(field.value && field.value !== 'delete') || this.state.path ? ( 59 {(field.value && field.value !== 'delete') || this.state.path ? (
60 <div> 60 <Fragment>
61 <div 61 <div
62 className="image-upload__preview" 62 className="image-upload__preview"
63 style={({ 63 style={({
@@ -84,7 +84,7 @@ export default @observer class ImageUpload extends Component {
84 </button> 84 </button>
85 <div className="image-upload__action-background" /> 85 <div className="image-upload__action-background" />
86 </div> 86 </div>
87 </div> 87 </Fragment>
88 ) : ( 88 ) : (
89 <Dropzone 89 <Dropzone
90 ref={(node) => { this.dropzoneRef = node; }} 90 ref={(node) => { this.dropzoneRef = node; }}
diff --git a/src/components/ui/InfoBar.js b/src/components/ui/InfoBar.js
index 94a1ddf76..612399e9f 100644
--- a/src/components/ui/InfoBar.js
+++ b/src/components/ui/InfoBar.js
@@ -5,7 +5,7 @@ import classnames from 'classnames';
5import Loader from 'react-loader'; 5import Loader from 'react-loader';
6 6
7// import { oneOrManyChildElements } from '../../prop-types'; 7// import { oneOrManyChildElements } from '../../prop-types';
8import Appear from '../ui/effects/Appear'; 8import Appear from './effects/Appear';
9 9
10export default @observer class InfoBar extends Component { 10export default @observer class InfoBar extends Component {
11 static propTypes = { 11 static propTypes = {
@@ -64,6 +64,7 @@ export default @observer class InfoBar extends Component {
64 {children} 64 {children}
65 {ctaLabel && ( 65 {ctaLabel && (
66 <button 66 <button
67 type="button"
67 className="info-bar__cta" 68 className="info-bar__cta"
68 onClick={onClick} 69 onClick={onClick}
69 > 70 >
@@ -80,6 +81,7 @@ export default @observer class InfoBar extends Component {
80 </div> 81 </div>
81 {!sticky && ( 82 {!sticky && (
82 <button 83 <button
84 type="button"
83 className="info-bar__close mdi mdi-close" 85 className="info-bar__close mdi mdi-close"
84 onClick={onHide} 86 onClick={onHide}
85 /> 87 />
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js
index 77051f567..a33c6474a 100644
--- a/src/components/ui/Infobox.js
+++ b/src/components/ui/Infobox.js
@@ -61,6 +61,7 @@ export default @observer class Infobox extends Component {
61 <button 61 <button
62 className="infobox__cta" 62 className="infobox__cta"
63 onClick={ctaOnClick} 63 onClick={ctaOnClick}
64 type="button"
64 > 65 >
65 <Loader 66 <Loader
66 loaded={!ctaLoading} 67 loaded={!ctaLoading}
@@ -74,6 +75,7 @@ export default @observer class Infobox extends Component {
74 )} 75 )}
75 {dismissable && ( 76 {dismissable && (
76 <button 77 <button
78 type="button"
77 onClick={() => this.setState({ 79 onClick={() => this.setState({
78 dismissed: true, 80 dismissed: true,
79 })} 81 })}
diff --git a/src/components/ui/Input.js b/src/components/ui/Input.js
index 7bf6e1b00..9b070c4df 100644
--- a/src/components/ui/Input.js
+++ b/src/components/ui/Input.js
@@ -33,6 +33,8 @@ export default @observer class Input extends Component {
33 passwordScore: 0, 33 passwordScore: 0,
34 } 34 }
35 35
36 inputElement = null;
37
36 componentDidMount() { 38 componentDidMount() {
37 if (this.props.focus) { 39 if (this.props.focus) {
38 this.focus(); 40 this.focus();
@@ -53,8 +55,6 @@ export default @observer class Input extends Component {
53 this.inputElement.focus(); 55 this.inputElement.focus();
54 } 56 }
55 57
56 inputElement = null;
57
58 render() { 58 render() {
59 const { 59 const {
60 field, 60 field,
@@ -110,7 +110,7 @@ export default @observer class Input extends Component {
110 'mdi-eye': !this.state.showPassword, 110 'mdi-eye': !this.state.showPassword,
111 'mdi-eye-off': this.state.showPassword, 111 'mdi-eye-off': this.state.showPassword,
112 })} 112 })}
113 onClick={() => this.setState({ showPassword: !this.state.showPassword })} 113 onClick={() => this.setState(prevState => ({ showPassword: !prevState.showPassword }))}
114 tabIndex="-1" 114 tabIndex="-1"
115 /> 115 />
116 )} 116 )}
diff --git a/src/components/ui/Link.js b/src/components/ui/Link.js
index 0602290f1..b88686d5e 100644
--- a/src/components/ui/Link.js
+++ b/src/components/ui/Link.js
@@ -72,5 +72,4 @@ Link.wrappedComponent.defaultProps = {
72 activeClassName: '', 72 activeClassName: '',
73 strictFilter: false, 73 strictFilter: false,
74 target: '', 74 target: '',
75 openInBrowser: false,
76}; 75};
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
index 73984be94..67cd6af0b 100644
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -73,4 +73,3 @@ PremiumFeatureContainer.wrappedComponent.propTypes = {
73 }).isRequired, 73 }).isRequired,
74 }).isRequired, 74 }).isRequired,
75}; 75};
76
diff --git a/src/components/ui/PremiumFeatureContainer/styles.js b/src/components/ui/PremiumFeatureContainer/styles.js
index 16c40d0ec..81d6666c6 100644
--- a/src/components/ui/PremiumFeatureContainer/styles.js
+++ b/src/components/ui/PremiumFeatureContainer/styles.js
@@ -5,6 +5,7 @@ export default theme => ({
5 margin: [0, 0, 20, -20], 5 margin: [0, 0, 20, -20],
6 padding: 20, 6 padding: 20,
7 'border-radius': theme.borderRadius, 7 'border-radius': theme.borderRadius,
8 pointerEvents: 'none',
8 }, 9 },
9 titleContainer: { 10 titleContainer: {
10 display: 'flex', 11 display: 'flex',
@@ -20,6 +21,7 @@ export default theme => ({
20 'border-radius': theme.borderRadiusSmall, 21 'border-radius': theme.borderRadiusSmall,
21 padding: [2, 4], 22 padding: [2, 4],
22 'font-size': 12, 23 'font-size': 12,
24 pointerEvents: 'initial',
23 }, 25 },
24 content: { 26 content: {
25 opacity: 0.5, 27 opacity: 0.5,
diff --git a/src/components/ui/Radio.js b/src/components/ui/Radio.js
index 63ca6f9b8..ba13aca63 100644
--- a/src/components/ui/Radio.js
+++ b/src/components/ui/Radio.js
@@ -18,6 +18,8 @@ export default @observer class Radio extends Component {
18 showLabel: true, 18 showLabel: true,
19 }; 19 };
20 20
21 inputElement = null;
22
21 componentDidMount() { 23 componentDidMount() {
22 if (this.props.focus) { 24 if (this.props.focus) {
23 this.focus(); 25 this.focus();
@@ -28,8 +30,6 @@ export default @observer class Radio extends Component {
28 this.inputElement.focus(); 30 this.inputElement.focus();
29 } 31 }
30 32
31 inputElement = null;
32
33 render() { 33 render() {
34 const { 34 const {
35 field, 35 field,
diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.js
index 5a9571d27..78d6aae8b 100644
--- a/src/components/ui/SearchInput.js
+++ b/src/components/ui/SearchInput.js
@@ -2,7 +2,6 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
5import uuidv1 from 'uuid/v1';
6import { debounce } from 'lodash'; 5import { debounce } from 'lodash';
7 6
8export default @observer class SearchInput extends Component { 7export default @observer class SearchInput extends Component {
@@ -22,7 +21,7 @@ export default @observer class SearchInput extends Component {
22 value: '', 21 value: '',
23 placeholder: '', 22 placeholder: '',
24 className: '', 23 className: '',
25 name: uuidv1(), 24 name: 'searchInput',
26 throttle: false, 25 throttle: false,
27 throttleDelay: 250, 26 throttleDelay: 250,
28 onChange: () => null, 27 onChange: () => null,
@@ -30,6 +29,8 @@ export default @observer class SearchInput extends Component {
30 autoFocus: false, 29 autoFocus: false,
31 } 30 }
32 31
32 input = null;
33
33 constructor(props) { 34 constructor(props) {
34 super(props); 35 super(props);
35 36
@@ -74,8 +75,6 @@ export default @observer class SearchInput extends Component {
74 onReset(); 75 onReset();
75 } 76 }
76 77
77 input = null;
78
79 render() { 78 render() {
80 const { className, name, placeholder } = this.props; 79 const { className, name, placeholder } = this.props;
81 const { value } = this.state; 80 const { value } = this.state;
@@ -90,15 +89,17 @@ export default @observer class SearchInput extends Component {
90 <label 89 <label
91 htmlFor={name} 90 htmlFor={name}
92 className="mdi mdi-magnify" 91 className="mdi mdi-magnify"
93 /> 92 >
94 <input 93 <input
95 name={name} 94 name={name}
96 type="text" 95 id={name}
97 placeholder={placeholder} 96 type="text"
98 value={value} 97 placeholder={placeholder}
99 onChange={e => this.onChange(e)} 98 value={value}
100 ref={(ref) => { this.input = ref; }} 99 onChange={e => this.onChange(e)}
101 /> 100 ref={(ref) => { this.input = ref; }}
101 />
102 </label>
102 {value.length > 0 && ( 103 {value.length > 0 && (
103 <span 104 <span
104 className="mdi mdi-close-circle-outline" 105 className="mdi mdi-close-circle-outline"
diff --git a/src/components/ui/Select.js b/src/components/ui/Select.js
index abcad417e..da52243ca 100644
--- a/src/components/ui/Select.js
+++ b/src/components/ui/Select.js
@@ -9,12 +9,13 @@ export default @observer class Select extends Component {
9 field: PropTypes.instanceOf(Field).isRequired, 9 field: PropTypes.instanceOf(Field).isRequired,
10 className: PropTypes.string, 10 className: PropTypes.string,
11 showLabel: PropTypes.bool, 11 showLabel: PropTypes.bool,
12 disabled: PropTypes.bool,
12 }; 13 };
13 14
14 static defaultProps = { 15 static defaultProps = {
15 className: null, 16 className: null,
16 focus: false,
17 showLabel: true, 17 showLabel: true,
18 disabled: false,
18 }; 19 };
19 20
20 render() { 21 render() {
@@ -22,6 +23,7 @@ export default @observer class Select extends Component {
22 field, 23 field,
23 className, 24 className,
24 showLabel, 25 showLabel,
26 disabled,
25 } = this.props; 27 } = this.props;
26 28
27 return ( 29 return (
@@ -29,6 +31,7 @@ export default @observer class Select extends Component {
29 className={classnames({ 31 className={classnames({
30 'franz-form__field': true, 32 'franz-form__field': true,
31 'has-error': field.error, 33 'has-error': field.error,
34 'is-disabled': disabled,
32 [`${className}`]: className, 35 [`${className}`]: className,
33 })} 36 })}
34 > 37 >
@@ -45,12 +48,13 @@ export default @observer class Select extends Component {
45 id={field.id} 48 id={field.id}
46 defaultValue={field.value} 49 defaultValue={field.value}
47 className="franz-form__select" 50 className="franz-form__select"
51 disabled={field.disabled || disabled}
48 > 52 >
49 {field.options.map(type => ( 53 {field.options.map(type => (
50 <option 54 <option
51 key={type.value} 55 key={type.value}
52 value={type.value} 56 value={type.value}
53 // selected={field.value === } 57 disabled={type.disabled}
54 > 58 >
55 {type.label} 59 {type.label}
56 </option> 60 </option>
diff --git a/src/components/ui/StatusBarTargetUrl.js b/src/components/ui/StatusBarTargetUrl.js
index 4285a343c..6fc50fe5c 100644
--- a/src/components/ui/StatusBarTargetUrl.js
+++ b/src/components/ui/StatusBarTargetUrl.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
5 5
6import Appear from '../ui/effects/Appear'; 6import Appear from './effects/Appear';
7 7
8export default @observer class StatusBarTargetUrl extends Component { 8export default @observer class StatusBarTargetUrl extends Component {
9 static propTypes = { 9 static propTypes = {
@@ -13,7 +13,6 @@ export default @observer class StatusBarTargetUrl extends Component {
13 13
14 static defaultProps = { 14 static defaultProps = {
15 className: '', 15 className: '',
16 position: 'bottom',
17 text: '', 16 text: '',
18 }; 17 };
19 18
diff --git a/src/components/ui/Tabs/TabItem.js b/src/components/ui/Tabs/TabItem.js
index 9ff9f009e..16881a7f7 100644
--- a/src/components/ui/Tabs/TabItem.js
+++ b/src/components/ui/Tabs/TabItem.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import React, { Component, Fragment } from 'react';
2 2
3import { oneOrManyChildElements } from '../../../prop-types'; 3import { oneOrManyChildElements } from '../../../prop-types';
4 4
@@ -11,7 +11,7 @@ export default class TabItem extends Component {
11 const { children } = this.props; 11 const { children } = this.props;
12 12
13 return ( 13 return (
14 <div>{children}</div> 14 <Fragment>{children}</Fragment>
15 ); 15 );
16 } 16 }
17} 17}
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js
new file mode 100644
index 000000000..3a3dbbe49
--- /dev/null
+++ b/src/components/ui/WebviewLoader/index.js
@@ -0,0 +1,25 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5
6import FullscreenLoader from '../FullscreenLoader';
7
8import styles from './styles';
9
10export default @observer @injectSheet(styles) class WebviewLoader extends Component {
11 static propTypes = {
12 name: PropTypes.string.isRequired,
13 classes: PropTypes.object.isRequired,
14 }
15
16 render() {
17 const { classes, name } = this.props;
18 return (
19 <FullscreenLoader
20 className={classes.component}
21 title={`Loading ${name}`}
22 />
23 );
24 }
25}
diff --git a/src/components/ui/WebviewLoader/styles.js b/src/components/ui/WebviewLoader/styles.js
new file mode 100644
index 000000000..dbd75db8a
--- /dev/null
+++ b/src/components/ui/WebviewLoader/styles.js
@@ -0,0 +1,9 @@
1export default theme => ({
2 component: {
3 background: theme.colorWebviewLoaderBackground,
4 padding: 20,
5 width: 'auto',
6 margin: [0, 'auto'],
7 borderRadius: 6,
8 },
9});
diff --git a/src/components/util/ErrorBoundary/index.js b/src/components/util/ErrorBoundary/index.js
new file mode 100644
index 000000000..5db0db226
--- /dev/null
+++ b/src/components/util/ErrorBoundary/index.js
@@ -0,0 +1,60 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import injectSheet from 'react-jss';
4import { defineMessages, intlShape } from 'react-intl';
5
6import Button from '../../ui/Button';
7
8import styles from './styles';
9
10const messages = defineMessages({
11 headline: {
12 id: 'app.errorHandler.headline',
13 defaultMessage: '!!!Something went wrong.',
14 },
15 action: {
16 id: 'app.errorHandler.action',
17 defaultMessage: '!!!Reload',
18 },
19});
20
21export default @injectSheet(styles) class ErrorBoundary extends Component {
22 state = {
23 hasError: false,
24 }
25
26 static propTypes = {
27 classes: PropTypes.object.isRequired,
28 children: PropTypes.node.isRequired,
29 }
30
31 static contextTypes = {
32 intl: intlShape,
33 };
34
35 componentDidCatch() {
36 this.setState({ hasError: true });
37 }
38
39 render() {
40 const { classes } = this.props;
41 const { intl } = this.context;
42
43 if (this.state.hasError) {
44 return (
45 <div className={classes.component}>
46 <h1 className={classes.title}>
47 {intl.formatMessage(messages.headline)}
48 </h1>
49 <Button
50 label={intl.formatMessage(messages.action)}
51 buttonType="inverted"
52 onClick={() => window.location.reload()}
53 />
54 </div>
55 );
56 }
57
58 return this.props.children;
59 }
60}
diff --git a/src/components/util/ErrorBoundary/styles.js b/src/components/util/ErrorBoundary/styles.js
new file mode 100644
index 000000000..0960546ff
--- /dev/null
+++ b/src/components/util/ErrorBoundary/styles.js
@@ -0,0 +1,13 @@
1export default theme => ({
2 component: {
3 display: 'flex',
4 width: '100%',
5 alignItems: 'center',
6 justifyContent: 'center',
7 flexDirection: 'column',
8 },
9 title: {
10 fontSize: 20,
11 color: theme.colorText,
12 },
13});