180 lines
6.3 KiB
Markdown
180 lines
6.3 KiB
Markdown
# Resource Page
|
|
|
|
The `ResourcePage` component is the primary generic shell for any CRUD list page. It composes layout, table, dialog, and state management into a single, reusable component.
|
|
|
|
---
|
|
|
|
## Files
|
|
|
|
| File | Description |
|
|
|---|---|
|
|
| `shared/data-view/resource-page/resource-page.tsx` | The React component |
|
|
| `shared/data-view/resource-page/use-resource-page.ts` | The state/logic hook |
|
|
| `shared/data-view/resource-page/index.ts` | Public exports |
|
|
|
|
---
|
|
|
|
## `<ResourcePage>` Component
|
|
|
|
### Props
|
|
|
|
```ts
|
|
type ResourcePageProps<TClient extends ResourcePageClient> = {
|
|
// Required
|
|
title: string // Used for the "Add" button label and dialog title
|
|
routeKey: string // React Query cache key (e.g. CUSTOMER_ROUTES.INDEX)
|
|
getClient: (api: ApiInstance) => TClient // Selects the domain client from the API
|
|
columns: // Column definitions or a factory receiving helpers
|
|
| ColumnDef<ResourceItem<TClient>>[]
|
|
| ((helpers: ResourcePageColumnHelpers<TClient>) => ColumnDef<ResourceItem<TClient>>[])
|
|
renderForm: (props: ResourceFormProps<TClient>) => React.ReactNode
|
|
|
|
// Optional
|
|
pageTitle?: string // Heading text (defaults to undefined)
|
|
queryOptions?: Omit<UseQueryOptions<...>, "queryKey" | "queryFn">
|
|
}
|
|
```
|
|
|
|
### `ResourcePageColumnHelpers`
|
|
|
|
Passed to the `columns` callback, providing three pre-wired helpers:
|
|
|
|
| Helper | Type | Description |
|
|
|---|---|---|
|
|
| `actionsColumn(options?)` | `ColumnDef` | Pre-built Edit + Delete dropdown column |
|
|
| `openEdit(row)` | `(row: TItem) => void` | Opens dialog with row pre-filled |
|
|
| `deleteItem(id)` | `(id: string) => Promise<unknown>` | Deletes with toast + confirmation |
|
|
|
|
The `actionsColumn` factory (`createActionsColumn`) can be further customized:
|
|
```ts
|
|
actionsColumn({
|
|
onEdit: (row) => customOpen(row),
|
|
onDelete: async (row) => {
|
|
// completely override delete behavior
|
|
},
|
|
})
|
|
```
|
|
|
|
### `ResourceFormProps`
|
|
|
|
Passed to the `renderForm` callback:
|
|
|
|
| Prop | Type | Description |
|
|
|---|---|---|
|
|
| `resourceId` | `string \| null` | `null` on create; the item's `id` string on edit |
|
|
| `initialData` | `TItem \| null` | The full row object on edit (from the table's in-memory state) |
|
|
| `onSuccess` | `() => void` | Call this after a successful mutation to refresh the list |
|
|
|
|
---
|
|
|
|
## `useResourcePage` Hook
|
|
|
|
Encapsulates all state and logic. Returned by `ResourcePage` internally but also exported for use in custom page layouts.
|
|
|
|
```ts
|
|
const page = useResourcePage<MyClient>({ routeKey, getClient, queryOptions })
|
|
```
|
|
|
|
### Returns
|
|
|
|
| Key | Type | Description |
|
|
|---|---|---|
|
|
| `data` | `CrudListResponse<TClient>` | Raw API response |
|
|
| `isLoading` | `boolean` | True while initial fetch is in progress |
|
|
| `pagination` | `DataViewPaginationState` | `{ page, pageSize, pageCount, total }` |
|
|
| `sorting` | `DataViewSorting` | Current sort state |
|
|
| `handleChange` | `(event: DataViewChangeEvent) => void` | Handles pagination and sort events |
|
|
| `invalidateQuery` | `() => void` | Busts the React Query cache for the current query key |
|
|
| `selectedItem` | `TItem \| null` | The row being edited (populated by `openEdit`) |
|
|
| `openEdit(row)` | fn | Sets `selectedItem` and opens dialog |
|
|
| `openCreate()` | fn | Clears `selectedItem` and opens dialog |
|
|
| `openDialog(id?)` | fn | Low-level dialog open (sets `?dialog=true&resourceId=id` in URL) |
|
|
| `closeDialog()` | fn | Closes dialog (removes URL params) |
|
|
| `isDialogOpen` | `boolean` | Current dialog open state |
|
|
| `dialogResourceId` | `string \| null` | Current resource ID from URL |
|
|
| `deleteItem(id)` | `(id: string) => Promise<unknown>` | Mutation that destroys a resource |
|
|
| `actionsColumn(options?)` | fn | Generates the actions `ColumnDef` |
|
|
| `client` | `TClient` | The domain API client |
|
|
| `api` | `ApiInstance` | Full authenticated API object |
|
|
|
|
---
|
|
|
|
## `FormDialog` — URL-Driven Dialog
|
|
|
|
`FormDialog` and its companion hook `useFormDialog` manage dialog open/close state **via URL query parameters**:
|
|
|
|
| URL param | Value | Meaning |
|
|
|---|---|---|
|
|
| `dialog` | `true` / absent | Dialog open/closed |
|
|
| `resourceId` | string / absent | ID of the item being edited |
|
|
|
|
This means sharing or refreshing the URL with `?dialog=true&resourceId=5` will reopen the dialog on the same item (as long as `initialData` is in memory—see [Enhancement Plan](./enhancement-plan.md)).
|
|
|
|
### `useFormDialog`
|
|
|
|
```ts
|
|
const { isOpen, resourceId, open, close } = useFormDialog()
|
|
|
|
open("5") // opens dialog in edit mode
|
|
open() // opens dialog in create mode
|
|
close() // closes dialog, clears resourceId
|
|
```
|
|
|
|
---
|
|
|
|
## `ConfirmDialog` — Imperative Async Confirm
|
|
|
|
`ConfirmDialog` is a **singleton store-driven dialog** mounted once in the root layout. It exposes an imperative `confirm()` function:
|
|
|
|
```ts
|
|
import { confirm } from "@/shared/components/confirm-dialog"
|
|
|
|
const ok = await confirm({
|
|
title: "Delete this item?",
|
|
description: "This action cannot be undone.",
|
|
confirmLabel: "Delete",
|
|
variant: "destructive", // shows destructive styling + trash icon
|
|
})
|
|
|
|
if (ok) { /* proceed */ }
|
|
```
|
|
|
|
> **Important:** `<ConfirmDialog />` must be rendered once in the root layout. If it is not mounted, `confirm()` will open a dialog that is never displayed.
|
|
|
|
---
|
|
|
|
## `createActionsColumn`
|
|
|
|
A standalone factory for generating the standard Edit + Delete column:
|
|
|
|
```ts
|
|
import { createActionsColumn } from "@/shared/data-view/table-view"
|
|
|
|
createActionsColumn<MyItem>({
|
|
onEdit: (row) => openEdit(row),
|
|
onDelete: async (row) => {
|
|
const confirmed = await confirm({ ... })
|
|
if (confirmed) await deleteItem(String(row.id))
|
|
},
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Component Tree
|
|
|
|
```
|
|
<ResourcePage>
|
|
└─ <DashboardPage header={...} title={pageTitle}>
|
|
├─ <DashboardHeader>
|
|
│ └─ <FormDialog title={title}> ← "Add Customer" button + Dialog shell
|
|
│ └─ renderForm(resourceId) ← Feature-specific form
|
|
└─ <Card>
|
|
└─ <CardContent>
|
|
└─ <DataTable columns data pagination sorting onChange isLoading>
|
|
├─ <DataViewProvider> ← Shares state via context
|
|
├─ TanStack Table (manual pagination + sorting)
|
|
├─ Skeleton rows while loading
|
|
└─ <DataViewPagination> ← Page controls + rows-per-page
|
|
```
|