cleanup forms

This commit is contained in:
Ildar Kamalov
2025-01-21 16:52:12 +03:00
parent 70aeee3037
commit 254b25a026
13 changed files with 143 additions and 143 deletions

View File

@@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Info from './Info'; import Info from './Info';
import { RootState } from '../../../initialState'; import { RootState } from '../../../initialState';
import { validateRequiredValue } from '../../../helpers/validators';
import { Input } from '../../ui/Controls/Input';
interface FormValues { interface FormValues {
name: string; name: string;
@@ -14,7 +16,7 @@ interface FormValues {
type Props = { type Props = {
onSubmit?: (data: FormValues) => void; onSubmit?: (data: FormValues) => void;
} };
const Check = ({ onSubmit }: Props) => { const Check = ({ onSubmit }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -23,7 +25,7 @@ const Check = ({ onSubmit }: Props) => {
const hostname = useSelector((state: RootState) => state.filtering.check.hostname); const hostname = useSelector((state: RootState) => state.filtering.check.hostname);
const { const {
register, control,
handleSubmit, handleSubmit,
formState: { isDirty, isValid }, formState: { isDirty, isValid },
} = useForm<FormValues>({ } = useForm<FormValues>({
@@ -38,24 +40,29 @@ const Check = ({ onSubmit }: Props) => {
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="row"> <div className="row">
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<div className="input-group"> <Controller
<input name="name"
id="name" control={control}
type="text" rules={{ validate: validateRequiredValue }}
className="form-control" render={({ field, fieldState }) => (
placeholder={t('form_enter_host') ?? ''} <Input
{...register('name', { required: t('form_error_required') })} {...field}
/> type="text"
<span className="input-group-append"> placeholder={t('form_enter_host')}
<button error={fieldState.error?.message}
className="btn btn-success btn-standard btn-large" rightAddon={
type="submit" <span className="input-group-append">
disabled={!isDirty || !isValid || processingCheck} <button
> className="btn btn-success btn-standard btn-large"
{t('check')} type="submit"
</button> disabled={!isDirty || !isValid || processingCheck}>
</span> {t('check')}
</div> </button>
</span>
}
/>
)}
/>
{hostname && ( {hostname && (
<> <>

View File

@@ -6,6 +6,7 @@ import { validatePath, validateRequiredValue } from '../../helpers/validators';
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants'; import { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants';
import filtersCatalog from '../../helpers/filters/filters'; import filtersCatalog from '../../helpers/filters/filters';
import { FiltersList } from './FiltersList'; import { FiltersList } from './FiltersList';
import { Input } from '../ui/Controls/Input';
type FormValues = { type FormValues = {
enabled: boolean; enabled: boolean;
@@ -15,7 +16,7 @@ type FormValues = {
type Props = { type Props = {
closeModal: (...args: unknown[]) => void; closeModal: (...args: unknown[]) => void;
onSubmit: (...args: unknown[]) => void; onSubmit: (values: FormValues) => void;
processingAddFilter: boolean; processingAddFilter: boolean;
processingConfigFilter: boolean; processingConfigFilter: boolean;
whitelist?: boolean; whitelist?: boolean;
@@ -81,13 +82,7 @@ export const Form = ({
name="name" name="name"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<input <Input {...field} type="text" placeholder={t('enter_name_hint')} trimOnBlur />
{...field}
type="text"
className="form-control"
placeholder={t('enter_name_hint')}
onBlur={(e) => field.onChange(e.target.value.trim())}
/>
)} )}
/> />
</div> </div>
@@ -98,12 +93,11 @@ export const Form = ({
control={control} control={control}
rules={{ validate: { validateRequiredValue, validatePath } }} rules={{ validate: { validateRequiredValue, validatePath } }}
render={({ field }) => ( render={({ field }) => (
<input <Input
{...field} {...field}
type="text" type="text"
className="form-control"
placeholder={t('enter_url_or_path_hint')} placeholder={t('enter_url_or_path_hint')}
onBlur={(e) => field.onChange(e.target.value.trim())} trimOnBlur
/> />
)} )}
/> />

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { validateAnswer, validateDomain } from '../../../helpers/validators'; import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';
import { Input } from '../../ui/Controls/Input';
interface FormValues { interface FormValues {
domain: string; domain: string;
@@ -11,19 +12,19 @@ interface FormValues {
type Props = { type Props = {
processingAdd: boolean; processingAdd: boolean;
currentRewrite?: { answer: string, domain: string; }; currentRewrite?: { answer: string; domain: string };
toggleRewritesModal: () => void; toggleRewritesModal: () => void;
onSubmit?: (data: FormValues) => Promise<void> | void; onSubmit?: (data: FormValues) => Promise<void> | void;
} };
const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }: Props) => { const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
register,
handleSubmit, handleSubmit,
reset, reset,
formState: { isDirty, isSubmitting, errors }, control,
formState: { isDirty, isSubmitting },
} = useForm<FormValues>({ } = useForm<FormValues>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
@@ -45,21 +46,24 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
<Trans>domain_desc</Trans> <Trans>domain_desc</Trans>
</div> </div>
<div className="form__group"> <div className="form__group">
<input <Controller
id="domain" name="domain"
type="text" control={control}
className="form-control" rules={{
placeholder={t('form_domain')} validate: {
{...register('domain', { validate: validateDomain,
validate: validateDomain, required: validateRequiredValue,
required: t('form_error_required'), },
})} }}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
placeholder={t('form_domain')}
error={fieldState.error?.message}
/>
)}
/> />
{errors.domain && (
<div className="form__message form__message--error">
{errors.domain.message}
</div>
)}
</div> </div>
<Trans>examples_title</Trans>: <Trans>examples_title</Trans>:
<ol className="leading-loose"> <ol className="leading-loose">
@@ -74,21 +78,24 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
</li> </li>
</ol> </ol>
<div className="form__group"> <div className="form__group">
<input <Controller
id="answer" name="answer"
type="text" control={control}
className="form-control" rules={{
placeholder={t('form_answer') ?? ''} validate: {
{...register('answer', { validate: validateAnswer,
validate: validateAnswer, required: validateRequiredValue,
required: t('form_error_required'), },
})} }}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
placeholder={t('form_answer')}
error={fieldState.error?.message}
/>
)}
/> />
{errors.answer && (
<div className="form__message form__message--error">
{errors.answer.message}
</div>
)}
</div> </div>
</div> </div>
@@ -109,16 +116,14 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
onClick={() => { onClick={() => {
reset(); reset();
toggleRewritesModal(); toggleRewritesModal();
}} }}>
>
<Trans>cancel_btn</Trans> <Trans>cancel_btn</Trans>
</button> </button>
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard" className="btn btn-success btn-standard"
disabled={isSubmitting || !isDirty || processingAdd} disabled={isSubmitting || !isDirty || processingAdd}>
>
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
</div> </div>

View File

@@ -19,7 +19,7 @@ type FormValues = {
interface FormProps { interface FormProps {
initialValues: Record<string, boolean>; initialValues: Record<string, boolean>;
blockedServices: BlockedService[]; blockedServices: BlockedService[];
onSubmit: (...args: unknown[]) => void; onSubmit: (values: FormValues) => void;
processing: boolean; processing: boolean;
processingSet: boolean; processingSet: boolean;
} }

View File

@@ -2,52 +2,40 @@ import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import { FieldValues, ControllerRenderProps } from 'react-hook-form'; import { FieldValues, ControllerRenderProps } from 'react-hook-form';
interface ServiceFieldProps extends ControllerRenderProps<FieldValues> { type Props = ControllerRenderProps<FieldValues> & {
placeholder: string; placeholder: string;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
icon?: string; icon?: string;
error?: string; error?: string;
} };
export const ServiceField = React.forwardRef<HTMLInputElement, ServiceFieldProps>(({ export const ServiceField = React.forwardRef<HTMLInputElement, Props>(
name, ({ name, value, onChange, onBlur, placeholder, disabled, className, icon, error }, ref) => (
value, <>
onChange, <label className={cn('service custom-switch', className)}>
onBlur, <input
placeholder, name={name}
disabled, type="checkbox"
className, className="custom-switch-input"
icon, checked={!!value}
error, onChange={onChange}
}, ref) => ( onBlur={onBlur}
<> ref={ref}
<label className={cn('service custom-switch', className)}> disabled={disabled}
<input />
name={name}
type="checkbox"
className="custom-switch-input"
checked={!!value}
onChange={onChange}
onBlur={onBlur}
ref={ref}
disabled={disabled}
/>
<span className="service__switch custom-switch-indicator"></span> <span className="service__switch custom-switch-indicator"></span>
<span className="service__text" title={placeholder}> <span className="service__text" title={placeholder}>
{placeholder} {placeholder}
</span> </span>
{icon && <div dangerouslySetInnerHTML={{ __html: window.atob(icon) }} className="service__icon" />} {icon && <div dangerouslySetInnerHTML={{ __html: window.atob(icon) }} className="service__icon" />}
</label> </label>
{!disabled && error && ( {!disabled && error && <span className="form__message form__message--error">{error}</span>}
<span className="form__message form__message--error"> </>
{error} ),
</span> );
)}
</>
));
ServiceField.displayName = 'ServiceField'; ServiceField.displayName = 'ServiceField';

View File

@@ -22,12 +22,12 @@ import { SearchField } from './SearchField';
export type FormValues = { export type FormValues = {
search: string; search: string;
response_status: string; response_status: string;
} };
type Props = { type Props = {
initialValues: FormValues; initialValues: FormValues;
className?: string; className?: string;
setIsLoading: (...args: unknown[]) => unknown; setIsLoading: (value: boolean) => void;
}; };
export const Form = ({ initialValues, className, setIsLoading }: Props) => { export const Form = ({ initialValues, className, setIsLoading }: Props) => {
@@ -35,11 +35,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const { const { register, watch, setValue } = useForm<FormValues>({
register,
watch,
setValue,
} = useForm<FormValues>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
search: initialValues.search || DEFAULT_LOGS_FILTER.search, search: initialValues.search || DEFAULT_LOGS_FILTER.search,
@@ -50,10 +46,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
const searchValue = watch('search'); const searchValue = watch('search');
const responseStatusValue = watch('response_status'); const responseStatusValue = watch('response_status');
const [debouncedSearch, setDebouncedSearch] = useDebounce( const [debouncedSearch, setDebouncedSearch] = useDebounce(searchValue.trim(), DEBOUNCE_FILTER_TIMEOUT);
searchValue.trim(),
DEBOUNCE_FILTER_TIMEOUT
);
useEffect(() => { useEffect(() => {
dispatch( dispatch(
@@ -91,7 +84,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
value: e.target.value, value: e.target.value,
onChange: (v: string) => setValue('search', v), onChange: (v: string) => setValue('search', v),
}, },
(data: string) => data.trim() (data: string) => data.trim(),
); );
return ( return (
@@ -99,8 +92,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
className="d-flex flex-wrap form-control--container" className="d-flex flex-wrap form-control--container"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
}} }}>
>
<div className="field__search"> <div className="field__search">
<SearchField <SearchField
value={searchValue} value={searchValue}
@@ -117,8 +109,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
<div className="field__select"> <div className="field__select">
<select <select
{...register('response_status')} {...register('response_status')}
className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block" className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block">
>
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => ( {Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
<option key={LABEL} value={QUERY} disabled={disabled}> <option key={LABEL} value={QUERY} disabled={disabled}>
{t(LABEL)} {t(LABEL)}

View File

@@ -111,6 +111,12 @@ const ClientsTable = ({
config.tags = []; config.tags = [];
} }
if (values.ids) {
config.ids = values.ids.map((id) => id.name);
} else {
config.ids = [];
}
if (typeof values.upstreams_cache_size === 'string') { if (typeof values.upstreams_cache_size === 'string') {
config.upstreams_cache_size = 0; config.upstreams_cache_size = 0;
} }

View File

@@ -36,7 +36,7 @@ const defaultFormValues: ClientForm = {
}; };
type Props = { type Props = {
onSubmit: (...args: unknown[]) => void; onSubmit: (values: ClientForm) => void;
onClose: () => void; onClose: () => void;
useGlobalSettings?: boolean; useGlobalSettings?: boolean;
useGlobalServices?: boolean; useGlobalServices?: boolean;
@@ -45,7 +45,7 @@ type Props = {
}; };
processingAdding: boolean; processingAdding: boolean;
processingUpdating: boolean; processingUpdating: boolean;
tagsOptions: unknown[]; tagsOptions: { label: string; value: string }[];
initialValues?: ClientForm; initialValues?: ClientForm;
}; };
@@ -79,15 +79,6 @@ export const Form = ({
const safeSearchServices = { ...safe_search }; const safeSearchServices = { ...safe_search };
delete safeSearchServices.enabled; delete safeSearchServices.enabled;
const onFormSubmit = (values: ClientForm) => {
const data = {
...values,
ids: values.ids.map((idObj) => idObj.name),
};
onSubmit(data);
};
const [activeTabLabel, setActiveTabLabel] = useState('settings'); const [activeTabLabel, setActiveTabLabel] = useState('settings');
const tabs = { const tabs = {
@@ -113,7 +104,7 @@ export const Form = ({
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="modal-body"> <div className="modal-body">
<div className="form__group mb-0"> <div className="form__group mb-0">
<div className="form__group"> <div className="form__group">

View File

@@ -21,3 +21,8 @@ export type ClientForm = {
ignore_querylog: boolean; ignore_querylog: boolean;
ignore_statistics: boolean; ignore_statistics: boolean;
}; };
export type SubmitClientForm = Omit<ClientForm, 'ids' | 'tags'> & {
ids: string[];
tags: string[];
};

View File

@@ -52,7 +52,7 @@ interface ModalProps {
handleClose: (...args: unknown[]) => unknown; handleClose: (...args: unknown[]) => unknown;
processingAdding: boolean; processingAdding: boolean;
processingUpdating: boolean; processingUpdating: boolean;
tagsOptions: unknown[]; tagsOptions: { label: string; value: string }[];
t: (...args: unknown[]) => string; t: (...args: unknown[]) => string;
clientId?: string; clientId?: string;
} }

View File

@@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isValid } from 'date-fns';
import { UINT32_RANGE } from '../../../helpers/constants'; import { UINT32_RANGE } from '../../../helpers/constants';
import { import {
validateGatewaySubnetMask, validateGatewaySubnetMask,

View File

@@ -93,7 +93,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
disabled={processing}> disabled={processing}>
{FILTERS_INTERVALS_HOURS.map((interval) => ( {FILTERS_INTERVALS_HOURS.map((interval) => (
<option value={interval} key={interval}> <option value={interval} key={interval}>
{getTitleForInterval(interval, t)} {getTitleForInterval(interval)}
</option> </option>
))} ))}
</select> </select>

View File

@@ -6,10 +6,11 @@ type Props = ComponentProps<'input'> & {
leftAddon?: ReactNode; leftAddon?: ReactNode;
rightAddon?: ReactNode; rightAddon?: ReactNode;
error?: string; error?: string;
trimOnBlur?: boolean;
}; };
export const Input = forwardRef<HTMLInputElement, Props>( export const Input = forwardRef<HTMLInputElement, Props>(
({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => ( ({ name, label, className, leftAddon, rightAddon, error, trimOnBlur, onBlur, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}> <div className={clsx('form-group', { 'has-error': !!error })}>
{label && ( {label && (
<label className="form__label" htmlFor={name}> <label className="form__label" htmlFor={name}>
@@ -18,7 +19,20 @@ export const Input = forwardRef<HTMLInputElement, Props>(
)} )}
<div className="input-group"> <div className="input-group">
{leftAddon && <div>{leftAddon}</div>} {leftAddon && <div>{leftAddon}</div>}
<input className={clsx('form-control', { 'is-invalid': !!error }, className)} ref={ref} {...rest} /> <input
className={clsx('form-control', { 'is-invalid': !!error }, className)}
ref={ref}
onBlur={(e) => {
if (trimOnBlur) {
e.target.value = e.target.value.trim();
rest.onChange(e);
}
if (onBlur) {
onBlur(e);
}
}}
{...rest}
/>
{rightAddon && <div>{rightAddon}</div>} {rightAddon && <div>{rightAddon}</div>}
</div> </div>
{error && <div className="form__message form__message--error mt-1">{error}</div>} {error && <div className="form__message form__message--error mt-1">{error}</div>}