# Form Page (Create / Edit)

For UI component API details, see [components.md](components.md).

## File Structure

```bash
<entity>/
  create/
    page.tsx                                # create route
    components/create-<entity>-form.tsx      # create form
  [id]/edit/
    page.tsx                                # edit route
    components/edit-<entity>-form.tsx        # edit form (pre-filled)
```

## Pattern

- **React Hook Form + Zod** for validation with `zodResolver`
- **urql `useMutation`** for GraphQL mutations
- **Cache invalidation**: pass `additionalTypenames` to the mutation context so urql invalidates related queries after create/edit/delete. Without this, list pages will show stale data.
  ```ts
  createUser(values, { additionalTypenames: ["User"] })
  ```
- **Toast notifications**: `useToast` from `@/hooks/use-toast`. `toast.error()` accepts `CombinedError` directly (e.g., `toast.error(result.error)`)
- **Navigate `".."`** to go back to list after success
- Edit form pre-fills `defaultValues` from existing entity via fragment

## Field Type Mapping (Screen Spec → Component + Zod)

Map each form field from its **Field Type** in the screen spec's Form Input Fields table to the correct component and Zod schema. The `Options / Lookup` column determines sync vs async: a comma-separated list means static items (sync); `<Entity> entity → <field>` means fetched from API (async).

| Field Type | Options / Lookup | Component | Zod Schema |
| --- | --- | --- | --- |
| `String` | — | `<Input />` | `z.string()` |
| `Text` | — | `<textarea />` | `z.string()` |
| `Email` | — | `<Input type="email" />` | `z.string().email()` |
| `Number` | — | `<Input type="number" />` | `z.coerce.number().int()` |
| `Decimal` | — | `<Input type="number" />` | `z.coerce.number()` |
| `Boolean` | — | `<Input type="checkbox" />` | `z.boolean()` |
| `Date` | — | `<Input type="date" />` | `z.string()` (ISO format) |
| `DateTime` | — | `<Input type="datetime-local" />` | `z.string()` (ISO format) |
| `Select` | Fixed values (e.g. `Draft, Active`) | `<Select items={[...]} />` | `z.enum([...])` |
| `Select` | `Entity entity → field` | `<Select.Async fetcher={...} />` | `z.string()` |
| `MultiSelect` | Fixed values | `<Combobox multiple items={[...]} />` | `z.array(z.enum([...]))` |
| `MultiSelect` | `Entity entity → field` | `<Combobox.Async multiple />` | `z.array(z.string())` |
| `Lookup` | `Entity entity → field` | `<Combobox.Async />` | `z.string()` (stores entity ID) |
| `MultiLookup` | `Entity entity → field` | `<Combobox.Async multiple />` | `z.array(z.string())` |
| `CreatableLookup` | `Entity entity → field` | `<Combobox.Async onCreateItem={...} />` | `z.string()` |
| `Autocomplete` | Fixed values | `<Autocomplete items={[...]} />` | `z.string()` |
| `Autocomplete` | `Entity entity → field` | `<Autocomplete.Async fetcher={...} />` | `z.string()` |

**How to determine the component:** Read the screen spec's Form Input Fields table. The `Field Type` column selects the component family. The `Options / Lookup` column determines whether to use the sync or async variant — if the value contains `entity →`, use the `.Async` variant and build a `fetcher` that queries that entity; if it's a comma-separated list, pass the values as static `items`. The text after `→` specifies the display field(s) for `mapItem`'s `label`.

### Validation Rules

- **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
- **Required: No** → `.optional()`

### Empty String Handling

RHF defaults text inputs to `""`. For optional fields, convert empty strings to `undefined` before submit:

```ts
parentId: values.parentId || undefined, // use || not ?? — empty string is not nullish
```

## Form / Field / Fieldset Components

> Full API reference: https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/form.md

app-shell provides native form components with automatic accessibility wiring.

### Basic Structure

```tsx
import { Form, Field } from "@tailor-platform/app-shell";

<Form onSubmit={handleSubmit(onSubmit)} noValidate>
  <Field.Root name="email" error={fieldState.error} {...fieldState}>
    <Field.Label>Email</Field.Label>
    <Field.Control type="email" {...field} />
    <Field.Error>{fieldState.error?.message}</Field.Error>
  </Field.Root>
  <button type="submit">Save</button>
</Form>
```

### Form Props

Key props (see [full API reference](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/form.md) for all props including `actionsRef`, `validationMode`):

- `onSubmit` — use with RHF's `handleSubmit`
- `onFormSubmit` — standalone mode (without RHF), receives parsed form values
- `errors` — `Record<string, string>` for external API errors keyed by field name
- `noValidate` — disable native browser validation (recommended)

### Field Sub-components

