refactor: memoize Zod schemas and internationalize form labels and messages in demo components
This commit is contained in:
+32
-32
@@ -109,7 +109,7 @@ export default function AllFieldsDemo() {
|
|||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
{/* --- Text & Numbers --- */}
|
{/* --- Text & Numbers --- */}
|
||||||
<div>
|
<div>
|
||||||
<Title order={5} mb="sm" c="brand">Text & Numbers</Title>
|
<Title order={5} mb="sm" c="brand">{t.sections.textAndNumbers}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<Group grow align="flex-start" mb="md">
|
<Group grow align="flex-start" mb="md">
|
||||||
<FieldTextInput name="customerName" control={control} label={t.fields.customerName} />
|
<FieldTextInput name="customerName" control={control} label={t.fields.customerName} />
|
||||||
@@ -131,7 +131,7 @@ export default function AllFieldsDemo() {
|
|||||||
|
|
||||||
{/* --- Selections --- */}
|
{/* --- Selections --- */}
|
||||||
<div>
|
<div>
|
||||||
<Title order={5} mb="sm" c="brand">Selections</Title>
|
<Title order={5} mb="sm" c="brand">{t.sections.selections}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<Group grow align="flex-start" mb="md">
|
<Group grow align="flex-start" mb="md">
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
@@ -151,13 +151,13 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldAutocomplete
|
<FieldAutocomplete
|
||||||
name="country"
|
name="country"
|
||||||
control={control}
|
control={control}
|
||||||
label="Country"
|
label={t.fields.country}
|
||||||
data={['Indonesia', 'Singapore', 'Malaysia']}
|
data={['Indonesia', 'Singapore', 'Malaysia']}
|
||||||
/>
|
/>
|
||||||
<FieldMultiSelect
|
<FieldMultiSelect
|
||||||
name="categories"
|
name="categories"
|
||||||
control={control}
|
control={control}
|
||||||
label="Categories"
|
label={t.fields.categories}
|
||||||
data={['Electronics', 'Fashion', 'Food']}
|
data={['Electronics', 'Fashion', 'Food']}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -165,8 +165,8 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldLocalSelect
|
<FieldLocalSelect
|
||||||
name="localSelect"
|
name="localSelect"
|
||||||
control={control}
|
control={control}
|
||||||
label="Local Select"
|
label={t.fields.localSelect}
|
||||||
placeholder="Select a complex object"
|
placeholder={t.placeholders.selectComplexObject}
|
||||||
options={[
|
options={[
|
||||||
{ id: 1, name: 'Apple', type: 'Fruit' },
|
{ id: 1, name: 'Apple', type: 'Fruit' },
|
||||||
{ id: 2, name: 'Carrot', type: 'Vegetable' },
|
{ id: 2, name: 'Carrot', type: 'Vegetable' },
|
||||||
@@ -180,8 +180,8 @@ export default function AllFieldsDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="multiLocalSelect"
|
name="multiLocalSelect"
|
||||||
control={control}
|
control={control}
|
||||||
label="Multi Local Select"
|
label={t.fields.multiLocalSelect}
|
||||||
placeholder="Select multiple objects"
|
placeholder={t.placeholders.selectMultipleObjects}
|
||||||
options={[
|
options={[
|
||||||
{ id: 1, name: 'Red', hex: '#f00' },
|
{ id: 1, name: 'Red', hex: '#f00' },
|
||||||
{ id: 2, name: 'Green', hex: '#0f0' },
|
{ id: 2, name: 'Green', hex: '#0f0' },
|
||||||
@@ -196,8 +196,8 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldAsyncSelect
|
<FieldAsyncSelect
|
||||||
name="asyncSelect"
|
name="asyncSelect"
|
||||||
control={control}
|
control={control}
|
||||||
label="Async Select (Mock API)"
|
label={t.fields.asyncSelectMock}
|
||||||
placeholder="Search pokemon..."
|
placeholder={t.placeholders.searchPokemon}
|
||||||
loadOptions={loadMockPokemonOptions}
|
loadOptions={loadMockPokemonOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
@@ -207,8 +207,8 @@ export default function AllFieldsDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="multiAsyncSelect"
|
name="multiAsyncSelect"
|
||||||
control={control}
|
control={control}
|
||||||
label="Multi Async Select"
|
label={t.fields.multiAsyncSelect}
|
||||||
placeholder="Select multiple pokemon..."
|
placeholder={t.placeholders.selectMultiplePokemon}
|
||||||
loadOptions={loadMockPokemonOptions}
|
loadOptions={loadMockPokemonOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
@@ -219,8 +219,8 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldAsyncSelect
|
<FieldAsyncSelect
|
||||||
name="realPokeSelect"
|
name="realPokeSelect"
|
||||||
control={control}
|
control={control}
|
||||||
label="Real PokeAPI (Single - Tests Deduplication)"
|
label={t.fields.realPokeSingle}
|
||||||
placeholder="Scroll to test deduplication..."
|
placeholder={t.placeholders.scrollDeduplication}
|
||||||
loadOptions={loadRealPokemonOptions}
|
loadOptions={loadRealPokemonOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
@@ -230,8 +230,8 @@ export default function AllFieldsDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="multiRealPokeSelect"
|
name="multiRealPokeSelect"
|
||||||
control={control}
|
control={control}
|
||||||
label="Real PokeAPI (Multi - Tests Deduplication)"
|
label={t.fields.realPokeMulti}
|
||||||
placeholder="Scroll to test deduplication..."
|
placeholder={t.placeholders.scrollDeduplication}
|
||||||
loadOptions={loadRealPokemonOptions}
|
loadOptions={loadRealPokemonOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
@@ -240,13 +240,13 @@ export default function AllFieldsDemo() {
|
|||||||
</Group>
|
</Group>
|
||||||
<FieldTagsInput name="tags" control={control} label={t.fields.tags} />
|
<FieldTagsInput name="tags" control={control} label={t.fields.tags} />
|
||||||
|
|
||||||
<Title order={5} mb="sm" mt="lg" c="brand">Advanced Object Selects (Custom Labels & Default Values)</Title>
|
<Title order={5} mb="sm" mt="lg" c="brand">{t.sections.advancedObjectSelects}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<Group grow align="flex-start" mb="md">
|
<Group grow align="flex-start" mb="md">
|
||||||
<FieldLocalSelect
|
<FieldLocalSelect
|
||||||
name="localSelectEmpty"
|
name="localSelectEmpty"
|
||||||
control={control}
|
control={control}
|
||||||
label="Local Empty"
|
label={t.fields.localEmpty}
|
||||||
options={MOCK_VENDORS}
|
options={MOCK_VENDORS}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -255,7 +255,7 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldLocalSelect
|
<FieldLocalSelect
|
||||||
name="localSelectPrefilled"
|
name="localSelectPrefilled"
|
||||||
control={control}
|
control={control}
|
||||||
label="Local Prefilled"
|
label={t.fields.localPrefilled}
|
||||||
options={MOCK_VENDORS}
|
options={MOCK_VENDORS}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -266,7 +266,7 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldAsyncSelect
|
<FieldAsyncSelect
|
||||||
name="asyncSelectEmpty"
|
name="asyncSelectEmpty"
|
||||||
control={control}
|
control={control}
|
||||||
label="Async Empty"
|
label={t.fields.asyncEmpty}
|
||||||
loadOptions={loadMockVendorsOptions}
|
loadOptions={loadMockVendorsOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -275,7 +275,7 @@ export default function AllFieldsDemo() {
|
|||||||
<FieldAsyncSelect
|
<FieldAsyncSelect
|
||||||
name="asyncSelectPrefilled"
|
name="asyncSelectPrefilled"
|
||||||
control={control}
|
control={control}
|
||||||
label="Async Prefilled (Edit Mode)"
|
label={t.fields.asyncPrefilled}
|
||||||
loadOptions={loadMockVendorsOptions}
|
loadOptions={loadMockVendorsOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -284,14 +284,14 @@ export default function AllFieldsDemo() {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Title order={5} mb="sm" mt="lg" c="brand">Multi-Select Edit Mode (No defaultOptions fallback)</Title>
|
<Title order={5} mb="sm" mt="lg" c="brand">{t.sections.multiSelectEditMode}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<Group grow align="flex-start" mb="md">
|
<Group grow align="flex-start" mb="md">
|
||||||
<FieldLocalSelect
|
<FieldLocalSelect
|
||||||
multiple
|
multiple
|
||||||
name="localMultiPrefilled"
|
name="localMultiPrefilled"
|
||||||
control={control}
|
control={control}
|
||||||
label="Local Multi Prefilled"
|
label={t.fields.localMultiPrefilled}
|
||||||
options={MOCK_VENDORS}
|
options={MOCK_VENDORS}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -301,7 +301,7 @@ export default function AllFieldsDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="asyncMultiPrefilled"
|
name="asyncMultiPrefilled"
|
||||||
control={control}
|
control={control}
|
||||||
label="Async Multi Prefilled (Ghost Items)"
|
label={t.fields.asyncMultiPrefilled}
|
||||||
loadOptions={loadMockVendorsOptions}
|
loadOptions={loadMockVendorsOptions}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -309,27 +309,27 @@ export default function AllFieldsDemo() {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Title order={5} mb="sm" mt="lg" c="brand">Rich Text Editor (TipTap)</Title>
|
<Title order={5} mb="sm" mt="lg" c="brand">{t.sections.richTextEditor}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<FieldRichTextEditor
|
<FieldRichTextEditor
|
||||||
name="richTextEmpty"
|
name="richTextEmpty"
|
||||||
control={control}
|
control={control}
|
||||||
label="Rich Text (Empty)"
|
label={t.fields.richTextEmpty}
|
||||||
description="A fresh TipTap editor instance"
|
description={t.descriptions.freshTipTap}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: '16px' }}>
|
<div style={{ marginTop: '16px' }}>
|
||||||
<FieldRichTextEditor
|
<FieldRichTextEditor
|
||||||
name="richTextPrefilled"
|
name="richTextPrefilled"
|
||||||
control={control}
|
control={control}
|
||||||
label="Rich Text (Prefilled / Edit Mode)"
|
label={t.fields.richTextPrefilled}
|
||||||
description="HTML string successfully loaded from default values"
|
description={t.descriptions.htmlStringLoaded}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- Toggles & Choices --- */}
|
{/* --- Toggles & Choices --- */}
|
||||||
<div>
|
<div>
|
||||||
<Title order={5} mb="sm" c="brand">Toggles & Choices</Title>
|
<Title order={5} mb="sm" c="brand">{t.sections.togglesAndChoices}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<FieldCheckbox name="terms" control={control} label={t.fields.terms} />
|
<FieldCheckbox name="terms" control={control} label={t.fields.terms} />
|
||||||
@@ -372,11 +372,11 @@ export default function AllFieldsDemo() {
|
|||||||
|
|
||||||
{/* --- Ranges & Specialized --- */}
|
{/* --- Ranges & Specialized --- */}
|
||||||
<div>
|
<div>
|
||||||
<Title order={5} mb="sm" c="brand">Ranges & Specialized</Title>
|
<Title order={5} mb="sm" c="brand">{t.sections.rangesAndSpecialized}</Title>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
<Group grow align="flex-start" mb="md">
|
<Group grow align="flex-start" mb="md">
|
||||||
<FieldSlider name="satisfaction" control={control} label={t.fields.rating} />
|
<FieldSlider name="satisfaction" control={control} label={t.fields.rating} />
|
||||||
<FieldRangeSlider name="priceRange" control={control} label="Price Range" />
|
<FieldRangeSlider name="priceRange" control={control} label={t.fields.priceRange} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group grow align="flex-start" mb="md">
|
<Group grow align="flex-start" mb="md">
|
||||||
<FieldColorInput name="themeColor" control={control} label={t.fields.themeColor} />
|
<FieldColorInput name="themeColor" control={control} label={t.fields.themeColor} />
|
||||||
|
|||||||
+19
-19
@@ -7,7 +7,7 @@ import { useConditionalField } from '@repo/ui/hooks';
|
|||||||
import { compose, required, emailValidator } from '@repo/ui/validators';
|
import { compose, required, emailValidator } from '@repo/ui/validators';
|
||||||
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
|
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
import { useEffect, useCallback, useRef } from 'react';
|
import { useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
|
|
||||||
interface Region {
|
interface Region {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -52,10 +52,10 @@ export default function ReactiveWatchDemo() {
|
|||||||
// Define atomic validators for conditional fields
|
// Define atomic validators for conditional fields
|
||||||
const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId));
|
const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId));
|
||||||
const spouseNameValidator = compose(z.string(), required(t.watch.spouseName));
|
const spouseNameValidator = compose(z.string(), required(t.watch.spouseName));
|
||||||
const newsletterEmailValidator = compose(z.string(), required('Newsletter Email'), emailValidator());
|
const newsletterEmailValidator = compose(z.string(), required(t.fields.email), emailValidator());
|
||||||
const roleValidator = compose(z.string(), required('Role'));
|
const roleValidator = compose(z.string(), required(t.fields.role));
|
||||||
|
|
||||||
const reactiveSchema = z
|
const reactiveSchema = useMemo(() => z
|
||||||
.object({
|
.object({
|
||||||
userType: z.enum(['PERSONAL', 'CORPORATE']),
|
userType: z.enum(['PERSONAL', 'CORPORATE']),
|
||||||
corporateTaxId: z.string().optional(),
|
corporateTaxId: z.string().optional(),
|
||||||
@@ -92,7 +92,7 @@ export default function ReactiveWatchDemo() {
|
|||||||
z.object({ department: z.string().min(1), role: roleValidator }),
|
z.object({ department: z.string().min(1), role: roleValidator }),
|
||||||
z.object({ department: z.union([z.literal(''), z.undefined(), z.null()]).optional() }),
|
z.object({ department: z.union([z.literal(''), z.undefined(), z.null()]).optional() }),
|
||||||
]),
|
]),
|
||||||
);
|
), [t, taxIdValidator, spouseNameValidator, newsletterEmailValidator, roleValidator]);
|
||||||
|
|
||||||
const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({
|
const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({
|
||||||
resolver: zodResolver(reactiveSchema as any),
|
resolver: zodResolver(reactiveSchema as any),
|
||||||
@@ -214,7 +214,7 @@ export default function ReactiveWatchDemo() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={5} mb="sm" c="brand">
|
<Title order={5} mb="sm" c="brand">
|
||||||
Dynamic Fields & Validation
|
{t.sections.reactiveWatchCascading}
|
||||||
</Title>
|
</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
@@ -264,8 +264,8 @@ export default function ReactiveWatchDemo() {
|
|||||||
<FieldSelect
|
<FieldSelect
|
||||||
name="department"
|
name="department"
|
||||||
control={control}
|
control={control}
|
||||||
label="Department"
|
label={t.fields.department}
|
||||||
placeholder="Select a department"
|
placeholder={t.placeholders.selectComplexObject}
|
||||||
data={[
|
data={[
|
||||||
{ value: 'IT', label: 'Information Technology' },
|
{ value: 'IT', label: 'Information Technology' },
|
||||||
{ value: 'HR', label: 'Human Resources' },
|
{ value: 'HR', label: 'Human Resources' },
|
||||||
@@ -286,15 +286,15 @@ export default function ReactiveWatchDemo() {
|
|||||||
key={`role-select-${department}`}
|
key={`role-select-${department}`}
|
||||||
name="role"
|
name="role"
|
||||||
control={control}
|
control={control}
|
||||||
label="Role"
|
label={t.fields.role}
|
||||||
placeholder="Select a role"
|
placeholder={t.placeholders.selectComplexObject}
|
||||||
disabled={!department}
|
disabled={!department}
|
||||||
data={currentRoleOptions}
|
data={currentRoleOptions}
|
||||||
withAsterisk={!!department}
|
withAsterisk={!!department}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Title order={5} mb="sm" c="brand" mt="lg">
|
<Title order={5} mb="sm" c="brand" mt="lg">
|
||||||
Cascading Object Selects
|
{t.sections.reactiveWatchCascading}
|
||||||
</Title>
|
</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ export default function ReactiveWatchDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="regions"
|
name="regions"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Regions"
|
label={t.fields.regions}
|
||||||
options={REGIONS}
|
options={REGIONS}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="code"
|
labelKey="code"
|
||||||
@@ -314,7 +314,7 @@ export default function ReactiveWatchDemo() {
|
|||||||
key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`}
|
key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`}
|
||||||
name="warehouses"
|
name="warehouses"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Warehouses"
|
label={t.fields.warehouses}
|
||||||
disabled={!regions || regions.length === 0}
|
disabled={!regions || regions.length === 0}
|
||||||
loadOptions={useCallback(async (search, page) => {
|
loadOptions={useCallback(async (search, page) => {
|
||||||
if (!regions || regions.length === 0) return { options: [], hasMore: false };
|
if (!regions || regions.length === 0) return { options: [], hasMore: false };
|
||||||
@@ -327,31 +327,31 @@ export default function ReactiveWatchDemo() {
|
|||||||
|
|
||||||
{regions && regions.length > 0 && (
|
{regions && regions.length > 0 && (
|
||||||
<Alert mt="sm" color="teal">
|
<Alert mt="sm" color="teal">
|
||||||
Selected regions tax rates: {regions.map((r: any) => `${r.code} (${(r.taxRate * 100).toFixed(0)}%)`).join(', ')}
|
{t.descriptions.selectedRegionsTax} {regions.map((r: any) => `${r.code} (${(r.taxRate * 100).toFixed(0)}%)`).join(', ')}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Title order={5} mb="sm" c="brand" mt="lg">
|
<Title order={5} mb="sm" c="brand" mt="lg">
|
||||||
Reactive Rich Text Preview
|
{t.sections.reactiveRichTextPreview}
|
||||||
</Title>
|
</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
<FieldRichTextEditor
|
<FieldRichTextEditor
|
||||||
name="richTextLive"
|
name="richTextLive"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Live Editor"
|
label={t.fields.liveEditor}
|
||||||
description="Type to see instantaneous reactive rendering below"
|
description={t.descriptions.typeToSeePreview}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Paper p="md" withBorder radius="md" mt="sm">
|
<Paper p="md" withBorder radius="md" mt="sm">
|
||||||
<Title order={6} mb="xs">Live HTML Preview Render</Title>
|
<Title order={6} mb="xs">{t.sections.liveHtmlPreview}</Title>
|
||||||
<TypographyStylesProvider>
|
<TypographyStylesProvider>
|
||||||
<div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} />
|
<div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} />
|
||||||
</TypographyStylesProvider>
|
</TypographyStylesProvider>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Button type="submit" mt="md">
|
<Button type="submit" mt="md">
|
||||||
{t.common?.submit || 'Submit Reactive Form'}
|
{t.common.submitReactive}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+24
-23
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -63,20 +64,20 @@ export default function ValidationBankDemo() {
|
|||||||
const t = useFormDemoTranslation();
|
const t = useFormDemoTranslation();
|
||||||
|
|
||||||
// Compose the Zod schema using the atomic validators
|
// Compose the Zod schema using the atomic validators
|
||||||
const validationSchema = z.object({
|
const validationSchema = useMemo(() => z.object({
|
||||||
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
|
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
|
||||||
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
|
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
|
||||||
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
|
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
|
||||||
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
|
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
|
||||||
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
|
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
|
||||||
phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
|
phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
|
||||||
department: z.object({ code: z.string(), name: z.string() }, { required_error: 'Department is required' }),
|
department: z.object({ code: z.string(), name: z.string() }, { required_error: t.errors.departmentRequired }),
|
||||||
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, "Select at least 2 assignees"),
|
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, t.errors.min2Assignees),
|
||||||
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }),
|
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
|
||||||
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }),
|
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
|
||||||
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, "Select at least 1 vendor"),
|
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, t.errors.min1Vendor),
|
||||||
richTextNotes: z.string().min(15, "Notes must be at least 15 characters long (including HTML tags)"),
|
richTextNotes: z.string().min(15, t.errors.notesMin15),
|
||||||
});
|
}), [t]);
|
||||||
|
|
||||||
type ValidationFormValues = z.infer<typeof validationSchema>;
|
type ValidationFormValues = z.infer<typeof validationSchema>;
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ export default function ValidationBankDemo() {
|
|||||||
<Paper p="xl" withBorder radius="md">
|
<Paper p="xl" withBorder radius="md">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={5} c="brand">Validation Bank (Atomic Registry)</Title>
|
<Title order={5} c="brand">{t.sections.validationBankTitle}</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
<FieldTextInput
|
<FieldTextInput
|
||||||
@@ -125,14 +126,14 @@ export default function ValidationBankDemo() {
|
|||||||
name="simplePass"
|
name="simplePass"
|
||||||
control={control}
|
control={control}
|
||||||
label={t.validation.simplePassword}
|
label={t.validation.simplePassword}
|
||||||
description="Min 6 chars"
|
description={t.descriptions.min6Chars}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
<FieldPasswordInput
|
<FieldPasswordInput
|
||||||
name="complexPass"
|
name="complexPass"
|
||||||
control={control}
|
control={control}
|
||||||
label={t.validation.complexPassword}
|
label={t.validation.complexPassword}
|
||||||
description="Min 8, 1 uppercase, 1 number, 1 special"
|
description={t.descriptions.min8Complex}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -149,7 +150,7 @@ export default function ValidationBankDemo() {
|
|||||||
name="score"
|
name="score"
|
||||||
control={control}
|
control={control}
|
||||||
label={t.validation.score}
|
label={t.validation.score}
|
||||||
description="Must be > 0"
|
description={t.descriptions.mustBePositive}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -158,17 +159,17 @@ export default function ValidationBankDemo() {
|
|||||||
name="phone"
|
name="phone"
|
||||||
control={control}
|
control={control}
|
||||||
label={t.validation.phone}
|
label={t.validation.phone}
|
||||||
description="Format: +62..."
|
description={t.descriptions.formatPhone}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Title order={5} c="brand" mt="md">Object Level Validations (Local & Async)</Title>
|
<Title order={5} c="brand" mt="md">{t.sections.objectLevelValidations}</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
<FieldLocalSelect<Department>
|
<FieldLocalSelect<Department>
|
||||||
name="department"
|
name="department"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Department"
|
label={t.fields.department}
|
||||||
options={MOCK_DEPARTMENTS}
|
options={MOCK_DEPARTMENTS}
|
||||||
valueKey="code"
|
valueKey="code"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -180,7 +181,7 @@ export default function ValidationBankDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="assignees"
|
name="assignees"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Assignees"
|
label={t.fields.assignees}
|
||||||
loadOptions={mockFetchUsers}
|
loadOptions={mockFetchUsers}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
labelKey="email"
|
labelKey="email"
|
||||||
@@ -189,14 +190,14 @@ export default function ValidationBankDemo() {
|
|||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Title order={5} c="brand" mt="lg">Validated Prefilled Objects</Title>
|
<Title order={5} c="brand" mt="lg">{t.sections.validatedPrefilledObjects}</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
<Group grow align="flex-start">
|
<Group grow align="flex-start">
|
||||||
<FieldAsyncSelect
|
<FieldAsyncSelect
|
||||||
name="emptyVendor"
|
name="emptyVendor"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Empty Vendor"
|
label={t.fields.emptyVendor}
|
||||||
loadOptions={mockFetchVendors}
|
loadOptions={mockFetchVendors}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -206,7 +207,7 @@ export default function ValidationBankDemo() {
|
|||||||
<FieldAsyncSelect
|
<FieldAsyncSelect
|
||||||
name="prefilledVendor"
|
name="prefilledVendor"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Prefilled Vendor"
|
label={t.fields.prefilledVendor}
|
||||||
loadOptions={mockFetchVendors}
|
loadOptions={mockFetchVendors}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -220,7 +221,7 @@ export default function ValidationBankDemo() {
|
|||||||
multiple
|
multiple
|
||||||
name="prefilledAsyncMulti"
|
name="prefilledAsyncMulti"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Prefilled Async Multi (No Fallback)"
|
label={t.fields.prefilledAsyncMulti}
|
||||||
loadOptions={mockFetchVendors}
|
loadOptions={mockFetchVendors}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||||
@@ -228,14 +229,14 @@ export default function ValidationBankDemo() {
|
|||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Title order={5} c="brand" mt="lg">Rich Text Editor Validations</Title>
|
<Title order={5} c="brand" mt="lg">{t.sections.richTextValidations}</Title>
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
|
|
||||||
<FieldRichTextEditor
|
<FieldRichTextEditor
|
||||||
name="richTextNotes"
|
name="richTextNotes"
|
||||||
control={control as any}
|
control={control as any}
|
||||||
label="Important Notes"
|
label={t.fields.importantNotes}
|
||||||
description="This uses Zod minimum length string validation"
|
description={t.descriptions.zodMinLengthString}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,26 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"submit": "Submit Data",
|
"submit": "Submit Data",
|
||||||
|
"submitReactive": "Submit Reactive Form",
|
||||||
"reset": "Reset Form",
|
"reset": "Reset Form",
|
||||||
"submittedData": "Submitted Data"
|
"submittedData": "Submitted Data"
|
||||||
},
|
},
|
||||||
|
"sections": {
|
||||||
|
"validationBankTitle": "Validation Bank (Atomic Registry)",
|
||||||
|
"objectLevelValidations": "Object Level Validations (Local & Async)",
|
||||||
|
"validatedPrefilledObjects": "Validated Prefilled Objects",
|
||||||
|
"richTextValidations": "Rich Text Editor Validations",
|
||||||
|
"textAndNumbers": "Text & Numbers",
|
||||||
|
"selections": "Selections",
|
||||||
|
"advancedObjectSelects": "Advanced Object Selects (Custom Labels & Default Values)",
|
||||||
|
"multiSelectEditMode": "Multi-Select Edit Mode (No defaultOptions fallback)",
|
||||||
|
"richTextEditor": "Rich Text Editor (TipTap)",
|
||||||
|
"togglesAndChoices": "Toggles & Choices",
|
||||||
|
"rangesAndSpecialized": "Ranges & Specialized",
|
||||||
|
"reactiveWatchCascading": "Reactive Watch (Cascading)",
|
||||||
|
"reactiveRichTextPreview": "Reactive Rich Text Preview",
|
||||||
|
"liveHtmlPreview": "Live HTML Preview Render"
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"customerName": "Customer Name",
|
"customerName": "Customer Name",
|
||||||
"email": "Email Address",
|
"email": "Email Address",
|
||||||
@@ -26,7 +43,60 @@
|
|||||||
"orderType": "Order Type",
|
"orderType": "Order Type",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"fabricColor": "Fabric Color",
|
"fabricColor": "Fabric Color",
|
||||||
"pin": "Security PIN"
|
"pin": "Security PIN",
|
||||||
|
"department": "Department",
|
||||||
|
"assignees": "Assignees",
|
||||||
|
"emptyVendor": "Empty Vendor",
|
||||||
|
"prefilledVendor": "Prefilled Vendor",
|
||||||
|
"prefilledAsyncMulti": "Prefilled Async Multi (No Fallback)",
|
||||||
|
"importantNotes": "Important Notes",
|
||||||
|
"country": "Country",
|
||||||
|
"categories": "Categories",
|
||||||
|
"localSelect": "Local Select",
|
||||||
|
"multiLocalSelect": "Multi Local Select",
|
||||||
|
"asyncSelectMock": "Async Select (Mock API)",
|
||||||
|
"multiAsyncSelect": "Multi Async Select",
|
||||||
|
"realPokeSingle": "Real PokeAPI (Single - Tests Deduplication)",
|
||||||
|
"realPokeMulti": "Real PokeAPI (Multi - Tests Deduplication)",
|
||||||
|
"localEmpty": "Local Empty",
|
||||||
|
"localPrefilled": "Local Prefilled",
|
||||||
|
"asyncEmpty": "Async Empty",
|
||||||
|
"asyncPrefilled": "Async Prefilled (Edit Mode)",
|
||||||
|
"localMultiPrefilled": "Local Multi Prefilled",
|
||||||
|
"asyncMultiPrefilled": "Async Multi Prefilled (Ghost Items)",
|
||||||
|
"richTextEmpty": "Rich Text (Empty)",
|
||||||
|
"richTextPrefilled": "Rich Text (Prefilled / Edit Mode)",
|
||||||
|
"priceRange": "Price Range",
|
||||||
|
"role": "Role",
|
||||||
|
"regions": "Regions",
|
||||||
|
"warehouses": "Warehouses",
|
||||||
|
"liveEditor": "Live Editor"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"selectComplexObject": "Select a complex object",
|
||||||
|
"selectMultipleObjects": "Select multiple objects",
|
||||||
|
"searchPokemon": "Search pokemon...",
|
||||||
|
"selectMultiplePokemon": "Select multiple pokemon...",
|
||||||
|
"scrollDeduplication": "Scroll to test deduplication..."
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"min6Chars": "Min 6 chars",
|
||||||
|
"min8Complex": "Min 8, 1 uppercase, 1 number, 1 special",
|
||||||
|
"mustBePositive": "Must be > 0",
|
||||||
|
"formatPhone": "Format: +62...",
|
||||||
|
"zodMinLengthString": "This uses Zod minimum length string validation",
|
||||||
|
"freshTipTap": "A fresh TipTap editor instance",
|
||||||
|
"htmlStringLoaded": "HTML string successfully loaded from default values",
|
||||||
|
"typeToSeePreview": "Type to see instantaneous reactive rendering below",
|
||||||
|
"selectedRegionsTax": "Selected regions tax rates:"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"departmentRequired": "Department is required",
|
||||||
|
"vendorRequired": "Vendor is required",
|
||||||
|
"min2Assignees": "Select at least 2 assignees",
|
||||||
|
"min1Vendor": "Select at least 1 vendor",
|
||||||
|
"notesMin15": "Notes must be at least 15 characters long (including HTML tags)",
|
||||||
|
"selectRegionFirst": "Select a region first to load warehouses"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"simplePassword": "Simple Password",
|
"simplePassword": "Simple Password",
|
||||||
|
|||||||
@@ -6,9 +6,26 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"submit": "Kirim Data",
|
"submit": "Kirim Data",
|
||||||
|
"submitReactive": "Kirim Form Reaktif",
|
||||||
"reset": "Reset Form",
|
"reset": "Reset Form",
|
||||||
"submittedData": "Data Terkirim"
|
"submittedData": "Data Terkirim"
|
||||||
},
|
},
|
||||||
|
"sections": {
|
||||||
|
"validationBankTitle": "Bank Validasi (Registri Atomik)",
|
||||||
|
"objectLevelValidations": "Validasi Tingkat Objek (Lokal & Async)",
|
||||||
|
"validatedPrefilledObjects": "Objek Terisi yang Divalidasi",
|
||||||
|
"richTextValidations": "Validasi Rich Text Editor",
|
||||||
|
"textAndNumbers": "Teks & Angka",
|
||||||
|
"selections": "Pilihan",
|
||||||
|
"advancedObjectSelects": "Pemilihan Objek Tingkat Lanjut (Label Kustom & Nilai Default)",
|
||||||
|
"multiSelectEditMode": "Mode Edit Multi-Select (Tanpa fallback defaultOptions)",
|
||||||
|
"richTextEditor": "Rich Text Editor (TipTap)",
|
||||||
|
"togglesAndChoices": "Tombol Sakelar & Pilihan",
|
||||||
|
"rangesAndSpecialized": "Rentang & Khusus",
|
||||||
|
"reactiveWatchCascading": "Reactive Watch (Berjenjang)",
|
||||||
|
"reactiveRichTextPreview": "Pratinjau Rich Text Reaktif",
|
||||||
|
"liveHtmlPreview": "Render Pratinjau HTML Langsung"
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"customerName": "Nama Pelanggan",
|
"customerName": "Nama Pelanggan",
|
||||||
"email": "Alamat Email",
|
"email": "Alamat Email",
|
||||||
@@ -26,7 +43,60 @@
|
|||||||
"orderType": "Tipe Pesanan",
|
"orderType": "Tipe Pesanan",
|
||||||
"quantity": "Jumlah",
|
"quantity": "Jumlah",
|
||||||
"fabricColor": "Warna Kain",
|
"fabricColor": "Warna Kain",
|
||||||
"pin": "PIN Keamanan"
|
"pin": "PIN Keamanan",
|
||||||
|
"department": "Departemen",
|
||||||
|
"assignees": "Penerima Tugas",
|
||||||
|
"emptyVendor": "Vendor Kosong",
|
||||||
|
"prefilledVendor": "Vendor Terisi",
|
||||||
|
"prefilledAsyncMulti": "Multi Async Terisi (Tanpa Fallback)",
|
||||||
|
"importantNotes": "Catatan Penting",
|
||||||
|
"country": "Negara",
|
||||||
|
"categories": "Kategori",
|
||||||
|
"localSelect": "Pilihan Lokal",
|
||||||
|
"multiLocalSelect": "Pilihan Lokal Multi",
|
||||||
|
"asyncSelectMock": "Pilihan Async (Mock API)",
|
||||||
|
"multiAsyncSelect": "Pilihan Async Multi",
|
||||||
|
"realPokeSingle": "API Pokemon Asli (Tunggal - Uji Deduplikasi)",
|
||||||
|
"realPokeMulti": "API Pokemon Asli (Multi - Uji Deduplikasi)",
|
||||||
|
"localEmpty": "Lokal Kosong",
|
||||||
|
"localPrefilled": "Lokal Terisi",
|
||||||
|
"asyncEmpty": "Async Kosong",
|
||||||
|
"asyncPrefilled": "Async Terisi (Mode Edit)",
|
||||||
|
"localMultiPrefilled": "Multi Lokal Terisi",
|
||||||
|
"asyncMultiPrefilled": "Multi Async Terisi (Item Hantu)",
|
||||||
|
"richTextEmpty": "Rich Text (Kosong)",
|
||||||
|
"richTextPrefilled": "Rich Text (Terisi / Mode Edit)",
|
||||||
|
"priceRange": "Rentang Harga",
|
||||||
|
"role": "Peran",
|
||||||
|
"regions": "Wilayah",
|
||||||
|
"warehouses": "Gudang",
|
||||||
|
"liveEditor": "Editor Langsung"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"selectComplexObject": "Pilih objek yang kompleks",
|
||||||
|
"selectMultipleObjects": "Pilih beberapa objek",
|
||||||
|
"searchPokemon": "Cari pokemon...",
|
||||||
|
"selectMultiplePokemon": "Pilih beberapa pokemon...",
|
||||||
|
"scrollDeduplication": "Gulir untuk menguji deduplikasi..."
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"min6Chars": "Minimal 6 karakter",
|
||||||
|
"min8Complex": "Min 8, 1 huruf besar, 1 angka, 1 karakter khusus",
|
||||||
|
"mustBePositive": "Harus > 0",
|
||||||
|
"formatPhone": "Format: +62...",
|
||||||
|
"zodMinLengthString": "Ini menggunakan validasi panjang string minimum Zod",
|
||||||
|
"freshTipTap": "Instance editor TipTap yang baru",
|
||||||
|
"htmlStringLoaded": "String HTML berhasil dimuat dari nilai default",
|
||||||
|
"typeToSeePreview": "Ketik untuk melihat render reaktif seketika di bawah",
|
||||||
|
"selectedRegionsTax": "Tarif pajak wilayah yang dipilih:"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"departmentRequired": "Departemen wajib diisi",
|
||||||
|
"vendorRequired": "Vendor wajib diisi",
|
||||||
|
"min2Assignees": "Pilih minimal 2 penerima tugas",
|
||||||
|
"min1Vendor": "Pilih minimal 1 vendor",
|
||||||
|
"notesMin15": "Catatan minimal harus terdiri dari 15 karakter (termasuk tag HTML)",
|
||||||
|
"selectRegionFirst": "Pilih wilayah terlebih dahulu untuk memuat gudang"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"simplePassword": "Sandi Sederhana",
|
"simplePassword": "Sandi Sederhana",
|
||||||
|
|||||||
Reference in New Issue
Block a user