diff --git a/demos/aurelia/src/examples/slickgrid/example27.html b/demos/aurelia/src/examples/slickgrid/example27.html index daf3dcea5..6364943bb 100644 --- a/demos/aurelia/src/examples/slickgrid/example27.html +++ b/demos/aurelia/src/examples/slickgrid/example27.html @@ -105,6 +105,35 @@

Log Hierarchical Structure +
+
+ + + +
+

diff --git a/demos/aurelia/src/examples/slickgrid/example27.scss b/demos/aurelia/src/examples/slickgrid/example27.scss index e4075e670..b8aef4aee 100644 --- a/demos/aurelia/src/examples/slickgrid/example27.scss +++ b/demos/aurelia/src/examples/slickgrid/example27.scss @@ -8,3 +8,10 @@ gap: 4px; } } + +/* limit the demo maxVisibleDepth input size */ +#maxVisibleDepthInput { + height: 22px; + width: 100%; + max-width: 150px; +} diff --git a/demos/aurelia/src/examples/slickgrid/example27.ts b/demos/aurelia/src/examples/slickgrid/example27.ts index 0d055ec0c..61316b37f 100644 --- a/demos/aurelia/src/examples/slickgrid/example27.ts +++ b/demos/aurelia/src/examples/slickgrid/example27.ts @@ -121,6 +121,9 @@ export class Example27 { indentMarginLeft: 15, initiallyCollapsed: true, + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden from the grid display (but not removed from the dataset) + // maxVisibleDepth: 2, + // you can optionally sort by a different column and/or sort direction // this is the recommend approach, unless you are 100% that your original array is already sorted (in most cases it's not) // initialSort: { @@ -379,4 +382,18 @@ export class Example27 { document.querySelector('.subtitle')?.classList[action]('hidden'); this.aureliaGrid.resizerService.resizeGrid(0); } + + setMaxVisibleDepthFromInput() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (!input) return; + const value = parseInt(input.value, 10); + const maxVisibleDepth = Number.isFinite(value) ? value : undefined; + this.aureliaGrid.treeDataService.setMaxVisibleDepth(maxVisibleDepth as number | undefined); + } + + clearMaxVisibleDepth() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (input) input.value = ''; + this.aureliaGrid.treeDataService.clearMaxVisibleDepth(); + } } diff --git a/demos/aurelia/test/cypress/e2e/example27.cy.ts b/demos/aurelia/test/cypress/e2e/example27.cy.ts index 0959df93d..8b7674972 100644 --- a/demos/aurelia/test/cypress/e2e/example27.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example27.cy.ts @@ -234,6 +234,48 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' }); }); + it('deterministic: hide Task 1 children with maxVisibleDepth=1 and restore on clear', () => { + // Task 1 was just expanded by the previous test; now ensure children exist + cy.get('[data-row="1"] > .slick-cell:nth(0) .slick-tree-title').should('contain', 'Task 1'); + + // ensure level-2 children exist; if not, expand Task 1 and re-check + cy.get('.slick-tree-title[level=2]') + .its('length') + .then((len) => { + if (len === 0) { + cy.get( + `.grid5 [style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(0) .slick-group-toggle.collapsed` + ).click({ force: true }); + cy.get('.grid5').find('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + } else { + expect(len).to.be.greaterThan(0); + } + }); + + // apply maxVisibleDepth=1 + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // children (level=2) should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + + // clear the maxVisibleDepth and expect children to reappear + cy.get('[data-test=clear-max-visible-depth-btn]').click(); + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + }); + + it('should set max visible depth via demo input and hide deeper nodes', () => { + // ensure there are level-2 items before applying maxVisibleDepth + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + + // set max visible depth to 1 and apply + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // after applying, level-2 items should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + }); + it('should be able to click on the "Collapse All (wihout event)" button', () => { cy.get('[data-test=collapse-all-noevent-btn]').contains('Collapse All (without triggering event)').click(); }); diff --git a/demos/react/src/examples/slickgrid/Example27.tsx b/demos/react/src/examples/slickgrid/Example27.tsx index dd29a2f19..300125b29 100644 --- a/demos/react/src/examples/slickgrid/Example27.tsx +++ b/demos/react/src/examples/slickgrid/Example27.tsx @@ -126,6 +126,9 @@ const Example27: React.FC = () => { indentMarginLeft: 15, initiallyCollapsed: true, + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden from the grid display (but not removed from the dataset) + // maxVisibleDepth: 2, + // you can optionally sort by a different column and/or sort direction // this is the recommend approach, unless you are 100% that your original array is already sorted (in most cases it's not) // initialSort: { @@ -347,6 +350,20 @@ const Example27: React.FC = () => { } } + function setMaxVisibleDepthFromInput() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (!input) return; + const value = parseInt(input.value, 10); + const maxVisibleDepth = Number.isFinite(value) ? value : undefined; + reactGridRef.current?.treeDataService.setMaxVisibleDepth(maxVisibleDepth as number | undefined); + } + + function clearMaxVisibleDepth() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (input) input.value = ''; + reactGridRef.current?.treeDataService.clearMaxVisibleDepth(); + } + function reapplyToggledItems() { reactGridRef.current?.treeDataService.applyToggledItemStateChanges(treeToggleItems); } @@ -471,6 +488,35 @@ const Example27: React.FC = () => { +
+
+ + + +
+
diff --git a/demos/react/src/examples/slickgrid/example27.scss b/demos/react/src/examples/slickgrid/example27.scss index 80ffeb658..df012e5f2 100644 --- a/demos/react/src/examples/slickgrid/example27.scss +++ b/demos/react/src/examples/slickgrid/example27.scss @@ -1,9 +1,16 @@ // @use '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-material.lite.scss'; .icon { - align-items: center; - display: inline-flex; - justify-content: center; - height: 1.5rem; - width: 1.5rem; + align-items: center; + display: inline-flex; + justify-content: center; + height: 1.5rem; + width: 1.5rem; +} + +/* limit the demo maxVisibleDepth input size */ +#maxVisibleDepthInput { + height: 22px; + width: 100%; + max-width: 150px; } diff --git a/demos/react/test/cypress/e2e/example27.cy.ts b/demos/react/test/cypress/e2e/example27.cy.ts index df3d2fd77..74b475087 100644 --- a/demos/react/test/cypress/e2e/example27.cy.ts +++ b/demos/react/test/cypress/e2e/example27.cy.ts @@ -231,6 +231,48 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' }); }); + it('deterministic: hide Task 1 children with maxVisibleDepth=1 and restore on clear', () => { + // Task 1 was just expanded by the previous test; now ensure children exist + cy.get('[data-row="1"] > .slick-cell:nth(0) .slick-tree-title').should('contain', 'Task 1'); + + // ensure level-2 children exist; if not, expand Task 1 and re-check + cy.get('.slick-tree-title[level=2]') + .its('length') + .then((len) => { + if (len === 0) { + cy.get( + `.grid5 [style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(0) .slick-group-toggle.collapsed` + ).click({ force: true }); + cy.get('.grid5').find('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + } else { + expect(len).to.be.greaterThan(0); + } + }); + + // apply maxVisibleDepth=1 + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // children (level=2) should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + + // clear the maxVisibleDepth and expect children to reappear + cy.get('[data-test=clear-max-visible-depth-btn]').click(); + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + }); + + it('should set max visible depth via demo input and hide deeper nodes', () => { + // ensure there are level-2 items before applying maxVisibleDepth + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + + // set max visible depth to 1 and apply + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // after applying, level-2 items should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + }); + it('should be able to click on the "Collapse All (wihout event)" button', () => { cy.get('[data-test=collapse-all-noevent-btn]').contains('Collapse All (without triggering event)').click(); }); diff --git a/demos/vanilla/src/examples/example05.html b/demos/vanilla/src/examples/example05.html index 134ad3f0e..d89855b09 100644 --- a/demos/vanilla/src/examples/example05.html +++ b/demos/vanilla/src/examples/example05.html @@ -73,6 +73,25 @@

