@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.
@@ -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={value}
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
- if (!searchQuery) return columns;
58
+ const base = showNonHideableColumns ? columns : hideableColumns;
59
+ if (!searchQuery) return base;
57
60
  const lowerQuery = searchQuery.toLowerCase();
58
- return columns.filter(col =>
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,
@@ -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][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][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][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][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 mod = await import('jspdf');
117
- JsPDF = mod.default as unknown as new (opts: Record<string, unknown>) => JsPDFDoc;
118
- await import('jspdf-autotable');
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
- doc.autoTable({
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.1",
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",