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:
| Status | |||
|---|---|---|---|
| Ada Lovelace | admin | active | |
| Alan Turing | member | active | |
| Grace Hopper | admin | inactive |
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.
| Prop | Type | Default | Description |
|---|---|---|---|
columns* | ResourceTableColumn<T>[] | — | |
data* | T[] | — | |
rowKey* | (row: T, index: number) => string | number | — | |
searchable | boolean | true | show the search box (default true) |
searchPlaceholder | string | — | |
filters | FilterDef[] | — | filter definitions rendered in a FilterBar |
applyFilter | ((row: T, values: FilterValues) => boolean) | — | custom filter predicate; the default matches by filter type against `row[name]` |
pageSize | number | 10 | client-side page size (default 10) |
selectable | boolean | — | row selection with checkboxes (enabled automatically when bulkActions are set) |
bulkActions | BulkAction[] | — | actions on the selected rows |
toolbarActions | ReactNode | — | right side of the toolbar ("New" button, …) |
exportFilename | string | — | adds a CSV/JSON export button with this file name |
columnToggle | boolean | true | show the column-visibility toggle (default true) |
onRowClick | ((row: T, index: number) => void) | — | |
empty | ReactNode | — | |
className | string | — |
* 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.
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()}
/>;| Prop | Type | Default | Description |
|---|---|---|---|
fields* | FieldDef[] | — | field schema (see FieldDef) |
initialValues | FormValues | — | record 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 |
submitLabel | string | Save | |
cancelLabel | string | Cancel | |
columns | 1 | 2 | 1 | field grid columns (default 1) |
error | ReactNode | — | form-level error shown above the fields (server error) |
loading | boolean | false | external loading/disable state |
className | string | — |
* 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} />;| Prop | Type | Default | Description |
|---|---|---|---|
fields* | FieldDef[] | — | field schema (see FieldDef) |
values* | FormValues | — | controlled values keyed by field name |
onChange* | (values: FormValues) => void | — | next values after any field change |
errors | Record<string, ReactNode> | {} | per-field error messages keyed by field name |
columns | 1 | 2 | 1 | field grid columns (default 1) |
disabled | boolean | false | disable every field |
className | string | — |
* required · generated from packages/cms/src/crud/FormBuilder.tsx
DetailView
Read-only record detail on DescriptionList.
<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
/>| Prop | Type | Default | Description |
|---|---|---|---|
record* | T | — | |
fields* | DetailField<T>[] | — | |
columns | number | 2 | DescriptionList columns (default 2) |
bordered | boolean | false | |
title | ReactNode | — | wraps the list in a titled Panel when set |
actions | ReactNode | — | Panel header actions (Edit button, …) |
className | string | — |
* required · generated from packages/cms/src/crud/DetailView.tsx
FilterBar
Composable filter row (ResourceTable renders one from its filters prop).
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}
/>;| Prop | Type | Default | Description |
|---|---|---|---|
filters* | FilterDef[] | — | |
values* | FilterValues | — | controlled filter values keyed by filter name |
onChange* | (values: FilterValues) => void | — | |
clearable | boolean | true | "Clear" resets all values to null; shown while any filter is set |
className | string | — |
* 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([])}
/>| Prop | Type | Default | Description |
|---|---|---|---|
selectedKeys* | (string | number)[] | — | currently selected row keys (from ResourceTable / DataTable selection) |
actions* | BulkAction[] | — | |
onClear | (() => void) | — | shows a "Clear" button that empties the selection |
itemName | string | item | noun for the count, default "item" → "3 items selected" |
className | string | — |
* 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" />}
/>| Prop | Type | Default | Description |
|---|---|---|---|
search | string | — | controlled search text; omit to hide the search box |
onSearchChange | ((value: string) => void) | — | |
searchPlaceholder | string | Search… | |
filters | ReactNode | — | filter controls, e.g. a <FilterBar /> |
columns | ToolbarColumn[] | — | column visibility toggle; pass the current columns with their state |
onColumnsChange | ((visibleKeys: string[]) => void) | — | called with the keys that should stay visible |
actions | ReactNode | — | right-aligned actions (New button, <DataExport />) |
className | string | — |
* 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
/>| Prop | Type | Default | Description |
|---|---|---|---|
open* | boolean | — | |
onClose* | () => void | — | |
onConfirm* | () => void | Promise<void> | — | perform the deletion; may be async (button shows loading) |
title | ReactNode | Delete? | |
description | ReactNode | — | custom body; defaults to a warning naming `itemName` |
itemName | string | — | what is being deleted, e.g. `"Project Alpha"` |
requireTyping | boolean | false | require typing `itemName` to enable the delete button |
confirmLabel | string | Delete | |
cancelLabel | string | Cancel |
* 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"
/>| Prop | Type | Default | Description |
|---|---|---|---|
data* | T[] | — | |
columns* | ExportColumn<T>[] | — | |
filename | string | export | file name without extension (default "export") |
formats | ("csv" | "json")[] | ['csv', 'json'] | offered formats (default both) |
label | string | Export | |
className | string | — |
* required · generated from packages/cms/src/crud/DataExport.tsx