feat: entry editor at /content/:key/new and /content/:key/:id

This commit is contained in:
Florian Sylvain committed 2026-10-21 00:12:34 +02:00
1 parent 91d916fb74
commit fba20f83c5
94 files changed
+7116 -423

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
# QA: Entry editor of a custom content type (SPA slice 7)
Runner: Vitest + Vue Test Utils (`task test:web`). No Go test: the five entry endpoints and /v1/user/lookup already exist and the backend does not change. Kit: reuse `views/__tests__/schemaTestKit.ts` and add GET/POST/PUT/DELETE handlers for `/v1/content-type/{key}/entry[/{id}]` with the plain-text bodies `entry_not_found`, `content_type_not_found`, `invalid_entry: <key>: <rule>`, and the Events type with its 7 fields and entry 42 of SchemaFrame.dc.html. Pin the time zone. Never freeze the system date: "Saved at" is derived from the mocked `updated_at`, so the expected text is built with the same local formatter from that value.
## Automated
1. Registry (lib/fieldTypes spec, it.each over the six types): empty value, default pre-fill, control component, help line, counter rule, validate (required, type, min, max, options), message text, payload value (null for empty, false for a required boolean). The builder and the list cells still pass on the same registry.
2. Entry API (composable spec): getEntry, createEntry, saveEntry, deleteEntry; the body is {data}; error parsing of "invalid_entry: key: rule" with status 422 and 409 into {key, rule, status}; 404 bodies; 400, 401, 403 and NetworkError pass through; lookupUsers is sent once with distinct ids.
3. Route resolution (it.each): "new", "42", "abc", "0" (resolves to id 0, sent as is: the API answers 404 entry_not_found), "4294967296", "-1".
4. State 1 New entry: title, subtitle, chip "New entry", asterisks, Default tag on Online, helps, placeholders, footer "Not saved yet" and "–", Save enabled, Delete absent; mobile layout.
5. Defaults (it.each from the feature table): value shown, tag shown, tag leaves on change and returns on restore; no tag in edit.
6. State 2 Editing: breadcrumb, title, "Entry 42", chip "Saved", Save disabled, footer names and dates after the lookup; "–" with the date kept when the lookup fails or lacks an id; mobile trash-2 action.
7. Page title (it.each): text, null → "Entry 42", richtext first line, number; unchanged while typing.
8. Controls and help lines (it.each): one control per type, placeholders, Off/On text, empty select "None" item on optional select only.
9. Layout rule (pairing of date, number, boolean; full width for text, richtext, select; single column below md).
10. State 3 Unsaved: counter "32 / 80", "91 / 80" in the error colour without blocking, no counter without max, chip, Save enabled, revert returns to clean.
11. State 4 Preview: rendering of bold and list, raw HTML shown as text, unsafe links inert, empty preview, tab switch keeps value and caret, toolbar inserts (it.each).
12. Payloads: create and edit bodies exactly as in the feature, explicit nulls, no unknown key, boolean table.
13. State 5 Saving: chip, button, dimming, disabled controls, no second request on click or Enter.
14. State 6 Saved: edit and create; toast; chip "Saved at HH:MM" from the answer; footer update; create replaces the URL to /content/events/43 with no extra GET; no leave prompt; edit after save is dirty again.
15. State 7 Save failed (500, network, 400, 422 with no field): alert copy, chip, values kept, Try again resends the same body.
16. State 8 Required: no request, all messages, chip "Not saved", `document.activeElement` is the first erroring control in schema order for each type (input, textarea, select trigger, date segment), aria-invalid and aria-describedby, per-type required messages (it.each).
17. State 9 Rules: one message per field, message table (it.each), server answers that pass the client map to their field and take focus.
18. Errors clear per field; chip returns to "Unsaved changes"; no blur validation.
19. State 10 Unique (409): alert and field message, per-type messages, alert clears when the field is edited.
20. State 11 Not found: GET 404, PUT 404, DELETE 404 and bad ids with no request; "Back to Events" goes to /content/events with no prompt.
21. Content type 404, 403 and 401; load failure variants with "Try again" resending only the failed request; loading skeleton with aria-busy.
22. State 12 Schema changed: unknown key, unknown_field, required on an unknown key, 409 on an unknown key; read-only form; Save disabled; Cancel enabled; chip "Not saved"; "Reload form" refetches the type (and the entry) and rebuilds the form; reload failure.
23. State 13 Delete: dialog copy, focus on Cancel, Cancel/Esc/close, no request; Deleting lock; 204 replaces the URL and shows the toast; 404, 403, 401, 500 and network outcomes; delete with unsaved edits.
24. Leave guard (it.each from the feature table), including defaults-only new form untouched, and Cancel history rule (previous entry is the list vs not).
25. Stale answers dropped; navigating between routes carries nothing.
26. Reaching the editor from the list: "New entry" and row links resolve to the editor (update the slice-6 specs that expected the catch-all 404). The slice-6 Background wording about `content_type_id` and `sort` is checked against the Go handler and corrected with the list code and tests if it differs.
27. Accessibility: labels, aria-required, live-region chip, tablist keyboard, toolbar names, dialog focus.
28. French: every new string has an entry (i18n completeness spec passes).
## Visual (task visual:check -- entry)
Register the group "6 Entry editor" in `extraSources` if needed. Every capture within 10 %, desktop and mobile, light and dark. Mock the sidebar's GET /v1/content-type and /v1/user/lookup. No state is clock-sensitive: "Saved at" comes from the response timestamp. If the runner cannot pin the zone, build `updated_at` as the builder's Saved setup does.
| Board state | Setup in web/visual/states.ts | Board sample data |
|---|---|---|
| 1 New entry | /content/events/new, Events type | "New entry", "Not saved yet", Online Off + "Default" |
| 2 Editing | /content/events/42, lookup 3 and 5 | "Lime plaster workshop", "Entry 42", "Inès Duarte · 12 Oct 2026, 09:14", "Marta Alves · 15 Oct 2026, 11:02" |
| 3 Unsaved changes | step: type ", two days" in Title | "Lime plaster workshop, two days", "32 / 80", focus ring on Title |
| 4 Markdown preview | step: click "Preview" on Description | "Two days of hands-on work with lime putty and old stone." and the two bullets |
| 5 Saving | PUT `hang: true` after an edit | "Saving…" |
| 6 Saved | PUT 200 with updated_at 2026-10-19 11:02 local, updated_by 5 | "Saved at 11:02", "Entry saved", "Marta Alves · 19 Oct 2026, 11:02" |
| 7 Save failed | PUT 500 after an edit | "Couldn't save the entry" |
| 8 Required fields empty | /content/events/new, click Save | "Fill in Title.", "Pick a date for Starts on." |
| 9 Rule errors | /content/events/42 with the board's values typed, PUT 422 for any server-side one | "91 / 80", "Capacity must be at most 5000.", "Choose one of: Talk, Workshop or Meetup." |
| 10 Duplicate (409) | /content/events/new, title "Heat pumps: what the numbers say", POST 409 | "Another entry already uses this Title. Enter a different one." |
| 11 Entry not found | /content/events/999, entry 404 entry_not_found | "Entry not found" |
| 12 Schema changed | /content/events/new, POST 422 invalid_entry: room: required | "The fields of Events changed" |
| 13 Delete entry: confirm | /content/events/42, step: click "Delete" | "Delete this entry?" |
Board caveats to report, never hide:
- State 9 draws `31/11/2026` as text in Ends on; a segmented date control cannot hold it, so that field will differ.
- State 11's breadcrumb shows the entry title, unknown on a 404.
- State 12's chip is drawn "Saved" and its Cancel is drawn disabled; the spec uses "Not saved" and an enabled Cancel (user decision).
- Mobile in state 9 depends on the chosen layout rule.
## Manual
- Real backend with two users: create an entry; Back from it returns to the list, not the empty form; edit; delete; the list shows the change.
- Two tabs: change the type's fields in one (add a required field), save the entry in the other: state 12, "Reload form". Delete the entry in one tab and save in the other: state 11.
- Duplicate a unique title for a real 409.
- Keyboard: tab order across fields, toolbar, tabs, footer buttons; Enter in a single-line field saves; focus lands on the first error; dialogs trap focus.
- Markdown with raw HTML and `javascript:` links stays inert in Preview.
- Reload on /content/events/42 keeps the page; the browser back from a saved new entry goes to the page before the new form.