- `Field.Root` — context boundary. Accepts `name`, `error`, `isTouched`, `isDirty`, `invalid` (spread RHF's `fieldState` directly)
- `Field.Label` — accessible label with auto `htmlFor`
- `Field.Control` — styled `<input>`. Omit when using Select/Combobox as the control
- `Field.Description` — help text linked via `aria-describedby`
- `Field.Error` — error message. Use `match` for specific validity states

### React Hook Form Integration

Spread RHF's `fieldState` directly onto `Field.Root`:

```tsx
<Controller
  name="email"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Email</Field.Label>
      <Field.Control {...field} type="email" />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>
```

### Using Select/Combobox as Field Control

Omit `Field.Control` — these components auto-register with the Field context. Use the **standalone API** (props-driven), not the Parts API. Always wrap with RHF's `Controller`:

```tsx
// Select with string items
<Controller
  name="status"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Status</Field.Label>
      <Select
        items={["Draft", "Active", "Inactive"]}
        placeholder="Select status"
        value={field.value}
        onValueChange={(value) => field.onChange(value)}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

// Select with object items — store the ID, not the whole object
<Controller
  name="currencyCode"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Currency</Field.Label>
      <Select
        items={currencies}
        mapItem={(c) => ({ label: c.name, key: c.code })}
        placeholder="Select currency"
        value={currencies.find((c) => c.code === field.value) ?? null}
        onValueChange={(currency) => field.onChange(currency?.code ?? "")}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

// Combobox.Async — searchable, async-loaded options
<Controller
  name="supplierId"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Supplier</Field.Label>
      <Combobox.Async
        fetcher={async (query, { signal }) => {
          const res = await fetch(`/api/suppliers?q=${query ?? ""}`, { signal });
          return res.json();
        }}
        mapItem={(s) => ({ label: s.name, key: s.id })}
        placeholder="Search suppliers..."
        value={field.value ? suppliers.find((s) => s.id === field.value) ?? null : null}
        onValueChange={(supplier) => field.onChange(supplier?.id ?? "")}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>
```

**Key patterns for RHF + dropdown:**
- `value` — convert the stored form value (e.g. ID string) back to the item object for display
- `onValueChange` — extract the value to store (e.g. `item?.id`) and call `field.onChange`
- `mapItem` — `key` should be the entity's UUID (internal value stored in the form); `label` should be a human-readable identifier (e.g. name, code, or `"CODE — Name"`) that users can recognize. Never display raw UUIDs to users.
- For string items, `value`/`onValueChange` pass strings directly

### Fieldset

Group related fields with `Fieldset.Root` + `Fieldset.Legend`. Propagates `disabled` to all children.

## Input

Native input props. Works with RHF via `<Field.Control {...field} />` or standalone `<Input />`.

## Select / Combobox / Autocomplete

> API references: [Select](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/select.md) · [Combobox](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/combobox.md) · [Autocomplete](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/autocomplete.md)

### Choosing the Right Component

| Component        | Value type         | When to use                                      |
| ---------------- | ------------------ | ------------------------------------------------ |
| **Select**       | Discrete item      | Predefined options, no searching needed           |
| **Combobox**     | Discrete item      | Many options that need search/filter. Also: multi-select, creatable |
| **Autocomplete** | Raw input string   | Free-text input with suggestions (value = typed text, not an item) |

### Select — Standalone API

```tsx
import { Select, Field } from "@tailor-platform/app-shell";
import { Controller } from "react-hook-form";

// String items
<Select
  items={["Draft", "Active", "Inactive"]}
  placeholder="Select status"
  value={selected}
  onValueChange={setSelected}
/>

// Object items with mapItem
type Currency = { code: string; name: string };

<Select
  items={currencies}
  mapItem={(c) => ({ label: c.name, key: c.code })}
  placeholder="Select currency"
  onValueChange={(currency) => console.log(currency?.code)}
/>

// Async — fetcher called each time dropdown opens
<Select.Async
  fetcher={async ({ signal }) => {
    const res = await fetch("/api/currencies", { signal });
    return res.json();
  }}
  mapItem={(c) => ({ label: c.name, key: c.code })}
  placeholder="Select currency"
  onValueChange={setCurrency}
/>

// RHF Controller — string items
<Controller
  name="status"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Status</Field.Label>
      <Select
        items={["Draft", "Active", "Inactive"]}
        placeholder="Select status"
        value={field.value}
        onValueChange={(value) => field.onChange(value)}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

// RHF Controller — object items, store the code
<Controller
  name="currencyCode"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Currency</Field.Label>
      <Select
        items={currencies}
        mapItem={(c) => ({ label: c.name, key: c.code })}
        placeholder="Select currency"
        value={currencies.find((c) => c.code === field.value) ?? null}
        onValueChange={(currency) => field.onChange(currency?.code ?? "")}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

// RHF Controller — Select.Async
<Controller
  name="currencyCode"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Currency</Field.Label>
      <Select.Async
        fetcher={async ({ signal }) => {
          const res = await fetch("/api/currencies", { signal });
          return res.json();
        }}
        mapItem={(c) => ({ label: c.name, key: c.code })}
        placeholder="Select currency"
        value={field.value}
        onValueChange={(currency) => field.onChange(currency?.code ?? "")}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>
```

### Combobox — Standalone API

```tsx
import { Combobox, Field } from "@tailor-platform/app-shell";
import { Controller } from "react-hook-form";

// Basic searchable
<Combobox
  items={users}
  mapItem={(u) => ({ label: u.name, key: u.id })}
  placeholder="Search users..."
  value={selectedUser}
  onValueChange={setSelectedUser}
/>

// Multi-select
<Combobox
  items={tags}
  mapItem={(t) => ({ label: t.name, key: t.id })}
  multiple
  placeholder="Select tags..."
  onValueChange={setSelectedTags}
/>

// Creatable — user can add new items (T must be an object type)
<Combobox
  items={tags}
  mapItem={(t) => ({ label: t.name, key: t.id })}
  onCreateItem={(value) => {
    const newTag = { id: crypto.randomUUID(), name: value };
    setTags((prev) => [...prev, newTag]);
    return newTag;
  }}
  placeholder="Search or create tag..."
/>

// Async — fetcher called on each keystroke (debounced)
<Combobox.Async
  fetcher={async (query, { signal }) => {
    const res = await fetch(`/api/users?q=${query ?? ""}`, { signal });
    return res.json();
  }}
  mapItem={(u) => ({ label: u.name, key: u.id })}
  placeholder="Search users..."
  onValueChange={setUser}
/>

// RHF Controller — basic searchable
<Controller
  name="userId"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>User</Field.Label>
      <Combobox
        items={users}
        mapItem={(u) => ({ label: u.name, key: u.id })}
        placeholder="Search users..."
        value={users.find((u) => u.id === field.value) ?? null}
        onValueChange={(user) => field.onChange(user?.id ?? "")}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

// RHF Controller — Combobox.Async
<Controller
  name="supplierId"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Supplier</Field.Label>
      <Combobox.Async
        fetcher={async (query, { signal }) => {
          const res = await fetch(`/api/suppliers?q=${query ?? ""}`, { signal });
          return res.json();
        }}
        mapItem={(s) => ({ label: s.name, key: s.id })}
        placeholder="Search suppliers..."
        value={field.value ? suppliers.find((s) => s.id === field.value) ?? null : null}
        onValueChange={(supplier) => field.onChange(supplier?.id ?? "")}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>
```

### Autocomplete — Standalone API

Value is the **raw input string**, not a discrete item:

```tsx
import { Autocomplete, Field } from "@tailor-platform/app-shell";
import { Controller } from "react-hook-form";

<Autocomplete
  items={suggestions}
  placeholder="Type an address..."
  value={query}
  onValueChange={setQuery}
/>

// RHF Controller — basic
<Controller
  name="address"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Address</Field.Label>
      <Autocomplete
        items={suggestions}
        placeholder="Type an address..."
        value={field.value}
        onValueChange={(value) => field.onChange(value)}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

// RHF Controller — Autocomplete.Async
<Controller
  name="address"
  control={control}
  render={({ field, fieldState }) => (
    <Field.Root {...fieldState}>
      <Field.Label>Address</Field.Label>
      <Autocomplete.Async
        fetcher={async (query, { signal }) => {
          if (!query) return [];
          const res = await fetch(`/api/address-lookup?q=${encodeURIComponent(query)}`, { signal });
          return res.json();
        }}
        placeholder="Start typing an address..."
        value={field.value}
        onValueChange={(address) => field.onChange(address)}
      />
      <Field.Error match={fieldState.invalid}>
        {fieldState.error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>
```

### mapItem Reference

When items are objects, `mapItem` tells the component how to display them:

```ts
mapItem={(item) => ({
  label: item.name,       // display text + filter target
  key: item.id,           // React key (defaults to label)
  render: <CustomJSX />,  // optional: custom dropdown row JSX
})}
```

### Async Fetcher Signature

All `.Async` variants share the same pattern (`Select.Async` omits `query`):

```ts
// Combobox.Async / Autocomplete.Async
type Fetcher<T> = (query: string | null, options: { signal: AbortSignal }) => Promise<T[]>;

// Select.Async (no search — fetcher has no query param)
type Fetcher<T> = (options: { signal: AbortSignal }) => Promise<T[]>;
```

`query` is `null` when the dropdown opens or input is cleared — return initial/default items.


