diff --git a/cypress/integration/tree_sorting.js b/cypress/integration/tree_sorting.js new file mode 100644 index 0000000..7120b6a --- /dev/null +++ b/cypress/integration/tree_sorting.js @@ -0,0 +1,152 @@ +describe('Tree sorting', function () { + const data = [ + { value: 7, indent: 0 }, + { value: 1, indent: 1 }, + { value: 8, indent: 1 }, + { value: 3, indent: 0 }, + { value: 4, indent: 1 }, + { value: 5, indent: 2 }, + { value: 6, indent: 2 }, + { value: 2, indent: 1 } + ]; + + beforeEach(function () { + cy.visit('/'); + }); + + it('sorts siblings, numbers rows and keeps collapse and expand working', function () { + createTable(data); + expectRowOrder([7, 1, 8, 3, 4, 5, 6, 2]); + cy.window().then(win => { + cy.spy(win.treeTable.rowmanager, 'refreshRows').as('refreshRows'); + }); + + sortTable('Sort Ascending'); + expectRowOrder([3, 2, 4, 5, 6, 7, 1, 8]); + expectSerialNumbers(8); + cy.get('@refreshRows').should('have.been.calledOnce'); + toggleRow(7); + expectRowOrder([3, 2, 4, 5, 6, 7]); + toggleRow(7); + expectRowOrder([3, 2, 4, 5, 6, 7, 1, 8]); + + cy.get('@refreshRows').then(spy => spy.resetHistory()); + sortTable('Sort Descending'); + expectRowOrder([7, 8, 1, 3, 4, 6, 5, 2]); + expectSerialNumbers(8); + cy.get('@refreshRows').should('have.been.calledOnce'); + toggleRow(3); + expectRowOrder([7, 8, 1, 3]); + toggleRow(3); + expectRowOrder([7, 8, 1, 3, 4, 2]); + toggleRow(4); + expectRowOrder([7, 8, 1, 3, 4, 6, 5, 2]); + + cy.get('@refreshRows').then(spy => spy.resetHistory()); + sortTable('Reset sorting'); + expectRowOrder([7, 1, 8, 3, 4, 5, 6, 2]); + expectSerialNumbers(8); + cy.get('@refreshRows').should('have.been.calledOnce'); + }); + + it('preserves the column sortValue hook', function () { + createTable(data, { + columns: [{ id: 'value', name: 'Value', sortValue: cell => -cell.content }] + }); + sortTable('Sort Ascending'); + expectRowOrder([7, 8, 1, 3, 4, 6, 5, 2]); + sortTable('Sort Descending'); + expectRowOrder([3, 2, 4, 5, 6, 7, 1, 8]); + sortTable('Reset sorting'); + expectRowOrder([7, 1, 8, 3, 4, 5, 6, 2]); + }); + + it('sorts flat tables normally even when rows have indent metadata', function () { + createTable(data, { treeView: false }); + sortTable('Sort Ascending'); + expectRowOrder([1, 2, 3, 4, 5, 6, 7, 8]); + sortTable('Sort Descending'); + expectRowOrder([8, 7, 6, 5, 4, 3, 2, 1]); + sortTable('Reset sorting'); + expectRowOrder([7, 1, 8, 3, 4, 5, 6, 2]); + }); + + it('supports nonzero root indents and tables without serial numbers', function () { + createTable(data.map(row => Object.assign({}, row, { indent: row.indent + 2 })), { + serialNoColumn: false + }); + sortTable('Sort Ascending'); + expectRowOrder([3, 2, 4, 5, 6, 7, 1, 8]); + sortTable('Sort Descending'); + expectRowOrder([7, 8, 1, 3, 4, 6, 5, 2]); + }); + + it('refreshes cached serial number formatting after sorting', function () { + createTable(data, { + serialNoColumn: false, + columns: [ + { id: '_rowIndex', name: 'Sr No', format: value => `No ${value}` }, + { id: 'value', name: 'Value' } + ] + }); + expectSerialNumbers(8, 'No '); + sortTable('Sort Ascending'); + expectRowOrder([3, 2, 4, 5, 6, 7, 1, 8]); + expectSerialNumbers(8, 'No '); + }); + + it('keeps sorting after a data refresh', function () { + createTable(data); + sortTable('Sort Ascending'); + expectRowOrder([3, 2, 4, 5, 6, 7, 1, 8]); + cy.window().then(win => win.treeTable.refresh(data)); + sortTable('Sort Descending'); + expectRowOrder([7, 8, 1, 3, 4, 6, 5, 2]); + expectSerialNumbers(8); + }); + + it('sorts an empty tree without errors', function () { + createTable([]); + cy.window().then(win => win.treeTable.datamanager.sortRows(1, 'asc')); + cy.window().then(win => { + expect(win.treeTable.datamanager.rowViewOrder).to.deep.equal([]); + }); + }); +}); + +function createTable(data, options = {}) { + cy.window().then(win => { + win.treeTable = new win.DataTable('#datatable2', Object.assign({ + columns: [{ id: 'value', name: 'Value', width: 180 }], + data, + treeView: true + }, options)); + }); + cy.get('#datatable2 .datatable').should('exist'); +} + +function sortTable(label) { + cy.get('#datatable2 .dt-cell--header').contains('Value') + .closest('.dt-cell').find('.dt-dropdown__toggle').click(); + cy.get('#datatable2 .dt-dropdown__list-item').contains(label).click({ force: true }); +} + +function toggleRow(value) { + cy.get('#datatable2 .dt-scrollable .dt-tree-node').contains('span', String(value)) + .closest('.dt-tree-node').find('.dt-tree-node__toggle').click(); +} + +function expectRowOrder(expected) { + cy.get('#datatable2 .dt-scrollable .dt-row .dt-cell:last-child .dt-cell__content') + .should(cells => { + expect(Array.from(cells, cell => cell.textContent.trim())).to.deep.equal(expected.map(String)); + }); +} + +function expectSerialNumbers(count, prefix = '') { + cy.get('#datatable2 .dt-scrollable .dt-row .dt-cell:first-child .dt-cell__content') + .should(cells => { + const expected = Array.from({ length: count }, (_, index) => prefix + (index + 1)); + expect(Array.from(cells, cell => cell.textContent.trim())).to.deep.equal(expected); + }); +} diff --git a/src/datamanager.js b/src/datamanager.js index 5b9065f..7d933bc 100644 --- a/src/datamanager.js +++ b/src/datamanager.js @@ -271,7 +271,7 @@ export default class DataManager { _sortRows(colIndex, sortOrder) { - if (this.currentSort.colIndex === colIndex) { + if (!this.options.treeView && this.currentSort.colIndex === colIndex) { // reverse the array if only sortOrder changed if ( (this.currentSort.sortOrder === 'asc' && sortOrder === 'desc') || @@ -310,17 +310,55 @@ export default class DataManager { return 0; }); + if (this.options.treeView) { + this._sortTreeRows(); + } + if (this.hasColumnById('_rowIndex')) { // update row index const srNoColIndex = this.getColumnIndexById('_rowIndex'); - this.rows.forEach((row, index) => { - const viewIndex = this.rowViewOrder.indexOf(index); - const cell = row[srNoColIndex]; + this.rowViewOrder.forEach((rowIndex, viewIndex) => { + const cell = this.rows[rowIndex][srNoColIndex]; cell.content = (viewIndex + 1) + ''; + cell.html = null; }); } } + _sortTreeRows() { + // Use the column sort order to rank siblings without separating their children. + const sortPositions = new Map(); + this.rowViewOrder.forEach((rowIndex, position) => { + sortPositions.set(rowIndex, position); + }); + + const root = { indent: -Infinity, children: [] }; + const ancestors = [root]; + + this.rows.forEach((row, rowIndex) => { + const indent = Number(row.meta.indent) || 0; + while (ancestors[ancestors.length - 1].indent >= indent) { + ancestors.pop(); + } + + const node = { rowIndex, indent, children: [] }; + ancestors[ancestors.length - 1].children.push(node); + ancestors.push(node); + }); + + const rowOrder = []; + function appendSiblings(siblings) { + siblings.sort((a, b) => sortPositions.get(a.rowIndex) - sortPositions.get(b.rowIndex)); + for (const node of siblings) { + rowOrder.push(node.rowIndex); + appendSiblings(node.children); + } + } + + appendSiblings(root.children); + this.rowViewOrder = rowOrder; + } + reverseArray(array) { let left = null; let right = null;