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">
|
||||
{/* --- Text & Numbers --- */}
|
||||
<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" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldTextInput name="customerName" control={control} label={t.fields.customerName} />
|
||||
@@ -131,7 +131,7 @@ export default function AllFieldsDemo() {
|
||||
|
||||
{/* --- Selections --- */}
|
||||
<div>
|
||||
<Title order={5} mb="sm" c="brand">Selections</Title>
|
||||
<Title order={5} mb="sm" c="brand">{t.sections.selections}</Title>
|
||||
<Divider mb="md" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldSelect
|
||||
@@ -151,13 +151,13 @@ export default function AllFieldsDemo() {
|
||||
<FieldAutocomplete
|
||||
name="country"
|
||||
control={control}
|
||||
label="Country"
|
||||
label={t.fields.country}
|
||||
data={['Indonesia', 'Singapore', 'Malaysia']}
|
||||
/>
|
||||
<FieldMultiSelect
|
||||
name="categories"
|
||||
control={control}
|
||||
label="Categories"
|
||||
label={t.fields.categories}
|
||||
data={['Electronics', 'Fashion', 'Food']}
|
||||
/>
|
||||
</Group>
|
||||
@@ -165,8 +165,8 @@ export default function AllFieldsDemo() {
|
||||
<FieldLocalSelect
|
||||
name="localSelect"
|
||||
control={control}
|
||||
label="Local Select"
|
||||
placeholder="Select a complex object"
|
||||
label={t.fields.localSelect}
|
||||
placeholder={t.placeholders.selectComplexObject}
|
||||
options={[
|
||||
{ id: 1, name: 'Apple', type: 'Fruit' },
|
||||
{ id: 2, name: 'Carrot', type: 'Vegetable' },
|
||||
@@ -180,8 +180,8 @@ export default function AllFieldsDemo() {
|
||||
multiple
|
||||
name="multiLocalSelect"
|
||||
control={control}
|
||||
label="Multi Local Select"
|
||||
placeholder="Select multiple objects"
|
||||
label={t.fields.multiLocalSelect}
|
||||
placeholder={t.placeholders.selectMultipleObjects}
|
||||
options={[
|
||||
{ id: 1, name: 'Red', hex: '#f00' },
|
||||
{ id: 2, name: 'Green', hex: '#0f0' },
|
||||
@@ -196,8 +196,8 @@ export default function AllFieldsDemo() {
|
||||
<FieldAsyncSelect
|
||||
name="asyncSelect"
|
||||
control={control}
|
||||
label="Async Select (Mock API)"
|
||||
placeholder="Search pokemon..."
|
||||
label={t.fields.asyncSelectMock}
|
||||
placeholder={t.placeholders.searchPokemon}
|
||||
loadOptions={loadMockPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
@@ -207,8 +207,8 @@ export default function AllFieldsDemo() {
|
||||
multiple
|
||||
name="multiAsyncSelect"
|
||||
control={control}
|
||||
label="Multi Async Select"
|
||||
placeholder="Select multiple pokemon..."
|
||||
label={t.fields.multiAsyncSelect}
|
||||
placeholder={t.placeholders.selectMultiplePokemon}
|
||||
loadOptions={loadMockPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
@@ -219,8 +219,8 @@ export default function AllFieldsDemo() {
|
||||
<FieldAsyncSelect
|
||||
name="realPokeSelect"
|
||||
control={control}
|
||||
label="Real PokeAPI (Single - Tests Deduplication)"
|
||||
placeholder="Scroll to test deduplication..."
|
||||
label={t.fields.realPokeSingle}
|
||||
placeholder={t.placeholders.scrollDeduplication}
|
||||
loadOptions={loadRealPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
@@ -230,8 +230,8 @@ export default function AllFieldsDemo() {
|
||||
multiple
|
||||
name="multiRealPokeSelect"
|
||||
control={control}
|
||||
label="Real PokeAPI (Multi - Tests Deduplication)"
|
||||
placeholder="Scroll to test deduplication..."
|
||||
label={t.fields.realPokeMulti}
|
||||
placeholder={t.placeholders.scrollDeduplication}
|
||||
loadOptions={loadRealPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
@@ -240,13 +240,13 @@ export default function AllFieldsDemo() {
|
||||
</Group>
|
||||
<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" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldLocalSelect
|
||||
name="localSelectEmpty"
|
||||
control={control}
|
||||
label="Local Empty"
|
||||
label={t.fields.localEmpty}
|
||||
options={MOCK_VENDORS}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -255,7 +255,7 @@ export default function AllFieldsDemo() {
|
||||
<FieldLocalSelect
|
||||
name="localSelectPrefilled"
|
||||
control={control}
|
||||
label="Local Prefilled"
|
||||
label={t.fields.localPrefilled}
|
||||
options={MOCK_VENDORS}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -266,7 +266,7 @@ export default function AllFieldsDemo() {
|
||||
<FieldAsyncSelect
|
||||
name="asyncSelectEmpty"
|
||||
control={control}
|
||||
label="Async Empty"
|
||||
label={t.fields.asyncEmpty}
|
||||
loadOptions={loadMockVendorsOptions}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -275,7 +275,7 @@ export default function AllFieldsDemo() {
|
||||
<FieldAsyncSelect
|
||||
name="asyncSelectPrefilled"
|
||||
control={control}
|
||||
label="Async Prefilled (Edit Mode)"
|
||||
label={t.fields.asyncPrefilled}
|
||||
loadOptions={loadMockVendorsOptions}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -284,14 +284,14 @@ export default function AllFieldsDemo() {
|
||||
/>
|
||||
</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" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldLocalSelect
|
||||
multiple
|
||||
name="localMultiPrefilled"
|
||||
control={control}
|
||||
label="Local Multi Prefilled"
|
||||
label={t.fields.localMultiPrefilled}
|
||||
options={MOCK_VENDORS}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -301,7 +301,7 @@ export default function AllFieldsDemo() {
|
||||
multiple
|
||||
name="asyncMultiPrefilled"
|
||||
control={control}
|
||||
label="Async Multi Prefilled (Ghost Items)"
|
||||
label={t.fields.asyncMultiPrefilled}
|
||||
loadOptions={loadMockVendorsOptions}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -309,27 +309,27 @@ export default function AllFieldsDemo() {
|
||||
/>
|
||||
</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" />
|
||||
<FieldRichTextEditor
|
||||
name="richTextEmpty"
|
||||
control={control}
|
||||
label="Rich Text (Empty)"
|
||||
description="A fresh TipTap editor instance"
|
||||
label={t.fields.richTextEmpty}
|
||||
description={t.descriptions.freshTipTap}
|
||||
/>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<FieldRichTextEditor
|
||||
name="richTextPrefilled"
|
||||
control={control}
|
||||
label="Rich Text (Prefilled / Edit Mode)"
|
||||
description="HTML string successfully loaded from default values"
|
||||
label={t.fields.richTextPrefilled}
|
||||
description={t.descriptions.htmlStringLoaded}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- Toggles & Choices --- */}
|
||||
<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" />
|
||||
<Group mb="md">
|
||||
<FieldCheckbox name="terms" control={control} label={t.fields.terms} />
|
||||
@@ -372,11 +372,11 @@ export default function AllFieldsDemo() {
|
||||
|
||||
{/* --- Ranges & Specialized --- */}
|
||||
<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" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<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 grow align="flex-start" mb="md">
|
||||
<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 { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
|
||||
interface Region {
|
||||
id: string;
|
||||
@@ -52,10 +52,10 @@ export default function ReactiveWatchDemo() {
|
||||
// Define atomic validators for conditional fields
|
||||
const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId));
|
||||
const spouseNameValidator = compose(z.string(), required(t.watch.spouseName));
|
||||
const newsletterEmailValidator = compose(z.string(), required('Newsletter Email'), emailValidator());
|
||||
const roleValidator = compose(z.string(), required('Role'));
|
||||
const newsletterEmailValidator = compose(z.string(), required(t.fields.email), emailValidator());
|
||||
const roleValidator = compose(z.string(), required(t.fields.role));
|
||||
|
||||
const reactiveSchema = z
|
||||
const reactiveSchema = useMemo(() => z
|
||||
.object({
|
||||
userType: z.enum(['PERSONAL', 'CORPORATE']),
|
||||
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.union([z.literal(''), z.undefined(), z.null()]).optional() }),
|
||||
]),
|
||||
);
|
||||
), [t, taxIdValidator, spouseNameValidator, newsletterEmailValidator, roleValidator]);
|
||||
|
||||
const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({
|
||||
resolver: zodResolver(reactiveSchema as any),
|
||||
@@ -214,7 +214,7 @@ export default function ReactiveWatchDemo() {
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Title order={5} mb="sm" c="brand">
|
||||
Dynamic Fields & Validation
|
||||
{t.sections.reactiveWatchCascading}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
@@ -264,8 +264,8 @@ export default function ReactiveWatchDemo() {
|
||||
<FieldSelect
|
||||
name="department"
|
||||
control={control}
|
||||
label="Department"
|
||||
placeholder="Select a department"
|
||||
label={t.fields.department}
|
||||
placeholder={t.placeholders.selectComplexObject}
|
||||
data={[
|
||||
{ value: 'IT', label: 'Information Technology' },
|
||||
{ value: 'HR', label: 'Human Resources' },
|
||||
@@ -286,15 +286,15 @@ export default function ReactiveWatchDemo() {
|
||||
key={`role-select-${department}`}
|
||||
name="role"
|
||||
control={control}
|
||||
label="Role"
|
||||
placeholder="Select a role"
|
||||
label={t.fields.role}
|
||||
placeholder={t.placeholders.selectComplexObject}
|
||||
disabled={!department}
|
||||
data={currentRoleOptions}
|
||||
withAsterisk={!!department}
|
||||
/>
|
||||
|
||||
<Title order={5} mb="sm" c="brand" mt="lg">
|
||||
Cascading Object Selects
|
||||
{t.sections.reactiveWatchCascading}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
@@ -302,7 +302,7 @@ export default function ReactiveWatchDemo() {
|
||||
multiple
|
||||
name="regions"
|
||||
control={control as any}
|
||||
label="Regions"
|
||||
label={t.fields.regions}
|
||||
options={REGIONS}
|
||||
valueKey="id"
|
||||
labelKey="code"
|
||||
@@ -314,7 +314,7 @@ export default function ReactiveWatchDemo() {
|
||||
key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`}
|
||||
name="warehouses"
|
||||
control={control as any}
|
||||
label="Warehouses"
|
||||
label={t.fields.warehouses}
|
||||
disabled={!regions || regions.length === 0}
|
||||
loadOptions={useCallback(async (search, page) => {
|
||||
if (!regions || regions.length === 0) return { options: [], hasMore: false };
|
||||
@@ -327,31 +327,31 @@ export default function ReactiveWatchDemo() {
|
||||
|
||||
{regions && regions.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
<Title order={5} mb="sm" c="brand" mt="lg">
|
||||
Reactive Rich Text Preview
|
||||
{t.sections.reactiveRichTextPreview}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
<FieldRichTextEditor
|
||||
name="richTextLive"
|
||||
control={control as any}
|
||||
label="Live Editor"
|
||||
description="Type to see instantaneous reactive rendering below"
|
||||
label={t.fields.liveEditor}
|
||||
description={t.descriptions.typeToSeePreview}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} />
|
||||
</TypographyStylesProvider>
|
||||
</Paper>
|
||||
|
||||
<Button type="submit" mt="md">
|
||||
{t.common?.submit || 'Submit Reactive Form'}
|
||||
{t.common.submitReactive}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
+24
-23
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
@@ -63,20 +64,20 @@ export default function ValidationBankDemo() {
|
||||
const t = useFormDemoTranslation();
|
||||
|
||||
// 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)),
|
||||
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
|
||||
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
|
||||
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)),
|
||||
phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
|
||||
department: z.object({ code: z.string(), name: z.string() }, { required_error: 'Department is required' }),
|
||||
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, "Select at least 2 assignees"),
|
||||
prefilledVendor: 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: 'Vendor is required' }),
|
||||
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"),
|
||||
richTextNotes: z.string().min(15, "Notes must be at least 15 characters long (including HTML tags)"),
|
||||
});
|
||||
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, t.errors.min2Assignees),
|
||||
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: t.errors.vendorRequired }),
|
||||
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, t.errors.notesMin15),
|
||||
}), [t]);
|
||||
|
||||
type ValidationFormValues = z.infer<typeof validationSchema>;
|
||||
|
||||
@@ -109,7 +110,7 @@ export default function ValidationBankDemo() {
|
||||
<Paper p="xl" withBorder radius="md">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<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" />
|
||||
|
||||
<FieldTextInput
|
||||
@@ -125,14 +126,14 @@ export default function ValidationBankDemo() {
|
||||
name="simplePass"
|
||||
control={control}
|
||||
label={t.validation.simplePassword}
|
||||
description="Min 6 chars"
|
||||
description={t.descriptions.min6Chars}
|
||||
withAsterisk
|
||||
/>
|
||||
<FieldPasswordInput
|
||||
name="complexPass"
|
||||
control={control}
|
||||
label={t.validation.complexPassword}
|
||||
description="Min 8, 1 uppercase, 1 number, 1 special"
|
||||
description={t.descriptions.min8Complex}
|
||||
withAsterisk
|
||||
/>
|
||||
</Group>
|
||||
@@ -149,7 +150,7 @@ export default function ValidationBankDemo() {
|
||||
name="score"
|
||||
control={control}
|
||||
label={t.validation.score}
|
||||
description="Must be > 0"
|
||||
description={t.descriptions.mustBePositive}
|
||||
withAsterisk
|
||||
/>
|
||||
</Group>
|
||||
@@ -158,17 +159,17 @@ export default function ValidationBankDemo() {
|
||||
name="phone"
|
||||
control={control}
|
||||
label={t.validation.phone}
|
||||
description="Format: +62..."
|
||||
description={t.descriptions.formatPhone}
|
||||
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" />
|
||||
|
||||
<FieldLocalSelect<Department>
|
||||
name="department"
|
||||
control={control as any}
|
||||
label="Department"
|
||||
label={t.fields.department}
|
||||
options={MOCK_DEPARTMENTS}
|
||||
valueKey="code"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -180,7 +181,7 @@ export default function ValidationBankDemo() {
|
||||
multiple
|
||||
name="assignees"
|
||||
control={control as any}
|
||||
label="Assignees"
|
||||
label={t.fields.assignees}
|
||||
loadOptions={mockFetchUsers}
|
||||
valueKey="id"
|
||||
labelKey="email"
|
||||
@@ -189,14 +190,14 @@ export default function ValidationBankDemo() {
|
||||
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" />
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<FieldAsyncSelect
|
||||
name="emptyVendor"
|
||||
control={control as any}
|
||||
label="Empty Vendor"
|
||||
label={t.fields.emptyVendor}
|
||||
loadOptions={mockFetchVendors}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -206,7 +207,7 @@ export default function ValidationBankDemo() {
|
||||
<FieldAsyncSelect
|
||||
name="prefilledVendor"
|
||||
control={control as any}
|
||||
label="Prefilled Vendor"
|
||||
label={t.fields.prefilledVendor}
|
||||
loadOptions={mockFetchVendors}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -220,7 +221,7 @@ export default function ValidationBankDemo() {
|
||||
multiple
|
||||
name="prefilledAsyncMulti"
|
||||
control={control as any}
|
||||
label="Prefilled Async Multi (No Fallback)"
|
||||
label={t.fields.prefilledAsyncMulti}
|
||||
loadOptions={mockFetchVendors}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
@@ -228,14 +229,14 @@ export default function ValidationBankDemo() {
|
||||
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" />
|
||||
|
||||
<FieldRichTextEditor
|
||||
name="richTextNotes"
|
||||
control={control as any}
|
||||
label="Important Notes"
|
||||
description="This uses Zod minimum length string validation"
|
||||
label={t.fields.importantNotes}
|
||||
description={t.descriptions.zodMinLengthString}
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,9 +6,26 @@
|
||||
},
|
||||
"common": {
|
||||
"submit": "Submit Data",
|
||||
"submitReactive": "Submit Reactive Form",
|
||||
"reset": "Reset Form",
|
||||
"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": {
|
||||
"customerName": "Customer Name",
|
||||
"email": "Email Address",
|
||||
@@ -26,7 +43,60 @@
|
||||
"orderType": "Order Type",
|
||||
"quantity": "Quantity",
|
||||
"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": {
|
||||
"simplePassword": "Simple Password",
|
||||
|
||||
@@ -6,9 +6,26 @@
|
||||
},
|
||||
"common": {
|
||||
"submit": "Kirim Data",
|
||||
"submitReactive": "Kirim Form Reaktif",
|
||||
"reset": "Reset Form",
|
||||
"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": {
|
||||
"customerName": "Nama Pelanggan",
|
||||
"email": "Alamat Email",
|
||||
@@ -26,7 +43,60 @@
|
||||
"orderType": "Tipe Pesanan",
|
||||
"quantity": "Jumlah",
|
||||
"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": {
|
||||
"simplePassword": "Sandi Sederhana",
|
||||
|
||||
Reference in New Issue
Block a user