Metadata-Driven Forms & List Views — Design Spec
Sub-project A of the v1.0 remaining work. Covers P3.2 (form renderer), P3.3 (form designer), P3.6 (list view designer), and R3 (metadata admin UIs).
1. Context & Motivation
vibe_erp's Tier 1 extensibility promise is that business analysts can customize
the system through the web UI — no code, no build, no restart. The foundation
is live: custom fields on core entities (P3.1 + P3.4), metadata YAML loader,
metadata__* tables, and the DynamicExtFields SPA component.
What's missing is the designer and renderer layer — the UIs that let key users create form layouts, configure list views, and manage all metadata through the browser. Without these, Tier 1 customization requires editing YAML files and restarting the server.
2. Scope Decisions (Agreed)
| Decision | Choice | Rationale |
|---|---|---|
| Form renderer scope | Hybrid — core entity forms stay handcrafted; renderer handles user-task forms, custom fields, and future custom entities | Core forms have complex interactions (line items, state transitions) that don't map well to JSON Schema |
| Custom entity creation | Deferred to v1.1 — no dynamic DDL or runtime endpoint registration in v1.0 | Large, risky feature; custom fields already give key users meaningful extensibility |
| Form renderer library | @rjsf/core with custom widget registry — React JSON Schema Form with ERP-specific widgets | FormSchema.kt already stores JSON Schema + UI Schema; @rjsf is the standard renderer |
| Form designer UX | Structured property editor — table/accordion UI with live preview tab | 90% of the value of drag-and-drop at 30% of the cost; upgradeable later |
3. Architecture
metadata__form / metadata__list_view (Postgres JSONB rows)
│
│ source = 'core' | 'plugin:<id>' | 'user'
│
MetadataController (REST API)
│
├── GET /api/v1/_meta/metadata/forms
├── GET /api/v1/_meta/metadata/forms/{slug}
├── PUT /api/v1/_meta/metadata/forms/{slug} ← designer writes
├── DELETE /api/v1/_meta/metadata/forms/{slug} ← user-created only
├── GET /api/v1/_meta/metadata/list-views
├── GET /api/v1/_meta/metadata/list-views/{slug}
├── PUT /api/v1/_meta/metadata/list-views/{slug}
└── DELETE /api/v1/_meta/metadata/list-views/{slug}
│
SPA (React + TypeScript)
│
├── MetadataFormRenderer (P3.2) — @rjsf/core, renders form from definition
├── FormDesigner (P3.3) — structured editor, writes definitions back
├── ListViewDesigner (P3.6) — column/filter/sort editor
└── MetadataAdmin (R3) — tabs: entities, permissions, menus,
custom fields, forms, list views
Source tagging
All form and list-view definitions follow the existing source convention:
-
core— shipped in metadata YAML, loaded by MetadataLoader at boot -
plugin:<id>— from plug-in JAR, loaded at plug-in start -
user— created through the designer UI, never touched by the loader
The PUT and DELETE endpoints only accept source='user' rows. Core and
plugin definitions are read-only in the UI (shown grayed out with a lock icon).
4. P3.2 — Form Renderer (MetadataFormRenderer)
Component
<MetadataFormRenderer
slug="plate-approval-task" // looks up from metadata__form
initialValues={{ ... }} // pre-fill for edit mode
onSubmit={(values) => { ... }} // called on valid submit
readOnly={false} // true for view / completed tasks
/>
Form definition schema (stored in metadata__form.payload)
interface FormDefinition {
entityName: string // "WorkOrder", "printing-shop.Plate"
slug: string // unique key: "plate-approval-task"
title: string // display title
purpose: 'create' | 'edit' | 'user-task' | 'view'
jsonSchema: object // JSON Schema draft 2020-12
uiSchema: object // @rjsf UI Schema (widget, order, sections)
version: number // monotonically increasing
}
@rjsf integration
-
Library:
@rjsf/core+@rjsf/validator-ajv8 -
Theme: Custom
VibeErpThemewrapping existing Tailwind CSS classes (slate-700 labels, rounded-md inputs, btn-primary buttons) so rendered forms look identical to handcrafted pages. - Custom widget registry:
| Widget ID | Purpose | Data source |
|---|---|---|
partner-picker |
Searchable partner dropdown | GET /api/v1/partners |
item-picker |
Searchable item dropdown | GET /api/v1/catalog/items |
uom-selector |
UoM dropdown | GET /api/v1/catalog/uoms |
location-picker |
Location dropdown | GET /api/v1/inventory/locations |
money-input |
Number + currency display | FieldType.Money |
quantity-input |
Number + UoM display | FieldType.Quantity |
Widgets are registered in a vibeWidgets map and passed to @rjsf/core's
widgets prop. Each widget is a standard React component that receives
WidgetProps from @rjsf.
User-task form bridge (P2.3)
Flowable user-tasks carry a formKey in BPMN XML (e.g. formKey="vibe:plate-approval-task").
When the SPA renders a pending user-task:
- Strip the
vibe:prefix → slugplate-approval-task - Fetch form definition:
GET /api/v1/_meta/metadata/forms/plate-approval-task - Fetch task variables:
GET /api/v1/workflow/tasks/{taskId} - Render:
<MetadataFormRenderer slug="..." initialValues={taskVars} /> - On submit:
POST /api/v1/workflow/tasks/{taskId}/completewith form values
Relationship with DynamicExtFields (P3.1)
DynamicExtFields stays as-is for custom fields on core entity create/edit pages. MetadataFormRenderer is a separate, more powerful component for full-form rendering. They share the same Tailwind styling but are independent components — no refactor of existing create/edit pages needed.
5. P3.3 — Form Designer
UX: Structured property editor
The form designer is a full-page editor with two panels:
Left panel — Field list (accordion/table):
- One row per field in the form
- Each row shows: field key, label, type, required checkbox, width (1/2/3 cols)
- Rows are reorderable via up/down buttons
- "Add field" button at the bottom with a type picker
- "Add section divider" to group fields under headings
- Click a row to expand its property panel inline:
- Label (per locale, with translation inputs)
- Placeholder text
- Help text / description
- Validation rules (min, max, pattern, required)
- Visibility condition (simple: "show when field X equals Y")
- Widget override (dropdown of available widgets)
Right panel — Live preview:
- Renders the current form definition using
<MetadataFormRenderer /> - Updates in real-time as the user edits fields in the left panel
- "Preview" / "Edit" toggle
Top bar:
- Form title (editable)
- Entity selector (which entity this form is for)
- Purpose selector (create / edit / user-task / view)
- Save button →
PUT /api/v1/_meta/metadata/forms/{slug} - Discard button
Visibility conditions
Simple "show when" rules stored in the UI Schema:
{
"ui:field:press_id": {
"ui:visible": { "field": "item_type", "equals": "GOOD" }
}
}
The MetadataFormRenderer evaluates these at render time — no server round-trip. Only single-field equality conditions in v1.0; complex expressions deferred.
Form definition generation
The designer generates a JSON Schema + UI Schema pair from the field list. It never exposes raw JSON to the user — the structured editor is the only interface. Power users who want raw JSON can use the R3 metadata admin UI's "Raw JSON" tab.
6. P3.6 — List View Designer
Definition schema (stored in metadata__list_view.payload)
interface ListViewDefinition {
entityName: string
slug: string // "sales-orders-default"
title: string
columns: Array<{
field: string // entity field key or custom field key
label: string // display header
width?: string // CSS width hint ("200px", "auto")
sortable: boolean
format?: 'date' | 'money' | 'status-badge' | 'link'
}>
defaultSort?: { field: string; direction: 'asc' | 'desc' }
filters?: Array<{
field: string
operator: 'eq' | 'contains' | 'gt' | 'lt' | 'in'
label: string
}>
pageSize: number // default 25
version: number
}
Designer UX
Simple two-section editor:
Columns section:
- Table of columns with checkboxes (show/hide), drag-to-reorder, label editing
- Available fields populated from entity's JSON Schema + custom fields
- Format selector per column (plain text, date, money, status badge, link)
Filters & sorting section:
- Add filterable fields (creates a filter bar above the list)
- Set default sort column and direction
- Page size selector
Preview: Shows a mock table with sample data using the current config.
Integration with existing list pages
v1.0 list pages (ItemsPage, PartnersPage, etc.) continue to use the existing
<DataTable> component with hardcoded columns. The ListViewDesigner creates
definitions for future use (custom entities in v1.1, and opt-in override of
core list views). A <MetadataListView entityName="..." /> component renders
a list from a ListViewDefinition, reusing DataTable internally.
7. R3 — Metadata Admin UIs
Route: /admin/metadata
A tabbed admin page with sections:
| Tab | Content | Editable? |
|---|---|---|
| Entities | List of all registered entities (core + plugin + user) | Read-only (v1.0 has no custom entities) |
| Custom Fields | List/create/edit custom fields for any entity | Full CRUD for source='user' fields |
| Permissions | List of all permissions | Read-only (permissions come from YAML) |
| Menus | List of all menu entries with section/order | Read-only for core/plugin; editable for user |
| Forms | List of form definitions; click to open FormDesigner | Full CRUD for source='user' forms |
| List Views | List of list-view definitions; click to open ListViewDesigner | Full CRUD for source='user' views |
Each tab shows a source badge (core / plugin:name / user) and a lock icon
for non-editable rows.
Custom field editor (inline in the admin)
The custom field tab lets key users:
- Add a new custom field to any entity (creates a
source='user'row) - Choose: target entity, field key, type (from FieldType sealed set), required, PII flag
- Add label translations (en, zh-CN, etc.)
- Delete user-created custom fields (with confirmation)
This replaces the need to edit YAML for Tier 1 custom fields — the full key-user story is now possible through the browser.
Permissions
-
admin.metadata.read— view all metadata tabs -
admin.metadata.write— create/edit/delete user metadata (custom fields, forms, list views, menus)
These are added to the core identity.yml metadata.
8. Backend Changes
New REST endpoints (MetadataController)
# Forms
GET /api/v1/_meta/metadata/forms → List<FormDefinition>
GET /api/v1/_meta/metadata/forms/{slug} → FormDefinition
PUT /api/v1/_meta/metadata/forms/{slug} → FormDefinition (upsert, source='user')
DELETE /api/v1/_meta/metadata/forms/{slug} → 204 (source='user' only)
# List views
GET /api/v1/_meta/metadata/list-views → List<ListViewDefinition>
GET /api/v1/_meta/metadata/list-views/{slug} → ListViewDefinition
PUT /api/v1/_meta/metadata/list-views/{slug} → ListViewDefinition (upsert, source='user')
DELETE /api/v1/_meta/metadata/list-views/{slug} → 204 (source='user' only)
# Custom fields (new write endpoints)
POST /api/v1/_meta/metadata/custom-fields → CustomField (source='user')
PUT /api/v1/_meta/metadata/custom-fields/{key} → CustomField (source='user')
DELETE /api/v1/_meta/metadata/custom-fields/{key} → 204 (source='user' only)
The existing GET endpoints remain unchanged. Write endpoints require
admin.metadata.write permission.
Database
The metadata__form and metadata__list_view tables already exist in
000-platform-init.xml. No new migrations needed — just new code that
reads/writes these tables.
MetadataLoader changes
Add forms: and listViews: sections to the MetadataYaml schema so core
PBCs and plug-ins can ship default form and list-view definitions in their
metadata YAML. The loader processes them with the same delete-by-source
idempotency as entities, permissions, menus, and custom fields.
CustomFieldRegistry refresh
After a custom field is created/updated/deleted through the REST API, the
registry must refresh. Add a refresh() call in the write endpoint handler
(same pattern as MetadataLoader already uses).
9. SPA Dependencies
New npm packages:
-
@rjsf/core— React JSON Schema Form renderer -
@rjsf/utils— shared utilities -
@rjsf/validator-ajv8— JSON Schema validation via Ajv
Estimated bundle impact: ~60KB gzipped (acceptable for an ERP SPA).
10. File Inventory (New & Modified)
New files
Backend (Kotlin):
-
platform/platform-metadata/src/.../web/FormDefinitionController.kt— CRUD for form defs -
platform/platform-metadata/src/.../web/ListViewDefinitionController.kt— CRUD for list-view defs -
platform/platform-metadata/src/.../web/CustomFieldWriteController.kt— write endpoints for custom fields -
platform/platform-metadata/src/.../yaml/FormYaml.kt— YAML deserialization for forms -
platform/platform-metadata/src/.../yaml/ListViewYaml.kt— YAML deserialization for list views
Frontend (TypeScript/React):
-
web/src/components/MetadataFormRenderer.tsx— @rjsf wrapper with VibeErp theme -
web/src/components/form-widgets/PartnerPicker.tsx— partner search widget -
web/src/components/form-widgets/ItemPicker.tsx— item search widget -
web/src/components/form-widgets/UomSelector.tsx— UoM dropdown widget -
web/src/components/form-widgets/LocationPicker.tsx— location dropdown widget -
web/src/components/form-widgets/MoneyInput.tsx— money input widget -
web/src/components/form-widgets/QuantityInput.tsx— quantity input widget -
web/src/components/form-widgets/index.ts— widget registry -
web/src/pages/FormDesignerPage.tsx— structured property editor -
web/src/pages/ListViewDesignerPage.tsx— column/filter/sort editor -
web/src/pages/MetadataAdminPage.tsx— tabbed admin with sub-sections
Metadata YAML (reference):
- Core PBCs ship default form definitions for user-task forms (if any)
- Printing-shop plug-in ships a
plate-approval-taskform definition
Modified files
-
platform/platform-metadata/src/.../yaml/MetadataYaml.kt— addforms+listViewssections -
platform/platform-metadata/src/.../MetadataLoader.kt— process forms + list views -
platform/platform-metadata/src/.../web/MetadataController.kt— add form/list-view GET endpoints -
web/src/App.tsx— add routes for designer + admin pages -
web/src/api/client.ts— add API functions for form/list-view/custom-field CRUD -
web/src/i18n/messages.ts— add message keys for new pages -
web/src/types/api.ts— add FormDefinition + ListViewDefinition types -
web/package.json— add @rjsf dependencies
11. Testing Strategy
Backend unit tests:
- FormDefinition CRUD (create, read, update, delete with source enforcement)
- ListViewDefinition CRUD (same)
- CustomField write endpoints (create, update, delete, registry refresh)
- MetadataLoader with forms + list views sections
- Source-tag enforcement (reject writes to core/plugin rows)
Frontend:
- Manual smoke test: open form designer, create a form, preview it, save it
- Manual smoke test: open list view designer, configure columns, save
- Manual smoke test: open metadata admin, browse all tabs
Integration:
- Printing-shop plug-in ships a
plate-approval-taskform definition - Boot the framework, start the plate-approval BPMN process, verify the user-task renders the form from metadata, complete it, verify values flow through to the workflow
12. Out of Scope (Deferred)
- Custom entities / dynamic DDL — v1.1
- Drag-and-drop form designer — future upgrade of the structured editor
- Complex visibility conditions — v1.0 supports "show when X equals Y" only
- Form versioning / migration — v1.1 (for now, bumping version is manual)
- List view as override for core pages — v1.1 (core pages keep hardcoded columns)
- Import/export of form/list-view definitions — v1.1