From c53238638533cdf5081912118d4fcbe5d7d828aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 11:13:39 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Grille=20:=20repeindre=20les=20lignes=20mon?= =?UTF-8?q?t=C3=A9es=20quand=20le=20jeu=20de=20colonnes=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replier le bloc mensuel ne vidait que l'en-tête. Les lignes déjà montées gardaient leurs douze cellules, seules celles virtualisées ensuite adoptaient le nouveau jeu — une grille à deux vitesses, en-tête d'un côté, corps de l'autre. TanStack met ses objets `Row` en cache sur la seule identité des DONNÉES : changer les colonnes ne leur donne aucune référence neuve, et `React.memo` concluait qu'il n'y avait rien à repeindre. Le contournement existait déjà pour `columnVisibility` et `columnSizing`, passés en props sans jamais être lus ; le drapeau du bloc mensuel, lui, n'y figurait pas. `columnVisibility` cède la place à `columnsKey`, la signature des colonnes réellement visibles : elle change pour TOUT changement de colonnes, case décochée dans le menu comme bloc replié, là où l'ancien objet ne voyait que le menu. Calculée une fois par rendu de grille, pas une fois par ligne. Vérifié en rendant la Grille sur données de démonstration : 29 cellules par ligne et 29 en-têtes, 17 et 17 une fois le bloc replié, 29 et 29 au retour. Avec le signal figé, l'en-tête tombe à 17 quand les lignes restent à 29 — le défaut signalé. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR --- src/features/grid/components/heatmap-grid.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 732e426..0af67cb 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -180,15 +180,27 @@ interface VirtualRowProps { row: Row; rowHeight: number; isSelected: boolean; - columnVisibility: Record; + /** Identifiants des colonnes visibles, concaténés — cf. le corps du composant. */ + columnsKey: string; columnSizing: Record; } -const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnVisibility, columnSizing }: VirtualRowProps) => { +const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnsKey, columnSizing }: VirtualRowProps) => { // Uniquement la ligne concernée écoute son propre changement pour l'effet visuel const effectiveGamme = useGridStore((s) => s.draftChanges[row.original.codein] ?? row.original.codeGamme); - void columnVisibility; // Force re-render via React.memo when visibility changes - void columnSizing; // Force re-render via React.memo when resizing columns + // ⚠️ Ces deux props ne sont pas lues : elles n'existent que pour rompre la + // mémoïsation. TanStack met ses objets `Row` en cache sur la seule identité + // des DONNÉES ; changer le jeu de colonnes ne leur donne aucune référence + // neuve. Sans un signal supplémentaire, `React.memo` laisserait les lignes + // déjà montées peindre l'ancien jeu de cellules pendant que l'en-tête, lui, + // affiche le nouveau — seules les lignes fraîchement virtualisées seraient à + // jour, et la grille se retrouverait à deux vitesses. + // + // La signature couvre TOUT changement de colonnes (case décochée dans le menu + // « Colonnes », bloc mensuel replié) là où l'ancien objet `columnVisibility` + // ne voyait que le menu. + void columnsKey; + void columnSizing; // idem, pour le redimensionnement des colonnes return ( s + ((c.columnDef as { size?: number }).size ?? 150), 0); + // Signature du jeu de colonnes affiché : elle sert de signal de re-rendu aux + // lignes virtuelles (cf. `GridRow`). Calculée une fois par rendu de grille, + // pas une fois par ligne. + const columnsKey = visibleColumns.map((c) => c.id).join("|"); if (!isMounted) { return ( @@ -1058,7 +1074,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) { row={row} rowHeight={rowHeight} isSelected={isSelected} - columnVisibility={columnVisibility} + columnsKey={columnsKey} columnSizing={columnSizing} /> From 0a2bc8318b43a2745e95301148eeb1169f09fcd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 19:15:15 +0000 Subject: [PATCH 2/2] Grille : remonter le corps du tableau au changement de colonnes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le repli du bloc mensuel ne repeignait toujours pas les lignes déjà montées : l'en-tête perdait ses douze colonnes, le corps les gardait, et il fallait recharger la page pour retrouver une grille cohérente. Le signal passé aux lignes (`columnsKey`) reposait sur la comparaison de props de `React.memo`. La clé du `` porte désormais cette même signature : un changement de colonnes remonte le corps, sans plus dépendre d'une comparaison qu'une prop oubliée suffit à mettre en défaut. Le mécanisme existait déjà pour la densité d'affichage. Le coût est celui des seules lignes virtualisées (une trentaine), et le redimensionnement d'une colonne ne déclenche rien : les identifiants de colonnes, eux, ne changent pas. Vérifié cette fois sur un BUILD DE PRODUCTION, 2 000 lignes : au repli, 17 en-têtes et 17 cellules pour chaque ligne montée ; toujours 17 après défilement vers des lignes virtualisées ensuite ; 29 partout au retour, et 17 au repli suivant — sans rechargement. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR --- src/features/grid/components/heatmap-grid.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 0af67cb..c48bcb4 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -1062,7 +1062,17 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) { ))} - + {/* + * La clé porte AUSSI la signature des colonnes : changer de jeu de + * colonnes remonte le corps du tableau au lieu de compter sur la + * comparaison de props de `React.memo`. Une ligne mémoïsée qui rate + * le signal continue de peindre l'ancien jeu de cellules, et la + * grille se retrouve à deux vitesses — en-tête à jour, corps en + * retard. Un remontage ne coûte que les ~30 lignes virtualisées, et + * le redimensionnement (continu) ne le déclenche pas : les + * identifiants de colonnes ne changent pas. + */} + {rowVirtualizer.getVirtualItems().map((virtualRow) => { const row = tableRows[virtualRow.index]; const isSelected = row.getIsSelected();