Combobox de catégorie avec saisie : AdjustmentForm, LinkTransfersModal, et liste rognée dans SplitAdjustmentModal #429
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#429
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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
#423 (PR #428, mergée le 2026-10-12) a remplacé par
CategoryComboboxles deux<select>de catégories du formulaire de catégorie et du dialogue « Nouveau mot-clé », et a sorti les champs de ce dialogue de son bloc défilant parce que la listeabsolutedu combobox était rognée par le scrollport. Deux autres<select>de catégories restent, et le rognage existe dans deux autres conteneurs.Analyse du 2026-10-12 (
/analyze, lu surmain92e9dd5). Prémisse vérifiée fichier par fichier.Constats
src/components/adjustments/AdjustmentForm.tsx:123-133— une ligne d'ajustement choisit sa catégorie dans un<select>natif, nom brut (c.name, pas d'i18n_key). La liste vient deuseAdjustments.ts:130:SELECT * FROM categories WHERE is_active = 1, saisissables ou non, alors que Transactions, le mot-clé (#423, D2) et le filtre des transferts ne montrent queis_inputable = 1. Une nouvelle ligne prend la première catégorie de la liste (AdjustmentForm.tsx:44), la valeur n'est jamaisnull: pas d'option « aucune » à prévoir.src/components/balance/LinkTransfersModal.tsx:259-272— c'est le filtre par catégorie du sélecteur de transactions à lier (« Toutes les catégories »,balance.transfers.modal.anyCategory), remise ànullpar l'option vide. La liste vient deBalancePage.tsx:69(transactionService.getAllCategories(), saisissables). Modèle à suivre :TransactionFilterBar.tsx:121-135(combobox +extraOptions« Toutes les catégories »). Ce modal est lui-même rendu parcreatePortal.src/components/transactions/SplitAdjustmentModal.tsx:156— le corps estmax-h-[60vh] overflow-y-autoet les lignes de ventilation (chacune avec sonCategoryCombobox, lignes 180-190) sont le contenu défilant : impossible de les sortir du bloc comme pour le dialogue mot-clé. La liste ouverte est rognée par le scrollport ; avec une seule ligne, elle n'est visible qu'en faisant défiler le corps.src/components/transactions/TransactionTable.tsx:139— le tableau vit dans un wrapperoverflow-x-auto, qui rogne aussi verticalement (overflow-x: autoforceoverflow-yàauto) : la liste ouverte depuis les dernières lignes est coupée au bas du tableau. Même défaut, non signalé jusqu'ici.TransactionTable.render.test.tsxépingle le markup exact du combobox fermé (<div class="relative"><input ... role="combobox" ...></div>, regexringedCategory) : le markup fermé ne doit pas changer, ou ce test est mis à jour en conséquence.vitesten environnement node,renderToStaticMarkup) : un portail et sa position ne sont pas testables unitairement.renderToStaticMarkupne supporte pascreatePortal: un test de rendu deLinkTransfersModaldoit remplacercreatePortalpar l'identité (vi.mock("react-dom", ...)).Décisions (Max, 2026-10-12)
CategoryCombobox: la liste ouverte est rendue dansdocument.bodyparcreatePortal, en position fixe calculée depuis le champ. Règle le rognage pour tous les conteneurs (modal de ventilation, dernières lignes du tableau Transactions, conteneurs futurs) au lieu d'une restructuration modal par modal.is_active = 1 AND is_inputable = 1), comme partout ailleurs. Une ligne existante qui vise une catégorie non saisissable s'affiche vide dans le combobox tant qu'on ne la change pas ; la valeur enregistrée reste.Travail à faire
CategoryCombobox: liste en portail. Quandopen && totalItems > 0, rendre le<ul role="listbox">parcreatePortal(..., document.body)avecposition: fixed,top/left/widthpris surinputRef.current.getBoundingClientRect()(top = bottom + 4px), recalculés surscroll(écouteur en capture surwindow, pour attraper les conteneurs défilants) etresize, et à l'ouverture. Si l'espace sous le champ est inférieur à la hauteur maximale de la liste (max-h-60= 240 px) et l'espace au-dessus plus grand, ouvrir vers le haut.z-indexau-dessus de tous les modals de l'app (AddKeywordDialogestz-[200]) :z-[300]. Le clic extérieur (mousedownsurdocument) doit considérer le nœud de la liste comme intérieur (listRef), sinon un clic dans la liste la ferme avant la sélection ;onMouseDown={e => e.preventDefault()}sur les options reste.aria-controls/aria-activedescendantrestent valides, les ids sont globaux. Markup fermé inchangé (<div class="relative"><input .../></div>).placeListbox({ anchor: DOMRect-like, viewportHeight, maxHeight })→{ top, left, width, openUpward }, pour que la règle de retournement soit épinglée sans DOM.AdjustmentForm: remplacer le<select>par<CategoryCombobox categories={categories} value={entry.category_id} onChange={id => updateEntryField(index, "category_id", id)} compact ariaLabel={t("adjustments.category")} />dans le conteneurflex-1 min-w-0; la catégorie par défaut d'une nouvelle ligne reste la première de la liste.useAdjustments.ts:130(D2) :AND is_inputable = 1dans la requête des catégories, ou appel detransactionService.getAllCategories()qui porte déjà ce filtre. Vérifier que rien d'autre dans le hook ne lit cette liste pour afficher le nom d'une catégorie d'une ligne existante (un ajustement sur une catégorie non saisissable perdrait son libellé dans le détail) ; si c'est le cas, garder une liste complète pour l'affichage et la liste filtrée pour le choix.LinkTransfersModal: remplacer le<select>parCategoryComboboxavecextraOptions=[{ value: "", label: t("balance.transfers.modal.anyCategory") }],activeExtra={categoryId === null ? "" : null},onExtraSelect={() => setCategoryId(null)},ariaLabel={t("balance.transfers.modal.category")}; même renducompactque les autres champs du filtre.SplitAdjustmentModal: rien à changer dans le composant si le portail est en place ; vérifier dans l'app lancée que la liste s'ouvre par-dessus le pied du modal, et que le défilement du corps la repositionne ou la ferme.placeListbox.test.ts(ou dansCategoryCombobox.test.ts) : position sous le champ, largeur du champ, retournement vers le haut quand l'espace manque en bas et suffit en haut, pas de retournement quand il manque des deux côtés.AdjustmentForm.render.test.tsx:role="combobox"par ligne,<select>absent, valeur affichée = nom (oui18n_key[defaultValue=…]avec le fauxtau formatk[...], #389) de la catégorie de la ligne.LinkTransfersModal.render.test.tsx:createPortalremplacé par l'identité ;role="combobox"présent,<select>absent, valeur affichéebalance.transfers.modal.anyCategoryquandcategoryIdestnull.TransactionTable.render.test.tsx: reste vert sans modification (markup fermé inchangé), ou mis à jour si la structure fermée change.useAdjustments: si un test de hook existe sur la requête, l'adapter ; sinon un test sur le texte SQL n'apporte rien, la preuve est la lecture./fix-issue(déduites à la lecture) : retournement vers le haut retiré ;is_inputableretiré de la requête (observable seulement si le hook est testé, sinon le dire) ;onExtraSelectdu filtre des transferts ne remet pasnull;compactou le combobox retiré d'une ligne d'ajustement.docs/architecture.mdsi la section composants décritCategoryCombobox; pas d'ADR.Fichiers concernés
src/components/shared/CategoryCombobox.tsx— portail, position, clic extérieursrc/components/shared/placeListbox.ts(nouveau) — calcul de position pursrc/components/adjustments/AdjustmentForm.tsx— combobox par lignesrc/hooks/useAdjustments.ts— requête des catégories (D2)src/components/balance/LinkTransfersModal.tsx— combobox du filtreCHANGELOG.md,CHANGELOG.fr.mdplaceListbox.test.ts,AdjustmentForm.render.test.tsx,LinkTransfersModal.render.test.tsx(nouveaux)Surface de test
CategoryCombobox.test.ts(7 cas,sortHierarchical),TransactionTable.render.test.tsx(markup fermé du combobox),CategoryForm.render.test.tsx,AddKeywordDialog.render.test.tsx,CategoryDetailPanel.render.test.tsx(#423). Aucun sur les trois composants de cette issue.type:feature), mais le rognage est un défaut existant : la preuve est manuelle, faute de jsdom.Options écartées
max-h-[60vh] overflow-y-autodu corps deSplitAdjustmentModal) : le pied du modal sort de l'écran avec beaucoup de lignes, et le tableau Transactions reste rogné. Écartée par D1.Critères d'acceptation
CategoryComboboxn'est plus coupée par le bord d'un conteneur défilant : modal de ventilation (1 et 6 lignes), dernières lignes du tableau Transactions, dialogue mot-clé, filtre des transferts. Elle s'ouvre vers le haut quand l'espace manque en bas.TransactionTable.render.test.tsxvert).npm run buildetnpm testverts ; tests listés présents et mutations rouges (ou déclarées non observables, avec la raison).Complexité estimée
Medium : le portail est le seul morceau non trivial (position, écouteurs, clic extérieur, retournement), les deux remplacements de
<select>suivent #423. Aucune migration, aucun changement Rust.Hors portée
Validation de cycle côté service : #393.
Plan d'exécution — milestone lot-v018 (planifié le 2026-10-16)
Les sections ci-dessous viennent de la séance
/plan-rundu 2026-10-16 (revue/review-specSécurité, Architecture, Technique comprise). En cas de conflit avec le corps au-dessus, elles priment.Consignes communes
v0.17.2existe (git ls-remote --tags origin v0.17.2). Sinon, arrêt sans rien écrire.main065f580; #369, #419 et #421 les ont déplacés depuis. Repérer chaque site par son contenu (symbole, chaîne), jamais par son numéro.STATE.md: le report dansSTATE.mdse fait au/merge-stack.Depends on
Note : dépendance d'ordre de la pile, pas de dépendance de code.
Décisions prises en planification
scrollen capture et surresize), et suit le champ même s'il sort de la zone visible de son conteneur. Elle ne se ferme pas. Le « repositionne ou la ferme » de la tâcheSplitAdjustmentModaldevient « se repositionne ».scrolletresizene vivent que pendantopen, avec un retrait dans le retour duuseEffect, comme lemousedownactuel.TransactionTable.tsx(bouton de répartition verrouillé) : relire le markup fermé épinglé parTransactionTable.render.test.tsxsur la base du maillon avant d'y toucher.Review caveats
Spec source
spec-plan-lot-v018.md
Corps d'origine (écrit en fin de run de #423, avant lecture des fichiers), archivé par le
/analyzedu 2026-10-12. Une imprécision corrigée : le<select>deLinkTransfersModalest le filtre du sélecteur de transactions (« Toutes les catégories »), pas « la catégorie du transfert ».Corps d'origine
Contexte
#423 (PR #428, mergée le 2026-10-12) a remplacé par
CategoryComboboxles deux<select>de catégories du formulaire de catégorie et du dialogue « Nouveau mot-clé ». Deux autres<select>de catégories restent dans l'app, et un rognage de liste identique à celui corrigé dansAddKeywordDialogexiste ailleurs.Constats
src/components/adjustments/AdjustmentForm.tsx:124— choix de catégorie par ligne d'ajustement,<select>natif sans saisie.src/components/balance/LinkTransfersModal.tsx:260— choix de catégorie du transfert,<select>natif, remise ànullpar l'option vide.src/components/transactions/SplitAdjustmentModal.tsx:156— le combobox vit dans un blocmax-h-[60vh] overflow-y-auto: sa listeabsoluteest rognée par le scrollport, le même défaut que celui corrigé dansAddKeywordDialogpar #428 (champs sortis du bloc défilant). Relevé par le plan check de #423.Travail à faire
AdjustmentForm:CategoryComboboxpar ligne (compact), liste des catégories saisissables comme dans Transactions ; vérifier la remise ànullsi elle existe.LinkTransfersModal:CategoryComboboxavec option extra pour « aucune » (modèleTransactionTable.tsx:214-222) ; liste limitée aux catégories de typetransfersi c'est le filtre actuel.SplitAdjustmentModal: sortir les lignes de ventilation du bloc défilant, ou rendre la liste du combobox hors du scrollport.renderToStaticMarkup, fauxtau formatk[...]) :role="combobox"présent,<select>absent, position du combobox avant toutoverflow-y-autopourSplitAdjustmentModal.Hors portée
Validation de cycle côté service : #393.