Add Field to Existing Admin Feature
Not for new CRUD modules — use @frontend-vite/create-feature.
Reference: src/features/{name}/ — types, pages, api.
Checklist
[ ] 1. types/index.ts — add to entity + create/update payloads
[ ] 2. Form Zod schema — validation + defaultValues
[ ] 3. Create/Edit page — FormField or file input
[ ] 4. form.reset(data) — ensure API response maps the new field
[ ] 5. Mutation payload — include field in create/update call
[ ] 6. List page — column if displayed in table
[ ] 7. List filters — URL param + api.ts if filterable
[ ] 8. api.ts getAll — append query param when filtering
Step 1 — Types
// features/product/types/index.ts
export interface Product {
id: string;
name_en: string;
name_ar: string;
sku?: string | null; // new
isFeatured: boolean; // new
typeId?: number | null; // new FK
type?: { id: number; name_en: string } | null;
}
export interface CreateProductPayload {
name_en: string;
name_ar: string;
sku?: string;
isFeatured?: boolean;
typeId?: number;
}
Match backend DTO names and optionality exactly.
Step 2 — Form schema + defaults
See @frontend-vite/forms.
const schema = z.object({
name_en: z.string().min(1),
name_ar: z.string().min(1),
sku: z.string().optional(),
isFeatured: z.coerce.boolean().optional(),
typeId: z.coerce.number().optional(),
});
type FormData = z.infer<typeof schema>;
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name_en: '',
name_ar: '',
sku: '',
isFeatured: false,
typeId: undefined,
},
});
| Backend type | Zod |
|---|---|
| optional string | z.string().optional() |
| required boolean | z.coerce.boolean() |
| optional number / FK | z.coerce.number().optional() |
| enum | z.nativeEnum(StatusEnum) |
| required array (M2M) | z.array(z.coerce.number()).min(1) |
Step 3 — Form input
<FormField control={form.control} name="sku" render={({ field }) => (
<FormItem>
<FormLabel>SKU</FormLabel>
<FormControl><Input {...field} value={field.value ?? ''} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="isFeatured" render={({ field }) => (
<FormItem className="flex items-center gap-2">
<FormControl>
<Checkbox checked={field.value} />
</FormControl>
<FormLabel>Featured</FormLabel>
</FormItem>
)} />
{/* FK — Select from related query */}
<FormField control={form.control} name="typeId" render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<Select => field.onChange(Number(v))} value={String(field.value ?? '')}>
<FormControl><SelectTrigger><SelectValue placeholder="Select type" /></SelectTrigger></FormControl>
<SelectContent>
{types?.map((t) => <SelectItem key={t.id} value={String(t.id)}>{t.name_en}</SelectItem>)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} />
File fields — not in RHF; separate useState<File | null> — see @frontend-vite/file-upload.
Bilingual — always pair _en / _ar fields.
Step 4 — Edit mode reset
useEffect(() => {
if (data) {
form.reset({
name_en: data.name_en,
name_ar: data.name_ar,
sku: data.sku ?? '',
isFeatured: data.isFeatured ?? false,
typeId: data.typeId ?? undefined,
});
}
}, [data]);
Step 5 — Mutation / API
Create and update payloads must include the new field:
// api/api.ts — FormData or JSON depending on files
export const createProduct = async (payload: CreateProductPayload) => {
return (await api.post('/product/create-product', payload)).data;
};
export const updateProduct = async (id: string, payload: Partial<CreateProductPayload>) => {
return (await api.patch(`/product/update-product?id=${id}`, payload)).data;
};
For file + field updates, append to FormData in api.ts.
Step 6 — List page column
See @frontend-vite/list-pages.
{
accessorKey: 'sku',
header: 'SKU',
cell: ({ row }) => row.original.sku ?? '—',
},
{
accessorKey: 'isFeatured',
header: 'Featured',
cell: ({ row }) => (row.original.isFeatured ? 'Yes' : 'No'),
},
Step 7 — List filter (optional)
URL-synced filter in list page:
const [isFeatured, setIsFeatured] = useSearchParamsState('isFeatured', '');
// pass to useGetProducts({ isFeatured: isFeatured === 'true' ? true : undefined })
api.ts:
if (filters.isFeatured !== undefined) params.append('isFeatured', String(filters.isFeatured));
Field-type quick reference
| Field | Types | Form | List |
|---|---|---|---|
| String | string? |
Input |
text column |
| Boolean | boolean |
Checkbox / Switch |
Yes/No badge |
| Number | number? |
Input type="number" |
number column |
| Enum | union / enum | Select |
badge with label map |
| FK | typeId + nested type |
Select from related query |
row.original.type?.name_en |
| M2M | categoryIds: number[] |
multi Select / checkbox group |
comma-separated names |
| File | imageUrl: string |
file state + preview | <img> thumbnail |
Verification
[ ] Type matches backend response
[ ] Create sends new field
[ ] Edit loads and saves new field
[ ] List shows column (if applicable)
[ ] Filter works (if applicable)
[ ] Validation messages clear