278 lines
17 KiB
Markdown
278 lines
17 KiB
Markdown
---
|
|
phase: 10.1-runtime-admin-extension-point
|
|
plan: 02
|
|
subsystem: ui
|
|
tags: [vue, admin-spa, custom-elements, openapi-fetch, tailwind, boardwalk, phrasebook]
|
|
|
|
requires:
|
|
- phase: 10.1-runtime-admin-extension-point
|
|
provides: "10.1-01 widget, toolbar and partial routes, plugin asset route, schema keys assets/toolbarActions/headerPartial and widget/action/actionLabel/fill/path, typed in schema.d.ts"
|
|
- phase: 10-admin-vue-spa
|
|
provides: admin SPA, typed openapi-fetch client, field registry, toast queue, hygiene and dist gates
|
|
provides:
|
|
- admin/src/app/pluginAssets.ts (loadScript, loadStyles, activateStyles, loadControllerAssets, assetAllowed, assetPrefix, OWNER_ATTRIBUTE)
|
|
- formContext InjectionKeys FORM_VALUES, FORM_PATCH, FORM_LOCALE, FORM_ASSETS and the summer-action / 5000 ms widget contract
|
|
- WidgetField (type widget) and PartialField (type partial), both valueless and group-labelled, rendered on create and update
|
|
- PartialHost with partialNodes.ts (PARTIAL_TAGS, PARTIAL_DROPPED_TAGS, partialAttrAllowed, renderPartialNodes)
|
|
- ExtensionFailure box (UI-SPEC S5)
|
|
- list headerPartial host with refetch after bulk delete and toolbar actions
|
|
- ListToolbar actions/busyAction props and action event; ListView onAction posting {} to toolbar/{action}
|
|
- partial style kit (.summer-partial, .summer-stats, .summer-stat, .summer-stat__label, .summer-stat__value) documented with the public --c-* variables in the cabana README
|
|
- backend::lang.extension.{busy,widget_failed,partial_failed,action_failed} in en and pl
|
|
- Vite dev proxy for {prefix}/assets
|
|
- rebuilt modules/boardwalk/dist
|
|
affects: [10.1-03 application widget, partial and toolbar proof, 10.1-04 unit tests and gate, 14 Discogs]
|
|
|
|
actuals:
|
|
tokens: 17900
|
|
tasks: 3
|
|
commits: 3
|
|
plan_head_before: c3b76b1afb977cb3f72272f9876c65c8da7877ed
|
|
plan_head_after: a5e7dac10f2ebf10b606e3de1a9c1dc89c0803a1
|
|
|
|
tech-stack:
|
|
added: []
|
|
patterns:
|
|
- "Plugin custom elements are created imperatively in a node Vue never renders children into; data goes in as attributes only and requests come out as a bubbling summer-action event"
|
|
- "Server node trees are rebuilt with h() under a client copy of the server allowlist; no string is parsed as markup"
|
|
- "Controller assets load when the controller's list or form schema arrives; stylesheet links are owned per controller and toggled on every open"
|
|
|
|
key-files:
|
|
created:
|
|
- admin/src/app/pluginAssets.ts
|
|
- admin/src/components/form/formContext.ts
|
|
- admin/src/components/form/fields/WidgetField.vue
|
|
- admin/src/components/form/fields/PartialField.vue
|
|
- admin/src/components/partial/PartialHost.vue
|
|
- admin/src/components/partial/partialNodes.ts
|
|
- admin/src/components/ui/ExtensionFailure.vue
|
|
- admin/tests/fixtures/extension.form-schema.json
|
|
- admin/tests/fixtures/extension.list-schema.json
|
|
- admin/tests/fixtures/extension.partial.json
|
|
- admin/tests/smoke/extension.smoke.test.ts
|
|
modified:
|
|
- admin/src/api/types.ts
|
|
- admin/src/components/form/registry.ts
|
|
- admin/src/components/form/FormField.vue
|
|
- admin/src/components/list/ListToolbar.vue
|
|
- admin/src/views/FormView.vue
|
|
- admin/src/views/ListView.vue
|
|
- admin/src/styles/main.css
|
|
- admin/vite.config.ts
|
|
- admin/tests/fixtures/typed.ts
|
|
- admin/tests/fixtures/lang.json
|
|
- modules/phrasebook/backend/lang/en/lang.yaml
|
|
- modules/phrasebook/backend/lang/pl/lang.yaml
|
|
- modules/cabana/README.md
|
|
- modules/boardwalk/dist/**
|
|
|
|
key-decisions:
|
|
- "A widget posts only the current values of its fill keys as values, since the server passes nothing else to the action"
|
|
- "loadControllerAssets resolves with the failed script URLs; a widget whose element is still undefined after a script failure shows the failure box at once instead of waiting 5000 ms"
|
|
- "Stylesheet links are keyed by controller and URL, so two controllers of one plugin sharing a file each keep their own link"
|
|
- "The client allowlist also drops the server's removed tags (script, style, svg and so on) with their subtree instead of unwrapping them"
|
|
- "A widget or partial field without a label has no label row and no aria-labelledby; FormField renders the label as a span only when one is declared"
|
|
- "Empty action messages are not toasted"
|
|
|
|
patterns-established:
|
|
- "Extension controls read form state through formContext injections with safe defaults, so settings forms without a provider still render"
|
|
- "Smoke tests stub document.head.appendChild so plugin scripts and links never load in happy-dom"
|
|
|
|
requirements-completed: [ADMIN-07]
|
|
|
|
coverage:
|
|
- id: D1
|
|
description: "A type: widget field loads its controller script, mounts the custom element with attributes only, posts summer-action through the typed client once while busy, patches only returned fill keys without saving, and toasts success or failure"
|
|
requirement: ADMIN-07
|
|
verification:
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#widget field (D-04, D-05, D-07, D-08)"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D2
|
|
description: "Plugin assets load only from {base}/assets/, once per URL with retry after failure, and stylesheet links of other controllers are disabled"
|
|
requirement: ADMIN-07
|
|
verification:
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#plugin assets (D-14, D-16)"
|
|
status: pass
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#controller stylesheets (D-14, UI-SPEC S6)"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D3
|
|
description: "Header and form partials render the server node tree through the client allowlist with skeleton, empty, failure and keep-on-refetch states; hostile nodes never reach the DOM"
|
|
requirement: ADMIN-07
|
|
verification:
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#list header partial (D-03, D-11, UI-SPEC S1)"
|
|
status: pass
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#form partial (D-09, UI-SPEC S2)"
|
|
status: pass
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#partial node allowlist (D-17)"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D4
|
|
description: "Registered toolbar actions render after delete for admins who may run them, post {} with busy state, toast, reload the list and refetch the header partial"
|
|
requirement: ADMIN-07
|
|
verification:
|
|
- kind: automated_ui
|
|
ref: "admin/tests/smoke/extension.smoke.test.ts#toolbar actions (D-12, UI-SPEC S4)"
|
|
status: pass
|
|
- kind: unit
|
|
ref: "admin/tests/list/ListToolbar.test.ts (Phase 10 create/delete behaviour unchanged)"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D5
|
|
description: "Framework strings resolve in en and pl, the embedded dist matches a fresh build and the Phase 10 hygiene rules hold"
|
|
requirement: ADMIN-07
|
|
verification:
|
|
- kind: unit
|
|
ref: "modules/phrasebook/phase10_test.go#TestPhase10SPAKeysResolve"
|
|
status: pass
|
|
- kind: other
|
|
ref: "scripts/check-admin-dist.sh"
|
|
status: pass
|
|
- kind: other
|
|
ref: "scripts/check-phase10.sh --hygiene"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D6
|
|
description: "Partial style kit and failure box look native in light and dark mode, labels wrap at 768px with pl copy, plugin module scripts load under CSP script-src 'self' and through the Vite /assets dev proxy"
|
|
verification: []
|
|
human_judgment: true
|
|
rationale: "Visual layout, real module loading and the dev proxy need a real browser; happy-dom neither lays out nor loads module scripts (plan Task 3 human-check, UI-SPEC backstop rows)"
|
|
|
|
duration: 17min
|
|
completed: 2026-09-29
|
|
status: complete
|
|
---
|
|
|
|
# Phase 10.1 Plan 02: Runtime admin extension point (SPA half) Summary
|
|
|
|
**The admin SPA now mounts plugin custom elements as form widgets and bridges their summer-action events to the typed widget POST. It renders header and form partials through a client-side h() allowlist, runs registered toolbar actions, and loads each controller's JS and CSS from {base}/assets/ with per-controller stylesheet scoping. The embedded dist is rebuilt.**
|
|
|
|
## Performance
|
|
|
|
- **Duration:** 17 min
|
|
- **Started:** 2026-09-28T23:56:52Z
|
|
- **Completed:** 2026-09-29T00:14:07Z
|
|
- **Tasks:** 3
|
|
- **Files modified:** 29 (24 source and test files plus the dist)
|
|
|
|
## Accomplishments
|
|
|
|
- `pluginAssets.ts` loads each module script once per URL, and a failed script can retry on a later navigation. It refuses any URL outside `{base}/assets/`, including dot segments, backslashes and control characters. Stylesheet links belong to one controller, and opening another controller's list or form disables them.
|
|
- `WidgetField` shows a skeleton while it waits for the controller's scripts and `customElements.whenDefined`, with a 5000 ms timeout. It then creates the element and sets attributes only: `record-id`, `field-name`, `locale`, `fill-values`, `label` and `busy-label`. `fill-values` stays in sync as the form changes. On `summer-action` it posts `{record_id, values}` through `api`, ignoring repeat events while `busy` is set. On success it patches only the declared fill keys the server returned, which makes the form dirty without saving it. On failure it shows a danger toast and sets `state="error"`. A load failure shows the S5 failure box.
|
|
- `partialNodes.ts` and `PartialHost` rebuild the server node tree with `h()` under the server's tag, attribute and URL lists. The first load shows an 80px (header) or 44px (field) skeleton. A refetch keeps the current nodes with `aria-busy`, zero nodes render nothing, and a failure shows the `partial_failed` box.
|
|
- `type: widget` and `type: partial` are registered as valueless, group-labelled types. They never appear in the save body and they render on create.
|
|
- The list's `headerPartial` sits between the heading and the list card. It refetches after a bulk delete or a toolbar action, but not on search, filter, sort or page changes.
|
|
- Registered toolbar actions render after Delete as outline buttons and stay enabled whatever the selection. During their POST of `{}` they are disabled with `aria-busy`. Success shows a toast, reloads the list and refetches the partial. Failure shows a danger toast.
|
|
- The partial style kit is in `main.css`. The cabana README documents it together with the public `--c-*` variables. The `backend::lang.extension.*` strings exist in en and pl. The Vite dev server proxies `{prefix}/assets`.
|
|
|
|
## Task Commits
|
|
|
|
1. **Task 1: widget tracer (assets, form context, WidgetField, strings)** - `107d820` (feat)
|
|
2. **Task 2: header and form partials, style kit, README** - `9df9fae` (feat)
|
|
3. **Task 3: toolbar actions, per-controller CSS on lists, dev proxy** - `a5e7dac` (feat)
|
|
|
|
## Files Created/Modified
|
|
|
|
- `admin/src/app/pluginAssets.ts`: asset loader with the same-origin prefix check and per-controller stylesheet ownership
|
|
- `admin/src/components/form/formContext.ts`: form injection keys and the widget event/timeout constants
|
|
- `admin/src/components/form/fields/WidgetField.vue`: custom-element host and action bridge
|
|
- `admin/src/components/form/fields/PartialField.vue`: form row wrapper around PartialHost
|
|
- `admin/src/components/partial/PartialHost.vue`, `partialNodes.ts`: partial fetch, states and the client allowlist renderer
|
|
- `admin/src/components/ui/ExtensionFailure.vue`: shared S5 failure box
|
|
- `admin/src/components/form/registry.ts`, `FormField.vue`: widget/partial registration, valueless set, `groupLabelled`, span label
|
|
- `admin/src/views/FormView.vue`: form context provider and background asset load
|
|
- `admin/src/views/ListView.vue`, `admin/src/components/list/ListToolbar.vue`: header partial, toolbar actions, list asset load
|
|
- `admin/src/styles/main.css`: partial style kit
|
|
- `admin/vite.config.ts`: `/assets` dev proxy
|
|
- `modules/phrasebook/backend/lang/{en,pl}/lang.yaml`: `extension:` group
|
|
- `modules/cabana/README.md`: "Partial style kit and plugin CSS variables"
|
|
- `admin/tests/smoke/extension.smoke.test.ts` and three `extension.*.json` fixtures
|
|
- `modules/boardwalk/dist`: rebuilt
|
|
|
|
## Decisions Made
|
|
|
|
- A widget sends only its fill keys' current values. The server passes nothing else to the action, so sending more would only expose form data.
|
|
- `loadControllerAssets` resolves with the list of failed script URLs. If a script failed and the element is still undefined, the widget shows its failure box at once instead of waiting out the 5000 ms timeout.
|
|
- Stylesheet links are keyed by controller and URL. Two controllers of one plugin can share a CSS file without one disabling the other's link. `loadControllerAssets` creates links before toggling them, so every link's `disabled` state is set explicitly.
|
|
- The client drops the server's removed-with-subtree tags (script, style, svg and so on) along with their subtree. Other unknown tags are unwrapped. This matches the server's behaviour and never turns script text into visible text.
|
|
- A widget or partial field without a `label` has no label row and no `aria-labelledby`.
|
|
|
|
## Deviations from Plan
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Rule 3 - Blocking] Added a shared ExtensionFailure component**
|
|
- **Found during:** Task 1
|
|
- **Issue:** WidgetField, PartialHost and PartialField all need the S5 box. The plan listed no shared component, and copying the markup three times would let the geometry drift.
|
|
- **Fix:** Created `admin/src/components/ui/ExtensionFailure.vue`, which the smoke test imports to satisfy the hygiene rule.
|
|
- **Files modified:** admin/src/components/ui/ExtensionFailure.vue
|
|
- **Committed in:** 107d820
|
|
|
|
**2. [Rule 3 - Blocking] Added a FORM_ASSETS injection key**
|
|
- **Found during:** Task 1
|
|
- **Issue:** WidgetField has to wait for the form controller's scripts, but it knows neither the controller's assets nor the load promise, which FormView only creates after the schema arrives.
|
|
- **Fix:** Added `FORM_ASSETS` next to the three planned keys. FormView provides a getter for its load promise.
|
|
- **Files modified:** admin/src/components/form/formContext.ts, admin/src/views/FormView.vue
|
|
- **Committed in:** 107d820
|
|
|
|
**3. [Rule 3 - Blocking] Added the extension strings to the test bundle fixture**
|
|
- **Found during:** Task 1
|
|
- **Issue:** Smoke tests render with `tests/fixtures/lang.json`, which lacked the new keys, so tests could not assert the Polish copy.
|
|
- **Fix:** Added the four pl `backend::lang.extension.*` entries to the fixture.
|
|
- **Files modified:** admin/tests/fixtures/lang.json
|
|
- **Committed in:** 107d820
|
|
|
|
**4. [Rule 3 - Blocking] Typed the partial fixture with an assertion**
|
|
- **Found during:** Task 2
|
|
- **Issue:** TypeScript widens sibling `attrs` objects in a JSON array with `key?: undefined`, which the generated `{[key]: string}` index signature rejects.
|
|
- **Fix:** `extensionPartialFixture` uses `as` instead of an annotation, with a comment explaining why. The assertion still refuses a fixture that does not overlap with the schema type.
|
|
- **Files modified:** admin/tests/fixtures/typed.ts
|
|
- **Committed in:** 9df9fae
|
|
|
|
**5. [Rule 1 - Bug] Stylesheet activation order**
|
|
- **Found during:** Task 3
|
|
- **Issue:** Calling activateStyles before loadStyles left newly created links without an explicit `disabled` state.
|
|
- **Fix:** loadControllerAssets now creates the links first and then activates them.
|
|
- **Files modified:** admin/src/app/pluginAssets.ts
|
|
- **Committed in:** a5e7dac
|
|
|
|
---
|
|
|
|
**Total deviations:** 5 auto-fixed (4 blocking, 1 bug)
|
|
**Impact on plan:** None changes the D-xx contract. The ListToolbar `actions` and `busyAction` props are optional with defaults, so the Phase 10 ListToolbar tests pass unchanged.
|
|
|
|
## Issues Encountered
|
|
|
|
- happy-dom tries to fetch appended `<script>` and `<link>` elements over the network. The smoke tests stub `document.head.appendChild` for those two element types, which also lets them assert the exact URLs and links.
|
|
|
|
## Known Stubs
|
|
|
|
None.
|
|
|
|
## User Setup Required
|
|
|
|
None. No external service configuration required.
|
|
|
|
## Next Phase Readiness
|
|
|
|
- Plan 10.1-03 can build the application widget (`golem15-fonoteka-discogs-lookup`), the stats header partial using the style kit classes, and the toolbar action against this SPA. Its JS must read `label`/`busy-label` and dispatch `summer-action`.
|
|
- Plan 10.1-04 can unit-test `pluginAssets` (including the per-controller toggle), `partialNodes` (`partialAttrAllowed`, `renderPartialNodes`), WidgetField timeout and failure branches, and PartialHost state transitions directly.
|
|
- Human check still outstanding (backstop): a real browser against `summer serve` should confirm that module scripts load under CSP `script-src 'self'`, that the Vite `/assets` proxy works, and how the pl labels wrap at 768px.
|
|
|
|
## Self-Check: PASSED
|
|
|
|
- FOUND: every key-files.created path
|
|
- FOUND commits: 107d820, 9df9fae, a5e7dac
|
|
- Plan verification: `npm --prefix admin run typecheck`, `npm --prefix admin test` (49 files, 466 tests), `scripts/check-admin-dist.sh`, `scripts/check-phase10.sh --hygiene`, `go test ./modules/phrasebook ./modules/boardwalk`, and the full `go vet ./... && go test ./...`: all pass.
|
|
|
|
---
|
|
*Phase: 10.1-runtime-admin-extension-point*
|
|
*Completed: 2026-09-29*
|