@opencorestack/opengridx 1.2.1 → 1.2.3
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 +41 -0
- package/dist/index.d.ts +5 -1
- package/dist/opengridx.es.js +2840 -2824
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +11 -11
- package/dist/opengridx.umd.js.map +1 -1
- package/lib/components/Cell/Cell.tsx +1 -1
- package/lib/components/Cell/CellErrorBoundary.tsx +14 -1
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +85 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +7 -4
- package/lib/components/DataGrid/DataGrid.tsx +1 -0
- package/lib/components/Toolbar/GridToolbar.tsx +8 -1
- package/lib/hooks/core/useDataGrid.ts +1 -0
- package/lib/types/index.ts +2 -0
- package/lib/utils/export/exportToPdf.test.ts +5 -6
- package/lib/utils/export/exportToPdf.ts +9 -7
- package/package.json +1 -1
|
@@ -241,7 +241,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
241
241
|
) : colDef.renderCell ? (
|
|
242
242
|
<CellErrorBoundary
|
|
243
243
|
field={colDef.field}
|
|
244
|
-
resetKey={
|
|
244
|
+
resetKey={row}
|
|
245
245
|
renderFn={() => colDef.renderCell!({
|
|
246
246
|
value,
|
|
247
247
|
row,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
// Must live at module scope (not inline) so React sees a stable component type.
|
|
4
|
+
// Calling renderFn() here puts the throw one level BELOW CellErrorBoundary,
|
|
5
|
+
// which is required — a component cannot catch errors thrown in its own render().
|
|
6
|
+
function CellRenderTarget({ renderFn }: { renderFn: () => React.ReactNode }) {
|
|
7
|
+
return <>{renderFn()}</>;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
interface CellErrorBoundaryProps {
|
|
4
11
|
renderFn: () => React.ReactNode;
|
|
5
12
|
field: string;
|
|
@@ -35,6 +42,12 @@ export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, C
|
|
|
35
42
|
return { hasError: true, error };
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
override componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
46
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
47
|
+
console.warn(`[CellErrorBoundary] field="${this.props.field}"`, error, info.componentStack);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
override render() {
|
|
39
52
|
if (this.state.hasError) {
|
|
40
53
|
return (
|
|
@@ -48,6 +61,6 @@ export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, C
|
|
|
48
61
|
</div>
|
|
49
62
|
);
|
|
50
63
|
}
|
|
51
|
-
return this.props.renderFn
|
|
64
|
+
return <CellRenderTarget renderFn={this.props.renderFn} />;
|
|
52
65
|
}
|
|
53
66
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ColumnVisibilityPanel } from './ColumnVisibilityPanel';
|
|
4
|
+
import type { GridColDef } from '../../types';
|
|
5
|
+
|
|
6
|
+
const columns: GridColDef[] = [
|
|
7
|
+
{ field: 'name', headerName: 'Name' },
|
|
8
|
+
{ field: 'department', headerName: 'Department' },
|
|
9
|
+
{ field: 'groupKey', headerName: 'Group Key', hideable: false },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const visibleColumns = new Set(['name', 'department', 'groupKey']);
|
|
13
|
+
|
|
14
|
+
const noop = () => {};
|
|
15
|
+
|
|
16
|
+
describe('ColumnVisibilityPanel', () => {
|
|
17
|
+
it('excludes hideable:false columns from the list by default', () => {
|
|
18
|
+
render(
|
|
19
|
+
<ColumnVisibilityPanel
|
|
20
|
+
columns={columns}
|
|
21
|
+
visibleColumns={visibleColumns}
|
|
22
|
+
onVisibilityChange={noop}
|
|
23
|
+
onShowAll={noop}
|
|
24
|
+
onHideAll={noop}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText('Department')).toBeInTheDocument();
|
|
29
|
+
expect(screen.queryByText('Group Key')).not.toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('shows hideable:false columns as disabled when showNonHideableColumns is true', () => {
|
|
33
|
+
render(
|
|
34
|
+
<ColumnVisibilityPanel
|
|
35
|
+
columns={columns}
|
|
36
|
+
visibleColumns={visibleColumns}
|
|
37
|
+
onVisibilityChange={noop}
|
|
38
|
+
onShowAll={noop}
|
|
39
|
+
onHideAll={noop}
|
|
40
|
+
showNonHideableColumns
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText('Department')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText('Group Key')).toBeInTheDocument();
|
|
46
|
+
|
|
47
|
+
// 3 columns shown + 1 "Show/Hide All" checkbox in footer = 4 checkboxes
|
|
48
|
+
// The 3rd column (Group Key, hideable:false) must be the disabled one
|
|
49
|
+
const checkboxes = screen.getAllByRole('checkbox');
|
|
50
|
+
const disabledCheckboxes = checkboxes.filter(cb => (cb as HTMLInputElement).disabled);
|
|
51
|
+
expect(disabledCheckboxes).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not render a disabled checkbox for hideable:false columns by default', () => {
|
|
55
|
+
render(
|
|
56
|
+
<ColumnVisibilityPanel
|
|
57
|
+
columns={columns}
|
|
58
|
+
visibleColumns={visibleColumns}
|
|
59
|
+
onVisibilityChange={noop}
|
|
60
|
+
onShowAll={noop}
|
|
61
|
+
onHideAll={noop}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
const checkboxes = screen.getAllByRole('checkbox');
|
|
65
|
+
const disabledCheckboxes = checkboxes.filter(cb => (cb as HTMLInputElement).disabled);
|
|
66
|
+
expect(disabledCheckboxes).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('never shows hideable:false columns even when search would match them', () => {
|
|
70
|
+
// Without showNonHideableColumns, hideable:false columns are not in the base list
|
|
71
|
+
// so they cannot appear regardless of search. This test verifies the base list,
|
|
72
|
+
// not a dynamic search interaction.
|
|
73
|
+
render(
|
|
74
|
+
<ColumnVisibilityPanel
|
|
75
|
+
columns={columns}
|
|
76
|
+
visibleColumns={visibleColumns}
|
|
77
|
+
onVisibilityChange={noop}
|
|
78
|
+
onShowAll={noop}
|
|
79
|
+
onHideAll={noop}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
// "Group Key" is hideable:false — must not appear in the default panel
|
|
83
|
+
expect(screen.queryByText('Group Key')).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -11,6 +11,8 @@ export interface ColumnVisibilityPanelProps<R extends GridRowModel = GridRowMode
|
|
|
11
11
|
onHideAll: () => void;
|
|
12
12
|
onColumnReorder?: (fromField: string, toField: string) => void;
|
|
13
13
|
onColumnOrderReset?: () => void;
|
|
14
|
+
/** Show columns with `hideable: false` in the panel as disabled rows. Default: false. */
|
|
15
|
+
showNonHideableColumns?: boolean;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function SearchIcon() {
|
|
@@ -44,7 +46,7 @@ function DragHandleIcon() {
|
|
|
44
46
|
export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
45
47
|
props: ColumnVisibilityPanelProps<R>
|
|
46
48
|
) {
|
|
47
|
-
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder, onColumnOrderReset } = props;
|
|
49
|
+
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder, onColumnOrderReset, showNonHideableColumns } = props;
|
|
48
50
|
const [searchQuery, setSearchQuery] = useState('');
|
|
49
51
|
const [dragOverField, setDragOverField] = useState<string | null>(null);
|
|
50
52
|
const dragFieldRef = useRef<string | null>(null);
|
|
@@ -53,12 +55,13 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
|
53
55
|
const allVisible = hideableColumns.every(col => visibleColumns.has(col.field));
|
|
54
56
|
|
|
55
57
|
const filteredColumns = useMemo(() => {
|
|
56
|
-
|
|
58
|
+
const base = showNonHideableColumns ? columns : hideableColumns;
|
|
59
|
+
if (!searchQuery) return base;
|
|
57
60
|
const lowerQuery = searchQuery.toLowerCase();
|
|
58
|
-
return
|
|
61
|
+
return base.filter(col =>
|
|
59
62
|
(col.headerName || col.field).toLowerCase().includes(lowerQuery)
|
|
60
63
|
);
|
|
61
|
-
}, [columns, searchQuery]);
|
|
64
|
+
}, [columns, hideableColumns, searchQuery, showNonHideableColumns]);
|
|
62
65
|
|
|
63
66
|
const handleToggleAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
64
67
|
if (e.target.checked) {
|
|
@@ -467,6 +467,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
467
467
|
|
|
468
468
|
useEffect(() => {
|
|
469
469
|
gridData.apiRef.current.getVisibleRows = () => pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
470
|
+
gridData.apiRef.current.getAllFilteredRows = () => sortedUnpinnedRows;
|
|
470
471
|
gridData.apiRef.current.getVisibleColumns = () => effectiveColumns as unknown as GridColDef[];
|
|
471
472
|
}, [sortedUnpinnedRows, paginatedUnpinnedRows, pagination, effectiveColumns, gridData.apiRef]);
|
|
472
473
|
|
|
@@ -74,6 +74,8 @@ export interface GridToolbarProps {
|
|
|
74
74
|
renderQuickFilter?: (props: ToolbarQuickFilterRenderProps) => React.ReactNode;
|
|
75
75
|
/** Additional CSS class applied to the toolbar root element. */
|
|
76
76
|
className?: string;
|
|
77
|
+
/** Show columns with `hideable: false` in the Columns panel as disabled rows. Default: false. */
|
|
78
|
+
showNonHideableColumns?: boolean;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const AGGREGATION_FUNCTIONS = ['none', 'sum', 'avg', 'count', 'min', 'max'] as const;
|
|
@@ -248,7 +250,8 @@ function ColumnsPanelWrapper({
|
|
|
248
250
|
onHideAll,
|
|
249
251
|
onColumnReorder,
|
|
250
252
|
onColumnOrderReset,
|
|
251
|
-
onClose
|
|
253
|
+
onClose,
|
|
254
|
+
showNonHideableColumns,
|
|
252
255
|
}: {
|
|
253
256
|
anchorRef: React.RefObject<HTMLElement | null>;
|
|
254
257
|
columns: GridColDef[];
|
|
@@ -259,6 +262,7 @@ function ColumnsPanelWrapper({
|
|
|
259
262
|
onColumnReorder?: (fromField: string, toField: string) => void;
|
|
260
263
|
onColumnOrderReset?: () => void;
|
|
261
264
|
onClose: () => void;
|
|
265
|
+
showNonHideableColumns?: boolean;
|
|
262
266
|
}) {
|
|
263
267
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
264
268
|
const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
|
|
@@ -317,6 +321,7 @@ function ColumnsPanelWrapper({
|
|
|
317
321
|
onHideAll={onHideAll}
|
|
318
322
|
onColumnReorder={onColumnReorder}
|
|
319
323
|
onColumnOrderReset={onColumnOrderReset}
|
|
324
|
+
showNonHideableColumns={showNonHideableColumns}
|
|
320
325
|
/>
|
|
321
326
|
</div>
|
|
322
327
|
);
|
|
@@ -464,6 +469,7 @@ export function GridToolbar({
|
|
|
464
469
|
renderExportButton,
|
|
465
470
|
renderQuickFilter,
|
|
466
471
|
className,
|
|
472
|
+
showNonHideableColumns,
|
|
467
473
|
}: GridToolbarProps) {
|
|
468
474
|
const [aggOpen, setAggOpen] = useState(false);
|
|
469
475
|
const [pivotOpen, setPivotOpen] = useState(false);
|
|
@@ -631,6 +637,7 @@ export function GridToolbar({
|
|
|
631
637
|
onColumnReorder={onColumnReorder}
|
|
632
638
|
onColumnOrderReset={onColumnOrderReset}
|
|
633
639
|
onClose={() => { setColsOpen(false); onColumnsPanelClose?.(); }}
|
|
640
|
+
showNonHideableColumns={showNonHideableColumns}
|
|
634
641
|
/>
|
|
635
642
|
)}
|
|
636
643
|
</div>
|
|
@@ -299,6 +299,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
|
|
|
299
299
|
getRow: (id: GridRowId) => stateRef.current.rows.idRowsLookup.get(id) || null,
|
|
300
300
|
getAllRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
|
|
301
301
|
getVisibleRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
|
|
302
|
+
getAllFilteredRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
|
|
302
303
|
getAggregationResult: () => null,
|
|
303
304
|
getAggregationModel: () => null,
|
|
304
305
|
getColumn: (field: string) => stateRef.current.columns.lookup.get(field) || null,
|
package/lib/types/index.ts
CHANGED
|
@@ -817,6 +817,8 @@ export interface GridApi {
|
|
|
817
817
|
getAllRows: () => GridRowModel[];
|
|
818
818
|
/** Returns all rows currently visible after filtering and sorting. */
|
|
819
819
|
getVisibleRows: () => GridRowModel[];
|
|
820
|
+
/** Returns all filtered and sorted rows, ignoring pagination. Use this for full-dataset exports. */
|
|
821
|
+
getAllFilteredRows: () => GridRowModel[];
|
|
820
822
|
/** Returns the current aggregation results. */
|
|
821
823
|
getAggregationResult: () => Record<string, unknown> | null;
|
|
822
824
|
/** Returns the active aggregation configuration. */
|
|
@@ -15,7 +15,6 @@ const mockAddImage = vi.fn();
|
|
|
15
15
|
const mockSetPage = vi.fn();
|
|
16
16
|
|
|
17
17
|
class MockJsPDF {
|
|
18
|
-
autoTable = mockAutoTable;
|
|
19
18
|
save = mockSave;
|
|
20
19
|
setFontSize = mockSetFontSize;
|
|
21
20
|
setFont = mockSetFont;
|
|
@@ -35,7 +34,7 @@ class MockJsPDF {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
vi.mock('jspdf', () => ({ default: MockJsPDF }));
|
|
38
|
-
vi.mock('jspdf-autotable', () => ({}));
|
|
37
|
+
vi.mock('jspdf-autotable', () => ({ default: mockAutoTable }));
|
|
39
38
|
|
|
40
39
|
const sampleColumns: GridColDef[] = [
|
|
41
40
|
{ field: 'name', headerName: 'Name', width: 150 },
|
|
@@ -72,7 +71,7 @@ describe('exportToPdf', () => {
|
|
|
72
71
|
it('excludes columns with exportable: false and system columns', async () => {
|
|
73
72
|
const { exportToPdf } = await import('./exportToPdf');
|
|
74
73
|
await exportToPdf(sampleRows, sampleColumns);
|
|
75
|
-
const call = mockAutoTable.mock.calls[0][
|
|
74
|
+
const call = mockAutoTable.mock.calls[0][1];
|
|
76
75
|
// head should only have Name and Salary — not Hidden or __check__
|
|
77
76
|
expect(call.head[0]).toEqual(['Name', 'Salary']);
|
|
78
77
|
});
|
|
@@ -80,7 +79,7 @@ describe('exportToPdf', () => {
|
|
|
80
79
|
it('applies valueFormatter to cell values', async () => {
|
|
81
80
|
const { exportToPdf } = await import('./exportToPdf');
|
|
82
81
|
await exportToPdf(sampleRows, sampleColumns);
|
|
83
|
-
const call = mockAutoTable.mock.calls[0][
|
|
82
|
+
const call = mockAutoTable.mock.calls[0][1];
|
|
84
83
|
// salary column should be formatted
|
|
85
84
|
expect(call.body[0][1]).toBe('$90000');
|
|
86
85
|
expect(call.body[1][1]).toBe('$80000');
|
|
@@ -89,7 +88,7 @@ describe('exportToPdf', () => {
|
|
|
89
88
|
it('filters to selectedRows when provided', async () => {
|
|
90
89
|
const { exportToPdf } = await import('./exportToPdf');
|
|
91
90
|
await exportToPdf(sampleRows, sampleColumns, { selectedRows: [1] });
|
|
92
|
-
const call = mockAutoTable.mock.calls[0][
|
|
91
|
+
const call = mockAutoTable.mock.calls[0][1];
|
|
93
92
|
expect(call.body).toHaveLength(1);
|
|
94
93
|
expect(call.body[0][0]).toBe('Alice');
|
|
95
94
|
});
|
|
@@ -100,7 +99,7 @@ describe('exportToPdf', () => {
|
|
|
100
99
|
aggregationResult: { salary: 170000 },
|
|
101
100
|
aggregationModel: { salary: 'sum' },
|
|
102
101
|
});
|
|
103
|
-
const call = mockAutoTable.mock.calls[0][
|
|
102
|
+
const call = mockAutoTable.mock.calls[0][1];
|
|
104
103
|
expect(call.foot).toBeDefined();
|
|
105
104
|
expect(call.foot[0][1]).toBe('$170000');
|
|
106
105
|
});
|
|
@@ -6,9 +6,7 @@ import type {
|
|
|
6
6
|
} from '../../types';
|
|
7
7
|
import { formatAggregationValue } from '../../hooks/features/useAggregation';
|
|
8
8
|
|
|
9
|
-
// jsPDF plugin augments the jsPDF prototype — we type only what we use
|
|
10
9
|
interface JsPDFDoc {
|
|
11
|
-
autoTable: (opts: AutoTableOptions) => void;
|
|
12
10
|
save: (filename: string) => void;
|
|
13
11
|
setFontSize: (size: number) => void;
|
|
14
12
|
setFont: (name: string, style: string) => void;
|
|
@@ -112,10 +110,14 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
112
110
|
): Promise<void> {
|
|
113
111
|
// Lazy-load peer deps — provides a clear error if not installed
|
|
114
112
|
let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
113
|
+
let autoTable: (doc: JsPDFDoc, opts: AutoTableOptions) => void;
|
|
115
114
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const [jspdfMod, autoTableMod] = await Promise.all([
|
|
116
|
+
import('jspdf'),
|
|
117
|
+
import('jspdf-autotable'),
|
|
118
|
+
]);
|
|
119
|
+
JsPDF = jspdfMod.default as unknown as new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
120
|
+
autoTable = autoTableMod.default as unknown as (doc: JsPDFDoc, opts: AutoTableOptions) => void;
|
|
119
121
|
} catch {
|
|
120
122
|
throw new Error(
|
|
121
123
|
"exportToPdf requires 'jspdf' and 'jspdf-autotable'. Run: npm install jspdf jspdf-autotable"
|
|
@@ -235,7 +237,7 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
235
237
|
const [hr, hg, hb] = hexToRgb(headerBackgroundColor);
|
|
236
238
|
const [tr, tg, tb] = hexToRgb(headerTextColor);
|
|
237
239
|
|
|
238
|
-
|
|
240
|
+
autoTable(doc, {
|
|
239
241
|
head,
|
|
240
242
|
body,
|
|
241
243
|
...(foot ? { foot } : {}),
|
|
@@ -250,7 +252,7 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
250
252
|
? { alternateRowStyles: { fillColor: [248, 250, 252] } }
|
|
251
253
|
: {}),
|
|
252
254
|
...(foot
|
|
253
|
-
? { footStyles: { fillColor: [241, 245, 249], fontStyle: 'bold' } }
|
|
255
|
+
? { footStyles: { fillColor: [241, 245, 249], fontStyle: 'bold', textColor: [0, 0, 0] } }
|
|
254
256
|
: {}),
|
|
255
257
|
columnStyles,
|
|
256
258
|
showFoot: foot ? 'lastPage' : 'never',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
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",
|