feat: edit an existing custom type

This commit is contained in:
Florian Sylvain committed 2026-10-20 04:42:50 +02:00
1 parent 083cc9e23e
commit 8e344b2500
57 files changed
+5648 -425

No files matched your search

+450
View File
@@ -0,0 +1,450 @@
Feature: Edit a custom content type in the Schema builder
As an owner or admin
I want to change the name and fields of a saved custom type
So that the type follows the content it holds
Background:
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
And API errors are plain text
And the builder, FieldList, FieldInspector, ChoiceList, ApiPreview, status chip and leave guard of features/schema_builder_create.feature are reused unchanged unless a scenario below says otherwise
And PUT /v1/content-type/{key} takes {name, icon, fields[{key, label, type, required, unique, default, min, max, options}]} and answers 200 with the content type
And PUT answers 400 (name or icon), 404 content_type_not_found, 409 schema_incompatible, 422 invalid_field, 403 or 500
And the number of entries comes from GET /v1/content-type/{key}/entry?page=1&size=5, field "total"
And the route /schema/:key keeps meta auth "verified" and title "Schema"
And the content types store is the source of the loaded type; no GET /v1/content-type/{key} is made
And only owners and admins can use Schema; the SPA decides from the session role, the API still enforces it
And every string goes through t() and has a French entry in i18n/messages.ts
And a copy tagged "(proposed copy)" is not in the design and is the coder's single point of change
Scenario Outline: Resolving the key of /schema/:key
Given the custom list holds events, recipes, press-mentions and the built-in list holds articles, authors, sites, media, users
When an owner opens /schema/<key>
Then <result>
Examples:
| key | result |
| events | the builder in edit mode |
| articles | the read-only built-in view, unchanged from schema_builtin_view.feature |
| unknown | "Content type not found" |
| new | the create builder, unchanged |
Scenario: Reaching the editor from the list
Given the Schema list is Populated
Then each custom row is a link to /schema/<key> on the table and on the mobile cards
And built-in rows still link to /schema/<key>
When the user activates the row "Events"
Then the router navigates to /schema/events and the builder opens in edit mode
Scenario: Loading, the session store and the entry count
Given an owner opens /schema/events as the first page of the session
Then GET /v1/content-type is called once and GET /v1/builtin-type once (the resolver needs both)
And GET /v1/content-type/events/entry?page=1&size=5 is called once
And the Schema skeleton is shown until the type, the built-in list and the count are known
And no GET /v1/content-type/events is made
Scenario Outline: The count or the list cannot be loaded
Given <failure>
Then the page shows the error "Couldn't load content type" (proposed copy) with the line "<line>" and a "Try again" action
And no builder, no Save is shown
When the user activates "Try again"
Then the failed request is sent again and the builder opens when it succeeds
Examples:
| failure | line |
| the entry count answers 500 or fails (network) | GET /v1/content-type/events/entry failed. Check your connection and try again. (proposed copy) |
Scenario: The entry count answers 404
Given the entry count answers 404 content_type_not_found
Then the page shows "Content type not found"
Scenario Outline: Roles and sessions in edit mode
Given <who> opens /schema/events
Then <result>
Examples:
| who | result |
| a verified owner or admin | the builder in edit mode |
| a verified user who is neither | "You don't have access to Schema" with "Go to Home", URL unchanged |
| an unverified user | the redirect to /register/pending |
| a signed-out visitor | the redirect to /login |
Scenario: Session-owned store
Given an owner has loaded the custom and built-in lists
When the user signs out
Then the content types store is reset
And the next signed-in user's first Schema page loads both lists again
And an in-progress edit is not carried to another session or to a second visit of /schema/events
Scenario: Editing an existing type, desktop (design state 2)
Given /schema/events is Events with 7 saved fields and 37 entries, on a 1440 px viewport
Then the breadcrumb is "Schema" (a link) then "Events"
And "Name" holds "Events" and is editable
And "Key" holds "events" in mono, read-only, with a lock icon, no pencil, no Auto badge and no "Reset to match the name"
And the chip reads "Saved"
And the info bar reads "37 entries use this content type. Changes are checked against them when you save." with an info icon, under the type bar and above the list
And the list header reads "Fields" and "7 of 50" with the 7 rows in saved order, none carrying a badge "New" or "Changed"
And "Add field" is shown under the list
And the first row is selected and the inspector title is "Field: Title" with no "New" badge
And "Save" and "Discard" are disabled
And the API preview bar is collapsed
Scenario: Editing an existing type, mobile (design state 2)
Given the same page on a 390 px viewport
Then the topbar shows an arrow-left, the title "Events"
And Name and Key are stacked, then the info bar, then the rows, then the "API preview" row
And no inspector is shown until a row is activated
And the bottom bar holds the chip "Saved", Discard and Save
Scenario Outline: The info bar follows the count
Given the entry count answers <total>
Then <bar>
Examples:
| total | bar |
| 0 | no info bar is shown |
| 1 | "1 entry uses this content type. Changes are checked against it when you save." (proposed copy) |
| 37 | "37 entries use this content type. Changes are checked against them when you save." |
| 1200 | "1,200 entries use this content type. Changes are checked against them when you save." |
Scenario: The locked type key (design state 3)
Given /schema/events
When the user hovers or focuses the locked key
Then a tooltip reads "The key is used in URLs and the API, so it can't change after the type is created."
And the tooltip closes on blur, mouse out and Esc
And the key input cannot be typed in and no key error can appear
Scenario: Saved fields are locked
Given the saved field Starts on (date) is selected
Then "Key" holds "starts_on" with a lock icon, no Auto badge, read-only, with the help "Used in the API, so it can't change."
And "Type" shows "Can't change after saving" with a lock icon
And the six tiles are shown, the date tile selected and the other five at reduced emphasis and not selectable
And label, Required, Unique values, Default value, limits and choices stay editable
Scenario: Fields added since the last save are not locked
Given the user activates "Add field" on /schema/events
Then the new row is selected with the badge "New", an Auto key, six selectable tiles and no lock
And the key and type behave as in the create builder
And a label typed for it derives its key without colliding with any saved key
Scenario Outline: What marks a saved field as changed
Given the saved field <field> is selected
When the user <edit>
Then its row and its inspector title show the badge "Changed"
When the user restores the original value
Then the badge goes
Examples:
| field | edit |
| Capacity | sets Highest value to 2000 |
| Title | renames the label to "Name of event" |
| Starts on| turns Required off |
| Category | adds the choice "Panel" |
| Online | turns the default on |
Scenario: Reordering is not a change of a field (proposed)
Given the saved fields in saved order
When the user moves Ends on above Starts on
Then no row shows "Changed"
And the chip reads "Unsaved changes" and Save is enabled
Scenario: Unsaved changes (design state 4)
Given Events with 37 entries
When the user sets Capacity's Highest value to 2000, adds Speaker (Text) and Room (Choice, required, default "Main hall", choices "Main hall" and "Studio"), and removes Description
Then the chip reads "Unsaved changes" and Save and Discard are enabled
And Capacity shows "Changed", Speaker and Room show "New"
And an undo row after the list reads "Description will be removed" with the action "Undo", with a minus icon and a dashed border
And the header reads "8 of 50"
And the info bar stays
Scenario: Removing a saved field and undoing it
Given Events with the saved field Description at position 7
When the user activates Remove on Description
Then it leaves the list and an undo row "Description will be removed" appears
And the next row (or the previous one at the end) is selected
When the user activates "Undo"
Then Description returns at position 7 with its original values and no badge
And the undo row goes, and with no other edit the chip returns to "Saved" and Save is disabled
Scenario: Several removals
Given the user removed Description and Ends on
Then two undo rows are shown, one per field, in the order of the removals (proposed)
And each Undo restores its own field at its original position
Scenario: A removed key stays reserved (proposed)
Given the user removed the saved field Description
When the user adds a field labelled "Description"
Then its Auto key is "description_2"
And "Undo" on the removed field still restores "description" without a duplicate key
Scenario: Undo at the limit of 50 fields (proposed)
Given 50 fields are in the list and one saved field is in an undo row
Then "Undo" is disabled until a field is removed
Scenario: Removing every field
Given the user removed all saved fields
Then the list shows "No fields yet" with "A content type needs at least one field." in the error colour
And undo rows are shown for the removed fields
And the chip counts one problem and Save is disabled
Scenario: Name edit only
Given Events
When the user changes the name to "Events and talks"
Then the Key stays "events"
And the chip reads "Unsaved changes"
And the sidebar and the list keep "Events" until the PUT answers
Scenario Outline: Type-level rules in edit mode
Given Events
When the user sets Name to <value>
Then <message> is shown under Name with an alert icon
And Save is disabled and no request is sent
Examples:
| value | message |
| (empty or spaces) | Give the content type a name. |
Scenario: Field errors (design state 5)
Given Events
When the user empties the label of Ends on, sets Capacity's Lowest value to 6000, and gives Category the choices Talk, Workshop, a blank and Talk
Then the chip reads "3 problems to fix" and Save is disabled
And the three rows are outlined with an alert icon
And the messages "Give the field a label.", "Lowest value can't be above the highest value.", "Choices can't be blank." and "Two choices are the same." are shown under their inputs
And errors follow the timing and the counting rules of the create builder
Scenario: A single field key error on a new field (design state 7)
Given Events and a field added since the last save with the key "start-date"
Then the Key shows "Start with a letter. Use lowercase letters, digits and underscores." with the alert icon and the chip reads "1 problem to fix"
And a saved field never shows a key error
Scenario: The choice list of a saved Choice field (design state 8)
Given the saved field Category with the choices Talk, Workshop, Meetup is selected
Then every control of the create builder's choice list works, including add, remove, reorder and the 50 cap
And the help keeps "Removing a choice that entries use will be refused when you save."
And the default select lists the choices and is cleared when its choice is removed or renamed
Scenario: Keyboard reorder in edit mode (design state 9)
Given Events with 7 saved fields and Ends on at position 3
When the user focuses the grip "Move Ends on", presses Space then Up
Then the polite live region reads "Moving Ends on. Position 2 of 7. Press Up or Down to move, Space to drop, Esc to cancel."
And Esc restores the order and Space drops it
Scenario: API preview in edit mode (design state 10)
Given Events
When the user opens "API preview"
Then the routes use the locked key "events"
And the sample entry follows the current fields, so added fields appear and removed ones disappear as the user edits
And the note "Updates as you edit. Fields you add or remove here appear after you save." is shown
And Copy JSON, the "Copied" label and the mobile sheet behave as in the create builder
Scenario: Saving a type that has no entries
Given Recipes with 4 saved fields and 0 entries
When the user changes the label of Servings and activates Save
Then PUT /v1/content-type/recipes is called once with no review dialog
And the body has exactly name, icon and fields
Scenario: The PUT body
Given Events with the loaded icon "calendar" (or null)
When the user saves
Then the body is {name, icon, fields} with name trimmed
And "icon" equals the loaded icon, so an edit never changes it (proposed, see open question)
And no "key" is sent in the body
And the fields are in list order, undo-row fields are not sent, and each field carries exactly key, label, type, required, unique, default, min, max, options
And unset default, min and max are null, options are [] for a non-select field, a date bound is "YYYY-MM-DD", the boolean default is true or null
And the key and type of every saved field equal the loaded ones
Scenario: Saving (design state 11, no entries)
Given the PUT has not answered
Then the chip reads "Saving…" with a loader, Save shows a loader and is disabled, Discard is disabled
And Name, the inspector, Add field, grips, Move, Remove and Undo are dimmed and disabled
And a second activation of Save sends no second request
Scenario: Saved (design state 12)
Given the PUT answers 200 with Events and 8 fields
Then a success toast "Content type saved" with "Events now has 8 fields." is shown
And the user stays on /schema/events
And every badge "New" and "Changed" and every undo row clear
And the added fields become saved fields: their key and type are now locked
And the chip reads "Saved at" followed by the time (HH:MM) of the response's updated_at, Save and Discard are disabled
And the content types store holds the answered type in place, so the list, the sidebar Content group and the field count show the new name and count with no second GET /v1/content-type
And with one field the toast reads "Events now has 1 field." (proposed copy)
And no "Leave without saving?" prompt is shown afterwards
Scenario: Edit after saving
Given the form was just saved
When the user edits the name
Then the chip reads "Unsaved changes" again
And Discard returns to the last saved state, not to the state at page load
Scenario: Discard
Given the user edited the name, a field and removed a field
When the user activates Discard
Then the form returns to the loaded type, with the first row selected, no badge, no undo row, no error and the chip "Saved"
And nothing is sent and the URL stays /schema/events
Scenario Outline: Leaving with unsaved edits
Given <state>
When the user leaves through the breadcrumb, the mobile arrow, the sidebar or the browser back
Then <result>
Examples:
| state | result |
| no edit | the router navigates with no prompt |
| an edit, even one only in an undo row | the dialog "Leave without saving?" with "Keep editing" and "Discard changes" |
| after Save answered 200 | the router navigates with no prompt |
| after Discard | the router navigates with no prompt |
Scenario: Save on a type with entries opens the review (design group 3, state 1)
Given Events with 37 entries, the edits of the "Unsaved changes" scenario and no error
When the user activates Save
Then no PUT is sent yet
And the dialog "Review changes to Events" with "Apply them to save the content type." opens (a bottom sheet on mobile)
And the summary reads "Add 2 fields, change 1, remove 1" and "Events has 37 entries. Each one is checked against the new fields when you apply."
And the changes are listed in this order: additions, changes, removals, each in list order
And the footer note reads "Flagged changes can be refused if existing entries don't fit. If that happens, nothing is changed."
And "Back to editing" and "Apply changes" are shown
Scenario Outline: Review rows
Given Events with entries and the edit <edit>
Then the review shows the row "<title>" with the text "<text>" and <flag>
Examples:
| edit | title | text | flag |
| adds Speaker, Text, optional | Add Speaker | Text, optional. | no flag |
| adds Room, Choice, required, default "Main hall" | Add Room | Choice, required, default "Main hall". Existing entries get the default, so they still fit. | no flag |
| adds Seats, Number, required, no default | Add Seats | Number, required. Existing entries have no value for it. (proposed copy) | "Entries may not fit" |
| sets Capacity's Highest value from 5000 to 2000 | Change Capacity | Highest value goes from 5000 to 2000. | "Entries may not fit" |
| raises Capacity's Lowest value from 1 to 10 (proposed copy) | Change Capacity | Lowest value goes from 1 to 10. | "Entries may not fit" |
| sets a first Longest on Title (80) where none was set (proposed copy) | Change Title | Longest is now 80. | "Entries may not fit" |
| turns Required on for Ends on (proposed copy) | Change Ends on | Now required. | "Entries may not fit" |
| turns Unique values on for Category (proposed copy) | Change Category | Values must now be unique. | "Entries may not fit" |
| removes the choice Meetup from Category (proposed copy) | Change Category | Choice removed: "Meetup". | "Entries may not fit" |
| loosens Capacity's Highest value from 5000 to 8000 (proposed copy) | Change Capacity | Highest value goes from 5000 to 8000. | no flag |
| renames the label of Title to "Name" (proposed copy) | Change Title | Label goes from "Title" to "Name". | no flag |
| adds the choice "Panel" to Category (proposed copy) | Change Category | Choice added: "Panel". | no flag |
| removes Description | Remove Description | Entries stop returning its values. | "Removes data" |
Scenario Outline: The summary line
Given Events with entries and <changes>
Then the summary reads "<summary>"
Examples:
| changes | summary |
| 2 added, 1 changed, 1 removed | Add 2 fields, change 1, remove 1 |
| 1 added | Add 1 field |
| 1 changed, 2 removed | Change 1, remove 2 |
| 1 changed | Change 1 |
Scenario Outline: When the review opens
Given Events with 37 entries
When the user makes <edit> and activates Save
Then <result>
Examples:
| edit | result |
| only a name change or only a reorder | the PUT is sent at once, with no review (proposed) |
| a field added, changed or removed | the review opens |
| a name change and a changed field | the review opens and lists only the field change (proposed) |
Scenario: Back to editing from the review
Given the review is open
When the user chooses "Back to editing" (or presses Esc or the close icon)
Then the dialog closes, no PUT is sent and every edit is kept
Scenario: Applying (group 3, state 2)
Given the review is open
When the user activates "Apply changes"
Then PUT /v1/content-type/events is sent once
And the confirm button reads "Applying…" with a loader and the dialog's other actions are disabled
And Esc, the close icon and a click on the overlay do not dismiss the dialog
And a second activation sends nothing
Scenario: Applied
Given the PUT answers 200
Then the dialog closes, the toast, the cleared badges, the locks and the chip "Saved at" are as in "Saved (design state 12)"
And the info bar and the count are unchanged
Scenario: Refused (design state 15 and group 3, state 3)
Given the user applied the edits "Capacity's Highest value goes from 5000 to 2000" plus "Description removed"
When the PUT answers 409 with the body "schema_incompatible"
Then the review is replaced by the dialog "Nothing was changed" with "RenewCMS refused the changes because some entries don't fit the new fields. It doesn't say which entries."
And an error alert reads "Your content type is as it was" with "No field was added, changed or removed."
And the section "The change that can cause this in your edit" lists only the flagged changes, here Change Capacity with "Highest value goes from 5000 to 2000. An entry may have a higher value." and the badge "Possible cause"
And Removes-data rows are not listed as a cause
And the explanation reads "This kind of change is refused when an entry is over the new limit, a new required field has no default, a field becomes unique while entries share a value, or a removed choice is still in use."
And "Discard changes" and "Back to editing" are shown
And no entry, id or value is ever named
And every edit is still in the form and the store still holds the type as loaded
Scenario: Refused with no flagged change (proposed)
Given the 409 comes back for edits that have no flagged change
Then the dialog shows "Nothing was changed", the "Your content type is as it was" alert and the explanation
And the section "The change that can cause this in your edit" is omitted
Scenario: Back to editing after a refusal (design state 15)
Given the refusal dialog is open
When the user chooses "Back to editing"
Then the dialog closes and the page shows the error alert "Nothing was changed" with "Some entries don't fit the new fields, so the whole change was refused. RenewCMS can't tell which entries. The changes that can cause this are marked." and the action "Review changes"
And the rows of the flagged changes show the badge "Possible cause" next to "Changed" or "New"
And the chip reads "Unsaved changes" and Save stays enabled
When the user activates "Review changes"
Then the review dialog (group 3, state 1) opens again, with the same rows
Scenario: Discard changes after a refusal
Given the refusal dialog is open
When the user chooses "Discard changes"
Then the dialog closes and the form returns to the loaded type, with no banner, no badge and the chip "Saved"
Scenario: The refusal markers clear
Given the banner "Nothing was changed" is shown
When a PUT succeeds, or the user activates Discard
Then the banner and every "Possible cause" badge go
And when the user undoes or reverts a flagged change, that row loses its "Possible cause" badge
Scenario Outline: Other answers of the PUT
Given the PUT answers <status> with <body>
Then <outcome>
Examples:
| status | body | outcome |
| 422 | invalid_field | the review dialog closes and the banner "Check the fields marked in the list." (proposed copy) is shown, edits kept |
| 400 | Bad request body | the review closes and "Give the content type a name." is shown under Name, edits kept (proposed copy) |
| 404 | content_type_not_found | the page "Content type not found" and the store drops the type (proposed copy) |
| 403 | Only ... | the page "You don't have access to Schema" |
| 401 | (any) | the existing redirect to /login with the expired reason |
Scenario Outline: Save failed (design state 13)
Given the PUT <failure>
Then the review dialog (if open) closes
And an error alert titled "Couldn't save the content type" shows "Something went wrong on our side. Your changes are still here. Try again in a moment."
And it has a "Try again" action and the chip reads "Save failed"
And every edit is kept and Save stays enabled
When the user activates "Try again"
Then the same body is sent again, through the review again when the type has entries
Examples:
| failure |
| answers 500 |
| fails (network) |
Scenario: Offline (design state 16)
Given the user edited Events and navigator.onLine becomes false
Then the banner "You're offline. Changes will be saved when you reconnect." with a wifi-off icon is shown
And the chip reads "Waiting for connection", Save is disabled and every edit is kept
When the connection returns
Then the banner goes, Save is enabled again and nothing is sent by itself
Scenario: Offline while the review is open
Given the review dialog is open and navigator.onLine becomes false
Then "Apply changes" is disabled and the edits are kept
Scenario: Mobile field sheet in edit mode
Given a 390 px viewport
When the user activates the row Capacity
Then the sheet "Field: Capacity" opens with the inspector, the locked key, Move up, Move down, Remove and "Done"
And Remove inside the sheet closes the sheet and shows the undo row
Scenario: Accessibility of the edit mode
Then the lock icons have a text alternative or are tied to the field's help text
And dialogs trap focus, return it to the opener on close, and the Applying dialog keeps focus on its title
And the undo row's "Undo" is a button reachable by keyboard
And controls are 44 px high on mobile and focus is a 2 px ring in the primary colour
+103
View File
@@ -0,0 +1,103 @@
# QA: Schema builder, edit (SPA slice 4; delete is slice 5)
Runner: Vitest + Vue Test Utils (`task test:web`). No Go test: PUT /v1/content-type/{key} and the entry list are already covered by features/custom_content_types.qa.md and nothing changes in the backend.
Same pattern as features/schema_builder_create.qa.md: mock `/v1` through the api composable (extend `views/__tests__/schemaTestKit.ts` with PUT and the entry count handlers, and a full "Events" type with the board's 7 saved fields), mount with the real router and a resolved session whose role varies, `components/__tests__/viewport.ts` for 1440 and 390, fake timers where a delay exists. Never freeze the system date. "Saved at HH:MM" is asserted with /^Saved at \d{2}:\d{2}$/ only.
## Automated
Fixtures: Events (id 1, 7 saved fields as drawn on the board: Title text required unique max 80, Starts on date required, Ends on date, Online boolean default false, Capacity number min 1 max 5000, Category select Talk/Workshop/Meetup, Description richtext) with icon "calendar"; Recipes (4 saved fields, 0 entries); the entry-count body {items:[],total:37,page:1,size:1}; plain-text bodies schema_incompatible, content_type_has_entries, content_type_not_found, invalid_field.
Pure logic (lib/__tests__, it.each over the feature's Examples):
1. Form from a ContentType: API field to BuilderField (number default to string, boolean default to defaultOn, options, ISO dates), saved flag, locked keyMode, and the round trip: loaded form to payload equals the loaded fields (no spurious diff).
2. Change detection per saved field: Changed on and off by restoring the value; reorder-only is not a field change; New for added fields.
3. Diff for the review: adds, changes, removes in that order, the summary line (singular, zero parts omitted), the row texts of the "Review rows" table, the flags ("Entries may not fit", "Removes data", none), and the flagged-only list used by the refusal. Name-only or reorder-only means no review.
4. Payload for PUT: {name, icon, fields}, no key, icon equal to the loaded one (null stays null), undo-row fields excluded, saved key and type unchanged.
5. Field key derivation against saved keys and keys in undo rows (`description_2`).
6. Undo bookkeeping: positions, several removals, limit of 50.
7. Info-bar copy for 0, 1, 37 and 1200 entries.
Store (stores/__tests__/contentTypes.spec.ts):
8. replace(type) swaps by key and keeps order by name when the name changed, does nothing when "idle", never touches built-in items, no request. reset() clears both slices (signed-out case).
Composable and API (composables/__tests__/useContentTypes.spec.ts):
9. updateContentType PUTs JSON to /v1/content-type/{key} and returns the type. countEntries reads `total` from the size=5 request. ApiError carries status and plain-text body for 400, 404, 409, 422, 403, 500. NetworkError passes through.
Resolver and routing:
10. Key resolver (built-in, custom, none, loading) for events, articles, unknown, new. /schema/new is still the create builder, GET /v1/builtin-type result is awaited for /schema/:key. Custom rows of SchemaTable and the mobile cards are links to /schema/<key>.
11. Roles: owner, admin, neither (403 page), unverified and signed-out redirects (extend routerGuard.spec) on /schema/events.
12. Session reset: sign-out resets the store and discards any edit state; a second visit starts from the store, not from an old draft.
Builder view in edit mode (new views/__tests__/SchemaBuilderEdit*.spec.ts), one test per scenario:
13. Loading: calls list (assert once each), skeleton until type, list and count are known, no GET /v1/content-type/events.
14. Count or list failure: error copy, Try again re-sends, builder shown after success. Count 404 gives "Content type not found".
15. State 2 desktop and mobile: breadcrumb, Name editable, locked Key without pencil/Auto/Reset, chip "Saved", info bar, "7 of 50", no badges, first row selected, Save and Discard disabled, collapsed API bar. Mobile topbar, stack order, bottom bar.
16. Info bar for 0, 1, 37, 1200.
17. Locked key tooltip on hover and on focus, closes on blur and Esc, input not editable.
18. Saved fields locked: key lock and help, "Can't change after saving", tiles not selectable, other controls editable. New fields unlocked.
19. Changed badge table (5 rows) and revert; reorder-only is no Changed badge but dirty.
20. State 4 end to end: badges, undo row text and action, header count, chip.
21. Remove and Undo: position restored, no residue, chip back to "Saved". Several removals, reserved key, Undo disabled at 50, empty list error.
22. Name-only edit keeps the key; empty name error and disabled Save.
23. Field errors (state 5) and single field key error on a new field; a saved field never shows a key error.
24. Choice list on a saved Choice field (state 8) and keyboard reorder (state 9) with the live-region texts.
25. API preview in edit mode (state 10): locked key in the routes, live sample, note text.
26. PUT without entries: one request, no dialog, exact body (icon preserved, no key), Saving state (state 11) with dimmed/disabled controls and no second request on double activation.
27. Saved (state 12): toast copy incl. singular, stays on the page, badges and undo rows cleared, newly saved fields now locked, chip /^Saved at \d{2}:\d{2}$/, store.replace, list/sidebar/count updated with no extra GET, no leave prompt, edit after save and Discard back to last saved.
28. Discard: back to the loaded form, nothing sent.
29. Leave guard (5 rows) with Keep editing, dismiss and Discard changes.
30. Review (group 3, state 1): opens only for adds/changes/removes with total > 0; no PUT yet; summary, rows (the table), order, footer note, Back to editing, Esc, mobile sheet.
31. Applying (state 2): one PUT, loader, non-dismissable (Esc, overlay, close icon), second activation ignored.
32. Applied: dialog closes, same results as item 27.
33. Refused: 409 schema_incompatible: refusal dialog copy, only flagged changes listed with "Possible cause", no entry named, store untouched, edits kept. Without flagged change: section omitted. Back to editing: banner "Nothing was changed", "Review changes" reopens the review, row markers. Discard changes resets. Markers clear on success, Discard or revert.
34. Other PUT answers: 422 banner, 400 name message, 404 not-found page and store drop, 403 page, 401 redirect.
35. Save failed (state 13): 500 and network, review closes, banner and chip, edits kept, Try again resends the same body through the review again.
36. Offline (state 16) and offline with the review open.
37. Mobile field sheet in edit mode.
40. Accessibility: focus return from dialogs, Applying focus on its title, Undo reachable by keyboard.
Existing tests to update:
41. features/schema_builtin_view.qa.md and SchemaTypeView.spec: "Content type not found" for a key that is neither built-in nor custom; events is now the editor. features/schema_list_and_content_nav.qa.md and SchemaView.spec: custom rows are links.
42. features/schema_builder_create.qa.md item 29/31: nothing changes for create; assert that the create builder still never sends "icon" (regression guard next to the edit icon test).
43. French: every new string has a French entry (the i18n completeness spec must pass).
## Visual (task visual:check -- schema)
Run `task visual:check -- schema`. Every capture within 10 %, desktop and mobile, light and dark. Mock GET /v1/auth/me (owner "Marta Alves"), GET /v1/builtin-type, and GET /v1/content-type with the 3 custom types. Replace the placeholder Events fields of `schemaCustomTypes` in `web/visual/states.ts` with the board's full 7 fields (EV), keep 7 as the list's field count, and add Recipes with the board's 4 fields (RC). Mock GET /v1/content-type/events/entry as total 37 and /recipes/entry as total 0. Register the group "3 Review and delete" in `extraSources` of `web/visual/check.ts`.
Setups to replace `skip: 'pending'` in `web/visual/states.ts` (screen `schema`, steps are Playwright actions on the SPA):
| Board state | Setup (path /schema/events unless noted) | Board sample data |
|---|---|---|
| Editing an existing type | no steps | Events, 7 saved fields, Title selected, info bar "37 entries use this content type…", chip "Saved", locked key |
| Locked key: tooltip | select the row "Starts on", hover (focus on mobile) the locked key | tooltip text, Starts on selected |
| Unsaved changes | steps: Capacity Highest value 2000, Add field "Speaker", Add field "Room" with Choice, Required, choices Main hall and Studio, default Main hall, remove Description, select Capacity | rows New and Changed, undo row "Description will be removed", chip "Unsaved changes" |
| Field errors | steps: empty the label of Ends on, Capacity Lowest value 6000, Category choices Talk, Workshop, blank, Talk, select Capacity | chip "3 problems to fix" and the four messages |
| Choice list editor | steps: Category choices Talk, Workshop, Meetup, blank, Talk, select Category | "5 of 50", chip "1 problem to fix" |
| Keyboard reorder | steps: focus the grip "Move Ends on", press Space, press Up | live region "Moving Ends on. Position 2 of 7. …", Ends on picked up |
| API preview open | steps: open "API preview" (sheet on mobile) | key events, board's sample JSON |
| Saved | `skip: 'clock-sensitive'` | the chip says "Saved at 11:02" |
| Save failed | PUT mocked 500; same edits as Unsaved changes; steps: Save, Apply changes | banner "Couldn't save the content type", chip "Save failed" |
| Schema incompatible | PUT mocked 409 schema_incompatible; same edits; steps: Save, Apply changes, Back to editing | banner "Nothing was changed" with "Review changes", "Possible cause" on Capacity |
| Offline | same edits; steps: go offline | offline banner, chip "Waiting for connection" |
| Saving | see open question 6 | |
| Review before saving | same edits; steps: Save | dialog copy and the 4 rows of group 3 state 1 |
| Applying | PUT mocked to hang; steps: Save, Apply changes | "Applying…" |
| Refused (schema_incompatible) | PUT mocked 409; steps: Save, Apply changes | refusal dialog with Change Capacity |
Not checked: "Review with counts and diff" (scope Later, not built; leave it out of `extraSources`). "Field key error" has no board frame: keep it as a documented `skip: 'pending'` with that reason and cover it in Vitest only. Do not hide it.
Known visual differences to report, never hide: the Key help copy (Q1) on /schema/new.
## Manual
- Real backend, as owner and admin:
- Create Events with all six field types and some entries.
- Open it from the list. Edit the name and a label. Confirm the review does not open for a name-only edit and does for a field edit.
- Lower Capacity's Highest value below an existing entry's value. Confirm the real 409 `schema_incompatible`, the refusal dialog and that nothing changed (reload).
- Add a required field with no default (409). Add one with a default (200 and existing entries get it).
- Turn Unique on over duplicate values (409). Remove a Choice option that entries use (409). Remove a field and check entries no longer return it.
- Create a type through curl with an `icon`, edit it in the SPA, and check the icon is still returned by GET.
- Check that `GET /v1/content-type/{key}/entry?page=1&size=5` is accepted as the count request.
- Keyboard: Tab order is Name, locked Key (tooltip on focus), Save, Discard, the field list (one stop, arrows move between rows), the inspector, Undo, API preview. Dialogs trap focus and return it to the trigger. Screen reader: the lock states are announced, the live region reads each reorder step, errors are read with their input.
- Hover and motion on the lock tooltip, the undo row and the dialogs. Sheet open and close focus return on a real phone.
- Real offline: edit, go offline with the review open, reconnect, press Apply.
- Reload or close the tab with unsaved edits (not handled by the leave guard): note the browser behaviour.