AdminGuides

SmartCrudPage Complete Guide

TL;DR (3 Steps to Generate a CRUD Page)

  1. Create Model → 2) createCrudActions Generate Actions → 3) Import SmartCrudPage + fieldsConfig in Page

Minimal Example (Can Run Directly in Your Module):

// app/(admin)/actions/demo/crud-action.demo.js
'use server'
import { createCrudActions } from '@/lib/core/crud-helper'
export const { getList, getDetail, create, update, delete: del } = createCrudActions({
  modelName: 'demo', primaryKey: 'id', softDelete: true,
  fields: { creatable: ['name'], updatable: ['name'], searchable: ['name'] },
})
// app/(admin)/admin/demo/demos/page.js
'use client'
import dynamic from 'next/dynamic'
import * as actions from '@/app/(admin)/actions/demo/crud-action.demo'
const SmartCrudPage = dynamic(() => import('@/components/admin/smart-crud-page'), { ssr: false })
const fieldsConfig = [{ key: 'name', title: 'Name', type: 'text', search: { mode: 'like' }, form: { required: true } }]
export default () => <SmartCrudPage title="Example Management" fieldsConfig={fieldsConfig} actions={actions} enableCreate />

Complete Guide to Configuration-Driven CRUD

Component List · Configuration Spec · Page Parameters · CRUD Practice


🎯 Overview

SmartCrudPage + SmartForm + createCrudActions form the admin "trinity": one fieldsConfig drives table, form, search, and details simultaneously, working with Server Actions to automatically handle permissions, validation, and logging. A CRUD page can be completed in 10-30 minutes.


🧩 Component List

NameLocationPurposeKey Points
SmartCrudPagecomponents/admin/smart-crud-page.jsxGenerate Table/Search/Details/ActionsBuilt-in pagination, sorting, batch operations, detail drawer, index column, hook callbacks
SmartForm / SmartModalForm / SmartDrawerFormcomponents/admin/smart-formGenerate FormSupports grouping, dynamic linkage, validation, drawer/modal forms
fieldsConfig Generatorlib/crud/field-generator.js · lib/crud/field-types.jsConvert Config to Table/Form/SearchSupports 20+ field types, compatible with Ant Design official fieldProps
createCrudActionslib/core/crud-helper.jsCRUD FactoryAuto naming, permission check, logging, batch operations
wrapActionlib/core/action-wrapper.jsAction Wrapperpub/auth/sys/_ prefix auto auth + operation logging
BaseDAOapp/(admin)/actions/dao/base.jsPrisma WrapperSearch, filtering, soft delete, joins, hooks, validation

📐 Configuration Spec

Basic Structure (fieldsConfig)

{
  key: 'fieldName',      // Required, keep consistent with Prisma field
  title: 'Display Name', // Required
  type: 'text',          // Required, see type reference
  options: [],           // Available for select / radio / checkbox / tree-select, supports function or Action name
  table: { ... },        // Table config, false or hideInTable to hide
  form: { ... },         // Form config, false or hideInForm to hide
  search: { ... },       // Search config, false or hideInSearch to hide
  detail: { ... },       // Detail config, false or hideInDetail to hide
  createOnly: true,      // Show only on create; editOnly is opposite
  showRule: 'status === "rejected"',  // Dynamic show/hide (supports string/function)
  disabled: (values) => values.locked // Dynamic disable
}

Type Reference

  • Text: text · textarea · markdown
  • Number: number · rate · slider
  • Selection: select · radio · checkbox · tree-select · cascader
  • Boolean/Status: switch
  • Time: date · datetime · daterange · datetimerange · time
  • Media/Files: image · images · avatar · file · upload
  • Structured: json · array · group (grouped layout)
  • Others: icon · color

All form component native properties go directly in form.fieldProps, options can also use valueEnum (supports text/status/color).

Table/Form/Search Key Points

  • Table: table.width/fixed/ellipsis/copyable/align; table.formatter pure function or table.render JSX; table.valueEnum renders colored Tag.
  • Form: form.rules compatible with Ant Design rules; form.tips (tooltip) and form.extra; form.colSpan controls row width; form.action: 'sysGetXxx' automatically calls corresponding Action as options.
  • Search: search.mode supports like/exact/in/range/gt/gte/lt/lte; search.lazyLoad hides item when search is collapsed; search.fieldProps passes through ProForm properties.
  • Detail: Default reuses table rendering, can use detail.render for customization.
  • Data Source: options/data can be array, function (receives formData), or Action name (auto requests via form.action).

Search Condition and Prisma Mapping

modeMappingApplicable
like{ contains, mode: 'insensitive' }Text Fuzzy
exactvalueExact Match
in{ in: [] }Multi-select
range{ gte, lte }Time/Number Range
gt/gte/lt/lteSame OperatorNumber/Time Comparison

