SmartCrudPage Complete Guide
TL;DR (3 Steps to Generate a CRUD Page)
- 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
| Name | Location | Purpose | Key Points |
|---|---|---|---|
SmartCrudPage | components/admin/smart-crud-page.jsx | Generate Table/Search/Details/Actions | Built-in pagination, sorting, batch operations, detail drawer, index column, hook callbacks |
SmartForm / SmartModalForm / SmartDrawerForm | components/admin/smart-form | Generate Form | Supports grouping, dynamic linkage, validation, drawer/modal forms |
fieldsConfig Generator | lib/crud/field-generator.js · lib/crud/field-types.js | Convert Config to Table/Form/Search | Supports 20+ field types, compatible with Ant Design official fieldProps |
createCrudActions | lib/core/crud-helper.js | CRUD Factory | Auto naming, permission check, logging, batch operations |
wrapAction | lib/core/action-wrapper.js | Action Wrapper | pub/auth/sys/_ prefix auto auth + operation logging |
BaseDAO | app/(admin)/actions/dao/base.js | Prisma Wrapper | Search, 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 usevalueEnum(supportstext/status/color).
Table/Form/Search Key Points
- Table:
table.width/fixed/ellipsis/copyable/align;table.formatterpure function ortable.renderJSX;table.valueEnumrenders colored Tag. - Form:
form.rulescompatible with Ant Design rules;form.tips(tooltip) andform.extra;form.colSpancontrols row width;form.action: 'sysGetXxx'automatically calls corresponding Action as options. - Search:
search.modesupportslike/exact/in/range/gt/gte/lt/lte;search.lazyLoadhides item when search is collapsed;search.fieldPropspasses through ProForm properties. - Detail: Default reuses table rendering, can use
detail.renderfor customization. - Data Source:
options/datacan be array, function (receives formData), or Action name (auto requests viaform.action).
Search Condition and Prisma Mapping
| mode | Mapping | Applicable |
|---|---|---|
like | { contains, mode: 'insensitive' } | Text Fuzzy |
exact | value | Exact Match |
in | { in: [] } | Multi-select |
range | { gte, lte } | Time/Number Range |
gt/gte/lt/lte | Same Operator | Number/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:
actionssupportsgetList/getDetail/create/update/delete/batchUpdate/batchDelete; missing capabilities automatically hide UI.tableApiRef.currentexposesrefresh/reloadAndRest/resetSearch/submitSearch/getSelectedRowsetc.dataSource + loadingcan directly take over data (skip request).- Index Column:
enableIndexColumn, calculates global index by pagination. - Detail Drawer: Automatically uses
fieldsConfigfor rendering, can customize header viarenderDetailHeader.
🔧 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
sysprefix automatically validate admin login + RBAC + operation logging;authrequires login,pubis public. baseFilteralways attached to all queries;foreignDB/includeenable joins for both list and detail.hooksreturning false blocks operation, throwingBusinessErrorlogs but doesn't print stack trace.- Missing
tableNamecauses 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_coupon2) 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.mdfor 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.
📚 Related Documentation
fieldsConfig Details
Complete API and examples for field configuration
SmartForm Guide
Form grouping, linkage, and layout details
Server Actions Development
Naming conventions, authentication, logging, and hooks
Prisma Guide
Database modeling, migration, and selects joins
Complete Example
Reference implementation for building CRUD pages from scratch