diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a579de..fb9dc2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Column totals for Grouped rows will now be shown inside the groups instead of on a seperate row ([#583][#583]). - Table actions: Copy to clipboard and Export to CSV directly from the button above the Analysis and Database table ([#589]). +- Sort grouped rows by clicking column headers ([#592]). + - Tri state on Group name column will sort by number of items in the group ### Changed @@ -388,6 +390,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea [#583]: https://github.com/certinia/debug-log-analyzer/issues/583 [#589]: https://github.com/certinia/debug-log-analyzer/issues/589 [#590]: https://github.com/certinia/debug-log-analyzer/issues/590 +[#592]: https://github.com/certinia/debug-log-analyzer/issues/592 diff --git a/log-viewer/modules/components/analysis-view/AnalysisView.ts b/log-viewer/modules/components/analysis-view/AnalysisView.ts index 7653d100..a942feea 100644 --- a/log-viewer/modules/components/analysis-view/AnalysisView.ts +++ b/log-viewer/modules/components/analysis-view/AnalysisView.ts @@ -19,7 +19,8 @@ import { Tabulator, type RowComponent } from 'tabulator-tables'; import { isVisible } from '../../Util.js'; import NumberAccessor from '../../datagrid/dataaccessor/Number.js'; import { progressFormatter } from '../../datagrid/format/Progress.js'; -import { GroupCalcs } from '../../datagrid/group-calcs/GroupCalcs.js'; +import { GroupCalcs } from '../../datagrid/groups/GroupCalcs.js'; +import { GroupSort } from '../../datagrid/groups/GroupSort.js'; import * as CommonModules from '../../datagrid/module/CommonModules.js'; import { RowKeyboardNavigation } from '../../datagrid/module/RowKeyboardNavigation.js'; import { RowNavigation } from '../../datagrid/module/RowNavigation.js'; @@ -188,8 +189,10 @@ export class AnalysisView extends LitElement { _groupBy(event: Event) { const target = event.target as HTMLInputElement; const fieldName = target.value.toLowerCase(); - - this.analysisTable?.setGroupBy(fieldName !== 'none' ? fieldName : ''); + if (this.analysisTable) { + //@ts-expect-error This is a custom function added in the GroupSort custom module + this.analysisTable?.setSortedGroupBy(fieldName !== 'none' ? fieldName : ''); + } } _appendTableWhenVisible() { @@ -254,7 +257,7 @@ export class AnalysisView extends LitElement { const metricList = groupMetrics(rootMethod); Tabulator.registerModule(Object.values(CommonModules)); - Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find, GroupCalcs]); + Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find, GroupCalcs, GroupSort]); this.analysisTable = new Tabulator(this._tableWrapper, { rowKeyboardNavigation: true, selectableRows: 'highlight', @@ -292,6 +295,7 @@ export class AnalysisView extends LitElement { height: '100%', maxHeight: '100%', groupCalcs: true, + groupSort: true, groupClosedShowCalcs: true, groupStartOpen: false, groupToggleElement: 'header', @@ -325,6 +329,7 @@ export class AnalysisView extends LitElement { formatter: 'textarea', headerSortStartingDir: 'asc', sorter: 'string', + headerSortTristate: true, cssClass: 'datagrid-code-text', bottomCalc: () => { return 'Total'; diff --git a/log-viewer/modules/components/database-view/DMLView.ts b/log-viewer/modules/components/database-view/DMLView.ts index fc18a495..66513405 100644 --- a/log-viewer/modules/components/database-view/DMLView.ts +++ b/log-viewer/modules/components/database-view/DMLView.ts @@ -11,10 +11,11 @@ import { customElement, property, state } from 'lit/decorators.js'; import { Tabulator, type GroupComponent, type RowComponent } from 'tabulator-tables'; // tabulator custom modules +import { GroupCalcs } from '../../datagrid/groups/GroupCalcs.js'; +import { GroupSort } from '../../datagrid/groups/GroupSort.js'; import * as CommonModules from '../../datagrid/module/CommonModules.js'; import { RowKeyboardNavigation } from '../../datagrid/module/RowKeyboardNavigation.js'; import { RowNavigation } from '../../datagrid/module/RowNavigation.js'; -import { GroupCalcs } from '.././../datagrid/group-calcs/GroupCalcs.js'; import { Find, formatter } from '../calltree-view/module/Find.js'; // tabulator others @@ -161,7 +162,8 @@ export class DMLView extends LitElement { _dmlGroupBy(event: Event) { const target = event.target as HTMLInputElement; - this.dmlTable?.setGroupBy(target.checked ? 'dml' : ''); + //@ts-expect-error This is a custom function added in the GroupSort custom module + this.dmlTable?.setSortedGroupBy(target.checked ? 'dml' : ''); } get _dmlTableWrapper(): HTMLDivElement | null { @@ -181,7 +183,13 @@ export class DMLView extends LitElement { this.dmlLines = dbAccess.getDMLLines() || []; Tabulator.registerModule(Object.values(CommonModules)); - Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find, GroupCalcs]); + Tabulator.registerModule([ + RowKeyboardNavigation, + RowNavigation, + Find, + GroupCalcs, + GroupSort, + ]); this._renderDMLTable(tableWrapper, this.dmlLines); } }); @@ -278,9 +286,11 @@ export class DMLView extends LitElement { placeholder: 'No DML statements found', columnCalcs: 'table', groupCalcs: true, + groupSort: true, groupClosedShowCalcs: true, groupStartOpen: false, groupValues: [dmlText], + groupBy: ['dml'], groupToggleElement: false, selectableRowsCheck: function (row: RowComponent) { return !row.getData().isDetail; @@ -295,7 +305,6 @@ export class DMLView extends LitElement { headerTooltip: true, headerWordWrap: true, }, - initialSort: [{ column: 'rowCount', dir: 'desc' }], headerSortElement: function (column, dir) { switch (dir) { case 'asc': @@ -316,6 +325,7 @@ export class DMLView extends LitElement { bottomCalc: () => { return 'Total'; }, + headerSortTristate: true, cssClass: 'datagrid-textarea datagrid-code-text', variableHeight: true, formatter: (cell, _formatterParams, _onRendered) => { @@ -371,10 +381,6 @@ export class DMLView extends LitElement { this._clearSearchHighlights(); }); - this.dmlTable.on('tableBuilt', () => { - this.dmlTable?.setGroupBy('dml'); - }); - this.dmlTable.on('groupClick', (e: UIEvent, group: GroupComponent) => { const { type } = window.getSelection() ?? {}; if (type === 'Range') { diff --git a/log-viewer/modules/components/database-view/SOQLView.ts b/log-viewer/modules/components/database-view/SOQLView.ts index e9d3882e..6aac16f1 100644 --- a/log-viewer/modules/components/database-view/SOQLView.ts +++ b/log-viewer/modules/components/database-view/SOQLView.ts @@ -17,7 +17,8 @@ import { } from 'tabulator-tables'; //tabulator custom modules -import { GroupCalcs } from '../../datagrid/group-calcs/GroupCalcs.js'; +import { GroupCalcs } from '../../datagrid/groups/GroupCalcs.js'; +import { GroupSort } from '../../datagrid/groups/GroupSort.js'; import * as CommonModules from '../../datagrid/module/CommonModules.js'; import { RowKeyboardNavigation } from '../../datagrid/module/RowKeyboardNavigation.js'; import { RowNavigation } from '../../datagrid/module/RowNavigation.js'; @@ -192,19 +193,14 @@ export class SOQLView extends LitElement { _findEvt = ((event: FindEvt) => this._find(event)) as EventListener; _soqlGroupBy(event: Event) { - const target = event.target as HTMLInputElement; - const fieldName = target.value.toLowerCase(); - const groupValue = fieldName !== 'none' ? fieldName : ''; if (!this.soqlTable) { return; } - - this.soqlTable.setGroupValues([ - groupValue - ? this.sortByFrequency(this.soqlTable.getData(), groupValue as keyof GridSOQLData) - : [''], - ]); - this.soqlTable.setGroupBy(groupValue); + const target = event.target as HTMLInputElement; + const fieldName = target.value.toLowerCase(); + const groupValue = fieldName !== 'none' ? fieldName : ''; + //@ts-expect-error This is a custom function added in the GroupSort custom module + this.soqlTable.setSortedGroupBy(groupValue); } _appendTableWhenVisible() { @@ -219,7 +215,13 @@ export class SOQLView extends LitElement { this.soqlLines = (await DatabaseAccess.create(treeRoot)).getSOQLLines() || []; Tabulator.registerModule(Object.values(CommonModules)); - Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find, GroupCalcs]); + Tabulator.registerModule([ + RowKeyboardNavigation, + RowNavigation, + Find, + GroupCalcs, + GroupSort, + ]); this._renderSOQLTable(tableWrapper, this.soqlLines); } }); @@ -326,8 +328,10 @@ export class SOQLView extends LitElement { keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, clipboardCopyRowRange: 'all', groupCalcs: true, + groupSort: true, groupClosedShowCalcs: true, groupStartOpen: false, + groupBy: 'soql', groupValues: [soqlText], groupToggleElement: false, selectableRows: 'highlight', @@ -343,7 +347,6 @@ export class SOQLView extends LitElement { headerTooltip: true, headerWordWrap: true, }, - initialSort: [{ column: 'rowCount', dir: 'desc' }], headerSortElement: function (column, dir) { switch (dir) { case 'asc': @@ -366,6 +369,7 @@ export class SOQLView extends LitElement { bottomCalc: () => { return 'Total'; }, + headerSortTristate: true, cssClass: 'datagrid-textarea datagrid-code-text', variableHeight: true, formatter: (cell, _formatterParams, _onRendered) => { @@ -501,10 +505,6 @@ export class SOQLView extends LitElement { }, }); - this.soqlTable.on('tableBuilt', () => { - this.soqlTable?.setGroupBy('soql'); - }); - this.soqlTable.on('groupClick', (e: UIEvent, group: GroupComponent) => { const { type } = window.getSelection() ?? {}; if (type === 'Range') { diff --git a/log-viewer/modules/datagrid/group-calcs/GroupCalcs.ts b/log-viewer/modules/datagrid/groups/GroupCalcs.ts similarity index 100% rename from log-viewer/modules/datagrid/group-calcs/GroupCalcs.ts rename to log-viewer/modules/datagrid/groups/GroupCalcs.ts diff --git a/log-viewer/modules/datagrid/groups/GroupSort.ts b/log-viewer/modules/datagrid/groups/GroupSort.ts new file mode 100644 index 00000000..a026065e --- /dev/null +++ b/log-viewer/modules/datagrid/groups/GroupSort.ts @@ -0,0 +1,145 @@ +import { Module, type ColumnComponent, type GroupArg, type Tabulator } from 'tabulator-tables'; + +export class GroupSort extends Module { + static moduleName = 'groupSort'; + + constructor(table: Tabulator) { + super(table); + this.registerTableOption('groupSort', false); + this.registerTableFunction('setSortedGroupBy', this._setSortedGroupBy.bind(this)); + } + + initialize() { + // @ts-expect-error groupSort is a custom propoerty see registerTableOption above + if (this.table.options.groupSort) { + this.table.on('dataSorting', () => { + this.table.blockRedraw(); + this._sortGroups(); + this.table.restoreRedraw(); + }); + } + } + + _setSortedGroupBy(...args: unknown[]) { + const grpArg = args[0] as GroupArg; + const grpArray = Array.isArray(grpArg) ? grpArg : [grpArg]; + const oldGrpArg = this.table.options.groupBy as GroupArg; + const oldGrpArray = Array.isArray(oldGrpArg) ? oldGrpArg : [oldGrpArg]; + if (!this._areGroupsEqual(oldGrpArray, grpArray)) { + this.table.options.groupBy = grpArg; + this.table.blockRedraw(); + this._sortGroups(); + this.table.setGroupBy(grpArg); + this.table.restoreRedraw(); + } + } + + _areGroupsEqual(oldGroups: unknown[], newGroups: unknown[]) { + return ( + oldGroups && + newGroups.length === oldGroups.length && + newGroups.every((value, index) => value === oldGroups[index]) + ); + } + + _sortGroups() { + const grpArray = Array.isArray(this.table.options.groupBy) + ? this.table.options.groupBy + : [this.table.options.groupBy]; + const { options } = this.table; + options.groupValues = []; + + const validGrps = grpArray.filter(Boolean).length > 0; + if (this.table && this.table.options.sortMode !== 'remote' && validGrps) { + const { modules } = this.table; + + const groupRows = modules.groupRows; + const rows = this.table.rowManager.rows; + groupRows.configureGroupSetup(); + groupRows.generateGroups(rows); + + const groupTotalsRows: InternalColumnTotal[] = []; + const columnCalcs = modules.columnCalcs; + const field = columnCalcs.botCalcs[0].field; + groupRows.groupList.forEach((group: { key: string; rows: { data: unknown }[] }) => { + const row = columnCalcs.generateBottomRow(group.rows); + row.data[field] = group.key; + row.key = group.key; + row.rows = group.rows; + row.generateCells(); + groupTotalsRows.push(row); + }); + + const sortListActual: unknown[] = []; + //build list of valid sorters and trigger column specific callbacks before sort begins + const sorter = modules.sort; + const sortList: InternalSortItem[] = options.sortOrderReverse + ? sorter.sortList.slice().reverse() + : sorter.sortList; + sortList.forEach((item) => { + let sortObj; + + if (item.column) { + sortObj = item.column.modules.sort; + + if (sortObj) { + //if no sorter has been defined, take a guess + if (!sortObj.sorter) { + sortObj.sorter = sorter.findSorter(item.column); + } + + item.params = + typeof sortObj.params === 'function' + ? sortObj.params(item.column.getComponent(), item.dir) + : sortObj.params; + + sortListActual.push(item); + } + } + }); + + //sort data + if (sortListActual.length) { + sorter._sortItems(groupTotalsRows, sortListActual); + } else { + groupTotalsRows.sort((a, b) => { + const index = b.rows.length - a.rows.length; + if (index === 0) { + return a.key.localeCompare(b.key); + } + return index; + }); + } + const groupValues: string[] = []; + groupTotalsRows.forEach((colTotals) => { + groupValues.push(colTotals.data[field] as string); + }); + + this.table?.setGroupValues([groupValues]); + } + } +} + +// Representations of the internal Tabulator structures, that are entirely private to Tabulator. Subject to change and likely to b a bit flaky. May not cover all cases yet. +type InternalColumnTotal = { + data: { [key: string]: unknown }; + key: string; + rows: { [key: string]: unknown }[]; +}; + +type InternalColumn = { + getField(): string; + getComponent(): ColumnComponent; + modules: { + sort: { + params: (column: ColumnComponent, dir: string) => object; + sorter: (...args: unknown[]) => number | boolean; + }; + }; +}; + +type InternalSortItem = { + column: InternalColumn; + dir: string; + params: object; +};