[ERP-73] Paginer toutes les listes côté front + composable de liste paginée réutilisable #30
Reference in New Issue
Block a user
Delete Branch "feature/ERP-73-frontend-l-paginer-toutes-les-listes-cote-front-co"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Contexte
Ticket Lesstime : #73 (id 492) — volet front de la pagination (groupe Transversal).
Dépend du back ERP-72 (déjà mergé sur develop). Pas de spec docs/specs ; référence = description #73 + .claude/rules/frontend.md.
Implémentation
usePaginatedList(frontend/shared/composables/) générique, branché directement surMalioDataTable(props page/perPage/totalItems + events update:page/update:per-page).Accept: application/ld+json(sans Accept, API Platform renvoie un tableau plat sans pagination).useCategoriesAdmin: ne porte plus la liste paginée (déplacée versusePaginatedList<Category>dans la page) et concentre son rôle sur le référentielCategoryType(chargé en une fois via?pagination=false, échappatoire prévue parpagination_client_enabled: truecôté back)..claude/rules/frontend.md(section dédiée + exemple).Décision URL
Le ticket suggérait « idéalement page/tri/filtre dans l'URL » — arbitré explicitement par Tristan en faveur de la règle ABSOLUE n°6 du CLAUDE.md (state local uniquement, jamais persisté dans l'URL). Aucun reflet URL implémenté ; comportement homogène entre toutes les listes migrées.
Tests
make nuxt-test: 101/101 OK (22 nouveaux tests surusePaginatedList, 6 anciens testsuseCategoriesAdmin.fetchAllretirés en cohérence avec la refacto).make dev-nuxt) : Sites, Utilisateurs, Rôles, Catégories affichent le sélecteurLignes : 10et les boutons Prev/Next ; audit-log (non migré, composable spécifique) intact avec ses 3 pages.Hors périmètre
audit-log.vuenon migré : composableuseAuditLogspécifique (cache partagé page/timeline, filtres complexes, persistance URL préexistante). Refactor risqué et net-zéro pour ERP-73.usePaginatedListdès sa création.Review ERP-73 —
usePaginatedList+ migration des listes adminRelecture du diff complet (composable, tests, 4 pages migrées, refacto
useCategoriesAdmin, doc). Beau travail dans l'ensemble : l'abstraction est propre, bien documentée en FR, et la couverture de tests est sérieuse. Quelques recommandations ci-dessous, classées par importance.Points positifs
sites.vue: vrai correctif de fond. L'ancienloadSitesfaisaititemsPerPage: 999→ il court-circuitait la pagination et violait la règle ABSOLUE n°13. La bascule surusePaginatedListcorrige ça réellement, pas juste cosmétiquement.useCategoriesAdminrecentré sur le référentielCategoryTypevia?pagination=false(échappatoire prévue) — séparation des responsabilités claire, le commentaire explique bien le « pourquoi singleton ».MalioDataTablene slice pas en interne (il renditemstel quel et émetupdate:page/update:per-page), donc passer la page serveur +totalItemsest exactement le contrat attendu. RAS de ce côté.Recommandations
1. [Correctness — moyen] Pas de garde anti-réponse périmée (race condition).
fetch()ne séquence pas les requêtes concurrentes. Si l'utilisateur enchaînegoToPage/setItemsPerPage/setFiltersrapidement (réseau lent), c'est la dernière réponse arrivée qui gagne, pas la dernière demandée → le tableau peut afficher une page périmée avec uncurrentPageincohérent.👉 Ajouter un jeton de séquence incrémenté à chaque
fetch()et ignorer la réponse si le jeton n'est plus le dernier :2. [UX/observabilité — moyen] L'erreur réseau/403 est indiscernable d'une liste vide.
Dans le
catch, on faititems=[],totalItems=0,hasFetched=true→isEmpty=true. Le composable n'expose aucunerror: un 403 ou une coupure réseau s'affiche désormais exactement comme une liste légitimement vide (l'empty-message). L'ancien code par page avait le même défaut, mais le composable est l'occasion de mieux faire.👉 Exposer un
error: Ref<unknown | null>(set dans lecatch, reset en début defetch) pour que les pages puissent afficher un bandeau d'erreur / un bouton « réessayer ». A minima, documenter explicitement queisEmptyconfond « vide » et « échec ».3. [Qualité de test — faible] Le test « page hors borne après filtre » ne teste pas ce que son nom annonce.
Dans
usePaginatedList.test.ts, le test'page hors borne apres filtre retombe sur la derniere page valide'arme deuxmockResolvedValueOncepuis appellesetFilters— orsetFiltersremetcurrentPage = 1, donc le chemin de retry hors-borne n'est jamais exercé (seul le 1ᵉʳ mock est consommé, le 2ᵉ est mort). Le commentaire interne l'admet d'ailleurs (« c'est le cas standard, pas le hors borne »). Le vrai retry est couvert par le test suivant ('declenche le retry sur derniere page...').👉 Soit supprimer le mock mort + renommer ce test pour ce qu'il vérifie réellement (« setFilters retombe en page 1 »), soit le faire réellement déclencher le hors-borne. En l'état il donne une fausse impression de couverture.
4. [Doc — faible] Commentaire
extraQuerytrompeur sur la réactivité.La JSDoc dit « Reactifs si une ref / computed est fournie via
refresh()». OrextraQueryest unRecord<string, unknown>figé à l'init, etbuildQueryfaitObject.assign(query, options.extraQuery): passer unerefcopierait l'objet ref non déballé dans le query param. Aucun appelant n'utilise ce cas aujourd'hui, donc c'est latent.👉 Soit accepter
MaybeRefOrGetterettoValue()les entrées, soit corriger le commentaire pour dire queextraQueryest un snapshot statique.5. [Robustesse — faible/défensif] Les filtres peuvent écraser les clés de pagination.
Dans
buildQuery,Object.assign(query, filters.value)est appliqué en dernier : un filtre nommépage,itemsPerPageouorder[...]écraserait silencieusement la pagination/le tri. Peu probable mais le composable est générique.👉 Assigner les filtres avant la pagination, ou logger/ignorer ces clés réservées.
6. [Nit]
loadingexposé mais non câblé.Le composable expose
loading, mais aucune des pages migrées ne le branche surMalioDataTable(qui n'a pas de prop loading ici, mais on pourrait au moins masquer/griser pendant un changement de page). Optionnel — à garder en tête pour l'UX de changement de page sur réseau lent.7. [Nit/futur] Pas de debounce pour les filtres texte.
setFiltersdéclenche unfetchimmédiat. C'est OK pour l'usage actuel (refetch piloté par le drawer), mais dès qu'un filtre texte « live » sera câblé, chaque frappe fera un appel. À documenter côté appelant ou à offrir en option le moment venu.Rien de bloquant. Les points 1 et 2 méritent à mon sens un traitement (ou au moins une décision explicite) avant merge ; le reste peut suivre.
Review ERP-73 — pagination front +
usePaginatedListLe composable est propre et bien testé : garde anti-réponse périmée par token,
errorexposé pour distinguer « vide » d'« échec »,MalioDataTablecorrectement piloté en mode contrôlé serveur (pas de slice client),useApiforwarde bienheaders(Accept ld+json OK), chargement initial viaonMountedconservé. 👍En revanche, la pagination ne fonctionne pas de bout en bout : la PR câble la moitié front mais la moitié back n'est pas alignée. Les tests passent car ils mockent
useApi, donc ces deux points ne sont pas couverts.🔴 1 —
/categoriesn'est pas paginé côté serveur (bloquant)CategoryProvider::provide()retourne->getResult()(tableau brut). API Platform ne pagine pas un tableau brut renvoyé par un provider custom : il sérialise toute la liste ettotalItems = count(liste), en ignorantpageetitemsPerPage.Conséquence dans
categories.vue:usePaginatedListreçoit l'intégralité des catégories →MalioDataTableaffiche toutes les lignes sur la « page 1 », la barre annonce N/10 pages, et cliquer sur une autre page refetch… toute la liste. La pagination est cosmétique.Fix : faire renvoyer un paginator par le provider (cf.
AuditLogProviderqui retourne unDbalPaginator, ou unApiPlatform\State\Pagination\TraversablePaginator/ArrayPaginator), ou laisser API Platform paginer unQueryBuilderDoctrine.🟠 2 —
itemsPerPageignoré sur/sites,/roles,/usersAucune des ressources
Site/Role/Userne déclarepaginationClientItemsPerPage, etconfig/packages/api_platform.yaml > defaultsne fixe aucune clé de pagination. On retombe donc sur les défauts framework :pagination_client_items_per_page: falseetpagination_items_per_page: 30.Conséquence : le
itemsPerPage=10envoyé par le composable est silencieusement ignoré, le serveur pagine par 30.totalPagescalculé côté front (÷10) ne correspond pas au paging serveur (÷30) ;Fix : ajouter
paginationClientItemsPerPage: true+paginationItemsPerPage: 10sur lesGetCollectionconcernées (ou globalement dansdefaults), pour que le serveur respecte la taille de page demandée.🟡 3 — Référence à une « règle ABSOLUE n°13 » inexistante
.claude/rules/frontend.mdet le docblock deusePaginatedList.tscitent la « règle ABSOLUE n°13 (toute collection paginée côté back) ».CLAUDE.mds'arrête à la règle 12 : cette règle n'existe pas. Soit l'ajouter àCLAUDE.md, soit corriger la référence — d'autant qu'avec les points 1 & 2, le back ne la respecte pas encore.En l'état, je recommande de compléter la pagination côté back (points 1 & 2) avant merge, sinon les barres de pagination introduites sont trompeuses pour l'utilisateur.