garage-erp/docs/dashboard/crud/resource-page.md
2026-03-27 16:20:46 +03:00

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