@opencorestack/opengridx 3.4.0 → 3.5.1
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 +45 -0
- package/README.md +10 -3
- package/dist/ai.cjs +2 -1
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +203 -3
- package/dist/ai.es.js +2 -1
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +248 -4
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7334 -5906
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +88 -7
- package/docs/README.md +3 -1
- package/docs/architecture/datagrid-orchestration.md +11 -5
- package/docs/components/filter-panel.md +1 -0
- package/docs/components/header.md +4 -1
- package/docs/components/toolbar.md +7 -0
- package/docs/customization/theming.md +3 -1
- package/docs/features/ai-toolkit.md +99 -5
- package/docs/features/cell-selection.md +8 -2
- package/docs/features/fill-handle.md +125 -0
- package/docs/features/filtering.md +3 -0
- package/docs/features/header-filters.md +138 -0
- package/docs/features/keyboard-navigation.md +20 -2
- package/docs/features/virtualization.md +5 -5
- package/docs/performance.md +212 -1
- package/docs/roadmap.md +3 -4
- package/lib/ai/agentTools.ts +160 -0
- package/lib/ai/index.ts +19 -2
- package/lib/ai/promptHandler.ts +109 -0
- package/lib/ai/types.ts +89 -25
- package/lib/ai/validate.ts +10 -0
- package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
- package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
- package/lib/components/Cell/Cell.css +28 -0
- package/lib/components/Cell/Cell.tsx +6 -2
- package/lib/components/DataGrid/DataGrid.tsx +84 -4
- package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
- package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
- package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
- package/lib/components/Header/Header.tsx +44 -23
- package/lib/components/Header/headerColumnStyle.ts +28 -0
- package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
- package/lib/components/Toolbar/GridToolbar.tsx +51 -0
- package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
- package/lib/hooks/core/useGridApiMethods.ts +39 -0
- package/lib/hooks/core/useGridAriaRows.ts +5 -3
- package/lib/hooks/core/useGridCellSelection.ts +77 -11
- package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
- package/lib/hooks/core/useGridControlledState.ts +27 -0
- package/lib/hooks/core/useGridDevWarnings.ts +21 -6
- package/lib/hooks/core/useGridFillHandle.ts +323 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
- package/lib/hooks/core/useGridMaxContentHeight.ts +16 -0
- package/lib/hooks/core/useGridToolbarProps.ts +19 -0
- package/lib/hooks/core/useGridVirtualization.ts +19 -0
- package/lib/hooks/features/useGridAiAssistant.ts +284 -0
- package/lib/hooks/features/useGridBatchEdit.ts +2 -2
- package/lib/index.ts +3 -0
- package/lib/styles/opengridx.css +2 -0
- package/lib/theme/DataGridThemeProvider.tsx +2 -0
- package/lib/theme/types.ts +2 -0
- package/lib/types/index.ts +219 -1
- package/lib/utils/aiAssistant.ts +205 -0
- package/lib/utils/aiBrand.ts +17 -0
- package/lib/utils/cellSelection.ts +25 -5
- package/lib/utils/editing/fill.ts +238 -0
- package/lib/utils/filtering/index.ts +14 -0
- package/lib/utils/focus/index.ts +9 -3
- package/lib/utils/headerFilters.ts +189 -0
- package/lib/utils/maxElementHeight.ts +53 -0
- package/llms.txt +44 -4
- package/package.json +3 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import type { GridFilterOperator } from '../../types';
|
|
4
|
+
import { HEADER_FILTER_OPERATOR_LABELS } from '../../utils/headerFilters';
|
|
5
|
+
import { getViewportHeight, getViewportWidth } from '../../utils/viewport';
|
|
6
|
+
|
|
7
|
+
export interface HeaderFilterOperatorMenuProps {
|
|
8
|
+
anchorEl: HTMLElement;
|
|
9
|
+
/** Column header name, for the menu's label. */
|
|
10
|
+
columnLabel: string;
|
|
11
|
+
operators: GridFilterOperator[];
|
|
12
|
+
current: GridFilterOperator;
|
|
13
|
+
onSelect: (operator: GridFilterOperator) => void;
|
|
14
|
+
/** `restoreFocus`: the menu was left from the keyboard or by choosing an item. */
|
|
15
|
+
onClose: (restoreFocus: boolean) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const HIDDEN_STYLE: React.CSSProperties = { position: 'fixed', zIndex: 1300, top: 0, left: 0, visibility: 'hidden' };
|
|
19
|
+
|
|
20
|
+
const itemsOf = (menu: HTMLElement | null): HTMLElement[] =>
|
|
21
|
+
menu ? Array.from(menu.querySelectorAll<HTMLElement>('[role="menuitemradio"]')) : [];
|
|
22
|
+
|
|
23
|
+
/** The operator menu of a header filter cell: a menu of radio items, portalled out of the header. */
|
|
24
|
+
export function HeaderFilterOperatorMenu({ anchorEl, columnLabel, operators, current, onSelect, onClose }: HeaderFilterOperatorMenuProps) {
|
|
25
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const [style, setStyle] = useState<React.CSSProperties>(HIDDEN_STYLE);
|
|
27
|
+
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
const menu = menuRef.current;
|
|
30
|
+
if (!menu) return;
|
|
31
|
+
const anchor = anchorEl.getBoundingClientRect();
|
|
32
|
+
const box = menu.getBoundingClientRect();
|
|
33
|
+
const margin = 8;
|
|
34
|
+
let left = Math.min(anchor.left, getViewportWidth() - box.width - margin);
|
|
35
|
+
if (left < margin) left = margin;
|
|
36
|
+
let top = anchor.bottom + 4;
|
|
37
|
+
if (top + box.height + margin > getViewportHeight()) top = anchor.top - box.height - 4;
|
|
38
|
+
setStyle({ position: 'fixed', zIndex: 1300, top, left, visibility: 'visible' });
|
|
39
|
+
}, [anchorEl]);
|
|
40
|
+
|
|
41
|
+
// Focus the checked item once the menu is positioned (a hidden element cannot take focus).
|
|
42
|
+
const isPositioned = style.visibility === 'visible';
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
if (!isPositioned) return;
|
|
45
|
+
const items = itemsOf(menuRef.current);
|
|
46
|
+
(items.find(item => item.getAttribute('aria-checked') === 'true') ?? items[0])?.focus({ preventScroll: true });
|
|
47
|
+
}, [isPositioned]);
|
|
48
|
+
|
|
49
|
+
const onCloseRef = useRef(onClose);
|
|
50
|
+
useLayoutEffect(() => { onCloseRef.current = onClose; });
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const doc = anchorEl.ownerDocument;
|
|
54
|
+
const view = doc.defaultView;
|
|
55
|
+
function handleMouseDown(event: MouseEvent) {
|
|
56
|
+
const target = event.target as Node;
|
|
57
|
+
if (menuRef.current?.contains(target) || anchorEl.contains(target)) return;
|
|
58
|
+
onCloseRef.current(false);
|
|
59
|
+
}
|
|
60
|
+
// The menu floats at a fixed position: it closes rather than drift away from its cell.
|
|
61
|
+
function handleScroll(event: Event) {
|
|
62
|
+
if (menuRef.current?.contains(event.target as Node)) return;
|
|
63
|
+
onCloseRef.current(false);
|
|
64
|
+
}
|
|
65
|
+
function handleResize() { onCloseRef.current(false); }
|
|
66
|
+
doc.addEventListener('mousedown', handleMouseDown);
|
|
67
|
+
view?.addEventListener('scroll', handleScroll, true);
|
|
68
|
+
view?.addEventListener('resize', handleResize);
|
|
69
|
+
return () => {
|
|
70
|
+
doc.removeEventListener('mousedown', handleMouseDown);
|
|
71
|
+
view?.removeEventListener('scroll', handleScroll, true);
|
|
72
|
+
view?.removeEventListener('resize', handleResize);
|
|
73
|
+
};
|
|
74
|
+
}, [anchorEl]);
|
|
75
|
+
|
|
76
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
77
|
+
// The menu is portalled: keys must not reach the cell or the grid through the React tree.
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
const items = itemsOf(menuRef.current);
|
|
80
|
+
const index = items.indexOf(event.target as HTMLElement);
|
|
81
|
+
let next = -1;
|
|
82
|
+
switch (event.key) {
|
|
83
|
+
case 'ArrowDown': next = index + 1 >= items.length ? 0 : index + 1; break;
|
|
84
|
+
case 'ArrowUp': next = index <= 0 ? items.length - 1 : index - 1; break;
|
|
85
|
+
case 'Home': next = 0; break;
|
|
86
|
+
case 'End': next = items.length - 1; break;
|
|
87
|
+
case 'Escape':
|
|
88
|
+
case 'Tab':
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
onClose(true);
|
|
91
|
+
return;
|
|
92
|
+
default:
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
items[next]?.focus();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return ReactDOM.createPortal(
|
|
100
|
+
<div
|
|
101
|
+
ref={menuRef}
|
|
102
|
+
className="ogx-column-menu ogx__header-filter-menu"
|
|
103
|
+
style={style}
|
|
104
|
+
role="menu"
|
|
105
|
+
aria-label={`Filter operator for ${columnLabel}`}
|
|
106
|
+
onKeyDown={handleKeyDown}
|
|
107
|
+
>
|
|
108
|
+
{operators.map(op => (
|
|
109
|
+
<button
|
|
110
|
+
key={op}
|
|
111
|
+
type="button"
|
|
112
|
+
role="menuitemradio"
|
|
113
|
+
aria-checked={op === current}
|
|
114
|
+
tabIndex={-1}
|
|
115
|
+
className={`ogx-menu-item${op === current ? ' ogx-menu-item--active' : ''}`}
|
|
116
|
+
onClick={() => onSelect(op)}
|
|
117
|
+
>
|
|
118
|
+
{HEADER_FILTER_OPERATOR_LABELS[op] ?? op}
|
|
119
|
+
</button>
|
|
120
|
+
))}
|
|
121
|
+
</div>,
|
|
122
|
+
anchorEl.closest('.ogx-theme-provider') ?? anchorEl.ownerDocument.body
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { GridColDef, GridColumnPinning, GridFilterItem, GridFilterModel } from '../../types';
|
|
3
|
+
import { isColumnPinned, SYSTEM_COLUMN_WIDTH } from '../../utils/pinning';
|
|
4
|
+
import { CHECKBOX_FIELD, EXPAND_FIELD, REORDER_FIELD } from '../../utils/focus';
|
|
5
|
+
import { getHeaderFilterState, hasHeaderFilter, setHeaderFilterItem } from '../../utils/headerFilters';
|
|
6
|
+
import { getHeaderColumnStyle } from '../Header/headerColumnStyle';
|
|
7
|
+
import { HeaderFilterCell } from './HeaderFilterCell';
|
|
8
|
+
|
|
9
|
+
export interface HeaderFilterRowProps {
|
|
10
|
+
/** The rendered columns of the column header row (render window with spacers), in render order. */
|
|
11
|
+
columns: GridColDef[];
|
|
12
|
+
columnWidths: Record<string, number>;
|
|
13
|
+
pinnedColumns?: GridColumnPinning;
|
|
14
|
+
/** Sticky offsets of the pinned columns (`calculatePinnedPositions`). */
|
|
15
|
+
pinnedPositions: Record<string, number>;
|
|
16
|
+
pinnedEdges: { lastLeft: string | null; firstRight: string | null };
|
|
17
|
+
rowReordering: boolean;
|
|
18
|
+
hasDetailPanel: boolean;
|
|
19
|
+
checkboxSelection: boolean;
|
|
20
|
+
pinExpandColumn: boolean;
|
|
21
|
+
pinCheckboxColumn: boolean;
|
|
22
|
+
columnIndexMap?: Map<string, number>;
|
|
23
|
+
/** `aria-rowindex` of the row: right after the column header row. */
|
|
24
|
+
ariaRowIndex: number;
|
|
25
|
+
filterModel?: GridFilterModel;
|
|
26
|
+
onFilterModelChange?: (model: GridFilterModel) => void;
|
|
27
|
+
onOpenFilterPanel?: () => void;
|
|
28
|
+
/** Field of the focused filter cell, or null. */
|
|
29
|
+
focusedField: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function SystemFilterCell({ field, ariaColIndex, sticky, left, focused }: {
|
|
33
|
+
field: string;
|
|
34
|
+
ariaColIndex: number;
|
|
35
|
+
sticky: boolean;
|
|
36
|
+
left: number;
|
|
37
|
+
focused: boolean;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={[
|
|
42
|
+
'ogx__header-filter-cell',
|
|
43
|
+
'ogx__header-filter-cell--system',
|
|
44
|
+
sticky && 'ogx__header-filter-cell--pinned ogx__header-filter-cell--pinned-left',
|
|
45
|
+
focused && 'ogx__header-filter-cell--focused',
|
|
46
|
+
].filter(Boolean).join(' ')}
|
|
47
|
+
style={{
|
|
48
|
+
width: SYSTEM_COLUMN_WIDTH,
|
|
49
|
+
minWidth: SYSTEM_COLUMN_WIDTH,
|
|
50
|
+
maxWidth: SYSTEM_COLUMN_WIDTH,
|
|
51
|
+
position: sticky ? 'sticky' : undefined,
|
|
52
|
+
left: sticky ? left : undefined,
|
|
53
|
+
}}
|
|
54
|
+
role="columnheader"
|
|
55
|
+
aria-colindex={ariaColIndex}
|
|
56
|
+
data-field={field}
|
|
57
|
+
tabIndex={-1}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The header filter row (`headerFilters`): one cell per rendered column, laid out like the column
|
|
64
|
+
* header row above it, so pinned and virtualized columns line up. It edits the shared filter model
|
|
65
|
+
* through the root-level items it owns (`header:<field>`).
|
|
66
|
+
*/
|
|
67
|
+
export function HeaderFilterRow(props: HeaderFilterRowProps) {
|
|
68
|
+
const {
|
|
69
|
+
columns, columnWidths, pinnedColumns, pinnedPositions, pinnedEdges, rowReordering, hasDetailPanel,
|
|
70
|
+
checkboxSelection, pinExpandColumn, pinCheckboxColumn, columnIndexMap, ariaRowIndex, filterModel,
|
|
71
|
+
onFilterModelChange, onOpenFilterPanel, focusedField,
|
|
72
|
+
} = props;
|
|
73
|
+
|
|
74
|
+
// Cells commit from timers and unmount cleanups: they read the newest model, and two commits
|
|
75
|
+
// before the next render build on each other.
|
|
76
|
+
const latestRef = useRef({ filterModel, onFilterModelChange });
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
latestRef.current = { filterModel, onFilterModelChange };
|
|
79
|
+
});
|
|
80
|
+
const handleItemChange = useCallback((field: string, item: GridFilterItem | null) => {
|
|
81
|
+
const { filterModel: current, onFilterModelChange: emit } = latestRef.current;
|
|
82
|
+
if (!emit) return;
|
|
83
|
+
const next = setHeaderFilterItem(current, field, item);
|
|
84
|
+
if (next === current) return;
|
|
85
|
+
latestRef.current = { filterModel: next, onFilterModelChange: emit };
|
|
86
|
+
emit(next);
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const reorderColIndex = rowReordering ? 1 : 0;
|
|
90
|
+
const expandColIndex = hasDetailPanel ? reorderColIndex + 1 : reorderColIndex;
|
|
91
|
+
const checkboxColIndex = checkboxSelection ? expandColIndex + 1 : expandColIndex;
|
|
92
|
+
const systemColumnCount = checkboxColIndex;
|
|
93
|
+
const expandLeft = rowReordering ? SYSTEM_COLUMN_WIDTH : 0;
|
|
94
|
+
const checkboxLeft = expandLeft + (hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="ogx__header-filter-row" role="row" aria-rowindex={ariaRowIndex}>
|
|
98
|
+
{rowReordering && (
|
|
99
|
+
<SystemFilterCell field={REORDER_FIELD} ariaColIndex={reorderColIndex} sticky left={0} focused={focusedField === REORDER_FIELD} />
|
|
100
|
+
)}
|
|
101
|
+
{hasDetailPanel && (
|
|
102
|
+
<SystemFilterCell field={EXPAND_FIELD} ariaColIndex={expandColIndex} sticky={pinExpandColumn} left={expandLeft} focused={focusedField === EXPAND_FIELD} />
|
|
103
|
+
)}
|
|
104
|
+
{checkboxSelection && (
|
|
105
|
+
<SystemFilterCell field={CHECKBOX_FIELD} ariaColIndex={checkboxColIndex} sticky={pinCheckboxColumn} left={checkboxLeft} focused={focusedField === CHECKBOX_FIELD} />
|
|
106
|
+
)}
|
|
107
|
+
{columns.map((colDef, index) => {
|
|
108
|
+
if (colDef.isSpacer) {
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
key={colDef.field}
|
|
112
|
+
style={{ width: colDef.width, minWidth: colDef.width, flexShrink: 0 }}
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const pinnedPosition = isColumnPinned(colDef.field, pinnedColumns);
|
|
118
|
+
const style = getHeaderColumnStyle(colDef, columnWidths, pinnedPosition, pinnedPosition ? pinnedPositions[colDef.field] : undefined);
|
|
119
|
+
const ariaColIndex = systemColumnCount + (columnIndexMap?.get(colDef.field) ?? index) + 1;
|
|
120
|
+
const className = [
|
|
121
|
+
pinnedPosition && 'ogx__header-filter-cell--pinned',
|
|
122
|
+
pinnedPosition && `ogx__header-filter-cell--pinned-${pinnedPosition}`,
|
|
123
|
+
pinnedPosition === 'left' && colDef.field === pinnedEdges.lastLeft && 'ogx__header-filter-cell--pinned-left-last',
|
|
124
|
+
pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight && 'ogx__header-filter-cell--pinned-right-first',
|
|
125
|
+
focusedField === colDef.field && 'ogx__header-filter-cell--focused',
|
|
126
|
+
].filter(Boolean).join(' ');
|
|
127
|
+
|
|
128
|
+
if (!hasHeaderFilter(colDef)) {
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
key={colDef.field}
|
|
132
|
+
className={`ogx__header-filter-cell ogx__header-filter-cell--empty ${className}`}
|
|
133
|
+
style={style}
|
|
134
|
+
role="columnheader"
|
|
135
|
+
aria-label={`No filter for ${colDef.headerName || colDef.field}`}
|
|
136
|
+
aria-colindex={ariaColIndex}
|
|
137
|
+
data-field={colDef.field}
|
|
138
|
+
tabIndex={-1}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return (
|
|
143
|
+
<HeaderFilterCell
|
|
144
|
+
key={colDef.field}
|
|
145
|
+
colDef={colDef}
|
|
146
|
+
state={getHeaderFilterState(filterModel, colDef.field)}
|
|
147
|
+
style={style}
|
|
148
|
+
className={className}
|
|
149
|
+
ariaColIndex={ariaColIndex}
|
|
150
|
+
onItemChange={handleItemChange}
|
|
151
|
+
onOpenFilterPanel={onOpenFilterPanel}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -10,6 +10,7 @@ import { GlobalSearch } from './GlobalSearch';
|
|
|
10
10
|
import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
|
|
11
11
|
import { FilterPanel } from '../FilterPanel/FilterPanel';
|
|
12
12
|
import { GridTooltip } from '../Tooltip/Tooltip';
|
|
13
|
+
import { AiSparkleIcon } from '../AiAssistant/GridAiAssistantPanel';
|
|
13
14
|
|
|
14
15
|
/** Props passed to a custom toolbar button renderer. */
|
|
15
16
|
export interface ToolbarButtonRenderProps {
|
|
@@ -48,6 +49,10 @@ export interface GridToolbarProps {
|
|
|
48
49
|
onColumnOrderReset?: () => void;
|
|
49
50
|
forceColumnsOpen?: boolean;
|
|
50
51
|
onColumnsPanelClose?: () => void;
|
|
52
|
+
/** Opens the filter panel when it turns true (the grid passes it for the header filter row). @since v3.5 */
|
|
53
|
+
forceFiltersOpen?: boolean;
|
|
54
|
+
/** Called whenever the filter panel closes. @since v3.5 */
|
|
55
|
+
onFiltersPanelClose?: () => void;
|
|
51
56
|
children?: React.ReactNode;
|
|
52
57
|
rightContent?: React.ReactNode;
|
|
53
58
|
style?: React.CSSProperties;
|
|
@@ -80,6 +85,16 @@ export interface GridToolbarProps {
|
|
|
80
85
|
className?: string;
|
|
81
86
|
/** Show columns with `hideable: false` in the Columns panel as disabled rows. Default: false. */
|
|
82
87
|
showNonHideableColumns?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Opens or closes the `aiAssistant` panel. The grid passes it when `aiAssistant` is set; the
|
|
90
|
+
* **Ask AI** button is shown only with it. Receives the button, which gets focus back on close.
|
|
91
|
+
* @since v3.5
|
|
92
|
+
*/
|
|
93
|
+
onAiAssistantToggle?: (trigger: HTMLElement | null) => void;
|
|
94
|
+
/** Whether the `aiAssistant` panel is open. @since v3.5 */
|
|
95
|
+
aiAssistantOpen?: boolean;
|
|
96
|
+
/** Label of the Ask AI button (`localeText.aiAssistantButton`). Default: `"Ask AI"`. @since v3.5 */
|
|
97
|
+
aiAssistantLabel?: string;
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
|
|
@@ -492,6 +507,8 @@ export function GridToolbar({
|
|
|
492
507
|
onColumnOrderReset,
|
|
493
508
|
forceColumnsOpen,
|
|
494
509
|
onColumnsPanelClose,
|
|
510
|
+
forceFiltersOpen,
|
|
511
|
+
onFiltersPanelClose,
|
|
495
512
|
children,
|
|
496
513
|
rightContent,
|
|
497
514
|
style,
|
|
@@ -502,6 +519,9 @@ export function GridToolbar({
|
|
|
502
519
|
renderQuickFilter,
|
|
503
520
|
className,
|
|
504
521
|
showNonHideableColumns,
|
|
522
|
+
onAiAssistantToggle,
|
|
523
|
+
aiAssistantOpen = false,
|
|
524
|
+
aiAssistantLabel = 'Ask AI',
|
|
505
525
|
}: GridToolbarProps) {
|
|
506
526
|
// At most one panel is open. Every way the Columns panel closes (its button, a custom
|
|
507
527
|
// button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
|
|
@@ -514,6 +534,7 @@ export function GridToolbar({
|
|
|
514
534
|
|
|
515
535
|
const switchPanel = (next: ToolbarPanel | null) => {
|
|
516
536
|
if (openPanel === 'columns' && next !== 'columns') onColumnsPanelClose?.();
|
|
537
|
+
if (openPanel === 'filters' && next !== 'filters') onFiltersPanelClose?.();
|
|
517
538
|
setOpenPanel(next);
|
|
518
539
|
};
|
|
519
540
|
const togglePanel = (panel: ToolbarPanel) => switchPanel(openPanel === panel ? null : panel);
|
|
@@ -526,6 +547,12 @@ export function GridToolbar({
|
|
|
526
547
|
setSeenForceColumnsOpen(forceColumnsOpen);
|
|
527
548
|
if (forceColumnsOpen) setOpenPanel('columns');
|
|
528
549
|
}
|
|
550
|
+
// forceFiltersOpen turning true opens the filter panel, the same way.
|
|
551
|
+
const [seenForceFiltersOpen, setSeenForceFiltersOpen] = useState(forceFiltersOpen);
|
|
552
|
+
if (seenForceFiltersOpen !== forceFiltersOpen) {
|
|
553
|
+
setSeenForceFiltersOpen(forceFiltersOpen);
|
|
554
|
+
if (forceFiltersOpen && onFilterModelChange) setOpenPanel('filters');
|
|
555
|
+
}
|
|
529
556
|
|
|
530
557
|
// Tell the grid this toolbar shows the Columns panel for the column menu; it can only do so
|
|
531
558
|
// when the grid's forceColumnsOpen reaches it and the Columns panel is enabled.
|
|
@@ -535,8 +562,14 @@ export function GridToolbar({
|
|
|
535
562
|
if (!toolbarHost || !hostsColumnsPanel) return undefined;
|
|
536
563
|
return toolbarHost.registerColumnsPanel();
|
|
537
564
|
}, [toolbarHost, hostsColumnsPanel]);
|
|
565
|
+
const hostsFiltersPanel = forceFiltersOpen !== undefined && Boolean(onFilterModelChange);
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
if (!toolbarHost || !hostsFiltersPanel) return undefined;
|
|
568
|
+
return toolbarHost.registerFiltersPanel();
|
|
569
|
+
}, [toolbarHost, hostsFiltersPanel]);
|
|
538
570
|
|
|
539
571
|
const aggButtonRef = useRef<HTMLButtonElement>(null);
|
|
572
|
+
const aiButtonRef = useRef<HTMLButtonElement>(null);
|
|
540
573
|
const pivotButtonRef = useRef<HTMLButtonElement>(null);
|
|
541
574
|
const colsButtonRef = useRef<HTMLButtonElement>(null);
|
|
542
575
|
const filterButtonRef = useRef<HTMLButtonElement>(null);
|
|
@@ -642,6 +675,24 @@ export function GridToolbar({
|
|
|
642
675
|
|
|
643
676
|
{/* Right: built-in buttons */}
|
|
644
677
|
<div className="ogx-toolbar__actions">
|
|
678
|
+
{onAiAssistantToggle && (
|
|
679
|
+
<div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
ref={aiButtonRef}
|
|
683
|
+
className={`ogx-toolbar__icon-btn ogx-toolbar__ai-btn${aiAssistantOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
|
|
684
|
+
aria-haspopup="dialog"
|
|
685
|
+
aria-expanded={aiAssistantOpen}
|
|
686
|
+
onClick={() => {
|
|
687
|
+
switchPanel(null);
|
|
688
|
+
onAiAssistantToggle(aiButtonRef.current);
|
|
689
|
+
}}
|
|
690
|
+
>
|
|
691
|
+
<AiSparkleIcon />
|
|
692
|
+
{aiAssistantLabel}
|
|
693
|
+
</button>
|
|
694
|
+
</div>
|
|
695
|
+
)}
|
|
645
696
|
{onFilterModelChange && (
|
|
646
697
|
<div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
|
|
647
698
|
{renderQuickFilter
|
|
@@ -45,5 +45,13 @@ export function createGridApiPlaceholder<R extends GridValidRowModel = GridRowMo
|
|
|
45
45
|
canUndo: () => false,
|
|
46
46
|
canRedo: () => false,
|
|
47
47
|
clearHistory: noop,
|
|
48
|
+
setSortModel: noop,
|
|
49
|
+
setRowGroupingModel: noop,
|
|
50
|
+
setAggregationModel: noop,
|
|
51
|
+
setColumnVisibilityModel: noop,
|
|
52
|
+
setPivotModel: noop,
|
|
53
|
+
getGridAiState: () => ({}),
|
|
54
|
+
openAiAssistant: noop,
|
|
55
|
+
closeAiAssistant: noop,
|
|
48
56
|
};
|
|
49
57
|
}
|
|
@@ -9,6 +9,11 @@ import { createContext } from 'react';
|
|
|
9
9
|
export interface GridToolbarHost {
|
|
10
10
|
/** Registers a toolbar that can show the Columns panel. Returns the function that unregisters it. */
|
|
11
11
|
registerColumnsPanel: () => () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Registers a toolbar that can show the filter panel when the header filter row asks for it
|
|
14
|
+
* (through `forceFiltersOpen`). Returns the function that unregisters it.
|
|
15
|
+
*/
|
|
16
|
+
registerFiltersPanel: () => () => void;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export const GridToolbarHostContext = createContext<GridToolbarHost | null>(null);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import type {
|
|
3
|
+
GridAggregationModel,
|
|
3
4
|
GridApi,
|
|
4
5
|
GridColDef,
|
|
6
|
+
GridColumnVisibilityModel,
|
|
5
7
|
GridFilterModel,
|
|
6
8
|
GridPaginationModel,
|
|
9
|
+
GridPivotModel,
|
|
10
|
+
GridRowGroupingModel,
|
|
7
11
|
GridRowId,
|
|
8
12
|
GridRowModel,
|
|
9
13
|
GridSortItem,
|
|
@@ -45,6 +49,16 @@ export interface UseGridApiMethodsParams {
|
|
|
45
49
|
getAllFilteredRows: () => GridRowModel[];
|
|
46
50
|
/** Columns on screen, in display order. */
|
|
47
51
|
visibleColumns: GridColDef[];
|
|
52
|
+
|
|
53
|
+
// v3.5: the remaining models, for the setters and getGridAiState.
|
|
54
|
+
rowGroupingModel: GridRowGroupingModel;
|
|
55
|
+
onRowGroupingModelChange: (model: GridRowGroupingModel) => void;
|
|
56
|
+
aggregationModel: GridAggregationModel;
|
|
57
|
+
onAggregationModelChange: (model: GridAggregationModel) => void;
|
|
58
|
+
columnVisibilityModel: GridColumnVisibilityModel;
|
|
59
|
+
onColumnVisibilityModelChange: (model: GridColumnVisibilityModel) => void;
|
|
60
|
+
pivotModel: GridPivotModel;
|
|
61
|
+
onPivotModelChange: (model: GridPivotModel) => void;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -111,5 +125,30 @@ export function useGridApiMethods(params: UseGridApiMethodsParams): void {
|
|
|
111
125
|
api.getVisibleRows = () => latest().getVisibleRows();
|
|
112
126
|
api.getAllFilteredRows = () => latest().getAllFilteredRows();
|
|
113
127
|
api.getVisibleColumns = () => latest().visibleColumns;
|
|
128
|
+
|
|
129
|
+
api.setSortModel = (model) => {
|
|
130
|
+
latestRef.current = { ...latest(), sortModel: model };
|
|
131
|
+
latest().onSortModelChange(model);
|
|
132
|
+
};
|
|
133
|
+
api.setRowGroupingModel = (model) => {
|
|
134
|
+
latestRef.current = { ...latest(), rowGroupingModel: model };
|
|
135
|
+
latest().onRowGroupingModelChange(model);
|
|
136
|
+
};
|
|
137
|
+
api.setAggregationModel = (model) => {
|
|
138
|
+
latestRef.current = { ...latest(), aggregationModel: model };
|
|
139
|
+
latest().onAggregationModelChange(model);
|
|
140
|
+
};
|
|
141
|
+
api.setColumnVisibilityModel = (model) => {
|
|
142
|
+
latestRef.current = { ...latest(), columnVisibilityModel: model };
|
|
143
|
+
latest().onColumnVisibilityModelChange(model);
|
|
144
|
+
};
|
|
145
|
+
api.setPivotModel = (model) => {
|
|
146
|
+
latestRef.current = { ...latest(), pivotModel: model };
|
|
147
|
+
latest().onPivotModelChange(model);
|
|
148
|
+
};
|
|
149
|
+
api.getGridAiState = () => {
|
|
150
|
+
const { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel } = latest();
|
|
151
|
+
return { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel };
|
|
152
|
+
};
|
|
114
153
|
}, [apiRef]);
|
|
115
154
|
}
|
|
@@ -5,6 +5,8 @@ import type { GridColumnGroupingModel, GridPaginationModel } from '../../types';
|
|
|
5
5
|
|
|
6
6
|
export interface UseGridAriaRowsParams {
|
|
7
7
|
columnGroupingModel: GridColumnGroupingModel | undefined;
|
|
8
|
+
/** Whether the header filter row (`headerFilters`) is rendered: one more header row. */
|
|
9
|
+
headerFilterRow?: boolean;
|
|
8
10
|
pinnedTopCount: number;
|
|
9
11
|
pinnedBottomCount: number;
|
|
10
12
|
pagination: boolean;
|
|
@@ -19,13 +21,13 @@ export interface UseGridAriaRowsParams {
|
|
|
19
21
|
/** `aria-rowcount` and the `aria-rowindex` bases of the pinned and centre rows. */
|
|
20
22
|
export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayout {
|
|
21
23
|
const {
|
|
22
|
-
columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
|
|
24
|
+
columnGroupingModel, headerFilterRow = false, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
|
|
23
25
|
paginationRowCount, paginatedUnpinnedRowCount, sortedUnpinnedRowCount,
|
|
24
26
|
} = params;
|
|
25
27
|
const { page, pageSize } = paginationModel;
|
|
26
28
|
|
|
27
29
|
return useMemo(() => getAriaRowLayout({
|
|
28
|
-
headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel),
|
|
30
|
+
headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel) + (headerFilterRow ? 1 : 0),
|
|
29
31
|
pinnedTopCount,
|
|
30
32
|
pinnedBottomCount,
|
|
31
33
|
pageRowCount: pagination ? paginatedUnpinnedRowCount : sortedUnpinnedRowCount,
|
|
@@ -33,6 +35,6 @@ export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayou
|
|
|
33
35
|
? Math.max(paginationRowCount, paginatedUnpinnedRowCount)
|
|
34
36
|
: sortedUnpinnedRowCount,
|
|
35
37
|
pageOffset: pagination ? page * pageSize : 0,
|
|
36
|
-
}), [columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
|
|
38
|
+
}), [columnGroupingModel, headerFilterRow, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
|
|
37
39
|
sortedUnpinnedRowCount, paginationMode, paginationRowCount, page, pageSize]);
|
|
38
40
|
}
|