Log Hierarchical Structure +
+
+

+ +

+

+ +

+

+ +

+
+
diff --git a/demos/vanilla/src/examples/example05.scss b/demos/vanilla/src/examples/example05.scss index 114dffad2..82095d1b3 100644 --- a/demos/vanilla/src/examples/example05.scss +++ b/demos/vanilla/src/examples/example05.scss @@ -5,4 +5,10 @@ .slick-cell { column-gap: 4px; } -} \ No newline at end of file +} + +/* limit the demo maxVisibleDepth input size */ +#maxVisibleDepthInput { + width: 100%; + max-width: 150px; +} diff --git a/demos/vanilla/src/examples/example05.ts b/demos/vanilla/src/examples/example05.ts index 71064e57e..964e21898 100644 --- a/demos/vanilla/src/examples/example05.ts +++ b/demos/vanilla/src/examples/example05.ts @@ -280,6 +280,9 @@ export default class Example05 { indentMarginLeft: 15, initiallyCollapsed: true, + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden from the grid display (but not removed from the dataset) + // maxVisibleDepth: 2, + // you can optionally sort by a different column and/or sort direction // this is the recommend approach, unless you are 100% that your original array is already sorted (in most cases it's not) // initialSort: { @@ -398,6 +401,20 @@ export default class Example05 { this.sgb.filterService.updateFilters([{ columnId: 'percentComplete', operator: '<', searchTerms: [40] }]); } + setMaxVisibleDepthFromInput() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (!input) return; + const value = parseInt(input.value, 10); + const maxVisibleDepth = Number.isFinite(value) ? value : undefined; + this.sgb.treeDataService.setMaxVisibleDepth(maxVisibleDepth as number | undefined); + } + + clearMaxVisibleDepth() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (input) input.value = ''; + this.sgb.treeDataService.clearMaxVisibleDepth(); + } + logHierarchicalStructure() { console.log('hierarchical array', this.sgb.treeDataService.datasetHierarchical); } diff --git a/demos/vue/src/components/Example27.vue b/demos/vue/src/components/Example27.vue index 445a955ae..d7c886a9e 100644 --- a/demos/vue/src/components/Example27.vue +++ b/demos/vue/src/components/Example27.vue @@ -123,6 +123,9 @@ function defineGrid() { indentMarginLeft: 15, initiallyCollapsed: true, + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden from the grid display (but not removed from the dataset) + // maxVisibleDepth: 2, + // you can optionally sort by a different column and/or sort direction // this is the recommend approach, unless you are 100% that your original array is already sorted (in most cases it's not) // initialSort: { @@ -373,6 +376,20 @@ function reapplyToggledItems() { vueGrid.treeDataService.applyToggledItemStateChanges(treeToggleItems.value); } +function setMaxVisibleDepthFromInput() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (!input) return; + const value = parseInt(input.value, 10); + const maxVisibleDepth = Number.isFinite(value) ? value : undefined; + vueGrid.treeDataService.setMaxVisibleDepth(maxVisibleDepth as number | undefined); +} + +function clearMaxVisibleDepth() { + const input = document.querySelector('#maxVisibleDepthInput') as HTMLInputElement; + if (input) input.value = ''; + vueGrid.treeDataService.clearMaxVisibleDepth(); +} + function toggleSubTitle() { showSubTitle.value = !showSubTitle.value; const action = showSubTitle.value ? 'remove' : 'add'; @@ -487,6 +504,35 @@ function vueGridReady(grid: SlickgridVueInstance) { Log Hierarchical Structure +
+
+ + + +
+

