docs(32-01): complete cross-plugin admin form extensions plan
This commit is contained in:
322
.planning/phases/32-seo-foundation/32-01-SUMMARY.md
Normal file
322
.planning/phases/32-seo-foundation/32-01-SUMMARY.md
Normal file
@@ -0,0 +1,322 @@
|
||||
---
|
||||
phase: 32-seo-foundation
|
||||
plan: 01
|
||||
subsystem: admin
|
||||
tags: [cabana, pact, form-extensions, d-91, yaml, admin-spa]
|
||||
|
||||
requires:
|
||||
- phase: 12.x/19.x admin (cabana)
|
||||
provides: compiled fields.yaml forms, plugin field types, widgets, partials, AdminActionExtension record actions
|
||||
provides:
|
||||
- "pact.AdminActionExtension.Form with pact.AdminFormExtension (fields document via Fields path or FieldsYAML bytes, Hide, Permissions, Visible, Dir, Actions, PartialData, Load, Save)"
|
||||
- "pact.AdminFormLoad, AdminFormSave, AdminFormSaveResult, AdminFormErrors, AdminFormZoneTabs/Outside/Secondary"
|
||||
- "cabana compileFormExtensions: boot-time placement (field and tab anchors, anchor cursor, Requires() rule, cycles, Hide, D-101 zones) with clear boot errors"
|
||||
- "cabana runtime: extension values lifted before any owner path, Save in the record's save transaction, Load on show and save, visibility on schema/show/save/widgets/partials"
|
||||
affects: [32-02, 32-03, 32-04, 32-05, 32-06, sm-seo-plugin]
|
||||
|
||||
actuals:
|
||||
tokens: 34400
|
||||
tasks: 4
|
||||
commits: 5
|
||||
plan_head_before: 9868838fe3d01e07d578f2b2dd557d4296c48e10
|
||||
plan_head_after: 9ae326908fed1aea0fd5e251e402cc1f01322d9e
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Form extension fields document: fields.yaml format plus before:/after:/zone: per field and a top-level tabs: list, compiled through decodeFieldMapping"
|
||||
- "Extension-owned field registry (CompiledController.extensionFields) keys every owner-aware branch: lift, decode, hydrate, widgets, partials"
|
||||
- "One visibility helper (visibleExtensions) for schema, show and save"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- modules/cabana/form_extensions.go
|
||||
- modules/cabana/form_extensions_postgres_test.go
|
||||
- modules/cabana/form_extensions_smoke_test.go
|
||||
- modules/cabana/example_form_extension_test.go
|
||||
modified:
|
||||
- modules/pact/capabilities.go
|
||||
- modules/pact/README.md
|
||||
- modules/cabana/contracts.go
|
||||
- modules/cabana/http.go
|
||||
- modules/cabana/crud.go
|
||||
- modules/cabana/actions.go
|
||||
- modules/cabana/partial_render.go
|
||||
- modules/cabana/field_type.go
|
||||
- modules/cabana/field_ml.go
|
||||
- modules/cabana/README.md
|
||||
- docs/backend/admin-controllers.md
|
||||
- docs/setup/coming-from-wintercms.md
|
||||
- admin/tests/form/FormView.test.ts
|
||||
|
||||
key-decisions:
|
||||
- "D-91 Go shape (Task 1, answered by Jakub before dispatch): yaml-document. An extension carries a fields.yaml-format document whose fields take before:/after:/zone: and whose top-level tabs: list declares new tabs; pact.AdminFormTab and pact.AdminFormField are not published (redundant with the document)"
|
||||
- "Declaration form: AdminFormExtension.Fields is a path in the contributing plugin's AdminFS resolved with assetPath (like SettingsItem.Form and config_form.yaml form:); AdminFormExtension.FieldsYAML carries the document bytes for fields built at boot (D-104 per-type tabs); setting both stops the boot"
|
||||
- "A field with an anchor and no tab: joins the anchor's tab; a field's tab: must be an owner tab or one declared under tabs: (by itself or a required plugin)"
|
||||
- "Hide is static (every administrator); hidden owner fields also leave the owner's bindings, so show and save skip them"
|
||||
- "Extension field types: text, textarea, number, checkbox, switch, dropdown with listed options, markdown, mltext, mltextarea, mlmarkdown, plugin field types, widget, partial; fileupload refused per D-100; value fields need Save"
|
||||
- "Save runs after the owner's FormAfterCreate/FormAfterUpdate hooks inside the save transaction, on every create and update of a visible extension; Load runs on show and in the save response"
|
||||
- "Translatable extension values without a TranslationWriter are keyed by the admin's configured locale"
|
||||
- "Localized tab-label collisions are checked by checkFormExtensionTabLabels in Activate (where the translator is available), not inside compileFormExtensions"
|
||||
- "A widget element's script comes from the contributor's field type scripts or its plugin-level pact.AdminClientAssets (added to the target form's assets)"
|
||||
|
||||
patterns-established:
|
||||
- "Form extension boot errors: cabana: plugin <id>: controller \"<ctl>\": <field|tab|Hide> <name>: <reason>"
|
||||
- "Owner-aware branches check cc.extensionOf(name) before treating a form field as the owner's"
|
||||
|
||||
requirements-completed: [SC-1, SC-5, D-91, D-101, D-90, D-98, D-100, D-104]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Plugin B adds field C after A on plugin A's form: schema lists a, c, b; Save stores c in the contributor's table in the record's transaction; show serves c; owner model untouched"
|
||||
requirement: "SC-1"
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "modules/cabana/form_extensions_postgres_test.go#TestFormExtensionTracer"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Placement rules: before/after, end of tab, anchor tab inheritance, own-field anchors, new tabs between tabs, tab-less form plus new tab, regrouping, Hide, span kept"
|
||||
requirement: "D-91"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/form_extensions_smoke_test.go#TestFormExtensionPlacement"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "modules/cabana/form_extensions_smoke_test.go#TestFormExtensionTwoPluginsSameAnchor"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "modules/cabana/form_extensions_smoke_test.go#TestFormExtensionOnePluginTwoControllers"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Boot errors (22 cases plus localized tab-label collision) name plugin, controller and offending name; D-101 zones and D-100 fileupload refused"
|
||||
requirement: "D-101"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/form_extensions_smoke_test.go#TestFormExtensionBootErrors"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Save transaction semantics: AdminFormErrors 422 with rollback, plain error 500 with rollback, notices, hidden owner field, translatable locale maps, invisible extension inert, create"
|
||||
requirement: "D-91"
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "modules/cabana/form_extensions_postgres_test.go#TestFormExtensionSave"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "Extension widgets dispatch to AdminFormExtension.Actions, partials render from the contributor's Dir through PartialData with the GORM view-model guard, invisible extensions offer neither"
|
||||
requirement: "D-91"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/form_extensions_smoke_test.go#TestFormExtensionWidgetsAndPartials"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D6
|
||||
description: "Strictly additive: a form no extension lays out compiles byte-identically (CanonicalJSON), whole repo vet and tests green, admin OpenAPI unchanged"
|
||||
requirement: "SC-5"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "modules/cabana/form_extensions_smoke_test.go#TestFormExtensionUnextendedSchemaUnchanged"
|
||||
status: pass
|
||||
- kind: other
|
||||
ref: "go vet ./... && go test -short ./... && go test ./modules/cabana -count=1"
|
||||
status: pass
|
||||
- kind: other
|
||||
ref: "scripts/check-admin-openapi.sh --check"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D7
|
||||
description: "SPA renders compiled order with no admin/src change (UI-SPEC C1 backstop)"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "admin/tests/form/FormView.test.ts#form extensions in compiled order (UI-SPEC C1, D-91)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D8
|
||||
description: "Docs: admin-controllers section with synced ExtrasPlugin snippet, runnable Example_formExtension, pact and cabana READMEs, WinterCMS extendFormFields mapping; no SEO concept or consuming app named (D-90)"
|
||||
requirement: "D-90"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "go run ./cmd/summer docs:build --check && go test ./cmd/summer -run TestDocsTree"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "modules/cabana/example_form_extension_test.go#Example_formExtension"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D9
|
||||
description: "The published pact shape (yaml-document form) fits the SEO plugin's needs in plans 32-03/32-04 (SEO tab per type, per-type settings tabs via FieldsYAML)"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "One-way framework API; its fitness for sm-seo-plugin is proven only when plan 32-03 dogfoods it"
|
||||
|
||||
duration: 32min
|
||||
completed: 2026-10-26
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 32 Plan 01: Cross-plugin admin form extensions (D-91) Summary
|
||||
|
||||
**A plugin now places fields and tabs inside another plugin's admin form from a fields.yaml-format document with before:/after:/zone: anchors and a tabs: list, compiled once at boot, and loads and saves the values itself inside the record's save transaction without touching the owner's model.**
|
||||
|
||||
## Task 1 answer (D-91 Go shape)
|
||||
|
||||
Answered by Jakub before dispatch: **yaml-document**. Recorded shape:
|
||||
|
||||
- `pact.AdminActionExtension` gains `Form *pact.AdminFormExtension` as its last field.
|
||||
- `pact.AdminFormExtension{Fields string; FieldsYAML []byte; Hide, Permissions []string; Visible func(ctx) bool; Dir string; Actions []pact.AdminAction; PartialData func(ctx, name, record) (any, error); Load func(ctx, pact.AdminFormLoad) (map[string]any, error); Save func(ctx, pact.AdminFormSave) (pact.AdminFormSaveResult, error)}`.
|
||||
- `Fields` is a path in the contributing plugin's AdminFS, resolved with `assetPath` exactly like `pact.SettingsItem.Form` and `config_form.yaml` `form:` (so `extensions/posts.yaml`, `$/vendor/plugin/...` and `~/plugins/vendor/plugin/...` work). `FieldsYAML` is the document itself, which is how a plugin generates fields at boot (D-104 per-type tabs). At most one may be set.
|
||||
- Document format: `fields:` (every fields.yaml key, plus `before:`, `after:`, `zone:` per field) and `tabs:` (a list of `name`, `before`, `after`, `zone`). The placement keys are taken out of each field node before the rest compiles through the same `decodeFieldMapping`/`compileFieldNode` path as an owner's fields.yaml.
|
||||
- Kept as planned: `AdminFormLoad{DB, Controller, Record, ID}`, `AdminFormSave{DB, Controller, Record, ID, Created, Values}`, `AdminFormSaveResult{Notices}`, `AdminFormErrors` (+`Error()`), `AdminFormZoneTabs/Outside/Secondary` (now the `zone:` values).
|
||||
- Not published: `pact.AdminFormTab` and `pact.AdminFormField`. In the yaml-document form, tabs and fields live in the document.
|
||||
- Plan 32-03 should use this shape instead of the go-placement sketch in its `<interfaces>`. `AdminFormField{Config}` becomes a `fields:` entry, and per-type tabs become a `FieldsYAML` document built at boot.
|
||||
|
||||
**Plan start SHA (phase base for scripts/check-phase32.sh in plan 32-05): `9868838fe3d01e07d578f2b2dd557d4296c48e10`**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 32 min
|
||||
- **Started:** 2026-10-26T20:34:49Z
|
||||
- **Completed:** 2026-10-26T21:07:01Z
|
||||
- **Tasks:** 4 (Task 1 resolved before dispatch)
|
||||
- **Files modified:** 17
|
||||
|
||||
## Accomplishments
|
||||
- Additive pact API for form extensions, with every boot error and runtime rule documented on the type
|
||||
- `compileFormExtensions` covers:
|
||||
- placement: field and tab anchors, end of tab, an anchored field joining its anchor's tab
|
||||
- plugin order through an anchor cursor
|
||||
- the `Requires()` rule for anchors on another plugin's additions, and cycle detection inside one document
|
||||
- Hide, with checks on fields that reference the hidden one
|
||||
- D-101 zones and D-100 fileupload refusal
|
||||
- regrouping only for forms an extension lays out
|
||||
- Owner-aware branches:
|
||||
- widget tags use the contributor's prefix and run the extension's own `Actions`
|
||||
- partials render from the contributor's `Dir` through `PartialData`
|
||||
- plugin field types run `Decode` before `Save` and their `Load` after the extension's
|
||||
- translatable values reach `Save`/`Load` as locale maps
|
||||
- ML and plugin owner paths skip extension fields
|
||||
- Save path:
|
||||
- extension keys leave the body before any owner path reads them
|
||||
- `Save` runs in the record's transaction after the owner row and its after hooks
|
||||
- `AdminFormErrors` answers 422; any other error is a 500. Both roll everything back
|
||||
- notices join `meta.notices`
|
||||
- Permissions/Visible apply to schema, show, save, widget actions and partials through one helper
|
||||
- Docs (admin-controllers section with synced snippet, cabana/pact READMEs, WinterCMS mapping), runnable example, SPA backstop tests
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Confirm the Go shape**: no commit (answered before dispatch: yaml-document)
|
||||
2. **Task 2: Tracer (c after a through the admin API)**: `0819442` (feat)
|
||||
3. **Task 3: Full placement rules, boot errors, owner-aware branches (TDD)**:
|
||||
- RED `4bb22a8` (test)
|
||||
- GREEN `e7423bd` (feat)
|
||||
4. **Task 4: Docs, example, SPA backstop**:
|
||||
- `fa1af69` (docs)
|
||||
- `9ae3269` (test)
|
||||
|
||||
Tracer feedback gate: interactive, end-of-phase mode, automated-only verify. It re-ran green (TestFormExtensionTracer, `go vet ./...`, `go test -short ./...`) before expanding.
|
||||
|
||||
## TDD Gate Compliance
|
||||
|
||||
- RED `4bb22a8` before GREEN `e7423bd`.
|
||||
- RED evidence (TAP transcription of `go test -json`, target `TestFormExtensionBootErrors`): `RED_EVIDENCE_OK`. Failing in RED: Placement, TwoPluginsSameAnchor, BootErrors, WidgetsAndPartials.
|
||||
- `TestFormExtensionOnePluginTwoControllers` and `TestFormExtensionUnextendedSchemaUnchanged` already passed in RED. They guard invariants the tracer already met.
|
||||
- An initial RED run hit fixture errors (an owner model without the test's columns) and was fixed before the RED commit.
|
||||
- No REFACTOR commit.
|
||||
|
||||
## Files Created/Modified
|
||||
- `modules/pact/capabilities.go`: `Form` field, `AdminFormExtension` and the Load/Save/result/error types, zone constants
|
||||
- `modules/pact/README.md`: rows for each new type
|
||||
- `modules/cabana/form_extensions.go`: document parsing, placement layout, per-kind compile checks, tab-label check, permission validation, runtime (lift, Save, Load, visibility)
|
||||
- `modules/cabana/contracts.go`: `formExtensions`, `extensionFields`, `extensionPartials` on `CompiledController`
|
||||
- `modules/cabana/http.go`: wiring in Activate and the form schema filter
|
||||
- `modules/cabana/crud.go`: save and show wiring; `BindWritableFields` skips extension fields
|
||||
- `modules/cabana/actions.go`, `partial_render.go`: extension widget and partial dispatch
|
||||
- `modules/cabana/field_type.go`, `field_ml.go`: owner paths skip extension fields
|
||||
- Tests:
|
||||
- `form_extensions_smoke_test.go`, `form_extensions_postgres_test.go`
|
||||
- `example_form_extension_test.go`
|
||||
- `admin/tests/form/FormView.test.ts`
|
||||
- Docs: `docs/backend/admin-controllers.md`, `docs/setup/coming-from-wintercms.md`, `modules/cabana/README.md`
|
||||
|
||||
## Decisions Made
|
||||
See `key-decisions` in the frontmatter. The main one is the Task 1 yaml-document shape and its `Fields`/`FieldsYAML` declaration form, explained above.
|
||||
|
||||
## Acceptance-criteria adjustments (yaml-document form)
|
||||
- Task 2, `go doc ./modules/pact AdminFormExtension`: the plan expected `Tabs` and `Fields` struct fields.
|
||||
- Now prints `Fields, FieldsYAML, Hide, Permissions, Visible, Dir, Actions, PartialData, Load, Save`.
|
||||
- Tabs are a `tabs:` list in the document, not a struct field.
|
||||
- `grep "Form \*AdminFormExtension"`, the `compileFormExtensions` signature and its position before `compileActionExtensions`: unchanged, all pass.
|
||||
- Task 3 boot-error criterion (14 or more `--- PASS` subtests): 23 subtests pass.
|
||||
- The zone errors come from the document's `zone:` key, not a struct field.
|
||||
- Artifact `contains: "Form *AdminFormExtension"`: holds.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] `modules/cabana/contracts.go` modified (not in files_modified)**
|
||||
- **Found during:** Task 2
|
||||
- **Issue:** The extension-owned field registry has to live on `CompiledController`.
|
||||
- **Fix:** Added the unexported fields `formExtensions`, `extensionFields` and `extensionPartials`.
|
||||
- **Committed in:** `0819442`, `e7423bd`
|
||||
|
||||
**2. [Rule 3 - Blocking] The RED commit includes a no-op `checkFormExtensionTabLabels`**
|
||||
- **Found during:** Task 3 RED
|
||||
- **Issue:** The tab-label test needs the function to exist, or the build fails (INVALID_RED).
|
||||
- **Fix:** Declared it returning nil in the RED commit and implemented it in GREEN.
|
||||
- **Committed in:** `4bb22a8`
|
||||
|
||||
**3. [Rule 1 - Test fixture] Smoke owners use their own model, `feWide`; partial templates read `.Data`**
|
||||
- **Found during:** Task 3 RED and GREEN
|
||||
- **Fix:** Test-only changes.
|
||||
|
||||
**4. [Plan detail] The example lives in package `cabana` as `Example_formExtension`**
|
||||
- **Issue:** The compile path is unexported, so an external `cabana_test` example could not show the compiled order.
|
||||
- **Fix:** The docs snippet (`ExtrasPlugin.AdminActionExtensions`) is unaffected.
|
||||
|
||||
**5. [Plan detail] Commit split**
|
||||
- Task 3's compile pass and runtime share `form_extensions.go`, so GREEN is one feat commit instead of two.
|
||||
- The Task 4 docs commit carries the planned message ("... and pact READMEs"), but the pact README rows had already landed in the Task 2 commit, as the documentation rule requires.
|
||||
|
||||
**6. [Rule 2 - Missing critical] Permission codes of extensions and their widget actions are validated at boot**
|
||||
- **Fix:** Added `validateFormExtensionPermissions` after `compileContributions`, mirroring the controller checks.
|
||||
- **Committed in:** `e7423bd`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 6 (2 blocking, 1 test fixture, 2 plan detail, 1 missing critical).
|
||||
**Impact on plan:** Behaviour is as planned; only the declaration form changed (Task 1). No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
None.
|
||||
|
||||
## Threat model
|
||||
- T-32-01 (extension values reaching the owner's model): mitigated. Values are lifted first, `BindWritableFields` skips extension fields, and ML/plugin owner paths skip them. Covered by TestFormExtensionTracer and TestFormExtensionSave.
|
||||
- T-32-02 (posted keys of an invisible extension): mitigated. `visibleExtensions` applies on schema, show, save, widgets and partials. Covered by TestFormExtensionSave and TestFormExtensionWidgetsAndPartials.
|
||||
- T-32-03 (partial disclosure): mitigated. `refusedViewModel` guards extension partials. Covered by TestFormExtensionWidgetsAndPartials (leak case).
|
||||
- T-32-SC: no packages installed.
|
||||
|
||||
## User Setup Required
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Plans 32-02 through 32-06 can build on the seam.
|
||||
- Plan 32-03 must use the yaml-document shape recorded above: the SEO tab is a `FieldsYAML` or `Fields` document with `tabs: [{name: golem15.seo::lang.tab.seo}]`, and the per-type settings tabs are `FieldsYAML` built at boot.
|
||||
- `AdminFormSave` has no `Changes` yet; plan 32-02 adds it.
|
||||
|
||||
## Self-Check: PASSED
|
||||
- All key files exist on disk, and commits 0819442, 4bb22a8, e7423bd, fa1af69 and 9ae3269 are present in `git log`.
|
||||
- Commands run green:
|
||||
- `go vet ./...` and `go test -short ./...`
|
||||
- `go test ./modules/cabana -count=1` (Docker), plus TestFormExtensionTracer and TestFormExtensionSave on their own
|
||||
- `docs:build --check` and `TestDocsTree`
|
||||
- the FormView tests and `typecheck`
|
||||
- `scripts/check-admin-openapi.sh --check`
|
||||
- `git diff --name-only 9868838..HEAD -- admin/src` is empty, and the D-90 grep finds nothing.
|
||||
|
||||
---
|
||||
*Phase: 32-seo-foundation*
|
||||
*Completed: 2026-10-26*
|
||||
Reference in New Issue
Block a user