129 lines
6.5 KiB
Markdown
129 lines
6.5 KiB
Markdown
# CRUD Pattern — Overview
|
|
|
|
This document describes the full-stack CRUD pattern used across the dashboard. Every resource page (Customers, Vendors, Employees, etc.) is built from the same thin layer of generic, composable utilities.
|
|
|
|
---
|
|
|
|
## Architecture Layers
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ Feature Page (apps/dashboard/app/…/page.tsx) │
|
|
│ • Declares columns, title, routeKey, getClient, renderForm │
|
|
└──────────────────┬───────────────────────────────────────────────┘
|
|
│ uses
|
|
┌──────────────────▼───────────────────────────────────────────────┐
|
|
│ ResourcePage (shared/data-view/resource-page) │
|
|
│ • Combines DashboardPage + FormDialog + DataTable │
|
|
│ • Delegates state to useResourcePage │
|
|
└──────┬───────────────────────────────┬────────────────────────────┘
|
|
│ │
|
|
┌──────▼──────────────┐ ┌────────────▼───────────────────────────┐
|
|
│ useResourcePage │ │ DataTable (shared/data-view/table-view)│
|
|
│ • useDataTableQuery │ │ • TanStack Table (manual mode) │
|
|
│ • form dialog state │ │ • Pagination, sorting, skeleton │
|
|
│ • delete mutation │ │ • DataViewProvider context │
|
|
└──────┬──────────────┘ └────────────────────────────────────────┘
|
|
│
|
|
┌──────▼──────────────────────────────────────────────────────────┐
|
|
│ useAuthApi → createApi() → CrudClient → openapi-fetch │
|
|
│ (packages/api — fully type-safe from OpenAPI schema) │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
┌──────▼──────────────────────────────────────────────────────────┐
|
|
│ Feature Form (modules/<feature>/<feature>-form.tsx) │
|
|
│ • useResourceForm (react-hook-form + Zod + optional fetch) │
|
|
│ • useMutation (create / update) │
|
|
│ • RhfTextField / RhfSelectField / RhfAsyncSelectField … │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Key Files
|
|
|
|
| Layer | File | Purpose |
|
|
|---|---|---|
|
|
| Feature page | `app/(authenticated)/sales/customers/page.tsx` | Minimal feature config |
|
|
| Generic page shell | `shared/data-view/resource-page/resource-page.tsx` | Layout + wiring |
|
|
| Page logic hook | `shared/data-view/resource-page/use-resource-page.ts` | State + mutations |
|
|
| Table | `shared/data-view/table-view/data-table.tsx` | TanStack Table UI |
|
|
| Table data hook | `shared/data-view/table-view/use-data-table-query.ts` | React Query + URL state |
|
|
| Form dialog | `shared/components/form-dialog.tsx` | URL-driven dialog trigger |
|
|
| Confirm dialog | `shared/components/confirm-dialog.tsx` | Imperative async confirm |
|
|
| Form state hook | `shared/hooks/use-resource-form.ts` | RHF + Zod + optional re-fetch |
|
|
| Form field wrappers | `shared/components/form/` | RhfTextField, RhfSelectField, … |
|
|
| API layer | `packages/api/src/infra/crud-client.ts` | Generic CRUD HTTP client |
|
|
| Auth API hook | `shared/useApi.ts` | Creates authenticated API instance |
|
|
|
|
---
|
|
|
|
## Data Flow — Read (LIST)
|
|
|
|
```
|
|
URL params change (page / sort)
|
|
→ useDataTableQuery re-runs queryFn
|
|
→ client.list({ page, per_page, sort_by, sort_order })
|
|
→ CrudClient.list()
|
|
→ ApiClient.get(indexRoute, { query })
|
|
→ openapi-fetch GET /api/customers
|
|
← { data: [...], meta: { last_page, total, ... } }
|
|
→ DataTable renders rows + pagination
|
|
```
|
|
|
|
## Data Flow — Write (CREATE / UPDATE)
|
|
|
|
```
|
|
User fills form → submits
|
|
→ useMutation mutationFn
|
|
→ api.customers.create(payload) or .update(id, payload)
|
|
→ CrudClient.create() / .update()
|
|
→ ApiClient.post() / .put()
|
|
→ openapi-fetch POST|PUT /api/customers[/{id}]
|
|
← 200/201 response
|
|
→ onSuccess: invalidateQuery() → list refreshes
|
|
→ onError: ApiError.validationErrors → form.setError(field, msg)
|
|
```
|
|
|
|
## Data Flow — Delete
|
|
|
|
```
|
|
User clicks Delete in actions column
|
|
→ confirm({ title, description, variant: "destructive" }) → awaits boolean
|
|
→ if confirmed: deleteItem(id)
|
|
→ useMutation → client.destroy(id)
|
|
→ ApiClient.delete()
|
|
← success
|
|
→ invalidateQuery()
|
|
```
|
|
|
|
---
|
|
|
|
## Creating a New Feature Page
|
|
|
|
To add a new resource page following this pattern:
|
|
|
|
1. Create schema: `modules/<feature>/<feature>.schema.ts`
|
|
2. Create form: `modules/<feature>/<feature>-form.tsx`
|
|
3. Create page: `app/(authenticated)/<section>/<feature>/page.tsx`
|
|
|
|
The page needs only 5 props on `<ResourcePage>`:
|
|
|
|
```tsx
|
|
<ResourcePage<MyClient>
|
|
title="My Resource"
|
|
pageTitle="My Resources"
|
|
routeKey={MY_ROUTES.INDEX}
|
|
getClient={(api) => api.myClient}
|
|
columns={({ actionsColumn }) => [
|
|
{ accessorKey: "name", header: () => <ColumnHeader ... /> },
|
|
actionsColumn(),
|
|
]}
|
|
renderForm={({ resourceId, initialData, onSuccess }) => (
|
|
<MyForm resourceId={resourceId} initialData={initialData} onSuccess={onSuccess} />
|
|
)}
|
|
/>
|
|
```
|
|
|
|
See the [Resource Page](./resource-page.md), [Data Fetching](./data-fetching.md), and [Form System](./form-system.md) docs for details on each layer.
|