From edd9bf7b598fdcb2c1d92f9bab8bc2496f08db97 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Tue, 21 Jan 2025 15:31:08 +0300 Subject: [PATCH] cleanup forms --- client/src/components/ui/Controls/Input.tsx | 8 +- client/src/components/ui/Controls/Select.tsx | 27 +++ .../src/components/ui/Controls/Textarea.tsx | 4 +- .../components/ui/Guide/MobileConfigForm.tsx | 132 +++++++------- client/src/helpers/constants.ts | 1 + client/src/install/Setup/Auth.tsx | 144 ++++++++------- client/src/install/Setup/Settings.tsx | 170 ++++++++---------- client/src/install/Setup/index.tsx | 18 +- client/src/login/Login/Form.tsx | 90 +++++----- 9 files changed, 293 insertions(+), 301 deletions(-) create mode 100644 client/src/components/ui/Controls/Select.tsx diff --git a/client/src/components/ui/Controls/Input.tsx b/client/src/components/ui/Controls/Input.tsx index 7e0407ee..d66b2318 100644 --- a/client/src/components/ui/Controls/Input.tsx +++ b/client/src/components/ui/Controls/Input.tsx @@ -1,12 +1,12 @@ import React, { ComponentProps, forwardRef, ReactNode } from 'react'; import clsx from 'clsx'; -interface Props extends ComponentProps<'input'> { +type Props = ComponentProps<'input'> & { label?: string; leftAddon?: ReactNode; rightAddon?: ReactNode; error?: string; -} +}; export const Input = forwardRef( ({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => ( @@ -18,10 +18,10 @@ export const Input = forwardRef( )}
{leftAddon &&
{leftAddon}
} - + {rightAddon &&
{rightAddon}
}
- {error &&
{error}
} + {error &&
{error}
} ), ); diff --git a/client/src/components/ui/Controls/Select.tsx b/client/src/components/ui/Controls/Select.tsx new file mode 100644 index 00000000..18ee61c2 --- /dev/null +++ b/client/src/components/ui/Controls/Select.tsx @@ -0,0 +1,27 @@ +import React, { ComponentProps, forwardRef } from 'react'; +import clsx from 'clsx'; + +type SelectProps = ComponentProps<'select'> & { + label?: string; + error?: string; +}; + +export const Select = forwardRef( + ({ name, label, className, error, children, ...rest }, ref) => ( +
+ {label && ( + + )} +
+ +
+ {error &&
{error}
} +
+ ), +); + +Select.displayName = 'Select'; diff --git a/client/src/components/ui/Controls/Textarea.tsx b/client/src/components/ui/Controls/Textarea.tsx index 002c1348..4ceb1390 100644 --- a/client/src/components/ui/Controls/Textarea.tsx +++ b/client/src/components/ui/Controls/Textarea.tsx @@ -1,11 +1,11 @@ import React, { ComponentProps, forwardRef } from 'react'; import clsx from 'clsx'; -interface Props extends ComponentProps<'textarea'> { +type Props = ComponentProps<'textarea'> & { className?: string; label?: string; error?: string; -} +}; export const Textarea = forwardRef(({ name, label, className, error, ...rest }, ref) => (
diff --git a/client/src/components/ui/Guide/MobileConfigForm.tsx b/client/src/components/ui/Guide/MobileConfigForm.tsx index d9a9a67c..f26fc6f3 100644 --- a/client/src/components/ui/Guide/MobileConfigForm.tsx +++ b/client/src/components/ui/Guide/MobileConfigForm.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Trans } from 'react-i18next'; -import { useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; +import { Controller, useForm } from 'react-hook-form'; import i18next from 'i18next'; import cn from 'classnames'; @@ -13,6 +13,7 @@ import { validatePort, validateIsSafePort, } from '../../../helpers/validators'; +import { Input } from '../Controls/Input'; const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => { if (!host || invalid) { @@ -23,7 +24,7 @@ const getDownloadLink = (host: string, clientId: string, protocol: string, inval ); } - const linkParams: { host: string, client_id?: string } = { host }; + const linkParams: { host: string; client_id?: string } = { host }; if (clientId) { linkParams.client_id = clientId; @@ -33,8 +34,7 @@ const getDownloadLink = (host: string, clientId: string, protocol: string, inval + download> {i18next.t('download_mobileconfig')} ); @@ -51,24 +51,32 @@ type FormValues = { clientId: string; protocol: string; port?: number; -} +}; type Props = { initialValues?: FormValues; -} +}; + +const defaultFormValues = { + host: '', + clientId: '', + protocol: MOBILE_CONFIG_LINKS.DOT, + port: undefined, +}; export const MobileConfigForm = ({ initialValues }: Props) => { + const { t } = useTranslation(); + const { register, watch, - formState: { errors, isValid }, - } = useForm({ + control, + formState: { isValid }, + } = useForm({ mode: 'onChange', defaultValues: { - host: initialValues?.host || '', - clientId: initialValues?.clientId || '', - protocol: initialValues?.protocol || MOBILE_CONFIG_LINKS.DOT, - port: initialValues?.port || undefined, + ...defaultFormValues, + ...initialValues, }, }); @@ -91,50 +99,46 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
- - - ( + + )} /> - {errors.host && ( -
- {errors.host.message} -
- )}
{protocol === MOBILE_CONFIG_LINKS.DOH && (
- - - toNumber(val), + validatePort(value) || true, safety: (value) => validateIsSafePort(value) || true, }, - })} + }} + render={({ field, fieldState }) => ( + { + const { value } = e.target; + field.onChange(toNumber(value)); + }} + /> + )} /> - - {errors.port && ( -
- {errors.port.message} -
- )}
)}
@@ -149,21 +153,21 @@ export const MobileConfigForm = ({ initialValues }: Props) => { client_id_desc
- ( + + )} /> - - {errors.clientId && ( -
- {errors.clientId.message} -
- )}
@@ -171,11 +175,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => { {i18next.t('protocol')} - diff --git a/client/src/helpers/constants.ts b/client/src/helpers/constants.ts index c94a6c00..e83983cf 100644 --- a/client/src/helpers/constants.ts +++ b/client/src/helpers/constants.ts @@ -91,6 +91,7 @@ export const STANDARD_WEB_PORT = 80; export const STANDARD_HTTPS_PORT = 443; export const DNS_OVER_TLS_PORT = 853; export const DNS_OVER_QUIC_PORT = 853; +export const MIN_PORT = 1; export const MAX_PORT = 65535; export const EMPTY_DATE = '0001-01-01T00:00:00Z'; diff --git a/client/src/install/Setup/Auth.tsx b/client/src/install/Setup/Auth.tsx index 64bc9b87..d7cd2158 100644 --- a/client/src/install/Setup/Auth.tsx +++ b/client/src/install/Setup/Auth.tsx @@ -1,27 +1,28 @@ import React from 'react'; -import { useForm } from 'react-hook-form'; -import { withTranslation, Trans } from 'react-i18next'; -import flow from 'lodash/flow'; -import cn from 'classnames'; -import i18n from '../../i18n'; +import { Controller, useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; import Controls from './Controls'; -import { validatePasswordLength } from '../../helpers/validators'; +import { validatePasswordLength, validateRequiredValue } from '../../helpers/validators'; +import { Input } from '../../components/ui/Controls/Input'; + +type AuthFormValues = { + username: string; + password: string; + confirm_password: string; +}; type Props = { - onAuthSubmit: (...args: unknown[]) => string; - pristine: boolean; - invalid: boolean; - t: (...args: unknown[]) => string; -} + onAuthSubmit: (values: AuthFormValues) => void; +}; -const Auth = (props: Props) => { - const { t, onAuthSubmit } = props; +export const Auth = ({ onAuthSubmit }: Props) => { + const { t } = useTranslation(); const { - register, handleSubmit, watch, - formState: { errors, isDirty, isValid }, - } = useForm({ + control, + formState: { isDirty, isValid }, + } = useForm({ mode: 'onChange', defaultValues: { username: '', @@ -34,7 +35,7 @@ const Auth = (props: Props) => { const validateConfirmPassword = (value: string) => { if (value !== password) { - return i18n.t('form_error_password'); + return t('form_error_password'); } return undefined; }; @@ -51,74 +52,71 @@ const Auth = (props: Props) => {

- - ( + + )} /> - {errors.username && ( -
- {errors.username.message} -
- )}
- - ( + + )} /> - {errors.password && ( -
- {errors.password.message} -
- )}
- - ( + + )} /> - {errors.confirm_password && ( -
- {errors.confirm_password.message} -
- )}
- + ); }; - -export default flow([ - withTranslation(), -])(Auth); diff --git a/client/src/install/Setup/Settings.tsx b/client/src/install/Setup/Settings.tsx index 8edb3887..6a6da205 100644 --- a/client/src/install/Setup/Settings.tsx +++ b/client/src/install/Setup/Settings.tsx @@ -15,14 +15,17 @@ import { STANDARD_DNS_PORT, STANDARD_WEB_PORT, MAX_PORT, + MIN_PORT, } from '../../helpers/constants'; -import { toNumber } from '../../helpers/form'; import { validateRequiredValue } from '../../helpers/validators'; import { DhcpInterface } from '../../initialState'; +import { Input } from '../../components/ui/Controls/Input'; +import { Select } from '../../components/ui/Controls/Select'; +import { toNumber } from '../../helpers/form'; -const validateInstallPort = (value: any) => { - if (value < 1 || value > MAX_PORT) { +const validateInstallPort = (value: number) => { + if (value < MIN_PORT || value > MAX_PORT) { return i18n.t('form_error_port'); } return undefined; @@ -77,13 +80,7 @@ const renderInterfaces = (interfaces: DhcpInterface[]) => return null; }); -const Settings: React.FC = ({ - handleSubmit, - handleFix, - validateForm, - config, - interfaces, -}) => { +export const Settings = ({ handleSubmit, handleFix, validateForm, config, interfaces }: Props) => { const { t } = useTranslation(); const defaultValues = { @@ -101,7 +98,7 @@ const Settings: React.FC = ({ control, watch, handleSubmit: reactHookFormSubmit, - formState: { isValid, errors }, + formState: { isValid }, } = useForm({ defaultValues, mode: 'onChange', @@ -113,12 +110,16 @@ const Settings: React.FC = ({ const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns; const { staticIp } = config; - const webIpVal = watch("web.ip"); - const webPortVal = watch("web.port"); - const dnsIpVal = watch("dns.ip"); - const dnsPortVal = watch("dns.port"); + const webIpVal = watch('web.ip'); + const webPortVal = watch('web.port'); + const dnsIpVal = watch('dns.ip'); + const dnsPortVal = watch('dns.port'); useEffect(() => { + if (!isValid || validateInstallPort(webPortVal) || validateInstallPort(dnsPortVal)) { + return; + } + validateForm({ web: { ip: webIpVal, @@ -171,44 +172,46 @@ const Settings: React.FC = ({ } }; - const getStaticIpMessage = useCallback((staticIp: StaticIpType) => { - const { static: status, ip } = staticIp; + const getStaticIpMessage = useCallback( + (staticIp: StaticIpType) => { + const { static: status, ip } = staticIp; - switch (status) { - case STATUS_RESPONSE.NO: - return ( - <> -
- text]}> - install_static_configure - + switch (status) { + case STATUS_RESPONSE.NO: + return ( + <> +
+ text]}> + install_static_configure + +
+ + + + ); + case STATUS_RESPONSE.ERROR: + return ( +
+ install_static_error
- - - - ); - case STATUS_RESPONSE.ERROR: - return ( -
- install_static_error -
- ); - case STATUS_RESPONSE.YES: - return ( -
- install_static_ok -
- ); - default: - return null; - } - }, [handleStaticIp]); + ); + case STATUS_RESPONSE.YES: + return ( +
+ install_static_ok +
+ ); + default: + return null; + } + }, + [handleStaticIp], + ); const onSubmit = (data: any) => { validateForm(data); @@ -232,17 +235,12 @@ const Settings: React.FC = ({ name="web.ip" control={control} render={({ field }) => ( - {renderInterfaces(interfaces)} - + )} />
@@ -257,29 +255,24 @@ const Settings: React.FC = ({ name="web.port" control={control} rules={{ - required: t('form_error_required'), validate: { + required: validateRequiredValue, installPort: validateInstallPort, }, }} - render={({ field }) => ( - ( + { - const val = toNumber(e.target.value); - field.onChange(val); + const { value } = e.target; + field.onChange(toNumber(value)); }} /> )} /> - {errors.web?.port && ( -
- {errors.web.port.message} -
- )} @@ -291,8 +284,7 @@ const Settings: React.FC = ({ )} @@ -331,17 +323,12 @@ const Settings: React.FC = ({ name="dns.ip" control={control} render={({ field }) => ( - {renderInterfaces(interfaces)} - + )} /> @@ -362,24 +349,19 @@ const Settings: React.FC = ({ installPort: validateInstallPort, }, }} - render={({ field }) => ( - ( + { - const val = toNumber(e.target.value); - field.onChange(val); + const { value } = e.target; + field.onChange(toNumber(value)); }} /> )} /> - {errors.dns?.port.message && ( -
- {t(errors.dns.port.message)} -
- )} @@ -415,16 +397,10 @@ const Settings: React.FC = ({ dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && ( + link , - ]} - > + ]}> port_53_faq_link )} @@ -463,5 +439,3 @@ const Settings: React.FC = ({ ); }; - -export default Settings; diff --git a/client/src/install/Setup/index.tsx b/client/src/install/Setup/index.tsx index 2408c65e..dece704c 100644 --- a/client/src/install/Setup/index.tsx +++ b/client/src/install/Setup/index.tsx @@ -9,10 +9,11 @@ import { INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../ import Loading from '../../components/ui/Loading'; import Greeting from './Greeting'; -import Settings from './Settings'; +import { Settings } from './Settings'; import Devices from './Devices'; import Submit from './Submit'; import Progress from './Progress'; +import { Auth } from './Auth'; import Toasts from '../../components/Toasts'; import Footer from '../../components/ui/Footer'; import Icons from '../../components/ui/Icons'; @@ -20,7 +21,6 @@ import { Logo } from '../../components/ui/svg/logo'; import './Setup.css'; import '../../components/ui/Tabler.css'; -import Auth from './Auth'; const Setup = () => { const dispatch = useDispatch(); @@ -37,11 +37,13 @@ const Setup = () => { delete config.staticIp; if (web.port && dns.port) { - dispatch(actionCreators.setAllSettings({ - web, - dns, - ...config, - })); + dispatch( + actionCreators.setAllSettings({ + web, + dns, + ...config, + }), + ); } }; @@ -120,4 +122,4 @@ const Setup = () => { ); }; -export default Setup; \ No newline at end of file +export default Setup; diff --git a/client/src/login/Login/Form.tsx b/client/src/login/Login/Form.tsx index f4773e6e..f6dd2283 100644 --- a/client/src/login/Login/Form.tsx +++ b/client/src/login/Login/Form.tsx @@ -1,23 +1,25 @@ import React from 'react'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { Input } from '../../components/ui/Controls/Input'; +import { validateRequiredValue } from '../../helpers/validators'; type FormValues = { username: string; password: string; -} +}; type LoginFormProps = { onSubmit: (data: FormValues) => void; processing: boolean; -} +}; const Form = ({ onSubmit, processing }: LoginFormProps) => { const { t } = useTranslation(); const { - register, handleSubmit, - formState: { errors, isValid }, + control, + formState: { isValid }, } = useForm({ mode: 'onChange', defaultValues: { @@ -30,58 +32,46 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
- - - ( + + )} /> - {errors.username && ( - - {errors.username.message} - - )}
- - - ( + + )} /> - {errors.password && ( - - {errors.password.message} - - )}
-
@@ -90,4 +80,4 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => { ); }; -export default Form; \ No newline at end of file +export default Form;