feat: delete a custom type

This commit is contained in:
Florian Sylvain committed 2026-10-20 17:56:44 +02:00
1 parent 76e9e1220c
commit 5404291af2
25 files changed
+1768 -46

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
Feature: Delete a custom content type from the Schema builder
As an owner or admin
I want to delete a custom type that has no entries
So that unused types leave the schema, the API and the sidebar
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 edit builder, its store, entry count, leave guard, chip and banners of features/schema_builder_edit.feature are reused unchanged unless a scenario below says otherwise
And DELETE /v1/content-type/{key} answers 204 with no body
And DELETE answers 404 content_type_not_found, 409 content_type_has_entries, 403 or 500
And the number of entries is the "total" already loaded from GET /v1/content-type/{key}/entry?page=1&size=5; no other request is made to open a dialog
And the dialogs are DeleteTypeDialog (UModal + UAlert, color "error" on the destructive action; a bottom sheet on mobile) from docs/design/screens/schema.md group "3 Review and delete", states 5 to 7
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: The overflow menu (proposed placement, see open question 1)
Given an owner opens /schema/recipes
Then the top bar holds an icon button "More actions" (proposed copy) with the ellipsis icon, after Discard and Save on desktop and as the topbar action on mobile
When the user activates it
Then a menu opens with one item "Delete content type" with a trash-2 icon in the error colour
And Esc or a click outside closes it and returns focus to the button
Scenario Outline: Where the menu is available
Given <page>
Then <result>
Examples:
| page | result |
| /schema/new | no overflow menu |
| /schema/articles (built-in, read-only) | no overflow menu and no delete |
| /schema/events (edit) | the overflow menu |
Scenario Outline: The menu is disabled while another action runs
Given /schema/recipes and <state>
Then the "More actions" button is disabled
Examples:
| state |
| the PUT is in flight (Saving, Applying) |
| a delete is in flight |
Scenario: Blocked, the type has entries (design group 3, state 5)
Given /schema/events has 37 entries
When the user chooses "Delete content type"
Then no request is sent
And the dialog "You can't delete Events yet" opens with "A content type can only be deleted when it has no entries."
And a warning alert reads "Events has 37 entries" with "Delete all of its entries first, then delete the content type."
And the actions are "Close" and "View entries"; there is no delete button
And the count is grouped like the info bar ("1,200 entries") and a single entry reads "Recipes has 1 entry" (proposed copy)
Scenario: View entries
Given the blocked dialog or the fallback dialog is open
When the user activates "View entries"
Then the dialog closes and the router navigates to /content/events (see open question 4)
Scenario: Close the blocked dialog
Given the blocked dialog is open
When the user chooses "Close", the close icon or presses Esc
Then the dialog closes, the page and every edit are unchanged and focus returns to the menu button
Scenario: Confirm, the type has no entries (design group 3, state 6)
Given /schema/recipes has 0 entries
When the user chooses "Delete content type"
Then no request is sent
And the dialog "Delete Recipes?" opens with "Recipes has no entries. Its fields and its API routes will be deleted. This can't be undone."
And the body reads "Key: " then "recipes" in mono
And the actions are "Cancel" and "Delete content type" (error colour)
And Cancel, the close icon and Esc close it with nothing sent and focus returned to the menu button
Scenario: Deleting (proposed, see open question 3)
Given the confirm dialog is open
When the user activates "Delete content type"
Then DELETE /v1/content-type/recipes is sent once
And the button reads "Deleting…" with a loader, Cancel is disabled
And Esc, the close icon and a click on the overlay do not dismiss the dialog
And a second activation sends nothing
Scenario: Deleted
Given DELETE answers 204
Then the dialog closes
And the content types store drops "recipes" (store.drop) with no GET /v1/content-type
And the router navigates to /schema with no "Leave without saving?" prompt
And a success toast "Content type deleted" with "Recipes was deleted." (proposed copy) is shown
And the Schema list no longer shows Recipes and the sidebar Content group no longer lists it
And no request to GET /v1/content-type/recipes/... is made afterwards
Scenario: Back or reload after a delete
Given Recipes was deleted
When the user opens /schema/recipes
Then the page "Content type not found" with "It may have been deleted, or the address may be mistyped." is shown
Scenario: Server fallback, entries appeared (design group 3, state 7)
Given the page loaded Recipes with 0 entries
When the user confirms and DELETE answers 409 content_type_has_entries
Then the dialog stays open with the title "Delete Recipes?" and the error alert "Recipes now has entries" with "Someone added an entry after this page loaded, so nothing was deleted. Delete the entries first, then try again."
And the actions are "Close" and "View entries"; there is no delete button
And the store still holds Recipes and the user stays on /schema/recipes
And "Close" closes the dialog; the loaded count is not refreshed, so choosing "Delete content type" again opens the confirm dialog (see open question 2)
Scenario Outline: Other answers of DELETE
Given the confirm dialog is open and DELETE <outcome>
Then <result>
Examples:
| outcome | result |
| answers 404 content_type_not_found | the dialog closes, the store drops the type and the page "Content type not found" is shown (proposed, see open question 5) |
| answers 403 "Only ..." | the dialog closes and the page "You don't have access to Schema" is shown |
| answers 401 | the existing redirect to /login with the expired reason |
| answers 500 or fails (network) | the dialog stays open with the error alert "Couldn't delete the content type" (proposed copy) and "Something went wrong on our side. Nothing was changed. Try again in a moment."; "Cancel" and "Delete content type" are enabled again and the store is untouched |
Scenario: Retry after a failure
Given the dialog shows the 500 alert
When the user activates "Delete content type"
Then the same DELETE is sent again and the alert clears while it is in flight
Scenario: Offline
Given the page is offline (the existing offline banner is shown)
When the confirm dialog is open
Then "Delete content type" is disabled and nothing is sent
And when the connection returns it is enabled again and nothing is sent by itself
Scenario Outline: Unsaved edits and the delete (the leave guard)
Given /schema/<key> with unsaved edits (chip "Unsaved changes")
When the user chooses "Delete content type"
Then the dialog is the one of the saved type's count, its copy never mentions the edits
And opening it does not discard, validate or save the edits
And <after>
Examples:
| key | after |
| recipes | on Cancel, Close, 409 and 500, the edits, the chip and the undo rows are exactly as they were |
| recipes | on 204 the router leaves with no "Leave without saving?" prompt and the edits are discarded with the page |
| events | the blocked dialog is shown for the saved count (37), and Close keeps every edit |
Scenario: Delete after a refused save
Given the banner "Nothing was changed" is shown for Recipes
When the user deletes the type with no entries
Then the delete flow is the same and the banner goes with the page
Scenario: Session and role
Given a user who is neither owner nor admin opens /schema/recipes
Then the page "You don't have access to Schema" is shown and no menu exists
And the API still answers 403 to DELETE for that user
Scenario: Accessibility of the delete flow
Then the menu button has an accessible name and the menu is keyboard operable
And dialogs trap focus, open with focus on the title and return it to "More actions" on close
And while Deleting, focus stays on the dialog title
And the destructive button is never the default focus (focus starts on Cancel or Close)
And controls are 44 px high on mobile and focus is a 2 px ring in the primary colour
Scenario: Key error on a new field in the edit builder (design state 7, visual only)
Given /schema/events and a field added with the label "Second date", type Date and the key "2nd-date"
Then the board shows 8 fields, that row selected with the badge "New", the Key "2nd-date" with "Start with a letter. Use lowercase letters, digits and underscores." and the alert icon, and the chip "1 problem to fix"
And Save is disabled
And behaviour is as in schema_builder_edit.feature "A single field key error on a new field"
+42
View File
@@ -0,0 +1,42 @@
# QA: Schema builder, delete (SPA slice 5)
Runner: Vitest + Vue Test Utils (`task test:web`). No Go test: DELETE /v1/content-type/{key} is already covered by features/custom_content_types.qa.md and the backend does not change.
Same kit as features/schema_builder_edit.qa.md (`views/__tests__/schemaTestKit.ts`: add a DELETE handler per status, plain-text bodies content_type_has_entries and content_type_not_found; Events with total 37, Recipes with 4 fields and total 0). Never freeze the system date.
## Automated
1. Composable (useContentTypes.spec): `deleteContentType(key)` sends DELETE to /v1/content-type/{key}, resolves on 204 with no body parsing, ApiError carries status and plain text for 404, 409, 403, 500; NetworkError passes through.
2. Store: `remove(key)` drops the custom type, keeps order of the rest, never touches built-ins, makes no request, does nothing for an unknown key. reset() unchanged.
3. Menu: shown in edit mode only (not /schema/new, not built-in view, not for a non-owner/admin); one item "Delete content type"; Esc closes and returns focus; disabled while Saving, Applying and Deleting.
4. Blocked (total 37): no request; copy, warning alert, "Close" and "View entries", no delete button; singular and 1,200 grouping (proposed copy); Close/Esc/close icon keep edits; View entries navigates to /content/events.
5. Confirm (total 0): no request; copy, "Key: recipes" in mono; Cancel/Esc/close icon send nothing; focus starts on Cancel.
6. Deleting: one DELETE, "Deleting…" with loader, Cancel disabled, Esc/overlay/close icon do not dismiss, second activation sends nothing, focus on the title.
7. Deleted: 204 closes the dialog, store.drop called, navigates to /schema with no leave prompt even with unsaved edits, toast copy, list and sidebar Content group no longer show the type, no GET /v1/content-type afterwards; reopening /schema/recipes shows "Content type not found".
8. Fallback 409: dialog keeps title "Delete Recipes?", error alert copy, "Close" and "View entries", no delete button, store untouched, user stays on the page, edits kept.
9. Other answers (it.each): 404 (dialog closes, store drops, not-found page), 403 (access page), 401 (redirect to /login with expired reason), 500 and network (alert copy in the dialog, store untouched, retry sends the same DELETE and clears the alert).
10. Offline: Delete disabled with the dialog open, enabled on reconnect, nothing sent by itself.
11. Unsaved edits: dialog uses the saved count; opening, Cancel, Close and failures leave edits, chip and undo rows unchanged; 204 leaves with no prompt; blocked dialog for Events keeps edits.
12. Accessibility: focus return to "More actions", destructive button not default focus, menu keyboard use.
13. Structural refactor (SchemaTypeView.vue, six flags simplified): no new behaviour. Every existing SchemaTypeView/SchemaBuilder spec, schema_builtin_view, create and edit specs pass unchanged with no edit to their assertions; if one needs a change, stop and report. The resolver outcomes (built-in, custom edit, create, not found, forbidden, loading, error) are covered by one it.each over the former flag combinations.
14. French: every new string has an entry (i18n completeness spec passes).
## Visual (task visual:check -- schema)
Every capture within 10 %, desktop and mobile, light and dark. Register nothing new in `extraSources` (group "3 Review and delete" is already registered).
| Board state | Setup in web/visual/states.ts | Board sample data |
|---|---|---|
| Delete type: blocked | path /schema/events, steps: open "More actions", choose "Delete content type" | "You can't delete Events yet", "Events has 37 entries" |
| Delete type: confirm | path /schema/recipes (Recipes 4 fields, entry total 0), same steps | "Delete Recipes?", "Key: recipes" |
| Delete type: server fallback (409) | path /schema/recipes, DELETE mocked 409 content_type_has_entries, steps: open menu, choose item, "Delete content type" | "Recipes now has entries" |
| Field key error | replace `skip: 'pending'`; path /schema/events (the board draws it in the edit builder, not /schema/new); steps: Add field, label "Second date", type Date, set key "2nd-date" | 8 fields, row selected, chip "1 problem to fix" |
Board caveat (report, never hide): in the three delete frames the page behind the dialog is the Events builder while the dialog says Recipes (the frame reuses one builder). The overlay is compared as drawn (see open question 6). The desktop overflow menu itself is not drawn on any board: it is not visually checked.
## Manual
- Real backend, owner and admin: create a type with entries and try to delete it (dialog blocked, no request). Delete the entries, delete the type (204), check the sidebar and the list, reload /schema/<key>.
- Race: open Recipes, add an entry elsewhere, confirm the delete and check the real 409 fallback.
- Delete a type while another tab shows it, then act in the other tab (404 page).
- Keyboard: Tab to "More actions", open with Enter, dialogs trap focus and return it. Screen reader announces the dialog titles and the error alert.
- Real offline: open the confirm dialog, go offline, reconnect.