🛠️ Page Parameters (SmartCrudPage)

<SmartCrudPage
  title="Example Management"
  fieldsConfig={fieldsConfig}
  actions={actions}              // getList required, others as needed
  enableCreate={true}            // Default false
  enableEdit={true}              // Default true
  enableDelete={true}            // Default true
  enableDetail={true}            // Default true
  enableBatchDelete={true}
  enableIndexColumn={true}
  baseQuery={{ enable: true }}   // Force filter condition
  customToolbarButtons={[<Button key="export" onClick={exportData}>Export</Button>]}
  customRowActions={[
    { key: 'publish', label: 'Publish', onClick: onPublish, confirm: { title: 'Confirm Publish?' }, showCondition: r => r.status === 'draft' },
  ]}
  batchActions={[
    { key: 'enable', label: 'Batch Enable', onClick: rows => batchEnable(rows.map(r => r.id)) },
  ]}
  beforeCreate={(values) => ({ ...values, code: values.code?.toUpperCase?.() })}
  beforeEdit={(record) => record.enable !== false}   // Return false to prevent edit
  beforeDelete={(record) => !record.locked}
  tableApiRef={tableApiRef}          // External can call refresh/reset/search
  refreshTrigger={refreshVersion}    // Auto refresh when value changes
/>

Key Points:

  • actions supports getList/getDetail/create/update/delete/batchUpdate/batchDelete; missing capabilities automatically hide UI.
  • tableApiRef.current exposes refresh/reloadAndRest/resetSearch/submitSearch/getSelectedRows etc.
  • dataSource + loading can directly take over data (skip request).
  • Index Column: enableIndexColumn, calculates global index by pagination.
  • Detail Drawer: Automatically uses fieldsConfig for rendering, can customize header via renderDetailHeader.

🔧 Server Actions / DAO Configuration

createCrudActions Configuration Keywords

const config = {
  modelName: 'coupon',          // Prisma model name (lowercase)
  tableName: 'coupons',         // Required for selects joins
  primaryKey: 'id',
  softDelete: true,             // Auto filter deletedAt
  requireAdmin: false,          // true for admin role only

  fields: {
    creatable: ['name', 'code', 'type', 'discount', 'enable'],
    updatable: ['name', 'type', 'discount', 'enable', 'remark'],
    searchable: ['name', 'code'],
  },

  query: {
    defaultSort: { createdAt: 'desc' },
    defaultPageSize: 20,
    baseFilter: { enable: true },
    include: { author: true },    // Prisma include
    foreignDB: [/* selects joins */],
  },

  validation: {                  // Validated via auto-schema
    name: { required: true, minLength: 2, maxLength: 100 },
    code: { required: true, pattern: /^[A-Z0-9_-]+$/ },
    discount: { type: 'number', min: 0 },
    enable: { type: 'boolean', default: true },
  },

  uniqueFields: ['code'],        // Auto uniqueness check

  hooks: {
    beforeCreate: async (data, ctx) => ({ ...data, creatorId: ctx.userId }),
    afterCreate: async (record) => record,
    beforeUpdate: async (id, data) => data,
    beforeDelete: async (id, ctx) => true,
  },

  transforms: {
    input: (data) => ({ ...data, code: data.code?.toUpperCase?.() }),
    output: (record) => ({ ...record, discountLabel: `${record.discount}%` }),
  },
}

export const {
  getList: sysGetCouponList,
  getDetail: sysGetCouponDetail,
  create: sysCreateCoupon,
  update: sysUpdateCoupon,
  delete: sysDeleteCoupon,
  batchDelete: sysBatchDeleteCoupon,
} = createCrudActions(config)

Conventions and Standards:

  • Action names using sys prefix automatically validate admin login + RBAC + operation logging; auth requires login, pub is public.
  • baseFilter always attached to all queries; foreignDB/include enable joins for both list and detail.
  • hooks returning false blocks operation, throwing BusinessError logs but doesn't print stack trace.
  • Missing tableName causes selects query errors, must fill when using joins.

🧪 CRUD Practice (10 Minutes to Complete a Coupon Page)

1) Model Creation

Add model to prisma/schema.prisma:

model Coupon {
  id          String   @id @default(uuid())
  name        String
  code        String   @unique
  type        String   @default("percentage") // percentage | fixed
  discount    Decimal  @db.Decimal(10, 2)
  startDate   DateTime?
  endDate     DateTime?
  usageLimit  Int      @default(0)
  enable      Boolean  @default(true)
  remark      String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  deletedAt   DateTime?

  @@index([code])
  @@index([enable])
  @@index([deletedAt])
  @@map("coupons")
}

Execute migration (any package manager):

