docs(10-04): complete relation manager and shell polish plan

This commit is contained in:
Jakub Zych
2026-09-27 17:29:15 +02:00
parent 445404e394
commit 8259a456bb
4 changed files with 299 additions and 18 deletions

View File

@@ -0,0 +1,276 @@
---
phase: 10-admin-vue-spa
plan: 04
subsystem: admin
tags: [vue, spa, relation-manager, reka-ui, shell, dark-mode, a11y]
requires:
- phase: 10-admin-vue-spa
plan: 03
provides: field registry, DataTable, Pagination, ConfirmDialog/useConfirm, toast queue, typed AdminIDsRequest body, backend::lang bundle
provides:
- relation-manager field renderer (RelationManager) with linked list, debounced search, selection, confirmed unlink and toasts
- RelationPickerModal: candidates five per page, selection across pages, Dodaj (N) link, Esc and focus return
- typed search, sort, dir, page and per_page on the linked and candidate relation routes in the admin OpenAPI
- useSidebar (viewport breakpoint plus persisted manual flag), SectionFlyout, collapse and expand buttons
- Breadcrumbs (plugin, controller, record), UserMenu with role and logout, useAuth().logout()
- applyColorScheme (prefers-color-scheme drives .dark), newest-first toasts, dialog and toast motion
affects: [10-05]
actuals:
tokens: 22100 # chars/4 over added lines in summercms.go; excludes boardwalk/dist, admin.json and schema.d.ts
tasks: 2
commits: 2 # MEASURED: git rev-list --count 6e1b6dd..445404e (summercms.go only; fonoteka.go untouched)
plan_head_before: 6e1b6dd5bf86984b99cc6224669eb3e714b5bb1a
plan_head_after: 445404e394d21c39b855d7a0a672885e49756a00
tech-stack:
added: []
patterns:
- Record-bound field types (relation-manager) are registered like any control but dropped on create and never enter the save body
- Dialog focus return is explicit: closeAutoFocus is prevented and the parent focuses its opener through a function ref
- Shell UI state that must survive reloads lives in one module (useSidebar) and is the only localStorage user
- Smoke tests run against a desktop, light matchMedia by default and spy on it to simulate narrow or dark screens
key-files:
created:
- admin/src/components/relation/RelationManager.vue
- admin/src/components/relation/RelationPickerModal.vue
- admin/src/components/shell/SectionFlyout.vue
- admin/src/components/shell/UserMenu.vue
- admin/src/components/shell/Breadcrumbs.vue
- admin/src/state/useSidebar.ts
- admin/src/state/useBreadcrumbs.ts
- admin/src/app/theme.ts
- admin/tests/fixtures/widgets.relation-schema.json
- admin/tests/fixtures/widgets.relation-linked.json
- admin/tests/fixtures/widgets.relation-candidates.json
- admin/tests/smoke/relation.smoke.test.ts
- admin/tests/smoke/shell.smoke.test.ts
modified:
- cabana/admin_openapi.go
- admin/openapi/admin.json
- admin/src/api/schema.d.ts
- admin/src/api/types.ts
- admin/src/components/form/registry.ts
- admin/src/components/form/FormGrid.vue
- admin/src/components/form/FormField.vue
- admin/src/components/form/FieldRenderer.vue
- admin/src/views/FormView.vue
- admin/src/components/list/DataTable.vue
- admin/src/components/shell/AppShell.vue
- admin/src/components/shell/PluginRail.vue
- admin/src/components/shell/SectionPanel.vue
- admin/src/components/ui/Toast.vue
- admin/src/state/useAuth.ts
- admin/src/state/useToasts.ts
- admin/src/main.ts
- admin/src/styles/main.css
- admin/tests/setup.ts
- admin/tests/fixtures/lang.json
- admin/tests/fixtures/widgets.form-schema.json
- admin/tests/smoke/form.smoke.test.ts
- phrasebook/backend/lang/pl/lang.yaml
- phrasebook/backend/lang/en/lang.yaml
- boardwalk/dist/**
key-decisions:
- "relation-manager is a record-bound type: the form drops it (and its tab) on create even when the YAML has no context: update, and editablePayload never sends it"
- "The admin OpenAPI document declares search, sort, dir, page and per_page on GET {id}/relations/{name} and .../candidates, so the relation manager and picker send typed queries"
- "The picker keeps its selection as a set of ids across pages and search changes and clears it on close; it renders candidates exactly as served (owner exclusion stays server-side, T-10-21)"
- "Focus returns to the link button explicitly: RelationPickerModal prevents Reka's closeAutoFocus and RelationManager focuses the opener through a function ref (a template ref inside v-for is an array)"
- "With the panel collapsed, Enter or ArrowDown on a rail item opens the flyout and moves focus into it instead of navigating; a click still navigates"
- "The expand button shows only for a manual collapse; below 1100px expanding is impossible, so the rail offers the flyout instead"
- "The record crumb comes from the form view through a small useBreadcrumbs module; settings pages show Ustawienia › page label"
- "logout(router) always clears user, navigation and settings and replaces the route with login, even on a failed or thrown call (T-10-23)"
- "The UserMenu root stays mounted when the user clears; only its content reads the user, so logout never tears down a teleport mid-update"
- "Dialog and toast motion is enter-only (200 ms fade plus scale from 0.98), so Reka's Presence never waits on an exit animation"
patterns-established:
- "Components that need the record id get it as FieldControlProps.recordId, passed through FormGrid, FormField and FieldRenderer"
- "Shell tests reset localStorage, spy on window.matchMedia with a controllable fake and auto-unmount wrappers before the body is cleared"
requirements-completed: [] # ADMIN-06 stays open until 10-05 (orchestrator instruction)
coverage:
- id: D1
description: "Relation manager: absent on create (even without context: update), label, comment, view columns, avatars, 56px rows, toolbar in declared order, debounced search, empty message"
requirement: ADMIN-06
verification:
- kind: unit
ref: "admin/tests/smoke/relation.smoke.test.ts#relation manager field"
status: pass
human_judgment: false
- id: D2
description: "Unlink: plural unlinkConfirm, POST unlink with ids, reload, plural unlinked toast; cancel sends nothing"
requirement: ADMIN-06
verification:
- kind: unit
ref: "admin/tests/smoke/relation.smoke.test.ts#confirms with the plural unlinkConfirm"
status: pass
human_judgment: false
- id: D3
description: "Picker: role dialog with aria-modal, search autofocus, listbox aria-multiselectable, five per page, range, selection across pages, Dodaj (N) disabled at 0, link body, Esc and Anuluj close with focus return, selection cleared"
requirement: ADMIN-06
verification:
- kind: unit
ref: "admin/tests/smoke/relation.smoke.test.ts#relation picker modal"
status: pass
human_judgment: false
- id: D4
description: "Server-side candidate scoping still holds through the prefix (T-10-21 regression)"
requirement: ADMIN-06
verification:
- kind: integration
ref: "../fonoteka.go/plugins/golem15/fonoteka#TestCollectionsAdmin (15 tests, incl. RelationSchema, Link, Unlink, CrossScope, RelationEdges)"
status: pass
human_judgment: false
- id: D5
description: "Sidebar: persisted boolean under summer-admin.sidebar, viewport-forced collapse without overwriting it, flyout by keyboard (Enter, ArrowDown), hover and 200 ms mouse-leave, Esc with focus return, no flyout when expanded"
requirement: ADMIN-06
verification:
- kind: unit
ref: "admin/tests/smoke/shell.smoke.test.ts#collapsible section panel, section flyout"
status: pass
human_judgment: false
- id: D6
description: "Header: breadcrumbs on record and list routes, user menu initials, name, role and email, logout POSTs /auth/logout and ends on login on success, 500 and network failure"
requirement: ADMIN-06
verification:
- kind: unit
ref: "admin/tests/smoke/shell.smoke.test.ts#header"
status: pass
human_judgment: false
- id: D7
description: "Dark class follows a mocked prefers-color-scheme and its changes; newest toast first"
requirement: ADMIN-06
verification:
- kind: unit
ref: "admin/tests/smoke/shell.smoke.test.ts#theme and toasts"
status: pass
human_judgment: false
- id: D8
description: "Real link and unlink round trip on a Kolekcje record at /plytadmin, focus return in a browser, visual fidelity at 1280px and about 900px in light and dark mode, limited admin versus superuser menus"
verification: []
human_judgment: true
rationale: "Both task human-checks: focus management, responsive behaviour and visual fidelity are judged in a browser; D-23 excludes browser e2e"
duration: 21min
completed: 2026-09-27
status: complete
---
# Phase 10 Plan 04: Relation manager and shell polish Summary
**An admin editing an existing Collection can now search its editors, add several through a five-per-page picker and remove selected ones, with confirmations and toasts. The shell now follows the design. The section panel collapses to the rail below 1100px or on request, with a keyboard-operable flyout. The header has breadcrumbs and a user menu that shows the role and logs out. Dark mode follows the system.**
## Performance
- **Duration:** 21 min
- **Started:** 2026-09-27T15:06:36Z
- **Completed:** 2026-09-27T15:27:25Z
- **Tasks:** 2
- **Files modified:** 42 in summercms.go (37 excluding rebuilt dist assets); fonoteka.go untouched
## Accomplishments
- **Relation manager (D-05, SC-3).** `relation-manager` is registered in the field registry. The form passes the record id down, and the field renders only on an existing record. On create it is dropped along with its tab, even without `context: update`, and it never enters the save body. `RelationManager` loads `schema/relation/{name}` once. Its header shows the label (17px/700), the YAML `comment` as helper text, a 220×38px search (300 ms debounce, resets the page and the selection), and the view's toolbar buttons in declared order. `link` is the primary `user-plus` button; `unlink` is the danger `user-minus` button, disabled without a selection. The linked list reuses `DataTable` in a new relation variant (56px rows, 30px initials avatar, muted secondary columns) inside a 12px bordered container, with sort, pagination from the meta and the relation `empty` message. Unlink confirms with the plural `unlinkConfirm`, POSTs `{ids}`, clears the selection, reloads and toasts the plural `unlinked`.
- **Picker (design screen 5).** The picker is a Reka Dialog with `aria-modal="true"`, a focus trap and Esc to close. It has the title, helper, 34px close button and autofocused 44px search from the relation messages. The candidates form a `role="listbox"` with `aria-multiselectable="true"`, five per page, as 54px option rows (the selected ones get the sel background and a `#e0b020` border). Space or Enter toggles an option and arrow keys move between options. The pager shows `:from–:to z :total`. The selection is a set kept across pages and searches and cleared on close. `Dodaj (N)` is disabled at zero; confirming POSTs `link`, closes, reloads the list and toasts the plural `linked`. Focus returns to the link button.
- **Sidebar (D-06, D-10).** `useSidebar` combines `matchMedia('(max-width: 1099px)')` with a manual flag persisted as `'true'`/`'false'` under `summer-admin.sidebar`. The viewport forces the collapse but never writes the stored choice. `SectionPanel` has its 30px "Zwiń menu" button, and the rail shows the 40px "Rozwiń menu" above Ustawienia after a manual collapse. While the panel is collapsed, `SectionFlyout` (`role="menu"`, 230px, radius 14, shadow `0 16px 40px rgba(20,27,45,.18)`) opens when a rail item is hovered or focused, or on Enter or ArrowDown, which also moves focus into it. Arrow keys, Home and End move between items. It closes on Esc and returns focus to the rail item, and it closes about 200 ms after the pointer or focus leaves.
- **Header.** `Breadcrumbs` shows plugin › controller › record: parent crumbs are muted links, the current crumb is 600 with `aria-current="page"`, and the separators are 14px chevrons. `UserMenu` (Reka DropdownMenu) has the 34px accent initials avatar, and the name and role are hidden below 1100px. Its 280px menu holds the 40px avatar, the bold name, the muted e-mail and Wyloguj. `useAuth().logout(router)` POSTs `/auth/logout`, then clears the user, navigation and settings and replaces the route with login, whatever the call returned.
- **Theme and motion.** `applyColorScheme()` runs before boot and toggles `.dark` on the document element from `prefers-color-scheme`, following changes (A6: no toggle). `.dark` also sets `color-scheme: dark`. The rail stays dark with its `#222b40` border in dark mode. The toast queue shows the newest toast first. Dialogs scale from 0.98 and fade in over 200 ms, backdrops and toasts fade in, and reduced motion turns all of it off.
## Task Commits
1. **Task 1 (tracer): search, link and unlink related records through the relation manager:** `f4e97cc` (feat)
2. **Task 2: collapsible panel with flyout, user menu with logout, breadcrumbs and dark mode:** `445404e` (feat)
**Plan metadata:** recorded in the docs commit that adds this file.
Tracer gate (Task 1): the whole Task 1 `<verify>` passed on the committed tree before Task 2 started (human_verify_mode end-of-phase, auto mode off): typecheck, the relation smoke tests, TestPhase10SPAKeysResolve, check-admin-dist and the five fonoteka Collections relation suites. The human-check moves to phase verification.
## Decisions Made
See `key-decisions` in the frontmatter. The two with the widest effect: relation-manager is a record-bound type (never on create, never in the save body), and the admin OpenAPI now types the relation list query.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] The relation list routes had no query parameters in the admin OpenAPI**
- **Found during:** Task 1
- **Issue:** `schema.d.ts` typed `GET {id}/relations/{name}` and `.../candidates` with `query?: never`, so openapi-fetch could not send search, page or per_page. The plan requires typed calls, and 10-03 rejected SPA-declared shapes.
- **Fix:** `cabana/admin_openapi.go` declares `search`, `sort`, `dir`, `page` and `per_page` on both routes, matching `relationQuery` in `cabana/http.go`. `admin.json` and `schema.d.ts` were regenerated, `check-admin-openapi.sh --check` passes, and handlers are unchanged. `types.ts` gains `RelationQuery`, `RelationPanel` and `RelationColumn` aliases.
- **Commit:** `f4e97cc`
**2. [Rule 1 - Bug] Registering relation-manager would have put it in the save body**
- **Found during:** Task 1
- **Issue:** `editablePayload` sends every registered type, so registering the relation manager would have sent `members`/`editors` in create and update bodies.
- **Fix:** A record-bound set in the registry: `isRegistered` excludes it, `needsRecord` drops it on create, and `ownsLabel` lets the component render its own heading. The existing edit smoke test still asserts the exact PUT body.
- **Commit:** `f4e97cc`
**3. [Rule 2 - Correctness] Relation manager dropped on create even without `context: update`**
- The design requires the tab to render only on an existing record. The form now filters record-bound types in create mode itself instead of relying on the YAML context. The relation smoke fixture omits the context to prove it.
- **Commit:** `f4e97cc`
**4. [Rule 1] Form smoke test and fixture updated for the real renderer**
- **Issue:** The 10-03 form test asserted the unsupported box on the relation-manager tab.
- **Fix:** The form fixture's relation manager is now the neutral `members` relation (tab Members, with a comment and no context). The test asserts `[data-relation-manager]` and no unsupported box.
- **Commit:** `f4e97cc`
**5. [Rule 1] Test harness teardown with teleported menus**
- **Found during:** Task 2
- **Issue:** Each smoke file clears `document.body` after a test while the app is still mounted. The next test's `beforeEach` (`clearUser()`) then unmounted the UserMenu's DropdownMenu teleport inside the detached body. That caused 80 unhandled rejections and a failing vitest exit, although every assertion passed.
- **Fix:** The UserMenu root no longer depends on the user (the trigger is disabled without one). The shell test also auto-unmounts wrappers before its body cleanup. The full smoke run exits 0 with no unhandled errors.
- **Commit:** `445404e`
**6. [Rule 3] Supporting files not named in the plan**
- `admin/src/state/useBreadcrumbs.ts` carries the record title from FormView to the header.
- `admin/tests/setup.ts` gives every test a desktop, light `matchMedia`, because happy-dom's 1024px default viewport is below the 1100px breakpoint and would collapse the panel in the older tests.
- `FormGrid`, `FormField`, `FieldRenderer` and `api/types.ts` pass `recordId` and carry the relation types.
- **Commits:** `f4e97cc`, `445404e`
**7. [Rule 1] Acceptance grep for the registry**
- The acceptance check expects one `'relation-manager'` literal in `registry.ts`. The type is now the exported `RELATION_MANAGER` constant, and `FormGrid` uses it too. This Task 1 file change landed in the Task 2 commit.
- **Commit:** `445404e`
**8. [Scope note] Lang keys**
- Task 1 added `backend::lang.relation.no_candidates` and `backend::lang.relation.load_failed` (pl and en). Task 2 needed no new keys because `nav.collapse`, `nav.expand`, `nav.user_menu`, `nav.breadcrumbs` and `auth.logout` already existed from 10-01. Only the two new keys were inserted into `tests/fixtures/lang.json`, so its existing plural ordering is kept.
**9. [Process] Not a TDD plan; commits on master**
- The tasks carry no `tdd="true"`. Each task's tests and code were committed together, keeping every commit green. As directed, commits land on `master` (`branching_strategy: none`).
---
**Total deviations:** 7 auto-fixed (2 blocking, 4 bugs, 1 correctness), 2 notes.
**Impact on plan:** None on scope. The OpenAPI change documents existing query parameters only.
## Issues Encountered
- The admin bundle grew from 229 kB to 297 kB (gzip 76 to 97 kB). Reka's DropdownMenu brings its popper (floating-ui), and the plan names DropdownMenu for the user menu. No new package was added and the lockfile is unchanged.
- The known fonoteka.go `parity` failures (`TestMigrateSeedsCanonicalGenres`, `TestSchemaMatchesPHPSnapshot`) and `gofmt -l internal/build/registry.go` remain, as logged in `deferred-items.md`. `go vet ./...` and `go test ./...` pass in summercms.go, and the full `TestCollectionsAdmin` family (15 tests) passes in the fonoteka plugin module.
## Known Stubs
None. WINDOWS entry 4 (relation-manager not registered) is marked fixed by this plan.
## Threat Flags
None. No new endpoint, auth path or storage beyond the plan. The OpenAPI change documents existing query parameters. `localStorage` holds only the sidebar boolean (T-10-22 grep is empty). Candidates are rendered as served, with no client-side filtering (T-10-21). Logout clears local state and leaves the shell even on failure (T-10-23).
## User Setup Required
None.
## Next Phase Readiness
- Plan 10-05 (unit tests) can target `useSidebar.ts`, `theme.ts`, `useBreadcrumbs.ts`, `useAuth.logout`, the registry's `needsRecord`/`isRegistered` and `editablePayload` directly. The smoke tests already cover the component behaviour.
- The human browser checks of both tasks are left for phase verification: the Kolekcje editors round trip, focus return, and fidelity at 1280px and about 900px in light and dark mode for a limited admin and a superuser.
---
*Phase: 10-admin-vue-spa*
*Completed: 2026-09-27*
## Self-Check: PASSED
All 13 created files listed above exist; commits f4e97cc and 445404e (summercms.go) are present. Plan verification re-run: typecheck, all 76 smoke tests (exit 0, no unhandled errors), go test ./phrasebook, check-admin-dist.sh, check-admin-openapi.sh --check, go vet ./... and go test ./... pass; the fonoteka TestCollectionsAdmin family (15 tests) passes.