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', () => {
const { table, cleanup } = withTable<Person>({
data: () => people,

@ -105,7 +105,7 @@ export type ColumnFiltersState = ColumnFilterEntry[];
export interface ResolvedColumn<TData = unknown> {
id: string;
def: ColumnDef<TData>;
getValue(row: TData): unknown;
getValue(row: TData, index: number): 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) => {
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<TData>(defs: ColumnDef<TData>[]): ResolvedColumn<TData>[
});
}
interface IndexedRow<TData> {
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<TData>(
rows: TData[],
rows: IndexedRow<TData>[],
sorting: SortingState,
columns: ResolvedColumn<TData>[]
): TData[] {
): IndexedRow<TData>[] {
if (sorting.length === 0) return rows;
const sorted = [...rows];
@ -149,7 +154,7 @@ function applySorting<TData>(
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<TData>(
rows: TData[],
rows: IndexedRow<TData>[],
filters: ColumnFiltersState,
columns: ResolvedColumn<TData>[]
): TData[] {
): IndexedRow<TData>[] {
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<TData>(
rows: TData[],
rows: IndexedRow<TData>[],
globalFilter: string,
columns: ResolvedColumn<TData>[],
globalFilterFn?: (row: TData, columnId: string, filterValue: unknown) => boolean
): TData[] {
): IndexedRow<TData>[] {
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<TData>(
// ── 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;
const start = pagination.pageIndex * 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 ─────────────────────────────────
const allRows = $derived.by(() => 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<TData>(config: TableConfig<TData>): TableInstance<TD
)
);
function buildRow(data: TData, globalIdx: number, rowIdx: number): TableRow<TData> {
const id = getRowId(data, globalIdx);
function buildRow(row: IndexedRow<TData>, rowIdx: number): TableRow<TData> {
const id = getRowId(row.data, row.index);
const orderedCols = getOrderedColumns();
const cells: TableCell<TData>[] = 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<string, TableRow<TData>>();
@ -675,8 +677,8 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
selection = {};
} else {
const next: RowSelectionState = {};
sortedRows.forEach((row, idx) => {
next[getRowId(row, idx)] = true;
sortedRows.forEach((row) => {
next[getRowId(row.data, row.index)] = true;
});
selection = next;
}
@ -689,7 +691,7 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
function getIsAllSelected(): boolean {
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 {
@ -698,7 +700,8 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
if (sortedRows.length === 0) return false;
let visibleSelected = 0;
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;
}

Loading…
Cancel
Save

Powered by TurnKey Linux.