6.3 KiB
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
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:
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.
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).
useFormDialog
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:
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:
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