refactor: improve code formatting and consistency across multiple files

- Standardized import statements and removed unnecessary line breaks for better readability in various components.
- Enhanced error handling and logging in the useElectronPrinter hook.
- Updated sample data formatting in AgGridShowcase for improved clarity.
- Refactored JSX elements for consistent indentation and structure in LandingSample, AuthPage, and EventsPage components.
- Consolidated and simplified conditional rendering logic in several components.

These changes aim to enhance code maintainability and readability throughout the project.
This commit is contained in:
shancheas
2026-08-25 17:50:48 +07:00
parent f2f0be111a
commit 67ae5b6c11
94 changed files with 1742 additions and 849 deletions
@@ -87,7 +87,11 @@ describe('FieldAsyncSelect', () => {
const { control, handleSubmit } = useForm({ defaultValues: { vendor: null } });
return (
<MantineProvider>
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
<form
onSubmit={handleSubmit((data) => {
capturedData = data;
})}
>
<FieldAsyncSelect
name="vendor"
control={control}
@@ -103,7 +107,7 @@ describe('FieldAsyncSelect', () => {
}
render(<TestForm />);
await waitFor(() => {
expect(mockLoadOptions).toHaveBeenCalled();
});
@@ -157,7 +161,7 @@ describe('FieldAsyncSelect', () => {
it('debounces search input and refetches', async () => {
const user = userEvent.setup();
const mockLoadOptions = createMockLoadOptions();
function TestForm() {
const { control } = useForm({ defaultValues: { vendor: null } });
return (
@@ -232,7 +236,7 @@ describe('FieldAsyncSelect', () => {
// Should only render "Vendor 1" once, ignoring the duplicate with id=1
const vendor1Options = screen.getAllByText('Vendor 1');
expect(vendor1Options.length).toBe(1);
// The duplicate name should NOT be rendered
expect(screen.queryByText('Vendor 1 (Duplicate)')).not.toBeInTheDocument();
});
@@ -49,11 +49,7 @@ describe('FieldCheckbox', () => {
const { control } = useForm({ defaultValues: { acceptTerms: false } });
return (
<MantineProvider>
<FieldCheckbox
name="acceptTerms"
control={control}
label="I accept the terms and conditions"
/>
<FieldCheckbox name="acceptTerms" control={control} label="I accept the terms and conditions" />
</MantineProvider>
);
}
@@ -69,11 +65,7 @@ describe('FieldCheckbox', () => {
const { control } = useForm({ defaultValues: { acceptTerms: false } });
return (
<MantineProvider>
<FieldCheckbox
name="acceptTerms"
control={control}
label="Accept Terms"
/>
<FieldCheckbox name="acceptTerms" control={control} label="Accept Terms" />
</MantineProvider>
);
}
@@ -114,10 +106,7 @@ describe('FieldCheckbox', () => {
await user.click(screen.getByText('Submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
{ acceptTerms: true },
expect.anything(),
);
expect(onSubmit).toHaveBeenCalledWith({ acceptTerms: true }, expect.anything());
});
});
@@ -70,7 +70,11 @@ describe('FieldLocalSelect', () => {
const { control, handleSubmit } = useForm({ defaultValues: { vendor: null } });
return (
<MantineProvider>
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
<form
onSubmit={handleSubmit((data) => {
capturedData = data;
})}
>
<FieldLocalSelect
name="vendor"
control={control}
@@ -86,13 +90,13 @@ describe('FieldLocalSelect', () => {
}
render(<TestForm />);
// Open dropdown
await user.click(screen.getByPlaceholderText('Select vendor'));
// Click Vendor 2
await user.click(screen.getByText('Vendor 2'));
// Submit
await user.click(screen.getByText('Submit'));
@@ -102,7 +106,7 @@ describe('FieldLocalSelect', () => {
it('renders with renderLabel for compound labels', async () => {
const user = userEvent.setup();
function TestForm() {
const { control } = useForm({ defaultValues: { vendor: null } });
return (
@@ -121,16 +125,14 @@ describe('FieldLocalSelect', () => {
render(<TestForm />);
await user.click(screen.getByPlaceholderText('Select vendor'));
// Compound label should be visible
expect(screen.getByText('V1 - Vendor 1')).toBeInTheDocument();
});
it('filterOption excludes items from dropdown', async () => {
const user = userEvent.setup();
function TestForm() {
const { control } = useForm({ defaultValues: { vendor: null } });
return (
@@ -150,7 +152,7 @@ describe('FieldLocalSelect', () => {
render(<TestForm />);
await user.click(screen.getByPlaceholderText('Select vendor'));
// Active vendors should be present
expect(screen.getByText('Vendor 1')).toBeInTheDocument();
expect(screen.getByText('Vendor 3')).toBeInTheDocument();
@@ -166,7 +168,11 @@ describe('FieldLocalSelect', () => {
const { control, handleSubmit } = useForm({ defaultValues: { vendors: [] } });
return (
<MantineProvider>
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
<form
onSubmit={handleSubmit((data) => {
capturedData = data;
})}
>
<FieldLocalSelect
multiple
name="vendors"
@@ -186,7 +192,7 @@ describe('FieldLocalSelect', () => {
await user.click(screen.getByPlaceholderText('Select vendors'));
await user.click(screen.getByText('Vendor 1'));
await user.click(screen.getByText('Vendor 3'));
await user.click(screen.getByText('Submit'));
expect(capturedData).toEqual({ vendors: [VENDORS[0], VENDORS[2]] });
});
@@ -199,7 +205,11 @@ describe('FieldLocalSelect', () => {
const { control, handleSubmit } = useForm({ defaultValues: { vendors: [VENDORS[0]] } });
return (
<MantineProvider>
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
<form
onSubmit={handleSubmit((data) => {
capturedData = data;
})}
>
<FieldLocalSelect
multiple
name="vendors"
@@ -216,8 +226,10 @@ describe('FieldLocalSelect', () => {
}
const { container } = render(<TestForm />);
const clearButton = container.querySelector('.mantine-CloseButton-root') || container.querySelector('button[aria-label="Clear value"]');
const clearButton =
container.querySelector('.mantine-CloseButton-root') ||
container.querySelector('button[aria-label="Clear value"]');
expect(clearButton).not.toBeNull();
await user.click(clearButton!);
@@ -32,10 +32,7 @@ vi.mock('@repo/core-i18n', () => ({
// ---------------------------------------------------------------------------
const loginSchema = z.object({
username: z
.string()
.min(1, 'Username cannot be empty')
.min(3, 'Username must be at least 3 characters'),
username: z.string().min(1, 'Username cannot be empty').min(3, 'Username must be at least 3 characters'),
email: z.string().email('Please enter a valid email address'),
});
@@ -57,12 +54,7 @@ describe('FieldTextInput', () => {
});
return (
<MantineProvider>
<FieldTextInput
name="username"
control={control}
label="Username"
placeholder="Enter username"
/>
<FieldTextInput name="username" control={control} label="Username" placeholder="Enter username" />
</MantineProvider>
);
}
@@ -143,10 +135,7 @@ describe('FieldTextInput', () => {
await user.click(screen.getByText('Login'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
{ username: 'john', email: 'john@example.com' },
expect.anything(),
);
expect(onSubmit).toHaveBeenCalledWith({ username: 'john', email: 'john@example.com' }, expect.anything());
});
});
@@ -177,10 +166,7 @@ describe('FieldTextInput', () => {
await user.click(screen.getByText('Login'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
{ username: 'johndoe', email: 'john@example.com' },
expect.anything(),
);
expect(onSubmit).toHaveBeenCalledWith({ username: 'johndoe', email: 'john@example.com' }, expect.anything());
});
});
});
@@ -41,11 +41,7 @@ interface FormTestWrapperProps {
onSubmit?: (data: Record<string, unknown>) => void;
}
function FormTestWrapper({
children,
defaultValues = {},
onSubmit = () => {},
}: FormTestWrapperProps) {
function FormTestWrapper({ children, defaultValues = {}, onSubmit = () => {} }: FormTestWrapperProps) {
const methods = useForm({ defaultValues });
return (
@@ -64,10 +60,7 @@ function FormTestWrapper({
// Create a test field component using the HOC
// ---------------------------------------------------------------------------
const TestFieldTextInput = withRHF<React.ComponentProps<typeof TextInput>>(
'TestFieldTextInput',
TextInput,
);
const TestFieldTextInput = withRHF<React.ComponentProps<typeof TextInput>>('TestFieldTextInput', TextInput);
// ---------------------------------------------------------------------------
// Tests
@@ -114,7 +107,11 @@ describe('withRHF HOC', () => {
const { control, handleSubmit } = useForm({ defaultValues: { email: '' } });
return (
<MantineProvider>
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
<form
onSubmit={handleSubmit((data) => {
capturedData = data;
})}
>
<TestFieldTextInput name="email" control={control} label="Email" />
<button type="submit">Submit</button>
</form>
@@ -215,16 +212,12 @@ describe('withRHF HOC', () => {
// The fallback should be the raw JSON string since neither namespace has the key.
// Our mock t() returns defaultValue when key is unknown, which is the raw JSON.
const errorElements = screen.getAllByText((content) =>
content.includes('validation.unknown_key'),
);
const errorElements = screen.getAllByText((content) => content.includes('validation.unknown_key'));
expect(errorElements.length).toBeGreaterThan(0);
});
it('has the correct displayName for React DevTools', () => {
expect(
(TestFieldTextInput as unknown as { displayName: string }).displayName,
).toBe('TestFieldTextInput');
expect((TestFieldTextInput as unknown as { displayName: string }).displayName).toBe('TestFieldTextInput');
});
it('forwards additional Mantine props (placeholder, etc.)', () => {
@@ -232,12 +225,7 @@ describe('withRHF HOC', () => {
const { control } = useForm({ defaultValues: { search: '' } });
return (
<MantineProvider>
<TestFieldTextInput
name="search"
control={control}
label="Search"
placeholder="Type to search..."
/>
<TestFieldTextInput name="search" control={control} label="Search" placeholder="Type to search..." />
</MantineProvider>
);
}
@@ -210,9 +210,7 @@ function AsyncSelectInner<T extends Record<string, any>>(props: AsyncSelectProps
// Disable Mantine's internal frontend filtering.
// The backend handles the search query, so we always display what the backend returns.
const mantineFilter = filterOption
? ({ options: opts }: any) => opts
: undefined;
const mantineFilter = filterOption ? ({ options: opts }: any) => opts : undefined;
// ----- Multi-select mode -----
if (multiple) {
@@ -25,8 +25,8 @@ type ManagedSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'filt
type ManagedMultiSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'filter' | 'name' | 'onSelect';
/** Props for single-select mode */
export type LocalSelectSingleProps<T extends Record<string, any>> =
LocalSelectBaseProps<T> & Omit<SelectProps, ManagedSelectProps> & {
export type LocalSelectSingleProps<T extends Record<string, any>> = LocalSelectBaseProps<T> &
Omit<SelectProps, ManagedSelectProps> & {
multiple?: false;
/** Controlled value — the full object or null */
value?: T | null;
@@ -35,8 +35,8 @@ export type LocalSelectSingleProps<T extends Record<string, any>> =
};
/** Props for multi-select mode */
export type LocalSelectMultiProps<T extends Record<string, any>> =
LocalSelectBaseProps<T> & Omit<MultiSelectProps, ManagedMultiSelectProps> & {
export type LocalSelectMultiProps<T extends Record<string, any>> = LocalSelectBaseProps<T> &
Omit<MultiSelectProps, ManagedMultiSelectProps> & {
multiple: true;
/** Controlled value — array of full objects */
value?: T[];
@@ -45,9 +45,7 @@ export type LocalSelectMultiProps<T extends Record<string, any>> =
};
/** Discriminated union — the component narrows based on `multiple` */
export type LocalSelectProps<T extends Record<string, any>> =
| LocalSelectSingleProps<T>
| LocalSelectMultiProps<T>;
export type LocalSelectProps<T extends Record<string, any>> = LocalSelectSingleProps<T> | LocalSelectMultiProps<T>;
// ---------------------------------------------------------------------------
// Helper: Resolve label for a data item
@@ -70,9 +68,7 @@ function resolveLabel<T extends Record<string, any>>(
// Component Implementation
// ---------------------------------------------------------------------------
function LocalSelectInner<T extends Record<string, any>>(
props: LocalSelectProps<T>,
) {
function LocalSelectInner<T extends Record<string, any>>(props: LocalSelectProps<T>) {
const {
options,
valueKey,
@@ -132,10 +128,7 @@ function LocalSelectInner<T extends Record<string, any>>(
// Passthrough filter — we handle filtering ourselves via filterOption in useMemo.
// This prevents Mantine from double-filtering.
const mantineFilter = filterOption
? ({ options: opts }: { options: ComboboxItem[] }) => opts
: undefined;
const mantineFilter = filterOption ? ({ options: opts }: { options: ComboboxItem[] }) => opts : undefined;
// ----- Multi-select mode -----
if (multiple) {
@@ -166,7 +159,7 @@ function LocalSelectInner<T extends Record<string, any>>(
const currentValue = value ? String((value as T)[valueKey]) : null;
const handleSingleChange = (val: string | null) => {
const obj = val ? lookupMap.get(val) ?? null : null;
const obj = val ? (lookupMap.get(val) ?? null) : null;
(onChange as ((v: T | null) => void) | undefined)?.(obj);
onSelectCallback?.(obj);
};
@@ -117,11 +117,7 @@ export interface LoadOptionsResponse<T> {
* };
* ```
*/
export type LoadOptionsFn<T> = (
search: string,
page: number,
prevOptions: T[],
) => Promise<LoadOptionsResponse<T>>;
export type LoadOptionsFn<T> = (search: string, page: number, prevOptions: T[]) => Promise<LoadOptionsResponse<T>>;
/**
* Internal cache entry for the search-keyed options cache.
@@ -1,10 +1,5 @@
import React from 'react';
import {
useController,
type FieldPath,
type FieldValues,
type UseControllerProps,
} from 'react-hook-form';
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
import type { SelectProps, MultiSelectProps } from '@mantine/core';
import { AsyncSelect } from '../custom/selects/AsyncSelect';
import type { AsyncSelectBaseProps, LoadOptionsFn } from '../custom/selects/types';
@@ -26,8 +21,26 @@ import { useTranslatedError } from '../useTranslatedError';
// ---------------------------------------------------------------------------
/** Mantine props we manage ourselves */
type ManagedSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
type ManagedMultiSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
type ManagedSelectProps =
| 'data'
| 'value'
| 'defaultValue'
| 'onChange'
| 'onBlur'
| 'error'
| 'filter'
| 'name'
| 'onSelect';
type ManagedMultiSelectProps =
| 'data'
| 'value'
| 'defaultValue'
| 'onChange'
| 'onBlur'
| 'error'
| 'filter'
| 'name'
| 'onSelect';
/** Async-specific props (IoC pattern) */
interface AsyncExtraProps<T> {
@@ -69,9 +82,7 @@ export type FieldAsyncSelectProps<
T extends Record<string, any>,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> =
| FieldAsyncSelectSingleProps<T, TFieldValues, TName>
| FieldAsyncSelectMultiProps<T, TFieldValues, TName>;
> = FieldAsyncSelectSingleProps<T, TFieldValues, TName> | FieldAsyncSelectMultiProps<T, TFieldValues, TName>;
// ---------------------------------------------------------------------------
// Component Implementation
@@ -149,12 +160,7 @@ function FieldAsyncSelectInner<
}
return (
<AsyncSelect<T>
{...engineProps}
value={field.value ?? null}
onChange={handleChange}
{...(mantineProps as any)}
/>
<AsyncSelect<T> {...engineProps} value={field.value ?? null} onChange={handleChange} {...(mantineProps as any)} />
);
}
@@ -1,10 +1,5 @@
import React from 'react';
import {
useController,
type FieldPath,
type FieldValues,
type UseControllerProps,
} from 'react-hook-form';
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
import type { SelectProps, MultiSelectProps } from '@mantine/core';
import { LocalSelect } from '../custom/selects/LocalSelect';
import type { LocalSelectBaseProps } from '../custom/selects/types';
@@ -28,8 +23,26 @@ import { useTranslatedError } from '../useTranslatedError';
// ---------------------------------------------------------------------------
/** Mantine props we manage ourselves */
type ManagedSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
type ManagedMultiSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
type ManagedSelectProps =
| 'data'
| 'value'
| 'defaultValue'
| 'onChange'
| 'onBlur'
| 'error'
| 'filter'
| 'name'
| 'onSelect';
type ManagedMultiSelectProps =
| 'data'
| 'value'
| 'defaultValue'
| 'onChange'
| 'onBlur'
| 'error'
| 'filter'
| 'name'
| 'onSelect';
/** Single-select RHF props — stores T | null */
export type FieldLocalSelectSingleProps<
@@ -58,9 +71,7 @@ export type FieldLocalSelectProps<
T extends Record<string, any>,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> =
| FieldLocalSelectSingleProps<T, TFieldValues, TName>
| FieldLocalSelectMultiProps<T, TFieldValues, TName>;
> = FieldLocalSelectSingleProps<T, TFieldValues, TName> | FieldLocalSelectMultiProps<T, TFieldValues, TName>;
// ---------------------------------------------------------------------------
// Component Implementation
@@ -22,17 +22,7 @@ function FieldRichTextEditorComponent<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(props: FieldRichTextEditorProps<TFieldValues, TName>) {
const {
name,
control,
rules,
shouldUnregister,
defaultValue,
disabled,
label,
description,
withAsterisk,
} = props;
const { name, control, rules, shouldUnregister, defaultValue, disabled, label, description, withAsterisk } = props;
const {
field,
@@ -71,12 +61,7 @@ function FieldRichTextEditorComponent<
}, [field.value, editor]);
return (
<Input.Wrapper
label={label}
description={description}
withAsterisk={withAsterisk}
error={translatedError}
>
<Input.Wrapper label={label} description={description} withAsterisk={withAsterisk} error={translatedError}>
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
@@ -9,8 +9,6 @@ export interface FieldSegmentedControlProps extends SegmentedControlProps {
// SegmentedControl does NOT have a native `error` prop.
// The HOC wraps it in Input.Wrapper to display validation errors.
export const FieldSegmentedControl = withRHF<FieldSegmentedControlProps>(
'FieldSegmentedControl',
SegmentedControl,
{ requiresWrapper: true },
);
export const FieldSegmentedControl = withRHF<FieldSegmentedControlProps>('FieldSegmentedControl', SegmentedControl, {
requiresWrapper: true,
});
+3 -12
View File
@@ -1,9 +1,5 @@
import type { ComponentType } from 'react';
import type {
FieldPath,
FieldValues,
UseControllerProps,
} from 'react-hook-form';
import type { FieldPath, FieldValues, UseControllerProps } from 'react-hook-form';
// ---------------------------------------------------------------------------
// Zod i18n JSON payload shape
@@ -42,8 +38,7 @@ export type WithRHFProps<
TComponentProps,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = Omit<TComponentProps, ManagedProps> &
UseControllerProps<TFieldValues, TName>;
> = Omit<TComponentProps, ManagedProps> & UseControllerProps<TFieldValues, TName>;
// ---------------------------------------------------------------------------
// Value transform — for components with non-standard value semantics
@@ -84,8 +79,4 @@ export interface WithRHFOptions {
// Utility: Extract the component's ref type for forwardRef
// ---------------------------------------------------------------------------
export type ExtractRef<T> = T extends ComponentType<infer P>
? P extends { ref?: infer R }
? R
: never
: never;
export type ExtractRef<T> = T extends ComponentType<infer P> ? (P extends { ref?: infer R } ? R : never) : never;
+2 -16
View File
@@ -1,10 +1,5 @@
import React, { type ComponentType, type Ref } from 'react';
import {
useController,
type FieldPath,
type FieldValues,
type UseControllerProps,
} from 'react-hook-form';
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
import { Input } from '@mantine/core';
import type { WithRHFOptions } from './types';
import { useTranslatedError } from './useTranslatedError';
@@ -73,16 +68,7 @@ export function withRHF<TComponentProps extends Record<string, any>>(
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(props: Props<TFieldValues, TName>) {
const {
name,
control,
rules,
shouldUnregister,
defaultValue,
disabled,
ref,
...mantineProps
} = props;
const { name, control, rules, shouldUnregister, defaultValue, disabled, ref, ...mantineProps } = props;
const {
field,
@@ -1,4 +1,4 @@
export * from './types';
export * from './utils';
export * from './page-actions';
export * from './row-actions';
export * from './row-actions';
@@ -6,16 +6,16 @@ export interface StatusPageProps {
heading?: string;
description?: string;
icon?: React.ReactNode;
showActionsBack?: boolean;
backButtonLabel?: string;
onClickGoBack?(): void;
showActionsHome?: boolean;
homeButtonLabel?: string;
onClickBackToHome?(): void;
homeUrl?: string;
height?: StyleProp<React.CSSProperties['height']>;
}
@@ -94,10 +94,13 @@ export function TableFilterDrawer({
setPreviousValues(form.getValues());
// Ensure all fields are explicitly cleared
const cleared = Object.keys(form.getValues()).reduce((acc, key) => {
acc[key] = '';
return acc;
}, {} as Record<string, unknown>);
const cleared = Object.keys(form.getValues()).reduce(
(acc, key) => {
acc[key] = '';
return acc;
},
{} as Record<string, unknown>,
);
form.reset({ ...cleared, ...(config?.defaultValues || {}) });
setHasReset(true);
+9 -24
View File
@@ -12,28 +12,20 @@ export interface UseConditionalFieldOptions<TFieldValues extends FieldValues> {
}
/**
* Automatically cleans up a conditionally rendered React Hook Form field
* Automatically cleans up a conditionally rendered React Hook Form field
* when its parent condition becomes false.
*
*
* @param options Configuration object for the conditional field behavior
*/
export function useConditionalField<TFieldValues extends FieldValues>(
options: UseConditionalFieldOptions<TFieldValues>
options: UseConditionalFieldOptions<TFieldValues>,
) {
// Destructure with default values
const {
condition,
name,
setValue,
unregister,
clearErrors,
defaultValue,
mode = 'unregister'
} = options;
const { condition, name, setValue, unregister, clearErrors, defaultValue, mode = 'unregister' } = options;
const config = options;
// Stabilize defaultValue using useRef to prevent infinite render loops
// Stabilize defaultValue using useRef to prevent infinite render loops
// if developers pass inline arrays/objects (e.g. defaultValue: [])
const defaultValueRef = useRef(defaultValue);
useEffect(() => {
@@ -49,7 +41,7 @@ export function useConditionalField<TFieldValues extends FieldValues>(
config.setValue(config.name, targetValue, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
shouldValidate: true,
});
// 2. Execute the appropriate side-effect strategy based on the active mode
@@ -59,17 +51,10 @@ export function useConditionalField<TFieldValues extends FieldValues>(
config.unregister(config.name);
} else if (mode === 'reset' && config.clearErrors) {
// Reset Mode: Keeps the field active in the DOM (e.g. cascading or disabled dependencies).
// Wipes the value and clears active validation errors so the user can interact
// Wipes the value and clears active validation errors so the user can interact
// with a fresh state, but keeps the property in the payload.
config.clearErrors(config.name);
}
}
}, [
condition,
name,
setValue,
unregister,
clearErrors,
mode
]);
}
}, [condition, name, setValue, unregister, clearErrors, mode]);
}
+3 -3
View File
@@ -1,7 +1,7 @@
export const radius = {
xs: '0.125rem', // 2px
sm: '0.25rem', // 4px
sm: '0.25rem', // 4px
md: '0.375rem', // 6px (Input radius)
lg: '0.5rem', // 8px
xl: '0.75rem', // 12px
lg: '0.5rem', // 8px
xl: '0.75rem', // 12px
};
@@ -22,7 +22,7 @@ describe('Validator Registry', () => {
const schema = compose(z.string(), required('Password'), complexPassword(8));
const res = schema.safeParse('Weak');
expect(res.success).toBe(false);
const res2 = schema.safeParse('StrongPass1!');
expect(res2.success).toBe(true);
});
@@ -34,7 +34,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse('');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:required', values: { field: 'TestField' } })
JSON.stringify({ key: 'validation:required', values: { field: 'TestField' } }),
);
});
@@ -42,9 +42,7 @@ describe('Validator Registry', () => {
const schema = compose(z.string(), emailValidator());
const res = schema.safeParse('invalid-email');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:invalid_email' })
);
expect(res.error?.issues[0].message).toBe(JSON.stringify({ key: 'validation:invalid_email' }));
});
});
@@ -54,7 +52,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse(5);
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } })
JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } }),
);
});
@@ -63,7 +61,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse(105);
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:max_val', values: { max: 100, field: 'Percentage' } })
JSON.stringify({ key: 'validation:max_val', values: { max: 100, field: 'Percentage' } }),
);
});
@@ -72,7 +70,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse(5);
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:range_val', values: { min: 10, max: 20, field: 'Range' } })
JSON.stringify({ key: 'validation:range_val', values: { min: 10, max: 20, field: 'Range' } }),
);
});
@@ -81,7 +79,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse(-5);
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:must_be_positive', values: { field: 'Amount' } })
JSON.stringify({ key: 'validation:must_be_positive', values: { field: 'Amount' } }),
);
});
});
@@ -92,7 +90,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse('abc');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:min_len', values: { min: 5, field: 'Username' } })
JSON.stringify({ key: 'validation:min_len', values: { min: 5, field: 'Username' } }),
);
});
@@ -101,7 +99,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse('thisisaverylongusername');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:max_len', values: { max: 10, field: 'Username' } })
JSON.stringify({ key: 'validation:max_len', values: { max: 10, field: 'Username' } }),
);
});
@@ -110,7 +108,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse('ab');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:range_len', values: { min: 3, max: 5, field: 'Code' } })
JSON.stringify({ key: 'validation:range_len', values: { min: 3, max: 5, field: 'Code' } }),
);
});
});
@@ -121,7 +119,7 @@ describe('Validator Registry', () => {
const res = schema.safeParse('short');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:invalid_password_simple', values: { min: 6 } })
JSON.stringify({ key: 'validation:invalid_password_simple', values: { min: 6 } }),
);
expect(schema.safeParse('longenough').success).toBe(true);
});
@@ -131,7 +129,7 @@ describe('Validator Registry', () => {
expect(schema.safeParse('weakpassword').success).toBe(false);
expect(schema.safeParse('NoSpecial1').success).toBe(false);
expect(schema.safeParse('ValidPass1!').success).toBe(true);
const res = schema.safeParse('short');
expect(res.success).toBe(false);
});
@@ -142,12 +140,10 @@ describe('Validator Registry', () => {
const schema = compose(z.string(), phoneValidator());
expect(schema.safeParse('08123456789').success).toBe(false);
expect(schema.safeParse('+628123456789').success).toBe(true);
const res = schema.safeParse('invalid');
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:invalid_phone' })
);
expect(res.error?.issues[0].message).toBe(JSON.stringify({ key: 'validation:invalid_phone' }));
});
});
});
@@ -2,10 +2,7 @@ import type { ZodString, ZodNumber, ZodTypeAny } from 'zod';
// ─── UTILITIES ─────────────────────────────────────────────────────────────
export const compose = <T extends ZodTypeAny>(
base: T,
...modifiers: ((schema: any) => any)[]
): any => {
export const compose = <T extends ZodTypeAny>(base: T, ...modifiers: ((schema: any) => any)[]): any => {
return modifiers.reduce((acc, curr) => curr(acc), base);
};
@@ -71,20 +68,24 @@ export const rangeLength = (min: number, max: number, field?: string) => (schema
// ─── SECURITY ──────────────────────────────────────────────────────────────
export const simplePassword = (min: number = 8) => (schema: ZodString) => {
return schema.min(min, {
message: JSON.stringify({ key: 'validation:invalid_password_simple', values: { min } }),
});
};
export const simplePassword =
(min: number = 8) =>
(schema: ZodString) => {
return schema.min(min, {
message: JSON.stringify({ key: 'validation:invalid_password_simple', values: { min } }),
});
};
export const complexPassword = (min: number = 8) => (schema: ZodString) => {
return schema
.min(min, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[A-Z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[a-z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[^A-Za-z0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) });
};
export const complexPassword =
(min: number = 8) =>
(schema: ZodString) => {
return schema
.min(min, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[A-Z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[a-z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
.regex(/[^A-Za-z0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) });
};
// ─── TECHNICAL ─────────────────────────────────────────────────────────────