docs(12.2-04): complete the admin SPA dates, uploads and relation modals plan summary

This commit is contained in:
Jakub Zych
2026-10-02 19:58:19 +02:00
parent 69a37456f6
commit 92d665f1b2

View File

@@ -0,0 +1,277 @@
---
phase: 12.2-admin-form-fields-date-file-upload-relation-editing-with-def
plan: 04
subsystem: admin-spa
tags: [admin, vue, reka-ui, fileupload, datepicker, relation-manager, deferred-binding, session-key]
requires:
- phase: 12.2-02
provides: parent file routes, X-Session-Key, datepicker keys and displayFormat, date/time list columns
- phase: 12.2-03
provides: child CRUD, pivot and child file routes, X-Child-Session-Key, RelationSchema kind/deferrable/manageForm/viewForm/pivotForm, relation-manager field deferrable
provides:
- sessionKey.ts (newSessionKey, SESSION_HEADER, CHILD_SESSION_HEADER) and FORM_SESSION in formContext
- api/files.ts FileRoutes with parentFileRoutes, childFileRoutes and uploadWithProgress (XHR)
- FileuploadField and FileCaptionModal per UI-SPEC section 3
- dateFormat.ts (parseFieldValue, emitFieldValue, formatDisplay, weekStart, dayBounds, boundValue, startValue)
- DatepickerField on Reka DatePicker/TimeField; date and time list cells
- RelationChildModal, RelationPivotModal, pivot link step in RelationPickerModal, toolbar create/delete, row click, create-screen deferral
- DataTable openable rows and trailing cell; useConfirm/ConfirmDialog busy action
- lang keys backend::lang.datepicker.*, fileupload.*, relation.{child_load_failed, child_gone, next, back, pending_note}
- "@internationalized/date 3.12.4 as an exact-pinned direct admin dependency"
affects: [12.2-05 SPA unit tests and backstops]
actuals:
tokens: 44300
tasks: 4
commits: 5
plan_head_before: cb653711f08fb0d2debaf9000b3b18d656be03a7
plan_head_after: 69a37456f6699ee2b8759d1bb5c39599c49d8407
tech-stack:
added: ["@internationalized/date 3.12.4 (direct; was transitive through reka-ui)"]
patterns:
- "A form session (FORM_SESSION) carries the key, the file routes and markDirty; FormView and the child modal each provide one, so fileupload works the same in both"
- "Date values cross the boundary only in dateFormat.ts: strings in the form, @internationalized/date objects in the picker, no global Date for date-only values"
- "The registry looks the relation manager up lazily: child forms put RelationManager on an import cycle with the registry"
- "useConfirm(request, action) keeps the confirm open and busy while the action runs"
key-files:
created:
- admin/src/app/sessionKey.ts
- admin/src/app/dateFormat.ts
- admin/src/api/files.ts
- admin/src/components/form/fields/FileuploadField.vue
- admin/src/components/form/fields/FileCaptionModal.vue
- admin/src/components/form/fields/DatepickerField.vue
- admin/src/components/relation/RelationChildModal.vue
- admin/src/components/relation/RelationPivotModal.vue
- admin/tests/smoke/deferred.smoke.test.ts
- admin/tests/fixtures/deferred.form-schema.json
modified:
- admin/package.json
- admin/package-lock.json
- admin/src/api/client.ts
- admin/src/api/types.ts
- admin/src/components/form/formContext.ts
- admin/src/components/form/registry.ts
- admin/src/components/list/CellValue.vue
- admin/src/components/list/DataTable.vue
- admin/src/components/relation/RelationManager.vue
- admin/src/components/relation/RelationPickerModal.vue
- admin/src/components/ui/ConfirmDialog.vue
- admin/src/components/ui/confirm.ts
- admin/src/views/FormView.vue
- admin/tests/fixtures/typed.ts
- admin/tests/form/registry.test.ts
- admin/tests/relation/RelationManager.test.ts
- admin/tests/relation/RelationPickerModal.test.ts
- modules/phrasebook/backend/lang/en/lang.yaml
- modules/phrasebook/backend/lang/pl/lang.yaml
- modules/boardwalk/dist
- docs/backend/admin-spa.md
key-decisions:
- "12.2-04: the user approved @internationalized/date 3.12.4 as an exact-pinned direct dependency (Task 2 answer: approve); no other dependency version changed"
- "12.2-04: by user decision, existing belongsToMany relation managers stay deferrable and appear on the create screen unless the relation-manager field has context: update; the create-screen deferral UI is not opt-in"
- "12.2-04: a datetime is emitted as YYYY-MM-DDTHH:MM:SSZ in UTC without milliseconds; a stored datetime without a zone designator is read as UTC"
- "12.2-04: the registry resolves RelationManager lazily, because the child modal renders FormGrid and closes an import cycle with the registry"
- "12.2-04: the toolbar delete runs inside the confirm (useConfirm action), so the confirm button shows the busy state and a failure keeps the selection"
- "12.2-04: a preview child modal (viewForm) disables its fieldset; read-only scalar controls without readonly support stay inert"
patterns-established:
- "Relation modals reuse FormGrid, FormTabs and FormErrorBanner with an idPrefix of child-<relation>, pivot-<relation> or link-<relation>"
- "RelationManager computes its owner id: the record id, or 0 on the create screen for a deferrable field inside a form session"
requirements-completed: [SC-1, SC-2, SC-3, SC-4]
coverage:
- id: D1
description: "On a new record an upload goes to record 0 with X-Session-Key, the form turns dirty, the create POST sends the same key and the saved record lists the file; pre-checks refuse wrong type, oversize and files past maxFiles"
requirement: SC-2
verification:
- kind: unit
ref: "admin/tests/smoke/deferred.smoke.test.ts#fileupload on a new record (tracer)"
status: pass
human_judgment: false
- id: D2
description: "In Europe/Warsaw a stored 2026-10-02T10:30:00Z shows 12:30 with a zone segment and saves unchanged; an edited minute goes out as 10:31:00Z; a typed date goes out as 2026-10-02; clear sends null"
requirement: SC-1
verification:
- kind: unit
ref: "admin/tests/smoke/deferred.smoke.test.ts#datepicker values in a fixed time zone"
status: pass
human_judgment: false
- id: D3
description: "A deferrable hasMany manager on the create screen shows the pending note, lists and creates on owner 0 with X-Session-Key, the child save adds its own X-Child-Session-Key, the form turns dirty and the create POST sends the key; a non-deferrable manager stays hidden"
requirement: SC-4
verification:
- kind: unit
ref: "admin/tests/smoke/deferred.smoke.test.ts#relation manager on a new record"
status: pass
human_judgment: false
- id: D4
description: "Toolbar order with one primary, row button opens the child modal in update mode and saves with the child key, 404 closes with child_gone and reloads, delete confirm failure keeps the selection; the pivot link step selects one record, posts {ids, pivot}, Back keeps the selection, 422 stays in the dialog"
requirement: SC-3
verification:
- kind: unit
ref: "admin/tests/relation/RelationManager.test.ts#relation manager child editing|admin/tests/relation/RelationPickerModal.test.ts#pivot link step"
status: pass
human_judgment: false
- id: D5
description: "Calendar keyboard flow, focus, visuals against Direction C; drag reorder and upload progress feel; the end-to-end modal flow across saved and unsaved records"
requirement: SC-1
verification:
- kind: manual
ref: "12.2-04-PLAN.md Task 3 and Task 4 human-check (harvested at end of phase, human_verify_mode end-of-phase)"
status: pending
human_judgment: true
duration: about 2h across two executors (Task 1 by the first, Tasks 2-4 by the continuation, 17:24Z to 17:57Z)
completed: 2026-10-02
status: complete
---
# Phase 12.2 Plan 04: Admin SPA for dates, uploads and related records Summary
**The admin SPA now edits date, datetime and time values with a Reka date picker, uploads files with progress, reorder and captions, and creates, edits, deletes, links and pivot-edits related records in modals. On a record that is not saved yet, uploads and relation work are held against the form's session key and committed by its first save.**
## Performance
- **Duration:** about 2 hours over two executors. Task 1 ran first, then the continuation ran Tasks 2 to 4 from 17:24Z to 17:57Z.
- **Completed:** 2026-10-02
- **Tasks:** 4 (Task 2 was the dependency decision)
- **Files modified:** 31 source, test, lang and docs files, plus the rebuilt `modules/boardwalk/dist`
## Accomplishments
- **Session keys (D-02).** FormView makes one 43-character base64url key per mount and sends it as `X-Session-Key` on the save. A relation child modal makes its own key and sends it as `X-Child-Session-Key`. No key ever goes in a URL.
- **FileuploadField (D-03, D-08 to D-10).** It covers the dropzone, the image tile grid, attachOne rows and file rows. Each upload shows its own state (queued, uploading with progress, failed, done). The SPA pre-checks type, size and maxFiles before sending. Removals wait for the save. Protected thumbnails and downloads use object URLs that are revoked on unmount. Reorder works by pointer and by keyboard, with one debounced request. Captions are edited in FileCaptionModal.
- **DatepickerField (D-18 to D-21).** It is built on Reka DatePicker and TimeField and orders segments by the admin locale.
- A datetime is shown in the browser time zone and emitted as UTC. With `ignoreTimezone` the wall clock is kept unchanged.
- A `date` is emitted as YYYY-MM-DD and a `time` as HH:MM:SS.
- The calendar popover maps `firstDay`, `minDate`/`maxDate` and `yearRange`.
- The field also has a clear button, a read-only text box in `displayFormat`, and an invalid state for partial or out-of-range input.
- It uses no native date input and no global Date.
- **List cells.** `date` and `time` columns show the stored string, with seconds dropped for time.
- **Relation manager (D-11, D-12, D-14, D-17).**
- The toolbar shows create, link, delete and unlink in the declared order, with exactly one primary button. Delete asks for confirmation, and the confirm button shows a busy state while the request runs.
- Clicking a row opens the child modal in update mode. Without update it opens the pivot modal, and without a pivot form it opens the view form read-only. A belongsToMany with a pivot form and update gets a trailing pivot button.
- RelationChildModal (720px) has tabs, an error banner, focus on the first invalid field, skeleton loading, a load-failure alert, and the 404 "gone" flow. Closing it with unsaved changes asks first. Uploads and dates inside it use the child's own key.
- RelationPivotModal edits link details.
- With a pivot form, the picker links one record at a time. It shows a Next step with the pivot form and posts `{ids:[id], pivot}`.
- **Create-screen deferral (D-03).** Deferrable relation managers render on create with the pending note, call every route with owner id 0 and the form key, and mark the form dirty.
- **Language keys.** The datepicker, fileupload and relation keys are in English and Polish. `TestPhase10SPAKeysResolve` passes.
## Task Commits
1. **Task 1 (tracer): fileupload on a new record** - `ea33296` (feat; the earlier executor)
2. **Task 2: approve @internationalized/date@3.12.4** - decision checkpoint. The user answered approve. The install is committed with Task 3.
3. **Task 3: datepicker, dateFormat, date/time cells, datepicker lang** - `a0c1827` (feat)
4. **Task 4: relation child, pivot and create-screen deferral modals** - `69a3745` (feat)
The plan ledger shows 5 commits from `cb65371` to HEAD. Two of them are concurrent phase 13 planning commits made by another session on master: `1ebfe69` and `d66812c`.
## Files Created/Modified
- `admin/src/app/sessionKey.ts`, `admin/src/api/files.ts`, `admin/src/components/form/formContext.ts`: the session key, the file route adapters and the form session
- `admin/src/app/dateFormat.ts`: parse, emit and format of date values, plus week start and bounds
- `admin/src/components/form/fields/{FileuploadField,FileCaptionModal,DatepickerField}.vue`: the new controls
- `admin/src/components/relation/{RelationManager,RelationPickerModal,RelationChildModal,RelationPivotModal}.vue`: relation editing
- `admin/src/components/list/{CellValue,DataTable}.vue`: date and time cells, openable rows and the trailing cell
- `admin/src/components/ui/{confirm.ts,ConfirmDialog.vue}`: a busy confirm action (additive)
- `admin/src/components/form/registry.ts`: `fileupload` and `datepicker` registered, RelationManager resolved lazily
- `admin/src/views/FormView.vue`: the session key, pending changes in `dirty`, and deferrable relation managers kept on create
- `modules/phrasebook/backend/lang/{en,pl}/lang.yaml`, `docs/backend/admin-spa.md`, `modules/boardwalk/dist`
## Decisions Made
See `key-decisions` in the frontmatter. Two of them came from the user. They approved the exact `@internationalized/date` pin. They also decided that existing belongsToMany managers stay deferrable on the create screen unless the field says `context: update`.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] The registry test used `fileupload` as its unsupported-type example**
- **Found during:** Task 1
- **Fix:** It now uses `codeeditor`.
- **Commit:** ea33296
**2. [Rule 2 - Critical] A lost session during an upload was not reported**
- **Found during:** Task 1
- **Fix:** `admin/src/api/client.ts` gains `reportUnauthorized()`. If the upload XMLHttpRequest still gets 401 after its one refresh, it reports the lost session through the typed client's handler.
- **Commit:** ea33296
**3. [Rule 3 - Blocking] An import cycle in FileCaptionModal**
- **Found during:** Task 1
- **Fix:** FileCaptionModal has its own small parser for 422 details. Importing `formState.fieldErrors` would have closed the cycle registry, field, formState, registry.
- **Commit:** ea33296
**4. [Fixture] Typed deferred fixture**
- **Found during:** Task 1
- **Fix:** `admin/tests/fixtures/typed.ts` gains `deferredFormSchemaFixture`, which type-checks the fixture against the generated FormView.
- **Commit:** ea33296
**5. [No change needed] formState.ts**
- **Found during:** Task 1
- **Note:** The plan listed formState.ts, but it needed no change. `fileupload` is registered as valueless, and `editablePayload` already leaves valueless types out.
**6. [Rule 3 - Blocking] RelationManager was registered as UnsupportedField**
- **Found during:** Task 4
- **Issue:** The child modal renders FormGrid and uses formState. That puts RelationManager on an import cycle with the registry. When RelationManager loaded first, the registry's map was built while RelationManager was still undefined, so `rendererFor('relation-manager')` returned UnsupportedField.
- **Fix:** The registry now looks RelationManager up lazily, at render time.
- **Commit:** 69a3745
**7. [Rule 2 - Missing] DataTable could not open rows or hold a trailing cell**
- **Found during:** Task 4
- **Issue:** UI-SPEC row click needs a first-cell button and a 56px trailing pivot cell. DataTable had neither.
- **Fix:** DataTable gains the additive props `openable` and `trailing`, an `open` emit and a `trailing` slot. The list views are unchanged.
- **Commit:** 69a3745
**8. [Rule 2 - Missing] The delete confirm could not show progress**
- **Found during:** Task 4
- **Issue:** UI-SPEC says the delete confirm button shows a busy state while the request runs. The shared confirm closed as soon as it was clicked.
- **Fix:** `useConfirm().ask(request, action)` now keeps the dialog open and busy until the action settles. ConfirmDialog gains a `busy` prop. Existing callers are unchanged.
- **Commit:** 69a3745
**9. [Rule 2 - Missing] The picker did not send the session key**
- **Found during:** Task 4
- **Issue:** On owner id 0 the candidate and link calls need `X-Session-Key`.
- **Fix:** RelationPickerModal gains a `sessionKey` prop.
- **Commit:** 69a3745
**10. [Tests] New relation tests were added**
- **Found during:** Task 4
- **Note:** The RelationManager and RelationPickerModal test files gain tests for the new toolbar, row click, delete and pivot step. No existing expectation changed, and all existing tests pass unchanged.
- **Commit:** 69a3745
### Issues Encountered
- Task 1's commit `ea33296` also contains `.planning/phases/13-.../13-VALIDATION.md`. The concurrent phase 13 session had staged that file in the shared index. The commit was left as is (master is shared; no history rewrite). The file belongs to phase 13.
- Running `npx --prefix admin vitest` from the repo root made a stray `node_modules/.vite` cache at the root. It was removed and never committed.
## Known Limitations
- In a preview child modal (view form) the form fieldset is disabled. Download buttons for protected files are therefore disabled in preview. Public files keep working links.
- DatepickerField detects partly filled segments on keyup, focusout and value changes, by reading Reka's `data-placeholder` markers.
## Known Stubs
None.
## Human Checks (end of phase)
- **Task 3:** Check datepicker keyboard entry, the calendar keyboard flow (Enter, arrow keys, PageUp/PageDown, Esc returning focus to the trigger), that disabled days cannot be picked, the zone segment, and the visuals against Direction C.
- **Task 4:** Run the create-screen flow: children with an uploaded image, a link with pivot details, attachMany drag reorder, then Save. Then edit a child, edit pivot details and delete a child on the saved record. Upload progress and the modals should match UI-SPEC.
## Threat Flags
None. Every new request uses the routes and headers in the plan's threat model. The session keys travel only in headers, there are no raw-HTML sinks, and the only new hex value is the inherited `#e0b020`.
## Next Phase Readiness
Plan 05 can write unit tests and backstops for:
- `dateFormat.ts`
- DatepickerField: Esc focus and disabled days
- FileuploadField: protected thumbnails, keyboard reorder
- RelationChildModal: tabs, 422 focus and the dirty close
- RelationPivotModal
- the busy confirm
- the lazy registry lookup
## Self-Check: PASSED
- Created files exist: dateFormat.ts, DatepickerField.vue, RelationChildModal.vue, RelationPivotModal.vue, sessionKey.ts, files.ts, FileuploadField.vue, FileCaptionModal.vue.
- Commits ea33296, a0c1827 and 69a3745 are on master.
- These checks pass: `npm --prefix admin run typecheck`, the vitest suite (54 files, 695 tests), `TestPhase10SPAKeysResolve`, `scripts/check-admin-dist.sh`, `go test ./modules/boardwalk`, `go vet ./...`, `go test -short ./...` and `TestDocsTree`.