diff --git a/packages/nimble-components/src/table-column/base/index.ts b/packages/nimble-components/src/table-column/base/index.ts index e6b9304159..08db3103a9 100644 --- a/packages/nimble-components/src/table-column/base/index.ts +++ b/packages/nimble-components/src/table-column/base/index.ts @@ -75,6 +75,8 @@ export abstract class TableColumn< this.setAttribute('slot', this.columnInternals.uniqueId); } + public getText?(cellRecord: unknown): string; + protected abstract getColumnInternalsOptions(): ColumnInternalsOptions; protected sortDirectionChanged(): void { diff --git a/packages/nimble-components/src/table-column/date-text/cell-view/index.ts b/packages/nimble-components/src/table-column/date-text/cell-view/index.ts index 676c4904f0..2f3f1af532 100644 --- a/packages/nimble-components/src/table-column/date-text/cell-view/index.ts +++ b/packages/nimble-components/src/table-column/date-text/cell-view/index.ts @@ -21,6 +21,16 @@ export class TableColumnDateTextCellView extends TableColumnTextCellViewBase< TableColumnDateTextCellRecord, TableColumnDateTextColumnConfig > { + public static getText(cellRecord: TableColumnDateTextCellRecord | undefined, columnConfig: TableColumnDateTextColumnConfig | undefined): string { + if (columnConfig) { + return formatNumericDate( + columnConfig.formatter, + cellRecord?.value + ); + } + return ''; + } + private columnConfigChanged(): void { this.updateText(); } @@ -30,14 +40,7 @@ TableColumnDateTextColumnConfig } private updateText(): void { - if (this.columnConfig) { - this.text = formatNumericDate( - this.columnConfig.formatter, - this.cellRecord?.value - ); - } else { - this.text = ''; - } + this.text = TableColumnDateTextCellView.getText(this.cellRecord, this.columnConfig); } } diff --git a/packages/nimble-components/src/table-column/date-text/index.ts b/packages/nimble-components/src/table-column/date-text/index.ts index 578b1da9d8..a74f678dda 100644 --- a/packages/nimble-components/src/table-column/date-text/index.ts +++ b/packages/nimble-components/src/table-column/date-text/index.ts @@ -9,7 +9,7 @@ import type { TableNumberField } from '../../table/types'; import { TableColumnTextBase } from '../text-base'; import { TableColumnSortOperation, TableColumnValidity } from '../base/types'; import { tableColumnDateTextGroupHeaderViewTag } from './group-header-view'; -import { tableColumnDateTextCellViewTag } from './cell-view'; +import { TableColumnDateTextCellView, tableColumnDateTextCellViewTag } from './cell-view'; import type { ColumnInternalsOptions } from '../base/models/column-internals'; import { DateTextFormat, @@ -135,6 +135,10 @@ export class TableColumnDateText extends TableColumnTextBase { return this.validator.getValidity(); } + public override getText(cellRecord: TableColumnDateTextCellRecord): string { + return TableColumnDateTextCellView.getText(cellRecord, this.columnInternals.columnConfig as TableColumnDateTextColumnConfig); + } + protected override getColumnInternalsOptions(): ColumnInternalsOptions { return { cellRecordFieldNames: ['value'], diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index d13ea2c918..a70d87575c 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -21,12 +21,14 @@ import { getSortedRowModel as tanStackGetSortedRowModel, getGroupedRowModel as tanStackGetGroupedRowModel, getExpandedRowModel as tanStackGetExpandedRowModel, + getFilteredRowModel as tanStackGetFilteredRowModel, TableOptionsResolved as TanStackTableOptionsResolved, SortingState as TanStackSortingState, RowSelectionState as TanStackRowSelectionState, GroupingState as TanStackGroupingState, ExpandedState as TanStackExpandedState, - OnChangeFn as TanStackOnChangeFn + OnChangeFn as TanStackOnChangeFn, + FilterFn as TanStackFilterFn } from '@tanstack/table-core'; import { keyShift } from '@microsoft/fast-web-utilities'; import { TableColumn } from '../table-column/base'; @@ -46,7 +48,9 @@ import { TableRowSelectionToggleEventDetail, TableRowState, TableValidity, - TableSetRecordHierarchyOptions + TableSetRecordHierarchyOptions, + TableFilter, + TableFieldName } from './types'; import { Virtualizer } from './models/virtualizer'; import { getTanStackSortingFunction } from './models/sort-operations'; @@ -58,6 +62,7 @@ import { InteractiveSelectionManager } from './models/interactive-selection-mana import { DataHierarchyManager } from './models/data-hierarchy-manager'; import { ExpansionManager } from './models/expansion-manager'; import { waitUntilCustomElementsDefinedAsync } from '../utilities/wait-until-custom-elements-defined-async'; +import { diacriticInsensitiveStringNormalizer } from '../utilities/models/string-normalizers'; declare global { interface HTMLElementTagNameMap { @@ -237,6 +242,8 @@ export class Table< public constructor() { super(); + + const that = this; this.options = { data: [], onStateChange: (_: TanStackUpdater) => {}, @@ -246,6 +253,7 @@ export class Table< getSortedRowModel: tanStackGetSortedRowModel(), getGroupedRowModel: tanStackGetGroupedRowModel(), getExpandedRowModel: tanStackGetExpandedRowModel(), + getFilteredRowModel: tanStackGetFilteredRowModel(), getRowCanExpand: this.getRowCanExpand, getIsRowExpanded: this.getIsRowExpanded, getSubRows: r => r.subRows, @@ -253,6 +261,7 @@ export class Table< state: { rowSelection: {}, grouping: [], + columnFilters: [], expanded: true // Workaround until we can apply a fix to TanStack regarding leveraging our getIsRowExpanded implementation }, enableRowSelection: row => !row.getIsGrouped(), @@ -260,8 +269,53 @@ export class Table< enableSubRowSelection: false, enableSorting: true, enableGrouping: true, + filterFromLeafRows: true, renderFallbackValue: null, - autoResetAll: false + autoResetAll: false, + // filterFns: { + // foo: this.getGlobalFilterFn + // } + // globalFilterFn: this.getGlobalFilterFn + globalFilterFn: (row: TanStackRow>, columnId: string, filterValue: string[]): boolean => { + let cellText: string | undefined = ''; + const record = row.original.clientRecord; + const column = that.columns.find(x => x.columnInternals.uniqueId === columnId); + if (!column) { + return false; + } + if (column.getText) { + const fieldNames = column.columnInternals.dataRecordFieldNames; + if (that.hasValidFieldNames(fieldNames) && record) { + const cellDataValues = fieldNames.map( + field => record[field] + ); + const cellRecord = Object.fromEntries( + column.columnInternals.cellRecordFieldNames.map((k, j) => [ + k, + cellDataValues[j] + ]) + ); + cellText = column.getText(cellRecord); + } else { + return false; + } + } else { + const fieldName = column.columnInternals.operandDataRecordFieldName; + if (typeof fieldName !== 'string') { + cellText = undefined; + } else { + const cellValue = record[fieldName]; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + cellText = cellValue === undefined || cellValue === null ? undefined : `${cellValue}`; + } + } + + if (typeof cellText !== 'string') { + return false; + } + const normalizedCellText = diacriticInsensitiveStringNormalizer(cellText); + return filterValue.some(x => normalizedCellText.includes(diacriticInsensitiveStringNormalizer(x.toString()))); + } }; this.table = tanStackCreateTable(this.options); this.virtualizer = new Virtualizer(this, this.table); @@ -281,6 +335,24 @@ export class Table< this.updateTableOptions(tanstackUpdates); } + public async filterData(/*filter: TableFilter[]*/filter: string[]): Promise { + await this.processPendingUpdates(); + // this.updateTableOptions({ + // filterFns: this.calculateTanStackFilter(filter) + // }); + // this.table.setGlobalFilter() + this.updateTableOptions({ + state: { + globalFilter: filter + } + }); + } + + public async clearFilter(): Promise { + await this.processPendingUpdates(); + this.table.resetGlobalFilter(); + } + public async getSelectedRecordIds(): Promise { await this.processPendingUpdates(); return this.selectionManager.getCurrentSelectedRecordIds(); @@ -1067,6 +1139,46 @@ export class Table< }); }; + // private readonly getGlobalFilterFn: TanStackFilterFn> = (row: TanStackRow>, columnId: string, filterValue: string): boolean => { + // let cellText: string | undefined = ''; + // const record = row.original.clientRecord; + // const column = this.columns.find(x => x.id === columnId); + // if (!column) { + // return false; + // } + // if (column.getText) { + // const fieldNames = column.columnInternals.dataRecordFieldNames; + // if (this.hasValidFieldNames(fieldNames) && record) { + // const cellDataValues = fieldNames.map( + // field => record[field] + // ); + // const cellRecord = Object.fromEntries( + // column.columnInternals.cellRecordFieldNames.map((k, j) => [ + // k, + // cellDataValues[j] + // ]) + // ); + // cellText = column.getText(cellRecord); + // } else { + // return false; + // } + // } else { + // const fieldName = column.columnInternals.operandDataRecordFieldName; + // if (typeof fieldName !== 'string') { + // cellText = undefined; + // } else { + // const cellValue = record[fieldName]; + // // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + // cellText = cellValue === undefined || cellValue === null ? undefined : `${cellValue}`; + // } + // } + + // if (typeof cellText !== 'string') { + // return false; + // } + // return diacriticInsensitiveStringNormalizer(filterValue).includes(diacriticInsensitiveStringNormalizer(filterValue.toString())); + // }; + private readonly handleExpandedChange: TanStackOnChangeFn = (updaterOrValue: TanStackUpdater): void => { const expandedState = updaterOrValue instanceof Function ? updaterOrValue(this.table.getState().expanded) @@ -1085,6 +1197,20 @@ export class Table< this.expansionManager.toggleRowExpansion(row); } + // private calculateTanStackFilter(filters: TableFilter[]): { [name: string]: TanStackFilterFn> } { + // const retValue = {}; + // filters.forEach((filter: TableFilter, index: number) => { + // retValue[`filter${index}`] = (row: TanStackRow, columnId: string, filterValue: any) => { + // const cellValue = row.getValue(columnId); + // return cellValue === filterValue; + // }; + // }); + // return retValue; + + // this.table.setGlobalFilter(); + // this.table.resetGlobalFilter(); + // } + private calculateTanStackSortState(): TanStackSortingState { const sortedColumns = this.getColumnsParticipatingInSorting().sort( (x, y) => x.columnInternals.currentSortIndex! @@ -1143,6 +1269,12 @@ export class Table< }); } + private hasValidFieldNames( + keys: readonly (TableFieldName | undefined)[] + ): keys is TableFieldName[] { + return keys.every(key => key !== undefined); + } + private calculateTanStackSelectionState( recordIdsToSelect: readonly string[] ): TanStackRowSelectionState { diff --git a/packages/nimble-components/src/table/types.ts b/packages/nimble-components/src/table/types.ts index c048ff0f7d..b1d3494f52 100644 --- a/packages/nimble-components/src/table/types.ts +++ b/packages/nimble-components/src/table/types.ts @@ -194,6 +194,14 @@ export interface TableColumnConfiguration { pixelWidth?: number; } +export interface TableFilter { + matchString: string; // Does this need to support different types? + // matchMode: 'recordValue' | 'renderedValue' | 'custom' + // hierachyMode: 'includeChildren' | 'includeParent' | 'includeMatchesOnly' | 'includeFulHierarchy' -- look at Tanstack's `filterFromLeafRows`. I think our options here may be limited + // columns: 'all' | 'visible-only' | 'custom' + // customColumns: [] +} + /** * @internal *