From d7e362658027a4138d61223be34cfa5d1be22a14 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 18:09:06 +0200 Subject: [PATCH] Stabilize Soma table row indexing --- .../table/table-core.svelte.test.ts | 30 +++++++++ .../components/table/table-core.svelte.ts | 67 ++++++++++--------- 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/uix/soma/components/table/table-core.svelte.test.ts b/src/uix/soma/components/table/table-core.svelte.test.ts index f500ae296..ad6659f08 100644 --- a/src/uix/soma/components/table/table-core.svelte.test.ts +++ b/src/uix/soma/components/table/table-core.svelte.test.ts @@ -148,6 +148,36 @@ describe('createTable', () => { } }); + it('passes the source row index to accessor functions after sorting and pagination', () => { + const indexedColumns: ColumnDef[] = [ + { + id: 'sourceIndex', + accessorFn: (_row, index) => index, + header: 'Source index', + enableSorting: true + } + ]; + const { table, cleanup } = withTable({ + data: () => people, + columns: indexedColumns, + sorting: { enabled: true }, + initialSorting: [{ id: 'sourceIndex', desc: true }], + pagination: { enabled: true, pageSize: 2 } + }); + + try { + expect(table.rows.map((row) => row.id)).toEqual(['3', '2']); + expect(table.rows.map((row) => row.cells[0]?.value)).toEqual([3, 2]); + + table.nextPage(); + flushSync(); + expect(table.rows.map((row) => row.id)).toEqual(['1', '0']); + expect(table.rows.map((row) => row.cells[0]?.value)).toEqual([1, 0]); + } finally { + cleanup(); + } + }); + it('orders pinned columns and computes pinned offsets', () => { const { table, cleanup } = withTable({ data: () => people, diff --git a/src/uix/soma/components/table/table-core.svelte.ts b/src/uix/soma/components/table/table-core.svelte.ts index 01fdfcf85..f9e37005b 100644 --- a/src/uix/soma/components/table/table-core.svelte.ts +++ b/src/uix/soma/components/table/table-core.svelte.ts @@ -105,7 +105,7 @@ export type ColumnFiltersState = ColumnFilterEntry[]; export interface ResolvedColumn { id: string; def: ColumnDef; - getValue(row: TData): unknown; + getValue(row: TData, index: number): unknown; } function readAccessorKey(row: unknown, key: string): unknown { @@ -117,7 +117,7 @@ function resolveColumns(defs: ColumnDef[]): ResolvedColumn[ return defs.map((def, idx) => { const id = def.id ?? def.accessorKey ?? `col_${idx}`; const getValue = def.accessorFn - ? (row: TData) => def.accessorFn!(row, 0) + ? (row: TData, index: number) => def.accessorFn!(row, index) : def.accessorKey ? (row: TData) => readAccessorKey(row, def.accessorKey!) : () => undefined; @@ -125,6 +125,11 @@ function resolveColumns(defs: ColumnDef[]): ResolvedColumn[ }); } +interface IndexedRow { + data: TData; + index: number; +} + // ── Sort logic ─────────────────────────────────────────────────────────────── function defaultSortFn(a: unknown, b: unknown): number { @@ -137,10 +142,10 @@ function defaultSortFn(a: unknown, b: unknown): number { } function applySorting( - rows: TData[], + rows: IndexedRow[], sorting: SortingState, columns: ResolvedColumn[] -): TData[] { +): IndexedRow[] { if (sorting.length === 0) return rows; const sorted = [...rows]; @@ -149,7 +154,7 @@ function applySorting( const col = columns.find((c) => c.id === entry.id); if (!col) continue; const sortFn = col.def.sortingFn ?? defaultSortFn; - const result = sortFn(col.getValue(a), col.getValue(b)); + const result = sortFn(col.getValue(a.data, a.index), col.getValue(b.data, b.index)); if (result !== 0) return entry.desc ? -result : result; } return 0; @@ -178,36 +183,36 @@ export type BuiltInFilterFn = keyof typeof filterFns; // ── Filter logic ───────────────────────────────────────────────────────── function applyColumnFilters( - rows: TData[], + rows: IndexedRow[], filters: ColumnFiltersState, columns: ResolvedColumn[] -): TData[] { +): IndexedRow[] { if (filters.length === 0) return rows; return rows.filter((row) => filters.every((filter) => { const col = columns.find((c) => c.id === filter.id); if (!col) return true; - const cellValue = col.getValue(row); - if (col.def.filterFn) return col.def.filterFn(row, col.id, filter.value); + const cellValue = col.getValue(row.data, row.index); + if (col.def.filterFn) return col.def.filterFn(row.data, col.id, filter.value); return filterFns.includesString(cellValue, filter.value); }) ); } function applyGlobalFilter( - rows: TData[], + rows: IndexedRow[], globalFilter: string, columns: ResolvedColumn[], globalFilterFn?: (row: TData, columnId: string, filterValue: unknown) => boolean -): TData[] { +): IndexedRow[] { if (!globalFilter) return rows; return rows.filter((row) => columns.some((col) => { if (col.def.enableFiltering !== true) return false; - const cellValue = col.getValue(row); - if (globalFilterFn) return globalFilterFn(row, col.id, globalFilter); + const cellValue = col.getValue(row.data, row.index); + if (globalFilterFn) return globalFilterFn(row.data, col.id, globalFilter); return filterFns.includesString(cellValue, globalFilter); }) ); @@ -215,7 +220,10 @@ function applyGlobalFilter( // ── Pagination logic ───────────────────────────────────────────────────────── -function applyPagination(rows: TData[], pagination: PaginationState | undefined): TData[] { +function applyPagination( + rows: IndexedRow[], + pagination: PaginationState | undefined +): IndexedRow[] { if (!pagination) return rows; const start = pagination.pageIndex * pagination.pageSize; return rows.slice(start, start + pagination.pageSize); @@ -434,10 +442,11 @@ export function createTable(config: TableConfig): TableInstance config.data()); + const indexedRows = $derived.by(() => allRows.map((data, index) => ({ data, index }))); const filteredRows = $derived.by(() => { - if (!config.filtering?.enabled || config.filtering?.manual) return allRows; - let rows = applyColumnFilters(allRows, columnFilters, resolvedColumns); + if (!config.filtering?.enabled || config.filtering?.manual) return indexedRows; + let rows = applyColumnFilters(indexedRows, columnFilters, resolvedColumns); rows = applyGlobalFilter(rows, globalFilter, resolvedColumns, config.filtering?.globalFilterFn); return rows; }); @@ -479,30 +488,23 @@ export function createTable(config: TableConfig): TableInstance { - const id = getRowId(data, globalIdx); + function buildRow(row: IndexedRow, rowIdx: number): TableRow { + const id = getRowId(row.data, row.index); const orderedCols = getOrderedColumns(); const cells: TableCell[] = orderedCols.map((col) => ({ id: `${id}_${col.id}`, column: col, - value: col.getValue(data) + value: col.getValue(row.data, row.index) })); return { id, index: rowIdx, - original: data, + original: row.data, cells }; } - const rows = $derived.by(() => - paginatedRows.map((data, idx) => { - const globalIdx = config.pagination?.enabled - ? pagination.pageIndex * pagination.pageSize + idx - : idx; - return buildRow(data, globalIdx, idx); - }) - ); + const rows = $derived.by(() => paginatedRows.map((row, idx) => buildRow(row, idx))); const rowsById = $derived.by(() => { const map = new Map>(); @@ -675,8 +677,8 @@ export function createTable(config: TableConfig): TableInstance { - next[getRowId(row, idx)] = true; + sortedRows.forEach((row) => { + next[getRowId(row.data, row.index)] = true; }); selection = next; } @@ -689,7 +691,7 @@ export function createTable(config: TableConfig): TableInstance selection[getRowId(row, idx)]); + return sortedRows.every((row) => selection[getRowId(row.data, row.index)]); } function getIsSomeSelected(): boolean { @@ -698,7 +700,8 @@ export function createTable(config: TableConfig): TableInstance 0 && visibleSelected < sortedRows.length; }