# Frontend Vite Add Field

> Add a new field to an existing Vite admin CRUD feature: TypeScript types, Zod schema, form input, list column, API payload, and optional filter. Use when extending an existing admin resource with a new property from the backend.

- Skill: `xmuhameed/frontend-vite-add-field` (Agent Skill)
- Install (CLI): `npx skillmds@latest add xmuhameed/frontend-vite-add-field`
- Raw SKILL.md: https://api.skillmd.com/api/skills/xmuhameed/frontend-vite-add-field/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: xmuhameed (https://skillmd.com/u/xmuhameed)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/xmuhameed/frontend-vite-add-field

---


# 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

```typescript
// 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`.

```typescript
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

```tsx
<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} onCheckedChange={field.onChange} />
    </FormControl>
    <FormLabel>Featured</FormLabel>
  </FormItem>
)} />

{/* FK — Select from related query */}
<FormField control={form.control} name="typeId" render={({ field }) => (
  <FormItem>
    <FormLabel>Type</FormLabel>
    <Select onValueChange={(v) => 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

```typescript
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:

```typescript
// 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`.

```tsx
{
  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:

```typescript
const [isFeatured, setIsFeatured] = useSearchParamsState('isFeatured', '');
// pass to useGetProducts({ isFeatured: isFeatured === 'true' ? true : undefined })
```

**api.ts:**

```typescript
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
```

