2026-03-27 16:03:58 +03:00

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:

  1. Define RESOURCE_ROUTES const with INDEX and BY_ID routes (and any extras)
  2. Create a class extending CrudClient with the route types
  3. Add any domain-specific methods beyond standard CRUD
  4. Register in packages/api/src/clients/index.ts (export class + routes)
  5. Register in packages/api/src/api.ts (import + add to createApi())

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:

  1. Define relationFieldSchema (reuse if already exported) for foreign-key fields
  2. Build the Zod object schema with all form fields
  3. Use .optional() for non-required fields, .min(1, "...") for required strings
  4. Use z.union([z.string().email(...), z.literal("")]).optional() for optional emails
  5. Export the schema, the inferred type, and relationFieldSchema if new

Step 4: Create Form Component

Read the Form Reference for the complete template.

Create apps/dashboard/modules/<feature>/<feature>-form.tsx:

  1. Define default values matching the schema
  2. Create mapToFormValues(data) — transforms API shape → form shape using toRelation()
  3. Create mapFormToPayload(values) — transforms form shape → API shape using toId()
  4. Use useResourceForm() for form initialization + edit pre-filling
  5. Use useFormMutation() for submit with automatic validation error mapping
  6. Render with Rhform + RhfTextField / RhfSelectField / RhfAsyncSelectField etc.
  7. 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:

  1. Add "use client" directive
  2. Import ResourcePage, ColumnHeader, the form, client type, and routes
  3. Configure: pageTitle, title, routeKey, getClient, columns, renderForm
  4. Use columns callback to receive actionsColumn helper
  5. Add sortable column headers with <ColumnHeader>
  6. 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.ts and api.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 '@repo/api'
import { <RESOURCE>_ROUTES } from '@repo/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/ and shared/components/form/fields/
  • Add new hooks in shared/hooks/
  • Extend ResourcePage props 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