From 85d1aac4cd70e79d5ab64684dea09e92b17ed2c2 Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Tue, 1 Nov 2022 06:42:12 +0530 Subject: Transform ChangeServer components tree to typescript (#725) --- src/@types/mobx-form.types.ts | 11 +- src/components/auth/ChangeServer.jsx | 153 --------------------- src/components/auth/ChangeServer.tsx | 150 ++++++++++++++++++++ src/components/auth/Login.tsx | 10 +- .../settings/account/AccountDashboard.tsx | 2 +- .../settings/recipes/RecipesDashboard.tsx | 4 +- src/components/ui/Infobox.js | 115 ---------------- src/components/ui/Infobox.tsx | 111 +++++++++++++++ src/components/ui/Input.tsx | 21 ++- src/components/ui/Select.js | 101 -------------- src/components/ui/Select.tsx | 112 +++++++++++++++ src/components/ui/infobox/index.tsx | 88 ++++++------ src/components/ui/select/index.tsx | 151 ++++++++++---------- src/containers/auth/ChangeServerScreen.tsx | 24 ++-- src/containers/auth/LoginScreen.tsx | 14 +- src/routes.tsx | 1 - 16 files changed, 544 insertions(+), 524 deletions(-) delete mode 100644 src/components/auth/ChangeServer.jsx create mode 100644 src/components/auth/ChangeServer.tsx delete mode 100644 src/components/ui/Infobox.js create mode 100644 src/components/ui/Infobox.tsx delete mode 100644 src/components/ui/Select.js create mode 100644 src/components/ui/Select.tsx (limited to 'src') diff --git a/src/@types/mobx-form.types.ts b/src/@types/mobx-form.types.ts index 7caddc9e4..c9932c053 100644 --- a/src/@types/mobx-form.types.ts +++ b/src/@types/mobx-form.types.ts @@ -26,15 +26,22 @@ export interface Field extends Partial { id?: string; type?: string; name?: string; - value: string; + value: string | string[]; label?: string; placeholder?: string; disabled?: boolean; error?: GlobalError | string; + options?: SelectOptions[]; +} + +export interface SelectOptions { + disabled?: boolean; + label?: string; + value?: string; } export interface Listeners { - onChange?: ChangeEventHandler; + onChange?: ChangeEventHandler; onBlur?: FocusEventHandler; onFocus?: FocusEventHandler; } diff --git a/src/components/auth/ChangeServer.jsx b/src/components/auth/ChangeServer.jsx deleted file mode 100644 index 8f4b85fbb..000000000 --- a/src/components/auth/ChangeServer.jsx +++ /dev/null @@ -1,153 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import { mdiArrowLeftCircle } from '@mdi/js'; -import Form from '../../lib/Form'; -import Input from '../ui/Input'; -import Select from '../ui/Select'; -import Button from '../ui/button'; -import Link from '../ui/Link'; -import Infobox from '../ui/Infobox'; -import { url, required } from '../../helpers/validation-helpers'; -import { LIVE_FERDIUM_API, LIVE_FRANZ_API } from '../../config'; -import globalMessages from '../../i18n/globalMessages'; -import { H1 } from '../ui/headline'; -import Icon from '../ui/icon'; - -const messages = defineMessages({ - headline: { - id: 'changeserver.headline', - defaultMessage: 'Change server', - }, - label: { - id: 'changeserver.label', - defaultMessage: 'Server', - }, - warning: { - id: 'changeserver.warning', - defaultMessage: 'Extra settings offered by Ferdium will not be saved', - }, - customServerLabel: { - id: 'changeserver.customServerLabel', - defaultMessage: 'Custom server', - }, - urlError: { - id: 'changeserver.urlError', - defaultMessage: 'Enter a valid URL', - }, -}); - -class ChangeServer extends Component { - static propTypes = { - onSubmit: PropTypes.func.isRequired, - server: PropTypes.string.isRequired, - }; - - ferdiumServer = LIVE_FERDIUM_API; - - franzServer = LIVE_FRANZ_API; - - defaultServers = [this.ferdiumServer, this.franzServer]; - - form = (() => { - const { intl } = this.props; - return new Form( - { - fields: { - server: { - label: intl.formatMessage(messages.label), - value: this.props.server, - options: [ - { value: this.ferdiumServer, label: 'Ferdium (Default)' }, - { value: this.franzServer, label: 'Franz' }, - { - value: this.defaultServers.includes(this.props.server) - ? '' - : this.props.server, - label: 'Custom', - }, - ], - }, - customServer: { - label: intl.formatMessage(messages.customServerLabel), - value: '', - validators: [url, required], - }, - }, - }, - intl, - ); - })(); - - componentDidMount() { - if (this.defaultServers.includes(this.props.server)) { - this.form.$('server').value = this.props.server; - } else { - this.form.$('server').value = ''; - this.form.$('customServer').value = this.props.server; - } - } - - submit(e) { - e.preventDefault(); - this.form.submit({ - onSuccess: form => { - if (!this.defaultServers.includes(form.values().server)) { - form.$('server').onChange(form.values().customServer); - } - this.props.onSubmit(form.values()); - }, - onError: form => { - if (this.defaultServers.includes(form.values().server)) { - this.props.onSubmit(form.values()); - } - }, - }); - } - - render() { - const { form } = this; - const { intl } = this.props; - return ( -
-
this.submit(e)}> - - - -