@@ -519,4 +565,11 @@ function vueGridReady(grid: SlickgridVueInstance) { gap: 4px; } } + +/* limit the demo maxVisibleDepth input size */ +#maxVisibleDepthInput { + height: 22px; + width: 100%; + max-width: 150px; +} diff --git a/demos/vue/test/cypress/e2e/example27.cy.ts b/demos/vue/test/cypress/e2e/example27.cy.ts index 0959df93d..8b7674972 100644 --- a/demos/vue/test/cypress/e2e/example27.cy.ts +++ b/demos/vue/test/cypress/e2e/example27.cy.ts @@ -234,6 +234,48 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' }); }); + it('deterministic: hide Task 1 children with maxVisibleDepth=1 and restore on clear', () => { + // Task 1 was just expanded by the previous test; now ensure children exist + cy.get('[data-row="1"] > .slick-cell:nth(0) .slick-tree-title').should('contain', 'Task 1'); + + // ensure level-2 children exist; if not, expand Task 1 and re-check + cy.get('.slick-tree-title[level=2]') + .its('length') + .then((len) => { + if (len === 0) { + cy.get( + `.grid5 [style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(0) .slick-group-toggle.collapsed` + ).click({ force: true }); + cy.get('.grid5').find('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + } else { + expect(len).to.be.greaterThan(0); + } + }); + + // apply maxVisibleDepth=1 + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // children (level=2) should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + + // clear the maxVisibleDepth and expect children to reappear + cy.get('[data-test=clear-max-visible-depth-btn]').click(); + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + }); + + it('should set max visible depth via demo input and hide deeper nodes', () => { + // ensure there are level-2 items before applying maxVisibleDepth + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + + // set max visible depth to 1 and apply + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // after applying, level-2 items should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + }); + it('should be able to click on the "Collapse All (wihout event)" button', () => { cy.get('[data-test=collapse-all-noevent-btn]').contains('Collapse All (without triggering event)').click(); }); diff --git a/docs/grid-functionalities/tree-data-grid.md b/docs/grid-functionalities/tree-data-grid.md index 5e8a2ed4b..dec235059 100644 --- a/docs/grid-functionalities/tree-data-grid.md +++ b/docs/grid-functionalities/tree-data-grid.md @@ -10,6 +10,7 @@ - [`autoApproveParentItemWhenTreeColumnIsValid`](#autoapproveparentitemwhentreecolumnisvalid-boolean-option) - [Tree Data Service Methods](#tree-data-service-methods) - extra methods to work with Tree Data - `getItemCount(x)`, `getToggledItems()`, `getCurrentToggleState()`, `dynamicallyToggleItemState(x)`, `applyToggledItemStateChanges(x)`, ... +- [Max Visible Depth](#max-visible-depth) - [Tree Totals with Aggregators](#tree-totals-with-aggregators) - [Tree Totals Formatter](#tree-totals-formatter) - [Lazy Loading Tree Data](#lazy-loading-tree-data) @@ -268,6 +269,37 @@ export class Example1 { } ``` +### Max Visible Depth + +You can limit how deep tree nodes are visible in the grid without removing them from the dataset by using the `treeDataOptions.maxVisibleDepth` option. When defined, any row whose tree level property is greater than the provided value will be hidden by the tree filter logic. + +Example grid option (static): +```ts +this.gridOptions = { + treeDataOptions: { + columnId: 'title', + levelPropName: 'treeLevel', + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden + // maxVisibleDepth: 2, + } +}; +``` + +Runtime API (preferred): use the `TreeDataService` convenience methods to set or clear the value at runtime. This avoids relying on nested `setOptions()` merges and ensures the change is applied consistently across the grid and demos. + +Example usage: +```ts +// set maximum visible tree depth to 1 +this.sgb.treeDataService.setMaxVisibleDepth(1); + +// clear the runtime limit (revert to unlimited depth) +this.sgb.treeDataService.clearMaxVisibleDepth(); +``` + +Notes: +- The option only hides rows from the visual grid (filtering), it does not remove them from the underlying dataset. +- When reading or updating runtime options in code, prefer the `TreeDataService` methods above rather than mutating nested option objects directly. + ### Tree Totals with Aggregators ##### requires `v3.2.0` or higher diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/tree-data-grid.md b/frameworks/angular-slickgrid/docs/grid-functionalities/tree-data-grid.md index 641cf17ba..2a5e4b9a9 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/tree-data-grid.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/tree-data-grid.md @@ -10,6 +10,7 @@ - [`autoApproveParentItemWhenTreeColumnIsValid`](#autoapproveparentitemwhentreecolumnisvalid-boolean-option) - [Tree Data Service Methods](#tree-data-service-methods) - extra methods to work with Tree Data - `getItemCount(x)`, `getToggledItems()`, `getCurrentToggleState()`, `dynamicallyToggleItemState(x)`, `applyToggledItemStateChanges(x)`, ... +- [Max Visible Depth](#max-visible-depth) - [Tree Totals with Aggregators](#tree-totals-with-aggregators) - [Tree Totals Formatter](#tree-totals-formatter) - [Lazy Loading Tree Data](#lazy-loading-tree-data) @@ -296,6 +297,37 @@ export class Example1 { } ``` +### Max Visible Depth + +You can limit how deep tree nodes are visible in the grid without removing them from the dataset by using the `treeDataOptions.maxVisibleDepth` option. When defined, any row whose tree level property is greater than the provided value will be hidden by the tree filter logic. + +Example grid option (static): +```ts +this.gridOptions = { + treeDataOptions: { + columnId: 'title', + levelPropName: 'treeLevel', + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden + // maxVisibleDepth: 2, + } +}; +``` + +Runtime API (Angular): use the `treeDataService` on the grid instance to set or clear the value at runtime. + +Example usage: +```ts +// set maximum visible tree depth to 1 +this.angularGrid.treeDataService.setMaxVisibleDepth(1); + +// clear the runtime limit (revert to unlimited depth) +this.angularGrid.treeDataService.clearMaxVisibleDepth(); +``` + +Notes: +- The option only hides rows from the visual grid (filtering), it does not remove them from the underlying dataset. +- When reading or updating runtime options in code, prefer the `TreeDataService` methods rather than mutating nested option objects directly. + ### Tree Totals with Aggregators You can calculate Tree Totals by adding Aggregators to your `treeDataOptions` configuration in your grid options. The Aggregators are the same ones that can be used for both Tree Data and/or Grouping usage (they were modified internally to work for both use case). This feature also comes with other options that you can choose to enable or not, below is a list of these extra options that can be configured diff --git a/frameworks/angular-slickgrid/src/demos/examples/example27.component.html b/frameworks/angular-slickgrid/src/demos/examples/example27.component.html index 366c0bfe2..3cd729230 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example27.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example27.component.html @@ -115,6 +115,35 @@

