7.9 KiB
| name | description |
|---|---|
| crud-page | Create CRUD resource pages, forms, schemas, and API clients for the carage-erp dashboard. Use when: adding a new resource page, creating a CRUD feature, building a list/create/edit/delete page, scaffolding a new module, adding a new entity to the dashboard. Covers API client, Zod schema, form component, and page component creation. |
CRUD Page Generator
Create fully functional CRUD resource pages following the established codebase patterns. This skill covers the full stack: API client → Zod schema → form component → page component.
When to Use
- User asks to create a new resource/entity page (e.g. "create a vendors page", "add invoices CRUD")
- User asks to add list/create/edit/delete functionality for a domain entity
- User asks to scaffold a new module or feature page
- User wants to extend the dashboard with a new data management page
Decision: ResourcePage vs Manual DataTable
Use ResourcePage (preferred) when the resource needs full CRUD (list + create + edit + delete in a dialog). This is the standard pattern.
Use manual DataTable + useDataTableQuery only when the page is read-only or has highly custom layout needs.
Always prefer ResourcePage unless the user explicitly needs something different.
Procedure
Follow these steps in order. Each step produces one file. Check the reference files for complete templates and patterns.
Step 1: Check if API Client Exists
Look in packages/api/src/clients/ for an existing client. Also check packages/api/src/clients/index.ts for all registered clients, and packages/api/src/api.ts for the factory.
- If client exists → skip to Step 3
- If client doesn't exist → continue to Step 2
Step 2: Create API Client
Read the API Client Reference for patterns and template.
Create the domain client file at packages/api/src/clients/<resource>.ts:
- Define
RESOURCE_ROUTESconst withINDEXandBY_IDroutes (and any extras) - Create a class extending
CrudClientwith the route types - Add any domain-specific methods beyond standard CRUD
- Register in
packages/api/src/clients/index.ts(export class + routes) - Register in
packages/api/src/api.ts(import + add tocreateApi())
Route pattern: "/api/<plural-resource>" for INDEX, "/api/<plural-resource>/{id}" for BY_ID.
IMPORTANT: Routes must exist in the OpenAPI schema (packages/api/types/index.ts) for type safety. If the route doesn't exist in the schema yet, inform the user and ask if they want to proceed with any types or wait for schema update.
Step 3: Create Zod Schema
Read the Schema Reference for patterns and template.
Create apps/dashboard/modules/<feature>/<feature>.schema.ts:
- Define
relationFieldSchema(reuse if already exported) for foreign-key fields - Build the Zod object schema with all form fields
- Use
.optional()for non-required fields,.min(1, "...")for required strings - Use
z.union([z.string().email(...), z.literal("")]).optional()for optional emails - Export the schema, the inferred type, and
relationFieldSchemaif new
Step 4: Create Form Component
Read the Form Reference for the complete template.
Create apps/dashboard/modules/<feature>/<feature>-form.tsx:
- Define default values matching the schema
- Create
mapToFormValues(data)— transforms API shape → form shape usingtoRelation() - Create
mapFormToPayload(values)— transforms form shape → API shape usingtoId() - Use
useResourceForm()for form initialization + edit pre-filling - Use
useFormMutation()for submit with automatic validation error mapping - Render with
Rhform+RhfTextField/RhfSelectField/RhfAsyncSelectFieldetc. - Include error alert, submit button with loading/edit states
Step 5: Create Page Component
Read the Page Reference for the complete template.
Create apps/dashboard/app/(authenticated)/<section>/<feature>/page.tsx:
- Add
"use client"directive - Import
ResourcePage,ColumnHeader, the form, client type, and routes - Configure:
pageTitle,title,routeKey,getClient,columns,renderForm - Use
columnscallback to receiveactionsColumnhelper - Add sortable column headers with
<ColumnHeader> - Include
actionsColumn()as last column
Step 6: Verify
- Ensure all imports resolve
- Check that route constants match OpenAPI paths
- Confirm the client is registered in both
clients/index.tsandapi.ts
Key Conventions
Naming
| Item | Pattern | Example |
|---|---|---|
| Client file | packages/api/src/clients/<kebab-resource>.ts |
job-cards.ts |
| Client class | <PascalResource>Client |
JobCardsClient |
| Routes const | <UPPER_SNAKE>_ROUTES |
JOB_CARD_ROUTES |
| Schema file | modules/<feature>/<feature>.schema.ts |
job-card.schema.ts |
| Form file | modules/<feature>/<feature>-form.tsx |
job-card-form.tsx |
| Page file | app/(authenticated)/<section>/<feature>/page.tsx |
sales/job-cards/page.tsx |
| Zod schema | <camelFeature>FormSchema |
jobCardFormSchema |
| Form values type | <PascalFeature>FormValues |
JobCardFormValues |
| Form component | <PascalFeature>Form |
JobCardForm |
| Page component | <PascalFeature>Page (default export) |
JobCardsPage |
Relation Fields (Foreign Keys)
- Stored in form as
{ value: string, label: string } | null - Use
toRelation(id, name)to convert API data → form value - Use
toId(relation)to convert form value → API payload - Schema uses
relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable() - Rendered with
<RhfAsyncSelectField>(fetches options via React Query)
Async Select Pattern
const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name })
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
<RhfAsyncSelectField
name="field_name"
label="Field Label"
placeholder="Select..."
queryKey={["query-key"]}
listFn={() => api.resource.listSomething()}
mapOption={mapLookupOption}
{...STORE_OBJECT}
/>
Available Form Field Components
| Component | Use For |
|---|---|
RhfTextField |
Text, email, phone, URL inputs |
RhfTextareaField |
Multi-line text |
RhfCheckboxField |
Boolean toggles |
RhfSelectField |
Static option dropdowns |
RhfAsyncSelectField |
Server-fetched single-select combobox |
RhfAsyncMultiSelectField |
Server-fetched multi-select combobox |
Imports Cheat Sheet
// Page
import { ResourcePage } from '@/shared/data-view/resource-page'
import { ColumnHeader } from '@/shared/data-view/table-view'
import type { <Resource>Client } from '@garage/api'
import { <RESOURCE>_ROUTES } from '@garage/api'
// Form
import { Rhform, RhfTextField, RhfSelectField, RhfAsyncSelectField } from "@/shared/components/form"
import { useResourceForm } from "@/shared/hooks/use-resource-form"
import { useFormMutation } from "@/shared/hooks/use-form-mutation"
import { useAuthApi } from "@/shared/useApi"
import { toRelation, toId } from "@/shared/lib/utils"
import { Button } from "@/shared/components/ui/button"
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
import { FieldGroup } from "@/shared/components/ui/field"
import { toast } from "sonner"
// Schema
import { z } from "zod"
Extending the CRUD Codebase
If a feature requires functionality not covered by existing utilities (e.g. inline editing, tab-based forms, file uploads, nested resources), you are encouraged to extend the shared infrastructure:
- Add new form field components in
shared/components/form/controls/andshared/components/form/fields/ - Add new hooks in
shared/hooks/ - Extend
ResourcePageprops if needed - Add new column helper factories in
shared/data-view/table-view/ - Keep extensions generic and reusable — follow the same patterns as existing code