{intl.formatMessage(messages.headline)}

- {form.$('server').value === this.franzServer && ( - - {intl.formatMessage(messages.warning)} - - )} - { - this.form.$('customServer').value = this.form - .$('customServer') - .value.replace(/\/$/, ''); - this.submit(e); - }} - field={form.$('customServer')} - /> - )} -
- ); - } -} - -export default injectIntl(observer(ChangeServer)); diff --git a/src/components/auth/ChangeServer.tsx b/src/components/auth/ChangeServer.tsx new file mode 100644 index 000000000..d8509f599 --- /dev/null +++ b/src/components/auth/ChangeServer.tsx @@ -0,0 +1,150 @@ +import { Component, FormEvent, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { mdiArrowLeftCircle } from '@mdi/js'; +import { noop } from 'lodash'; +import Form from '../../lib/Form'; +import Input from '../ui/Input'; +import Select from '../ui/Select'; +import Button from '../ui/button'; +import Link from '../ui/Link'; +import Infobox from '../ui/Infobox'; +import { url, required } from '../../helpers/validation-helpers'; +import { LIVE_FERDIUM_API, LIVE_FRANZ_API } from '../../config'; +import globalMessages from '../../i18n/globalMessages'; +import { H1 } from '../ui/headline'; +import Icon from '../ui/icon'; + +const messages = defineMessages({ + headline: { + id: 'changeserver.headline', + defaultMessage: 'Change server', + }, + label: { + id: 'changeserver.label', + defaultMessage: 'Server', + }, + warning: { + id: 'changeserver.warning', + defaultMessage: 'Extra settings offered by Ferdium will not be saved', + }, + customServerLabel: { + id: 'changeserver.customServerLabel', + defaultMessage: 'Custom server', + }, + urlError: { + id: 'changeserver.urlError', + defaultMessage: 'Enter a valid URL', + }, +}); + +interface IProps extends WrappedComponentProps { + onSubmit: (...args: any[]) => void; + server: string; +} + +@observer +class ChangeServer extends Component { + ferdiumServer: string = LIVE_FERDIUM_API; + + franzServer: string = LIVE_FRANZ_API; + + defaultServers: string[] = [LIVE_FERDIUM_API, LIVE_FRANZ_API]; + + form: Form; + + constructor(props: IProps) { + super(props); + + this.form = new Form({ + fields: { + server: { + label: this.props.intl.formatMessage(messages.label), + value: this.props.server, + options: [ + { value: this.ferdiumServer, label: 'Ferdium (Default)' }, + { value: this.franzServer, label: 'Franz' }, + { + value: this.defaultServers.includes(this.props.server) + ? '' + : this.props.server, + label: 'Custom', + }, + ], + }, + customServer: { + label: this.props.intl.formatMessage(messages.customServerLabel), + placeholder: this.props.intl.formatMessage( + messages.customServerLabel, + ), + value: '', + validators: [url, required], + }, + }, + }); + } + + componentDidMount(): void { + if (this.defaultServers.includes(this.props.server)) { + this.form.$('server').value = this.props.server; + } else { + this.form.$('server').value = ''; + this.form.$('customServer').value = this.props.server; + } + } + + submit(e: FormEvent): void { + e.preventDefault(); + this.form.submit({ + onSuccess: form => { + if (!this.defaultServers.includes(form.values().server)) { + form.$('server').onChange(form.values().customServer); + } + this.props.onSubmit(form.values()); + }, + onError: form => { + if (this.defaultServers.includes(form.values().server)) { + this.props.onSubmit(form.values()); + } + }, + }); + } + + render(): ReactElement { + const { form } = this; + const { intl } = this.props; + + return ( +
+
this.submit(e)}> + + + +

{intl.formatMessage(messages.headline)}

+ {form.$('server').value === this.franzServer && ( + + {intl.formatMessage(messages.warning)} + + )} + + )} +
+ ); + } +} + +export default injectIntl(ChangeServer); diff --git a/src/components/auth/Login.tsx b/src/components/auth/Login.tsx index 65381fe04..66a567fe4 100644 --- a/src/components/auth/Login.tsx +++ b/src/components/auth/Login.tsx @@ -1,5 +1,5 @@ import { Component, FormEvent, ReactElement } from 'react'; -import { observer, inject } from 'mobx-react'; +import { observer } from 'mobx-react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { mdiArrowLeftCircle } from '@mdi/js'; import { noop } from 'lodash'; @@ -13,10 +13,7 @@ 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'; +import { GlobalError } from '../../@types/ferdium-components.types'; const messages = defineMessages({ headline: { @@ -65,7 +62,7 @@ const messages = defineMessages({ }, }); -interface IProps extends Partial, WrappedComponentProps { +interface IProps extends WrappedComponentProps { onSubmit: (...args: any[]) => void; isSubmitting: boolean; isTokenExpired: boolean; @@ -76,7 +73,6 @@ interface IProps extends Partial, WrappedComponentProps { error: GlobalError; } -@inject('actions') @observer class Login extends Component { form: Form; diff --git a/src/components/settings/account/AccountDashboard.tsx b/src/components/settings/account/AccountDashboard.tsx index b0debdaf2..163b0a160 100644 --- a/src/components/settings/account/AccountDashboard.tsx +++ b/src/components/settings/account/AccountDashboard.tsx @@ -6,7 +6,7 @@ import { H1, H2 } from '../../ui/headline'; import Loader from '../../ui/Loader'; import Button from '../../ui/button'; -import Infobox from '../../ui/infobox'; +import Infobox from '../../ui/infobox/index'; import { LOCAL_SERVER, LIVE_FRANZ_API } from '../../../config'; import User from '../../../models/User'; diff --git a/src/components/settings/recipes/RecipesDashboard.tsx b/src/components/settings/recipes/RecipesDashboard.tsx index fc687bc79..7b7ba19b1 100644 --- a/src/components/settings/recipes/RecipesDashboard.tsx +++ b/src/components/settings/recipes/RecipesDashboard.tsx @@ -8,7 +8,7 @@ import Button from '../../ui/button'; import Input from '../../ui/input/index'; import { H1, H2, H3 } from '../../ui/headline'; import SearchInput from '../../ui/SearchInput'; -import Infobox from '../../ui/infobox'; +import Infobox from '../../ui/infobox/index'; import RecipeItem from './RecipeItem'; import Loader from '../../ui/Loader'; import Appear from '../../ui/effects/Appear'; @@ -166,7 +166,7 @@ class RecipesDashboard extends Component { {intl.formatMessage(messages.servicesSuccessfulAddedInfo)} diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js deleted file mode 100644 index 8fb80d87f..000000000 --- a/src/components/ui/Infobox.js +++ /dev/null @@ -1,115 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import Loader from 'react-loader'; -import { defineMessages, injectIntl } from 'react-intl'; -import { mdiAlert, mdiCheckboxMarkedCircleOutline, mdiClose } from '@mdi/js'; -import Icon from '../ui/icon'; - -const icons = { - 'checkbox-marked-circle-outline': mdiCheckboxMarkedCircleOutline, - alert: mdiAlert, -}; - -const messages = defineMessages({ - dismiss: { - id: 'infobox.dismiss', - defaultMessage: 'Dismiss', - }, -}); - -// Can this file be merged into the './infobox/index.tsx' file? -class Infobox extends Component { - static propTypes = { - // eslint-disable-next-line react/forbid-prop-types - children: PropTypes.any.isRequired, - icon: PropTypes.string, - type: PropTypes.string, - ctaOnClick: PropTypes.func, - ctaLabel: PropTypes.string, - ctaLoading: PropTypes.bool, - dismissable: PropTypes.bool, - onDismiss: PropTypes.func, - onSeen: PropTypes.func, - }; - - static defaultProps = { - icon: '', - type: 'primary', - dismissable: false, - ctaOnClick: () => null, - ctaLabel: '', - ctaLoading: false, - onDismiss: () => null, - onSeen: () => null, - }; - - state = { - dismissed: false, - }; - - componentDidMount() { - const { onSeen } = this.props; - if (onSeen) onSeen(); - } - - render() { - const { - children, - icon, - type, - ctaLabel, - ctaLoading, - ctaOnClick, - dismissable, - onDismiss, - } = this.props; - - const { intl } = this.props; - - if (this.state.dismissed) { - return null; - } - - return ( -
- {icon && } -
{children}
- {ctaLabel && ( - - )} - {dismissable && ( - - )} -
- ); - } -} - -export default injectIntl(observer(Infobox)); diff --git a/src/components/ui/Infobox.tsx b/src/components/ui/Infobox.tsx new file mode 100644 index 000000000..1fc24816a --- /dev/null +++ b/src/components/ui/Infobox.tsx @@ -0,0 +1,111 @@ +import { Component, MouseEventHandler, ReactElement, ReactNode } from 'react'; +import classnames from 'classnames'; +import Loader from 'react-loader'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { mdiAlert, mdiCheckboxMarkedCircleOutline, mdiClose } from '@mdi/js'; +import { noop } from 'lodash'; +import { observer } from 'mobx-react'; +import Icon from './icon'; + +const icons = { + 'checkbox-marked-circle-outline': mdiCheckboxMarkedCircleOutline, + alert: mdiAlert, +}; + +const messages = defineMessages({ + dismiss: { + id: 'infobox.dismiss', + defaultMessage: 'Dismiss', + }, +}); + +interface IProps extends WrappedComponentProps { + children: ReactNode; + icon?: string; + type?: string; + ctaLabel?: string; + ctaLoading?: boolean; + dismissible?: boolean; + ctaOnClick?: MouseEventHandler; + onDismiss?: () => void; + onSeen?: () => void; +} + +interface IState { + dismissed: boolean; +} + +// Can this file be merged into the './infobox/index.tsx' file? +@observer +class Infobox extends Component { + constructor(props: IProps) { + super(props); + + this.state = { + dismissed: false, + }; + } + + componentDidMount(): void { + const { onSeen = noop } = this.props; + onSeen(); + } + + render(): ReactElement | null { + const { + children, + icon = '', + type = 'primary', + dismissible = false, + ctaOnClick = noop, + ctaLabel = '', + ctaLoading = false, + onDismiss = noop, + intl, + } = this.props; + + if (this.state.dismissed) { + return null; + } + + return ( +
+ {icon && } +
{children}
+ {ctaLabel && ( + + )} + {dismissible && ( + + )} +
+ ); + } +} + +export default injectIntl(Infobox); diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 78b3a9200..c22dc5838 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -1,8 +1,16 @@ -import { ChangeEvent, Component, createRef, RefObject } from 'react'; +import { + ChangeEvent, + ChangeEventHandler, + Component, + createRef, + ReactElement, + 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 { noop } from 'lodash'; import { scorePassword as scorePasswordFunc } from '../../helpers/password-helpers'; import Icon from './icon'; import { Field } from '../../@types/mobx-form.types'; @@ -23,6 +31,8 @@ interface IProps extends WrappedComponentProps { scorePassword?: boolean; prefix?: string; suffix?: string; + placeholder?: string; + onChange?: ChangeEventHandler; } interface IState { @@ -53,14 +63,17 @@ class Input extends Component { } onChange(e: ChangeEvent): void { - const { field, scorePassword } = this.props; + const { field, scorePassword, onChange = noop } = this.props; if (field.onChange) { + onChange(e); field.onChange(e); } if (scorePassword) { - this.setState({ passwordScore: scorePasswordFunc(field.value) }); + this.setState({ + passwordScore: scorePasswordFunc(field.value as string), + }); } } @@ -70,7 +83,7 @@ class Input extends Component { } } - render() { + render(): ReactElement { const { field, className = null, diff --git a/src/components/ui/Select.js b/src/components/ui/Select.js deleted file mode 100644 index ca5ec9964..000000000 --- a/src/components/ui/Select.js +++ /dev/null @@ -1,101 +0,0 @@ -import { createRef, Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { Field } from 'mobx-react-form'; -import classnames from 'classnames'; - -// Can this file be merged into the './select/index.tsx' file? -class Select extends Component { - static propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - className: PropTypes.string, - showLabel: PropTypes.bool, - disabled: PropTypes.bool, - multiple: PropTypes.bool, - }; - - static defaultProps = { - className: null, - showLabel: true, - disabled: false, - multiple: false, - }; - - constructor(props) { - super(props); - - this.element = createRef(); - } - - multipleChange() { - const element = this.element.current; - - const result = []; - const options = element && element.options; - - for (const option of options) { - if (option.selected) { - result.push(option.value || option.text); - } - } - - const { field } = this.props; - field.value = result; - } - - render() { - const { field, className, showLabel, disabled, multiple } = this.props; - - let selected = field.value; - - if (multiple) { - if (typeof field.value === 'string' && field.value.slice(0, 1) === '[') { - // Value is JSON encoded - selected = JSON.parse(field.value); - } else if (typeof field.value === 'object') { - selected = field.value; - } else { - selected = [field.value]; - } - } - - return ( -
- {field.label && showLabel && ( - - )} - - {field.error &&
{field.error}
} -
- ); - } -} - -export default observer(Select); diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 000000000..1d69a9acf --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,112 @@ +import { + createRef, + Component, + ReactElement, + RefObject, + ChangeEvent, +} from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import { Field } from '../../@types/mobx-form.types'; + +interface IProps { + field: Field; + className?: string; + showLabel?: boolean; + disabled?: boolean; + multiple?: boolean; +} + +// Can this file be merged into the './select/index.tsx' file? +@observer +class Select extends Component { + private element: RefObject = + createRef(); + + constructor(props: IProps) { + super(props); + } + + multipleChange(e: ChangeEvent): void { + e.preventDefault(); + if (!this.element.current) { + return; + } + const result: string[] = []; + const { options } = this.element.current; + for (const option of options) { + if (option.selected && (option.value || option.text)) { + result.push(option.value || option.text); + } + } + + const { field } = this.props; + field.value = result; + } + + render(): ReactElement { + const { + field, + className = null, + showLabel = true, + disabled = false, + multiple = false, + } = this.props; + + let selected = field.value; + + if (multiple) { + if (typeof field.value === 'string' && field.value.slice(0, 1) === '[') { + // Value is JSON encoded + selected = JSON.parse(field.value); + } else if (typeof field.value === 'object') { + selected = field.value; + } else { + selected = [field.value]; + } + } + + return ( +
+ {field.label && showLabel && ( + + )} + + {field.error &&
{field.error}
} +
+ ); + } +} + +export default Select; diff --git a/src/components/ui/infobox/index.tsx b/src/components/ui/infobox/index.tsx index ad59ea81e..3b878a9de 100644 --- a/src/components/ui/infobox/index.tsx +++ b/src/components/ui/infobox/index.tsx @@ -1,32 +1,14 @@ import { mdiClose } from '@mdi/js'; import classnames from 'classnames'; -import { Component, ReactNode } from 'react'; -import injectStyle, { WithStylesProps } from 'react-jss'; - +import { noop } from 'lodash'; +import { Component, ReactElement, ReactNode } from 'react'; +import withStyles, { WithStylesProps } from 'react-jss'; import { Theme } from '../../../themes'; import Icon from '../icon'; -interface IProps extends WithStylesProps { - children: ReactNode; - icon?: string; - type?: string; - dismissable?: boolean; - ctaLabel?: string; - - className?: string; - onDismiss?: () => void; - onUnmount?: () => void; - ctaOnClick?: () => void; -} - -interface IState { - isDismissing: boolean; - dismissed: boolean; -} - const buttonStyles = (theme: Theme) => { const styles = {}; - Object.keys(theme.styleTypes).map(style => { + for (const style of Object.keys(theme.styleTypes)) { Object.assign(styles, { [style]: { background: theme.styleTypes[style].accent, @@ -38,7 +20,7 @@ const buttonStyles = (theme: Theme) => { }, }, }); - }); + } return styles; }; @@ -108,23 +90,35 @@ const styles = (theme: Theme) => ({ ...buttonStyles(theme), }); +interface IProps extends WithStylesProps { + children: ReactNode; + icon?: string; + type?: string; + dismissible?: boolean; + ctaLabel?: string; + className?: string; + onDismiss?: () => void; + onUnmount?: () => void; + ctaOnClick?: () => void; +} + +interface IState { + isDismissing: boolean; + dismissed: boolean; +} + class InfoboxComponent extends Component { - public static defaultProps = { - type: 'primary', - dismissable: false, - ctaOnClick: () => {}, - onDismiss: () => {}, - ctaLabel: '', - className: '', - }; - - state = { - isDismissing: false, - dismissed: false, - }; - - dismiss() { - const { onDismiss } = this.props; + constructor(props: IProps) { + super(props); + + this.state = { + isDismissing: false, + dismissed: false, + }; + } + + dismiss(): void { + const { onDismiss = noop } = this.props; this.setState({ isDismissing: true, @@ -146,16 +140,16 @@ class InfoboxComponent extends Component { if (onUnmount) onUnmount(); } - render() { + render(): ReactElement | null { const { classes, children, icon, - type, - ctaLabel, - ctaOnClick, - dismissable, - className, + type = 'primary', + dismissible = false, + ctaOnClick = noop, + ctaLabel = '', + className = '', } = this.props; const { isDismissing, dismissed } = this.state; @@ -186,7 +180,7 @@ class InfoboxComponent extends Component { {ctaLabel} )} - {dismissable && ( + {dismissible && (