diff --git a/packages/table-core/src/index.ts b/packages/table-core/src/index.ts index 1186d27202..b4f7ab5bab 100755 --- a/packages/table-core/src/index.ts +++ b/packages/table-core/src/index.ts @@ -27,6 +27,7 @@ export * from './features/RowSorting' //utils export * from './utils' +export * from './utils/filterRowsUtils' export * from './utils/getCoreRowModel' export * from './utils/getExpandedRowModel' export * from './utils/getFacetedMinMaxValues' diff --git a/packages/table-core/src/utils/getFilteredRowModel.ts b/packages/table-core/src/utils/getFilteredRowModel.ts index 28e26fce52..6354218cf0 100644 --- a/packages/table-core/src/utils/getFilteredRowModel.ts +++ b/packages/table-core/src/utils/getFilteredRowModel.ts @@ -1,9 +1,151 @@ -import { ResolvedColumnFilter } from '../features/ColumnFiltering' +import { ColumnFiltersState, ResolvedColumnFilter } from '../features/ColumnFiltering' import { Table, RowModel, Row, RowData } from '../types' import { getMemoOptions, memo } from '../utils' import { filterRows } from './filterRowsUtils' -export function getFilteredRowModel(): ( +export function getFilteredRowModelUnmemoized( + table: Table, + columnFilters: ColumnFiltersState, + globalFilter: any, + rowModel: RowModel + +) { + if ( + !rowModel.rows.length || + (!columnFilters?.length && !globalFilter) + ) { + // TODO: Does this have any consequences? Would avoid iterating the rows again + return rowModel; + /* + for (let i = 0; i < rowModel.flatRows.length; i++) { + rowModel.flatRows[i]!.columnFilters = {} + rowModel.flatRows[i]!.columnFiltersMeta = {} + } + return rowModel + */ + } + + const resolvedColumnFilters: ResolvedColumnFilter[] = [] + const resolvedGlobalFilters: ResolvedColumnFilter[] = [] + + ; (columnFilters ?? []).forEach(d => { + const column = table.getColumn(d.id) + + if (!column) { + return + } + + const filterFn = column.getFilterFn() + + if (!filterFn) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `Could not find a valid 'column.filterFn' for column with the ID: ${column.id}.` + ) + } + return + } + + resolvedColumnFilters.push({ + id: d.id, + filterFn, + resolvedValue: filterFn.resolveFilterValue?.(d.value) ?? d.value, + }) + }) + + const filterableIds = (columnFilters ?? []).map(d => d.id) + + const globalFilterFn = table.getGlobalFilterFn() + + const globallyFilterableColumns = table + .getAllLeafColumns() + .filter(column => column.getCanGlobalFilter()) + + if ( + globalFilter && + globalFilterFn && + globallyFilterableColumns.length + ) { + filterableIds.push('__global__') + + globallyFilterableColumns.forEach(column => { + resolvedGlobalFilters.push({ + id: column.id, + filterFn: globalFilterFn, + resolvedValue: + globalFilterFn.resolveFilterValue?.(globalFilter) ?? + globalFilter, + }) + }) + } + + let currentColumnFilter + let currentGlobalFilter + + // Flag the prefiltered row model with each filter state + for (let j = 0; j < rowModel.flatRows.length; j++) { + const row = rowModel.flatRows[j]! + + row.columnFilters = {} + + if (resolvedColumnFilters.length) { + for (let i = 0; i < resolvedColumnFilters.length; i++) { + currentColumnFilter = resolvedColumnFilters[i]! + const id = currentColumnFilter.id + + // Tag the row with the column filter state + row.columnFilters[id] = currentColumnFilter.filterFn( + row, + id, + currentColumnFilter.resolvedValue, + filterMeta => { + row.columnFiltersMeta[id] = filterMeta + } + ) + } + } + + if (resolvedGlobalFilters.length) { + for (let i = 0; i < resolvedGlobalFilters.length; i++) { + currentGlobalFilter = resolvedGlobalFilters[i]! + const id = currentGlobalFilter.id + // Tag the row with the first truthy global filter state + if ( + currentGlobalFilter.filterFn( + row, + id, + currentGlobalFilter.resolvedValue, + filterMeta => { + row.columnFiltersMeta[id] = filterMeta + } + ) + ) { + row.columnFilters.__global__ = true + break + } + } + + if (row.columnFilters.__global__ !== true) { + row.columnFilters.__global__ = false + } + } + } + + const filterRowsImpl = (row: Row) => { + // Horizontally filter rows through each column + for (let i = 0; i < filterableIds.length; i++) { + if (row.columnFilters[filterableIds[i]!] === false) { + return false + } + } + return true + } + + // Filter final rows using all of the active filters + return filterRows(rowModel.rows, filterRowsImpl, table) +} + +export function getFilteredRowModel(middleware?: (rowModel: RowModel) => RowModel): ( table: Table ) => () => RowModel { return table => @@ -14,139 +156,13 @@ export function getFilteredRowModel(): ( table.getState().globalFilter, ], (rowModel, columnFilters, globalFilter) => { - if ( - !rowModel.rows.length || - (!columnFilters?.length && !globalFilter) - ) { - // TODO: Does this have any consequences? Would avoid iterating the rows again - return rowModel; - /* - for (let i = 0; i < rowModel.flatRows.length; i++) { - rowModel.flatRows[i]!.columnFilters = {} - rowModel.flatRows[i]!.columnFiltersMeta = {} - } - return rowModel - */ - } - - const resolvedColumnFilters: ResolvedColumnFilter[] = [] - const resolvedGlobalFilters: ResolvedColumnFilter[] = [] - - ;(columnFilters ?? []).forEach(d => { - const column = table.getColumn(d.id) - - if (!column) { - return - } - - const filterFn = column.getFilterFn() - - if (!filterFn) { - if (process.env.NODE_ENV !== 'production') { - console.warn( - `Could not find a valid 'column.filterFn' for column with the ID: ${column.id}.` - ) - } - return - } - - resolvedColumnFilters.push({ - id: d.id, - filterFn, - resolvedValue: filterFn.resolveFilterValue?.(d.value) ?? d.value, - }) - }) - - const filterableIds = (columnFilters ?? []).map(d => d.id) - - const globalFilterFn = table.getGlobalFilterFn() - - const globallyFilterableColumns = table - .getAllLeafColumns() - .filter(column => column.getCanGlobalFilter()) - - if ( - globalFilter && - globalFilterFn && - globallyFilterableColumns.length - ) { - filterableIds.push('__global__') - - globallyFilterableColumns.forEach(column => { - resolvedGlobalFilters.push({ - id: column.id, - filterFn: globalFilterFn, - resolvedValue: - globalFilterFn.resolveFilterValue?.(globalFilter) ?? - globalFilter, - }) - }) - } - - let currentColumnFilter - let currentGlobalFilter - - // Flag the prefiltered row model with each filter state - for (let j = 0; j < rowModel.flatRows.length; j++) { - const row = rowModel.flatRows[j]! - - row.columnFilters = {} - - if (resolvedColumnFilters.length) { - for (let i = 0; i < resolvedColumnFilters.length; i++) { - currentColumnFilter = resolvedColumnFilters[i]! - const id = currentColumnFilter.id - - // Tag the row with the column filter state - row.columnFilters[id] = currentColumnFilter.filterFn( - row, - id, - currentColumnFilter.resolvedValue, - filterMeta => { - row.columnFiltersMeta[id] = filterMeta - } - ) - } - } - - if (resolvedGlobalFilters.length) { - for (let i = 0; i < resolvedGlobalFilters.length; i++) { - currentGlobalFilter = resolvedGlobalFilters[i]! - const id = currentGlobalFilter.id - // Tag the row with the first truthy global filter state - if ( - currentGlobalFilter.filterFn( - row, - id, - currentGlobalFilter.resolvedValue, - filterMeta => { - row.columnFiltersMeta[id] = filterMeta - } - ) - ) { - row.columnFilters.__global__ = true - break - } - } - - if (row.columnFilters.__global__ !== true) { - row.columnFilters.__global__ = false - } - } - } - - const filterRowsImpl = (row: Row) => { - // Horizontally filter rows through each column - for (let i = 0; i < filterableIds.length; i++) { - if (row.columnFilters[filterableIds[i]!] === false) { - return false - } - } - return true - } - - // Filter final rows using all of the active filters - return filterRows(rowModel.rows, filterRowsImpl, table) + const newRowModel = getFilteredRowModelUnmemoized( + table, + columnFilters, + globalFilter, + rowModel + ) + return middleware ? middleware(newRowModel) : newRowModel }, getMemoOptions(table.options, 'debugTable', 'getFilteredRowModel', () => table._autoResetPageIndex() diff --git a/packages/table-core/src/utils/getGroupedRowModel.ts b/packages/table-core/src/utils/getGroupedRowModel.ts index 8a5390fba6..87fd9dc835 100644 --- a/packages/table-core/src/utils/getGroupedRowModel.ts +++ b/packages/table-core/src/utils/getGroupedRowModel.ts @@ -3,160 +3,137 @@ import { Row, RowData, RowModel, Table } from '../types' import { flattenBy, getMemoOptions, memo } from '../utils' import { GroupingState } from '../features/ColumnGrouping' -export function getGroupedRowModel(): ( - table: Table -) => () => RowModel { - return table => - memo( - () => [table.getState().grouping, table.getPreGroupedRowModel()], - (grouping, rowModel) => { - if (!rowModel.rows.length || !grouping.length) { - // TODO: Does this have any consequences? Would avoid iterating the rows again - return rowModel; - /* - rowModel.rows.forEach(row => { - row.depth = 0 - row.parentId = undefined - }) - return rowModel - */ +export function getGroupedRowModelUnmemoized( + table: Table, + grouping: GroupingState, + rowModel: RowModel +) { + if (!rowModel.rows.length || !grouping.length) { + // TODO: Does this have any consequences? Would avoid iterating the rows again + return rowModel; + /* + rowModel.rows.forEach(row => { + row.depth = 0 + row.parentId = undefined + }) + return rowModel + */ + } + + // Filter the grouping list down to columns that exist + const existingGrouping = grouping.filter(columnId => + table.getColumn(columnId) + ) + + const groupedFlatRows: Row[] = [] + const groupedRowsById: Record> = {} + // const onlyGroupedFlatRows: Row[] = []; + // const onlyGroupedRowsById: Record = {}; + // const nonGroupedFlatRows: Row[] = []; + // const nonGroupedRowsById: Record = {}; + + // Recursively group the data + const groupUpRecursively = ( + rows: Row[], + depth = 0, + parentId?: string + ) => { + // Grouping depth has been been met + // Stop grouping and simply rewrite thd depth and row relationships + if (depth >= existingGrouping.length) { + return rows.map(row => { + row.depth = depth + + groupedFlatRows.push(row) + groupedRowsById[row.id] = row + + if (row.subRows) { + row.subRows = groupUpRecursively(row.subRows, depth + 1, row.id) } - // Filter the grouping list down to columns that exist - const existingGrouping = grouping.filter(columnId => - table.getColumn(columnId) + return row + }) + } + + const columnId: string = existingGrouping[depth]! + + // Group the rows together for this level + const rowGroupsMap = groupBy(rows, columnId) + + // Perform aggregations for each group + const aggregatedGroupedRows = Array.from(rowGroupsMap.entries()).map( + ([groupingValue, groupedRows], index) => { + let id = `${columnId}:${groupingValue}` + id = parentId ? `${parentId}>${id}` : id + + // First, Recurse to group sub rows before aggregation + const subRows = groupUpRecursively(groupedRows, depth + 1, id) + + subRows.forEach(subRow => { + subRow.parentId = id + }) + + // Flatten the leaf rows of the rows in this group + const leafRows = depth + ? flattenBy(groupedRows, row => row.subRows) + : groupedRows + + const row = createRow( + table, + id, + leafRows[0]!.original, + index, + depth, + undefined, + parentId ) - const groupedFlatRows: Row[] = [] - const groupedRowsById: Record> = {} - // const onlyGroupedFlatRows: Row[] = []; - // const onlyGroupedRowsById: Record = {}; - // const nonGroupedFlatRows: Row[] = []; - // const nonGroupedRowsById: Record = {}; - - // Recursively group the data - const groupUpRecursively = ( - rows: Row[], - depth = 0, - parentId?: string - ) => { - // Grouping depth has been been met - // Stop grouping and simply rewrite thd depth and row relationships - if (depth >= existingGrouping.length) { - return rows.map(row => { - row.depth = depth - - groupedFlatRows.push(row) - groupedRowsById[row.id] = row - - if (row.subRows) { - row.subRows = groupUpRecursively(row.subRows, depth + 1, row.id) + Object.assign(row, { + groupingColumnId: columnId, + groupingValue, + subRows, + leafRows, + getValue: (columnId: string) => { + // Don't aggregate columns that are in the grouping + if (existingGrouping.includes(columnId)) { + /* + if (row._valuesCache.hasOwnProperty(columnId)) { + return row._valuesCache[columnId] + } + */ + if (groupedRows[0]) { + return groupedRows[0].getValue(columnId) ?? undefined + /* + row._valuesCache[columnId] = + groupedRows[0].getValue(columnId) ?? undefined + */ } - return row - }) - } - - const columnId: string = existingGrouping[depth]! - - // Group the rows together for this level - const rowGroupsMap = groupBy(rows, columnId) - - // Perform aggregations for each group - const aggregatedGroupedRows = Array.from(rowGroupsMap.entries()).map( - ([groupingValue, groupedRows], index) => { - let id = `${columnId}:${groupingValue}` - id = parentId ? `${parentId}>${id}` : id - - // First, Recurse to group sub rows before aggregation - const subRows = groupUpRecursively(groupedRows, depth + 1, id) - - subRows.forEach(subRow => { - subRow.parentId = id - }) - - // Flatten the leaf rows of the rows in this group - const leafRows = depth - ? flattenBy(groupedRows, row => row.subRows) - : groupedRows - - const row = createRow( - table, - id, - leafRows[0]!.original, - index, - depth, - undefined, - parentId - ) + return undefined; + // return row._valuesCache[columnId] + } - Object.assign(row, { - groupingColumnId: columnId, - groupingValue, - subRows, - leafRows, - getValue: (columnId: string) => { - // Don't aggregate columns that are in the grouping - if (existingGrouping.includes(columnId)) { - /* - if (row._valuesCache.hasOwnProperty(columnId)) { - return row._valuesCache[columnId] - } - */ - if (groupedRows[0]) { - return groupedRows[0].getValue(columnId) ?? undefined - /* - row._valuesCache[columnId] = - groupedRows[0].getValue(columnId) ?? undefined - */ - } - - return undefined; - // return row._valuesCache[columnId] - } - - if (row._groupingValuesCache.hasOwnProperty(columnId)) { - return row._groupingValuesCache[columnId] - } - - // Aggregate the values - const column = table.getColumn(columnId) - const aggregateFn = column?.getAggregationFn() - - if (aggregateFn) { - row._groupingValuesCache[columnId] = aggregateFn( - columnId, - leafRows, - groupedRows - ) - - return row._groupingValuesCache[columnId] - } - }, - }) - - subRows.forEach(subRow => { - groupedFlatRows.push(subRow) - groupedRowsById[subRow.id] = subRow - // if (subRow.getIsGrouped?.()) { - // onlyGroupedFlatRows.push(subRow); - // onlyGroupedRowsById[subRow.id] = subRow; - // } else { - // nonGroupedFlatRows.push(subRow); - // nonGroupedRowsById[subRow.id] = subRow; - // } - }) - - return row + if (row._groupingValuesCache.hasOwnProperty(columnId)) { + return row._groupingValuesCache[columnId] } - ) - return aggregatedGroupedRows - } + // Aggregate the values + const column = table.getColumn(columnId) + const aggregateFn = column?.getAggregationFn() - const groupedRows = groupUpRecursively(rowModel.rows, 0) + if (aggregateFn) { + row._groupingValuesCache[columnId] = aggregateFn( + columnId, + leafRows, + groupedRows + ) - groupedRows.forEach(subRow => { + return row._groupingValuesCache[columnId] + } + }, + }) + + subRows.forEach(subRow => { groupedFlatRows.push(subRow) groupedRowsById[subRow.id] = subRow // if (subRow.getIsGrouped?.()) { @@ -168,11 +145,43 @@ export function getGroupedRowModel(): ( // } }) - return { - rows: groupedRows, - flatRows: groupedFlatRows, - rowsById: groupedRowsById, - } + return row + } + ) + + return aggregatedGroupedRows + } + + const groupedRows = groupUpRecursively(rowModel.rows, 0) + + groupedRows.forEach(subRow => { + groupedFlatRows.push(subRow) + groupedRowsById[subRow.id] = subRow + // if (subRow.getIsGrouped?.()) { + // onlyGroupedFlatRows.push(subRow); + // onlyGroupedRowsById[subRow.id] = subRow; + // } else { + // nonGroupedFlatRows.push(subRow); + // nonGroupedRowsById[subRow.id] = subRow; + // } + }) + + return { + rows: groupedRows, + flatRows: groupedFlatRows, + rowsById: groupedRowsById, + } +} + +export function getGroupedRowModel(middleware?: (rowModel: RowModel) => RowModel): ( + table: Table +) => () => RowModel { + return table => + memo( + () => [table.getState().grouping, table.getPreGroupedRowModel()], + (grouping, rowModel) => { + const newRowModel = getGroupedRowModelUnmemoized(table, grouping, rowModel) + return middleware ? middleware(newRowModel) : newRowModel }, getMemoOptions(table.options, 'debugTable', 'getGroupedRowModel', () => { table._queue(() => { diff --git a/packages/table-core/src/utils/getSortedRowModel.ts b/packages/table-core/src/utils/getSortedRowModel.ts index 88c7549bb7..52bc9bbe27 100644 --- a/packages/table-core/src/utils/getSortedRowModel.ts +++ b/packages/table-core/src/utils/getSortedRowModel.ts @@ -1,118 +1,127 @@ import { Table, Row, RowModel, RowData } from '../types' -import { SortingFn } from '../features/RowSorting' +import { SortingFn, SortingState } from '../features/RowSorting' import { getMemoOptions, memo } from '../utils' -export function getSortedRowModel(): ( - table: Table -) => () => RowModel { - return table => - memo( - () => [table.getState().sorting, table.getPreSortedRowModel()], - (sorting, rowModel) => { - if (!rowModel.rows.length || !sorting?.length) { - return rowModel +export function getSortedRowModelUnmemoized( + table: Table, + sorting: SortingState, + rowModel: RowModel +) { + if (!rowModel.rows.length || !sorting?.length) { + return rowModel + } + + const sortingState = table.getState().sorting + + const sortedFlatRows: Row[] = [] + + // Filter out sortings that correspond to non existing columns + const availableSorting = sortingState.filter(sort => + table.getColumn(sort.id)?.getCanSort() + ) + + const columnInfoById: Record< + string, + { + sortUndefined?: false | -1 | 1 | 'first' | 'last' + invertSorting?: boolean + sortingFn: SortingFn + } + > = {} + + availableSorting.forEach(sortEntry => { + const column = table.getColumn(sortEntry.id) + if (!column) return + + columnInfoById[sortEntry.id] = { + sortUndefined: column.columnDef.sortUndefined, + invertSorting: column.columnDef.invertSorting, + sortingFn: column.getSortingFn(), + } + }) + + const sortData = (rows: Row[]) => { + // This will also perform a stable sorting using the row index + // if needed. + const sortedData = rows.map(row => row.clone()) + + sortedData.sort((rowA, rowB) => { + for (let i = 0; i < availableSorting.length; i += 1) { + const sortEntry = availableSorting[i]! + const columnInfo = columnInfoById[sortEntry.id]! + const sortUndefined = columnInfo.sortUndefined + const isDesc = sortEntry?.desc ?? false + + let sortInt = 0 + + // All sorting ints should always return in ascending order + if (sortUndefined) { + const aValue = rowA.getValue(sortEntry.id) + const bValue = rowB.getValue(sortEntry.id) + + const aUndefined = aValue === undefined + const bUndefined = bValue === undefined + + if (aUndefined || bUndefined) { + if (sortUndefined === 'first') return aUndefined ? -1 : 1 + if (sortUndefined === 'last') return aUndefined ? 1 : -1 + sortInt = + aUndefined && bUndefined + ? 0 + : aUndefined + ? sortUndefined + : -sortUndefined + } } - const sortingState = table.getState().sorting - - const sortedFlatRows: Row[] = [] - - // Filter out sortings that correspond to non existing columns - const availableSorting = sortingState.filter(sort => - table.getColumn(sort.id)?.getCanSort() - ) + if (sortInt === 0) { + sortInt = columnInfo.sortingFn(rowA, rowB, sortEntry.id) + } - const columnInfoById: Record< - string, - { - sortUndefined?: false | -1 | 1 | 'first' | 'last' - invertSorting?: boolean - sortingFn: SortingFn + // If sorting is non-zero, take care of desc and inversion + if (sortInt !== 0) { + if (isDesc) { + sortInt *= -1 } - > = {} - availableSorting.forEach(sortEntry => { - const column = table.getColumn(sortEntry.id) - if (!column) return - - columnInfoById[sortEntry.id] = { - sortUndefined: column.columnDef.sortUndefined, - invertSorting: column.columnDef.invertSorting, - sortingFn: column.getSortingFn(), + if (columnInfo.invertSorting) { + sortInt *= -1 } - }) - - const sortData = (rows: Row[]) => { - // This will also perform a stable sorting using the row index - // if needed. - const sortedData = rows.map(row => row.clone()) - - sortedData.sort((rowA, rowB) => { - for (let i = 0; i < availableSorting.length; i += 1) { - const sortEntry = availableSorting[i]! - const columnInfo = columnInfoById[sortEntry.id]! - const sortUndefined = columnInfo.sortUndefined - const isDesc = sortEntry?.desc ?? false - - let sortInt = 0 - - // All sorting ints should always return in ascending order - if (sortUndefined) { - const aValue = rowA.getValue(sortEntry.id) - const bValue = rowB.getValue(sortEntry.id) - - const aUndefined = aValue === undefined - const bUndefined = bValue === undefined - - if (aUndefined || bUndefined) { - if (sortUndefined === 'first') return aUndefined ? -1 : 1 - if (sortUndefined === 'last') return aUndefined ? 1 : -1 - sortInt = - aUndefined && bUndefined - ? 0 - : aUndefined - ? sortUndefined - : -sortUndefined - } - } - - if (sortInt === 0) { - sortInt = columnInfo.sortingFn(rowA, rowB, sortEntry.id) - } - - // If sorting is non-zero, take care of desc and inversion - if (sortInt !== 0) { - if (isDesc) { - sortInt *= -1 - } - - if (columnInfo.invertSorting) { - sortInt *= -1 - } - - return sortInt - } - } - - return rowA.index - rowB.index - }) - - // If there are sub-rows, sort them - sortedData.forEach(row => { - sortedFlatRows.push(row) - if (row.subRows?.length) { - row.subRows = sortData(row.subRows) - } - }) - - return sortedData - } - return { - rows: sortData(rowModel.rows), - flatRows: sortedFlatRows, - rowsById: rowModel.rowsById, + return sortInt } + } + + return rowA.index - rowB.index + }) + + // If there are sub-rows, sort them + sortedData.forEach(row => { + sortedFlatRows.push(row) + if (row.subRows?.length) { + row.subRows = sortData(row.subRows) + } + }) + + return sortedData + } + + return { + rows: sortData(rowModel.rows), + flatRows: sortedFlatRows, + rowsById: rowModel.rowsById, + } +} + +export function getSortedRowModel(middleware?: (rowModel: RowModel) => RowModel): ( + table: Table +) => () => RowModel { + return table => + memo( + () => [table.getState().sorting, table.getPreSortedRowModel()], + (sorting, rowModel) => { + const newRowModel = getSortedRowModelUnmemoized(table, sorting, rowModel) + return middleware ? middleware(newRowModel) : newRowModel }, getMemoOptions(table.options, 'debugTable', 'getSortedRowModel', () => table._autoResetPageIndex()