Dynamically Change Filter (% complete < 40) +
+
+ + + +
+

diff --git a/frameworks/angular-slickgrid/src/demos/examples/example27.component.scss b/frameworks/angular-slickgrid/src/demos/examples/example27.component.scss index e227c93ef..f7a9273e4 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example27.component.scss +++ b/frameworks/angular-slickgrid/src/demos/examples/example27.component.scss @@ -8,3 +8,10 @@ gap: 4px; } } + +/* limit the demo maxVisibleDepth input size */ +#maxVisibleDepthInput { + height: 22px; + width: 100%; + max-width: 150px; +} diff --git a/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts index db9691a41..efe6d45ac 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts @@ -138,6 +138,9 @@ export class Example27Component implements OnInit { indentMarginLeft: 15, initiallyCollapsed: true, + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden from the grid display (but not removed from the dataset) + // maxVisibleDepth: 2, + // you can optionally sort by a different column and/or sort direction // this is the recommend approach, unless you are 100% that your original array is already sorted (in most cases it's not) initialSort: { @@ -328,18 +331,24 @@ export class Example27Component implements OnInit { /** Whenever a parent is being toggled, we'll keep a reference of all of these changes so that we can reapply them whenever we want */ handleOnTreeItemToggled(treeToggleExecution: TreeToggleStateChange) { - this.hasNoExpandCollapseChanged = false; - this.treeToggleItems = treeToggleExecution.toggledItems as TreeToggledItem[]; + // defer to microtask to avoid ExpressionChangedAfterItHasBeenCheckedError in Angular + queueMicrotask(() => { + this.hasNoExpandCollapseChanged = false; + this.treeToggleItems = treeToggleExecution.toggledItems as TreeToggledItem[]; + }); console.log('Tree Data changes', treeToggleExecution); } handleOnGridStateChanged(gridStateChange: GridStateChange) { - this.hasNoExpandCollapseChanged = false; + // defer state updates to microtask to avoid ExpressionChangedAfterItHasBeenCheckedError + queueMicrotask(() => { + this.hasNoExpandCollapseChanged = false; - if (gridStateChange?.change?.type === 'treeData') { - console.log('Tree Data gridStateChange', gridStateChange?.gridState?.treeData); - this.treeToggleItems = gridStateChange?.gridState?.treeData?.toggledItems as TreeToggledItem[]; - } + if (gridStateChange?.change?.type === 'treeData') { + console.log('Tree Data gridStateChange', gridStateChange?.gridState?.treeData); + this.treeToggleItems = gridStateChange?.gridState?.treeData?.toggledItems as TreeToggledItem[]; + } + }); } logTreeDataToggledItems() { @@ -372,4 +381,18 @@ export class Example27Component implements OnInit { document.querySelector('.subtitle')?.classList[action]('hidden'); this.angularGrid.resizerService.resizeGrid(0); } + + setMaxVisibleDepthFromInput() { + const input = document.getElementById('maxVisibleDepthInput') as HTMLInputElement; + if (!input) return; + const value = parseInt(input.value, 10); + const maxVisibleDepth = Number.isFinite(value) ? value : undefined; + this.angularGrid.treeDataService.setMaxVisibleDepth(maxVisibleDepth as number | undefined); + } + + clearMaxVisibleDepth() { + const input = document.getElementById('maxVisibleDepthInput') as HTMLInputElement; + if (input) input.value = ''; + this.angularGrid.treeDataService.clearMaxVisibleDepth(); + } } diff --git a/frameworks/angular-slickgrid/test/cypress/e2e/example27.cy.ts b/frameworks/angular-slickgrid/test/cypress/e2e/example27.cy.ts index fbdda5c62..afab0ccc9 100644 --- a/frameworks/angular-slickgrid/test/cypress/e2e/example27.cy.ts +++ b/frameworks/angular-slickgrid/test/cypress/e2e/example27.cy.ts @@ -229,6 +229,48 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' }); }); + it('deterministic: hide Task 1 children with maxVisibleDepth=1 and restore on clear', () => { + // Task 1 was just expanded by the previous test; now ensure children exist + cy.get('[data-row="1"] > .slick-cell:nth(0) .slick-tree-title').should('contain', 'Task 1'); + + // ensure level-2 children exist; if not, expand Task 1 and re-check + cy.get('.slick-tree-title[level=2]') + .its('length') + .then((len) => { + if (len === 0) { + cy.get( + `.grid5 [style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(0) .slick-group-toggle.collapsed` + ).click({ force: true }); + cy.get('.grid5').find('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + } else { + expect(len).to.be.greaterThan(0); + } + }); + + // apply maxVisibleDepth=1 + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // children (level=2) should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + + // clear the maxVisibleDepth and expect children to reappear + cy.get('[data-test=clear-max-visible-depth-btn]').click(); + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + }); + + it('should set max visible depth via demo input and hide deeper nodes', () => { + // ensure there are level-2 items before applying maxVisibleDepth + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + + // set max visible depth to 1 and apply + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // after applying, level-2 items should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + }); + it('should be able to click on the "Collapse All (wihout event)" button', () => { cy.get('[data-test=collapse-all-noevent-btn]').contains('Collapse All (without triggering event)').click(); }); diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/tree-data-grid.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/tree-data-grid.md index 10f36bd58..c966d7bdd 100644 --- a/frameworks/aurelia-slickgrid/docs/grid-functionalities/tree-data-grid.md +++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/tree-data-grid.md @@ -10,6 +10,7 @@ - [`autoApproveParentItemWhenTreeColumnIsValid`](#autoapproveparentitemwhentreecolumnisvalid-boolean-option) - [Tree Data Service Methods](#tree-data-service-methods) - extra methods to work with Tree Data - `getItemCount(x)`, `getToggledItems()`, `getCurrentToggleState()`, `dynamicallyToggleItemState(x)`, `applyToggledItemStateChanges(x)`, ... +- [Max Visible Depth](#max-visible-depth) - [Tree Totals with Aggregators](#tree-totals-with-aggregators) - [Tree Totals Formatter](#tree-totals-formatter) - [Lazy Loading Tree Data](#lazy-loading-tree-data) @@ -309,6 +310,37 @@ export class Example1 { } ``` +### Max Visible Depth + +You can limit how deep tree nodes are visible in the grid without removing them from the dataset by using the `treeDataOptions.maxVisibleDepth` option. When defined, any row whose tree level property is greater than the provided value will be hidden by the tree filter logic. + +Example grid option (static): +```ts +this.gridOptions = { + treeDataOptions: { + columnId: 'title', + levelPropName: 'treeLevel', + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden + // maxVisibleDepth: 2, + } +}; +``` + +Runtime API (Aurelia): use the `treeDataService` on the grid instance to set or clear the value at runtime. + +Example usage: +```ts +// set maximum visible tree depth to 1 +this.aureliaGrid.treeDataService.setMaxVisibleDepth(1); + +// clear the runtime limit (revert to unlimited depth) +this.aureliaGrid.treeDataService.clearMaxVisibleDepth(); +``` + +Notes: +- The option only hides rows from the visual grid (filtering), it does not remove them from the underlying dataset. +- When reading or updating runtime options in code, prefer the `TreeDataService` methods rather than mutating nested option objects directly. + ### Tree Totals with Aggregators You can calculate Tree Totals by adding Aggregators to your `treeDataOptions` configuration in your grid options. The Aggregators are the same ones that can be used for both Tree Data and/or Grouping usage (they were modified internally to work for both use case). This feature also comes with other options that you can choose to enable or not, below is a list of these extra options that can be configured diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/tree-data-grid.md b/frameworks/slickgrid-react/docs/grid-functionalities/tree-data-grid.md index 218ceb69d..ac9b0a80c 100644 --- a/frameworks/slickgrid-react/docs/grid-functionalities/tree-data-grid.md +++ b/frameworks/slickgrid-react/docs/grid-functionalities/tree-data-grid.md @@ -10,6 +10,7 @@ - [`autoApproveParentItemWhenTreeColumnIsValid`](#autoapproveparentitemwhentreecolumnisvalid-boolean-option) - [Tree Data Service Methods](#tree-data-service-methods) - extra methods to work with Tree Data - `getItemCount(x)`, `getToggledItems()`, `getCurrentToggleState()`, `dynamicallyToggleItemState(x)`, `applyToggledItemStateChanges(x)`, ... +- [Max Visible Depth](#max-visible-depth) - [Tree Totals with Aggregators](#tree-totals-with-aggregators) - [Tree Totals Formatter](#tree-totals-formatter) - [Lazy Loading Tree Data](#lazy-loading-tree-data) @@ -314,6 +315,37 @@ const Example: React.FC = () => { } ``` +### Max Visible Depth + +You can limit how deep tree nodes are visible in the grid without removing them from the dataset by using the `treeDataOptions.maxVisibleDepth` option. When defined, any row whose tree level property is greater than the provided value will be hidden by the tree filter logic. + +Example grid option (static): +```tsx +const gridOptions: GridOption = { + treeDataOptions: { + columnId: 'title', + levelPropName: 'treeLevel', + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden + // maxVisibleDepth: 2, + } +}; +``` + +Runtime API (React): use the `treeDataService` on the grid instance to set or clear the value at runtime. + +Example usage: +```tsx +// set maximum visible tree depth to 1 +reactGridRef.current?.treeDataService.setMaxVisibleDepth(1); + +// clear the runtime limit (revert to unlimited depth) +reactGridRef.current?.treeDataService.clearMaxVisibleDepth(); +``` + +Notes: +- The option only hides rows from the visual grid (filtering), it does not remove them from the underlying dataset. +- When reading or updating runtime options in code, prefer the `TreeDataService` methods rather than mutating nested option objects directly. + ### Tree Totals with Aggregators You can calculate Tree Totals by adding Aggregators to your `treeDataOptions` configuration in your grid options. The Aggregators are the same ones that can be used for both Tree Data and/or Grouping usage (they were modified internally to work for both use case). This feature also comes with other options that you can choose to enable or not, below is a list of these extra options that can be configured diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/tree-data-grid.md b/frameworks/slickgrid-vue/docs/grid-functionalities/tree-data-grid.md index 699e86fb2..e4aea67f5 100644 --- a/frameworks/slickgrid-vue/docs/grid-functionalities/tree-data-grid.md +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/tree-data-grid.md @@ -10,6 +10,7 @@ - [`autoApproveParentItemWhenTreeColumnIsValid`](#autoapproveparentitemwhentreecolumnisvalid-boolean-option) - [Tree Data Service Methods](#tree-data-service-methods) - extra methods to work with Tree Data - `getItemCount(x)`, `getToggledItems()`, `getCurrentToggleState()`, `dynamicallyToggleItemState(x)`, `applyToggledItemStateChanges(x)`, ... +- [Max Visible Depth](#max-visible-depth) - [Tree Totals with Aggregators](#tree-totals-with-aggregators) - [Tree Totals Formatter](#tree-totals-formatter) - [Lazy Loading Tree Data](#lazy-loading-tree-data) @@ -325,6 +326,37 @@ function getTreeDataState() { console.log(vueGrid.getCurrentToggleState()); } + +### Max Visible Depth + +You can limit how deep tree nodes are visible in the grid without removing them from the dataset by using the `treeDataOptions.maxVisibleDepth` option. When defined, any row whose tree level property is greater than the provided value will be hidden by the tree filter logic. + +Example grid option (static): +```vue +gridOptions.value = { + treeDataOptions: { + columnId: 'title', + levelPropName: 'treeLevel', + // when `maxVisibleDepth` is defined, any tree node with a level greater than this number will be hidden + // maxVisibleDepth: 2, + } +}; +``` + +Runtime API (Vue): use the `treeDataService` on the grid instance to set or clear the value at runtime. + +Example usage: +```ts +// set maximum visible tree depth to 1 +vueGrid.treeDataService.setMaxVisibleDepth(1); + +// clear the runtime limit (revert to unlimited depth) +vueGrid.treeDataService.clearMaxVisibleDepth(); +``` + +Notes: +- The option only hides rows from the visual grid (filtering), it does not remove them from the underlying dataset. +- When reading or updating runtime options in code, prefer the `TreeDataService` methods rather than mutating nested option objects directly. ``` ### Tree Totals with Aggregators diff --git a/packages/common/src/interfaces/treeDataOption.interface.ts b/packages/common/src/interfaces/treeDataOption.interface.ts index b12326ff7..62a40b5de 100644 --- a/packages/common/src/interfaces/treeDataOption.interface.ts +++ b/packages/common/src/interfaces/treeDataOption.interface.ts @@ -78,6 +78,13 @@ export interface TreeDataOption extends TreeDataPropNames { /** Optional Title Formatter (allows you to format/style the title text differently) */ titleFormatter?: Formatter; + /** + * Optional maximum visible tree depth. When provided, any tree node with a level + * greater than this number will be hidden from the grid display (but not removed + * from the dataset). Depths are zero-based (0 = root level). + */ + maxVisibleDepth?: number; + /** Defaults to False, should we toggle the tree node when clicking on the tree node title or toggle icon (tree node must have element with `.slick-tree-title` class name) */ toggleOnNodeTitle?: boolean; diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index 2d7438682..d2d27653f 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -139,6 +139,7 @@ describe('FilterService', () => { service = new FilterService(filterFactory, pubSubServiceStub, sharedService, backendUtilityService, rxjsResourceStub); slickgridEventHandler = service.eventHandler; vi.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(div); + vi.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionMock); }); afterEach(() => { @@ -1213,6 +1214,33 @@ describe('FilterService', () => { expect(output).toBe(true); }); + it('should hide items deeper than treeDataOptions.maxVisibleDepth when tree data is enabled', () => { + // save previous values to avoid leaking state to other tests + const prevEnableTree = gridOptionMock.enableTreeData; + const prevTreeOptions = gridOptionMock.treeDataOptions; + + try { + // enable tree data with maxVisibleDepth = 1 + gridOptionMock.enableTreeData = true; + gridOptionMock.treeDataOptions = { columnId: 'file', levelPropName: '__treeLevel', maxVisibleDepth: 1 } as any; + + const deepItem = { id: 10, __treeLevel: 2, __parentId: 1 } as any; + const shallowItem = { id: 11, __treeLevel: 1, __parentId: 1 } as any; + + service.init(gridStub); + + const deepOutput = service.customLocalFilter(deepItem, { dataView: dataViewStub, grid: gridStub, columnFilters: {} as any }); + const shallowOutput = service.customLocalFilter(shallowItem, { dataView: dataViewStub, grid: gridStub, columnFilters: {} as any }); + + expect(deepOutput).toBe(false); + expect(shallowOutput).toBe(true); + } finally { + // restore previous state + gridOptionMock.enableTreeData = prevEnableTree; + gridOptionMock.treeDataOptions = prevTreeOptions; + } + }); + it('should return True when using row detail and the item is found in its parent', () => { gridOptionMock.enableRowDetailView = true; const mockColumn1 = { id: 'zip', field: 'zip', filterable: true, queryFieldFilter: 'address.zip', type: 'number' } as Column; diff --git a/packages/common/src/services/__tests__/treeData.service.spec.ts b/packages/common/src/services/__tests__/treeData.service.spec.ts index f9f5e5650..e1573671e 100644 --- a/packages/common/src/services/__tests__/treeData.service.spec.ts +++ b/packages/common/src/services/__tests__/treeData.service.spec.ts @@ -68,6 +68,7 @@ const gridStub = { onClick: new SlickEvent(), onKeyDown: new SlickEvent(), render: vi.fn(), + setOptions: vi.fn(), setSortColumns: vi.fn(), } as unknown as SlickGrid; @@ -212,6 +213,26 @@ describe('TreeData Service', () => { expect(service.datasetHierarchical).toEqual(mockHierarchical); }); + it('setMaxVisibleDepth should update sharedService.gridOptions.treeDataOptions and refresh filters', () => { + // prepare shared options + sharedService.gridOptions = { treeDataOptions: { columnId: 'file' } } as any; + + service.setMaxVisibleDepth(2); + + expect(sharedService.gridOptions.treeDataOptions.maxVisibleDepth).toBe(2); + expect(filterServiceStub.refreshTreeDataFilters).toHaveBeenCalled(); + }); + + it('clearMaxVisibleDepth should remove maxVisibleDepth and refresh filters', () => { + // prepare shared options with existing maxVisibleDepth + sharedService.gridOptions = { treeDataOptions: { columnId: 'file', maxVisibleDepth: 3 } } as any; + + service.clearMaxVisibleDepth(); + + expect(sharedService.gridOptions.treeDataOptions.maxVisibleDepth).toBeUndefined(); + expect(filterServiceStub.refreshTreeDataFilters).toHaveBeenCalled(); + }); + describe('handleOnCellClick method', () => { let div: HTMLDivElement; let mockColumn: Column; @@ -544,6 +565,9 @@ describe('TreeData Service', () => { ]); }; + // also set a maxVisibleDepth to ensure deeper nodes are hidden by filter logic + gridOptionsMock.treeDataOptions!.maxVisibleDepth = 1; + service.init(gridStub); const eventData = new SlickEventData(); div.className = 'slick-group-toggle'; @@ -565,6 +589,16 @@ describe('TreeData Service', () => { expect(service.getToggledItems()).toEqual([{ itemId: 123, isCollapsed: false }]); expect(SharedService.prototype.hierarchicalDataset![0].file).toBe('myFile.txt'); expect(pubSubSpy).not.toHaveBeenCalledWith(`onTreeItemToggled`); + + // Verify maxVisibleDepth has an effect in integration: the mocked flat dataset contains a level 2 node, + // but when maxVisibleDepth is 1 those deeper nodes would be hidden by the filter logic. + const maxDepth = gridOptionsMock.treeDataOptions!.maxVisibleDepth as number; + // mockFlatDataset defined above in this test + const visibleFlat = mockFlatDataset.filter((d) => d.__treeLevel <= maxDepth); + const hiddenFlat = mockFlatDataset.filter((d) => d.__treeLevel > maxDepth); + + expect(hiddenFlat.some((d) => d.__treeLevel === 2)).toBe(true); + expect(visibleFlat.length).toBeLessThan(mockFlatDataset.length); }); describe('toggleOnNodeTitle option', () => { diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts index 89e685b5c..3023409bb 100644 --- a/packages/common/src/services/filter.service.ts +++ b/packages/common/src/services/filter.service.ts @@ -79,9 +79,13 @@ export class FilterService { return this._onSearchChange; } - /** Getter for the Grid Options pulled through the Grid Object */ + /** Getter for the Grid Options pulled through the Grid Object. + * Prefer the SharedService gridOptions when available to allow runtime updates + * made to `sharedService.gridOptions` to be visible without relying on SlickGrid's + * internal deep-merged `getOptions()` value. + */ protected get _gridOptions(): GridOption { - return this._grid?.getOptions() ?? {}; + return this.sharedService?.gridOptions ?? this._grid?.getOptions() ?? {}; } /** Getter for the Column Definitions pulled through the Grid Object */ @@ -326,9 +330,16 @@ export class FilterService { const collapsedPropName = treeDataOptions.collapsedPropName ?? Constants.treeDataProperties.COLLAPSED_PROP; const parentPropName = treeDataOptions.parentPropName ?? Constants.treeDataProperties.PARENT_PROP; const childrenPropName = treeDataOptions?.childrenPropName ?? Constants.treeDataProperties.CHILDREN_PROP; + const levelPropName = treeDataOptions.levelPropName ?? Constants.treeDataProperties.TREE_LEVEL_PROP; + const maxVisibleDepth = treeDataOptions.maxVisibleDepth; const primaryDataId = this._gridOptions.datasetIdPropertyName ?? 'id'; const autoRecalcTotalsOnFilterChange = treeDataOptions.autoRecalcTotalsOnFilterChange ?? false; + // if a maxVisibleDepth is provided, hide any row deeper than that level + if (typeof maxVisibleDepth === 'number' && item[levelPropName] !== undefined && item[levelPropName] > maxVisibleDepth) { + return false; + } + // typically when a parent is collapsed we can exit early (by returning false) but we can't do that when we use auto-recalc totals // if that happens, we need to keep a ref and recalculate total for all tree leafs then only after we can exit let isParentCollapsed = false; // will be used only when auto-recalc is enabled diff --git a/packages/common/src/services/treeData.service.ts b/packages/common/src/services/treeData.service.ts index 8ea97a01b..3f08aaefd 100644 --- a/packages/common/src/services/treeData.service.ts +++ b/packages/common/src/services/treeData.service.ts @@ -292,6 +292,33 @@ export class TreeDataService { return this._currentToggledItems; } + /** + * Set or clear the runtime `maxVisibleDepth` for Tree Data and refresh the grid. + * Calling with `undefined` clears the value. + */ + setMaxVisibleDepth(maxVisibleDepth?: number): void { + const gridOptions = this.sharedService.gridOptions ?? {}; + const treeDataOptions = { ...(gridOptions.treeDataOptions || {}) } as any; + + if (typeof maxVisibleDepth === 'number') { + treeDataOptions.maxVisibleDepth = maxVisibleDepth; + } else { + delete treeDataOptions.maxVisibleDepth; + } + + gridOptions.treeDataOptions = treeDataOptions; + this.sharedService.gridOptions = gridOptions; + this._grid?.setOptions(gridOptions); + + // refreshing the filters will trigger a grid refresh with new tree depth + this.filterService.refreshTreeDataFilters(); + } + + /** Clear the runtime `maxVisibleDepth` */ + clearMaxVisibleDepth(): void { + this.setMaxVisibleDepth(undefined); + } + /** Clear the sorting and set it back to initial sort */ clearSorting(): void { const initialSort = this.getInitialSort(this.sharedService.columnDefinitions, this.sharedService.gridOptions); diff --git a/test/cypress/e2e/example05.cy.ts b/test/cypress/e2e/example05.cy.ts index c70874700..b85d3e919 100644 --- a/test/cypress/e2e/example05.cy.ts +++ b/test/cypress/e2e/example05.cy.ts @@ -258,6 +258,48 @@ describe('Example 05 - Tree Data (from a flat dataset with parentId references)' }); }); + it('deterministic: hide Task 1 children with maxVisibleDepth=1 and restore on clear', () => { + // Task 1 was just expanded by the previous test; now ensure children exist + cy.get('[data-row="1"] > .slick-cell:nth(0) .slick-tree-title').should('contain', 'Task 1'); + + // ensure level-2 children exist; if not, expand Task 1 and re-check + cy.get('.slick-tree-title[level=2]') + .its('length') + .then((len) => { + if (len === 0) { + cy.get( + `.grid5 [style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(0) .slick-group-toggle.collapsed` + ).click({ force: true }); + cy.get('.grid5').find('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + } else { + expect(len).to.be.greaterThan(0); + } + }); + + // apply maxVisibleDepth=1 + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // children (level=2) should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + + // clear the maxVisibleDepth and expect children to reappear + cy.get('[data-test=clear-max-visible-depth-btn]').click(); + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + }); + + it('should set max visible depth via demo input and hide deeper nodes', () => { + // ensure there are level-2 items before applying maxVisibleDepth + cy.get('.slick-tree-title[level=2]').its('length').should('be.greaterThan', 0); + + // set max visible depth to 1 and apply + cy.get('#maxVisibleDepthInput').clear().type('1'); + cy.get('[data-test=set-max-visible-depth-btn]').click(); + + // after applying, level-2 items should be hidden + cy.get('.slick-tree-title[level=2]').should('have.length', 0); + }); + it('should be able to click on the "Collapse All (wihout event)" button', () => { cy.get('[data-test=collapse-all-noevent-btn]').contains('Collapse All (without triggering event)').click(); });