# Frontend Pages

### GraphQL Setup

All operations use gql.tada for type safety:

```ts
import { graphql, type FragmentOf, readFragment } from "@/graphql";
```

Built-in queries (`gqlOperations: "query"`) generate:

- `<Entity>(id: ID!)` — get by ID
- `<entities>` — list with connection pattern (`edges { node { ... } }`)

Mutations are defined by custom resolvers.

### ListView

Key points:

- Query uses connection pattern: `edges { node { ...Fragment } }`
- Two-level fragments: row fragment for items, table fragment for the connection
- `EmptyState` when `edges.length === 0`
- Columns map to screen spec's "Required Columns"
- `Layout` with title and create action button

### DetailView

See **[detail-view.md](detail-view.md)** for the full pattern including `DescriptionCard` field types and `ActionPanel` usage.

### Form Pattern

Create and edit forms share the same structure:

- **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

| Field Type | Component                 | Zod Schema                      |
| ---------- | ------------------------- | ------------------------------- |
| Email      | `<Input type="email" />`  | `z.email()`                     |
| Text       | `<Input />`               | `z.string()`                    |
| Textarea   | `<textarea />`            | `z.string()`                    |
| Dropdown   | `<Select />`              | `z.string()` or `z.enum([...])` |
| Date       | `<Input type="date" />`   | `z.string()` (ISO format)       |
| Number     | `<Input type="number" />` | `z.coerce.number()`             |
| Checkbox   | `<Checkbox />`            | `z.boolean()`                   |

#### Validation

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

#### Empty String Handling for Optional Fields

React Hook Form defaults text inputs to `""`. For optional fields (e.g., `parentId`), convert empty strings to `undefined` before submitting to avoid backend errors. Use `||` not `??` — empty string is not nullish:

```ts
parentId: values.parentId || undefined,
```

### Shared Component Patterns

#### Fragment Collocation

Each component defines and exports its own GraphQL fragment. The parent page imports it and includes it in the query:

```tsx
// components/user-card.tsx
export const UserCardFragment = graphql(`
  fragment UserCard on User {
    id
    name
    email
  }
`);

export const UserCard = ({ user }: { user: FragmentOf<typeof UserCardFragment> }) => {
  const data = readFragment(UserCardFragment, user);
  return <div>{data.name}</div>;
};

// page.tsx
const UserQuery = graphql(
  `
    query User($id: ID!) {
      user(id: $id) {
        ...UserCard
      }
    }
  `,
  [UserCardFragment],
);
```

#### App.tsx

Refer to existing examples in the codebase for `AuthGuard`, `GraphQLProvider`, auth client setup, and sidebar configuration.

- Required env vars: `VITE_TAILOR_APP_URL`, `VITE_TAILOR_CLIENT_ID`

#### Module-level page.tsx (required)

Every module directory must have a `page.tsx` that redirects to the first child. Use **absolute paths** to avoid double-path bugs:

```tsx
useEffect(() => {
  void navigate("/item-management/item", { replace: true });
}, [navigate]);
```

#### Page conventions

Every page component must:

1. Be the **default export** of `page.tsx`
2. Set `appShellPageProps` with at least `meta.title`
