From 011e73f24f8ae15091d41781c93c313d0167d887 Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Mon, 31 Oct 2022 05:20:17 +0530 Subject: Convert LoginScreen component tree to typescript (#721) --- src/@types/mobx-form.types.ts | 20 ++++ src/@types/mobx-react-form.d.ts | 1 + src/components/auth/Login.jsx | 205 -------------------------------- src/components/auth/Login.tsx | 210 +++++++++++++++++++++++++++++++++ src/components/ui/ColorPickerInput.tsx | 34 +++--- src/components/ui/Input.js | 156 ------------------------ src/components/ui/Input.tsx | 157 ++++++++++++++++++++++++ src/components/ui/input/index.tsx | 16 ++- src/containers/auth/LoginScreen.tsx | 20 ++-- src/features/basicAuth/Form.ts | 1 - src/lib/Form.ts | 5 + src/routes.tsx | 4 +- 12 files changed, 432 insertions(+), 397 deletions(-) create mode 100644 src/@types/mobx-react-form.d.ts delete mode 100644 src/components/auth/Login.jsx create mode 100644 src/components/auth/Login.tsx delete mode 100644 src/components/ui/Input.js create mode 100644 src/components/ui/Input.tsx (limited to 'src') diff --git a/src/@types/mobx-form.types.ts b/src/@types/mobx-form.types.ts index 6bc20f5e1..7caddc9e4 100644 --- a/src/@types/mobx-form.types.ts +++ b/src/@types/mobx-form.types.ts @@ -1,3 +1,6 @@ +import { ChangeEventHandler, FocusEventHandler } from 'react'; +import { GlobalError } from './ferdium-components.types'; + export interface FormFieldOptions { value?: string; label?: string; @@ -18,3 +21,20 @@ export interface FormFields { }; }; } + +export interface Field extends Partial { + id?: string; + type?: string; + name?: string; + value: string; + label?: string; + placeholder?: string; + disabled?: boolean; + error?: GlobalError | string; +} + +export interface Listeners { + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; + onFocus?: FocusEventHandler; +} diff --git a/src/@types/mobx-react-form.d.ts b/src/@types/mobx-react-form.d.ts new file mode 100644 index 000000000..4e19dc1c2 --- /dev/null +++ b/src/@types/mobx-react-form.d.ts @@ -0,0 +1 @@ +declare module 'mobx-react-form'; diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx deleted file mode 100644 index 33b4d3e0d..000000000 --- a/src/components/auth/Login.jsx +++ /dev/null @@ -1,205 +0,0 @@ -/* eslint jsx-a11y/anchor-is-valid: 0 */ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer, inject } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; - -import { mdiArrowLeftCircle } from '@mdi/js'; -import Icon from '../ui/icon'; -import { LIVE_FRANZ_API } from '../../config'; -import { API_VERSION } from '../../environment-remote'; -import { serverBase } from '../../api/apiBase'; // TODO: Remove this line after fixing password recovery in-app -import Form from '../../lib/Form'; -import { required, email } from '../../helpers/validation-helpers'; -import Input from '../ui/Input'; -import Button from '../ui/button'; -import Link from '../ui/Link'; - -import { globalError as globalErrorPropType } from '../../prop-types'; -import { H1 } from '../ui/headline'; - -const messages = defineMessages({ - headline: { - id: 'login.headline', - defaultMessage: 'Sign in', - }, - emailLabel: { - id: 'login.email.label', - defaultMessage: 'Email address', - }, - passwordLabel: { - id: 'login.password.label', - defaultMessage: 'Password', - }, - submitButtonLabel: { - id: 'login.submit.label', - defaultMessage: 'Sign in', - }, - invalidCredentials: { - id: 'login.invalidCredentials', - defaultMessage: 'Email or password not valid', - }, - customServerQuestion: { - id: 'login.customServerQuestion', - defaultMessage: 'Using a custom Ferdium server?', - }, - customServerSuggestion: { - id: 'login.customServerSuggestion', - defaultMessage: 'Try importing your Franz account', - }, - tokenExpired: { - id: 'login.tokenExpired', - defaultMessage: 'Your session expired, please login again.', - }, - serverLogout: { - id: 'login.serverLogout', - defaultMessage: 'Your session expired, please login again.', - }, - signupLink: { - id: 'login.link.signup', - defaultMessage: 'Create a free account', - }, - passwordLink: { - id: 'login.link.password', - defaultMessage: 'Reset password', - }, -}); - -class Login extends Component { - static propTypes = { - onSubmit: PropTypes.func.isRequired, - isSubmitting: PropTypes.bool.isRequired, - isTokenExpired: PropTypes.bool.isRequired, - isServerLogout: PropTypes.bool.isRequired, - signupRoute: PropTypes.string.isRequired, - // passwordRoute: PropTypes.string.isRequired, // TODO: Uncomment this line after fixing password recovery in-app - error: globalErrorPropType.isRequired, - }; - - form = (() => { - const { intl } = this.props; - return new Form( - { - fields: { - email: { - label: intl.formatMessage(messages.emailLabel), - value: '', - validators: [required, email], - }, - password: { - label: intl.formatMessage(messages.passwordLabel), - value: '', - validators: [required], - type: 'password', - }, - }, - }, - intl, - ); - })(); - - submit(e) { - e.preventDefault(); - this.form.submit({ - onSuccess: form => { - this.props.onSubmit(form.values()); - }, - onError: () => {}, - }); - } - - render() { - const { form } = this; - const { intl } = this.props; - const { - isSubmitting, - isTokenExpired, - isServerLogout, - signupRoute, - // passwordRoute, // TODO: Uncomment this line after fixing password recovery in-app - error, - } = this.props; - - return ( -
-
this.submit(e)}> - - - -

