docs(16.2-01): complete plugin field type framework plan
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
---
|
||||
phase: 16.2-plugin-provided-field-types-and-the-blog-editor-formwidget
|
||||
plan: 01
|
||||
subsystem: admin
|
||||
tags: [pact, cabana, admin-spa, field-types, custom-elements, i18n, uploads, openapi]
|
||||
|
||||
requires:
|
||||
- phase: 16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f
|
||||
provides: bluemonday-era Journal plugin and the v0.1.17 framework release this API builds on
|
||||
provides:
|
||||
- pact plugin field type API (HasFormFieldTypes, FormFieldType, FormFieldValue, FormFieldDecoded, FormFieldNotice, FormFieldUpload, FormFieldUploadResult, FormFieldError)
|
||||
- cabana per-boot field type registry, compile checks, Decode/Load/ListValue runner, translatable storage through the ML path, untouched skip, meta.notices
|
||||
- cabana FieldWrite, FieldWritesFromContext, WithFieldWrites, CompileFormWithTypes, FieldTypeAssets, FieldTypesResponse, FieldUploadResponse, FieldNotice, RecordMeta.Notices
|
||||
- routes GET {api}/field-types and POST {api}/{vendor}/{plugin}/{controller}/{id}/fields/{field}/upload
|
||||
- admin SPA global field type loader, PluginField and MLPluginField element hosts, element uploads, save notices
|
||||
affects: [16.2-02, 16.2-03, 16.2-05, 16.2-06, journal, payment]
|
||||
|
||||
actuals:
|
||||
tokens: 62800
|
||||
tasks: 4
|
||||
commits: 5
|
||||
plan_head_before: 0754a6e5ab88b1a4ad3b6fc3599a338490220077
|
||||
plan_head_after: 4adb48a76e5bd81f589385d22fc82a5c55dcb3e7
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Plugin capability registry built per Activate (compileFieldTypes) and passed down the compile functions; no package-level registry"
|
||||
- "Untouched-value skip by canonical encoding/json comparison against the served value (Load of the stored value)"
|
||||
- "Global plugin stylesheets owned by GLOBAL_STYLE_OWNER, never disabled by activateStyles"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- modules/cabana/field_type.go
|
||||
- modules/cabana/field_type_upload.go
|
||||
- modules/cabana/field_type_smoke_test.go
|
||||
- modules/cabana/example_field_type_test.go
|
||||
- admin/src/app/fieldTypes.ts
|
||||
- admin/src/components/form/fields/PluginField.vue
|
||||
- admin/src/components/form/fields/MLPluginField.vue
|
||||
- admin/tests/form/PluginField.test.ts
|
||||
- docs/backend/field-types.md
|
||||
modified:
|
||||
- modules/pact/capabilities.go
|
||||
- modules/pact/README.md
|
||||
- modules/cabana/form_schema.go
|
||||
- modules/cabana/schema_types.go
|
||||
- modules/cabana/registry.go
|
||||
- modules/cabana/contracts.go
|
||||
- modules/cabana/http.go
|
||||
- modules/cabana/crud.go
|
||||
- modules/cabana/field_ml.go
|
||||
- modules/cabana/tx_context.go
|
||||
- modules/cabana/list_schema.go
|
||||
- modules/cabana/query.go
|
||||
- modules/cabana/settings.go
|
||||
- modules/cabana/relation.go
|
||||
- modules/cabana/relation_form.go
|
||||
- modules/cabana/relation_field.go
|
||||
- modules/cabana/extension.go
|
||||
- modules/cabana/admin_openapi.go
|
||||
- modules/cabana/security_coverage_test.go
|
||||
- modules/cabana/phase10_coverage_test.go
|
||||
- modules/cabana/phase10_csrf_test.go
|
||||
- modules/cabana/README.md
|
||||
- admin/openapi/admin.json
|
||||
- admin/src/api/schema.d.ts
|
||||
- admin/src/api/types.ts
|
||||
- admin/src/app/pluginAssets.ts
|
||||
- admin/src/main.ts
|
||||
- admin/src/views/LoginView.vue
|
||||
- admin/src/views/FormView.vue
|
||||
- admin/src/components/form/registry.ts
|
||||
- admin/src/components/form/FieldRenderer.vue
|
||||
- admin/src/components/form/PreviewField.vue
|
||||
- admin/src/components/form/FormField.vue
|
||||
- admin/src/components/form/FormGrid.vue
|
||||
- admin/src/components/form/formState.ts
|
||||
- admin/src/components/form/formContext.ts
|
||||
- admin/src/components/form/mlLocale.ts
|
||||
- admin/src/components/form/fields/MLField.vue
|
||||
- admin/src/components/relation/RelationChildModal.vue
|
||||
- admin/tests/form/FormField.test.ts
|
||||
- admin/tests/form/formState.test.ts
|
||||
- admin/tests/list/CellValue.test.ts
|
||||
- modules/boardwalk/dist
|
||||
- docs/backend/forms.md
|
||||
- docs/backend/lists-and-filters.md
|
||||
|
||||
key-decisions:
|
||||
- "pact.FormFieldType.ListValue is func(ctx context.Context, value any) string (human adjust at the Task 1 checkpoint); plans 03, 05 and 06 must use the ctx form"
|
||||
- "The plugin field type registry is built per Activate by compileFieldTypes and passed down; CompileForm, compileRegistry, compileList, compileRelations and compileSetting keep their signatures as nil-registry wrappers"
|
||||
- "Only a pact.FormFieldError from Decode or Upload is a 422 (messages localized); every other plugin error, Load errors included, is the opaque lifecycle 500"
|
||||
- "Untouched values (D-03 server half) are skipped on update when the canonical encoding/json of the submitted value equals the served value (stored host value or exact translation, then Load)"
|
||||
- "FieldTypesResponse is the GET /field-types envelope {data, meta}; FieldUploadResponse is the upload data object {url}, served as Envelope[FieldUploadResponse]"
|
||||
|
||||
patterns-established:
|
||||
- "Plugin field type: alias + element + assets + options + Go funcs; cabana owns routes, permissions, scope, locale storage, notices and the untouched skip"
|
||||
- "Element contract: value property, plain attributes, summer-change, summer-upload/summer-upload-result; no credentials, --c-* only"
|
||||
|
||||
requirements-completed: [D-03, D-06, D-10, D-11, D-12, D-13, D-14, D-15, D-18]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "pact field type API and cabana boot checks: alias collisions (two plugins, core type), element prefix, unknown keys, CheckOptions errors, settings/relation refusal"
|
||||
requirement: D-11
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/field_type_smoke_test.go#TestFieldTypeTracerSmoke"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Save decodes plugin values (FormFieldError 422, opaque 500 otherwise), meta.notices only when a type reported something, Show/save serve through Load, GET /field-types and form schema keys"
|
||||
requirement: D-12
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/field_type_smoke_test.go#TestFieldTypeTracerSmoke"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "modules/cabana/example_field_type_test.go#ExampleCompileFormWithTypes"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Translatable types per locale through the ML path, per-locale notices, untouched skip on update, FieldWritesFromContext in Form hooks, ListValue list columns, upload route (201, 404, 403, CSRF, 413, 422)"
|
||||
requirement: D-14
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/field_type_smoke_test.go#TestFieldTypeExpansionSmoke"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Admin SPA: global field type loader, PluginField/MLPluginField, element uploads, save notices under fields and in the toast, stored value reload, plugin list columns as text"
|
||||
requirement: D-13
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "admin/tests/form/PluginField.test.ts"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "admin/tests/form/FormField.test.ts#save notices"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "admin/tests/form/formState.test.ts#noticeLines"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "admin/tests/list/CellValue.test.ts#plugin field type columns (16.2)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "OpenAPI, schema.d.ts, dist, READMEs and docs/backend/field-types.md describe the API with neutral names"
|
||||
requirement: D-15
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "bash scripts/check-admin-openapi.sh --check && bash scripts/check-admin-dist.sh && go test ./cmd/summer -run TestDocsTree"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D6
|
||||
description: "Postgres-backed save, show and upload with a real record (id != 0 upload path, real transaction, real translation writer)"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "No Docker on this host (RESEARCH Pitfall 11); plan 06 runs these on a Docker host"
|
||||
|
||||
duration: 34min
|
||||
completed: 2026-10-10
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 16.2 Plan 01: Plugin field type framework Summary
|
||||
|
||||
**Plugins register value-bearing admin field types through pact.HasFormFieldTypes; cabana compiles, decodes (with localized meta.notices), loads, lists, uploads and stores them (translatable ones through the ML path, untouched values skipped), and the admin SPA hosts their custom elements globally with uploads and save notices.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 34 min
|
||||
- **Started:** 2026-10-10T08:53:13Z
|
||||
- **Completed:** 2026-10-10T09:27:00Z
|
||||
- **Tasks:** 4 (Task 1 checkpoint resolved by the orchestrator, Tasks 2-4 executed)
|
||||
- **Files modified:** 51 (excluding the rebuilt dist assets)
|
||||
|
||||
## Task 1 checkpoint (resolved)
|
||||
|
||||
Baseline verify was run by the orchestrator (vet passed, 4 PASS lines, clean `git status --porcelain -- modules admin docs scripts`). Human reply, verbatim:
|
||||
|
||||
> adjust: ListValue(ctx context.Context, value any) string
|
||||
|
||||
Final identifier list used by Tasks 2-4: every pact and cabana identifier exactly as in the plan's `<interfaces>` block, except `pact.FormFieldType.ListValue` is `func(ctx context.Context, value any) string` (was `func(value any) string`). The element contract is unchanged (value property; attributes field-name, record-id, locale, ui-locale, options, disabled, invalid; events summer-change, summer-upload, summer-upload-result).
|
||||
|
||||
**Note for plans 03, 05 and 06:** they refer to `ListValue` and must use the ctx form; plan 03's Journal `ListValue` implementation receives `ctx`.
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- pact: `FormFieldType` (Name, Element, Translatable, Scripts, Styles, Options, CheckOptions, Decode, Load, ListValue with ctx, Upload), `FormFieldValue`, `FormFieldDecoded`, `FormFieldNotice`, `FormFieldUpload`, `FormFieldUploadResult`, `FormFieldError`, `HasFormFieldTypes`.
|
||||
- cabana boot: `compileFieldTypes` (alias shape, core-type and cross-plugin collisions naming both plugins, element prefix via `checkWidgetTag`, Decode required, option keys, assets through the extracted `readClientAssets`), passed to `compileRegistryWithTypes`; fields.yaml accepts a plugin alias with the general keys plus its options (`typeOptions`, `element`, `translatable` in the schema); `CheckOptions` at boot; settings and relation forms refuse plugin aliases; `CompileFormWithTypes`.
|
||||
- cabana save/show: `liftPluginValues`, `decodePluginFields` inside the transaction (per locale for translatable types, untouched skip on update via `servedFieldValue`), `localizeNotices` into `RecordMeta.Notices` (`meta.notices`, max 10 per field and locale, 500 runes), `hydratePluginFields` (per locale), `FieldWritesFromContext` for Form hooks, `isMLField` routing translatable types through `liftMLValues`/`applyMLTranslations`/`hydrateMLRecord`.
|
||||
- cabana list and routes: plugin aliases as column types with `ListValue` in `projectRowCtx`; `GET /field-types`; `POST .../{id}/fields/{field}/upload` (`fieldUpload`), annotated, in `phase09Routes`, CSRF inventories updated.
|
||||
- SPA: `loadFieldTypes` after boot and login, `loadGlobalStyles`/`GLOBAL_STYLE_OWNER`, `PluginField` (value/attributes/summer-change/echo rule/uploads), `MLPluginField`, `rendererForField`/`isRegisteredField`/`isMLField`, `noticeLines` with the notice status line in `FormField` and the saved toast, rebuilt `modules/boardwalk/dist`.
|
||||
- Docs: pact and cabana READMEs, new `docs/backend/field-types.md` (registration, aliases, options, Decode/notices/refusal, Load, notices, translatable types, untouched values, companion columns, list columns, uploads, full element contract), links from forms.md and lists-and-filters.md.
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Confirm the pact API** - resolved by the orchestrator (no commit)
|
||||
2. **Task 2: Tracer** - `1e20ace` (feat, pact), `717d46e` (feat, cabana + docs + OpenAPI), `83fa2f7` (feat, admin SPA + dist)
|
||||
3. **Task 3: Go expansion** - `8fa92db` (feat)
|
||||
4. **Task 4: SPA expansion** - `4adb48a` (feat)
|
||||
|
||||
## Smoke test results
|
||||
|
||||
- `go vet ./...` clean; `TestFieldTypeTracerSmoke` (15 subtests), `TestFieldTypeExpansionSmoke` (14 subtests), `TestPhase09ContractInventory`, `TestPhase09PermissionMatrix`, `TestFormSchemaCompile`, `TestFormSchemaRejects`, `TestListRelationSearchSmoke`, `TestEnabledLocalesDefaultFirstSmoke`, `TestPhase10CSRF`, `ExampleCompileFormWithTypes` pass.
|
||||
- `go test ./...`: the only failures are the testcontainers suites that need Docker (permission denied on /var/run/docker.sock), the same set as before this plan; no non-Docker failure message.
|
||||
- Admin: `npm run typecheck` clean, full vitest suite 77 files / 1190 tests pass, `check-admin-openapi.sh --check`, `check-admin-dist.sh`, `TestDocsTree` and `summer docs:build --check` pass.
|
||||
- Neutral names: `grep -rniE 'journal|grzyby' modules/pact/README.md modules/cabana/README.md docs/backend/field-types.md` prints nothing.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
See key-decisions in the frontmatter. In short: ListValue takes ctx (human), the registry is passed down per boot with nil-registry wrappers for existing callers, only `pact.FormFieldError` is a 422, untouched detection compares canonical JSON with the served value, and the two response types are an envelope (`FieldTypesResponse`) and a data object (`FieldUploadResponse`).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Route inventories outside the plan's file list**
|
||||
- **Found during:** Task 3
|
||||
- **Issue:** `TestPhase10CSRF` (counts 29 state-changing routes) and `TestPhase10Coverage/every unsafe mounted route is CSRF-walked` failed once the upload route was mounted.
|
||||
- **Fix:** Added the route to both inventories (29 -> 30).
|
||||
- **Files modified:** modules/cabana/phase10_csrf_test.go, modules/cabana/phase10_coverage_test.go
|
||||
- **Commit:** 8fa92db
|
||||
|
||||
**2. [Rule 3 - Blocking] Docs Go snippets need src= references**
|
||||
- **Found during:** Task 2
|
||||
- **Issue:** TestDocsTree refuses a Go code block without `src=`.
|
||||
- **Fix:** Added `modules/cabana/example_field_type_test.go` (`NotesPlugin.FormFieldTypes`, `ExampleCompileFormWithTypes` with output) and referenced it from docs/backend/field-types.md (synced with `summer docs:sync`).
|
||||
- **Commit:** 717d46e
|
||||
|
||||
**3. [Rule 2 - Correctness] Strict error mapping for plugin code**
|
||||
- **Found during:** Task 2
|
||||
- **Issue:** `lifecycleFailure` passes a plugin-returned `ValidationError` or `ForbiddenError` through; the plan allows only `pact.FormFieldError` to reach the client.
|
||||
- **Fix:** `fieldTypeFailure` maps only `*pact.FormFieldError` (messages localized) to 422; every other Decode/Upload error and every Load error is logged and becomes the opaque lifecycle 500.
|
||||
- **Commit:** 717d46e
|
||||
|
||||
**4. Additional files touched for threading the registry and list values**
|
||||
- modules/cabana/contracts.go (`Registry.fieldTypes`), modules/cabana/relation.go (`compileRelationsWithTypes`), modules/cabana/query.go (`projectRowCtx` with the request ctx for `ListValue`), docs/backend/lists-and-filters.md (plugin column types), admin/tests/list/CellValue.test.ts. `admin/src/views/ListView.vue` needed no change: `CellValue` already renders a column type it does not know as plain text.
|
||||
|
||||
**5. [CLAUDE.md] TDD commit shape**
|
||||
- Tasks 3 and 4 are `tdd="true"`, but CLAUDE.md requires `go test ./...` green at every commit, so no failing RED test was committed. Task 3: the expansion smoke test was written first against compile stubs and failed on the planned behavior (`compile: ... unsupported column type notes`) before the implementation made it pass; test and implementation were committed together. Task 4: the SPA implementation was written before its tests in the same uncommitted change; the tests were then added and pass.
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 3 auto-fixed (2 blocking, 1 correctness) plus 2 documented process/scope notes. **Impact:** no change to the planned API or behaviour; extra files are route inventories, a docs example and registry plumbing.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- No Docker on this host: the Postgres-backed save, show and upload paths with a real record (the upload route with `{id}` other than 0, a real transaction and translation writer) are covered only by DryRun/unit smoke tests here; full coverage is plan 06 on a Docker host.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Plans 02 (Journal editor element) and 03 (Journal Go integration) can build on this API; plan 03 must implement `ListValue(ctx, value)`.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- Created files exist: modules/cabana/field_type.go, modules/cabana/field_type_upload.go, modules/cabana/field_type_smoke_test.go, modules/cabana/example_field_type_test.go, admin/src/app/fieldTypes.ts, admin/src/components/form/fields/PluginField.vue, admin/src/components/form/fields/MLPluginField.vue, admin/tests/form/PluginField.test.ts, docs/backend/field-types.md.
|
||||
- Commits exist: 1e20ace, 717d46e, 83fa2f7, 8fa92db, 4adb48a.
|
||||
Reference in New Issue
Block a user