defineCustomFieldsConfig is Mercur’s model-scoped extension surface: from one file per model, you add validated fields to built-in create/edit forms, replace/remove/add fields in detail sections, and add columns to the list table — all wired into the built-in page, all typed against the model’s generated registry.
UI, not schema. This helper is a panel surface — it renders, validates, and displays fields. It does not create database columns. To store extra data, use the backend Custom Fields module or your own API route/workflow. In the MVP, panel custom fields for
product are submitted under additional_data and persisted onto the product’s metadata.What you’ll build
AnERP ID field on the vendor product edit form, shown in the product’s detail section and as a list-table column — from a single src/custom-fields/product.tsx.
Register the typed targets (once)
Models, form zones, display zones, and built-in field ids are typed per panel. Register them once (shipped bycreate-mercur-app):
apps/vendor/src/extension-targets.d.ts
Build the config
Create the model file
Drop
src/custom-fields/<model>.tsx and default-export a defineCustomFieldsConfig. createFormHelper (from @mercurjs/dashboard-shared) turns a Zod schema into an input type + validation:apps/vendor/src/custom-fields/product.tsx
Add a detail-section display
displays[] targets a detail-page section by its zone id. Keyed by field id, an entry adds, replaces, or removes a field:subtitle, handle, status, title) autocomplete from the panel’s generated CustomFieldsRegistry; an unknown id is treated as an added row.Add a list column
The
list block extends the model’s list table — add or override columns by id, hide built-in columns, and reorder:The createFormHelper surface
createFormHelper<T>() exposes a Zod-based surface that drives both the input type and its validation:
Form.Field → Form.Item chain (never a raw Controller) and participate in the existing TabbedForm / RouteDrawer submit and validation flow.
Linked-module data
To read data from a linked module alongside the entity, declare it withlink; those relations are fetched with the entity and become available to columns and displays:
link and merges it into the built-in query with the +/- convention — you never hand-write the field list.
Verify
- The ERP ID field renders in the product edit drawer and validates on submit.
- Saving persists the value (visible on reload) via
additional_data→metadata. - The detail general section shows the ERP row, hides
subtitle, and re-rendershandle. - The product list shows the ERP column, hides
collection, and reorders columns. - Set
zone: "nope"informs—bun run lint(tsc) fails against the model’s registry.
FAQ
Which models and zones are available?
Which models and zones are available?
Today: the
product model in the vendor portal, with form zone edit and display zone general. The valid set per panel is generated into CustomFieldsRegistry in extension-targets.d.ts — autocomplete model / zone to see what’s mounted.Where is the value actually stored?
Where is the value actually stored?
In the MVP, product custom fields are submitted under
additional_data and persisted onto product.metadata. defineCustomFieldsConfig itself doesn’t create a column — for durable, queryable storage model it with the backend Custom Fields module or a custom route/workflow.Can I extend the onboarding wizard?
Can I extend the onboarding wizard?
That’s the same helper with
zone: "onboarding" and tab set to a wizard step id (vendor only). It’s designed but not mounted in the current MVP — the runtime host exists; the wizard mount is a follow-up.Can a block ship custom fields?
Can a block ship custom fields?
Yes — a block can include
src/custom-fields/ files in its vendor_ui / admin_ui entry, aggregated like the host app’s.Next steps
Custom Fields module
Persist extra data on an entity with a generated side table.
Add a widget
Inject a component at a built-in zone.