feat: replace ObjectSelect with LocalSelect and introduce custom FieldSelect wrappers for object-based RHF state management
This commit is contained in:
+124
-41
@@ -6,8 +6,9 @@ import {
|
||||
FieldMultiSelect, FieldNativeSelect, FieldTagsInput, FieldCheckbox,
|
||||
FieldRadioGroup, FieldSwitch, FieldChipGroup, FieldSegmentedControl,
|
||||
FieldSlider, FieldRangeSlider, FieldRating, FieldColorInput,
|
||||
FieldColorPicker, FieldFileInput, FieldObjectSelect, FieldAsyncSelect
|
||||
FieldColorPicker, FieldFileInput, FieldLocalSelect, FieldAsyncSelect
|
||||
} from '@repo/ui/form';
|
||||
import type { LoadOptionsFn } from '@repo/ui/form';
|
||||
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
|
||||
|
||||
const MOCK_POKEMON = Array.from({ length: 100 }, (_, i) => ({
|
||||
@@ -15,22 +16,39 @@ const MOCK_POKEMON = Array.from({ length: 100 }, (_, i) => ({
|
||||
name: `Pokemon ${i + 1}`,
|
||||
}));
|
||||
|
||||
const fetchMockPokemon = async (url: string) => {
|
||||
// Parse the query params sent by useInfiniteScroll
|
||||
const urlObj = new URL(url, 'http://localhost');
|
||||
const page = parseInt(urlObj.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(urlObj.searchParams.get('pageSize') || '20', 10);
|
||||
const search = urlObj.searchParams.get('search')?.toLowerCase() || '';
|
||||
|
||||
// Fake network delay
|
||||
const loadMockPokemonOptions: LoadOptionsFn<any> = async (search, page) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Filter and paginate
|
||||
const filtered = MOCK_POKEMON.filter((p) => p.name.toLowerCase().includes(search));
|
||||
const filtered = MOCK_POKEMON.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()));
|
||||
const pageSize = 20;
|
||||
const start = (page - 1) * pageSize;
|
||||
const paginated = filtered.slice(start, start + pageSize);
|
||||
return {
|
||||
options: paginated,
|
||||
hasMore: start + pageSize < filtered.length,
|
||||
};
|
||||
};
|
||||
|
||||
return { results: paginated };
|
||||
const loadRealPokemonOptions: LoadOptionsFn<any> = async (_search, page) => {
|
||||
const limit = 20;
|
||||
const offset = (page - 1) * limit;
|
||||
const res = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=${limit}&offset=${offset}`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
options: data.results.map((p: any, i: number) => ({ id: offset + i + 1, ...p })),
|
||||
hasMore: !!data.next,
|
||||
};
|
||||
};
|
||||
|
||||
const MOCK_VENDORS = [
|
||||
{ id: 'V1', code: 'VN-01', name: 'Vendor One' },
|
||||
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' },
|
||||
{ id: 'V3', code: 'VN-03', name: 'Vendor Three' },
|
||||
];
|
||||
|
||||
const loadMockVendorsOptions: LoadOptionsFn<any> = async (search, _page) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const filtered = MOCK_VENDORS.filter(v => v.name.toLowerCase().includes(search.toLowerCase()) || v.code.toLowerCase().includes(search.toLowerCase()));
|
||||
return { options: filtered, hasMore: false };
|
||||
};
|
||||
|
||||
export default function AllFieldsDemo() {
|
||||
@@ -61,10 +79,18 @@ export default function AllFieldsDemo() {
|
||||
themeColor: '',
|
||||
colorPicker: '#1c7ed6',
|
||||
avatar: null,
|
||||
objectSelect: null,
|
||||
asyncSelect: null,
|
||||
multiObjectSelect: [],
|
||||
multiAsyncSelect: [],
|
||||
localSelectEmpty: null,
|
||||
localSelectPrefilled: { id: 'V2', code: 'VN-02', name: 'Vendor Two' },
|
||||
asyncSelectEmpty: null,
|
||||
asyncSelectPrefilled: { id: 999, code: 'ASYNC-99', name: 'Pre-loaded Async Vendor' },
|
||||
localMultiPrefilled: [
|
||||
{ id: 'V1', code: 'VN-01', name: 'Vendor One' },
|
||||
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' }
|
||||
],
|
||||
asyncMultiPrefilled: [
|
||||
{ id: 888, code: 'ASYNC-88', name: 'Ghost Async Vendor 1' },
|
||||
{ id: 999, code: 'ASYNC-99', name: 'Ghost Async Vendor 2' }
|
||||
],
|
||||
realPokeSelect: null,
|
||||
multiRealPokeSelect: []
|
||||
}
|
||||
@@ -133,12 +159,12 @@ export default function AllFieldsDemo() {
|
||||
/>
|
||||
</Group>
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldObjectSelect
|
||||
name="objectSelect"
|
||||
<FieldLocalSelect
|
||||
name="localSelect"
|
||||
control={control}
|
||||
label="Object Select"
|
||||
label="Local Select"
|
||||
placeholder="Select a complex object"
|
||||
data={[
|
||||
options={[
|
||||
{ id: 1, name: 'Apple', type: 'Fruit' },
|
||||
{ id: 2, name: 'Carrot', type: 'Vegetable' },
|
||||
{ id: 3, name: 'Banana', type: 'Fruit' }
|
||||
@@ -147,13 +173,13 @@ export default function AllFieldsDemo() {
|
||||
labelKey="name"
|
||||
clearable
|
||||
/>
|
||||
<FieldObjectSelect
|
||||
<FieldLocalSelect
|
||||
multiple
|
||||
name="multiObjectSelect"
|
||||
name="multiLocalSelect"
|
||||
control={control}
|
||||
label="Multi Object Select"
|
||||
label="Multi Local Select"
|
||||
placeholder="Select multiple objects"
|
||||
data={[
|
||||
options={[
|
||||
{ id: 1, name: 'Red', hex: '#f00' },
|
||||
{ id: 2, name: 'Green', hex: '#0f0' },
|
||||
{ id: 3, name: 'Blue', hex: '#00f' }
|
||||
@@ -169,9 +195,7 @@ export default function AllFieldsDemo() {
|
||||
control={control}
|
||||
label="Async Select (Mock API)"
|
||||
placeholder="Search pokemon..."
|
||||
apiEndpoint="/api/mock/pokemon"
|
||||
fetchFn={fetchMockPokemon}
|
||||
transformResponse={(res) => res.results}
|
||||
loadOptions={loadMockPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
clearable
|
||||
@@ -182,9 +206,7 @@ export default function AllFieldsDemo() {
|
||||
control={control}
|
||||
label="Multi Async Select"
|
||||
placeholder="Select multiple pokemon..."
|
||||
apiEndpoint="/api/mock/pokemon"
|
||||
fetchFn={fetchMockPokemon}
|
||||
transformResponse={(res) => res.results}
|
||||
loadOptions={loadMockPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
clearable
|
||||
@@ -196,11 +218,7 @@ export default function AllFieldsDemo() {
|
||||
control={control}
|
||||
label="Real PokeAPI (Single - Tests Deduplication)"
|
||||
placeholder="Scroll to test deduplication..."
|
||||
apiEndpoint="https://pokeapi.co/api/v2/pokemon"
|
||||
transformResponse={(res) => ({
|
||||
data: res.results.map((p: any, i: number) => ({ id: i + 1, ...p })),
|
||||
hasMore: !!res.next
|
||||
})}
|
||||
loadOptions={loadRealPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
clearable
|
||||
@@ -211,17 +229,82 @@ export default function AllFieldsDemo() {
|
||||
control={control}
|
||||
label="Real PokeAPI (Multi - Tests Deduplication)"
|
||||
placeholder="Scroll to test deduplication..."
|
||||
apiEndpoint="https://pokeapi.co/api/v2/pokemon"
|
||||
transformResponse={(res) => ({
|
||||
data: res.results.map((p: any, i: number) => ({ id: i + 1, ...p })),
|
||||
hasMore: !!res.next
|
||||
})}
|
||||
loadOptions={loadRealPokemonOptions}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
clearable
|
||||
/>
|
||||
</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>
|
||||
<Divider mb="md" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldLocalSelect
|
||||
name="localSelectEmpty"
|
||||
control={control}
|
||||
label="Local Empty"
|
||||
options={MOCK_VENDORS}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
/>
|
||||
<FieldLocalSelect
|
||||
name="localSelectPrefilled"
|
||||
control={control}
|
||||
label="Local Prefilled"
|
||||
options={MOCK_VENDORS}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldAsyncSelect
|
||||
name="asyncSelectEmpty"
|
||||
control={control}
|
||||
label="Async Empty"
|
||||
loadOptions={loadMockVendorsOptions}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
/>
|
||||
<FieldAsyncSelect
|
||||
name="asyncSelectPrefilled"
|
||||
control={control}
|
||||
label="Async Prefilled (Edit Mode)"
|
||||
loadOptions={loadMockVendorsOptions}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
defaultOptions={[{ id: 999, code: 'ASYNC-99', name: 'Pre-loaded Async Vendor' }]}
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Title order={5} mb="sm" mt="lg" c="brand">Multi-Select Edit Mode (No defaultOptions fallback)</Title>
|
||||
<Divider mb="md" />
|
||||
<Group grow align="flex-start" mb="md">
|
||||
<FieldLocalSelect
|
||||
multiple
|
||||
name="localMultiPrefilled"
|
||||
control={control}
|
||||
label="Local Multi Prefilled"
|
||||
options={MOCK_VENDORS}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
/>
|
||||
<FieldAsyncSelect
|
||||
multiple
|
||||
name="asyncMultiPrefilled"
|
||||
control={control}
|
||||
label="Async Multi Prefilled (Ghost Items)"
|
||||
loadOptions={loadMockVendorsOptions}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
{/* --- Toggles & Choices --- */}
|
||||
|
||||
+106
-1
@@ -2,11 +2,49 @@ import { useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button, Paper, Title, Divider, Stack, Code, Alert } from '@repo/ui/components';
|
||||
import { FieldTextInput, FieldSelect, FieldSwitch } from '@repo/ui/form';
|
||||
import { FieldTextInput, FieldSelect, FieldSwitch, FieldLocalSelect, FieldAsyncSelect } from '@repo/ui/form';
|
||||
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';
|
||||
|
||||
interface Region {
|
||||
id: string;
|
||||
code: string;
|
||||
taxRate: number;
|
||||
}
|
||||
|
||||
interface Warehouse {
|
||||
id: string;
|
||||
regionId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const REGIONS: Region[] = [
|
||||
{ id: 'R1', code: 'APAC', taxRate: 0.1 },
|
||||
{ id: 'R2', code: 'EMEA', taxRate: 0.2 }
|
||||
];
|
||||
|
||||
const mockFetchWarehouses = async (regionIds: string[], search: string, page: number) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const allWarehouses: Warehouse[] = [
|
||||
{ id: 'W1', regionId: 'R1', name: 'Singapore Hub' },
|
||||
{ id: 'W2', regionId: 'R1', name: 'Tokyo Depot' },
|
||||
{ id: 'W3', regionId: 'R2', name: 'London Central' },
|
||||
{ id: 'W4', regionId: 'R2', name: 'Berlin Storage' },
|
||||
];
|
||||
|
||||
const filtered = allWarehouses.filter(w => regionIds.includes(w.regionId) && w.name.toLowerCase().includes(search.toLowerCase()));
|
||||
const pageSize = 10;
|
||||
const start = (page - 1) * pageSize;
|
||||
const paginated = filtered.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
options: paginated,
|
||||
hasMore: start + pageSize < filtered.length
|
||||
};
|
||||
};
|
||||
|
||||
export default function ReactiveWatchDemo() {
|
||||
const t = useFormDemoTranslation();
|
||||
@@ -27,6 +65,8 @@ export default function ReactiveWatchDemo() {
|
||||
newsletterEmail: z.string().optional(),
|
||||
department: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
regions: z.array(z.object({ id: z.string(), code: z.string(), taxRate: z.number() })).optional(),
|
||||
warehouses: z.array(z.object({ id: z.string(), regionId: z.string(), name: z.string() })).optional(),
|
||||
})
|
||||
.and(
|
||||
z.discriminatedUnion('userType', [
|
||||
@@ -64,6 +104,11 @@ export default function ReactiveWatchDemo() {
|
||||
newsletterEmail: '',
|
||||
department: '',
|
||||
role: '',
|
||||
regions: [{ id: 'R1', code: 'APAC', taxRate: 0.1 }],
|
||||
warehouses: [
|
||||
{ id: 'W-99', regionId: 'R1', name: 'APAC Central Hub' },
|
||||
{ id: 'W-98', regionId: 'R1', name: 'APAC Backup Hub' }
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,6 +118,7 @@ export default function ReactiveWatchDemo() {
|
||||
const newsletter = useWatch({ control, name: 'newsletter' });
|
||||
const department = useWatch({ control, name: 'department' });
|
||||
const role = useWatch({ control, name: 'role' });
|
||||
const regions = useWatch({ control, name: 'regions' });
|
||||
|
||||
// Use the custom hook to cleanly unregister and reset fields when hidden
|
||||
useConditionalField({
|
||||
@@ -133,6 +179,27 @@ export default function ReactiveWatchDemo() {
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const isMounted = useRef(false);
|
||||
const prevRegionIds = useRef<string[]>(regions?.map((r: Region) => r.id) || []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted.current) {
|
||||
isMounted.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIds = regions?.map((r: Region) => r.id) || [];
|
||||
const prevIds = prevRegionIds.current;
|
||||
|
||||
const hasChanged = currentIds.length !== prevIds.length || currentIds.some((id: string) => !prevIds.includes(id));
|
||||
|
||||
if (hasChanged) {
|
||||
setValue('warehouses', []);
|
||||
clearErrors('warehouses');
|
||||
prevRegionIds.current = currentIds;
|
||||
}
|
||||
}, [regions, setValue, clearErrors]);
|
||||
|
||||
// Use watch only to display the JSON output at the bottom
|
||||
const allValues = useWatch({ control });
|
||||
|
||||
@@ -223,6 +290,44 @@ export default function ReactiveWatchDemo() {
|
||||
withAsterisk={!!department}
|
||||
/>
|
||||
|
||||
<Title order={5} mb="sm" c="brand" mt="lg">
|
||||
Cascading Object Selects
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
<FieldLocalSelect<Region>
|
||||
multiple
|
||||
name="regions"
|
||||
control={control as any}
|
||||
label="Regions"
|
||||
options={REGIONS}
|
||||
valueKey="id"
|
||||
labelKey="code"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<FieldAsyncSelect<Warehouse>
|
||||
multiple
|
||||
key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`}
|
||||
name="warehouses"
|
||||
control={control as any}
|
||||
label="Warehouses"
|
||||
disabled={!regions || regions.length === 0}
|
||||
loadOptions={useCallback(async (search, page) => {
|
||||
if (!regions || regions.length === 0) return { options: [], hasMore: false };
|
||||
return mockFetchWarehouses(regions.map((r: any) => r.id), search, page);
|
||||
}, [regions])}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.id}] ${item.name}`}
|
||||
clearable
|
||||
/>
|
||||
|
||||
{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(', ')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" mt="md">
|
||||
{t.common?.submit || 'Submit Reactive Form'}
|
||||
</Button>
|
||||
|
||||
+129
-3
@@ -3,8 +3,55 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button, Paper, Title, Group, Stack, Code, Divider } from '@repo/ui/components';
|
||||
import {
|
||||
FieldTextInput, FieldPasswordInput, FieldNumberInput
|
||||
FieldTextInput, FieldPasswordInput, FieldNumberInput,
|
||||
FieldLocalSelect, FieldAsyncSelect
|
||||
} from '@repo/ui/form';
|
||||
import type { LoadOptionsFn } from '@repo/ui/form';
|
||||
|
||||
interface Department {
|
||||
code: string;
|
||||
name: string;
|
||||
costCenter: string;
|
||||
}
|
||||
|
||||
interface Assignee {
|
||||
id: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const MOCK_DEPARTMENTS: Department[] = [
|
||||
{ code: 'IT', name: 'Information Technology', costCenter: 'CC-100' },
|
||||
{ code: 'HR', name: 'Human Resources', costCenter: 'CC-200' },
|
||||
{ code: 'FIN', name: 'Finance', costCenter: 'CC-300' },
|
||||
];
|
||||
|
||||
const mockFetchUsers: LoadOptionsFn<Assignee> = async (search, page) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const allUsers = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
email: `user${i + 1}@company.com`
|
||||
}));
|
||||
const filtered = allUsers.filter(u => u.email.toLowerCase().includes(search.toLowerCase()));
|
||||
const pageSize = 5;
|
||||
const start = (page - 1) * pageSize;
|
||||
const paginated = filtered.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
options: paginated,
|
||||
hasMore: start + pageSize < filtered.length
|
||||
};
|
||||
};
|
||||
|
||||
const MOCK_VENDORS = [
|
||||
{ id: 'V1', code: 'VN-01', name: 'Vendor One' },
|
||||
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' },
|
||||
];
|
||||
|
||||
const mockFetchVendors: LoadOptionsFn<any> = async (search, _page) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const filtered = MOCK_VENDORS.filter(v => v.name.toLowerCase().includes(search.toLowerCase()) || v.code.toLowerCase().includes(search.toLowerCase()));
|
||||
return { options: filtered, hasMore: false };
|
||||
};
|
||||
import {
|
||||
compose, required, rangeLength,
|
||||
positiveNumber, simplePassword,
|
||||
@@ -22,7 +69,12 @@ export default function ValidationBankDemo() {
|
||||
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())
|
||||
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"),
|
||||
});
|
||||
|
||||
type ValidationFormValues = z.infer<typeof validationSchema>;
|
||||
@@ -35,7 +87,15 @@ export default function ValidationBankDemo() {
|
||||
complexPass: '',
|
||||
age: undefined as any,
|
||||
score: undefined as any,
|
||||
phone: ''
|
||||
phone: '',
|
||||
department: null as any,
|
||||
assignees: [],
|
||||
prefilledVendor: { id: 'V1', code: 'VN-01', name: 'Vendor One' } as any,
|
||||
emptyVendor: null as any,
|
||||
prefilledAsyncMulti: [
|
||||
{ id: 'V1', code: 'VN-01', name: 'Vendor One' },
|
||||
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' }
|
||||
] as any,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,6 +160,72 @@ export default function ValidationBankDemo() {
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Title order={5} c="brand" mt="md">Object Level Validations (Local & Async)</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
<FieldLocalSelect<Department>
|
||||
name="department"
|
||||
control={control as any}
|
||||
label="Department"
|
||||
options={MOCK_DEPARTMENTS}
|
||||
valueKey="code"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<FieldAsyncSelect<Assignee>
|
||||
multiple
|
||||
name="assignees"
|
||||
control={control as any}
|
||||
label="Assignees"
|
||||
loadOptions={mockFetchUsers}
|
||||
valueKey="id"
|
||||
labelKey="email"
|
||||
searchable
|
||||
clearable
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Title order={5} c="brand" mt="lg">Validated Prefilled Objects</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<FieldAsyncSelect
|
||||
name="emptyVendor"
|
||||
control={control as any}
|
||||
label="Empty Vendor"
|
||||
loadOptions={mockFetchVendors}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
withAsterisk
|
||||
/>
|
||||
<FieldAsyncSelect
|
||||
name="prefilledVendor"
|
||||
control={control as any}
|
||||
label="Prefilled Vendor"
|
||||
loadOptions={mockFetchVendors}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
defaultOptions={[{ id: 'V1', code: 'VN-01', name: 'Vendor One' }]}
|
||||
clearable
|
||||
withAsterisk
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<FieldAsyncSelect
|
||||
multiple
|
||||
name="prefilledAsyncMulti"
|
||||
control={control as any}
|
||||
label="Prefilled Async Multi (No Fallback)"
|
||||
loadOptions={mockFetchVendors}
|
||||
valueKey="id"
|
||||
renderLabel={(item) => `[${item.code}] ${item.name}`}
|
||||
clearable
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Button type="submit" mt="md">{t.common.submit}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user