@opencorestack/opengridx 1.0.6 → 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 +3212 -3130
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- 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/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +0 -2
- package/lib/components/DataGrid/DataGrid.css +47 -0
- package/lib/components/DataGrid/DataGrid.tsx +34 -8
- package/lib/components/DataGrid/GridAggregationFooter.tsx +47 -9
- package/lib/components/DataGrid/GridListView.tsx +4 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +5 -4
- package/lib/components/Header/Header.tsx +4 -3
- package/lib/components/Pagination/Pagination.test.tsx +64 -0
- package/lib/components/Pagination/Pagination.tsx +19 -7
- package/lib/components/Row/Row.tsx +9 -5
- package/lib/components/Toolbar/GlobalSearch.tsx +0 -2
- package/lib/components/Toolbar/GridToolbar.tsx +0 -4
- 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
|
+
}
|
|
@@ -109,8 +109,6 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
|
109
109
|
<div className="ogx-column-visibility-panel__search-container">
|
|
110
110
|
<SearchIcon />
|
|
111
111
|
<input
|
|
112
|
-
id="ogx-column-search"
|
|
113
|
-
name="ogx-column-search"
|
|
114
112
|
autoComplete="off"
|
|
115
113
|
className="ogx-column-visibility-panel__search-input"
|
|
116
114
|
placeholder="Search"
|
|
@@ -177,6 +177,42 @@
|
|
|
177
177
|
overflow: hidden;
|
|
178
178
|
box-sizing: border-box;
|
|
179
179
|
border-right: 1px solid var(--ogx-grid-border-color);
|
|
180
|
+
position: relative;
|
|
181
|
+
}
|
|
182
|
+
.ogx__aggregation-cell--pinned-left,
|
|
183
|
+
.ogx__aggregation-cell--pinned-right {
|
|
184
|
+
position: sticky;
|
|
185
|
+
z-index: 3;
|
|
186
|
+
background: var(--ogx-grid-header-background);
|
|
187
|
+
}
|
|
188
|
+
.ogx__aggregation-cell--pinned-right {
|
|
189
|
+
border-left: 1px solid var(--ogx-grid-border-color);
|
|
190
|
+
}
|
|
191
|
+
.ogx__aggregation-cell--pinned-left-last {
|
|
192
|
+
overflow: visible;
|
|
193
|
+
}
|
|
194
|
+
.ogx__aggregation-cell--pinned-left-last::after {
|
|
195
|
+
content: '';
|
|
196
|
+
position: absolute;
|
|
197
|
+
top: 0;
|
|
198
|
+
right: -10px;
|
|
199
|
+
bottom: 0;
|
|
200
|
+
width: 10px;
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
203
|
+
}
|
|
204
|
+
.ogx__aggregation-cell--pinned-right-first {
|
|
205
|
+
overflow: visible;
|
|
206
|
+
}
|
|
207
|
+
.ogx__aggregation-cell--pinned-right-first::before {
|
|
208
|
+
content: '';
|
|
209
|
+
position: absolute;
|
|
210
|
+
top: 0;
|
|
211
|
+
left: -10px;
|
|
212
|
+
bottom: 0;
|
|
213
|
+
width: 10px;
|
|
214
|
+
pointer-events: none;
|
|
215
|
+
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
180
216
|
}
|
|
181
217
|
.ogx__aggregation-label {
|
|
182
218
|
font-size: 0.7em;
|
|
@@ -222,3 +258,14 @@
|
|
|
222
258
|
white-space: nowrap;
|
|
223
259
|
border: 0;
|
|
224
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,
|
|
@@ -685,6 +696,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
685
696
|
const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
|
|
686
697
|
const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
|
|
687
698
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
699
|
+
const standalonePanelRef = React.useRef<HTMLDivElement>(null);
|
|
688
700
|
|
|
689
701
|
const toolbarProps = React.useMemo(() => {
|
|
690
702
|
if (!slots?.toolbar) return null;
|
|
@@ -735,8 +747,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
735
747
|
useEffect(() => {
|
|
736
748
|
if (!columnsPanelOpen || slots?.toolbar) return;
|
|
737
749
|
function handleClickOutside(e: MouseEvent) {
|
|
738
|
-
|
|
739
|
-
if (panel && !panel.contains(e.target as Node)) {
|
|
750
|
+
if (standalonePanelRef.current && !standalonePanelRef.current.contains(e.target as Node)) {
|
|
740
751
|
setColumnsPanelOpen(false);
|
|
741
752
|
}
|
|
742
753
|
}
|
|
@@ -762,6 +773,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
762
773
|
<GridStandaloneColumnPanel<R>
|
|
763
774
|
isOpen={columnsPanelOpen}
|
|
764
775
|
containerRef={containerRef}
|
|
776
|
+
panelRef={standalonePanelRef}
|
|
765
777
|
effectiveColumns={effectiveColumns}
|
|
766
778
|
columnVisibilityModel={columnVisibilityModel}
|
|
767
779
|
effectiveColumnOrder={effectiveColumnOrder}
|
|
@@ -784,7 +796,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
784
796
|
pageSizeOptions={pageSizeOptions}
|
|
785
797
|
selectedRowIds={selectedRowIds}
|
|
786
798
|
listViewColumn={listViewColumn}
|
|
787
|
-
noRowsLabel={
|
|
799
|
+
noRowsLabel={effectiveNoRowsLabel}
|
|
788
800
|
rowHeight={rowHeight}
|
|
789
801
|
checkboxSelection={checkboxSelection}
|
|
790
802
|
paginationMode={paginationMode}
|
|
@@ -792,6 +804,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
792
804
|
serverRowCount={state.pagination.rowCount || 0}
|
|
793
805
|
paginationSlot={slots?.pagination}
|
|
794
806
|
paginationSlotProps={slotProps?.pagination}
|
|
807
|
+
localeText={localeText ? {
|
|
808
|
+
paginationRowsPerPage: localeText.paginationRowsPerPage,
|
|
809
|
+
paginationOf: localeText.paginationOf,
|
|
810
|
+
paginationPage: localeText.paginationPage,
|
|
811
|
+
} : undefined}
|
|
795
812
|
onRowClick={handleRowClick}
|
|
796
813
|
onSelectionChange={handleSelectionChange}
|
|
797
814
|
onPaginationModelChange={handlePaginationModelChange}
|
|
@@ -892,7 +909,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
892
909
|
|
|
893
910
|
{/* Empty State Overlay (Standard View) — showing after header */}
|
|
894
911
|
{!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
|
|
895
|
-
<GridEmptyState noRowsLabel={
|
|
912
|
+
<GridEmptyState noRowsLabel={effectiveNoRowsLabel} width={virtualization.totalWidth} />
|
|
896
913
|
)}
|
|
897
914
|
|
|
898
915
|
<GridPinnedRows<R>
|
|
@@ -918,6 +935,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
918
935
|
colspanMap={spanning.colspanMap}
|
|
919
936
|
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
920
937
|
rowHeight={rowHeight}
|
|
938
|
+
rowMetaMap={rowMetaMap}
|
|
921
939
|
/>
|
|
922
940
|
|
|
923
941
|
<GridVirtualRows<R>
|
|
@@ -955,6 +973,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
955
973
|
sortedUnpinnedRowCount={sortedUnpinnedRows.length}
|
|
956
974
|
infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
|
|
957
975
|
unpinnedRowsLength={layout.unpinnedRowsLength}
|
|
976
|
+
rowMetaMap={rowMetaMap}
|
|
958
977
|
/>
|
|
959
978
|
|
|
960
979
|
<GridPinnedRows<R>
|
|
@@ -980,6 +999,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
980
999
|
colspanMap={spanning.colspanMap}
|
|
981
1000
|
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
982
1001
|
rowHeight={rowHeight}
|
|
1002
|
+
rowMetaMap={rowMetaMap}
|
|
983
1003
|
/>
|
|
984
1004
|
|
|
985
1005
|
{/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
|
|
@@ -995,6 +1015,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
995
1015
|
checkboxSelection={checkboxSelection}
|
|
996
1016
|
hasDetailPanel={hasDetailPanel}
|
|
997
1017
|
rowReordering={rowReordering}
|
|
1018
|
+
pinnedColumns={pinnedColumns}
|
|
998
1019
|
/>
|
|
999
1020
|
)}
|
|
1000
1021
|
|
|
@@ -1012,6 +1033,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1012
1033
|
pageSizeOptions={pageSizeOptions}
|
|
1013
1034
|
onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
|
|
1014
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}
|
|
1015
1041
|
{...slotProps?.pagination}
|
|
1016
1042
|
/>
|
|
1017
1043
|
);
|
|
@@ -1023,7 +1049,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1023
1049
|
{state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
|
|
1024
1050
|
{!loading && !state.dataSource.error && (
|
|
1025
1051
|
filteredRows.length === 0
|
|
1026
|
-
?
|
|
1052
|
+
? effectiveNoRowsLabel
|
|
1027
1053
|
: (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
|
|
1028
1054
|
? `${filteredRows.length} ${filteredRows.length === 1 ? 'row' : 'rows'} found`
|
|
1029
1055
|
: ''
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { formatAggregationValue } from '../../hooks/features/useAggregation';
|
|
3
|
-
import type { GridColDef, GridAggregationModel, GridAggregationResult } from '../../types';
|
|
3
|
+
import type { GridColDef, GridAggregationModel, GridAggregationResult, GridColumnPinning } from '../../types';
|
|
4
|
+
import { calculatePinnedPositions, isColumnPinned } from '../../utils/pinning';
|
|
4
5
|
|
|
5
6
|
interface GridAggregationFooterProps {
|
|
6
7
|
columns: GridColDef[];
|
|
@@ -11,6 +12,7 @@ interface GridAggregationFooterProps {
|
|
|
11
12
|
checkboxSelection: boolean;
|
|
12
13
|
hasDetailPanel: boolean;
|
|
13
14
|
rowReordering: boolean;
|
|
15
|
+
pinnedColumns?: GridColumnPinning;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function GridAggregationFooter({
|
|
@@ -22,7 +24,28 @@ export function GridAggregationFooter({
|
|
|
22
24
|
checkboxSelection,
|
|
23
25
|
hasDetailPanel,
|
|
24
26
|
rowReordering,
|
|
27
|
+
pinnedColumns,
|
|
25
28
|
}: GridAggregationFooterProps) {
|
|
29
|
+
// Compute sticky left/right pixel offsets for each pinned column.
|
|
30
|
+
// The footer always renders checkbox/detail/reorder spacers unconditionally,
|
|
31
|
+
// so treat pinCheckboxColumn and pinExpandColumn as always true.
|
|
32
|
+
const pinnedOffsets = useMemo(
|
|
33
|
+
() => calculatePinnedPositions(
|
|
34
|
+
columns,
|
|
35
|
+
columnWidths,
|
|
36
|
+
pinnedColumns,
|
|
37
|
+
checkboxSelection,
|
|
38
|
+
true,
|
|
39
|
+
hasDetailPanel,
|
|
40
|
+
true,
|
|
41
|
+
rowReordering,
|
|
42
|
+
),
|
|
43
|
+
[columns, columnWidths, pinnedColumns, checkboxSelection, hasDetailPanel, rowReordering]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const lastLeftField = pinnedColumns?.left?.[pinnedColumns.left.length - 1];
|
|
47
|
+
const firstRightField = pinnedColumns?.right?.[0];
|
|
48
|
+
|
|
26
49
|
return (
|
|
27
50
|
<div
|
|
28
51
|
className="ogx__aggregation-footer"
|
|
@@ -40,17 +63,32 @@ export function GridAggregationFooter({
|
|
|
40
63
|
const rawValue = aggregationResult[col.field];
|
|
41
64
|
const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
|
|
42
65
|
|
|
66
|
+
const pinnedPosition = isColumnPinned(col.field, pinnedColumns);
|
|
67
|
+
const pinnedOffset = pinnedPosition ? pinnedOffsets[col.field] : undefined;
|
|
68
|
+
|
|
69
|
+
const className = [
|
|
70
|
+
'ogx__aggregation-cell',
|
|
71
|
+
pinnedPosition === 'left' && 'ogx__aggregation-cell--pinned-left',
|
|
72
|
+
pinnedPosition === 'right' && 'ogx__aggregation-cell--pinned-right',
|
|
73
|
+
pinnedPosition === 'left' && col.field === lastLeftField && 'ogx__aggregation-cell--pinned-left-last',
|
|
74
|
+
pinnedPosition === 'right' && col.field === firstRightField && 'ogx__aggregation-cell--pinned-right-first',
|
|
75
|
+
].filter(Boolean).join(' ');
|
|
76
|
+
|
|
77
|
+
const style: React.CSSProperties = {
|
|
78
|
+
width: colWidth,
|
|
79
|
+
minWidth: colWidth,
|
|
80
|
+
maxWidth: colWidth,
|
|
81
|
+
textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
|
|
82
|
+
};
|
|
83
|
+
if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.left = pinnedOffset;
|
|
84
|
+
if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.right = pinnedOffset;
|
|
85
|
+
|
|
43
86
|
return (
|
|
44
87
|
<div
|
|
45
88
|
key={col.field}
|
|
46
|
-
className=
|
|
89
|
+
className={className}
|
|
47
90
|
role="gridcell"
|
|
48
|
-
style={
|
|
49
|
-
width: colWidth,
|
|
50
|
-
minWidth: colWidth,
|
|
51
|
-
maxWidth: colWidth,
|
|
52
|
-
textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
|
|
53
|
-
}}
|
|
91
|
+
style={style}
|
|
54
92
|
>
|
|
55
93
|
{fnName ? (
|
|
56
94
|
<>
|