From 340a0ad46161333cfdeef6f7768ee819a6fbadf6 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Mon, 19 Oct 2026 04:00:47 +0200 Subject: [PATCH] feat: pagination in the SPA for the Sites and Authors lists --- .../sites_authors_pagination_screen.feature | 307 +++++++ .../sites_authors_pagination_screen.qa.md | 47 ++ web/src/components/AppPaginator.vue | 78 +- web/src/components/DirectoryLoadError.vue | 34 + web/src/components/DirectoryNoResults.vue | 33 + web/src/components/DirectoryPageNotice.vue | 57 ++ web/src/components/DirectoryPagination.vue | 45 + web/src/components/PageSizeSelect.vue | 49 ++ .../__tests__/directoryComposables.spec.ts | 68 -- web/src/composables/__tests__/useApi.spec.ts | 50 +- web/src/composables/useApi.ts | 32 +- web/src/composables/useArticleList.ts | 257 ++---- web/src/composables/useArticleSites.ts | 4 +- web/src/composables/useAuthors.ts | 17 +- web/src/composables/useDirectoryCollection.ts | 58 -- web/src/composables/useDirectoryListState.ts | 46 ++ web/src/composables/useDirectoryPages.ts | 153 ++++ web/src/composables/usePagedQuery.ts | 247 ++++++ web/src/composables/useSites.ts | 14 +- web/src/i18n/directoryMessages.spec.ts | 3 + web/src/i18n/messages.ts | 6 + web/src/lib/articleListQuery.ts | 47 +- web/src/lib/directoryQuery.ts | 31 +- web/src/lib/directoryRequest.ts | 9 + web/src/lib/pageQuery.ts | 24 + web/src/views/AuthorsView.vue | 284 ++----- web/src/views/EditorView.vue | 4 +- web/src/views/MediaView.vue | 144 ++-- web/src/views/SitesView.vue | 197 +++-- .../__tests__/SitesAuthorsPagination.spec.ts | 771 ++++++++++++++++++ web/src/views/__tests__/SitesForm.spec.ts | 8 +- web/src/views/__tests__/directoryTestKit.ts | 40 +- web/visual/check.ts | 27 +- web/visual/states.ts | 538 ++++++++++-- 34 files changed, 2833 insertions(+), 896 deletions(-) create mode 100644 features/sites_authors_pagination_screen.feature create mode 100644 features/sites_authors_pagination_screen.qa.md create mode 100644 web/src/components/DirectoryLoadError.vue create mode 100644 web/src/components/DirectoryNoResults.vue create mode 100644 web/src/components/DirectoryPageNotice.vue create mode 100644 web/src/components/DirectoryPagination.vue create mode 100644 web/src/components/PageSizeSelect.vue delete mode 100644 web/src/composables/useDirectoryCollection.ts create mode 100644 web/src/composables/useDirectoryListState.ts create mode 100644 web/src/composables/useDirectoryPages.ts create mode 100644 web/src/composables/usePagedQuery.ts create mode 100644 web/src/lib/pageQuery.ts create mode 100644 web/src/views/__tests__/SitesAuthorsPagination.spec.ts diff --git a/features/sites_authors_pagination_screen.feature b/features/sites_authors_pagination_screen.feature new file mode 100644 index 0000000..dcf44e9 --- /dev/null +++ b/features/sites_authors_pagination_screen.feature @@ -0,0 +1,307 @@ +Feature: Sites and Authors list pagination + + As a RenewCMS owner (Sites) or any signed-in role (Authors) + I want the Sites and Authors lists split into pages with a page-size menu, server-side search and shareable URLs + So that I can browse a long directory quickly and come back to the same page + + Background: + Given /settings/sites (SitesView, owner only) and /authors (AuthorsView, every role) are the verified routes of features/sites_authors_screens.feature, per docs/design/routes.md, docs/design/screens/sites.md group "Sites: pagination" and docs/design/screens/authors.md group "Authors: pagination" + And the Paginator and the page-size select are the Articles and Media ones (AppPaginator, docs/design/components.md "Paginator"); nothing new is drawn except the single-page variant below + And the screens request GET /v1/site/?q=Q&page=P&size=S and GET /v1/author/?q=Q&page=P&size=S through useApi only, with relative URLs, always sending page and size, and q only when it is not empty + And the total is read from the X-Total-Count response header, and the body stays the array of sites (Sites) or the envelope {items,total,page,size} (Authors) + And a request that fails or carries no X-Total-Count is treated as a failed load + And page, size and q live in the URL as ?page=&size=&q=; sizes are 5, 25, 50, 100 and the default is 25; the defaults (page 1, size 25, no q) are omitted from the URL + And an invalid page (not a positive integer) or an invalid size (not 5, 25, 50 or 100) is replaced silently by the default with a history replace and no notice, and the request carries the default + And a change to search or to size resets page to 1 and drops it from the URL; search keeps its existing debounce + And the URL changes when a response succeeds, not when the request is sent, and Browser Back returns to the previous page of results + And a page past the last page (a 200 with items [] and total above 0) is replaced in the URL (no history entry) by ?page=, is requested, and the info notice "That page doesn't exist. Showing the last page." is shown + And the summary reads "Showing {from}–{to} of {total}" with locale-formatted numbers (thousands separators above 999), computed from total (the filtered count) + And the footer sits below the list: summary on the left, the page-size select next to it, the page controls right-aligned; below md the summary and the select share one row (wrapping onto two if needed), then Previous, "Page X of Y" and Next on one row with 44 px targets and a label that never wraps + And the page controls are First, Previous, page numbers with ellipses marking skipped ranges once there are more than 7 pages, Next and Last; the current page uses --rc-sand fill, --rc-on-sand text, a --ui-primary border and aria-current="page"; disabled controls are at 40 % opacity with aria-disabled + And on a single page (total <= size, total > 0) the controls and the page-size select are not rendered and the footer holds only the count line "{N} sites" or "{N} authors" in --ui-text-muted ("1 site", "1 author"); this replaces the AuthorsView rule that showed the pager only above 25 items + And the count line or the footer appears whenever the list has loaded with total above 0, including the existing "Default" frames + And the same footer is shown to owner, admin and read-only roles on Authors; Sites stays owner only (403 page for the others) + And SitesView gains a search field with the placeholder "Search by name or key", matching name, key and base URL on the server; Authors keeps "Search by name or slug" + And the places that need every site (deep link ?edit=, duplicate-key check, ReassignDialog destinations, the form's default-site text) read the unpaged GET /v1/site/ and never the current page + And keyboard and aria are those of Articles: Tab reaches the page-size select then the controls, Enter or Space activates, ArrowDown opens the page-size list, Enter selects, Esc closes and returns focus; every control shows a 2 px --ui-primary focus outline with a 2 px offset + And every new visible string goes through t(...) with the English text as the key and its French entry in web/src/i18n/messages.ts; the placeholders are written {0}, as in the other messages; the new strings are "Search by name or key" (French "Rechercher par nom ou clé"), "{0} sites" and "1 site" ("{0} sites", "1 site"), "{0} authors" and "1 author" ("{0} auteurs", "1 auteur"), and 'No sites match "{0}"' (French 'Aucun site ne correspond à « {0} »'); the other strings reuse the Articles and Media entries + And the sites fixture is 61 sites (3 pages at size 25) and the authors fixture is 128 authors (6 pages at size 25), with generated rows, unless stated + + Rule: Page states, one set per screen + + Scenario Outline: 1 First page + Given resolves GET ?page=1&size=25 with 25 rows and X-Total-Count + When the user navigates to + Then the summary reads "", the page-size select reads "25 per page" and the controls read "Page 1 of " + And First and Previous are disabled with aria-disabled, page 1 is current, Next and Last are enabled + And below md Previous is disabled + And the URL has no page and no size + + Examples: + | screen | path | route | total | pages | summary | + | Sites | /v1/site/ | /settings/sites | 61 | 3 | Showing 1–25 of 61 | + | Authors | /v1/author/ | /authors | 128 | 6 | Showing 1–25 of 128 | + + Scenario Outline: 2 Middle page + Given resolves GET ?page=&size=25 with 25 rows and X-Total-Count + When the user navigates to ?page= + Then the summary reads "" and the label reads "Page of " + And page is current with --rc-sand, --rc-on-sand, a --ui-primary border and aria-current="page" + And every control is enabled + + Examples: + | screen | path | route | page | total | pages | summary | + | Sites | /v1/site/ | /settings/sites | 2 | 61 | 3 | Showing 26–50 of 61 | + | Authors | /v1/author/ | /authors | 3 | 128 | 6 | Showing 51–75 of 128 | + + Scenario Outline: 3 Last page + Given resolves GET ?page=&size=25 with rows and X-Total-Count + When the user navigates to ?page= + Then the summary reads "" and the label reads "Page of " + And Next and Last are disabled with aria-disabled and Previous and First are enabled + + Examples: + | screen | path | route | pages | rows | total | summary | + | Sites | /v1/site/ | /settings/sites | 3 | 11 | 61 | Showing 51–61 of 61 | + | Authors | /v1/author/ | /authors | 6 | 3 | 128 | Showing 126–128 of 128 | + + Scenario Outline: 4 Single page + Given resolves GET ?page=1&size=25 with rows and X-Total-Count + When the user navigates to + Then the footer shows only "" + And the page controls and the page-size select are not rendered + And no "Showing" summary is rendered + + Examples: + | screen | path | route | total | line | + | Sites | /v1/site/ | /settings/sites | 4 | 4 sites | + | Authors | /v1/author/ | /authors | 6 | 6 authors | + + Scenario Outline: 4b Singular count line + Given resolves its list with 1 row and X-Total-Count 1 + When the user navigates to + Then the footer shows only "" + + Examples: + | screen | route | line | + | Sites | /settings/sites | 1 site | + | Authors | /authors | 1 author | + + Scenario Outline: 4c Five per page with a few rows still pages + Given resolves GET ?size=5&page=1 with 5 rows and X-Total-Count + When the user navigates to ?size=5 + Then the summary "Showing 1–5 of ", the page-size select "5 per page" and the controls read "Page 1 of 2" + + Examples: + | screen | path | route | total | + | Sites | /v1/site/ | /settings/sites | 6 | + | Authors | /v1/author/ | /authors | 6 | + + Scenario Outline: 5 Page size menu + Given the user is on ?page= + When the user opens the page-size select (it opens upward) and chooses "