fix forms

This commit is contained in:
Ildar Kamalov
2025-01-24 16:42:30 +03:00
parent f3f38e1a57
commit f78dc10c2a
16 changed files with 144 additions and 73 deletions

View File

@@ -48,6 +48,7 @@ const Check = ({ onSubmit }: Props) => {
<Input
{...field}
type="text"
data-testid="check_domain_name"
placeholder={t('form_enter_host')}
error={fieldState.error?.message}
rightAddon={
@@ -55,6 +56,7 @@ const Check = ({ onSubmit }: Props) => {
<button
className="btn btn-success btn-standard btn-large"
type="submit"
data-testid="check_domain_submit"
disabled={!isDirty || !isValid || processingCheck}>
{t('check')}
</button>

View File

@@ -74,7 +74,12 @@ export const FiltersList = ({ categories, filters, selectedSources }: Props) =>
name={id}
control={control}
render={({ field }) => (
<Checkbox {...field} title={name} disabled={isSelected} />
<Checkbox
{...field}
data-testid={`filters_${id}`}
title={name}
disabled={isSelected}
/>
)}
/>
{renderIcons(iconsData)}

View File

@@ -15,13 +15,13 @@ type FormValues = {
};
type Props = {
closeModal: (...args: unknown[]) => void;
closeModal: () => void;
onSubmit: (values: FormValues) => void;
processingAddFilter: boolean;
processingConfigFilter: boolean;
whitelist?: boolean;
modalType: string;
toggleFilteringModal: (...args: unknown[]) => void;
toggleFilteringModal: ({ type }: { type?: keyof typeof MODAL_TYPE }) => void;
selectedSources?: Record<string, boolean>;
initialValues?: FormValues;
};
@@ -42,14 +42,14 @@ export const Form = ({
const methods = useForm({ defaultValues: initialValues });
const { handleSubmit, control } = methods;
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
toggleFilteringModal();
const openModal = (modalType: keyof typeof MODAL_TYPE, timeout = MODAL_OPEN_TIMEOUT) => {
toggleFilteringModal(undefined);
setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);
};
const openFilteringListModal = () => openModal(MODAL_TYPE.CHOOSE_FILTERING_LIST);
const openFilteringListModal = () => openModal('CHOOSE_FILTERING_LIST');
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
const openAddFiltersModal = () => openModal('ADD_FILTERS');
return (
<FormProvider {...methods}>
@@ -81,8 +81,15 @@ export const Form = ({
<Controller
name="name"
control={control}
render={({ field }) => (
<Input {...field} type="text" placeholder={t('enter_name_hint')} trimOnBlur />
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="filters_name"
placeholder={t('enter_name_hint')}
error={fieldState.error?.message}
trimOnBlur
/>
)}
/>
</div>
@@ -92,11 +99,13 @@ export const Form = ({
name="url"
control={control}
rules={{ validate: { validateRequiredValue, validatePath } }}
render={({ field }) => (
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="filters_url"
placeholder={t('enter_url_or_path_hint')}
error={fieldState.error?.message}
trimOnBlur
/>
)}
@@ -118,6 +127,7 @@ export const Form = ({
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
<button
type="submit"
data-testid="filters_save"
className="btn btn-success"
disabled={processingAddFilter || processingConfigFilter}>
{t('save_btn')}

View File

@@ -5,16 +5,16 @@ import { Trans, useTranslation } from 'react-i18next';
import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';
import { Input } from '../../ui/Controls/Input';
interface FormValues {
interface RewriteFormValues {
domain: string;
answer: string;
}
type Props = {
processingAdd: boolean;
currentRewrite?: { answer: string; domain: string };
currentRewrite?: RewriteFormValues;
toggleRewritesModal: () => void;
onSubmit?: (data: FormValues) => Promise<void> | void;
onSubmit?: (data: RewriteFormValues) => Promise<void> | void;
};
const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }: Props) => {
@@ -25,7 +25,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
reset,
control,
formState: { isDirty, isSubmitting },
} = useForm<FormValues>({
} = useForm<RewriteFormValues>({
mode: 'onBlur',
defaultValues: {
domain: currentRewrite?.domain || '',
@@ -33,7 +33,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
},
});
const handleFormSubmit = async (data: FormValues) => {
const handleFormSubmit = async (data: RewriteFormValues) => {
if (onSubmit) {
await onSubmit(data);
}
@@ -59,6 +59,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
<Input
{...field}
type="text"
data-testid="rewrites_domain"
placeholder={t('form_domain')}
error={fieldState.error?.message}
/>
@@ -91,6 +92,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
<Input
{...field}
type="text"
data-testid="rewrites_answer"
placeholder={t('form_answer')}
error={fieldState.error?.message}
/>
@@ -111,6 +113,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
<div className="btn-list">
<button
type="button"
data-testid="rewrites_cancel"
className="btn btn-secondary btn-standard"
disabled={isSubmitting || processingAdd}
onClick={() => {
@@ -122,6 +125,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
<button
type="submit"
data-testid="rewrites_save"
className="btn btn-success btn-standard"
disabled={isSubmitting || !isDirty || processingAdd}>
<Trans>save_btn</Trans>

View File

@@ -46,6 +46,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
<div className="col-6">
<button
type="button"
data-testid="blocked_services_block_all"
className="btn btn-secondary btn-block"
disabled={processing || processingSet}
onClick={() => handleToggleAllServices(true)}>
@@ -56,6 +57,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
<div className="col-6">
<button
type="button"
data-testid="blocked_services_unblock_all"
className="btn btn-secondary btn-block"
disabled={processing || processingSet}
onClick={() => handleToggleAllServices(false)}>
@@ -65,7 +67,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
</div>
<div className="services">
{blockedServices.map((service: any) => (
{blockedServices.map((service: BlockedService) => (
<Controller
key={service.id}
name={`blocked_services.${service.id}`}
@@ -73,6 +75,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
render={({ field }) => (
<ServiceField
{...field}
data-testid={`blocked_services_${service.id}`}
placeholder={service.name}
disabled={processing || processingSet}
icon={service.icon_svg}
@@ -86,6 +89,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
<div className="btn-list">
<button
type="submit"
data-testid="blocked_services_save"
className="btn btn-success btn-standard btn-large"
disabled={isSubmitting || !isDirty || processing || processingSet}>
<Trans>save_btn</Trans>

View File

@@ -11,7 +11,7 @@ type Props = ControllerRenderProps<FieldValues> & {
};
export const ServiceField = React.forwardRef<HTMLInputElement, Props>(
({ name, value, onChange, onBlur, placeholder, disabled, className, icon, error }, ref) => (
({ name, value, onChange, onBlur, placeholder, disabled, className, icon, error, ...rest }, ref) => (
<>
<label className={cn('service custom-switch', className)}>
<input
@@ -23,6 +23,7 @@ export const ServiceField = React.forwardRef<HTMLInputElement, Props>(
onBlur={onBlur}
ref={ref}
disabled={disabled}
{...rest}
/>
<span className="service__switch custom-switch-indicator"></span>

View File

@@ -28,6 +28,7 @@ export const BlockedServices = ({ services }: Props) => {
render={({ field }) => (
<ServiceField
{...field}
data-testid="clients_use_global_blocked_services"
placeholder={t('blocked_services_global')}
className="service--global"
/>
@@ -38,6 +39,7 @@ export const BlockedServices = ({ services }: Props) => {
<div className="col-6">
<button
type="button"
data-testid="clients_block_all"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => handleToggleAllServices(true)}>
@@ -48,6 +50,7 @@ export const BlockedServices = ({ services }: Props) => {
<div className="col-6">
<button
type="button"
data-testid="clients_unblock_all"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => handleToggleAllServices(false)}>
@@ -65,6 +68,7 @@ export const BlockedServices = ({ services }: Props) => {
render={({ field }) => (
<ServiceField
{...field}
data-testid={`clients_service_${service.id}`}
placeholder={service.name}
disabled={useGlobalServices}
icon={service.icon_svg}

View File

@@ -31,6 +31,7 @@ export const ClientIds = () => {
<Input
{...field}
type="text"
data-testid={`clients_id_${index}`}
placeholder={t('form_enter_id')}
error={fieldState.error?.message}
onBlur={(event) => {
@@ -43,6 +44,7 @@ export const ClientIds = () => {
<span className="input-group-append">
<button
type="button"
data-testid={`clients_id_remove_${index}`}
className="btn btn-secondary btn-icon btn-icon--green"
onClick={() => remove(index)}>
<svg className="icon icon--24">
@@ -59,6 +61,7 @@ export const ClientIds = () => {
))}
<button
type="button"
data-testid="clients_id_add"
className="btn btn-link btn-block btn-sm"
onClick={() => append({ name: '' })}
title={t('form_add_id')}>

View File

@@ -64,6 +64,7 @@ export const MainSettings = ({ safeSearchServices }: Props) => {
render={({ field }) => (
<Checkbox
{...field}
data-testid={`clients_${setting.name}`}
title={setting.placeholder}
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
/>
@@ -77,7 +78,12 @@ export const MainSettings = ({ safeSearchServices }: Props) => {
name="safe_search.enabled"
control={control}
render={({ field }) => (
<Checkbox {...field} title={t('enforce_safe_search')} disabled={useGlobalSettings} />
<Checkbox
data-testid="clients_safe_search"
{...field}
title={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
)}
/>
</div>
@@ -89,7 +95,12 @@ export const MainSettings = ({ safeSearchServices }: Props) => {
name={`safe_search.${searchKey}`}
control={control}
render={({ field }) => (
<Checkbox {...field} title={captitalizeWords(searchKey)} disabled={useGlobalSettings} />
<Checkbox
{...field}
data-testid={`clients_safe_search_${searchKey}`}
title={captitalizeWords(searchKey)}
disabled={useGlobalSettings}
/>
)}
/>
</div>
@@ -104,7 +115,9 @@ export const MainSettings = ({ safeSearchServices }: Props) => {
<Controller
name={setting.name}
control={control}
render={({ field }) => <Checkbox {...field} title={setting.placeholder} />}
render={({ field }) => (
<Checkbox {...field} data-testid={`clients_${setting.name}`} title={setting.placeholder} />
)}
/>
</div>
))}

View File

@@ -8,7 +8,7 @@ import { Textarea } from '../../../../ui/Controls/Textarea';
import { ClientForm } from '../types';
import { Checkbox } from '../../../../ui/Controls/Checkbox';
import { Input } from '../../../../ui/Controls/Input';
import { trimLinesAndRemoveEmpty } from '../../../../../helpers/helpers';
import { toNumber } from '../../../../../helpers/form';
export const UpstreamDns = () => {
const { t } = useTranslation();
@@ -18,14 +18,7 @@ export const UpstreamDns = () => {
return (
<div title={t('upstream_dns')}>
<div className="form__desc mb-3">
<Trans
components={[
<a href="#dns" key="0">
link
</a>,
]}>
upstream_dns_client_desc
</Trans>
<Trans components={[<a href="#dns" key="0" />]}>upstream_dns_client_desc</Trans>
</div>
<Controller
@@ -34,13 +27,10 @@ export const UpstreamDns = () => {
render={({ field }) => (
<Textarea
{...field}
data-testid="clients_upstreams"
className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')}
onBlur={(event) => {
const normalizedValue = trimLinesAndRemoveEmpty(event.target.value);
field.onBlur();
field.onChange(normalizedValue);
}}
trimOnBlur
/>
)}
/>
@@ -53,7 +43,13 @@ export const UpstreamDns = () => {
<Controller
name="upstreams_cache_enabled"
control={control}
render={({ field }) => <Checkbox {...field} title={t('enable_upstream_dns_cache')} />}
render={({ field }) => (
<Checkbox
{...field}
data-testid="clients_upstreams_cache_enabled"
title={t('enable_upstream_dns_cache')}
/>
)}
/>
</div>
@@ -69,10 +65,15 @@ export const UpstreamDns = () => {
<Input
{...field}
type="number"
data-testid="clients_upstreams_cache_size"
placeholder={t('enter_cache_size')}
error={fieldState.error?.message}
min={0}
max={UINT32_RANGE.MAX}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>

View File

@@ -70,7 +70,6 @@ export const Form = ({
handleSubmit,
reset,
control,
setValue,
formState: { isSubmitting, isValid },
} = methods;
@@ -116,6 +115,7 @@ export const Form = ({
<Input
{...field}
type="text"
data-testid="clients_name"
placeholder={t('form_client_name')}
error={fieldState.error?.message}
onBlur={(event) => {
@@ -155,13 +155,11 @@ export const Form = ({
render={({ field }) => (
<Select
{...field}
data-testid="clients_tags"
options={tagsOptions}
className="basic-multi-select"
classNamePrefix="select"
isMulti
onChange={(selectedOptions) => {
setValue('tags', selectedOptions);
}}
/>
)}
/>

View File

@@ -27,6 +27,7 @@ import { Radio } from '../../ui/Controls/Radio';
import { Input } from '../../ui/Controls/Input';
import { Textarea } from '../../ui/Controls/Textarea';
import { EncryptionData } from '../../../initialState';
import { toNumber } from '../../../helpers/form';
const certificateSourceOptions = [
{
@@ -335,6 +336,10 @@ export const Form = ({
placeholder={t('encryption_https')}
error={fieldState.error?.message}
disabled={!isEnabled}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
@@ -362,6 +367,10 @@ export const Form = ({
placeholder={t('encryption_dot')}
error={fieldState.error?.message}
disabled={!isEnabled}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
@@ -389,6 +398,10 @@ export const Form = ({
placeholder={t('encryption_doq')}
error={fieldState.error?.message}
disabled={!isEnabled}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>

View File

@@ -5,7 +5,6 @@ import i18next from 'i18next';
import { Controller, useForm } from 'react-hook-form';
import { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
import '../FormButton.css';
import { Checkbox } from '../../ui/Controls/Checkbox';
import { Input } from '../../ui/Controls/Input';
@@ -75,11 +74,6 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
onSubmit(data);
};
const handleIgnoredBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const trimmed = trimLinesAndRemoveEmpty(e.target.value);
setValue('ignored', trimmed);
};
const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);
return (
@@ -179,7 +173,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
className="text-input"
disabled={processing}
error={fieldState.error?.message}
onBlur={handleIgnoredBlur}
trimOnBlur
/>
)}
/>

View File

@@ -15,7 +15,7 @@ type Props = {
};
export const Checkbox = forwardRef<HTMLInputElement, Props>(
({ title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange }, ref) => (
({ title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange, ...rest }, ref) => (
<>
<label className={clsx('checkbox', className)}>
<span className="checkbox__marker" />
@@ -27,6 +27,7 @@ export const Checkbox = forwardRef<HTMLInputElement, Props>(
checked={value}
onChange={(e) => onChange(e.target.checked)}
ref={ref}
{...rest}
/>
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">

View File

@@ -1,29 +1,42 @@
import React, { ComponentProps, forwardRef } from 'react';
import clsx from 'clsx';
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
type Props = ComponentProps<'textarea'> & {
className?: string;
label?: string;
error?: string;
trimOnBlur?: boolean;
};
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ name, label, className, error, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}>
{label && (
<label className="form__label" htmlFor={name}>
{label}
</label>
)}
<textarea
className={clsx(
'form-control form-control--textarea form-control--textarea-small font-monospace',
className,
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
({ name, label, className, error, trimOnBlur, onBlur, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}>
{label && (
<label className="form__label" htmlFor={name}>
{label}
</label>
)}
ref={ref}
{...rest}
/>
{error && <div className="form__message form__message--error">{error}</div>}
</div>
));
<textarea
className={clsx(
'form-control form-control--textarea form-control--textarea-small font-monospace',
className,
)}
ref={ref}
onBlur={(e) => {
if (trimOnBlur) {
const normalizedValue = trimLinesAndRemoveEmpty(e.target.value);
rest.onChange(normalizedValue);
}
if (onBlur) {
onBlur(e);
}
}}
{...rest}
/>
{error && <div className="form__message form__message--error">{error}</div>}
</div>
),
);
Textarea.displayName = 'Textarea';

View File

@@ -14,6 +14,7 @@ import {
validateIsSafePort,
} from '../../../helpers/validators';
import { Input } from '../Controls/Input';
import { Select } from '../Controls/Select';
const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => {
if (!host || invalid) {
@@ -62,7 +63,6 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
const { t } = useTranslation();
const {
register,
watch,
control,
formState: { isValid },
@@ -101,6 +101,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<Input
{...field}
type="text"
data-testid="mobile_config_host"
label={t('dhcp_table_hostname')}
placeholder={t('form_enter_hostname')}
error={fieldState.error?.message}
@@ -123,6 +124,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<Input
{...field}
type="number"
data-testid="mobile_config_port"
label={t('encryption_https')}
placeholder={t('encryption_https')}
error={fieldState.error?.message}
@@ -160,6 +162,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<Input
{...field}
type="text"
data-testid="mobile_config_client_id"
placeholder={t('client_id_placeholder')}
error={fieldState.error?.message}
/>
@@ -168,14 +171,16 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
</div>
<div className="form__group form__group--settings">
<label htmlFor="protocol" className="form__label">
{i18next.t('protocol')}
</label>
<select id="protocol" className="form-control" {...register('protocol')}>
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
</select>
<Controller
name="protocol"
control={control}
render={({ field }) => (
<Select {...field} label={t('protocol')} data-testid="mobile_config_protocol">
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
</Select>
)}
/>
</div>
</div>