{intl.formatMessage(messages.headline)}

- {isTokenExpired && ( -

- {intl.formatMessage(messages.tokenExpired)} -

- )} - {isServerLogout && ( -

- {intl.formatMessage(messages.serverLogout)} -

- )} - - - {error.code === 'invalid-credentials' && ( - <> -

- {intl.formatMessage(messages.invalidCredentials)} -

- {window['ferdium'].stores.settings.all.app.server !== - LIVE_FRANZ_API && ( -

- {intl.formatMessage(messages.customServerQuestion)}{' '} - - {intl.formatMessage(messages.customServerSuggestion)} - -

- )} - - )} - {isSubmitting ? ( -
- ); - } -} - -export default injectIntl(inject('actions')(observer(Login))); diff --git a/src/components/auth/Login.tsx b/src/components/auth/Login.tsx new file mode 100644 index 000000000..65381fe04 --- /dev/null +++ b/src/components/auth/Login.tsx @@ -0,0 +1,210 @@ +import { Component, FormEvent, ReactElement } from 'react'; +import { observer, inject } from 'mobx-react'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { mdiArrowLeftCircle } from '@mdi/js'; +import { noop } from 'lodash'; +import Icon from '../ui/icon'; +import { LIVE_FRANZ_API } from '../../config'; +import { API_VERSION } from '../../environment-remote'; +import { serverBase } from '../../api/apiBase'; // TODO: Remove this line after fixing password recovery in-app +import Form from '../../lib/Form'; +import { required, email } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Button from '../ui/button'; +import Link from '../ui/Link'; +import { H1 } from '../ui/headline'; +import { + GlobalError, + StoresProps, +} from '../../@types/ferdium-components.types'; + +const messages = defineMessages({ + headline: { + id: 'login.headline', + defaultMessage: 'Sign in', + }, + emailLabel: { + id: 'login.email.label', + defaultMessage: 'Email address', + }, + passwordLabel: { + id: 'login.password.label', + defaultMessage: 'Password', + }, + submitButtonLabel: { + id: 'login.submit.label', + defaultMessage: 'Sign in', + }, + invalidCredentials: { + id: 'login.invalidCredentials', + defaultMessage: 'Email or password not valid', + }, + customServerQuestion: { + id: 'login.customServerQuestion', + defaultMessage: 'Using a custom Ferdium server?', + }, + customServerSuggestion: { + id: 'login.customServerSuggestion', + defaultMessage: 'Try importing your Franz account', + }, + tokenExpired: { + id: 'login.tokenExpired', + defaultMessage: 'Your session expired, please login again.', + }, + serverLogout: { + id: 'login.serverLogout', + defaultMessage: 'Your session expired, please login again.', + }, + signupLink: { + id: 'login.link.signup', + defaultMessage: 'Create a free account', + }, + passwordLink: { + id: 'login.link.password', + defaultMessage: 'Reset password', + }, +}); + +interface IProps extends Partial, WrappedComponentProps { + onSubmit: (...args: any[]) => void; + isSubmitting: boolean; + isTokenExpired: boolean; + isServerLogout: boolean; + signupRoute: string; + // eslint-disable-next-line react/no-unused-prop-types + passwordRoute: string; // TODO: Uncomment this line after fixing password recovery in-app + error: GlobalError; +} + +@inject('actions') +@observer +class Login extends Component { + form: Form; + + constructor(props: IProps) { + super(props); + + this.form = new Form({ + fields: { + email: { + label: this.props.intl.formatMessage(messages.emailLabel), + value: '', + validators: [required, email], + }, + password: { + label: this.props.intl.formatMessage(messages.passwordLabel), + value: '', + validators: [required], + type: 'password', + }, + }, + }); + } + + submit(e: FormEvent): void { + e.preventDefault(); + this.form.submit({ + onSuccess: form => { + this.props.onSubmit(form.values()); + }, + onError: () => {}, + }); + } + + render(): ReactElement { + const { form } = this; + const { + isSubmitting, + isTokenExpired, + isServerLogout, + signupRoute, + error, + intl, + // passwordRoute, // TODO: Uncomment this line after fixing password recovery in-app + } = this.props; + + return ( +
+
this.submit(e)}> + + + +

