# DetailView Pattern

## File Structure

```bash
[id]/
  page.tsx                          # route entry — fetches data, composes fragments
  components/
    <entity>-detail.tsx             # DescriptionCard with field definitions
    <entity>-actions.tsx            # ActionPanel with mutation actions
```

## page.tsx

```tsx
import {
  Layout,
  useParams,
  type AppShellPageProps,
} from "@tailor-platform/app-shell";
import { useQuery } from "urql";
import { UserDetail, UserDetailFragment } from "./components/user-detail";
import { UserActions, UserActionsFragment } from "./components/user-actions";
import { graphql } from "@/graphql";
import { ErrorFallback } from "@/components/composed/error-fallback";
import { Loading } from "@/components/composed/loading";

const UserQuery = graphql(
  `
    query User($id: ID!) {
      user(id: $id) {
        ...UserDetail
        ...UserActions
      }
    }
  `,
  [UserDetailFragment, UserActionsFragment],
);

const UserPage = () => {
  const { id } = useParams();
  const [{ data, error, fetching }, reexecuteQuery] = useQuery({
    query: UserQuery,
    variables: { id: id! },
  });

  if (fetching) return <Loading />;
  if (error || !data?.user)
    return (
      <ErrorFallback
        title="Failed to load user"
        message="An error occurred while fetching the user details."
        onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
      />
    );

  return (
    <Layout>
      <Layout.Header title="User Detail" />
      <Layout.Column>
        <UserDetail user={data.user} />
      </Layout.Column>
      <Layout.Column>
        <UserActions user={data.user} />
      </Layout.Column>
    </Layout>
  );
};

UserPage.appShellPageProps = {
  meta: { title: "User Detail" },
} satisfies AppShellPageProps;
export default UserPage;
```

## `<entity>-detail.tsx` — DescriptionCard

```tsx
import { DescriptionCard } from "@tailor-platform/app-shell";
import { graphql, type FragmentOf, readFragment } from "@/graphql";

export const UserDetailFragment = graphql(`
  fragment UserDetail on User {
    id
    name
    email
    status
    createdAt
    updatedAt
  }
`);

export const UserDetail = ({
  user: userFragment,
}: {
  user: FragmentOf<typeof UserDetailFragment>;
}) => {
  const user = readFragment(UserDetailFragment, userFragment);
  return (
    <DescriptionCard
      data={user}
      title="User Information"
      columns={3}
      fields={[
        { key: "name", label: "Name" },
        { key: "email", label: "Email", meta: { copyable: true } },
        {
          key: "status",
          label: "Status",
          type: "badge",
          meta: {
            badgeVariantMap: {
              ACTIVE: "success",
              PENDING: "warning",
              INACTIVE: "neutral",
            },
          },
        },
        { type: "divider" },
        {
          key: "createdAt",
          label: "Created At",
          type: "date",
          meta: { dateFormat: "medium" },
        },
        {
          key: "updatedAt",
          label: "Updated At",
          type: "date",
          meta: { dateFormat: "relative" },
        },
      ]}
    />
  );
};
```

### DescriptionCard Field Types

| `type`      | Description                       | `meta` options                                                               |
| ----------- | --------------------------------- | ---------------------------------------------------------------------------- |
| (omitted)   | Plain text                        | `copyable: true`                                                             |
| `"badge"`   | Status badge with variant mapping | `badgeVariantMap: { VALUE: "success" \| "warning" \| "neutral" \| "error" }` |
| `"date"`    | Formatted date/time               | `dateFormat: "short" \| "medium" \| "long" \| "relative"`                    |
| `"divider"` | Horizontal rule                   | —                                                                            |

### Multiple DescriptionCards

When an entity has a logically distinct secondary collection (e.g. taxonomy assignments, related items), render it as a second `DescriptionCard` stacked below the primary one.

## `<entity>-actions.tsx` — ActionPanel

```tsx
import { ActionPanel, useNavigate } from "@tailor-platform/app-shell";
import { useMutation } from "urql";
import { Pencil, UserX, UserCheck } from "lucide-react";
import { graphql, type FragmentOf, readFragment } from "@/graphql";
import { useToast } from "@/hooks/use-toast";

const DeactivateUserMutation = graphql(`
  mutation DeactivateUser($userId: String!) {
    deactivateUser(userId: $userId) {
      id
      status
    }
  }
`);

export const UserActionsFragment = graphql(`
  fragment UserActions on User {
    id
    status
  }
`);

export const UserActions = ({
  user: userFragment,
}: {
  user: FragmentOf<typeof UserActionsFragment>;
}) => {
  const user = readFragment(UserActionsFragment, userFragment);
  const navigate = useNavigate();
  const toast = useToast();
  const [{ fetching: deactivating }, deactivateUser] = useMutation(
    DeactivateUserMutation,
  );

  return (
    <ActionPanel
      title="Actions"
      actions={[
        {
          key: "edit",
          label: "Edit User",
          icon: <Pencil />,
          onClick: () => navigate("edit"),
        },
        ...(user.status === "ACTIVE"
          ? [
              {
                key: "deactivate",
                label: "Deactivate User",
                icon: <UserX />,
                loading: deactivating,
                onClick: () =>
                  deactivateUser({ userId: user.id }).then((result) => {
                    if (result.error) {
                      toast.error(result.error);
                      return;
                    }
                    toast.success("User deactivated successfully");
                  }),
              },
            ]
          : []),
      ]}
    />
  );
};
```

### Error Handling in Actions

Always handle mutation results with `useToast`:

```tsx
import { useToast } from "@/hooks/use-toast";

const toast = useToast();

// In onClick:
onClick: () =>
  someMutation({ ... }).then((result) => {
    if (result.error) {
      toast.error(result.error); // accepts CombinedError directly
      return;
    }
    toast.success("Action completed successfully");
  }),
```

- `toast.error(result.error)` accepts `CombinedError` from urql directly — no manual message extraction needed
- `toast.success(message)` for successful operations
- Import from `@/hooks/use-toast`, not from `@tailor-platform/app-shell` directly (ESLint enforced)

### ActionItem Fields

| Field      | Type                          | Description                            |
| ---------- | ----------------------------- | -------------------------------------- |
| `key`      | `string`                      | React key                              |
| `label`    | `string`                      | Visible row label                      |
| `icon`     | `ReactNode`                   | Icon from `lucide-react`               |
| `onClick`  | `() => void \| Promise<void>` | Use `useNavigate()` for routing        |
| `disabled` | `boolean?`                    | Disable the row                        |
| `loading`  | `boolean?`                    | Show spinner, make row non-interactive |
