feat: entry list at /content/:key

This commit is contained in:
Florian Sylvain committed 2026-10-20 21:05:03 +02:00
1 parent b4cbcb0395
commit 91d916fb74
44 files changed
+4984 -32

No files matched your search

+224
View File
@@ -0,0 +1,224 @@
Feature: Entry list of a custom content type
As a verified user
I want to browse the entries of a custom content type
So that I can find an entry before editing it
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 route is /content/:key with meta auth "verified" and title "Content", per docs/design/screens/content.md group "5 Content and entry list", states 1 to 9
And the page requests GET /v1/content-type/{key} (the type) and GET /v1/content-type/{key}/entry?page=P&size=S&sort=O (the entries) at the same time; the entries answer {items, total, page, size, sort} and each item is {id, content_type_id, data, created_by, updated_by, created_at, updated_at}
And the sidebar Content group of features/schema_list_and_content_nav.feature is reused unchanged; this slice only checks it in the "Many types" state
And page, size and sort live in the URL (/content/events?page=2&size=50&sort=created_at:desc), the defaults (page 1, size 25, sort updated_at:desc) are omitted from the URL, and the request always carries explicit page and size
And size is one of 5, 25, 50, 100; sort is <id|created_at|updated_at>:<asc|desc>; an invalid value is replaced silently by the default in the URL and in the request, so the API never answers 400 here
And a size or sort change resets page to 1; the URL changes when a response succeeds, not when the request is sent
And the pager, the page-size select, the summary, the loading page, the page error notice and the out-of-range redirect behave as in features/articles_pagination_screen.feature scenarios 1 to 7, 10, 10b and 11, with the content URL and request
And the page size is not remembered
And the columns are ID, up to 6 chosen field columns (default: the first 4 fields of the type, fewer if the type has fewer), Created, Updated and Updated by; ID, Created, Updated and Updated by are always shown
And a field cell shows: Text truncated on one line with the full text as tooltip; Rich text as its first line in plain text, truncated; Number right-aligned with tabular figures; Yes or no as an icon plus "Yes" or "No"; Date as day, month and year ("21 Nov 2026") with no time-zone shift; Choice as a small outline tag; an empty value as "–"
And Created reads "12 Oct 2026" and Updated reads "15 Oct 2026" (date only, local time zone), ID is mono and muted
And "Updated by" is the display_name from one GET /v1/user/lookup?ids=<distinct updated_by of the page> sent after the entries answer; until it resolves, and if it fails or does not know the id, the cell shows a muted "–"
And the desktop list is a table with a 36 px header and 48 px rows; below md it is a card list with 68 px cards, each card a link with a chevron-right, showing the first field, the second field, the first Choice field as a tag if the type has one, and "· {Updated by}"; cards ignore the column picker
And every row is a link to /content/:key/:id (a link on the first field cell on desktop, the whole card on mobile); the click uses the router
And "New entry" (primary, plus icon, in the title row on desktop; a floating button on mobile) navigates to /content/:key/new
And /content/:key/new and /content/:key/:id are not routed in this slice and land on the catch-all 404 until the entry editor slice
And "Edit content type" (ghost, settings-2 icon, right of the toolbar) navigates to /schema/:key; it is shown to owner and admin only and not on mobile
And no filter or search exists
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: 1 Populated, one page
Given GET /v1/content-type/events resolves 200 with 7 fields and GET .../entry?page=1&size=25&sort=updated_at:desc resolves 200 with 7 items and total 7
When the user navigates to /content/events
Then the breadcrumb reads "Content" then "Events", the title "Events" with the subtitle "events · 7 fields"
And "New entry" is in the title row and the toolbar reads "Sort by" "Last updated", a direction button, "Columns", then "Edit content type" at the right
And the table headers read "ID" "Title" "Starts on" "Ends on" "Online" "Created" "Updated" "Updated by" and the Updated header shows an arrow-down icon
And 7 rows show, the first row ID 42 "Lime plaster workshop" "21 Nov 2026" "22 Nov 2026" "No" "12 Oct 2026" "15 Oct 2026" "Marta Alves"
And no pager, no page-size select, only the text "7 entries" ("1 entry" for one, proposed copy)
And the sidebar row Events is the current page and the URL has no query
Scenario: 2 Paginated, 5 per page
Given GET .../entry?page=1&size=5&sort=updated_at:desc resolves 200 with 5 items and total 37
When the user navigates to /content/events?size=5
Then the summary reads "Showing 1–5 of 37", the page-size select reads "5 per page" and the paginator shows First, Previous (both disabled), 1 (current), 2, an ellipsis, 8, Next, Last
And on mobile the paginator reads Previous (disabled), "Page 1 of 8", Next
And activating Next sends page=2 and, on success, the URL is /content/events?size=5&page=2
Scenario: 3 Columns menu
Given the populated state of the 7-field type Events
When the user activates "Columns"
Then a popover opens on desktop and a bottom sheet on mobile, titled "Columns" with "4 of 6"
And the sheet adds the text "Choose up to 6 fields to show. 4 selected." and the actions "Reset" and "Done"; the popover has "Reset" only
And each of the 7 fields is a checkbox row with its type icon, its label and its key in mono, in schema order; Title, Starts on, Ends on and Online are checked
And a last row with a clock icon reads "Created and updated dates always show" with a disabled, unchecked checkbox
When the user checks "Capacity"
Then the table gets a Capacity column (right-aligned) at once, with no request, and the count reads "5 of 6"
And Esc, a click outside or "Done" closes it and returns focus to "Columns"
Scenario Outline: The column cap and the stored choice
Given the 7-field type Events and <before>
When <action>
Then <after>
Examples:
| before | action | after |
| 6 fields checked | the user opens the menu | the unchecked fields are disabled and the text "You can show up to 6 fields." (proposed copy) shows |
| 5 fields checked | the user checks a 6th | it is checked and the others become disabled |
| 6 fields checked | the user unchecks one | the others are enabled again |
| the default 4 and nothing stored | the user checks "Capacity" | localStorage renewcms-content-columns-events is ["title","starts_on","ends_on","online","capacity"] |
| a stored selection | the user activates "Reset" | the default 4 apply and the stored key is removed |
| stored ["title","gone","capacity"] | the page loads | the columns are Title and Capacity, and nothing is written |
| stored with 8 valid keys | the page loads | the first 6 in schema order show |
| stored "not json" or [] | the page loads | the default 4 show |
| localStorage throws or is undefined | the user changes the selection | the columns change for this visit and no error shows |
| stored for events | the user opens /content/recipes | Recipes uses its own key and its default |
Scenario: The last field column cannot be unchecked
Given the 7-field type Events with exactly one field checked
When the user opens the Columns menu
Then that field's checkbox is disabled, so at least one field column always shows and every row has its link
And a stored empty selection loads as the default 4
Scenario: A type with fewer than 4 fields
Given a type with 2 fields
When the user opens its list
Then both field columns show and "of 6" in the menu still reads the cap
Scenario Outline: Sort control
Given GET .../entry resolves 200 and the user is on /content/events?page=3
When the user chooses "<option>" in the "Sort by" select
Then GET ...&sort=<sort>&page=1 is sent and, on success, the URL is <url>
And the sorted column header shows the arrow icon of its direction
Examples:
| option | sort | url |
| ID | id:desc | /content/events?sort=id:desc |
| Created | created_at:desc | /content/events?sort=created_at:desc |
| Last updated | updated_at:desc | /content/events |
Scenario Outline: Direction button
Given the user is on <from>
When the user activates the direction button
Then GET ...&sort=<sort>&page=1 is sent and, on success, the URL is <url>
And the button shows arrow-up-narrow-wide when ascending and arrow-down-wide-narrow when descending, with the accessible name "Sort ascending" or "Sort descending" for the next action (proposed copy)
Examples:
| from | sort | url |
| /content/events | updated_at:asc | /content/events?sort=updated_at:asc |
| /content/events?sort=updated_at:asc | updated_at:desc | /content/events |
| /content/events?sort=id:desc | id:asc | /content/events?sort=id:asc |
Scenario Outline: Sort values in the URL
When the user navigates to /content/events?sort=<value>
Then <result>
Examples:
| value | result |
| created_at:asc | GET carries sort=created_at:asc and the select reads "Created" with the ascending direction icon |
| title:asc | GET carries sort=updated_at:desc and the URL is replaced by /content/events |
| data:asc | GET carries sort=updated_at:desc and the URL is replaced by /content/events |
| id | GET carries sort=updated_at:desc and the URL is replaced by /content/events |
Scenario: Sort is kept across paging and size changes
Given the URL is /content/events?sort=id:asc
When the user activates Next, or changes the size
Then every request carries sort=id:asc and the URL keeps it (a size change also resets the page)
Scenario: Switching to another type resets the page
Given the user is on /content/events?page=3&size=50&sort=id:asc
When the user opens /content/recipes from the sidebar
Then the requests are for recipes with page=1, size=25 and sort=updated_at:desc, its own columns apply and no Events row shows meanwhile
Scenario: 4 Many types in the sidebar
Given GET /v1/content-type returns 14 custom types, the 4th named "Frequently asked questions and answers"
When the user opens /content/events
Then on desktop the Content group shows "Content", the first 5 types and the selected one, the long name truncated, and "Show all 14"
And on mobile the drawer shows the group expanded with "All content types 14" collapsed row
And behaviour is as in features/schema_list_and_content_nav.feature
Scenario: 5 Loading
Given the type and the entries have not resolved
When the user navigates to /content/events
Then the title row shows "Events" once the type has resolved and a skeleton until then, "New entry" is disabled and the toolbar is shown
And the list is 8 skeleton rows on desktop (6 on mobile) with a 48 px row (68 px on mobile) and aria-busy="true"
And when both resolve the rows replace the skeleton
Scenario: 6 Empty for this type
Given GET .../entry resolves 200 with items [] and total 0
When the user navigates to /content/events
Then the empty state reads "No entries yet" with "Use New entry to add the first entry. Each entry follows the fields of this content type." and a "New entry" action
And the toolbar stays, the pager, the summary and the count text do not show
And "New entry" navigates to /content/events/new
And an out-of-range page with total 0 shows this state and the URL becomes /content/events
Scenario Outline: 7 Error loading entries
Given the type resolves 200 and GET .../entry <failure>
When the user navigates to /content/events
Then an error alert reads "Couldn't load entries" with "GET /v1/content-type/events/entry failed. Check your connection and try again." and a "Try again" action
And no toolbar, no list and no pager show; the title row stays
When the user activates "Try again"
Then the skeleton (state 5) shows and the failed request is sent again
Examples:
| failure |
| resolves with a 500 response |
| never reaches the server |
Scenario: Error loading the type
Given GET /v1/content-type/events answers 500
Then the same alert shows with "GET /v1/content-type/events failed. Check your connection and try again." (proposed copy)
And "Try again" sends only the failed request again
Scenario Outline: 8 Forbidden (403)
Given <which> answers 403
When the user navigates to /content/events
Then a centered state with the lock icon reads "You don't have access to Events" with "Your account can't open this content right now. If you think this is a mistake, ask an owner or an admin." and "Go to Home"
And no toolbar or list shows and "Go to Home" navigates to /home
And the title uses the type name when the type loaded, else the key
Examples:
| which |
| the entries request |
| the type request |
Scenario Outline: 9 Type not found
Given <which> answers 404 content_type_not_found
When the user navigates to /content/unknown
Then a centered state with the file-x icon reads "Content type not found" with "It may have been deleted, or the address may be mistyped." and "Go to Home"
And the breadcrumb reads "Content" only, the mobile title is "Content" and no sidebar row is current
And "Go to Home" navigates to /home
Examples:
| which |
| the type request |
| the entries request |
Scenario: A deleted type behind the sidebar
Given Recipes was deleted in features/schema_builder_delete.feature
When the user opens /content/recipes
Then the "Content type not found" state shows with no further request than the type's
Scenario: Session expired
Given any of the requests answers 401
Then the existing redirect to /login with the expired reason happens
Scenario: Stale answers are ignored
Given a request for page 2 is in flight
When the user changes the sort and that request resolves later
Then its answer is dropped and only the latest request updates the list and the URL
Scenario: Updated by lookup
Given the page has entries by users 3, 3 and 5
Then GET /v1/user/lookup?ids=3,5 is sent once after the entries answer and the cells read the display names
And when it answers 500 or lacks id 5 the cells read "–" and no error shows
Scenario: Navigation actions
Given the populated state and an owner
Then "New entry" and the first-cell links go to /content/events/new and /content/events/42, "Edit content type" goes to /schema/events
And with a role other than owner or admin "Edit content type" is not rendered
And below md "Edit content type" is not rendered and the New entry button is a floating button (hidden while loading and in the error state)
Scenario: Accessibility
Then the table has a visually hidden caption "Events, sorted by last updated, newest first" ("sorted by {id|created|updated}, {ascending|descending}" otherwise; proposed copy, French entry)
And "Columns", the direction button and the select have accessible names; the picker traps focus on mobile and is operable with Space and Esc
And controls are 44 px on mobile and focus is a 2 px --ui-primary outline with a 2 px offset