From 914affe0c0790d88197bedb7a3333984a0ac13e1 Mon Sep 17 00:00:00 2001 From: dmitrii Date: Tue, 24 Dec 2024 13:55:54 +0300 Subject: [PATCH] update components to use react-hook-form --- client/src/actions/index.tsx | 5 +- .../components/Settings/Dhcp/FormDHCPv4.tsx | 202 +++++---- .../components/Settings/Dhcp/FormDHCPv6.tsx | 156 ++++--- .../components/Settings/Dhcp/Interfaces.tsx | 63 ++- .../components/Settings/Dns/Access/Form.tsx | 170 ++++---- .../components/Settings/Dns/Cache/Form.tsx | 99 +++-- .../components/Settings/Dns/Config/Form.tsx | 345 +++++++++------ .../components/Settings/Dns/Upstream/Form.tsx | 405 +++++++++--------- 8 files changed, 830 insertions(+), 615 deletions(-) diff --git a/client/src/actions/index.tsx b/client/src/actions/index.tsx index 1a4bd1d2..c069f6ad 100644 --- a/client/src/actions/index.tsx +++ b/client/src/actions/index.tsx @@ -424,10 +424,9 @@ export const testUpstream = } }; -export const testUpstreamWithFormValues = () => async (dispatch: any, getState: any) => { +export const testUpstreamWithFormValues = (formValues: any) => async (dispatch: any, getState: any) => { const { upstream_dns_file } = getState().dnsConfig; - const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } = - getState().form[FORM_NAME.UPSTREAM].values; + const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } = formValues; return dispatch( testUpstream( diff --git a/client/src/components/Settings/Dhcp/FormDHCPv4.tsx b/client/src/components/Settings/Dhcp/FormDHCPv4.tsx index 48e33ee2..9ca03d5d 100644 --- a/client/src/components/Settings/Dhcp/FormDHCPv4.tsx +++ b/client/src/components/Settings/Dhcp/FormDHCPv4.tsx @@ -1,28 +1,32 @@ -import React, { useCallback } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; - -import { Field, reduxForm } from 'redux-form'; +import React from 'react'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; -import { renderInputField, toNumber } from '../../../helpers/form'; -import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants'; +import { UINT32_RANGE } from '../../../helpers/constants'; import { - validateIpv4, - validateRequiredValue, - validateIpv4RangeEnd, validateGatewaySubnetMask, validateIpForGatewaySubnetMask, + validateIpv4, + validateIpv4RangeEnd, validateNotInRange, + validateRequiredValue, } from '../../../helpers/validators'; import { RootState } from '../../../initialState'; -interface FormDHCPv4Props { - handleSubmit: (...args: unknown[]) => string; - submitting: boolean; - initialValues: { v4?: any }; +type FormValues = { + v4?: { + gateway_ip?: string; + subnet_mask?: string; + range_start?: string; + range_end?: string; + lease_duration?: number; + }; +} + +type FormDHCPv4Props = { processingConfig?: boolean; - change: (field: string, value: any) => void; - reset: () => void; + initialValues?: FormValues; ipv4placeholders?: { gateway_ip: string; subnet_mask: string; @@ -30,70 +34,96 @@ interface FormDHCPv4Props { range_end: string; lease_duration: string; }; + onSubmit?: (data: FormValues) => Promise | void; } -const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholders }: FormDHCPv4Props) => { +const FormDHCPv4 = ({ + processingConfig, + initialValues, + ipv4placeholders, + onSubmit +}: FormDHCPv4Props) => { const { t } = useTranslation(); - const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual); - - const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual); + const interfaces = useSelector((state: RootState) => state.form.DHCP_INTERFACES); const interface_name = interfaces?.values?.interface_name; const isInterfaceIncludesIpv4 = useSelector( (state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses, ); - const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean); - - const invalid = - dhcp?.syncErrors || - interfaces?.syncErrors || - !isInterfaceIncludesIpv4 || - isEmptyConfig || - submitting || - processingConfig; - - const validateRequired = useCallback( - (value) => { - if (isEmptyConfig) { - return undefined; - } - return validateRequiredValue(value); + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + watch, + } = useForm({ + mode: 'onChange', + defaultValues: { + v4: initialValues?.v4 || { + gateway_ip: '', + subnet_mask: '', + range_start: '', + range_end: '', + lease_duration: 0, + }, }, - [isEmptyConfig], - ); + }); + + const formValues = watch('v4'); + const isEmptyConfig = !Object.values(formValues || {}).some(Boolean); + + const handleFormSubmit = async (data: FormValues) => { + if (onSubmit) { + await onSubmit(data); + } + }; return ( -
+
- - isEmptyConfig ? undefined : validateRequiredValue(value), + notInRange: validateNotInRange, + } + })} /> + {errors.v4?.gateway_ip && ( +
+ {t(errors.v4.gateway_ip.message)} +
+ )}
- - isEmptyConfig ? undefined : validateRequiredValue(value), + subnet: validateGatewaySubnetMask, + } + })} /> + {errors.v4?.subnet_mask && ( +
+ {t(errors.v4.subnet_mask.message)} +
+ )}
@@ -105,52 +135,79 @@ const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholde
- + {errors.v4?.range_start && ( +
+ {t(errors.v4.range_start.message)} +
+ )}
- + {errors.v4?.range_end && ( +
+ {errors.v4.range_end.message} +
+ )}
- - isEmptyConfig ? undefined : validateRequiredValue(value), + } + })} /> + {errors.v4?.lease_duration && ( +
+ {t(errors.v4.lease_duration.message)} +
+ )}
-
@@ -158,9 +215,4 @@ const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholde ); }; -export default reduxForm< - Record, - Omit ->({ - form: FORM_NAME.DHCPv4, -})(FormDHCPv4); +export default FormDHCPv4; diff --git a/client/src/components/Settings/Dhcp/FormDHCPv6.tsx b/client/src/components/Settings/Dhcp/FormDHCPv6.tsx index 14b7782b..36a27120 100644 --- a/client/src/components/Settings/Dhcp/FormDHCPv6.tsx +++ b/client/src/components/Settings/Dhcp/FormDHCPv6.tsx @@ -1,64 +1,73 @@ -import React, { useCallback } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; - -import { Field, reduxForm } from 'redux-form'; +import React from 'react'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; -import { renderInputField, toNumber } from '../../../helpers/form'; -import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants'; +import { UINT32_RANGE } from '../../../helpers/constants'; import { validateIpv6, validateRequiredValue } from '../../../helpers/validators'; import { RootState } from '../../../initialState'; -interface FormDHCPv6Props { - handleSubmit: (...args: unknown[]) => string; - submitting: boolean; - initialValues: { - v6?: any; +type FormValues = { + v6?: { + range_start?: string; + range_end?: string; + lease_duration?: number; }; - change: (field: string, value: any) => void; - reset: () => void; +} + +type FormDHCPv6Props = { processingConfig?: boolean; + initialValues?: FormValues; ipv6placeholders?: { range_start: string; range_end: string; lease_duration: string; }; + onSubmit?: (data: FormValues) => Promise | void; } -const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholders }: FormDHCPv6Props) => { +const FormDHCPv6 = ({ + processingConfig, + initialValues, + ipv6placeholders, + onSubmit, +}: FormDHCPv6Props) => { const { t } = useTranslation(); - const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv6], shallowEqual); - - const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual); + const interfaces = useSelector((state: RootState) => state.form.DHCP_INTERFACES); const interface_name = interfaces?.values?.interface_name; const isInterfaceIncludesIpv6 = useSelector( (state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses, ); - const isEmptyConfig = !Object.values(dhcp?.values?.v6 ?? {}).some(Boolean); - - const invalid = - dhcp?.syncErrors || - interfaces?.syncErrors || - !isInterfaceIncludesIpv6 || - isEmptyConfig || - submitting || - processingConfig; - - const validateRequired = useCallback( - (value) => { - if (isEmptyConfig) { - return undefined; - } - return validateRequiredValue(value); + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + watch, + } = useForm({ + mode: 'onChange', + defaultValues: { + v6: initialValues?.v6 || { + range_start: '', + range_end: '', + lease_duration: 0, + }, }, - [isEmptyConfig], - ); + }); + + const formValues = watch('v6'); + const isEmptyConfig = !Object.values(formValues || {}).some(Boolean); + + const handleFormSubmit = async (data: FormValues) => { + if (onSubmit) { + await onSubmit(data); + } + }; return ( - +
@@ -68,27 +77,43 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
- isEmptyConfig ? undefined : validateRequiredValue(value), + } + })} /> + {errors.v6?.range_start && ( +
+ {t(errors.v6.range_start.message)} +
+ )}
- isEmptyConfig ? undefined : validateRequiredValue(value), + } + })} /> + {errors.v6?.range_end && ( +
+ {t(errors.v6.range_end.message)} +
+ )}
@@ -98,24 +123,34 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
- - isEmptyConfig ? undefined : validateRequiredValue(value), + } + })} /> + {errors.v6?.lease_duration && ( +
+ {t(errors.v6.lease_duration.message)} +
+ )}
-
@@ -123,9 +158,4 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde ); }; -export default reduxForm< - Record, - Omit ->({ - form: FORM_NAME.DHCPv6, -})(FormDHCPv6); +export default FormDHCPv6; diff --git a/client/src/components/Settings/Dhcp/Interfaces.tsx b/client/src/components/Settings/Dhcp/Interfaces.tsx index 63b5ece3..83a3718a 100644 --- a/client/src/components/Settings/Dhcp/Interfaces.tsx +++ b/client/src/components/Settings/Dhcp/Interfaces.tsx @@ -1,14 +1,21 @@ import React from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; - -import { Field, reduxForm } from 'redux-form'; +import { useSelector } from 'react-redux'; +import { useForm } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; -import { renderSelectField } from '../../../helpers/form'; import { validateRequiredValue } from '../../../helpers/validators'; -import { FORM_NAME } from '../../../helpers/constants'; import { RootState } from '../../../initialState'; +type FormValues = { + interface_name: string; +}; + +type InterfacesProps = { + initialValues?: { + interface_name: string; + }; +}; + const renderInterfaces = (interfaces: any) => Object.keys(interfaces).map((item) => { const option = interfaces[item]; @@ -47,13 +54,13 @@ const getInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: any) }, ]; -interface renderInterfaceValuesProps { +interface RenderInterfaceValuesProps { gateway_ip: string; hardware_address: string; ip_addresses: string[]; } -const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: renderInterfaceValuesProps) => ( +const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: RenderInterfaceValuesProps) => (
    {getInterfaceValues({ @@ -75,13 +82,21 @@ const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: r
); -const Interfaces = () => { +const Interfaces = ({ initialValues }: InterfacesProps) => { const { t } = useTranslation(); - const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp, shallowEqual); + const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp); - const interface_name = - useSelector((store: RootState) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name); + const { + register, + watch, + formState: { errors }, + } = useForm({ + mode: 'onChange', + defaultValues: initialValues, + }); + + const interface_name = watch('interface_name'); if (processingInterfaces || !interfaces) { return null; @@ -92,17 +107,27 @@ const Interfaces = () => { return (
- + {t('dhcp_interface_select')} + + + {errors.interface_name && ( +
+ {t(errors.interface_name.message)} +
+ )}
{interfaceValue && renderInterfaceValues({ gateway_ip: interfaceValue.gateway_ip, @@ -113,6 +138,4 @@ const Interfaces = () => { ); }; -export default reduxForm({ - form: FORM_NAME.DHCP_INTERFACES, -})(Interfaces); +export default Interfaces; diff --git a/client/src/components/Settings/Dns/Access/Form.tsx b/client/src/components/Settings/Dns/Access/Form.tsx index c81d9e86..53190c6c 100644 --- a/client/src/components/Settings/Dns/Access/Form.tsx +++ b/client/src/components/Settings/Dns/Access/Form.tsx @@ -1,13 +1,9 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { Controller, useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; -import { Trans, withTranslation } from 'react-i18next'; -import flow from 'lodash/flow'; - -import { renderTextareaField } from '../../../../helpers/form'; -import { trimMultilineString, removeEmptyLines } from '../../../../helpers/helpers'; -import { CLIENT_ID_LINK, FORM_NAME } from '../../../../helpers/constants'; +import { CLIENT_ID_LINK } from '../../../../helpers/constants'; +import { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers'; const fields = [ { @@ -31,88 +27,108 @@ const fields = [ ]; interface FormProps { - handleSubmit: (...args: unknown[]) => string; - submitting: boolean; - invalid: boolean; - initialValues: object; + initialValues?: { + allowed_clients?: string; + disallowed_clients?: string; + blocked_hosts?: string; + }; + onSubmit: (data: any) => void; processingSet: boolean; - t: (...args: unknown[]) => string; - textarea?: boolean; - allowedClients?: string; } -interface renderFieldProps { - id?: string; - title?: string; - subtitle?: string; - disabled?: boolean; - processingSet?: boolean; - normalizeOnBlur?: (...args: unknown[]) => unknown; +interface FormData { + allowed_clients: string; + disallowed_clients: string; + blocked_hosts: string; } -let Form = (props: FormProps) => { - const { allowedClients, handleSubmit, submitting, invalid, processingSet } = props; +const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => { + const { t } = useTranslation(); + + const { + control, + handleSubmit, + watch, + formState: { isSubmitting, isDirty }, + } = useForm({ + mode: 'onChange', + defaultValues: { + allowed_clients: initialValues?.allowed_clients || '', + disallowed_clients: initialValues?.disallowed_clients || '', + blocked_hosts: initialValues?.blocked_hosts || '', + }, + }); + + const allowedClients = watch('allowed_clients'); const renderField = ({ id, title, subtitle, - disabled = false, - processingSet, normalizeOnBlur, - }: renderFieldProps) => ( -
- + return ( +
+ -
- - text - - ), - }}> - {subtitle} - +
+ + {t('text')} + + ), + }}> + {subtitle} + +
+ + ( +