aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/auth/SetupAssistant.js3
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js3
-rw-r--r--src/components/settings/user/EditUserForm.js3
-rw-r--r--src/components/ui/button/index.tsx265
-rw-r--r--src/components/ui/error/index.tsx20
-rw-r--r--src/components/ui/error/styles.ts9
-rw-r--r--src/components/ui/input/index.tsx208
-rw-r--r--src/components/ui/input/scorePassword.ts42
-rw-r--r--src/components/ui/input/styles.ts102
-rw-r--r--src/components/ui/label/index.tsx52
-rw-r--r--src/components/ui/label/styles.ts12
-rw-r--r--src/components/ui/select/index.tsx461
-rw-r--r--src/components/ui/textarea/index.tsx126
-rw-r--r--src/components/ui/textarea/styles.ts54
-rw-r--r--src/components/ui/toggle/index.tsx125
-rw-r--r--src/components/ui/typings/generic.ts8
-rw-r--r--src/components/ui/wrapper/index.tsx37
-rw-r--r--src/features/quickSwitch/Component.js2
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.js4
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js5
-rw-r--r--src/features/workspaces/components/WorkspaceServiceListItem.tsx2
21 files changed, 1534 insertions, 9 deletions
diff --git a/src/components/auth/SetupAssistant.js b/src/components/auth/SetupAssistant.js
index d009a2878..1665bf837 100644
--- a/src/components/auth/SetupAssistant.js
+++ b/src/components/auth/SetupAssistant.js
@@ -5,7 +5,8 @@ import { defineMessages, injectIntl } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import classnames from 'classnames'; 6import classnames from 'classnames';
7 7
8import { Input, Button } from '@meetfranz/forms'; 8import { Input } from '../ui/input/index';
9import { Button } from '../ui/button/index';
9import { Badge } from '../ui/badge'; 10import { Badge } from '../ui/badge';
10import Modal from '../ui/Modal'; 11import Modal from '../ui/Modal';
11import Infobox from '../ui/Infobox'; 12import Infobox from '../ui/Infobox';
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js
index 8ab726eb3..bdb6f3ca0 100644
--- a/src/components/settings/recipes/RecipesDashboard.js
+++ b/src/components/settings/recipes/RecipesDashboard.js
@@ -4,9 +4,10 @@ import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6 6
7import { Button, Input } from '@meetfranz/forms';
8import injectSheet from 'react-jss'; 7import injectSheet from 'react-jss';
9 8
9import { Button } from '../../ui/button/index';
10import { Input } from '../../ui/input/index';
10import { H3, H2 } from '../../ui/headline'; 11import { H3, H2 } from '../../ui/headline';
11import SearchInput from '../../ui/SearchInput'; 12import SearchInput from '../../ui/SearchInput';
12import Infobox from '../../ui/Infobox'; 13import Infobox from '../../ui/Infobox';
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js
index 55883e65f..1b8a4f25a 100644
--- a/src/components/settings/user/EditUserForm.js
+++ b/src/components/settings/user/EditUserForm.js
@@ -3,10 +3,9 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6import { Input } from '@meetfranz/forms';
7 6
7import { Input } from '../../ui/input/index';
8import Form from '../../../lib/Form'; 8import Form from '../../../lib/Form';
9// import Input from '../../ui/Input';
10import Button from '../../ui/Button'; 9import Button from '../../ui/Button';
11import Radio from '../../ui/Radio'; 10import Radio from '../../ui/Radio';
12import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/Infobox';
diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx
new file mode 100644
index 000000000..5b8927b51
--- /dev/null
+++ b/src/components/ui/button/index.tsx
@@ -0,0 +1,265 @@
1import Icon from '@mdi/react';
2import classnames from 'classnames';
3import { Property } from 'csstype';
4import { Component, MouseEvent } from 'react';
5import injectStyle, { withTheme } from 'react-jss';
6import Loader from 'react-loader';
7import { Theme } from '@meetfranz/theme';
8
9import { IFormField, IWithStyle } from '../typings/generic';
10
11type ButtonType =
12 | 'primary'
13 | 'secondary'
14 | 'success'
15 | 'danger'
16 | 'warning'
17 | 'inverted';
18
19interface IProps extends IFormField, IWithStyle {
20 className?: string;
21 disabled?: boolean;
22 id?: string;
23 type?: 'button' | 'reset' | 'submit' | undefined;
24 onClick: (
25 event: MouseEvent<HTMLButtonElement> | MouseEvent<HTMLAnchorElement>,
26 ) => void;
27 buttonType?: ButtonType;
28 stretch?: boolean;
29 loaded?: boolean;
30 busy?: boolean;
31 icon?: string;
32 href?: string;
33 target?: string;
34}
35
36let buttonTransition: string = 'none';
37let loaderContainerTransition: string = 'none';
38
39if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
40 buttonTransition = 'background .5s, opacity 0.3s';
41 loaderContainerTransition = 'all 0.3s';
42}
43
44const styles = (theme: Theme) => ({
45 button: {
46 borderRadius: theme.borderRadiusSmall,
47 border: 'none',
48 display: 'inline-flex',
49 position: 'relative' as Property.Position,
50 transition: buttonTransition,
51 textAlign: 'center' as Property.TextAlign,
52 outline: 'none',
53 alignItems: 'center',
54 padding: 0,
55 width: (props: IProps) =>
56 (props.stretch ? '100%' : 'auto') as Property.Width<string>,
57 fontSize: theme.uiFontSize,
58 textDecoration: 'none',
59
60 '&:hover': {
61 opacity: 0.8,
62 },
63 '&:active': {
64 opacity: 0.5,
65 transition: 'none',
66 },
67 },
68 label: {
69 margin: '10px 20px',
70 width: '100%',
71 display: 'flex',
72 alignItems: 'center',
73 justifyContent: 'center',
74 },
75 primary: {
76 background: theme.buttonPrimaryBackground,
77 color: theme.buttonPrimaryTextColor,
78
79 '& svg': {
80 fill: theme.buttonPrimaryTextColor,
81 },
82 },
83 secondary: {
84 background: theme.buttonSecondaryBackground,
85 color: theme.buttonSecondaryTextColor,
86
87 '& svg': {
88 fill: theme.buttonSecondaryTextColor,
89 },
90 },
91 success: {
92 background: theme.buttonSuccessBackground,
93 color: theme.buttonSuccessTextColor,
94
95 '& svg': {
96 fill: theme.buttonSuccessTextColor,
97 },
98 },
99 danger: {
100 background: theme.buttonDangerBackground,
101 color: theme.buttonDangerTextColor,
102
103 '& svg': {
104 fill: theme.buttonDangerTextColor,
105 },
106 },
107 warning: {
108 background: theme.buttonWarningBackground,
109 color: theme.buttonWarningTextColor,
110
111 '& svg': {
112 fill: theme.buttonWarningTextColor,
113 },
114 },
115 inverted: {
116 background: theme.buttonInvertedBackground,
117 color: theme.buttonInvertedTextColor,
118 border: theme.buttonInvertedBorder,
119
120 '& svg': {
121 fill: theme.buttonInvertedTextColor,
122 },
123 },
124 disabled: {
125 opacity: theme.inputDisabledOpacity,
126 },
127 loader: {
128 position: 'relative' as Property.Position,
129 width: 20,
130 height: 18,
131 zIndex: 9999,
132 },
133 loaderContainer: {
134 width: (props: IProps): string => (!props.busy ? '0' : '40px'),
135 height: 20,
136 overflow: 'hidden',
137 transition: loaderContainerTransition,
138 marginLeft: (props: IProps): number => (!props.busy ? 10 : 20),
139 marginRight: (props: IProps): number => (!props.busy ? -10 : -20),
140 position: (props: IProps): Property.Position =>
141 props.stretch ? 'absolute' : 'inherit',
142 },
143 icon: {
144 margin: [1, 10, 0, -5],
145 },
146});
147
148class ButtonComponent extends Component<IProps> {
149 public static defaultProps = {
150 type: 'button',
151 disabled: false,
152 onClick: () => null,
153 buttonType: 'primary' as ButtonType,
154 stretch: false,
155 busy: false,
156 };
157
158 state = {
159 busy: false,
160 };
161
162 componentWillMount() {
163 this.setState({ busy: this.props.busy });
164 }
165
166 componentWillReceiveProps(nextProps: IProps) {
167 if (nextProps.busy !== this.props.busy) {
168 if (this.props.busy) {
169 setTimeout(() => {
170 this.setState({ busy: nextProps.busy });
171 }, 300);
172 } else {
173 this.setState({ busy: nextProps.busy });
174 }
175 }
176 }
177
178 render() {
179 const {
180 classes,
181 className,
182 theme,
183 disabled,
184 id,
185 label,
186 type,
187 onClick,
188 buttonType,
189 loaded,
190 icon,
191 href,
192 target,
193 } = this.props;
194
195 const { busy } = this.state;
196
197 let showLoader = false;
198 if (loaded) {
199 showLoader = !loaded;
200 console.warn(
201 'Ferdi Button prop `loaded` will be deprecated in the future. Please use `busy` instead',
202 );
203 }
204 if (busy) {
205 showLoader = busy;
206 }
207
208 const content = (
209 <>
210 <div className={classes.loaderContainer}>
211 {showLoader && (
212 <Loader
213 loaded={false}
214 width={4}
215 scale={0.45}
216 color={theme.buttonLoaderColor[buttonType!]}
217 parentClassName={classes.loader}
218 />
219 )}
220 </div>
221 <div className={classes.label}>
222 {icon && <Icon path={icon} size={0.8} className={classes.icon} />}
223 {label}
224 </div>
225 </>
226 );
227
228 const wrapperComponent = !href ? (
229 <button
230 id={id}
231 type={type}
232 onClick={onClick}
233 className={classnames({
234 [`${classes.button}`]: true,
235 [`${classes[buttonType as ButtonType]}`]: true,
236 [`${classes.disabled}`]: disabled,
237 [`${className}`]: className,
238 })}
239 disabled={disabled}
240 data-type="franz-button"
241 >
242 {content}
243 </button>
244 ) : (
245 <a
246 href={href}
247 target={target}
248 onClick={onClick}
249 className={classnames({
250 [`${classes.button}`]: true,
251 [`${classes[buttonType as ButtonType]}`]: true,
252 [`${className}`]: className,
253 })}
254 rel={target === '_blank' ? 'noopener' : ''}
255 data-type="franz-button"
256 >
257 {content}
258 </a>
259 );
260
261 return wrapperComponent;
262 }
263}
264
265export const Button = injectStyle(styles)(withTheme(ButtonComponent));
diff --git a/src/components/ui/error/index.tsx b/src/components/ui/error/index.tsx
new file mode 100644
index 000000000..8439bfc8b
--- /dev/null
+++ b/src/components/ui/error/index.tsx
@@ -0,0 +1,20 @@
1import { Classes } from 'jss';
2import { Component } from 'react';
3import injectSheet from 'react-jss';
4
5import styles from './styles';
6
7interface IProps {
8 classes: Classes;
9 message: string;
10}
11
12class ErrorComponent extends Component<IProps> {
13 render() {
14 const { classes, message } = this.props;
15
16 return <p className={classes.message}>{message}</p>;
17 }
18}
19
20export const Error = injectSheet(styles)(ErrorComponent);
diff --git a/src/components/ui/error/styles.ts b/src/components/ui/error/styles.ts
new file mode 100644
index 000000000..ed993ddd5
--- /dev/null
+++ b/src/components/ui/error/styles.ts
@@ -0,0 +1,9 @@
1import { Theme } from '@meetfranz/theme';
2
3export default (theme: Theme) => ({
4 message: {
5 color: theme.brandDanger,
6 margin: '5px 0 0',
7 fontSize: theme.uiFontSize,
8 },
9});
diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx
new file mode 100644
index 000000000..0b16fe688
--- /dev/null
+++ b/src/components/ui/input/index.tsx
@@ -0,0 +1,208 @@
1import { mdiEye, mdiEyeOff } from '@mdi/js';
2import Icon from '@mdi/react';
3import classnames from 'classnames';
4import { Component, createRef, InputHTMLAttributes } from 'react';
5import injectSheet from 'react-jss';
6
7import { IFormField, IWithStyle } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12import { scorePasswordFunc } from './scorePassword';
13
14import styles from './styles';
15
16interface IData {
17 [index: string]: string;
18}
19
20interface IProps
21 extends InputHTMLAttributes<HTMLInputElement>,
22 IFormField,
23 IWithStyle {
24 focus?: boolean;
25 prefix?: string;
26 suffix?: string;
27 scorePassword?: boolean;
28 showPasswordToggle?: boolean;
29 data: IData;
30 inputClassName?: string;
31 onEnterKey?: Function;
32}
33
34interface IState {
35 showPassword: boolean;
36 passwordScore: number;
37}
38
39class InputComponent extends Component<IProps, IState> {
40 static defaultProps = {
41 focus: false,
42 onChange: () => {},
43 onBlur: () => {},
44 onFocus: () => {},
45 scorePassword: false,
46 showLabel: true,
47 showPasswordToggle: false,
48 type: 'text',
49 disabled: false,
50 };
51
52 state = {
53 passwordScore: 0,
54 showPassword: false,
55 };
56
57 private inputRef = createRef<HTMLInputElement>();
58
59 componentDidMount() {
60 const { focus, data } = this.props;
61
62 if (this.inputRef && this.inputRef.current) {
63 if (focus) {
64 this.inputRef.current.focus();
65 }
66
67 if (data) {
68 Object.keys(data).map(
69 key => (this.inputRef.current!.dataset[key] = data[key]),
70 );
71 }
72 }
73 }
74
75 onChange(e: React.ChangeEvent<HTMLInputElement>) {
76 const { scorePassword, onChange } = this.props;
77
78 if (onChange) {
79 onChange(e);
80 }
81
82 if (this.inputRef && this.inputRef.current && scorePassword) {
83 this.setState({
84 passwordScore: scorePasswordFunc(this.inputRef.current.value),
85 });
86 }
87 }
88
89 onInputKeyPress(e: React.KeyboardEvent) {
90 if (e.key === 'Enter') {
91 const { onEnterKey } = this.props;
92 onEnterKey && onEnterKey();
93 }
94 }
95
96 render() {
97 const {
98 classes,
99 className,
100 disabled,
101 error,
102 id,
103 inputClassName,
104 label,
105 prefix,
106 scorePassword,
107 suffix,
108 showLabel,
109 showPasswordToggle,
110 type,
111 value,
112 name,
113 placeholder,
114 spellCheck,
115 onBlur,
116 onFocus,
117 min,
118 max,
119 step,
120 required,
121 noMargin,
122 } = this.props;
123
124 const { showPassword, passwordScore } = this.state;
125
126 const inputType = type === 'password' && showPassword ? 'text' : type;
127
128 return (
129 <Wrapper
130 className={className}
131 identifier="franz-input"
132 noMargin={noMargin}
133 >
134 <Label
135 title={label}
136 showLabel={showLabel}
137 htmlFor={id}
138 className={classes.label}
139 isRequired={required}
140 >
141 <div
142 className={classnames({
143 [`${inputClassName}`]: inputClassName,
144 [`${classes.hasPasswordScore}`]: scorePassword,
145 [`${classes.wrapper}`]: true,
146 [`${classes.disabled}`]: disabled,
147 [`${classes.hasError}`]: error,
148 })}
149 >
150 {prefix && <span className={classes.prefix}>{prefix}</span>}
151 <input
152 id={id}
153 type={inputType}
154 name={name}
155 value={value as string}
156 placeholder={placeholder}
157 spellCheck={spellCheck}
158 className={classes.input}
159 ref={this.inputRef}
160 onChange={this.onChange.bind(this)}
161 onFocus={onFocus}
162 onBlur={onBlur}
163 disabled={disabled}
164 onKeyPress={this.onInputKeyPress.bind(this)}
165 min={min}
166 max={max}
167 step={step}
168 />
169 {suffix && <span className={classes.suffix}>{suffix}</span>}
170 {showPasswordToggle && (
171 <button
172 type="button"
173 className={classes.formModifier}
174 onClick={() =>
175 this.setState(prevState => ({
176 showPassword: !prevState.showPassword,
177 }))
178 }
179 tabIndex={-1}
180 >
181 <Icon path={!showPassword ? mdiEye : mdiEyeOff} size={1} />
182 </button>
183 )}
184 </div>
185 {scorePassword && (
186 <div
187 className={classnames({
188 [`${classes.passwordScore}`]: true,
189 [`${classes.hasError}`]: error,
190 })}
191 >
192 <meter
193 value={passwordScore < 5 ? 5 : passwordScore}
194 low={30}
195 high={75}
196 optimum={100}
197 max={100}
198 />
199 </div>
200 )}
201 </Label>
202 {error && <Error message={error} />}
203 </Wrapper>
204 );
205 }
206}
207
208export const Input = injectSheet(styles)(InputComponent);
diff --git a/src/components/ui/input/scorePassword.ts b/src/components/ui/input/scorePassword.ts
new file mode 100644
index 000000000..59502e2b0
--- /dev/null
+++ b/src/components/ui/input/scorePassword.ts
@@ -0,0 +1,42 @@
1interface ILetters {
2 [key: string]: number;
3}
4
5interface IVariations {
6 [index: string]: boolean;
7 digits: boolean;
8 lower: boolean;
9 nonWords: boolean;
10 upper: boolean;
11}
12
13export function scorePasswordFunc(password: string): number {
14 let score = 0;
15 if (!password) {
16 return score;
17 }
18
19 // award every unique letter until 5 repetitions
20 const letters: ILetters = {};
21 for (const element of password) {
22 letters[element] = (letters[element] || 0) + 1;
23 score += 5 / letters[element];
24 }
25
26 // bonus points for mixing it up
27 const variations: IVariations = {
28 digits: /\d/.test(password),
29 lower: /[a-z]/.test(password),
30 nonWords: /\W/.test(password),
31 upper: /[A-Z]/.test(password),
32 };
33
34 let variationCount = 0;
35 for (const key of Object.keys(variations)) {
36 variationCount += variations[key] === true ? 1 : 0;
37 }
38
39 score += (variationCount - 1) * 10;
40
41 return Math.round(score);
42}
diff --git a/src/components/ui/input/styles.ts b/src/components/ui/input/styles.ts
new file mode 100644
index 000000000..27426152e
--- /dev/null
+++ b/src/components/ui/input/styles.ts
@@ -0,0 +1,102 @@
1import { Property } from 'csstype';
2
3import { Theme } from '@meetfranz/theme';
4
5const prefixStyles = (theme: Theme) => ({
6 background: theme.inputPrefixBackground,
7 color: theme.inputPrefixColor,
8 lineHeight: `${theme.inputHeight}px`,
9 padding: '0 10px',
10 fontSize: theme.uiFontSize,
11});
12
13export default (theme: Theme) => ({
14 label: {
15 '& > div': {
16 marginTop: 5,
17 },
18 },
19 disabled: {
20 opacity: theme.inputDisabledOpacity,
21 },
22 formModifier: {
23 background: 'none',
24 border: 0,
25 borderLeft: theme.inputBorder,
26 padding: '4px 20px 0',
27 outline: 'none',
28
29 '&:active': {
30 opacity: 0.5,
31 },
32
33 '& svg': {
34 fill: theme.inputModifierColor,
35 },
36 },
37 input: {
38 background: 'none',
39 border: 0,
40 fontSize: theme.uiFontSize,
41 outline: 'none',
42 padding: 8,
43 width: '100%',
44 color: theme.inputColor,
45
46 '&::placeholder': {
47 color: theme.inputPlaceholderColor,
48 },
49 },
50 passwordScore: {
51 background: theme.inputScorePasswordBackground,
52 border: theme.inputBorder,
53 borderTopWidth: 0,
54 borderBottomLeftRadius: theme.borderRadiusSmall,
55 borderBottomRightRadius: theme.borderRadiusSmall,
56 display: 'block',
57 flexBasis: '100%',
58 height: 5,
59 overflow: 'hidden',
60
61 '& meter': {
62 display: 'block',
63 height: '100%',
64 width: '100%',
65
66 '&::-webkit-meter-bar': {
67 background: 'none',
68 },
69
70 '&::-webkit-meter-even-less-good-value': {
71 background: theme.brandDanger,
72 },
73
74 '&::-webkit-meter-suboptimum-value': {
75 background: theme.brandWarning,
76 },
77
78 '&::-webkit-meter-optimum-value': {
79 background: theme.brandSuccess,
80 },
81 },
82 },
83 prefix: prefixStyles(theme),
84 suffix: prefixStyles(theme),
85 wrapper: {
86 background: theme.inputBackground,
87 border: theme.inputBorder,
88 borderRadius: theme.borderRadiusSmall,
89 boxSizing: 'border-box' as Property.BoxSizing,
90 display: 'flex',
91 height: theme.inputHeight,
92 order: 1,
93 width: '100%',
94 },
95 hasPasswordScore: {
96 borderBottomLeftRadius: 0,
97 borderBottomRightRadius: 0,
98 },
99 hasError: {
100 borderColor: theme.brandDanger,
101 },
102});
diff --git a/src/components/ui/label/index.tsx b/src/components/ui/label/index.tsx
new file mode 100644
index 000000000..4d86f23f7
--- /dev/null
+++ b/src/components/ui/label/index.tsx
@@ -0,0 +1,52 @@
1import classnames from 'classnames';
2import { Classes } from 'jss';
3import { Component, LabelHTMLAttributes } from 'react';
4import injectSheet from 'react-jss';
5
6import { IFormField } from '../typings/generic';
7
8import styles from './styles';
9
10interface ILabel extends IFormField, LabelHTMLAttributes<HTMLLabelElement> {
11 classes: Classes;
12 isRequired: boolean;
13}
14
15class LabelComponent extends Component<ILabel> {
16 static defaultProps = {
17 showLabel: true,
18 };
19
20 render() {
21 const {
22 title,
23 showLabel,
24 classes,
25 className,
26 children,
27 htmlFor,
28 isRequired,
29 } = this.props;
30
31 if (!showLabel) return children;
32
33 return (
34 <label
35 className={classnames({
36 [`${className}`]: className,
37 })}
38 htmlFor={htmlFor}
39 >
40 {showLabel && (
41 <span className={classes.label}>
42 {title}
43 {isRequired && ' *'}
44 </span>
45 )}
46 <div className={classes.content}>{children}</div>
47 </label>
48 );
49 }
50}
51
52export const Label = injectSheet(styles)(LabelComponent);
diff --git a/src/components/ui/label/styles.ts b/src/components/ui/label/styles.ts
new file mode 100644
index 000000000..0c9cef8bf
--- /dev/null
+++ b/src/components/ui/label/styles.ts
@@ -0,0 +1,12 @@
1import { Theme } from '@meetfranz/theme';
2
3export default (theme: Theme) => ({
4 content: {},
5 label: {
6 color: theme.labelColor,
7 fontSize: theme.uiFontSize,
8 },
9 hasError: {
10 color: theme.brandDanger,
11 },
12});
diff --git a/src/components/ui/select/index.tsx b/src/components/ui/select/index.tsx
new file mode 100644
index 000000000..41cab7818
--- /dev/null
+++ b/src/components/ui/select/index.tsx
@@ -0,0 +1,461 @@
1import {
2 mdiArrowRightDropCircleOutline,
3 mdiCloseCircle,
4 mdiMagnify,
5} from '@mdi/js';
6import Icon from '@mdi/react';
7import classnames from 'classnames';
8import { ChangeEvent, Component, createRef } from 'react';
9import injectStyle from 'react-jss';
10
11import { Theme } from '@meetfranz/theme';
12
13import { IFormField, IWithStyle } from '../typings/generic';
14
15import { Error } from '../error';
16import { Label } from '../label';
17import { Wrapper } from '../wrapper';
18
19interface IOptions {
20 [index: string]: string;
21}
22
23interface IData {
24 [index: string]: string;
25}
26
27interface IProps extends IFormField, IWithStyle {
28 actionText: string;
29 className?: string;
30 inputClassName?: string;
31 defaultValue?: string;
32 disabled?: boolean;
33 id?: string;
34 name: string;
35 options: IOptions;
36 value: string;
37 onChange: (event: ChangeEvent<HTMLInputElement>) => void;
38 showSearch: boolean;
39 data: IData;
40}
41
42interface IState {
43 open: boolean;
44 value: string;
45 needle: string;
46 selected: number;
47 options: IOptions;
48}
49
50let popupTransition: string = 'none';
51let toggleTransition: string = 'none';
52
53if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
54 popupTransition = 'all 0.3s';
55 toggleTransition = 'transform 0.3s';
56}
57
58const styles = (theme: Theme) => ({
59 select: {
60 background: theme.selectBackground,
61 border: theme.selectBorder,
62 borderRadius: theme.borderRadiusSmall,
63 height: theme.selectHeight,
64 fontSize: theme.uiFontSize,
65 width: '100%',
66 display: 'flex',
67 alignItems: 'center',
68 textAlign: 'left',
69 color: theme.selectColor,
70 },
71 label: {
72 '& > div': {
73 marginTop: 5,
74 },
75 },
76 popup: {
77 opacity: 0,
78 height: 0,
79 overflowX: 'scroll',
80 border: theme.selectBorder,
81 borderTop: 0,
82 transition: popupTransition,
83 },
84 open: {
85 opacity: 1,
86 height: 350,
87 background: theme.selectPopupBackground,
88 },
89 option: {
90 padding: 10,
91 borderBottom: theme.selectOptionBorder,
92 color: theme.selectOptionColor,
93
94 '&:hover': {
95 background: theme.selectOptionItemHover,
96 color: theme.selectOptionItemHoverColor,
97 },
98 '&:active': {
99 background: theme.selectOptionItemActive,
100 color: theme.selectOptionItemActiveColor,
101 },
102 },
103 selected: {
104 background: theme.selectOptionItemActive,
105 color: theme.selectOptionItemActiveColor,
106 },
107 toggle: {
108 marginLeft: 'auto',
109 fill: theme.selectToggleColor,
110 transition: toggleTransition,
111 },
112 toggleOpened: {
113 transform: 'rotateZ(90deg)',
114 },
115 searchContainer: {
116 display: 'flex',
117 background: theme.selectSearchBackground,
118 alignItems: 'center',
119 paddingLeft: 10,
120 color: theme.selectColor,
121
122 '& svg': {
123 fill: theme.selectSearchColor,
124 },
125 },
126 search: {
127 border: 0,
128 width: '100%',
129 fontSize: theme.uiFontSize,
130 background: 'none',
131 marginLeft: 10,
132 padding: [10, 0],
133 color: theme.selectSearchColor,
134 },
135 clearNeedle: {
136 background: 'none',
137 border: 0,
138 },
139 focused: {
140 fontWeight: 'bold',
141 background: theme.selectOptionItemHover,
142 color: theme.selectOptionItemHoverColor,
143 },
144 hasError: {
145 borderColor: theme.brandDanger,
146 },
147 disabled: {
148 opacity: theme.selectDisabledOpacity,
149 },
150});
151
152class SelectComponent extends Component<IProps> {
153 public static defaultProps = {
154 onChange: () => {},
155 showLabel: true,
156 disabled: false,
157 error: '',
158 };
159
160 state = {
161 open: false,
162 value: '',
163 needle: '',
164 selected: 0,
165 options: null,
166 };
167
168 private componentRef = createRef<HTMLDivElement>();
169
170 private inputRef = createRef<HTMLInputElement>();
171
172 private searchInputRef = createRef<HTMLInputElement>();
173
174 private scrollContainerRef = createRef<HTMLDivElement>();
175
176 private activeOptionRef = createRef<HTMLDivElement>();
177
178 private keyListener: any;
179
180 componentWillReceiveProps(nextProps: IProps) {
181 if (nextProps.value && nextProps.value !== this.props.value) {
182 this.setState({
183 value: nextProps.value,
184 });
185 }
186 }
187
188 componentDidUpdate() {
189 const { open } = this.state;
190
191 if (this.searchInputRef && this.searchInputRef.current && open) {
192 this.searchInputRef.current.focus();
193 }
194 }
195
196 componentDidMount() {
197 if (this.inputRef && this.inputRef.current) {
198 const { data } = this.props;
199
200 if (data) {
201 Object.keys(data).map(
202 key => (this.inputRef.current!.dataset[key] = data[key]),
203 );
204 }
205 }
206
207 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
208 }
209
210 componentWillMount() {
211 const { value } = this.props;
212
213 if (this.componentRef && this.componentRef.current) {
214 this.componentRef.current.removeEventListener(
215 'keydown',
216 this.keyListener,
217 );
218 }
219
220 if (value) {
221 this.setState({
222 value,
223 });
224 }
225
226 this.setFilter();
227 }
228
229 componentWillUnmount() {
230 // eslint-disable-next-line unicorn/no-invalid-remove-event-listener
231 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
232 }
233
234 setFilter(needle = '') {
235 const { options } = this.props;
236
237 let filteredOptions = {};
238 if (needle) {
239 Object.keys(options).map(key => {
240 if (
241 key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) ||
242 options[key]
243 .toLocaleLowerCase()
244 .startsWith(needle.toLocaleLowerCase())
245 ) {
246 Object.assign(filteredOptions, {
247 [`${key}`]: options[key],
248 });
249 }
250 });
251 } else {
252 filteredOptions = options;
253 }
254
255 this.setState({
256 needle,
257 options: filteredOptions,
258 selected: 0,
259 });
260 }
261
262 select(key: string) {
263 this.setState(() => ({
264 value: key,
265 open: false,
266 }));
267
268 this.setFilter();
269
270 if (this.props.onChange) {
271 this.props.onChange(key as any);
272 }
273 }
274
275 arrowKeysHandler(e: KeyboardEvent) {
276 const { selected, open, options } = this.state;
277
278 if (!open) return;
279
280 if (e.keyCode === 38 || e.keyCode === 40) {
281 e.preventDefault();
282 }
283
284 if (this.componentRef && this.componentRef.current) {
285 if (e.keyCode === 38 && selected > 0) {
286 this.setState((state: IState) => ({
287 selected: state.selected - 1,
288 }));
289 } else if (
290 e.keyCode === 40 &&
291 selected < Object.keys(options!).length - 1
292 ) {
293 this.setState((state: IState) => ({
294 selected: state.selected + 1,
295 }));
296 } else if (e.keyCode === 13) {
297 this.select(Object.keys(options!)[selected]);
298 }
299
300 if (
301 this.activeOptionRef &&
302 this.activeOptionRef.current &&
303 this.scrollContainerRef &&
304 this.scrollContainerRef.current
305 ) {
306 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
307 const optionTopOffset = this.activeOptionRef.current.offsetTop;
308
309 const topOffset = optionTopOffset - containerTopOffset;
310
311 this.scrollContainerRef.current.scrollTop = topOffset - 35;
312 }
313 }
314
315 switch (e.keyCode) {
316 case 37:
317 case 39:
318 case 38:
319 case 40: // Arrow keys
320 case 32:
321 break; // Space
322 default:
323 break; // do not block other keys
324 }
325 }
326
327 render() {
328 const {
329 actionText,
330 classes,
331 className,
332 defaultValue,
333 disabled,
334 error,
335 id,
336 inputClassName,
337 name,
338 label,
339 showLabel,
340 showSearch,
341 onChange,
342 required,
343 } = this.props;
344
345 const { open, needle, value, selected, options } = this.state;
346
347 let selection = '';
348 if (!value && defaultValue && options![defaultValue]) {
349 selection = options![defaultValue];
350 } else if (value && options![value]) {
351 selection = options![value];
352 } else {
353 selection = actionText;
354 }
355
356 return (
357 <Wrapper className={className} identifier="franz-select">
358 <Label
359 title={label}
360 showLabel={showLabel}
361 htmlFor={id}
362 className={classes.label}
363 isRequired={required}
364 >
365 <div
366 className={classnames({
367 [`${classes.hasError}`]: error,
368 [`${classes.disabled}`]: disabled,
369 })}
370 ref={this.componentRef}
371 >
372 <button
373 type="button"
374 className={classnames({
375 [`${inputClassName}`]: inputClassName,
376 [`${classes.select}`]: true,
377 [`${classes.hasError}`]: error,
378 })}
379 onClick={
380 !disabled
381 ? () =>
382 this.setState((state: IState) => ({
383 open: !state.open,
384 }))
385 : () => {}
386 }
387 >
388 {selection}
389 <Icon
390 path={mdiArrowRightDropCircleOutline}
391 size={0.8}
392 className={classnames({
393 [`${classes.toggle}`]: true,
394 [`${classes.toggleOpened}`]: open,
395 })}
396 />
397 </button>
398 {showSearch && open && (
399 <div className={classes.searchContainer}>
400 <Icon path={mdiMagnify} size={0.8} />
401 <input
402 type="text"
403 value={needle}
404 onChange={e => this.setFilter(e.currentTarget.value)}
405 placeholder="Search"
406 className={classes.search}
407 ref={this.searchInputRef}
408 />
409 {needle && (
410 <button
411 type="button"
412 className={classes.clearNeedle}
413 onClick={() => this.setFilter()}
414 >
415 <Icon path={mdiCloseCircle} size={0.7} />
416 </button>
417 )}
418 </div>
419 )}
420 <div
421 className={classnames({
422 [`${classes.popup}`]: true,
423 [`${classes.open}`]: open,
424 })}
425 ref={this.scrollContainerRef}
426 >
427 {Object.keys(options!).map((key, i) => (
428 <div
429 key={key}
430 onClick={() => this.select(key)}
431 className={classnames({
432 [`${classes.option}`]: true,
433 [`${classes.selected}`]: options![key] === selection,
434 [`${classes.focused}`]: selected === i,
435 })}
436 onMouseOver={() => this.setState({ selected: i })}
437 ref={selected === i ? this.activeOptionRef : null}
438 >
439 {options![key]}
440 </div>
441 ))}
442 </div>
443 </div>
444 <input
445 className={classes.input}
446 id={id}
447 name={name}
448 type="hidden"
449 defaultValue={value}
450 onChange={onChange}
451 disabled={disabled}
452 ref={this.inputRef}
453 />
454 </Label>
455 {error && <Error message={error} />}
456 </Wrapper>
457 );
458 }
459}
460
461export const Select = injectStyle(styles)(SelectComponent);
diff --git a/src/components/ui/textarea/index.tsx b/src/components/ui/textarea/index.tsx
new file mode 100644
index 000000000..1b16698eb
--- /dev/null
+++ b/src/components/ui/textarea/index.tsx
@@ -0,0 +1,126 @@
1import classnames from 'classnames';
2import { Component, createRef, TextareaHTMLAttributes } from 'react';
3import injectSheet from 'react-jss';
4
5import { IFormField, IWithStyle } from '../typings/generic';
6
7import { Error } from '../error';
8import { Label } from '../label';
9import { Wrapper } from '../wrapper';
10
11import styles from './styles';
12
13interface IData {
14 [index: string]: string;
15}
16
17interface IProps
18 extends TextareaHTMLAttributes<HTMLTextAreaElement>,
19 IFormField,
20 IWithStyle {
21 focus?: boolean;
22 data: IData;
23 textareaClassName?: string;
24}
25
26class TextareaComponent extends Component<IProps> {
27 static defaultProps = {
28 focus: false,
29 onChange: () => {},
30 onBlur: () => {},
31 onFocus: () => {},
32 showLabel: true,
33 disabled: false,
34 rows: 5,
35 };
36
37 private textareaRef = createRef<HTMLTextAreaElement>();
38
39 componentDidMount() {
40 const { data } = this.props;
41
42 if (this.textareaRef && this.textareaRef.current && data) {
43 Object.keys(data).map(
44 key => (this.textareaRef.current!.dataset[key] = data[key]),
45 );
46 }
47 }
48
49 onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
50 const { onChange } = this.props;
51
52 if (onChange) {
53 onChange(e);
54 }
55 }
56
57 render() {
58 const {
59 classes,
60 className,
61 disabled,
62 error,
63 id,
64 textareaClassName,
65 label,
66 showLabel,
67 value,
68 name,
69 placeholder,
70 spellCheck,
71 onBlur,
72 onFocus,
73 minLength,
74 maxLength,
75 required,
76 rows,
77 noMargin,
78 } = this.props;
79
80 return (
81 <Wrapper
82 className={className}
83 identifier="franz-textarea"
84 noMargin={noMargin}
85 >
86 <Label
87 title={label}
88 showLabel={showLabel}
89 htmlFor={id}
90 className={classes.label}
91 isRequired={required}
92 >
93 <div
94 className={classnames({
95 [`${textareaClassName}`]: textareaClassName,
96 [`${classes.wrapper}`]: true,
97 [`${classes.disabled}`]: disabled,
98 [`${classes.hasError}`]: error,
99 })}
100 >
101 <textarea
102 id={id}
103 name={name}
104 placeholder={placeholder}
105 spellCheck={spellCheck}
106 className={classes.textarea}
107 ref={this.textareaRef}
108 onChange={this.onChange.bind(this)}
109 onFocus={onFocus}
110 onBlur={onBlur}
111 disabled={disabled}
112 minLength={minLength}
113 maxLength={maxLength}
114 rows={rows}
115 >
116 {value}
117 </textarea>
118 </div>
119 </Label>
120 {error && <Error message={error} />}
121 </Wrapper>
122 );
123 }
124}
125
126export const Textarea = injectSheet(styles)(TextareaComponent);
diff --git a/src/components/ui/textarea/styles.ts b/src/components/ui/textarea/styles.ts
new file mode 100644
index 000000000..f2267e000
--- /dev/null
+++ b/src/components/ui/textarea/styles.ts
@@ -0,0 +1,54 @@
1import { Property } from 'csstype';
2
3import { Theme } from '@meetfranz/theme';
4
5export default (theme: Theme) => ({
6 label: {
7 '& > div': {
8 marginTop: 5,
9 },
10 },
11 disabled: {
12 opacity: theme.inputDisabledOpacity,
13 },
14 formModifier: {
15 background: 'none',
16 border: 0,
17 borderLeft: theme.inputBorder,
18 padding: '4px 20px 0',
19 outline: 'none',
20
21 '&:active': {
22 opacity: 0.5,
23 },
24
25 '& svg': {
26 fill: theme.inputModifierColor,
27 },
28 },
29 textarea: {
30 background: 'none',
31 border: 0,
32 fontSize: theme.uiFontSize,
33 outline: 'none',
34 padding: 8,
35 width: '100%',
36 color: theme.inputColor,
37
38 '&::placeholder': {
39 color: theme.inputPlaceholderColor,
40 },
41 },
42 wrapper: {
43 background: theme.inputBackground,
44 border: theme.inputBorder,
45 borderRadius: theme.borderRadiusSmall,
46 boxSizing: 'border-box' as Property.BoxSizing,
47 display: 'flex',
48 order: 1,
49 width: '100%',
50 },
51 hasError: {
52 borderColor: theme.brandDanger,
53 },
54});
diff --git a/src/components/ui/toggle/index.tsx b/src/components/ui/toggle/index.tsx
new file mode 100644
index 000000000..67b6c3835
--- /dev/null
+++ b/src/components/ui/toggle/index.tsx
@@ -0,0 +1,125 @@
1import classnames from 'classnames';
2import { Property } from 'csstype';
3import { Component, InputHTMLAttributes } from 'react';
4import injectStyle from 'react-jss';
5import { Theme } from '@meetfranz/theme';
6
7import { IFormField, IWithStyle } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12
13interface IProps
14 extends InputHTMLAttributes<HTMLInputElement>,
15 IFormField,
16 IWithStyle {
17 className?: string;
18}
19
20let buttonTransition: string = 'none';
21
22if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
23 buttonTransition = 'all .5s';
24}
25
26const styles = (theme: Theme) => ({
27 toggle: {
28 background: theme.toggleBackground,
29 borderRadius: theme.borderRadius,
30 height: theme.toggleHeight,
31 position: 'relative' as Property.Position,
32 width: theme.toggleWidth,
33 },
34 button: {
35 background: theme.toggleButton,
36 borderRadius: '100%',
37 boxShadow: '0 1px 4px rgba(0, 0, 0, .3)',
38 width: theme.toggleHeight - 2,
39 height: theme.toggleHeight - 2,
40 left: 1,
41 top: 1,
42 position: 'absolute' as Property.Position,
43 transition: buttonTransition,
44 },
45 buttonActive: {
46 background: theme.toggleButtonActive,
47 left: theme.toggleWidth - theme.toggleHeight + 1,
48 },
49 input: {
50 visibility: 'hidden' as any,
51 },
52 disabled: {
53 opacity: theme.inputDisabledOpacity,
54 },
55 toggleLabel: {
56 display: 'flex',
57 alignItems: 'center',
58
59 '& > span': {
60 order: 1,
61 marginLeft: 15,
62 },
63 },
64});
65
66class ToggleComponent extends Component<IProps> {
67 public static defaultProps = {
68 onChange: () => {},
69 showLabel: true,
70 disabled: false,
71 error: '',
72 };
73
74 render() {
75 const {
76 classes,
77 className,
78 disabled,
79 error,
80 id,
81 label,
82 showLabel,
83 checked,
84 value,
85 onChange,
86 } = this.props;
87
88 return (
89 <Wrapper className={className} identifier="franz-toggle">
90 <Label
91 title={label}
92 showLabel={showLabel}
93 htmlFor={id}
94 className={classes.toggleLabel}
95 >
96 <div
97 className={classnames({
98 [`${classes.toggle}`]: true,
99 [`${classes.disabled}`]: disabled,
100 })}
101 >
102 <div
103 className={classnames({
104 [`${classes.button}`]: true,
105 [`${classes.buttonActive}`]: checked,
106 })}
107 />
108 <input
109 className={classes.input}
110 id={id}
111 type="checkbox"
112 checked={checked}
113 value={value}
114 onChange={onChange}
115 disabled={disabled}
116 />
117 </div>
118 </Label>
119 {error && <Error message={error} />}
120 </Wrapper>
121 );
122 }
123}
124
125export const Toggle = injectStyle(styles)(ToggleComponent);
diff --git a/src/components/ui/typings/generic.ts b/src/components/ui/typings/generic.ts
index ddce3f7c7..084e0e0a5 100644
--- a/src/components/ui/typings/generic.ts
+++ b/src/components/ui/typings/generic.ts
@@ -2,6 +2,14 @@ import { Classes } from 'jss';
2 2
3import { Theme } from '@meetfranz/theme'; 3import { Theme } from '@meetfranz/theme';
4 4
5export interface IFormField {
6 showLabel?: boolean;
7 label?: string;
8 error?: string;
9 required?: boolean;
10 noMargin?: boolean;
11}
12
5export interface IWithStyle { 13export interface IWithStyle {
6 classes: Classes; 14 classes: Classes;
7 theme: Theme; 15 theme: Theme;
diff --git a/src/components/ui/wrapper/index.tsx b/src/components/ui/wrapper/index.tsx
new file mode 100644
index 000000000..ffcd6fe0b
--- /dev/null
+++ b/src/components/ui/wrapper/index.tsx
@@ -0,0 +1,37 @@
1import classnames from 'classnames';
2import { Component, ReactNode } from 'react';
3import injectStyle from 'react-jss';
4import { IWithStyle } from '../typings/generic';
5
6interface IProps extends IWithStyle {
7 children: ReactNode;
8 className?: string;
9 identifier: string;
10 noMargin?: boolean;
11}
12
13const styles = {
14 container: {
15 marginBottom: (props: IProps) => (props.noMargin ? 0 : 20),
16 },
17};
18
19class WrapperComponent extends Component<IProps> {
20 render() {
21 const { children, classes, className, identifier } = this.props;
22
23 return (
24 <div
25 className={classnames({
26 [`${classes.container}`]: true,
27 [`${className}`]: className,
28 })}
29 data-type={identifier}
30 >
31 {children}
32 </div>
33 );
34 }
35}
36
37export const Wrapper = injectStyle(styles)(WrapperComponent);
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
index ced047a93..d5cb9179f 100644
--- a/src/features/quickSwitch/Component.js
+++ b/src/features/quickSwitch/Component.js
@@ -5,9 +5,9 @@ import { observer, inject } from 'mobx-react';
5import { reaction } from 'mobx'; 5import { reaction } from 'mobx';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7import { defineMessages, injectIntl } from 'react-intl'; 7import { defineMessages, injectIntl } from 'react-intl';
8import { Input } from '@meetfranz/forms';
9import { compact, invoke } from 'lodash'; 8import { compact, invoke } from 'lodash';
10 9
10import { Input } from '../../components/ui/input/index';
11import { H1 } from '../../components/ui/headline'; 11import { H1 } from '../../components/ui/headline';
12import Modal from '../../components/ui/Modal'; 12import Modal from '../../components/ui/Modal';
13import { state as ModalState } from './store'; 13import { state as ModalState } from './store';
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js
index c93f8f6f7..75f6d9f4a 100644
--- a/src/features/workspaces/components/CreateWorkspaceForm.js
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -2,8 +2,10 @@ import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Input, Button } from '@meetfranz/forms';
6import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6
7import { Input } from '../../../components/ui/input/index';
8import { Button } from '../../../components/ui/button/index';
7import Form from '../../../lib/Form'; 9import Form from '../../../lib/Form';
8import { required } from '../../../helpers/validation-helpers'; 10import { required } from '../../../helpers/validation-helpers';
9import { workspaceStore } from '../index'; 11import { workspaceStore } from '../index';
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
index 1bd06c4b2..fa3ea4289 100644
--- a/src/features/workspaces/components/EditWorkspaceForm.js
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -1,11 +1,12 @@
1import { Component, Fragment } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6import { Input, Button } from '@meetfranz/forms';
7import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
8 7
8import { Input } from '../../../components/ui/input/index';
9import { Button } from '../../../components/ui/button/index';
9import Workspace from '../models/Workspace'; 10import Workspace from '../models/Workspace';
10import Service from '../../../models/Service'; 11import Service from '../../../models/Service';
11import Form from '../../../lib/Form'; 12import Form from '../../../lib/Form';
diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.tsx b/src/features/workspaces/components/WorkspaceServiceListItem.tsx
index b6faaf4ce..6e012eb1e 100644
--- a/src/features/workspaces/components/WorkspaceServiceListItem.tsx
+++ b/src/features/workspaces/components/WorkspaceServiceListItem.tsx
@@ -2,8 +2,8 @@ import { Component } from 'react';
2import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
3import injectSheet from 'react-jss'; 3import injectSheet from 'react-jss';
4import classnames from 'classnames'; 4import classnames from 'classnames';
5import { Toggle } from '@meetfranz/forms';
6 5
6import { Toggle } from '../../../components/ui/toggle/index';
7import ServiceIcon from '../../../components/ui/ServiceIcon'; 7import ServiceIcon from '../../../components/ui/ServiceIcon';
8 8
9const styles = theme => ({ 9const styles = theme => ({