Stabilize Soma table row indexing

active-uix
dev 5 months ago
parent 9a8779b9cb
commit d7e3626580

@ -148,6 +148,36 @@ describe('createTable', () => {
} }
}); });
it('passes the source row index to accessor functions after sorting and pagination', () => {
const indexedColumns: ColumnDef<Person>[] = [
{
id: 'sourceIndex',
accessorFn: (_row, index) => index,
header: 'Source index',
enableSorting: true
}
];
const { table, cleanup } = withTable<Person>({
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', () => { it('orders pinned columns and computes pinned offsets', () => {
const { table, cleanup } = withTable<Person>({ const { table, cleanup } = withTable<Person>({
data: () => people, data: () => people,

@ -105,7 +105,7 @@ export type ColumnFiltersState = ColumnFilterEntry[];
export interface ResolvedColumn<TData = unknown> { export interface ResolvedColumn<TData = unknown> {
id: string; id: string;
def: ColumnDef<TData>; def: ColumnDef<TData>;
getValue(row: TData): unknown; getValue(row: TData, index: number): unknown;
} }
function readAccessorKey(row: unknown, key: string): unknown { function readAccessorKey(row: unknown, key: string): unknown {
@ -117,7 +117,7 @@ function resolveColumns<TData>(defs: ColumnDef<TData>[]): ResolvedColumn<TData>[
return defs.map((def, idx) => { return defs.map((def, idx) => {
const id = def.id ?? def.accessorKey ?? `col_${idx}`; const id = def.id ?? def.accessorKey ?? `col_${idx}`;
const getValue = def.accessorFn const getValue = def.accessorFn
? (row: TData) => def.accessorFn!(row, 0) ? (row: TData, index: number) => def.accessorFn!(row, index)
: def.accessorKey : def.accessorKey
? (row: TData) => readAccessorKey(row, def.accessorKey!) ? (row: TData) => readAccessorKey(row, def.accessorKey!)
: () => undefined; : () => undefined;
@ -125,6 +125,11 @@ function resolveColumns<TData>(defs: ColumnDef<TData>[]): ResolvedColumn<TData>[
}); });
} }
interface IndexedRow<TData> {
data: TData;
index: number;
}
// ── Sort logic ─────────────────────────────────────────────────────────────── // ── Sort logic ───────────────────────────────────────────────────────────────
function defaultSortFn(a: unknown, b: unknown): number { function defaultSortFn(a: unknown, b: unknown): number {
@ -137,10 +142,10 @@ function defaultSortFn(a: unknown, b: unknown): number {
} }
function applySorting<TData>( function applySorting<TData>(
rows: TData[], rows: IndexedRow<TData>[],
sorting: SortingState, sorting: SortingState,
columns: ResolvedColumn<TData>[] columns: ResolvedColumn<TData>[]
): TData[] { ): IndexedRow<TData>[] {
if (sorting.length === 0) return rows; if (sorting.length === 0) return rows;
const sorted = [...rows]; const sorted = [...rows];
@ -149,7 +154,7 @@ function applySorting<TData>(
const col = columns.find((c) => c.id === entry.id); const col = columns.find((c) => c.id === entry.id);
if (!col) continue; if (!col) continue;
const sortFn = col.def.sortingFn ?? defaultSortFn; 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; if (result !== 0) return entry.desc ? -result : result;
} }
return 0; return 0;
@ -178,36 +183,36 @@ export type BuiltInFilterFn = keyof typeof filterFns;
// ── Filter logic ───────────────────────────────────────────────────────── // ── Filter logic ─────────────────────────────────────────────────────────
function applyColumnFilters<TData>( function applyColumnFilters<TData>(
rows: TData[], rows: IndexedRow<TData>[],
filters: ColumnFiltersState, filters: ColumnFiltersState,
columns: ResolvedColumn<TData>[] columns: ResolvedColumn<TData>[]
): TData[] { ): IndexedRow<TData>[] {
if (filters.length === 0) return rows; if (filters.length === 0) return rows;
return rows.filter((row) => return rows.filter((row) =>
filters.every((filter) => { filters.every((filter) => {
const col = columns.find((c) => c.id === filter.id); const col = columns.find((c) => c.id === filter.id);
if (!col) return true; if (!col) return true;
const cellValue = col.getValue(row); const cellValue = col.getValue(row.data, row.index);
if (col.def.filterFn) return col.def.filterFn(row, col.id, filter.value); if (col.def.filterFn) return col.def.filterFn(row.data, col.id, filter.value);
return filterFns.includesString(cellValue, filter.value); return filterFns.includesString(cellValue, filter.value);
}) })
); );
} }
function applyGlobalFilter<TData>( function applyGlobalFilter<TData>(
rows: TData[], rows: IndexedRow<TData>[],
globalFilter: string, globalFilter: string,
columns: ResolvedColumn<TData>[], columns: ResolvedColumn<TData>[],
globalFilterFn?: (row: TData, columnId: string, filterValue: unknown) => boolean globalFilterFn?: (row: TData, columnId: string, filterValue: unknown) => boolean
): TData[] { ): IndexedRow<TData>[] {
if (!globalFilter) return rows; if (!globalFilter) return rows;
return rows.filter((row) => return rows.filter((row) =>
columns.some((col) => { columns.some((col) => {
if (col.def.enableFiltering !== true) return false; if (col.def.enableFiltering !== true) return false;
const cellValue = col.getValue(row); const cellValue = col.getValue(row.data, row.index);
if (globalFilterFn) return globalFilterFn(row, col.id, globalFilter); if (globalFilterFn) return globalFilterFn(row.data, col.id, globalFilter);
return filterFns.includesString(cellValue, globalFilter); return filterFns.includesString(cellValue, globalFilter);
}) })
); );
@ -215,7 +220,10 @@ function applyGlobalFilter<TData>(
// ── Pagination logic ───────────────────────────────────────────────────────── // ── Pagination logic ─────────────────────────────────────────────────────────
function applyPagination<TData>(rows: TData[], pagination: PaginationState | undefined): TData[] { function applyPagination<TData>(
rows: IndexedRow<TData>[],
pagination: PaginationState | undefined
): IndexedRow<TData>[] {
if (!pagination) return rows; if (!pagination) return rows;
const start = pagination.pageIndex * pagination.pageSize; const start = pagination.pageIndex * pagination.pageSize;
return rows.slice(start, start + pagination.pageSize); return rows.slice(start, start + pagination.pageSize);
@ -434,10 +442,11 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
// ── Derived: sorted + paginated rows ───────────────────────────────── // ── Derived: sorted + paginated rows ─────────────────────────────────
const allRows = $derived.by(() => config.data()); const allRows = $derived.by(() => config.data());
const indexedRows = $derived.by(() => allRows.map((data, index) => ({ data, index })));
const filteredRows = $derived.by(() => { const filteredRows = $derived.by(() => {
if (!config.filtering?.enabled || config.filtering?.manual) return allRows; if (!config.filtering?.enabled || config.filtering?.manual) return indexedRows;
let rows = applyColumnFilters(allRows, columnFilters, resolvedColumns); let rows = applyColumnFilters(indexedRows, columnFilters, resolvedColumns);
rows = applyGlobalFilter(rows, globalFilter, resolvedColumns, config.filtering?.globalFilterFn); rows = applyGlobalFilter(rows, globalFilter, resolvedColumns, config.filtering?.globalFilterFn);
return rows; return rows;
}); });
@ -479,30 +488,23 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
) )
); );
function buildRow(data: TData, globalIdx: number, rowIdx: number): TableRow<TData> { function buildRow(row: IndexedRow<TData>, rowIdx: number): TableRow<TData> {
const id = getRowId(data, globalIdx); const id = getRowId(row.data, row.index);
const orderedCols = getOrderedColumns(); const orderedCols = getOrderedColumns();
const cells: TableCell<TData>[] = orderedCols.map((col) => ({ const cells: TableCell<TData>[] = orderedCols.map((col) => ({
id: `${id}_${col.id}`, id: `${id}_${col.id}`,
column: col, column: col,
value: col.getValue(data) value: col.getValue(row.data, row.index)
})); }));
return { return {
id, id,
index: rowIdx, index: rowIdx,
original: data, original: row.data,
cells cells
}; };
} }
const rows = $derived.by(() => const rows = $derived.by(() => paginatedRows.map((row, idx) => buildRow(row, idx)));
paginatedRows.map((data, idx) => {
const globalIdx = config.pagination?.enabled
? pagination.pageIndex * pagination.pageSize + idx
: idx;
return buildRow(data, globalIdx, idx);
})
);
const rowsById = $derived.by(() => { const rowsById = $derived.by(() => {
const map = new Map<string, TableRow<TData>>(); const map = new Map<string, TableRow<TData>>();
@ -675,8 +677,8 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
selection = {}; selection = {};
} else { } else {
const next: RowSelectionState = {}; const next: RowSelectionState = {};
sortedRows.forEach((row, idx) => { sortedRows.forEach((row) => {
next[getRowId(row, idx)] = true; next[getRowId(row.data, row.index)] = true;
}); });
selection = next; selection = next;
} }
@ -689,7 +691,7 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
function getIsAllSelected(): boolean { function getIsAllSelected(): boolean {
if (sortedRows.length === 0) return false; if (sortedRows.length === 0) return false;
return sortedRows.every((row, idx) => selection[getRowId(row, idx)]); return sortedRows.every((row) => selection[getRowId(row.data, row.index)]);
} }
function getIsSomeSelected(): boolean { function getIsSomeSelected(): boolean {
@ -698,7 +700,8 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
if (sortedRows.length === 0) return false; if (sortedRows.length === 0) return false;
let visibleSelected = 0; let visibleSelected = 0;
for (let idx = 0; idx < sortedRows.length; idx++) { for (let idx = 0; idx < sortedRows.length; idx++) {
if (selection[getRowId(sortedRows[idx], idx)]) visibleSelected++; const row = sortedRows[idx];
if (selection[getRowId(row.data, row.index)]) visibleSelected++;
} }
return visibleSelected > 0 && visibleSelected < sortedRows.length; return visibleSelected > 0 && visibleSelected < sortedRows.length;
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.