Skip to Content
CMSCRUD

CMS CRUD

The heart of an admin panel: a list view (ResourceTable), a schema-driven create/edit form (ResourceForm) and the pieces they’re made of.

The field schema (FieldDef)

ResourceForm, FormBuilder and SettingsForm all render from one schema shape:

type FieldDef = { name: string; // key in the values object label?: ReactNode; type?: 'text' | 'email' | 'password' | 'textarea' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'switch' | 'date' | 'time' | 'color' | 'slider' | 'rating' | 'file' | 'custom'; // default "text" placeholder?: string; hint?: ReactNode; required?: boolean; disabled?: boolean; options?: { label: ReactNode; value: string; disabled?: boolean }[]; // select/multiselect/radio min?: number; max?: number; step?: number; // number/slider rows?: number; // textarea defaultValue?: unknown; validate?: (value, values) => ReactNode | null; // return an error message render?: (props) => ReactNode; // for type "custom" fullWidth?: boolean; // span both columns in a two-column layout };

Value types per field type: strings for text-likes, number | null for number/slider/rating, string | null for select/radio/time, string[] for multiselect, boolean for checkbox/switch, Date | null for date, File[] for file.

ResourceTable

The CRUD list view: DataTable + search + filters + column toggle + bulk actions + CSV/JSON export, all client-side. Try it — search, filter by role, select rows:

Role
Active
Status
Ada Lovelaceadminactive
Alan Turingmemberactive
Grace Hopperadmininactive
import { ResourceTable } from '@gg-software/cms'; <ResourceTable<Person> columns={[ { key: 'name', header: 'Name', sortable: true }, { key: 'role', header: 'Role', sortable: true }, { key: 'active', header: 'Status', render: (row) => <Badge label={row.active ? 'active' : 'inactive'} /> }, ]} data={people} rowKey={(row) => row.id} pageSize={20} filters={[ { name: 'role', label: 'Role', options: roleOptions }, { name: 'active', label: 'Active', type: 'boolean' }, ]} bulkActions={[{ key: 'delete', label: 'Delete', danger: true, onClick: (keys) => removeAll(keys) }]} exportFilename="people" toolbarActions={<Button variant="primary">New person</Button>} onRowClick={(row) => openDetail(row)} />;

For server-side data, filter/paginate yourself and compose DataTableToolbar + DataTable directly.

PropTypeDefaultDescription
columns*ResourceTableColumn<T>[]
data*T[]
rowKey*(row: T, index: number) => string | number
searchablebooleantrueshow the search box (default true)
searchPlaceholderstring
filtersFilterDef[]filter definitions rendered in a FilterBar
applyFilter((row: T, values: FilterValues) => boolean)custom filter predicate; the default matches by filter type against `row[name]`
pageSizenumber10client-side page size (default 10)
selectablebooleanrow selection with checkboxes (enabled automatically when bulkActions are set)
bulkActionsBulkAction[]actions on the selected rows
toolbarActionsReactNoderight side of the toolbar ("New" button, …)
exportFilenamestringadds a CSV/JSON export button with this file name
columnTogglebooleantrueshow the column-visibility toggle (default true)
onRowClick((row: T, index: number) => void)
emptyReactNode
classNamestring

* required · generated from packages/cms/src/crud/ResourceTable.tsx

ResourceForm

Create/edit form driven by the field schema. Validates on submit (required, email format, per-field validate); async onSubmit drives the loading state.

Role
import { ResourceForm } from '@gg-software/cms'; <ResourceForm columns={2} fields={[ { name: 'name', label: 'Name', required: true }, { name: 'email', label: 'Email', type: 'email', required: true }, { name: 'role', label: 'Role', type: 'select', options: roleOptions, defaultValue: 'member' }, { name: 'bio', label: 'Bio', type: 'textarea', fullWidth: true }, ]} initialValues={person} // omit for a create form onSubmit={(values) => api.save(values)} onCancel={() => router.back()} />;
PropTypeDefaultDescription
fields*FieldDef[]field schema (see FieldDef)
initialValuesFormValuesrecord being edited; omit for a create form
onSubmit*(values: FormValues) => void | Promise<void>called with the validated values; may be async (submit button shows loading)
onCancel(() => void)renders a cancel button
submitLabelstringSave
cancelLabelstringCancel
columns1 | 21field grid columns (default 1)
errorReactNodeform-level error shown above the fields (server error)
loadingbooleanfalseexternal loading/disable state
classNamestring

* required · generated from packages/cms/src/crud/ResourceForm.tsx

FormBuilder

The controlled schema→controls core used by ResourceForm and SettingsForm — use it directly for custom form state or layout.

const [values, setValues] = useState<FormValues>({}); <FormBuilder fields={fields} values={values} onChange={setValues} errors={errors} columns={2} />;
PropTypeDefaultDescription
fields*FieldDef[]field schema (see FieldDef)
values*FormValuescontrolled values keyed by field name
onChange*(values: FormValues) => voidnext values after any field change
errorsRecord<string, ReactNode>{}per-field error messages keyed by field name
columns1 | 21field grid columns (default 1)
disabledbooleanfalsedisable every field
classNamestring

* required · generated from packages/cms/src/crud/FormBuilder.tsx

DetailView

Read-only record detail on DescriptionList.

Customer
NameAda Lovelace
Emailada@acme.io
Statusactive
Created2026-07-01
<DetailView title="Customer" actions={<Button size="sm">Edit</Button>} record={customer} fields={[ { key: 'name', label: 'Name' }, { label: 'Status', render: (r) => <Badge label={r.status} /> }, ]} bordered />
PropTypeDefaultDescription
record*T
fields*DetailField<T>[]
columnsnumber2DescriptionList columns (default 2)
borderedbooleanfalse
titleReactNodewraps the list in a titled Panel when set
actionsReactNodePanel header actions (Edit button, …)
classNamestring

* required · generated from packages/cms/src/crud/DetailView.tsx

FilterBar

Composable filter row (ResourceTable renders one from its filters prop).

Status
Created
const [values, setValues] = useState<FilterValues>({}); <FilterBar filters={[ { name: 'status', label: 'Status', options: statusOptions }, { name: 'q', label: 'Search text', type: 'text' }, { name: 'created', label: 'Created', type: 'date' }, ]} values={values} onChange={setValues} />;
PropTypeDefaultDescription
filters*FilterDef[]
values*FilterValuescontrolled filter values keyed by filter name
onChange*(values: FilterValues) => void
clearablebooleantrue"Clear" resets all values to null; shown while any filter is set
classNamestring

* required · generated from packages/cms/src/crud/FilterBar.tsx

BulkActions

Action bar for the current selection — renders nothing while empty.

<BulkActions selectedKeys={selected} actions={[{ key: 'delete', label: 'Delete', danger: true, onClick: (keys) => removeAll(keys) }]} onClear={() => setSelected([])} />
PropTypeDefaultDescription
selectedKeys*(string | number)[]currently selected row keys (from ResourceTable / DataTable selection)
actions*BulkAction[]
onClear(() => void)shows a "Clear" button that empties the selection
itemNamestringitemnoun for the count, default "item" → "3 items selected"
classNamestring

* required · generated from packages/cms/src/crud/BulkActions.tsx

DataTableToolbar

Search + filters + column toggle + actions row for use with a plain DataTable.

<DataTableToolbar search={search} onSearchChange={setSearch} columns={columns.map((c) => ({ key: c.key, header: c.header, visible: visible.has(c.key) }))} onColumnsChange={(keys) => setVisible(new Set(keys))} actions={<DataExport data={rows} columns={exportColumns} filename="orders" />} />
PropTypeDefaultDescription
searchstringcontrolled search text; omit to hide the search box
onSearchChange((value: string) => void)
searchPlaceholderstringSearch…
filtersReactNodefilter controls, e.g. a <FilterBar />
columnsToolbarColumn[]column visibility toggle; pass the current columns with their state
onColumnsChange((visibleKeys: string[]) => void)called with the keys that should stay visible
actionsReactNoderight-aligned actions (New button, <DataExport />)
classNamestring

* required · generated from packages/cms/src/crud/DataTableToolbar.tsx

ConfirmDeleteDialog

Guarded delete confirmation — optionally requires typing the item’s name.

<ConfirmDeleteDialog open={open} onClose={() => setOpen(false)} onConfirm={() => api.remove(project.id)} itemName={project.name} requireTyping />
PropTypeDefaultDescription
open*boolean
onClose*() => void
onConfirm*() => void | Promise<void>perform the deletion; may be async (button shows loading)
titleReactNodeDelete?
descriptionReactNodecustom body; defaults to a warning naming `itemName`
itemNamestringwhat is being deleted, e.g. `"Project Alpha"`
requireTypingbooleanfalserequire typing `itemName` to enable the delete button
confirmLabelstringDelete
cancelLabelstringCancel

* required · generated from packages/cms/src/crud/ConfirmDeleteDialog.tsx

DataExport

Export button downloading the rows as CSV or JSON (utils downloadFile). The toCsv helper is exported too.

<DataExport data={orders} columns={[ { key: 'id', header: 'Order' }, { key: 'total', header: 'Total', value: (o) => o.total.toFixed(2) }, ]} filename="orders" />
PropTypeDefaultDescription
data*T[]
columns*ExportColumn<T>[]
filenamestringexportfile name without extension (default "export")
formats("csv" | "json")[]['csv', 'json']offered formats (default both)
labelstringExport
classNamestring

* required · generated from packages/cms/src/crud/DataExport.tsx