From 257339911e56515e00fa840e65d1d38db9dde3d0 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 21 May 2026 13:26:33 +0200 Subject: [PATCH] Notes --- AG_GRID_PROGRESS.md | 11 +++++ AG_GRID_SESSION_INDEX.md | 2 + dist/tryton-sao.css | 5 +++ dist/tryton-sao.js | 96 +++++++++++++++++++++++++++++++++++++++- tryton-sao.css | 5 +++ tryton-sao.js | 96 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 213 insertions(+), 2 deletions(-) diff --git a/AG_GRID_PROGRESS.md b/AG_GRID_PROGRESS.md index 65271b4..05a484e 100644 --- a/AG_GRID_PROGRESS.md +++ b/AG_GRID_PROGRESS.md @@ -997,6 +997,17 @@ Point de deploiement: - test obligatoire apres toute retouche layout/scroll: ouvrir une forme Purchase sans AG Grid principale, menu gauche ouvert puis cache; verifier que le formulaire descend jusqu'aux onglets/lignes et que la zone centrale se redimensionne correctement - test complementaire: ouvrir Lots Management ou une vue report AG Grid avec filtres hauts; verifier que le scroll vertical utile reste celui de l'AG Grid et que le conteneur SAO ne cree pas un deuxieme scroll parasite +### Selection globale AG Grid + +- demande UX: retrouver la tick d'en-tete de l'ancien tree SAO pour selectionner/deselectionner toutes les lignes visibles de la AG Grid +- implementation: colonne speciale `__selection__` avec un `headerComponent` custom et une checkbox d'en-tete +- comportement retenu: la tick agit sur les vrais nodes `record` apres filtre AG Grid, et ignore explicitement les lignes synthetiques `group` / `total` +- la checkbox d'en-tete passe en `indeterminate` quand une partie seulement des lignes visibles est selectionnee +- synchronisation: apres un changement de selection, un filtre ou un rebuild de `rowData`, appeler `_sync_selection_header_checkbox()` pour garder l'etat visuel coherent avec `_selected_ids` +- subtilite AG Grid importante: ne pas passer l'instance `Sao.View.AGGridTree` dans `headerComponentParams` ou `filterParams`; l'objet contient des references circulaires et AG Grid peut partir en `too much recursion` en traitant les `columnDefs` +- correctif retenu: creer le composant d'en-tete via une closure (`_get_selection_header_component`) plutot qu'en passant l'instance SAO complete dans les params +- invariant: dans les params AG Grid, passer seulement des primitives, des tableaux/objets simples ou de petites fonctions ciblees; eviter les gros objets SAO (`screen`, `tree`, `view`, widgets jQuery) + ### Commits importants - `fc9ab5d Save SAO AG Grid work` diff --git a/AG_GRID_SESSION_INDEX.md b/AG_GRID_SESSION_INDEX.md index 3432507..b04a65a 100644 --- a/AG_GRID_SESSION_INDEX.md +++ b/AG_GRID_SESSION_INDEX.md @@ -323,12 +323,14 @@ Traces a lire si le sujet revient: - resize colonnes: ne pas lancer le refresh layout global depuis `onColumnResized`; annuler les timers pendant le drag et sauver la largeur avec `skipLayoutRefresh`, sinon `doLayout()` peut annuler la largeur choisie par l'utilisateur - resize colonnes: `_force_horizontal_scroll_width()` ne doit jamais appeler `setColumnWidth()`; calculer le scroll avec `getActualWidth()` pour respecter les largeurs utilisateur - futur tri en mode grouping: autoriser asc/desc/none et le memoriser dans la config de vue, mais ne pas reactiver le tri natif AG Grid sur le `rowData` groupe; capturer l'intention, trier les records source avant `_flatten_grouped_rows()`, puis reconstruire les lignes `group`/`record`/`total` en preservant expansions, totaux et filtres checklist actifs +- composants custom AG Grid: ne jamais passer l'instance SAO complete (`tree`, `screen`, `view`, widgets jQuery) dans `headerComponentParams` ou `filterParams`; ces objets sont circulaires et peuvent provoquer `too much recursion` pendant le traitement des `columnDefs`. Preferer une closure ou de petites fonctions ciblees. ### AG Grid - règles d'or - lire d'abord `AG_GRID_PROGRESS.md > Synthese fin de session AG Grid` avant de toucher aux filtres, vues, rowData, layout ou resize - ne pas confondre: largeur par defaut, largeur minimale, largeur reelle utilisateur, largeur scrollable - ne pas confondre: filtre AG Grid natif, checklist UI, `_sao_value_filters`, `valueFilters` persiste, `rowData` prefiltre +- ne pas passer de gros objets SAO circulaires dans les params AG Grid; preferer closures, primitives et fonctions explicites - toute correction de stabilisation doit etre testee contre une action utilisateur inverse: clear filtre, elargir filtre, reduire colonne, revenir a `Default view` - si une correction semble logique mais ne marche pas, ajouter une trace de transition avant/apres la fonction suspecte; c'est comme ca que `_force_horizontal_scroll_width()` a ete identifie - traces temporaires a supprimer/reduire apres validation finale: `[AGGRID][COLUMN_RESIZE_JSON]` et les logs lifecycle/value filter devenus trop bruyants diff --git a/dist/tryton-sao.css b/dist/tryton-sao.css index ed0f2a7..ac068ac 100644 --- a/dist/tryton-sao.css +++ b/dist/tryton-sao.css @@ -11761,6 +11761,11 @@ html[theme="default"] .ag-grid-tree-grouping-panel input[type="checkbox"]:after padding-left: 6px; } +.ag-grid-tree-host .ag-header-cell[col-id="__selection__"] .ag-header-cell-comp-wrapper { + justify-content: center; + padding-left: 0; +} + .ag-grid-tree-host .ag-selection-checkbox, .ag-grid-tree-host .ag-header-select-all { display: flex; diff --git a/dist/tryton-sao.js b/dist/tryton-sao.js index 29e750c..f8e5a6b 100644 --- a/dist/tryton-sao.js +++ b/dist/tryton-sao.js @@ -24613,7 +24613,10 @@ function eval_pyson(value){ onRowDoubleClicked: this._on_row_double_clicked.bind(this), onSelectionChanged: this._on_selection_changed.bind(this), onSortChanged: this._on_sort_changed.bind(this), - onFilterChanged: this._schedule_state_save.bind(this), + onFilterChanged: () => { + this._schedule_state_save(); + this._sync_selection_header_checkbox(); + }, onColumnMoved: this._on_grid_column_layout_changed.bind(this), onColumnPinned: this._on_grid_column_layout_changed.bind(this), onColumnResized: this._on_grid_column_resized.bind(this), @@ -24715,6 +24718,7 @@ function eval_pyson(value){ sortable: false, resizable: false, filter: false, + headerComponent: this._get_selection_header_component(), cellRenderer: this._create_selection_cell_renderer.bind(this), }); } @@ -24878,6 +24882,94 @@ function eval_pyson(value){ } return checkbox; }, + _get_selection_header_component: function() { + var tree = this; + var SelectionHeader = function() {}; + SelectionHeader.prototype.init = function() { + this.gui = tree._create_selection_header_renderer(); + }; + SelectionHeader.prototype.getGui = function() { + return this.gui; + }; + SelectionHeader.prototype.destroy = function() { + if (tree._selection_header_checkbox === this.gui) { + tree._selection_header_checkbox = null; + } + this.gui = null; + }; + return SelectionHeader; + }, + _create_selection_header_renderer: function() { + if (this.selection_mode != Sao.common.SELECTION_MULTIPLE) { + return document.createTextNode(''); + } + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'ag-custom-checkbox ag-custom-header-checkbox'; + checkbox.title = Sao.i18n.gettext('Select all'); + checkbox.setAttribute('aria-label', Sao.i18n.gettext('Select all')); + checkbox.addEventListener('click', event => { + event.stopPropagation(); + }); + checkbox.addEventListener('change', event => { + event.stopPropagation(); + this._set_visible_records_selection(checkbox.checked); + }); + this._selection_header_checkbox = checkbox; + window.requestAnimationFrame(() => { + this._sync_selection_header_checkbox(); + }); + return checkbox; + }, + _get_selectable_grid_nodes: function() { + var nodes = []; + if (!this.gridApi) { + return nodes; + } + var collect = node => { + if (node && node.data && node.data.__kind == 'record' && + node.data.__record) { + nodes.push(node); + } + }; + if (this.gridApi.forEachNodeAfterFilter) { + this.gridApi.forEachNodeAfterFilter(collect); + } else { + this.gridApi.forEachNode(collect); + } + return nodes; + }, + _set_visible_records_selection: function(value) { + if (this.selection_mode != Sao.common.SELECTION_MULTIPLE || + !this.gridApi) { + return; + } + var nodes = this._get_selectable_grid_nodes(); + this._syncing_grid = true; + try { + nodes.forEach(node => { + node.setSelected(Boolean(value)); + }); + } finally { + this._syncing_grid = false; + } + this._on_selection_changed(); + this._sync_selection_header_checkbox(); + }, + _sync_selection_header_checkbox: function() { + var checkbox = this._selection_header_checkbox; + if (!checkbox || + this.selection_mode != Sao.common.SELECTION_MULTIPLE) { + return; + } + var nodes = this._get_selectable_grid_nodes(); + var selected_count = nodes.filter(node => node.isSelected()).length; + checkbox.disabled = !nodes.length; + checkbox.checked = Boolean( + nodes.length && selected_count == nodes.length); + checkbox.indeterminate = Boolean( + selected_count && selected_count < nodes.length); + }, _get_column_filter: function(column) { if (column instanceof Sao.View.Tree.ButtonColumn) { return false; @@ -26251,6 +26343,7 @@ function eval_pyson(value){ selected_nodes[selected_nodes.length - 1].data.__record : null; this.screen.current_record = current; this._update_copy_state(); + this._sync_selection_header_checkbox(); }, _on_sort_changed: function() { if (this._syncing_grid || !this.gridApi) { @@ -26306,6 +26399,7 @@ function eval_pyson(value){ }); this._syncing_grid = false; this._update_copy_state(); + this._sync_selection_header_checkbox(); }, _sync_sort_state: function() { var column_api = this.gridColumnApi || this.gridApi; diff --git a/tryton-sao.css b/tryton-sao.css index ed0f2a7..ac068ac 100644 --- a/tryton-sao.css +++ b/tryton-sao.css @@ -11761,6 +11761,11 @@ html[theme="default"] .ag-grid-tree-grouping-panel input[type="checkbox"]:after padding-left: 6px; } +.ag-grid-tree-host .ag-header-cell[col-id="__selection__"] .ag-header-cell-comp-wrapper { + justify-content: center; + padding-left: 0; +} + .ag-grid-tree-host .ag-selection-checkbox, .ag-grid-tree-host .ag-header-select-all { display: flex; diff --git a/tryton-sao.js b/tryton-sao.js index 29e750c..f8e5a6b 100644 --- a/tryton-sao.js +++ b/tryton-sao.js @@ -24613,7 +24613,10 @@ function eval_pyson(value){ onRowDoubleClicked: this._on_row_double_clicked.bind(this), onSelectionChanged: this._on_selection_changed.bind(this), onSortChanged: this._on_sort_changed.bind(this), - onFilterChanged: this._schedule_state_save.bind(this), + onFilterChanged: () => { + this._schedule_state_save(); + this._sync_selection_header_checkbox(); + }, onColumnMoved: this._on_grid_column_layout_changed.bind(this), onColumnPinned: this._on_grid_column_layout_changed.bind(this), onColumnResized: this._on_grid_column_resized.bind(this), @@ -24715,6 +24718,7 @@ function eval_pyson(value){ sortable: false, resizable: false, filter: false, + headerComponent: this._get_selection_header_component(), cellRenderer: this._create_selection_cell_renderer.bind(this), }); } @@ -24878,6 +24882,94 @@ function eval_pyson(value){ } return checkbox; }, + _get_selection_header_component: function() { + var tree = this; + var SelectionHeader = function() {}; + SelectionHeader.prototype.init = function() { + this.gui = tree._create_selection_header_renderer(); + }; + SelectionHeader.prototype.getGui = function() { + return this.gui; + }; + SelectionHeader.prototype.destroy = function() { + if (tree._selection_header_checkbox === this.gui) { + tree._selection_header_checkbox = null; + } + this.gui = null; + }; + return SelectionHeader; + }, + _create_selection_header_renderer: function() { + if (this.selection_mode != Sao.common.SELECTION_MULTIPLE) { + return document.createTextNode(''); + } + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'ag-custom-checkbox ag-custom-header-checkbox'; + checkbox.title = Sao.i18n.gettext('Select all'); + checkbox.setAttribute('aria-label', Sao.i18n.gettext('Select all')); + checkbox.addEventListener('click', event => { + event.stopPropagation(); + }); + checkbox.addEventListener('change', event => { + event.stopPropagation(); + this._set_visible_records_selection(checkbox.checked); + }); + this._selection_header_checkbox = checkbox; + window.requestAnimationFrame(() => { + this._sync_selection_header_checkbox(); + }); + return checkbox; + }, + _get_selectable_grid_nodes: function() { + var nodes = []; + if (!this.gridApi) { + return nodes; + } + var collect = node => { + if (node && node.data && node.data.__kind == 'record' && + node.data.__record) { + nodes.push(node); + } + }; + if (this.gridApi.forEachNodeAfterFilter) { + this.gridApi.forEachNodeAfterFilter(collect); + } else { + this.gridApi.forEachNode(collect); + } + return nodes; + }, + _set_visible_records_selection: function(value) { + if (this.selection_mode != Sao.common.SELECTION_MULTIPLE || + !this.gridApi) { + return; + } + var nodes = this._get_selectable_grid_nodes(); + this._syncing_grid = true; + try { + nodes.forEach(node => { + node.setSelected(Boolean(value)); + }); + } finally { + this._syncing_grid = false; + } + this._on_selection_changed(); + this._sync_selection_header_checkbox(); + }, + _sync_selection_header_checkbox: function() { + var checkbox = this._selection_header_checkbox; + if (!checkbox || + this.selection_mode != Sao.common.SELECTION_MULTIPLE) { + return; + } + var nodes = this._get_selectable_grid_nodes(); + var selected_count = nodes.filter(node => node.isSelected()).length; + checkbox.disabled = !nodes.length; + checkbox.checked = Boolean( + nodes.length && selected_count == nodes.length); + checkbox.indeterminate = Boolean( + selected_count && selected_count < nodes.length); + }, _get_column_filter: function(column) { if (column instanceof Sao.View.Tree.ButtonColumn) { return false; @@ -26251,6 +26343,7 @@ function eval_pyson(value){ selected_nodes[selected_nodes.length - 1].data.__record : null; this.screen.current_record = current; this._update_copy_state(); + this._sync_selection_header_checkbox(); }, _on_sort_changed: function() { if (this._syncing_grid || !this.gridApi) { @@ -26306,6 +26399,7 @@ function eval_pyson(value){ }); this._syncing_grid = false; this._update_copy_state(); + this._sync_selection_header_checkbox(); }, _sync_sort_state: function() { var column_api = this.gridColumnApi || this.gridApi;