{intl.formatMessage(messages.headline)}

+ {isTokenExpired && ( +

+ {intl.formatMessage(messages.tokenExpired)} +

+ )} + {isServerLogout && ( +

+ {intl.formatMessage(messages.serverLogout)} +

+ )} + + + {error.code === 'invalid-credentials' && ( + <> +

+ {intl.formatMessage(messages.invalidCredentials)} +

+ {window['ferdium'].stores.settings.all.app.server !== + LIVE_FRANZ_API && ( +

+ {intl.formatMessage(messages.customServerQuestion)}{' '} + + {intl.formatMessage(messages.customServerSuggestion)} + +

+ )} + + )} + {isSubmitting ? ( +
+ ); + } +} + +export default injectIntl(Login); diff --git a/src/components/ui/ColorPickerInput.tsx b/src/components/ui/ColorPickerInput.tsx index 710d05586..7e3965331 100644 --- a/src/components/ui/ColorPickerInput.tsx +++ b/src/components/ui/ColorPickerInput.tsx @@ -1,8 +1,8 @@ -import { ChangeEvent, Component } from 'react'; +import { ChangeEvent, Component, createRef, RefObject } from 'react'; import { observer } from 'mobx-react'; -import { Field } from 'mobx-react-form'; import classnames from 'classnames'; import { SliderPicker } from 'react-color'; +import { Field } from '../../@types/mobx-form.types'; interface IProps { field: Field; @@ -11,27 +11,27 @@ interface IProps { } class ColorPickerInput extends Component { - static defaultProps = { - className: null, - focus: false, - }; - - inputElement: HTMLInputElement | null | undefined; + private inputElement: RefObject = + createRef(); componentDidMount() { - if (this.props.focus) { + const { focus = false } = this.props; + if (focus) { this.focus(); } } onChange(e: ChangeEvent) { const { field } = this.props; - - field.onChange(e); + if (field.onChange) { + field.onChange(e); + } } focus() { - this.inputElement?.focus(); + if (this.inputElement && this.inputElement.current) { + this.inputElement.current.focus(); + } } handleChangeComplete = (color: { hex: string }) => { @@ -40,7 +40,7 @@ class ColorPickerInput extends Component { }; render() { - const { field, className } = this.props; + const { field, className = null } = this.props; let { type } = field; type = 'text'; @@ -64,9 +64,7 @@ class ColorPickerInput extends Component { placeholder={field.placeholder} onBlur={field.onBlur} onFocus={field.onFocus} - ref={(element: HTMLInputElement | null | undefined) => { - this.inputElement = element; - }} + ref={this.inputElement} disabled={field.disabled} />
@@ -80,9 +78,7 @@ class ColorPickerInput extends Component { onChange={e => this.onChange(e)} onBlur={field.onBlur} onFocus={field.onFocus} - ref={element => { - this.inputElement = element; - }} + ref={this.inputElement} disabled={field.disabled} />
diff --git a/src/components/ui/Input.js b/src/components/ui/Input.js deleted file mode 100644 index ae14493ca..000000000 --- a/src/components/ui/Input.js +++ /dev/null @@ -1,156 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { Field } from 'mobx-react-form'; -import classnames from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; - -import { mdiEye, mdiEyeOff } from '@mdi/js'; -import { scorePassword as scorePasswordFunc } from '../../helpers/password-helpers'; -import Icon from './icon'; - -const messages = defineMessages({ - passwordToggle: { - id: 'settings.app.form.passwordToggle', - defaultMessage: 'Password toggle', - }, -}); - -// Can this file be merged into the './input/index.tsx' file? -class Input extends Component { - static propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - className: PropTypes.string, - focus: PropTypes.bool, - showPasswordToggle: PropTypes.bool, - showLabel: PropTypes.bool, - scorePassword: PropTypes.bool, - prefix: PropTypes.string, - suffix: PropTypes.string, - }; - - static defaultProps = { - className: null, - focus: false, - showPasswordToggle: false, - showLabel: true, - scorePassword: false, - prefix: '', - suffix: '', - }; - - state = { - showPassword: false, - passwordScore: 0, - }; - - inputElement; - - componentDidMount() { - if (this.props.focus) { - this.focus(); - } - } - - onChange(e) { - const { field, scorePassword } = this.props; - - field.onChange(e); - - if (scorePassword) { - this.setState({ passwordScore: scorePasswordFunc(field.value) }); - } - } - - focus() { - this.inputElement.focus(); - } - - render() { - const { - field, - className, - showPasswordToggle, - showLabel, - scorePassword, - prefix, - suffix, - } = this.props; - - const { passwordScore } = this.state; - - const { intl } = this.props; - - let { type } = field; - if (type === 'password' && this.state.showPassword) { - type = 'text'; - } - - return ( -
-
- {prefix && {prefix}} - this.onChange(e)} - onBlur={field.onBlur} - onFocus={field.onFocus} - ref={element => { - this.inputElement = element; - }} - disabled={field.disabled} - /> - {suffix && {suffix}} - {showPasswordToggle && ( - - )} - {scorePassword && ( -
- {/* */} - -
- )} -
- {field.label && showLabel && ( - - )} - {field.error &&
{field.error}
} -
- ); - } -} - -export default injectIntl(observer(Input)); diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 000000000..78b3a9200 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,157 @@ +import { ChangeEvent, Component, createRef, RefObject } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { mdiEye, mdiEyeOff } from '@mdi/js'; +import { scorePassword as scorePasswordFunc } from '../../helpers/password-helpers'; +import Icon from './icon'; +import { Field } from '../../@types/mobx-form.types'; + +const messages = defineMessages({ + passwordToggle: { + id: 'settings.app.form.passwordToggle', + defaultMessage: 'Password toggle', + }, +}); + +interface IProps extends WrappedComponentProps { + field: Field; + className?: string; + focus?: boolean; + showPasswordToggle?: boolean; + showLabel?: boolean; + scorePassword?: boolean; + prefix?: string; + suffix?: string; +} + +interface IState { + showPassword: boolean; + passwordScore: number; +} + +// Can this file be merged into the './input/index.tsx' file? +@observer +class Input extends Component { + private inputElement: RefObject = + createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + showPassword: false, + passwordScore: 0, + }; + } + + componentDidMount(): void { + const { focus = false } = this.props; + if (focus) { + this.focus(); + } + } + + onChange(e: ChangeEvent): void { + const { field, scorePassword } = this.props; + + if (field.onChange) { + field.onChange(e); + } + + if (scorePassword) { + this.setState({ passwordScore: scorePasswordFunc(field.value) }); + } + } + + focus() { + if (this.inputElement && this.inputElement.current) { + this.inputElement.current!.focus(); + } + } + + render() { + const { + field, + className = null, + showPasswordToggle = false, + showLabel = true, + scorePassword = false, + prefix = '', + suffix = '', + intl, + } = this.props; + + const { passwordScore } = this.state; + + let { type } = field; + if (type === 'password' && this.state.showPassword) { + type = 'text'; + } + + return ( +
+
+ {prefix && {prefix}} + this.onChange(e)} + onBlur={field.onBlur} + onFocus={field.onFocus} + ref={this.inputElement} + disabled={field.disabled} + /> + {suffix && {suffix}} + {showPasswordToggle && ( + + )} + {scorePassword && ( +
+ {/* */} + +
+ )} +
+ {field.label && showLabel && ( + + )} + {field.error &&
{field.error}
} +
+ ); + } +} + +export default injectIntl(Input); diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx index 3bafc93e7..2a36d7aa9 100644 --- a/src/components/ui/input/index.tsx +++ b/src/components/ui/input/index.tsx @@ -1,7 +1,13 @@ import { mdiEye, mdiEyeOff } from '@mdi/js'; import Icon from '@mdi/react'; import classnames from 'classnames'; -import { Component, createRef, InputHTMLAttributes } from 'react'; +import { + Component, + createRef, + InputHTMLAttributes, + ReactElement, + RefObject, +} from 'react'; import injectSheet, { WithStylesProps } from 'react-jss'; import { noop } from 'lodash'; import { IFormField } from '../typings/generic'; @@ -26,7 +32,7 @@ interface IProps showPasswordToggle?: boolean; data?: IData; inputClassName?: string; - onEnterKey?: Function; + onEnterKey?: () => {}; } interface IState { @@ -35,7 +41,7 @@ interface IState { } class InputComponent extends Component { - private inputRef = createRef(); + private inputRef: RefObject = createRef(); constructor(props: IProps) { super(props); @@ -73,14 +79,14 @@ class InputComponent extends Component { } } - onInputKeyPress(e: React.KeyboardEvent) { + onInputKeyPress(e: React.KeyboardEvent): void { if (e.key === 'Enter') { const { onEnterKey } = this.props; onEnterKey && onEnterKey(); } } - render() { + render(): ReactElement { const { classes, className, diff --git a/src/containers/auth/LoginScreen.tsx b/src/containers/auth/LoginScreen.tsx index 64e06e59d..c4782d287 100644 --- a/src/containers/auth/LoginScreen.tsx +++ b/src/containers/auth/LoginScreen.tsx @@ -6,27 +6,29 @@ import { } from '../../@types/ferdium-components.types'; import Login from '../../components/auth/Login'; -interface LoginScreenProps extends StoresProps { +interface IProps extends Partial { error: GlobalError; } -class LoginScreen extends Component { +@inject('stores', 'actions') +@observer +class LoginScreen extends Component { render(): ReactElement { const { actions, stores, error } = this.props; return ( ); } } -export default inject('stores', 'actions')(observer(LoginScreen)); +export default LoginScreen; diff --git a/src/features/basicAuth/Form.ts b/src/features/basicAuth/Form.ts index e84156d96..95721d0e9 100644 --- a/src/features/basicAuth/Form.ts +++ b/src/features/basicAuth/Form.ts @@ -1,6 +1,5 @@ import Form from '../../lib/Form'; -// @ts-expect-error Expected 0 arguments, but got 1 export default new Form({ fields: { user: { diff --git a/src/lib/Form.ts b/src/lib/Form.ts index 14ea82948..ca96406e7 100644 --- a/src/lib/Form.ts +++ b/src/lib/Form.ts @@ -1,7 +1,12 @@ import Form from 'mobx-react-form'; import vjf from 'mobx-react-form/lib/validators/VJF'; +import { FormFields } from '../@types/mobx-form.types'; export default class DefaultForm extends Form { + constructor(fields: FormFields) { + super(fields); + } + bindings() { return { default: { diff --git a/src/routes.tsx b/src/routes.tsx index 8150d135e..e757de72b 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -27,7 +27,7 @@ import PasswordScreen from './containers/auth/PasswordScreen'; import ChangeServerScreen from './containers/auth/ChangeServerScreen'; import SignupScreen from './containers/auth/SignupScreen'; import ImportScreen from './containers/auth/ImportScreen'; -import SetupAssistentScreen from './containers/auth/SetupAssistantScreen'; +import SetupAssistantScreen from './containers/auth/SetupAssistantScreen'; import InviteScreen from './containers/auth/InviteScreen'; import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; import WorkspacesScreen from './features/workspaces/containers/WorkspacesScreen'; @@ -82,7 +82,7 @@ class FerdiumRoutes extends Component { /> } + element={} />