@opencorestack/opengridx 3.5.1 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +2 -0
- package/dist/ai.cjs +4 -2
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.es.js +4 -2
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +32 -0
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6573 -6390
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +14 -1
- package/docs/README.md +2 -0
- package/docs/features/ai-toolkit.md +2 -2
- package/docs/features/export-guide.md +2 -0
- package/docs/features/header-filters.md +34 -0
- package/docs/features/keyboard-navigation.md +2 -0
- package/docs/features/nextjs.md +241 -0
- package/docs/features/pdf-export.md +2 -0
- package/docs/features/pinning.md +2 -0
- package/docs/features/rtl.md +58 -0
- package/docs/performance.md +86 -118
- package/docs/roadmap.md +4 -2
- package/lib/ai/promptHandler.ts +12 -8
- package/lib/ai/validate.ts +5 -2
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
- package/lib/components/Cell/Cell.css +29 -9
- package/lib/components/Cell/Cell.tsx +2 -2
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
- package/lib/components/DataGrid/DataGrid.css +12 -6
- package/lib/components/DataGrid/DataGrid.tsx +1 -0
- package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
- package/lib/components/DataGrid/GridListView.tsx +3 -1
- package/lib/components/Header/ColumnMenu.css +1 -1
- package/lib/components/Header/ColumnMenu.tsx +14 -6
- package/lib/components/Header/Header.css +18 -10
- package/lib/components/Header/Header.tsx +16 -9
- package/lib/components/Header/headerColumnStyle.ts +3 -2
- package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
- package/lib/components/ListView/ListViewRow.tsx +1 -1
- package/lib/components/Pagination/Pagination.css +4 -0
- package/lib/components/Pagination/Pagination.tsx +4 -4
- package/lib/components/Row/Row.tsx +3 -3
- package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
- package/lib/components/Toolbar/GridToolbar.tsx +8 -28
- package/lib/components/Toolbar/PivotPanel.tsx +4 -7
- package/lib/components/ui/ExpandIcon.css +13 -0
- package/lib/components/ui/ExpandIcon.tsx +2 -4
- package/lib/hooks/core/useGridCellSelection.ts +20 -5
- package/lib/hooks/core/useGridColumns.tsx +2 -2
- package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
- package/lib/hooks/core/useGridScrollSync.ts +28 -5
- package/lib/hooks/core/useLayout.ts +13 -6
- package/lib/hooks/useRowGrouping.ts +9 -9
- package/lib/types/index.ts +30 -0
- package/lib/utils/aggregation/index.ts +56 -1
- package/lib/utils/direction.ts +52 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
- package/lib/utils/export/exportToPdf.ts +2 -1
- package/lib/utils/filtering/index.ts +58 -6
- package/lib/utils/headerFilterLocale.ts +37 -0
- package/lib/utils/scroll/index.ts +5 -3
- package/lib/utils/sorting/index.ts +141 -26
- package/lib/utils/values.ts +10 -6
- package/llms.txt +4 -2
- package/package.json +1 -1
|
@@ -358,8 +358,15 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
358
358
|
groupSubtotalFillColor = '#f0f4ff',
|
|
359
359
|
} = options;
|
|
360
360
|
|
|
361
|
-
// Lazy-load ExcelJS to keep initial bundle lean
|
|
362
|
-
|
|
361
|
+
// Lazy-load ExcelJS to keep initial bundle lean. The import must stay inside `try`: webpack
|
|
362
|
+
// (Next.js 15, plain webpack 5) and Turbopack (Next.js 16) treat an unresolvable import() in a
|
|
363
|
+
// try block as optional, so apps without the peer still build. Outside `try` the build fails.
|
|
364
|
+
let ExcelJS: typeof import('exceljs');
|
|
365
|
+
try {
|
|
366
|
+
ExcelJS = (await import('exceljs')).default;
|
|
367
|
+
} catch {
|
|
368
|
+
throw new Error("exportToExcelAdvanced requires 'exceljs'. Run: npm install exceljs");
|
|
369
|
+
}
|
|
363
370
|
const workbook = new ExcelJS.Workbook();
|
|
364
371
|
workbook.creator = 'OpenGridX';
|
|
365
372
|
workbook.created = new Date();
|
|
@@ -206,7 +206,8 @@ export async function runPdfExport<R extends GridRowModel>(
|
|
|
206
206
|
};
|
|
207
207
|
checkAbort();
|
|
208
208
|
|
|
209
|
-
// Lazy-load peer deps — provides a clear error if not installed
|
|
209
|
+
// Lazy-load peer deps — provides a clear error if not installed. Keep the imports inside `try`:
|
|
210
|
+
// webpack and Turbopack only build an app without the peers when the import() sits in a try block.
|
|
210
211
|
let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
211
212
|
let autoTable: AutoTableFn;
|
|
212
213
|
try {
|
|
@@ -255,15 +255,56 @@ function getCachedQuickFilterTexts(row: GridRowModel, columns: GridColumnLookup
|
|
|
255
255
|
return texts;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
function
|
|
259
|
-
|
|
258
|
+
function quickFilterTerms(values: readonly string[] | undefined): string[] {
|
|
259
|
+
return (values ?? [])
|
|
260
260
|
.map(value => String(value ?? '').trim().toLowerCase())
|
|
261
261
|
.filter(term => term !== '');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function textsMatchTerms(texts: readonly string[], terms: readonly string[]): boolean {
|
|
265
|
+
// Plain loops: this runs once per row on every keystroke.
|
|
266
|
+
for (let t = 0; t < terms.length; t++) {
|
|
267
|
+
const term = terms[t];
|
|
268
|
+
let found = false;
|
|
269
|
+
for (let i = 0; i < texts.length; i++) {
|
|
270
|
+
if (texts[i].includes(term)) {
|
|
271
|
+
found = true;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!found) return false;
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function compileQuickFilter(values: readonly string[] | undefined, columns: GridColumnLookup | undefined): RowPredicate | null {
|
|
281
|
+
const terms = quickFilterTerms(values);
|
|
262
282
|
if (terms.length === 0) return null;
|
|
263
|
-
return (row) =>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
283
|
+
return (row) => textsMatchTerms(getCachedQuickFilterTexts(row, columns), terms);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// filterRows also keeps the texts of the last rows array it searched, by position, per column
|
|
287
|
+
// lookup. Looking a text up by index is much cheaper than one WeakMap entry per row, and after an
|
|
288
|
+
// edit (a new array with mostly the same row objects at the same positions) only the rows whose
|
|
289
|
+
// object changed are read again.
|
|
290
|
+
interface QuickFilterArrayTexts {
|
|
291
|
+
rows: readonly GridRowModel[];
|
|
292
|
+
texts: string[][];
|
|
293
|
+
}
|
|
294
|
+
const quickFilterArrayCache = new WeakMap<GridColumnLookup, QuickFilterArrayTexts>();
|
|
295
|
+
|
|
296
|
+
function getQuickFilterTextsByIndex(rows: readonly GridRowModel[], columns: GridColumnLookup): string[][] {
|
|
297
|
+
const previous = quickFilterArrayCache.get(columns);
|
|
298
|
+
if (previous && previous.rows === rows) return previous.texts;
|
|
299
|
+
const texts = new Array<string[]>(rows.length);
|
|
300
|
+
for (let i = 0; i < rows.length; i++) {
|
|
301
|
+
const row = rows[i];
|
|
302
|
+
texts[i] = previous && i < previous.rows.length && previous.rows[i] === row
|
|
303
|
+
? previous.texts[i]
|
|
304
|
+
: getQuickFilterTexts(row, columns);
|
|
305
|
+
}
|
|
306
|
+
quickFilterArrayCache.set(columns, { rows, texts });
|
|
307
|
+
return texts;
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
/**
|
|
@@ -333,6 +374,17 @@ export function filterRows<R extends GridRowModel>(
|
|
|
333
374
|
filterModel: GridFilterModel,
|
|
334
375
|
columns?: GridColumnLookup
|
|
335
376
|
): R[] {
|
|
377
|
+
const terms = columns && filterModel ? quickFilterTerms(filterModel.quickFilterValues) : [];
|
|
378
|
+
if (columns && terms.length > 0) {
|
|
379
|
+
// The same result as createRowFilter's predicate: the quick filter and then the items.
|
|
380
|
+
const itemsPredicate = compileGroup(filterModel.items ?? [], filterModel.logicOperator ?? 'and', { columns, warned: new Set() });
|
|
381
|
+
const texts = getQuickFilterTextsByIndex(rows, columns);
|
|
382
|
+
const result: R[] = [];
|
|
383
|
+
for (let i = 0; i < rows.length; i++) {
|
|
384
|
+
if (textsMatchTerms(texts[i], terms) && (!itemsPredicate || itemsPredicate(rows[i]))) result.push(rows[i]);
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
336
388
|
const predicate = createRowFilter(filterModel, columns);
|
|
337
389
|
if (!predicate) return rows;
|
|
338
390
|
return rows.filter(row => predicate(row));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GridFilterOperator, GridLocaleText } from '../types';
|
|
2
|
+
import { HEADER_FILTER_OPERATOR_LABELS } from './headerFilters';
|
|
3
|
+
|
|
4
|
+
/** The header filter row's strings, with the English defaults filled in. */
|
|
5
|
+
export interface HeaderFilterLocale {
|
|
6
|
+
custom: string;
|
|
7
|
+
customTooltip: string;
|
|
8
|
+
cellLabel: (header: string) => string;
|
|
9
|
+
valueLabel: (header: string) => string;
|
|
10
|
+
operatorButtonLabel: (header: string, operatorLabel: string) => string;
|
|
11
|
+
operatorMenuLabel: (header: string) => string;
|
|
12
|
+
clearLabel: (header: string) => string;
|
|
13
|
+
clearTooltip: string;
|
|
14
|
+
any: string;
|
|
15
|
+
yes: string;
|
|
16
|
+
no: string;
|
|
17
|
+
operator: (operator: GridFilterOperator) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Resolves the header filter strings of `localeText`, falling back to English. */
|
|
21
|
+
export function resolveHeaderFilterLocale(localeText: GridLocaleText | undefined): HeaderFilterLocale {
|
|
22
|
+
const operators = localeText?.headerFilterOperators;
|
|
23
|
+
return {
|
|
24
|
+
custom: localeText?.headerFilterCustom ?? 'Custom filter',
|
|
25
|
+
customTooltip: localeText?.headerFilterCustomTooltip ?? 'Set in the filter panel',
|
|
26
|
+
cellLabel: localeText?.headerFilterCellLabel ?? (h => `Filter ${h}`),
|
|
27
|
+
valueLabel: localeText?.headerFilterValueLabel ?? (h => `Filter value for ${h}`),
|
|
28
|
+
operatorButtonLabel: localeText?.headerFilterOperatorButtonLabel ?? ((h, op) => `Filter operator for ${h}: ${op}`),
|
|
29
|
+
operatorMenuLabel: localeText?.headerFilterOperatorMenuLabel ?? (h => `Filter operator for ${h}`),
|
|
30
|
+
clearLabel: localeText?.headerFilterClearLabel ?? (h => `Clear filter for ${h}`),
|
|
31
|
+
clearTooltip: localeText?.headerFilterClearTooltip ?? 'Clear',
|
|
32
|
+
any: localeText?.headerFilterAny ?? 'Any',
|
|
33
|
+
yes: localeText?.headerFilterTrue ?? 'Yes',
|
|
34
|
+
no: localeText?.headerFilterFalse ?? 'No',
|
|
35
|
+
operator: op => operators?.[op] ?? HEADER_FILTER_OPERATOR_LABELS[op] ?? op,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getScrollStart, setScrollStart } from '../direction';
|
|
1
2
|
/**
|
|
2
3
|
* Height of the sticky block at the bottom of the viewport (bottom-pinned rows, their expanded
|
|
3
4
|
* detail panels and the aggregation footer), measured from the DOM.
|
|
@@ -68,11 +69,12 @@ export function scrollColumnIntoView(viewport: HTMLElement, unpinnedIndex: numbe
|
|
|
68
69
|
const colLeft = contentStart + (unpinnedIndex > 0 ? unpinnedAccWidths[unpinnedIndex - 1] : 0);
|
|
69
70
|
const colRight = contentStart + unpinnedAccWidths[unpinnedIndex];
|
|
70
71
|
const stickyLeft = pinnedSystemColumnsWidth + leftWidth;
|
|
71
|
-
const {
|
|
72
|
+
const { clientWidth } = viewport;
|
|
73
|
+
const scrollLeft = getScrollStart(viewport);
|
|
72
74
|
|
|
73
75
|
if (colLeft < scrollLeft + stickyLeft) {
|
|
74
|
-
viewport
|
|
76
|
+
setScrollStart(viewport, Math.max(0, colLeft - stickyLeft));
|
|
75
77
|
} else if (colRight > scrollLeft + clientWidth - rightWidth) {
|
|
76
|
-
viewport
|
|
78
|
+
setScrollStart(viewport, colRight - clientWidth + rightWidth);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -154,16 +154,20 @@ export function sortRows<R extends GridRowModel>(
|
|
|
154
154
|
if (sortModel.length === 0) {
|
|
155
155
|
return rows;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
if (rows.length < 2) return [...rows];
|
|
158
|
+
const sortColumns = sortModel.map(sortItem => {
|
|
158
159
|
const colDef = columns?.byField.get(sortItem.field);
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
160
|
+
const comparator = colDef?.sortComparator;
|
|
161
|
+
if (comparator) {
|
|
162
|
+
return genericSortColumn(rows.map(row => {
|
|
163
|
+
const value = getCellValue(row, sortItem.field, colDef);
|
|
164
|
+
return { value, type: colDef.type, comparator, params: { id: getRowId(row), field: sortItem.field, row, value } };
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
const type = colDef?.type;
|
|
168
|
+
return encodeSortColumn(rows.length, i => getCellValue(rows[i], sortItem.field, colDef), () => type);
|
|
166
169
|
});
|
|
170
|
+
return sortByColumns(rows, sortModel, sortColumns);
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
/** The value an item sorts by for one sort key, and how to compare it. */
|
|
@@ -194,26 +198,137 @@ export function sortItemsBySortModel<T>(
|
|
|
194
198
|
return [...items];
|
|
195
199
|
}
|
|
196
200
|
|
|
197
|
-
// Read
|
|
198
|
-
//
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const b = values[k][ib];
|
|
207
|
-
const ascending = a.comparator && a.comparator === b.comparator && a.params && b.params
|
|
208
|
-
? callSortComparator(a.comparator, a.params, b.params)
|
|
209
|
-
: compareSortKeys(keys[k][ia], keys[k][ib]);
|
|
210
|
-
const comparison = applySortDirection(ascending, sortModel[k].sort);
|
|
211
|
-
if (comparison !== 0) return comparison;
|
|
212
|
-
}
|
|
213
|
-
return ia - ib;
|
|
201
|
+
// Read every sort value once, instead of calling valueGetter and parsing dates O(n log n) times
|
|
202
|
+
// inside the comparator.
|
|
203
|
+
const columns = sortModel.map(sortItem => {
|
|
204
|
+
const values = items.map(item => readValue(item, sortItem));
|
|
205
|
+
// A key where no item carries a comparator compares by the built-in order only, so it can be
|
|
206
|
+
// encoded up front.
|
|
207
|
+
return values.some(v => v.comparator && v.params)
|
|
208
|
+
? genericSortColumn(values)
|
|
209
|
+
: encodeSortColumn(values.length, i => values[i].value, i => values[i].type);
|
|
214
210
|
});
|
|
211
|
+
return sortByColumns(items, sortModel, columns);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** One sort key, ready to compare: encoded into typed arrays, or kept as values when a sortComparator applies. */
|
|
215
|
+
type SortColumn =
|
|
216
|
+
| { kind: 'encoded'; group: Uint8Array; num: Float64Array }
|
|
217
|
+
| { kind: 'generic'; values: readonly GridSortValue[]; keys: readonly SortKey[] };
|
|
218
|
+
|
|
219
|
+
/** The group of the empty bucket: after every rank, as in compareSortKeys. */
|
|
220
|
+
const GROUP_EMPTY = RANK_OTHER + 1;
|
|
221
|
+
|
|
222
|
+
/** Give each item of `buckets` (distinct text → item indexes) its text's position in collator order. */
|
|
223
|
+
function rankTexts(buckets: Map<string, number[]>, num: Float64Array): void {
|
|
224
|
+
const distinct = [...buckets.keys()].sort(collator.compare);
|
|
225
|
+
// Texts the collator calls equal share a position, so they tie exactly as in compareSortKeys.
|
|
226
|
+
let position = 0;
|
|
227
|
+
for (let d = 0; d < distinct.length; d++) {
|
|
228
|
+
if (d > 0 && collator.compare(distinct[d - 1], distinct[d]) !== 0) position++;
|
|
229
|
+
for (const i of buckets.get(distinct[d]) ?? []) num[i] = position;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function addToBucket(buckets: Map<string, number[]>, text: string, index: number): void {
|
|
234
|
+
const bucket = buckets.get(text);
|
|
235
|
+
if (bucket) bucket.push(index);
|
|
236
|
+
else buckets.set(text, [index]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Encode one key as (group, number) per item. The group is the value's rank, with the empty bucket
|
|
241
|
+
* last; the number is the value itself for numbers, dates and booleans, and for strings (and other
|
|
242
|
+
* values) the position of its text among the key's distinct texts in collator order. Comparing
|
|
243
|
+
* (group, number) gives exactly compareSortKeys' order, while the collator runs only to sort the
|
|
244
|
+
* distinct texts instead of in every row comparison.
|
|
245
|
+
*/
|
|
246
|
+
function encodeSortColumn(count: number, valueAt: (i: number) => unknown, typeAt: (i: number) => GridColDef['type']): SortColumn {
|
|
247
|
+
const group = new Uint8Array(count);
|
|
248
|
+
const num = new Float64Array(count);
|
|
249
|
+
const strings = new Map<string, number[]>();
|
|
250
|
+
const others = new Map<string, number[]>();
|
|
251
|
+
for (let i = 0; i < count; i++) {
|
|
252
|
+
const value = valueAt(i);
|
|
253
|
+
const type = typeAt(i);
|
|
254
|
+
if (typeof value === 'number' && type !== 'date') {
|
|
255
|
+
// toSortKey for a number, without the allocation.
|
|
256
|
+
if (Number.isNaN(value)) {
|
|
257
|
+
group[i] = GROUP_EMPTY;
|
|
258
|
+
} else {
|
|
259
|
+
group[i] = RANK_NUMBER;
|
|
260
|
+
num[i] = value;
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const key = toSortKey(value, type);
|
|
265
|
+
if (key === null) {
|
|
266
|
+
group[i] = GROUP_EMPTY;
|
|
267
|
+
} else {
|
|
268
|
+
group[i] = key.rank;
|
|
269
|
+
if (key.rank === RANK_STRING) addToBucket(strings, key.str, i);
|
|
270
|
+
else if (key.rank === RANK_OTHER) addToBucket(others, key.str, i);
|
|
271
|
+
else num[i] = key.num;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
rankTexts(strings, num);
|
|
275
|
+
rankTexts(others, num);
|
|
276
|
+
return { kind: 'encoded', group, num };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function genericSortColumn(values: readonly GridSortValue[]): SortColumn {
|
|
280
|
+
return { kind: 'generic', values, keys: values.map(({ value, type }) => toSortKey(value, type)) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Ascending comparison of items `ia` and `ib` on one key. */
|
|
284
|
+
function compareInColumn(column: SortColumn, ia: number, ib: number): number {
|
|
285
|
+
if (column.kind === 'encoded') {
|
|
286
|
+
const ga = column.group[ia];
|
|
287
|
+
const gb = column.group[ib];
|
|
288
|
+
if (ga !== gb) return ga - gb;
|
|
289
|
+
const na = column.num[ia];
|
|
290
|
+
const nb = column.num[ib];
|
|
291
|
+
return na < nb ? -1 : na > nb ? 1 : 0;
|
|
292
|
+
}
|
|
293
|
+
const a = column.values[ia];
|
|
294
|
+
const b = column.values[ib];
|
|
295
|
+
return a.comparator && a.comparator === b.comparator && a.params && b.params
|
|
296
|
+
? callSortComparator(a.comparator, a.params, b.params)
|
|
297
|
+
: compareSortKeys(column.keys[ia], column.keys[ib]);
|
|
298
|
+
}
|
|
215
299
|
|
|
216
|
-
|
|
300
|
+
/** Stable sort by prepared keys: the first key that differs decides, ties keep the input order. */
|
|
301
|
+
function sortByColumns<T>(items: readonly T[], sortModel: readonly GridSortItem[], columns: readonly SortColumn[]): T[] {
|
|
302
|
+
const n = items.length;
|
|
303
|
+
const order = new Array<number>(n);
|
|
304
|
+
for (let i = 0; i < n; i++) order[i] = i;
|
|
305
|
+
const first = columns[0];
|
|
306
|
+
if (columns.length === 1 && first.kind === 'encoded') {
|
|
307
|
+
// The common single-key case, without the per-comparison key loop.
|
|
308
|
+
const { group, num } = first;
|
|
309
|
+
const sign = sortModel[0].sort === 'asc' ? 1 : -1;
|
|
310
|
+
order.sort((ia, ib) => {
|
|
311
|
+
const ga = group[ia];
|
|
312
|
+
const gb = group[ib];
|
|
313
|
+
if (ga !== gb) return (ga - gb) * sign;
|
|
314
|
+
const na = num[ia];
|
|
315
|
+
const nb = num[ib];
|
|
316
|
+
if (na < nb) return -sign;
|
|
317
|
+
if (na > nb) return sign;
|
|
318
|
+
return ia - ib;
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
order.sort((ia, ib) => {
|
|
322
|
+
for (let k = 0; k < columns.length; k++) {
|
|
323
|
+
const comparison = applySortDirection(compareInColumn(columns[k], ia, ib), sortModel[k].sort);
|
|
324
|
+
if (comparison !== 0) return comparison;
|
|
325
|
+
}
|
|
326
|
+
return ia - ib;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const result = new Array<T>(n);
|
|
330
|
+
for (let i = 0; i < n; i++) result[i] = items[order[i]];
|
|
331
|
+
return result;
|
|
217
332
|
}
|
|
218
333
|
|
|
219
334
|
/**
|
package/lib/utils/values.ts
CHANGED
|
@@ -53,6 +53,9 @@ export function tryGetCellValue<R extends GridRowModel>(
|
|
|
53
53
|
* per column), so one bad record cannot take the whole grid down.
|
|
54
54
|
*/
|
|
55
55
|
export function getCellValue<R extends GridRowModel>(row: R, field: string, colDef?: ValueGetterColumn<R>): unknown {
|
|
56
|
+
// Plain columns read the field directly: this runs once per row and column in every
|
|
57
|
+
// whole-dataset pass (sort, filter, grouping, aggregation), so it must not allocate.
|
|
58
|
+
if (!colDef?.valueGetter) return row[field];
|
|
56
59
|
const { value, error } = tryGetCellValue(row, field, colDef);
|
|
57
60
|
if (error !== undefined) {
|
|
58
61
|
warnCallbackFailure(`valueGetter:${field}`, `valueGetter for column "${field}" threw; the value is read as undefined`, error);
|
|
@@ -72,12 +75,13 @@ export function getFormattedValue<R extends GridRowModel>(
|
|
|
72
75
|
): string | undefined {
|
|
73
76
|
const formatter = colDef?.valueFormatter;
|
|
74
77
|
if (!formatter) return undefined;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`valueFormatter for column "${field}" threw; the value is shown unformatted`,
|
|
80
|
-
|
|
78
|
+
// callSafely inlined: this runs for every cell of the quick filter's first search.
|
|
79
|
+
try {
|
|
80
|
+
return formatter({ value, row, field });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
warnCallbackFailure(`valueFormatter:${field}`, `valueFormatter for column "${field}" threw; the value is shown unformatted`, error);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/**
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenGridX — AI Agent Context
|
|
2
2
|
# Package: @opencorestack/opengridx
|
|
3
|
-
# Version: 3.
|
|
3
|
+
# Version: 3.6.0
|
|
4
4
|
# Docs: https://opencorestack.github.io/OpenGridX/
|
|
5
5
|
# This file is machine-readable. AI developers: start here.
|
|
6
6
|
|
|
@@ -75,7 +75,7 @@ export default function App() {
|
|
|
75
75
|
| autoHeight | boolean | false | Fit height to rows (renders every row) |
|
|
76
76
|
| overscanRowCount | number | 3 | Minimum rows rendered outside the viewport (raised automatically while scrolling fast) |
|
|
77
77
|
| ariaLabel | string | — | Accessible name for the grid |
|
|
78
|
-
| localeText | GridLocaleText | — | Override
|
|
78
|
+
| localeText | GridLocaleText | — | Override strings: paginationRowsPerPage, paginationOf(from,to,count), paginationPage(page,count), noRowsLabel, cellSelectionAnnouncement, aiAssistantButton; header filter row (3.6): headerFilterCustom, headerFilterCustomTooltip, headerFilterCellLabel(h), headerFilterValueLabel(h), headerFilterOperatorButtonLabel(h, op), headerFilterOperatorMenuLabel(h), headerFilterClearLabel(h), headerFilterClearTooltip, headerFilterAny, headerFilterTrue, headerFilterFalse, headerFilterOperators (Partial<Record<GridFilterOperator,string>>) |
|
|
79
79
|
| rowHeight | number | 52 | Row height in pixels |
|
|
80
80
|
| headerHeight | number | 56 | Header height in pixels |
|
|
81
81
|
| wrapHeaderText | boolean | false | Wrap header titles within headerHeight (2 lines at 56px, 3 at 72, 4 at 88; ellipsis on the last line). Header never grows: raise headerHeight for more lines. Class ogx__header-cell--wrap. Per column: GridColDef.wrapHeaderText (3.3.0+) |
|
|
@@ -725,3 +725,5 @@ Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, Pa
|
|
|
725
725
|
17. **Server modes without dataSource**: with `paginationMode` / `sortingMode` / `filterMode` = `'server'` the grid shows `rows` as given (no slicing, sorting or filtering). Fetch from the change callbacks and control the models.
|
|
726
726
|
18. **`valueFormatter` / `valueGetter` params are `unknown`-typed**: cast (`(value as number).toFixed(2)`) before calling methods, or TypeScript strict mode rejects it.
|
|
727
727
|
19. **Inline `columns` are reused while shallow-equal**: a new array with the same column objects does not re-run filtering/sorting, but mutating a column object in place is not detected — pass new objects when a definition changes.
|
|
728
|
+
20. **Next.js App Router**: render `<DataGrid>` from a `'use client'` file (column functions such as `valueFormatter` cannot cross from a Server Component) and import the stylesheet in `app/layout.tsx`. Rows fetched in a Server Component pass as props (serialisable data only). The ES bundle starts with `'use client'` (3.6+), so a Server Component may also render `<DataGrid>` with serialisable props. `next build` fails with `Can't resolve 'exceljs'` unless the optional peers are installed or aliased in `next.config.ts` (`turbopack.resolveAlias` to an empty module; webpack `resolve.alias[name] = false`). `@opencorestack/opengridx/ai` runs on the server too, but the validation brand is a Symbol that does not survive JSON: validate on the client (`createGridAiPromptHandler`). Guide: `docs/features/nextjs.md`.
|
|
729
|
+
21. **Right-to-left**: wrap the grid in `dir="rtl"` (or set `document.dir`); no prop. Works without pinned columns (3.6+): column order, text alignment (`align: 'left'`/'right' = start/end), horizontal scrolling + column virtualization, ArrowLeft = next column, resize handle on the left edge, fill handle bottom-left, toolbar panels open rightwards. **Pinned columns are not supported in RTL** (they do not stick); do not pin in RTL grids. Guide: `docs/features/rtl.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/opengridx.umd.js",
|