2026-04-07 12:19:59 +00:00
|
|
|
import type { Column, ISortDescriptor, TDisplayFunction } from './types.js';
|
2025-09-16 14:53:59 +00:00
|
|
|
|
|
|
|
|
export function computeColumnsFromDisplayFunction<T>(
|
|
|
|
|
displayFunction: TDisplayFunction<T>,
|
|
|
|
|
data: T[]
|
|
|
|
|
): Column<T>[] {
|
|
|
|
|
if (!data || data.length === 0) return [];
|
|
|
|
|
const firstTransformedItem = displayFunction(data[0]);
|
|
|
|
|
const keys: string[] = Object.keys(firstTransformedItem);
|
|
|
|
|
return keys.map((key) => ({
|
|
|
|
|
key,
|
|
|
|
|
header: key,
|
|
|
|
|
value: (row: T) => displayFunction(row)[key],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function computeEffectiveColumns<T>(
|
|
|
|
|
columns: Column<T>[] | undefined,
|
|
|
|
|
augmentFromDisplayFunction: boolean,
|
|
|
|
|
displayFunction: TDisplayFunction<T>,
|
|
|
|
|
data: T[]
|
|
|
|
|
): Column<T>[] {
|
|
|
|
|
const base = (columns || []).slice();
|
|
|
|
|
if (!augmentFromDisplayFunction) return base;
|
|
|
|
|
const fromDisplay = computeColumnsFromDisplayFunction(displayFunction, data);
|
|
|
|
|
const existingKeys = new Set(base.map((c) => String(c.key)));
|
|
|
|
|
for (const col of fromDisplay) {
|
|
|
|
|
if (!existingKeys.has(String(col.key))) {
|
|
|
|
|
base.push(col);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getCellValue<T>(row: T, col: Column<T>, displayFunction?: TDisplayFunction<T>): any {
|
|
|
|
|
return col.value ? col.value(row) : (row as any)[col.key as any];
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 12:19:59 +00:00
|
|
|
/**
|
|
|
|
|
* Compares two cell values in ascending order. Returns -1, 0, or 1.
|
|
|
|
|
* Null/undefined values sort before defined values. Numbers compare numerically;
|
|
|
|
|
* everything else compares as case-insensitive strings.
|
|
|
|
|
*/
|
|
|
|
|
export function compareCellValues(va: any, vb: any): number {
|
|
|
|
|
if (va == null && vb == null) return 0;
|
|
|
|
|
if (va == null) return -1;
|
|
|
|
|
if (vb == null) return 1;
|
|
|
|
|
if (typeof va === 'number' && typeof vb === 'number') {
|
|
|
|
|
if (va < vb) return -1;
|
|
|
|
|
if (va > vb) return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const sa = String(va).toLowerCase();
|
|
|
|
|
const sb = String(vb).toLowerCase();
|
|
|
|
|
if (sa < sb) return -1;
|
|
|
|
|
if (sa > sb) return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 14:53:59 +00:00
|
|
|
export function getViewData<T>(
|
|
|
|
|
data: T[],
|
|
|
|
|
effectiveColumns: Column<T>[],
|
2026-04-07 12:19:59 +00:00
|
|
|
sortBy: ISortDescriptor[],
|
2025-09-16 15:17:33 +00:00
|
|
|
filterText?: string,
|
2025-09-16 15:46:44 +00:00
|
|
|
columnFilters?: Record<string, string>,
|
|
|
|
|
filterMode: 'table' | 'data' = 'table',
|
|
|
|
|
lucenePredicate?: (row: T) => boolean
|
2025-09-16 14:53:59 +00:00
|
|
|
): T[] {
|
|
|
|
|
let arr = data.slice();
|
|
|
|
|
const ft = (filterText || '').trim().toLowerCase();
|
2025-09-16 15:17:33 +00:00
|
|
|
const cf = columnFilters || {};
|
|
|
|
|
const cfKeys = Object.keys(cf).filter((k) => (cf[k] ?? '').trim().length > 0);
|
|
|
|
|
if (ft || cfKeys.length > 0) {
|
2025-09-16 14:53:59 +00:00
|
|
|
arr = arr.filter((row) => {
|
2025-09-16 15:17:33 +00:00
|
|
|
// column filters (AND across columns)
|
|
|
|
|
for (const k of cfKeys) {
|
2025-09-16 15:46:44 +00:00
|
|
|
if (filterMode === 'data') {
|
|
|
|
|
// raw object check for that key
|
|
|
|
|
const val = (row as any)[k];
|
|
|
|
|
const s = String(val ?? '').toLowerCase();
|
|
|
|
|
const needle = String(cf[k]).toLowerCase();
|
|
|
|
|
if (!s.includes(needle)) return false;
|
|
|
|
|
} else {
|
|
|
|
|
const col = effectiveColumns.find((c) => String(c.key) === k);
|
|
|
|
|
if (!col || col.hidden || col.filterable === false) continue;
|
|
|
|
|
const val = getCellValue(row, col);
|
|
|
|
|
const s = String(val ?? '').toLowerCase();
|
|
|
|
|
const needle = String(cf[k]).toLowerCase();
|
|
|
|
|
if (!s.includes(needle)) return false;
|
|
|
|
|
}
|
2025-09-16 14:53:59 +00:00
|
|
|
}
|
2025-09-16 15:46:44 +00:00
|
|
|
// global filter (OR across visible columns) or lucene predicate
|
2025-09-16 15:17:33 +00:00
|
|
|
if (ft) {
|
2025-09-16 15:46:44 +00:00
|
|
|
if (lucenePredicate) {
|
|
|
|
|
if (!lucenePredicate(row)) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-09-16 15:17:33 +00:00
|
|
|
let any = false;
|
2025-09-16 15:46:44 +00:00
|
|
|
if (filterMode === 'data') {
|
|
|
|
|
for (const val of Object.values(row as any)) {
|
|
|
|
|
const s = String(val ?? '').toLowerCase();
|
|
|
|
|
if (s.includes(ft)) { any = true; break; }
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (const col of effectiveColumns) {
|
|
|
|
|
if (col.hidden) continue;
|
|
|
|
|
const val = getCellValue(row, col);
|
|
|
|
|
const s = String(val ?? '').toLowerCase();
|
|
|
|
|
if (s.includes(ft)) { any = true; break; }
|
2025-09-16 15:17:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!any) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2025-09-16 14:53:59 +00:00
|
|
|
});
|
|
|
|
|
}
|
2026-04-07 12:19:59 +00:00
|
|
|
if (!sortBy || sortBy.length === 0) return arr;
|
|
|
|
|
// Pre-resolve descriptors -> columns once for performance.
|
|
|
|
|
const resolved = sortBy
|
|
|
|
|
.map((desc) => ({ desc, col: effectiveColumns.find((c) => String(c.key) === desc.key) }))
|
|
|
|
|
.filter((entry): entry is { desc: ISortDescriptor; col: Column<T> } => !!entry.col);
|
|
|
|
|
if (resolved.length === 0) return arr;
|
2025-09-16 14:53:59 +00:00
|
|
|
arr.sort((a, b) => {
|
2026-04-07 12:19:59 +00:00
|
|
|
for (const { desc, col } of resolved) {
|
|
|
|
|
const cmp = compareCellValues(getCellValue(a, col), getCellValue(b, col));
|
|
|
|
|
if (cmp !== 0) return desc.dir === 'asc' ? cmp : -cmp;
|
|
|
|
|
}
|
2025-09-16 14:53:59 +00:00
|
|
|
return 0;
|
|
|
|
|
});
|
|
|
|
|
return arr;
|
|
|
|
|
}
|