Commit 39827f040f15aa63a087a59071c170565c6881a9
1 parent
634c18fe
docs(spec): metadata-driven forms & list views design (P3.2/P3.3/P3.6/R3)
Sub-project A of remaining v1.0 work. Key decisions: - Hybrid: core forms stay handcrafted, renderer for user-task forms - @rjsf/core with custom ERP widget registry - Structured property editor (not drag-and-drop) for form designer - Custom entities deferred to v1.1
Showing
1 changed file
with
391 additions
and
0 deletions
docs/superpowers/specs/2026-04-10-metadata-forms-listviews-design.md
0 → 100644
| 1 | +# Metadata-Driven Forms & List Views — Design Spec | |
| 2 | + | |
| 3 | +> Sub-project A of the v1.0 remaining work. Covers P3.2 (form renderer), | |
| 4 | +> P3.3 (form designer), P3.6 (list view designer), and R3 (metadata admin UIs). | |
| 5 | + | |
| 6 | +## 1. Context & Motivation | |
| 7 | + | |
| 8 | +vibe_erp's Tier 1 extensibility promise is that business analysts can customize | |
| 9 | +the system through the web UI — no code, no build, no restart. The foundation | |
| 10 | +is live: custom fields on core entities (P3.1 + P3.4), metadata YAML loader, | |
| 11 | +`metadata__*` tables, and the `DynamicExtFields` SPA component. | |
| 12 | + | |
| 13 | +What's missing is the **designer and renderer layer** — the UIs that let key | |
| 14 | +users create form layouts, configure list views, and manage all metadata | |
| 15 | +through the browser. Without these, Tier 1 customization requires editing | |
| 16 | +YAML files and restarting the server. | |
| 17 | + | |
| 18 | +## 2. Scope Decisions (Agreed) | |
| 19 | + | |
| 20 | +| Decision | Choice | Rationale | | |
| 21 | +|---|---|---| | |
| 22 | +| 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 | | |
| 23 | +| 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 | | |
| 24 | +| 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 | | |
| 25 | +| 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 | | |
| 26 | + | |
| 27 | +## 3. Architecture | |
| 28 | + | |
| 29 | +``` | |
| 30 | +metadata__form / metadata__list_view (Postgres JSONB rows) | |
| 31 | + │ | |
| 32 | + │ source = 'core' | 'plugin:<id>' | 'user' | |
| 33 | + │ | |
| 34 | + MetadataController (REST API) | |
| 35 | + │ | |
| 36 | + ├── GET /api/v1/_meta/metadata/forms | |
| 37 | + ├── GET /api/v1/_meta/metadata/forms/{slug} | |
| 38 | + ├── PUT /api/v1/_meta/metadata/forms/{slug} ← designer writes | |
| 39 | + ├── DELETE /api/v1/_meta/metadata/forms/{slug} ← user-created only | |
| 40 | + ├── GET /api/v1/_meta/metadata/list-views | |
| 41 | + ├── GET /api/v1/_meta/metadata/list-views/{slug} | |
| 42 | + ├── PUT /api/v1/_meta/metadata/list-views/{slug} | |
| 43 | + └── DELETE /api/v1/_meta/metadata/list-views/{slug} | |
| 44 | + │ | |
| 45 | + SPA (React + TypeScript) | |
| 46 | + │ | |
| 47 | + ├── MetadataFormRenderer (P3.2) — @rjsf/core, renders form from definition | |
| 48 | + ├── FormDesigner (P3.3) — structured editor, writes definitions back | |
| 49 | + ├── ListViewDesigner (P3.6) — column/filter/sort editor | |
| 50 | + └── MetadataAdmin (R3) — tabs: entities, permissions, menus, | |
| 51 | + custom fields, forms, list views | |
| 52 | +``` | |
| 53 | + | |
| 54 | +### Source tagging | |
| 55 | + | |
| 56 | +All form and list-view definitions follow the existing `source` convention: | |
| 57 | +- `core` — shipped in metadata YAML, loaded by MetadataLoader at boot | |
| 58 | +- `plugin:<id>` — from plug-in JAR, loaded at plug-in start | |
| 59 | +- `user` — created through the designer UI, never touched by the loader | |
| 60 | + | |
| 61 | +The `PUT` and `DELETE` endpoints only accept `source='user'` rows. Core and | |
| 62 | +plugin definitions are read-only in the UI (shown grayed out with a lock icon). | |
| 63 | + | |
| 64 | +## 4. P3.2 — Form Renderer (`MetadataFormRenderer`) | |
| 65 | + | |
| 66 | +### Component | |
| 67 | + | |
| 68 | +```tsx | |
| 69 | +<MetadataFormRenderer | |
| 70 | + slug="plate-approval-task" // looks up from metadata__form | |
| 71 | + initialValues={{ ... }} // pre-fill for edit mode | |
| 72 | + onSubmit={(values) => { ... }} // called on valid submit | |
| 73 | + readOnly={false} // true for view / completed tasks | |
| 74 | +/> | |
| 75 | +``` | |
| 76 | + | |
| 77 | +### Form definition schema (stored in `metadata__form.payload`) | |
| 78 | + | |
| 79 | +```typescript | |
| 80 | +interface FormDefinition { | |
| 81 | + entityName: string // "WorkOrder", "printing-shop.Plate" | |
| 82 | + slug: string // unique key: "plate-approval-task" | |
| 83 | + title: string // display title | |
| 84 | + purpose: 'create' | 'edit' | 'user-task' | 'view' | |
| 85 | + jsonSchema: object // JSON Schema draft 2020-12 | |
| 86 | + uiSchema: object // @rjsf UI Schema (widget, order, sections) | |
| 87 | + version: number // monotonically increasing | |
| 88 | +} | |
| 89 | +``` | |
| 90 | + | |
| 91 | +### @rjsf integration | |
| 92 | + | |
| 93 | +- **Library:** `@rjsf/core` + `@rjsf/validator-ajv8` | |
| 94 | +- **Theme:** Custom `VibeErpTheme` wrapping existing Tailwind CSS classes | |
| 95 | + (slate-700 labels, rounded-md inputs, btn-primary buttons) so rendered | |
| 96 | + forms look identical to handcrafted pages. | |
| 97 | +- **Custom widget registry:** | |
| 98 | + | |
| 99 | +| Widget ID | Purpose | Data source | | |
| 100 | +|---|---|---| | |
| 101 | +| `partner-picker` | Searchable partner dropdown | `GET /api/v1/partners` | | |
| 102 | +| `item-picker` | Searchable item dropdown | `GET /api/v1/catalog/items` | | |
| 103 | +| `uom-selector` | UoM dropdown | `GET /api/v1/catalog/uoms` | | |
| 104 | +| `location-picker` | Location dropdown | `GET /api/v1/inventory/locations` | | |
| 105 | +| `money-input` | Number + currency display | FieldType.Money | | |
| 106 | +| `quantity-input` | Number + UoM display | FieldType.Quantity | | |
| 107 | + | |
| 108 | +Widgets are registered in a `vibeWidgets` map and passed to `@rjsf/core`'s | |
| 109 | +`widgets` prop. Each widget is a standard React component that receives | |
| 110 | +`WidgetProps` from @rjsf. | |
| 111 | + | |
| 112 | +### User-task form bridge (P2.3) | |
| 113 | + | |
| 114 | +Flowable user-tasks carry a `formKey` in BPMN XML (e.g. `formKey="vibe:plate-approval-task"`). | |
| 115 | +When the SPA renders a pending user-task: | |
| 116 | + | |
| 117 | +1. Strip the `vibe:` prefix → slug `plate-approval-task` | |
| 118 | +2. Fetch form definition: `GET /api/v1/_meta/metadata/forms/plate-approval-task` | |
| 119 | +3. Fetch task variables: `GET /api/v1/workflow/tasks/{taskId}` | |
| 120 | +4. Render: `<MetadataFormRenderer slug="..." initialValues={taskVars} />` | |
| 121 | +5. On submit: `POST /api/v1/workflow/tasks/{taskId}/complete` with form values | |
| 122 | + | |
| 123 | +### Relationship with DynamicExtFields (P3.1) | |
| 124 | + | |
| 125 | +DynamicExtFields stays as-is for custom fields on core entity create/edit pages. | |
| 126 | +MetadataFormRenderer is a separate, more powerful component for full-form rendering. | |
| 127 | +They share the same Tailwind styling but are independent components — no refactor | |
| 128 | +of existing create/edit pages needed. | |
| 129 | + | |
| 130 | +## 5. P3.3 — Form Designer | |
| 131 | + | |
| 132 | +### UX: Structured property editor | |
| 133 | + | |
| 134 | +The form designer is a full-page editor with two panels: | |
| 135 | + | |
| 136 | +**Left panel — Field list (accordion/table):** | |
| 137 | +- One row per field in the form | |
| 138 | +- Each row shows: field key, label, type, required checkbox, width (1/2/3 cols) | |
| 139 | +- Rows are reorderable via up/down buttons | |
| 140 | +- "Add field" button at the bottom with a type picker | |
| 141 | +- "Add section divider" to group fields under headings | |
| 142 | +- Click a row to expand its property panel inline: | |
| 143 | + - Label (per locale, with translation inputs) | |
| 144 | + - Placeholder text | |
| 145 | + - Help text / description | |
| 146 | + - Validation rules (min, max, pattern, required) | |
| 147 | + - Visibility condition (simple: "show when field X equals Y") | |
| 148 | + - Widget override (dropdown of available widgets) | |
| 149 | + | |
| 150 | +**Right panel — Live preview:** | |
| 151 | +- Renders the current form definition using `<MetadataFormRenderer />` | |
| 152 | +- Updates in real-time as the user edits fields in the left panel | |
| 153 | +- "Preview" / "Edit" toggle | |
| 154 | + | |
| 155 | +**Top bar:** | |
| 156 | +- Form title (editable) | |
| 157 | +- Entity selector (which entity this form is for) | |
| 158 | +- Purpose selector (create / edit / user-task / view) | |
| 159 | +- Save button → `PUT /api/v1/_meta/metadata/forms/{slug}` | |
| 160 | +- Discard button | |
| 161 | + | |
| 162 | +### Visibility conditions | |
| 163 | + | |
| 164 | +Simple "show when" rules stored in the UI Schema: | |
| 165 | + | |
| 166 | +```json | |
| 167 | +{ | |
| 168 | + "ui:field:press_id": { | |
| 169 | + "ui:visible": { "field": "item_type", "equals": "GOOD" } | |
| 170 | + } | |
| 171 | +} | |
| 172 | +``` | |
| 173 | + | |
| 174 | +The MetadataFormRenderer evaluates these at render time — no server round-trip. | |
| 175 | +Only single-field equality conditions in v1.0; complex expressions deferred. | |
| 176 | + | |
| 177 | +### Form definition generation | |
| 178 | + | |
| 179 | +The designer generates a JSON Schema + UI Schema pair from the field list. | |
| 180 | +It never exposes raw JSON to the user — the structured editor is the only | |
| 181 | +interface. Power users who want raw JSON can use the R3 metadata admin UI's | |
| 182 | +"Raw JSON" tab. | |
| 183 | + | |
| 184 | +## 6. P3.6 — List View Designer | |
| 185 | + | |
| 186 | +### Definition schema (stored in `metadata__list_view.payload`) | |
| 187 | + | |
| 188 | +```typescript | |
| 189 | +interface ListViewDefinition { | |
| 190 | + entityName: string | |
| 191 | + slug: string // "sales-orders-default" | |
| 192 | + title: string | |
| 193 | + columns: Array<{ | |
| 194 | + field: string // entity field key or custom field key | |
| 195 | + label: string // display header | |
| 196 | + width?: string // CSS width hint ("200px", "auto") | |
| 197 | + sortable: boolean | |
| 198 | + format?: 'date' | 'money' | 'status-badge' | 'link' | |
| 199 | + }> | |
| 200 | + defaultSort?: { field: string; direction: 'asc' | 'desc' } | |
| 201 | + filters?: Array<{ | |
| 202 | + field: string | |
| 203 | + operator: 'eq' | 'contains' | 'gt' | 'lt' | 'in' | |
| 204 | + label: string | |
| 205 | + }> | |
| 206 | + pageSize: number // default 25 | |
| 207 | + version: number | |
| 208 | +} | |
| 209 | +``` | |
| 210 | + | |
| 211 | +### Designer UX | |
| 212 | + | |
| 213 | +Simple two-section editor: | |
| 214 | + | |
| 215 | +**Columns section:** | |
| 216 | +- Table of columns with checkboxes (show/hide), drag-to-reorder, label editing | |
| 217 | +- Available fields populated from entity's JSON Schema + custom fields | |
| 218 | +- Format selector per column (plain text, date, money, status badge, link) | |
| 219 | + | |
| 220 | +**Filters & sorting section:** | |
| 221 | +- Add filterable fields (creates a filter bar above the list) | |
| 222 | +- Set default sort column and direction | |
| 223 | +- Page size selector | |
| 224 | + | |
| 225 | +**Preview:** Shows a mock table with sample data using the current config. | |
| 226 | + | |
| 227 | +### Integration with existing list pages | |
| 228 | + | |
| 229 | +v1.0 list pages (ItemsPage, PartnersPage, etc.) continue to use the existing | |
| 230 | +`<DataTable>` component with hardcoded columns. The `ListViewDesigner` creates | |
| 231 | +definitions for future use (custom entities in v1.1, and opt-in override of | |
| 232 | +core list views). A `<MetadataListView entityName="..." />` component renders | |
| 233 | +a list from a `ListViewDefinition`, reusing `DataTable` internally. | |
| 234 | + | |
| 235 | +## 7. R3 — Metadata Admin UIs | |
| 236 | + | |
| 237 | +### Route: `/admin/metadata` | |
| 238 | + | |
| 239 | +A tabbed admin page with sections: | |
| 240 | + | |
| 241 | +| Tab | Content | Editable? | | |
| 242 | +|---|---|---| | |
| 243 | +| **Entities** | List of all registered entities (core + plugin + user) | Read-only (v1.0 has no custom entities) | | |
| 244 | +| **Custom Fields** | List/create/edit custom fields for any entity | Full CRUD for `source='user'` fields | | |
| 245 | +| **Permissions** | List of all permissions | Read-only (permissions come from YAML) | | |
| 246 | +| **Menus** | List of all menu entries with section/order | Read-only for core/plugin; editable for user | | |
| 247 | +| **Forms** | List of form definitions; click to open FormDesigner | Full CRUD for `source='user'` forms | | |
| 248 | +| **List Views** | List of list-view definitions; click to open ListViewDesigner | Full CRUD for `source='user'` views | | |
| 249 | + | |
| 250 | +Each tab shows a `source` badge (core / plugin:name / user) and a lock icon | |
| 251 | +for non-editable rows. | |
| 252 | + | |
| 253 | +### Custom field editor (inline in the admin) | |
| 254 | + | |
| 255 | +The custom field tab lets key users: | |
| 256 | +- Add a new custom field to any entity (creates a `source='user'` row) | |
| 257 | +- Choose: target entity, field key, type (from FieldType sealed set), required, PII flag | |
| 258 | +- Add label translations (en, zh-CN, etc.) | |
| 259 | +- Delete user-created custom fields (with confirmation) | |
| 260 | + | |
| 261 | +This replaces the need to edit YAML for Tier 1 custom fields — the full | |
| 262 | +key-user story is now possible through the browser. | |
| 263 | + | |
| 264 | +### Permissions | |
| 265 | + | |
| 266 | +- `admin.metadata.read` — view all metadata tabs | |
| 267 | +- `admin.metadata.write` — create/edit/delete user metadata (custom fields, forms, list views, menus) | |
| 268 | + | |
| 269 | +These are added to the core `identity.yml` metadata. | |
| 270 | + | |
| 271 | +## 8. Backend Changes | |
| 272 | + | |
| 273 | +### New REST endpoints (MetadataController) | |
| 274 | + | |
| 275 | +``` | |
| 276 | +# Forms | |
| 277 | +GET /api/v1/_meta/metadata/forms → List<FormDefinition> | |
| 278 | +GET /api/v1/_meta/metadata/forms/{slug} → FormDefinition | |
| 279 | +PUT /api/v1/_meta/metadata/forms/{slug} → FormDefinition (upsert, source='user') | |
| 280 | +DELETE /api/v1/_meta/metadata/forms/{slug} → 204 (source='user' only) | |
| 281 | + | |
| 282 | +# List views | |
| 283 | +GET /api/v1/_meta/metadata/list-views → List<ListViewDefinition> | |
| 284 | +GET /api/v1/_meta/metadata/list-views/{slug} → ListViewDefinition | |
| 285 | +PUT /api/v1/_meta/metadata/list-views/{slug} → ListViewDefinition (upsert, source='user') | |
| 286 | +DELETE /api/v1/_meta/metadata/list-views/{slug} → 204 (source='user' only) | |
| 287 | + | |
| 288 | +# Custom fields (new write endpoints) | |
| 289 | +POST /api/v1/_meta/metadata/custom-fields → CustomField (source='user') | |
| 290 | +PUT /api/v1/_meta/metadata/custom-fields/{key} → CustomField (source='user') | |
| 291 | +DELETE /api/v1/_meta/metadata/custom-fields/{key} → 204 (source='user' only) | |
| 292 | +``` | |
| 293 | + | |
| 294 | +The existing `GET` endpoints remain unchanged. Write endpoints require | |
| 295 | +`admin.metadata.write` permission. | |
| 296 | + | |
| 297 | +### Database | |
| 298 | + | |
| 299 | +The `metadata__form` and `metadata__list_view` tables already exist in | |
| 300 | +`000-platform-init.xml`. No new migrations needed — just new code that | |
| 301 | +reads/writes these tables. | |
| 302 | + | |
| 303 | +### MetadataLoader changes | |
| 304 | + | |
| 305 | +Add `forms:` and `listViews:` sections to the MetadataYaml schema so core | |
| 306 | +PBCs and plug-ins can ship default form and list-view definitions in their | |
| 307 | +metadata YAML. The loader processes them with the same delete-by-source | |
| 308 | +idempotency as entities, permissions, menus, and custom fields. | |
| 309 | + | |
| 310 | +### CustomFieldRegistry refresh | |
| 311 | + | |
| 312 | +After a custom field is created/updated/deleted through the REST API, the | |
| 313 | +registry must refresh. Add a `refresh()` call in the write endpoint handler | |
| 314 | +(same pattern as MetadataLoader already uses). | |
| 315 | + | |
| 316 | +## 9. SPA Dependencies | |
| 317 | + | |
| 318 | +New npm packages: | |
| 319 | +- `@rjsf/core` — React JSON Schema Form renderer | |
| 320 | +- `@rjsf/utils` — shared utilities | |
| 321 | +- `@rjsf/validator-ajv8` — JSON Schema validation via Ajv | |
| 322 | + | |
| 323 | +Estimated bundle impact: ~60KB gzipped (acceptable for an ERP SPA). | |
| 324 | + | |
| 325 | +## 10. File Inventory (New & Modified) | |
| 326 | + | |
| 327 | +### New files | |
| 328 | + | |
| 329 | +**Backend (Kotlin):** | |
| 330 | +- `platform/platform-metadata/src/.../web/FormDefinitionController.kt` — CRUD for form defs | |
| 331 | +- `platform/platform-metadata/src/.../web/ListViewDefinitionController.kt` — CRUD for list-view defs | |
| 332 | +- `platform/platform-metadata/src/.../web/CustomFieldWriteController.kt` — write endpoints for custom fields | |
| 333 | +- `platform/platform-metadata/src/.../yaml/FormYaml.kt` — YAML deserialization for forms | |
| 334 | +- `platform/platform-metadata/src/.../yaml/ListViewYaml.kt` — YAML deserialization for list views | |
| 335 | + | |
| 336 | +**Frontend (TypeScript/React):** | |
| 337 | +- `web/src/components/MetadataFormRenderer.tsx` — @rjsf wrapper with VibeErp theme | |
| 338 | +- `web/src/components/form-widgets/PartnerPicker.tsx` — partner search widget | |
| 339 | +- `web/src/components/form-widgets/ItemPicker.tsx` — item search widget | |
| 340 | +- `web/src/components/form-widgets/UomSelector.tsx` — UoM dropdown widget | |
| 341 | +- `web/src/components/form-widgets/LocationPicker.tsx` — location dropdown widget | |
| 342 | +- `web/src/components/form-widgets/MoneyInput.tsx` — money input widget | |
| 343 | +- `web/src/components/form-widgets/QuantityInput.tsx` — quantity input widget | |
| 344 | +- `web/src/components/form-widgets/index.ts` — widget registry | |
| 345 | +- `web/src/pages/FormDesignerPage.tsx` — structured property editor | |
| 346 | +- `web/src/pages/ListViewDesignerPage.tsx` — column/filter/sort editor | |
| 347 | +- `web/src/pages/MetadataAdminPage.tsx` — tabbed admin with sub-sections | |
| 348 | + | |
| 349 | +**Metadata YAML (reference):** | |
| 350 | +- Core PBCs ship default form definitions for user-task forms (if any) | |
| 351 | +- Printing-shop plug-in ships a `plate-approval-task` form definition | |
| 352 | + | |
| 353 | +### Modified files | |
| 354 | + | |
| 355 | +- `platform/platform-metadata/src/.../yaml/MetadataYaml.kt` — add `forms` + `listViews` sections | |
| 356 | +- `platform/platform-metadata/src/.../MetadataLoader.kt` — process forms + list views | |
| 357 | +- `platform/platform-metadata/src/.../web/MetadataController.kt` — add form/list-view GET endpoints | |
| 358 | +- `web/src/App.tsx` — add routes for designer + admin pages | |
| 359 | +- `web/src/api/client.ts` — add API functions for form/list-view/custom-field CRUD | |
| 360 | +- `web/src/i18n/messages.ts` — add message keys for new pages | |
| 361 | +- `web/src/types/api.ts` — add FormDefinition + ListViewDefinition types | |
| 362 | +- `web/package.json` — add @rjsf dependencies | |
| 363 | + | |
| 364 | +## 11. Testing Strategy | |
| 365 | + | |
| 366 | +**Backend unit tests:** | |
| 367 | +- FormDefinition CRUD (create, read, update, delete with source enforcement) | |
| 368 | +- ListViewDefinition CRUD (same) | |
| 369 | +- CustomField write endpoints (create, update, delete, registry refresh) | |
| 370 | +- MetadataLoader with forms + list views sections | |
| 371 | +- Source-tag enforcement (reject writes to core/plugin rows) | |
| 372 | + | |
| 373 | +**Frontend:** | |
| 374 | +- Manual smoke test: open form designer, create a form, preview it, save it | |
| 375 | +- Manual smoke test: open list view designer, configure columns, save | |
| 376 | +- Manual smoke test: open metadata admin, browse all tabs | |
| 377 | + | |
| 378 | +**Integration:** | |
| 379 | +- Printing-shop plug-in ships a `plate-approval-task` form definition | |
| 380 | +- Boot the framework, start the plate-approval BPMN process, verify the | |
| 381 | + user-task renders the form from metadata, complete it, verify values flow | |
| 382 | + through to the workflow | |
| 383 | + | |
| 384 | +## 12. Out of Scope (Deferred) | |
| 385 | + | |
| 386 | +- **Custom entities / dynamic DDL** — v1.1 | |
| 387 | +- **Drag-and-drop form designer** — future upgrade of the structured editor | |
| 388 | +- **Complex visibility conditions** — v1.0 supports "show when X equals Y" only | |
| 389 | +- **Form versioning / migration** — v1.1 (for now, bumping version is manual) | |
| 390 | +- **List view as override for core pages** — v1.1 (core pages keep hardcoded columns) | |
| 391 | +- **Import/export of form/list-view definitions** — v1.1 | ... | ... |