@opencorestack/opengridx 1.0.7 → 1.1.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 +18 -0
- package/dist/index.d.ts +40 -12
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +2953 -2885
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +11 -11
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +35 -0
- package/docs/components/cell.md +14 -0
- package/docs/components/filter-panel.md +13 -0
- package/docs/components/pagination.md +26 -0
- package/docs/components/row.md +26 -0
- package/docs/roadmap.md +8 -0
- package/lib/components/Cell/Cell.tsx +44 -44
- package/lib/components/Cell/CellErrorBoundary.tsx +53 -0
- package/lib/components/DataGrid/DataGrid.css +11 -0
- package/lib/components/DataGrid/DataGrid.tsx +30 -6
- package/lib/components/DataGrid/GridListView.tsx +4 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
- package/lib/components/Pagination/Pagination.test.tsx +64 -0
- package/lib/components/Pagination/Pagination.tsx +15 -4
- package/lib/components/Row/Row.tsx +6 -3
- package/lib/hooks/core/useGridColumns.tsx +11 -11
- package/lib/hooks/core/useGridControlledState.test.ts +86 -0
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
- package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
- package/lib/hooks/useRowGrouping.test.ts +84 -0
- package/lib/hooks/useRowGrouping.ts +38 -13
- package/lib/hooks/useTreeData.ts +22 -2
- package/lib/index.ts +2 -0
- package/lib/types/index.ts +38 -12
- package/package.json +1 -1
package/docs/API_REFERENCE.md
CHANGED
|
@@ -143,6 +143,12 @@ The main component for displaying and interacting with data.
|
|
|
143
143
|
| `className` | `string` | — | Additional CSS class applied to the outermost grid wrapper element. |
|
|
144
144
|
| `style` | `React.CSSProperties` | — | Inline styles applied to the grid wrapper element. |
|
|
145
145
|
|
|
146
|
+
#### Localization
|
|
147
|
+
|
|
148
|
+
| Prop | Type | Default | Description |
|
|
149
|
+
| :--- | :--- | :--- | :--- |
|
|
150
|
+
| `localeText` | `GridLocaleText` | — | Override user-visible strings in the Pagination component. See `GridLocaleText` for the full interface. |
|
|
151
|
+
|
|
146
152
|
#### Imperative Ref
|
|
147
153
|
|
|
148
154
|
| Prop | Type | Default | Description |
|
|
@@ -564,6 +570,35 @@ interface GridPivotModel {
|
|
|
564
570
|
}
|
|
565
571
|
```
|
|
566
572
|
|
|
573
|
+
### `GridLocaleText`
|
|
574
|
+
|
|
575
|
+
Override user-visible strings in the Pagination component. All fields are optional.
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
interface GridLocaleText {
|
|
579
|
+
paginationRowsPerPage?: string;
|
|
580
|
+
paginationOf?: (from: number, to: number, count: number) => string;
|
|
581
|
+
paginationPage?: (page: number, pageCount: number) => string;
|
|
582
|
+
noRowsLabel?: string;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### `GridRowMeta`
|
|
587
|
+
|
|
588
|
+
Available via `params.rowMeta` in `renderCell`. Undefined for non-hierarchy rows.
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
interface GridRowMeta {
|
|
592
|
+
hasChildren?: boolean;
|
|
593
|
+
treeDepth?: number;
|
|
594
|
+
groupingField?: string;
|
|
595
|
+
groupingValue?: unknown;
|
|
596
|
+
descendantCount?: number;
|
|
597
|
+
isExpanded?: boolean;
|
|
598
|
+
isGroupRow?: boolean;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
567
602
|
---
|
|
568
603
|
|
|
569
604
|
## 📡 Event Callback Types
|
package/docs/components/cell.md
CHANGED
|
@@ -42,3 +42,17 @@ const columns: GridColDef[] = [
|
|
|
42
42
|
}
|
|
43
43
|
];
|
|
44
44
|
```
|
|
45
|
+
|
|
46
|
+
## 🛡️ Error containment
|
|
47
|
+
|
|
48
|
+
Custom `renderCell` callbacks are wrapped in a `CellErrorBoundary`. If a renderCell throws during render, that single cell shows a `⚠` indicator (CSS class `ogx__cell-error`) with the error message as a tooltip. The rest of the grid continues rendering normally.
|
|
49
|
+
|
|
50
|
+
Default cell display (no `renderCell`) does not go through the boundary — it cannot throw.
|
|
51
|
+
|
|
52
|
+
To style the error indicator:
|
|
53
|
+
|
|
54
|
+
```css
|
|
55
|
+
.ogx__cell-error {
|
|
56
|
+
color: red; /* override the default */
|
|
57
|
+
}
|
|
58
|
+
```
|
|
@@ -9,6 +9,19 @@ The Filter Panel allows users to define a list of `GridFilterItem` objects. Each
|
|
|
9
9
|
- **Operator**: How to compare (e.g., `contains`, `equals`, `>`, `is empty`).
|
|
10
10
|
- **Value**: The target value for comparison.
|
|
11
11
|
|
|
12
|
+
### Hierarchy-aware custom cells
|
|
13
|
+
|
|
14
|
+
When a column has a custom `renderCell` and the grid is in tree-data or row-grouping mode, use `params.rowMeta` to access hierarchy context:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
renderCell: (params) => {
|
|
18
|
+
if (params.rowMeta?.hasChildren) {
|
|
19
|
+
return <strong>{params.value}</strong>;
|
|
20
|
+
}
|
|
21
|
+
return params.value;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
12
25
|
## 🔌 Integration
|
|
13
26
|
|
|
14
27
|
The `<DataGrid />` manages the visibility of the Filter Panel via its internal state. It is usually triggered from the Column Menu or the Toolbar.
|
|
@@ -32,3 +32,29 @@ You can replace the internal pagination UI using the `pagination` slot:
|
|
|
32
32
|
}}
|
|
33
33
|
/>
|
|
34
34
|
```
|
|
35
|
+
|
|
36
|
+
## 🌍 Internationalisation
|
|
37
|
+
|
|
38
|
+
Pass `localeText` on `<DataGrid />` to override the text labels in the Pagination component (rows-per-page, range, and page-counter labels). Note: ARIA labels (`aria-label="Pagination"`, `aria-label="Rows per page"`) and button titles (`title="Previous page"`, `title="Next page"`) are currently hardcoded and not overrideable via `localeText`.
|
|
39
|
+
|
|
40
|
+
| Key | Type | Default |
|
|
41
|
+
| :--- | :--- | :--- |
|
|
42
|
+
| `paginationRowsPerPage` | `string` | `"Rows per page:"` |
|
|
43
|
+
| `paginationOf` | `(from, to, count) => string` | `` `${from}–${to} of ${count}` `` |
|
|
44
|
+
| `paginationPage` | `(page, pageCount) => string` | `` `Page ${page} of ${pageCount}` `` |
|
|
45
|
+
| `noRowsLabel` | `string` | `"No Data"` |
|
|
46
|
+
|
|
47
|
+
**Example — French locale:**
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<DataGrid
|
|
51
|
+
localeText={{
|
|
52
|
+
paginationRowsPerPage: 'Lignes par page :',
|
|
53
|
+
paginationOf: (from, to, count) => `${from}–${to} sur ${count}`,
|
|
54
|
+
paginationPage: (page, pageCount) => `Page ${page} sur ${pageCount}`,
|
|
55
|
+
noRowsLabel: 'Aucune donnée',
|
|
56
|
+
}}
|
|
57
|
+
rows={rows}
|
|
58
|
+
columns={columns}
|
|
59
|
+
/>
|
|
60
|
+
```
|
package/docs/components/row.md
CHANGED
|
@@ -29,3 +29,29 @@ The grid supports "Zebra" striping via CSS:
|
|
|
29
29
|
## 📋 Detail Panel
|
|
30
30
|
|
|
31
31
|
When `getDetailPanelContent` is provided, the `<Row />` renders an expandable container below itself to show supplemental data.
|
|
32
|
+
|
|
33
|
+
## ⚠️ v1.1 — Hierarchy Field Deprecation
|
|
34
|
+
|
|
35
|
+
The internal hierarchy fields (`_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`) are no longer typed on `GridRowModel`. They will be removed from the runtime row object in v2.0.
|
|
36
|
+
|
|
37
|
+
**Before (still works at runtime in v1.1 — `params.row._hasChildren` is `unknown`, not `boolean`; assignment to typed variables and arithmetic will error, truthiness checks will not):**
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
renderCell: (params) => {
|
|
41
|
+
// v1.1 accepts this at runtime; _hasChildren resolves to unknown via [key: string]: unknown
|
|
42
|
+
const hasChildren = (params.row as Record<string, unknown>)._hasChildren;
|
|
43
|
+
return hasChildren ? <GroupIcon /> : params.value;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**After (v1.1+):**
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
renderCell: (params) => {
|
|
51
|
+
const hasChildren = params.rowMeta?.hasChildren;
|
|
52
|
+
return hasChildren ? <GroupIcon /> : params.value;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`params.rowMeta` is `undefined` for flat rows with no active tree-data or row-grouping.
|
|
57
|
+
See [`GridRowMeta` architecture doc](../architecture/grid-row-meta.md) for the full data flow.
|
package/docs/roadmap.md
CHANGED
|
@@ -86,5 +86,13 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
86
86
|
* **GitHub Pages deployment**: CI/CD via GitHub Actions (`base: '/OpenGridX/'` already configured in `vite.config.js`).
|
|
87
87
|
|
|
88
88
|
## ✅ Implemented Features (Recent)
|
|
89
|
+
|
|
90
|
+
### Library Hardening *(completed 2026-09-02)*
|
|
91
|
+
- **`GridRowMeta`**: Hierarchy metadata (`hasChildren`, `treeDepth`, `groupingField`, `groupingValue`, `descendantCount`, `isExpanded`, `isGroupRow`) moved from `GridRowModel` into a separate `Map<GridRowId, GridRowMeta>`. Exposed as `params.rowMeta` in `renderCell`. Runtime shim maintains backward compatibility; removed in v2.0.
|
|
92
|
+
- **`CellErrorBoundary`**: Custom `renderCell` errors are now caught per-cell. Failing cells show a `⚠` indicator with the error as a tooltip; the rest of the grid renders normally.
|
|
93
|
+
- **`GridLocaleText` / `localeText` prop**: All user-visible pagination strings (`paginationRowsPerPage`, `paginationOf`, `paginationPage`, `noRowsLabel`) are now overrideable for internationalisation.
|
|
94
|
+
- **Core hook tests**: `useGridRowPipeline`, `useGridControlledState`, `useGridKeyboardNavigation` now have full test coverage (25 new tests).
|
|
95
|
+
- **`CLAUDE.md`**: Root-level AI context file auto-loaded by Claude Code and other AI coding assistants.
|
|
96
|
+
|
|
89
97
|
* **Demo app (partial)**: 32 interactive examples across 6 categories (Main features, Advanced features, Components, Customization, Tutorials, Resources) with categorized sidebar navigation and per-page Table of Contents. Source code viewer present on 6 of 32 examples. Syntax highlighting and public deployment are in the upcoming Demo Site section above.
|
|
90
98
|
* **Quickstart & Installation Guides**: Comprehensive onboarding documentation for developers.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type { GridColDef, GridRowModel, GridPinnedPosition, GridCellParams } from '../../types';
|
|
3
|
+
import type { GridColDef, GridRowModel, GridPinnedPosition, GridCellParams, GridRowMeta } from '../../types';
|
|
4
4
|
import type { CellColSpanInfo } from '../../hooks/features/useGridSpanning';
|
|
5
5
|
|
|
6
6
|
import { GridEditInputCell } from './GridEditInputCell';
|
|
7
|
+
import { CellErrorBoundary } from './CellErrorBoundary';
|
|
7
8
|
|
|
8
9
|
export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
9
10
|
onCellClick?: (params: GridCellParams<R>) => void;
|
|
@@ -32,6 +33,7 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
|
32
33
|
colSpanInfo?: CellColSpanInfo;
|
|
33
34
|
rowSpan?: number;
|
|
34
35
|
isHiddenByRowSpan?: boolean;
|
|
36
|
+
rowMeta?: GridRowMeta;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
@@ -59,7 +61,8 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
59
61
|
|
|
60
62
|
colSpanInfo,
|
|
61
63
|
rowSpan: rowSpanProp,
|
|
62
|
-
isHiddenByRowSpan
|
|
64
|
+
isHiddenByRowSpan,
|
|
65
|
+
rowMeta
|
|
63
66
|
} = props;
|
|
64
67
|
|
|
65
68
|
// All hooks must come before any early returns (Rules of Hooks)
|
|
@@ -87,47 +90,6 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
87
90
|
return String(value);
|
|
88
91
|
}, [value, row, colDef]);
|
|
89
92
|
|
|
90
|
-
const cellContent = React.useMemo(() => {
|
|
91
|
-
if (isEditing && (onCellValueChange || onValueChange) && onEditStop) {
|
|
92
|
-
if (colDef.renderEditCell) {
|
|
93
|
-
return colDef.renderEditCell({
|
|
94
|
-
value,
|
|
95
|
-
row,
|
|
96
|
-
field: colDef.field,
|
|
97
|
-
colDef,
|
|
98
|
-
rowIndex,
|
|
99
|
-
colIndex
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return (
|
|
103
|
-
<GridEditInputCell
|
|
104
|
-
value={value}
|
|
105
|
-
row={row}
|
|
106
|
-
field={colDef.field}
|
|
107
|
-
colDef={colDef as unknown as GridColDef}
|
|
108
|
-
rowIndex={rowIndex}
|
|
109
|
-
colIndex={colIndex}
|
|
110
|
-
onValueChange={handleValueChange}
|
|
111
|
-
onCommit={() => onEditStop()}
|
|
112
|
-
onCancel={() => onEditStop(true)}
|
|
113
|
-
/>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (colDef.renderCell) {
|
|
118
|
-
return colDef.renderCell({
|
|
119
|
-
value,
|
|
120
|
-
row,
|
|
121
|
-
field: colDef.field,
|
|
122
|
-
colDef,
|
|
123
|
-
rowIndex,
|
|
124
|
-
colIndex
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return formattedValue;
|
|
129
|
-
}, [value, row, colDef, rowIndex, colIndex, formattedValue, isEditing, handleValueChange, onEditStop, onCellValueChange, onValueChange]);
|
|
130
|
-
|
|
131
93
|
const resolvedCellClassName = React.useMemo(() => {
|
|
132
94
|
if (!colDef.cellClassName) return '';
|
|
133
95
|
if (typeof colDef.cellClassName === 'function') {
|
|
@@ -252,7 +214,45 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
252
214
|
{...(rowSpan > 1 ? { 'aria-rowspan': rowSpan } : {})}
|
|
253
215
|
>
|
|
254
216
|
<div className="ogx__cell-content">
|
|
255
|
-
{
|
|
217
|
+
{isEditing && (onCellValueChange || onValueChange) && onEditStop ? (
|
|
218
|
+
colDef.renderEditCell ? (
|
|
219
|
+
colDef.renderEditCell({
|
|
220
|
+
value,
|
|
221
|
+
row,
|
|
222
|
+
field: colDef.field,
|
|
223
|
+
colDef,
|
|
224
|
+
rowIndex,
|
|
225
|
+
colIndex,
|
|
226
|
+
rowMeta,
|
|
227
|
+
})
|
|
228
|
+
) : (
|
|
229
|
+
<GridEditInputCell
|
|
230
|
+
value={value}
|
|
231
|
+
row={row}
|
|
232
|
+
field={colDef.field}
|
|
233
|
+
colDef={colDef as unknown as GridColDef}
|
|
234
|
+
rowIndex={rowIndex}
|
|
235
|
+
colIndex={colIndex}
|
|
236
|
+
onValueChange={handleValueChange}
|
|
237
|
+
onCommit={() => onEditStop()}
|
|
238
|
+
onCancel={() => onEditStop(true)}
|
|
239
|
+
/>
|
|
240
|
+
)
|
|
241
|
+
) : colDef.renderCell ? (
|
|
242
|
+
<CellErrorBoundary field={colDef.field} resetKey={value}>
|
|
243
|
+
{colDef.renderCell({
|
|
244
|
+
value,
|
|
245
|
+
row,
|
|
246
|
+
field: colDef.field,
|
|
247
|
+
colDef,
|
|
248
|
+
rowIndex,
|
|
249
|
+
colIndex,
|
|
250
|
+
rowMeta,
|
|
251
|
+
})}
|
|
252
|
+
</CellErrorBoundary>
|
|
253
|
+
) : (
|
|
254
|
+
formattedValue
|
|
255
|
+
)}
|
|
256
256
|
</div>
|
|
257
257
|
</div>
|
|
258
258
|
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface CellErrorBoundaryProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
field: string;
|
|
6
|
+
resetKey?: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CellErrorBoundaryState {
|
|
10
|
+
hasError: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
resetKey?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, CellErrorBoundaryState> {
|
|
16
|
+
constructor(props: CellErrorBoundaryProps) {
|
|
17
|
+
super(props);
|
|
18
|
+
this.state = { hasError: false, error: null, resetKey: props.resetKey };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromProps(
|
|
22
|
+
props: CellErrorBoundaryProps,
|
|
23
|
+
state: CellErrorBoundaryState
|
|
24
|
+
): Partial<CellErrorBoundaryState> | null {
|
|
25
|
+
if (state.hasError && props.resetKey !== state.resetKey) {
|
|
26
|
+
return { hasError: false, error: null, resetKey: props.resetKey };
|
|
27
|
+
}
|
|
28
|
+
if (props.resetKey !== state.resetKey) {
|
|
29
|
+
return { resetKey: props.resetKey };
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static getDerivedStateFromError(error: Error): Partial<CellErrorBoundaryState> {
|
|
35
|
+
return { hasError: true, error };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override render() {
|
|
39
|
+
if (this.state.hasError) {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className="ogx__cell-error"
|
|
43
|
+
role="status"
|
|
44
|
+
aria-label={`Error in cell: ${this.props.field}`}
|
|
45
|
+
title={this.state.error?.message ?? 'Render error'}
|
|
46
|
+
>
|
|
47
|
+
⚠
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return this.props.children;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -258,3 +258,14 @@
|
|
|
258
258
|
white-space: nowrap;
|
|
259
259
|
border: 0;
|
|
260
260
|
}
|
|
261
|
+
|
|
262
|
+
.ogx__cell-error {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
width: 100%;
|
|
267
|
+
height: 100%;
|
|
268
|
+
color: var(--ogx-color-error, #d32f2f);
|
|
269
|
+
font-size: 1rem;
|
|
270
|
+
cursor: help;
|
|
271
|
+
}
|
|
@@ -27,7 +27,9 @@ import { GridListView } from './GridListView';
|
|
|
27
27
|
import { GridPinnedRows } from './GridPinnedRows';
|
|
28
28
|
import { GridVirtualRows } from './GridVirtualRows';
|
|
29
29
|
import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
|
|
30
|
-
import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem } from '../../types';
|
|
30
|
+
import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem, GridRowMeta } from '../../types';
|
|
31
|
+
|
|
32
|
+
const EMPTY_ROW_META_MAP: Map<GridRowId, GridRowMeta> = new Map();
|
|
31
33
|
|
|
32
34
|
export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>) {
|
|
33
35
|
const {
|
|
@@ -109,9 +111,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
109
111
|
listViewColumn,
|
|
110
112
|
columnGroupingModel,
|
|
111
113
|
noRowsLabel = 'No Data',
|
|
114
|
+
localeText,
|
|
112
115
|
apiRef: propApiRef,
|
|
113
116
|
} = props;
|
|
114
117
|
|
|
118
|
+
const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
|
|
119
|
+
|
|
115
120
|
// Stable defaults
|
|
116
121
|
const defaultFilterModel: GridFilterModel = useMemo(() => ({ items: [] }), []);
|
|
117
122
|
const filterModel = (propFilterModel || defaultFilterModel) as GridFilterModel;
|
|
@@ -333,18 +338,24 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
333
338
|
const isHierarchyEnabled = isTreeData || isRowGrouping;
|
|
334
339
|
const activeHierarchyHandlers = isTreeData ? treeDataHandlers : (isRowGrouping ? rowGroupingHandlers : null);
|
|
335
340
|
|
|
341
|
+
const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
|
|
342
|
+
if (isTreeData) return treeDataHandlers.rowMetaMap;
|
|
343
|
+
if (isRowGrouping) return rowGroupingHandlers.rowMetaMap;
|
|
344
|
+
return EMPTY_ROW_META_MAP;
|
|
345
|
+
}, [isTreeData, treeDataHandlers.rowMetaMap, isRowGrouping, rowGroupingHandlers.rowMetaMap]);
|
|
346
|
+
|
|
336
347
|
const pagination = propPagination && !isRowGrouping;
|
|
337
348
|
|
|
338
349
|
const handleRowClick = useCallback((params: GridRowParams<R>) => {
|
|
339
350
|
const { row, id } = params;
|
|
340
351
|
|
|
341
|
-
if (isHierarchyEnabled && row.
|
|
352
|
+
if (isHierarchyEnabled && rowMetaMap.get(row.id)?.hasChildren) {
|
|
342
353
|
activeHierarchyHandlers?.toggleExpansion(id);
|
|
343
354
|
return;
|
|
344
355
|
}
|
|
345
356
|
|
|
346
357
|
onRowClick?.(params);
|
|
347
|
-
}, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick]);
|
|
358
|
+
}, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap]);
|
|
348
359
|
|
|
349
360
|
const dataSourceHandlers = useGridDataSource({
|
|
350
361
|
dataSource,
|
|
@@ -785,7 +796,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
785
796
|
pageSizeOptions={pageSizeOptions}
|
|
786
797
|
selectedRowIds={selectedRowIds}
|
|
787
798
|
listViewColumn={listViewColumn}
|
|
788
|
-
noRowsLabel={
|
|
799
|
+
noRowsLabel={effectiveNoRowsLabel}
|
|
789
800
|
rowHeight={rowHeight}
|
|
790
801
|
checkboxSelection={checkboxSelection}
|
|
791
802
|
paginationMode={paginationMode}
|
|
@@ -793,6 +804,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
793
804
|
serverRowCount={state.pagination.rowCount || 0}
|
|
794
805
|
paginationSlot={slots?.pagination}
|
|
795
806
|
paginationSlotProps={slotProps?.pagination}
|
|
807
|
+
localeText={localeText ? {
|
|
808
|
+
paginationRowsPerPage: localeText.paginationRowsPerPage,
|
|
809
|
+
paginationOf: localeText.paginationOf,
|
|
810
|
+
paginationPage: localeText.paginationPage,
|
|
811
|
+
} : undefined}
|
|
796
812
|
onRowClick={handleRowClick}
|
|
797
813
|
onSelectionChange={handleSelectionChange}
|
|
798
814
|
onPaginationModelChange={handlePaginationModelChange}
|
|
@@ -893,7 +909,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
893
909
|
|
|
894
910
|
{/* Empty State Overlay (Standard View) — showing after header */}
|
|
895
911
|
{!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
|
|
896
|
-
<GridEmptyState noRowsLabel={
|
|
912
|
+
<GridEmptyState noRowsLabel={effectiveNoRowsLabel} width={virtualization.totalWidth} />
|
|
897
913
|
)}
|
|
898
914
|
|
|
899
915
|
<GridPinnedRows<R>
|
|
@@ -919,6 +935,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
919
935
|
colspanMap={spanning.colspanMap}
|
|
920
936
|
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
921
937
|
rowHeight={rowHeight}
|
|
938
|
+
rowMetaMap={rowMetaMap}
|
|
922
939
|
/>
|
|
923
940
|
|
|
924
941
|
<GridVirtualRows<R>
|
|
@@ -956,6 +973,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
956
973
|
sortedUnpinnedRowCount={sortedUnpinnedRows.length}
|
|
957
974
|
infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
|
|
958
975
|
unpinnedRowsLength={layout.unpinnedRowsLength}
|
|
976
|
+
rowMetaMap={rowMetaMap}
|
|
959
977
|
/>
|
|
960
978
|
|
|
961
979
|
<GridPinnedRows<R>
|
|
@@ -981,6 +999,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
981
999
|
colspanMap={spanning.colspanMap}
|
|
982
1000
|
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
983
1001
|
rowHeight={rowHeight}
|
|
1002
|
+
rowMetaMap={rowMetaMap}
|
|
984
1003
|
/>
|
|
985
1004
|
|
|
986
1005
|
{/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
|
|
@@ -1014,6 +1033,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1014
1033
|
pageSizeOptions={pageSizeOptions}
|
|
1015
1034
|
onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
|
|
1016
1035
|
onPageSizeChange={(newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
|
|
1036
|
+
localeText={localeText ? {
|
|
1037
|
+
paginationRowsPerPage: localeText.paginationRowsPerPage,
|
|
1038
|
+
paginationOf: localeText.paginationOf,
|
|
1039
|
+
paginationPage: localeText.paginationPage,
|
|
1040
|
+
} : undefined}
|
|
1017
1041
|
{...slotProps?.pagination}
|
|
1018
1042
|
/>
|
|
1019
1043
|
);
|
|
@@ -1025,7 +1049,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1025
1049
|
{state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
|
|
1026
1050
|
{!loading && !state.dataSource.error && (
|
|
1027
1051
|
filteredRows.length === 0
|
|
1028
|
-
?
|
|
1052
|
+
? effectiveNoRowsLabel
|
|
1029
1053
|
: (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
|
|
1030
1054
|
? `${filteredRows.length} ${filteredRows.length === 1 ? 'row' : 'rows'} found`
|
|
1031
1055
|
: ''
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
GridListViewColDef,
|
|
9
9
|
GridPaginationModel,
|
|
10
10
|
GridDataSource,
|
|
11
|
+
GridLocaleText,
|
|
11
12
|
} from '../../types';
|
|
12
13
|
|
|
13
14
|
export interface GridListViewProps<R extends GridRowModel> {
|
|
@@ -27,6 +28,7 @@ export interface GridListViewProps<R extends GridRowModel> {
|
|
|
27
28
|
serverRowCount: number;
|
|
28
29
|
paginationSlot?: React.ComponentType<Record<string, unknown>>;
|
|
29
30
|
paginationSlotProps?: Record<string, unknown>;
|
|
31
|
+
localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
|
|
30
32
|
onRowClick: (params: GridRowParams<R>) => void;
|
|
31
33
|
onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
|
|
32
34
|
onPaginationModelChange: (model: GridPaginationModel) => void;
|
|
@@ -49,6 +51,7 @@ export function GridListView<R extends GridRowModel>({
|
|
|
49
51
|
serverRowCount,
|
|
50
52
|
paginationSlot,
|
|
51
53
|
paginationSlotProps,
|
|
54
|
+
localeText,
|
|
52
55
|
onRowClick,
|
|
53
56
|
onSelectionChange,
|
|
54
57
|
onPaginationModelChange,
|
|
@@ -108,6 +111,7 @@ export function GridListView<R extends GridRowModel>({
|
|
|
108
111
|
pageSizeOptions={pageSizeOptions}
|
|
109
112
|
onPageChange={(newPage: number) => onPaginationModelChange({ ...effectivePaginationModel, page: newPage })}
|
|
110
113
|
onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
|
|
114
|
+
localeText={localeText}
|
|
111
115
|
{...paginationSlotProps}
|
|
112
116
|
/>
|
|
113
117
|
)}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
GridRowPinning,
|
|
11
11
|
GridDetailPanelParams,
|
|
12
12
|
GridDetailPanelHeight,
|
|
13
|
+
GridRowMeta,
|
|
13
14
|
} from '../../types';
|
|
14
15
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
15
16
|
|
|
@@ -36,6 +37,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
|
|
|
36
37
|
colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
|
|
37
38
|
rowSpanningCaches?: RowSpanningCaches;
|
|
38
39
|
rowHeight: number;
|
|
40
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export function GridPinnedRows<R extends GridRowModel>({
|
|
@@ -61,6 +63,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
61
63
|
colspanMap,
|
|
62
64
|
rowSpanningCaches,
|
|
63
65
|
rowHeight,
|
|
66
|
+
rowMetaMap,
|
|
64
67
|
}: GridPinnedRowsProps<R>) {
|
|
65
68
|
if (rows.length === 0) return null;
|
|
66
69
|
|
|
@@ -91,6 +94,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
91
94
|
colspanMap={colspanMap}
|
|
92
95
|
rowSpanningCaches={rowSpanningCaches}
|
|
93
96
|
rowHeight={rowHeight}
|
|
97
|
+
rowMeta={rowMetaMap.get(row.id)}
|
|
94
98
|
/>
|
|
95
99
|
))}
|
|
96
100
|
</div>
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
GridRowPinning,
|
|
13
13
|
GridDetailPanelParams,
|
|
14
14
|
GridDetailPanelHeight,
|
|
15
|
+
GridRowMeta,
|
|
15
16
|
} from '../../types';
|
|
16
17
|
import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
|
|
17
18
|
import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
|
|
@@ -60,6 +61,7 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
|
|
|
60
61
|
sortedUnpinnedRowCount: number;
|
|
61
62
|
infiniteScrollSkeletonCount: number;
|
|
62
63
|
unpinnedRowsLength: number;
|
|
64
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export function GridVirtualRows<R extends GridRowModel>({
|
|
@@ -97,6 +99,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
97
99
|
sortedUnpinnedRowCount,
|
|
98
100
|
infiniteScrollSkeletonCount,
|
|
99
101
|
unpinnedRowsLength,
|
|
102
|
+
rowMetaMap,
|
|
100
103
|
}: GridVirtualRowsProps<R>) {
|
|
101
104
|
const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
|
|
102
105
|
? baseColumns
|
|
@@ -179,6 +182,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
179
182
|
colspanMap={colspanMap}
|
|
180
183
|
rowSpanningCaches={rowSpanningCaches}
|
|
181
184
|
rowHeight={rowHeight}
|
|
185
|
+
rowMeta={rowMetaMap.get(row.id)}
|
|
182
186
|
/>
|
|
183
187
|
))
|
|
184
188
|
)}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Pagination } from './Pagination';
|
|
4
|
+
|
|
5
|
+
const NOOP = () => {};
|
|
6
|
+
|
|
7
|
+
describe('Pagination', () => {
|
|
8
|
+
it('renders "0 of 0" when rowCount is 0', () => {
|
|
9
|
+
render(
|
|
10
|
+
<Pagination
|
|
11
|
+
page={0}
|
|
12
|
+
pageSize={10}
|
|
13
|
+
rowCount={0}
|
|
14
|
+
onPageChange={NOOP}
|
|
15
|
+
onPageSizeChange={NOOP}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
expect(screen.getByText('0 of 0')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('calls localeText.paginationOf with (0, 0, 0) when rowCount is 0', () => {
|
|
22
|
+
const paginationOf = vi.fn().mockReturnValue('custom empty');
|
|
23
|
+
render(
|
|
24
|
+
<Pagination
|
|
25
|
+
page={0}
|
|
26
|
+
pageSize={10}
|
|
27
|
+
rowCount={0}
|
|
28
|
+
onPageChange={NOOP}
|
|
29
|
+
onPageSizeChange={NOOP}
|
|
30
|
+
localeText={{ paginationOf }}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
expect(paginationOf).toHaveBeenCalledWith(0, 0, 0);
|
|
34
|
+
expect(screen.getByText('custom empty')).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders correct row range label for non-empty data', () => {
|
|
38
|
+
render(
|
|
39
|
+
<Pagination
|
|
40
|
+
page={0}
|
|
41
|
+
pageSize={10}
|
|
42
|
+
rowCount={25}
|
|
43
|
+
onPageChange={NOOP}
|
|
44
|
+
onPageSizeChange={NOOP}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByText('1–10 of 25')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('calls localeText.paginationOf with correct args for non-empty data', () => {
|
|
51
|
+
const paginationOf = vi.fn().mockReturnValue('custom range');
|
|
52
|
+
render(
|
|
53
|
+
<Pagination
|
|
54
|
+
page={1}
|
|
55
|
+
pageSize={10}
|
|
56
|
+
rowCount={25}
|
|
57
|
+
onPageChange={NOOP}
|
|
58
|
+
onPageSizeChange={NOOP}
|
|
59
|
+
localeText={{ paginationOf }}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
expect(paginationOf).toHaveBeenCalledWith(11, 20, 25);
|
|
63
|
+
});
|
|
64
|
+
});
|