bunx prisma migrate dev --name add_coupon

2) Create Actions

app/(admin)/actions/cms/crud-action.coupon.js:

'use server'
import { createCrudActions } from '@/lib/core/crud-helper'

const config = {
  modelName: 'coupon',
  tableName: 'coupons',
  primaryKey: 'id',
  softDelete: true,
  fields: {
    creatable: ['name', 'code', 'type', 'discount', 'startDate', 'endDate', 'usageLimit', 'enable', 'remark'],
    updatable: ['name', 'type', 'discount', 'startDate', 'endDate', 'usageLimit', 'enable', 'remark'],
    searchable: ['name', 'code'],
  },
  query: { defaultSort: { createdAt: 'desc' }, defaultPageSize: 20 },
  validation: {
    name: { required: true, minLength: 2, maxLength: 100 },
    code: { required: true, pattern: /^[A-Z0-9_-]{3,20}$/ },
    type: { enum: ['percentage', 'fixed'], default: 'percentage' },
    discount: { type: 'number', min: 0 },
    usageLimit: { type: 'number', min: 0, default: 0 },
  },
  uniqueFields: ['code'],
  transforms: {
    input: (data) => ({ ...data, code: data.code?.toUpperCase?.() }),
  },
}

export const {
  getList: sysGetCouponList,
  getDetail: sysGetCouponDetail,
  create: sysCreateCoupon,
  update: sysUpdateCoupon,
  delete: sysDeleteCoupon,
  batchDelete: sysBatchDeleteCoupon,
} = createCrudActions(config)

3) Write Page

app/(admin)/admin/cms/coupons/page.js:

'use client'
import dynamic from 'next/dynamic'
import { Tag } from 'antd'
import * as actions from '@/app/(admin)/actions/cms/crud-action.coupon'

const SmartCrudPage = dynamic(() => import('@/components/admin/smart-crud-page'), { ssr: false })

const fieldsConfig = [
  { key: 'id', title: 'ID', type: 'text', table: { width: 140, copyable: true }, form: { hidden: true }, search: false },
  { key: 'name', title: 'Name', type: 'text', table: { ellipsis: true }, form: { required: true }, search: { mode: 'like' } },
  { key: 'code', title: 'Coupon Code', type: 'text', table: { width: 140, formatter: (v) => v || '-' }, form: { required: true } },
  {
    key: 'type',
    title: 'Type',
    type: 'select',
    options: [
      { label: 'Percentage', value: 'percentage' },
      { label: 'Fixed Amount', value: 'fixed' },
    ],
    table: {
      width: 120,
      valueEnum: {
        percentage: { text: 'Percentage', status: 'Processing' },
        fixed: { text: 'Fixed Amount', status: 'Default' },
      },
    },
    form: { required: true, defaultValue: 'percentage' },
  },
  { key: 'discount', title: 'Discount Value', type: 'number', table: { width: 100 }, form: { required: true, min: 0 }, search: { mode: 'range' } },
  { key: 'usageLimit', title: 'Usage Limit', type: 'number', table: { width: 100 }, form: { min: 0, defaultValue: 0 } },
  {
    key: 'enable',
    title: 'Status',
    type: 'switch',
    table: {
      width: 100,
      render: (v) => <Tag color={v ? 'green' : 'red'}>{v ? 'Enabled' : 'Disabled'}</Tag>,
    },
    form: { defaultValue: true },
    search: { mode: 'exact' },
  },
  { key: 'startDate', title: 'Start Date', type: 'datetime', table: { width: 170 }, search: { mode: 'range' } },
  { key: 'endDate', title: 'End Date', type: 'datetime', table: { width: 170 }, search: { mode: 'range' } },
  { key: 'remark', title: 'Remarks', type: 'textarea', table: { hidden: true }, form: { rows: 3, colSpan: 24 } },
  { key: 'createdAt', title: 'Created At', type: 'datetime', table: { width: 180, sorter: true }, form: { hidden: true }, search: { mode: 'range' } },
]

export default function CouponPage() {
  return (
    <SmartCrudPage
      title="Coupon Management"
      fieldsConfig={fieldsConfig}
      actions={actions}
      enableCreate
      enableDelete
      enableBatchDelete
      enableIndexColumn
    />
  )
}

4) Permissions and Menu

  • Actions are automatically named sysGetCouponList/sysCreateCoupon/..., add the same Action names in RBAC → Permissions to take effect.
  • Add a menu item in "Menu Management" pointing to /admin/cms/coupons, bind the above permissions, immediately controlled.
  • For seed data, refer to templates/crud/PERMISSIONS.md for Action/menu examples.

After completion, enter the admin panel to see the new "Coupon Management" page, with table/search/create/edit/delete/details all ready to use out of the box.