2026-04-10-metadata-forms-listviews-design.md 16.5 KB

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 VibeErpTheme wrapping 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:

  1. Strip the vibe: prefix → slug plate-approval-task
  2. Fetch form definition: GET /api/v1/_meta/metadata/forms/plate-approval-task
  3. Fetch task variables: GET /api/v1/workflow/tasks/{taskId}
  4. Render: <MetadataFormRenderer slug="..." initialValues={taskVars} />
  5. On submit: POST /api/v1/workflow/tasks/{taskId}/complete with 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-task form definition

Modified files

  • platform/platform-metadata/src/.../yaml/MetadataYaml.kt — add forms + listViews sections
  • 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-task form 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