@opencorestack/opengridx 3.9.1 → 3.10.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 +20 -0
- package/dist/index.d.ts +55 -0
- package/dist/opengridx.es.js +6346 -6279
- 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 +4 -0
- package/docs/features/ai-toolkit.md +1 -0
- package/docs/performance.md +1 -1
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.tsx +2 -1
- package/lib/components/DataGrid/DataGrid.tsx +19 -18
- package/lib/components/DataGrid/GridListView.tsx +1 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridViewport.tsx +2 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
- package/lib/components/Header/Header.tsx +4 -3
- package/lib/components/Row/Row.tsx +6 -2
- package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
- package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
- package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
- package/lib/components/Toolbar/GridToolbar.tsx +7 -380
- package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
- package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
- package/lib/hooks/core/gridKeyDown.ts +272 -0
- package/lib/hooks/core/useDataGrid.ts +13 -5
- package/lib/hooks/core/useGridApiMethods.ts +2 -2
- package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
- package/lib/hooks/core/useGridCellSelection.ts +17 -297
- package/lib/hooks/core/useGridColumnLookup.ts +2 -1
- package/lib/hooks/core/useGridColumns.tsx +3 -2
- package/lib/hooks/core/useGridControlledState.ts +3 -2
- package/lib/hooks/core/useGridHierarchy.ts +1 -1
- package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
- package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
- package/lib/hooks/core/useGridRowInteractions.ts +1 -1
- package/lib/hooks/core/useGridRowSelection.ts +11 -2
- package/lib/hooks/core/useGridToolbarProps.ts +3 -2
- package/lib/hooks/features/useGridAiAssistant.ts +3 -2
- package/lib/hooks/useColumnReorder.ts +2 -1
- package/lib/hooks/useTreeData.ts +4 -2
- package/lib/types/locale.ts +22 -0
- package/lib/utils/aggregation/index.ts +16 -0
- package/lib/utils/aiAssistant.ts +46 -15
- package/lib/utils/export/excelCells.ts +244 -0
- package/lib/utils/export/exportCsv.ts +179 -0
- package/lib/utils/export/exportExcelHtml.ts +220 -0
- package/lib/utils/export/exportJson.ts +179 -0
- package/lib/utils/export/exportRows.ts +85 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
- package/lib/utils/export/index.ts +5 -695
- package/lib/utils/export/printGrid.ts +282 -0
- package/lib/utils/gridLocale.ts +30 -0
- package/lib/utils/selectionSets.ts +36 -0
- package/lib/utils/untypedColumns.ts +16 -0
- package/llms.txt +3 -1
- package/package.json +4 -2
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, useCallback,
|
|
2
|
-
import ReactDOM from 'react-dom';
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback, useContext } from 'react';
|
|
3
2
|
import type { GridColDef, GridAggregationModel, GridPivotModel, GridFilterModel } from '../../types';
|
|
4
|
-
import {
|
|
5
|
-
import type { BuiltInAggFn } from '../../utils/aggregation';
|
|
6
|
-
import { getViewportWidth } from '../../utils/viewport';
|
|
7
|
-
import { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
|
|
3
|
+
import { getOfferedAggregationFunctions } from '../../utils/aggregation';
|
|
8
4
|
import { GridToolbarHostContext } from '../../hooks/core/gridToolbarHostContext';
|
|
9
5
|
import { PivotPanel, PivotIcon } from './PivotPanel';
|
|
10
6
|
import { GlobalSearch } from './GlobalSearch';
|
|
11
|
-
import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
|
|
12
|
-
import { FilterPanel } from '../FilterPanel/FilterPanel';
|
|
13
7
|
import { GridTooltip } from '../Tooltip/Tooltip';
|
|
14
8
|
import { AiSparkleIcon } from '../AiAssistant/GridAiAssistantPanel';
|
|
15
9
|
import { useGridLocale } from '../DataGrid/gridLocaleContext';
|
|
10
|
+
import { SigmaIcon, ViewColumnIcon, FilterIcon } from './ToolbarIcons';
|
|
11
|
+
import { AggregationPanel } from './AggregationPanel';
|
|
12
|
+
import { ColumnsPanelWrapper } from './ColumnsPanelWrapper';
|
|
13
|
+
import { FilterPanelWrapper } from './FilterPanelWrapper';
|
|
16
14
|
|
|
17
15
|
/** Props passed to a custom toolbar button renderer. */
|
|
18
16
|
export interface ToolbarButtonRenderProps {
|
|
@@ -99,379 +97,8 @@ export interface GridToolbarProps {
|
|
|
99
97
|
aiAssistantLabel?: string;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
|
|
103
|
-
|
|
104
|
-
function isBuiltInAggregationFunction(fn: string): fn is BuiltInAggFn {
|
|
105
|
-
return Object.prototype.hasOwnProperty.call(AGGREGATION_FUNCTIONS, fn);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* The summary functions offered for a column: its `availableAggregationFunctions` when set,
|
|
110
|
-
* otherwise every built-in one. Names the aggregation engine does not implement are dropped,
|
|
111
|
-
* because choosing one would show the label in the header but never compute a value.
|
|
112
|
-
*/
|
|
113
|
-
function getAggregationFunctions(col: GridColDef): BuiltInAggFn[] {
|
|
114
|
-
if (!col.availableAggregationFunctions) return BUILT_IN_AGGREGATION_FUNCTIONS;
|
|
115
|
-
return Array.from(new Set(col.availableAggregationFunctions.filter(isBuiltInAggregationFunction)));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
100
|
type ToolbarPanel = 'summaries' | 'pivot' | 'columns' | 'filters';
|
|
119
101
|
|
|
120
|
-
function SigmaIcon() {
|
|
121
|
-
return (
|
|
122
|
-
<svg
|
|
123
|
-
width="18" height="18" viewBox="0 0 24 24"
|
|
124
|
-
fill="none" stroke="currentColor" strokeWidth="2"
|
|
125
|
-
strokeLinecap="round" strokeLinejoin="round"
|
|
126
|
-
aria-hidden="true"
|
|
127
|
-
>
|
|
128
|
-
<path d="M18 4H6l6 8-6 8h12" />
|
|
129
|
-
</svg>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function ViewColumnIcon() {
|
|
134
|
-
return (
|
|
135
|
-
<svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none">
|
|
136
|
-
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="1.8" />
|
|
137
|
-
<rect x="8.5" y="4" width="7" height="16" fill="currentColor" opacity="0.25" />
|
|
138
|
-
<line x1="8.5" y1="4" x2="8.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
|
|
139
|
-
<line x1="15.5" y1="4" x2="15.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
|
|
140
|
-
<line x1="2" y1="9" x2="22" y2="9" stroke="currentColor" strokeWidth="1.2" />
|
|
141
|
-
<line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" strokeWidth="1.2" />
|
|
142
|
-
</svg>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function FilterIcon() {
|
|
147
|
-
return (
|
|
148
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
149
|
-
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
150
|
-
</svg>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
type PanelPosition = GridPanelPosition;
|
|
155
|
-
|
|
156
|
-
function AggregationPanel({
|
|
157
|
-
anchorRef,
|
|
158
|
-
aggregationModel,
|
|
159
|
-
aggregableColumns,
|
|
160
|
-
onFunctionChange,
|
|
161
|
-
onClearAll,
|
|
162
|
-
onClose,
|
|
163
|
-
activeCount,
|
|
164
|
-
}: {
|
|
165
|
-
anchorRef: React.RefObject<HTMLElement | null>;
|
|
166
|
-
aggregationModel: GridAggregationModel;
|
|
167
|
-
aggregableColumns: GridColDef[];
|
|
168
|
-
onFunctionChange: (field: string, fn: string) => void;
|
|
169
|
-
onClearAll: () => void;
|
|
170
|
-
onClose: () => void;
|
|
171
|
-
activeCount: number;
|
|
172
|
-
}) {
|
|
173
|
-
const locale = useGridLocale();
|
|
174
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
175
|
-
const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
|
|
176
|
-
const [portalContainer, setPortalContainer] = useState<Element | null>(null);
|
|
177
|
-
|
|
178
|
-
useLayoutEffect(() => {
|
|
179
|
-
setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
|
|
180
|
-
}, [anchorRef]);
|
|
181
|
-
|
|
182
|
-
const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
|
|
183
|
-
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
setPos(computePos());
|
|
186
|
-
const update = () => setPos(computePos());
|
|
187
|
-
window.addEventListener('scroll', update, true);
|
|
188
|
-
window.addEventListener('resize', update);
|
|
189
|
-
return () => {
|
|
190
|
-
window.removeEventListener('scroll', update, true);
|
|
191
|
-
window.removeEventListener('resize', update);
|
|
192
|
-
};
|
|
193
|
-
}, [computePos]);
|
|
194
|
-
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
function handleClick(e: MouseEvent) {
|
|
197
|
-
if (
|
|
198
|
-
panelRef.current && !panelRef.current.contains(e.target as Node) &&
|
|
199
|
-
anchorRef.current && !anchorRef.current.contains(e.target as Node)
|
|
200
|
-
) {
|
|
201
|
-
onClose();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
document.addEventListener('mousedown', handleClick);
|
|
205
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
206
|
-
}, [anchorRef, onClose]);
|
|
207
|
-
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
|
|
210
|
-
document.addEventListener('keydown', onKey);
|
|
211
|
-
return () => document.removeEventListener('keydown', onKey);
|
|
212
|
-
}, [onClose]);
|
|
213
|
-
|
|
214
|
-
const panel = (
|
|
215
|
-
<div
|
|
216
|
-
ref={panelRef}
|
|
217
|
-
className="ogx-toolbar__panel"
|
|
218
|
-
role="dialog"
|
|
219
|
-
aria-label={locale.toolbarSummariesPanelLabel}
|
|
220
|
-
style={{ top: pos.top, right: pos.right, left: pos.left, width: 400, maxWidth: 'calc(100vw - 24px)' }}
|
|
221
|
-
>
|
|
222
|
-
<div className="ogx-toolbar__panel-header">
|
|
223
|
-
<span className="ogx-toolbar__panel-title">
|
|
224
|
-
<SigmaIcon />
|
|
225
|
-
{locale.toolbarSummaries}
|
|
226
|
-
</span>
|
|
227
|
-
{activeCount > 0 && (
|
|
228
|
-
<button
|
|
229
|
-
type="button"
|
|
230
|
-
className="ogx-toolbar__clear-btn"
|
|
231
|
-
onClick={onClearAll}
|
|
232
|
-
title={locale.toolbarSummariesClearAllTooltip}
|
|
233
|
-
>
|
|
234
|
-
{locale.toolbarSummariesClearAll}
|
|
235
|
-
</button>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
<div className="ogx-toolbar__panel-body">
|
|
240
|
-
{aggregableColumns.length === 0 ? (
|
|
241
|
-
<p className="ogx-toolbar__empty">
|
|
242
|
-
{locale.toolbarSummariesEmpty}<br />
|
|
243
|
-
{`${locale.toolbarSummariesEmptyHintBefore} `}<code>aggregable: true</code>{` ${locale.toolbarSummariesEmptyHintAfter}`}
|
|
244
|
-
</p>
|
|
245
|
-
) : (
|
|
246
|
-
aggregableColumns.map((col) => {
|
|
247
|
-
const currentFn = aggregationModel[col.field] || 'none';
|
|
248
|
-
return (
|
|
249
|
-
<div key={col.field} className="ogx-toolbar__agg-row">
|
|
250
|
-
<span className="ogx-toolbar__agg-label" title={col.field}>
|
|
251
|
-
{col.headerName || col.field}
|
|
252
|
-
</span>
|
|
253
|
-
<div className="ogx-toolbar__agg-pills">
|
|
254
|
-
{['none', ...getAggregationFunctions(col)].map((fn) => (
|
|
255
|
-
<button
|
|
256
|
-
type="button"
|
|
257
|
-
key={fn}
|
|
258
|
-
className={`ogx-toolbar__pill${currentFn === fn ? ' ogx-toolbar__pill--active' : ''}`}
|
|
259
|
-
onClick={() => onFunctionChange(col.field, fn)}
|
|
260
|
-
aria-pressed={currentFn === fn}
|
|
261
|
-
>
|
|
262
|
-
{locale.toolbarAggregationFunction(fn)}
|
|
263
|
-
</button>
|
|
264
|
-
))}
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
267
|
-
);
|
|
268
|
-
})
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function ColumnsPanelWrapper({
|
|
278
|
-
anchorRef,
|
|
279
|
-
columns,
|
|
280
|
-
visibleColumns,
|
|
281
|
-
onVisibilityChange,
|
|
282
|
-
onShowAll,
|
|
283
|
-
onHideAll,
|
|
284
|
-
onColumnReorder,
|
|
285
|
-
onColumnOrderReset,
|
|
286
|
-
onClose,
|
|
287
|
-
showNonHideableColumns,
|
|
288
|
-
}: {
|
|
289
|
-
anchorRef: React.RefObject<HTMLElement | null>;
|
|
290
|
-
columns: GridColDef[];
|
|
291
|
-
visibleColumns: Set<string>;
|
|
292
|
-
onVisibilityChange: (field: string, isVisible: boolean) => void;
|
|
293
|
-
onShowAll: () => void;
|
|
294
|
-
onHideAll: () => void;
|
|
295
|
-
onColumnReorder?: (fromField: string, toField: string) => void;
|
|
296
|
-
onColumnOrderReset?: () => void;
|
|
297
|
-
onClose: () => void;
|
|
298
|
-
showNonHideableColumns?: boolean;
|
|
299
|
-
}) {
|
|
300
|
-
const locale = useGridLocale();
|
|
301
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
302
|
-
const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
|
|
303
|
-
const [portalContainer, setPortalContainer] = useState<Element | null>(null);
|
|
304
|
-
|
|
305
|
-
useLayoutEffect(() => {
|
|
306
|
-
setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
|
|
307
|
-
}, [anchorRef]);
|
|
308
|
-
|
|
309
|
-
const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
|
|
310
|
-
|
|
311
|
-
useEffect(() => {
|
|
312
|
-
setPos(computePos());
|
|
313
|
-
const update = () => setPos(computePos());
|
|
314
|
-
window.addEventListener('scroll', update, true);
|
|
315
|
-
window.addEventListener('resize', update);
|
|
316
|
-
return () => {
|
|
317
|
-
window.removeEventListener('scroll', update, true);
|
|
318
|
-
window.removeEventListener('resize', update);
|
|
319
|
-
};
|
|
320
|
-
}, [computePos]);
|
|
321
|
-
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
function handleClick(e: MouseEvent) {
|
|
324
|
-
if (
|
|
325
|
-
panelRef.current && !panelRef.current.contains(e.target as Node) &&
|
|
326
|
-
anchorRef.current && !anchorRef.current.contains(e.target as Node)
|
|
327
|
-
) {
|
|
328
|
-
onClose();
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
document.addEventListener('mousedown', handleClick);
|
|
332
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
333
|
-
}, [anchorRef, onClose]);
|
|
334
|
-
|
|
335
|
-
useEffect(() => {
|
|
336
|
-
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
|
|
337
|
-
document.addEventListener('keydown', onKey);
|
|
338
|
-
return () => document.removeEventListener('keydown', onKey);
|
|
339
|
-
}, [onClose]);
|
|
340
|
-
|
|
341
|
-
const panel = (
|
|
342
|
-
<div
|
|
343
|
-
ref={panelRef}
|
|
344
|
-
className="ogx-toolbar__panel"
|
|
345
|
-
role="dialog"
|
|
346
|
-
aria-label={locale.toolbarColumnsPanelLabel}
|
|
347
|
-
style={{ top: pos.top, right: pos.right, left: pos.left, width: 280, padding: 0 }}
|
|
348
|
-
>
|
|
349
|
-
<ColumnVisibilityPanel
|
|
350
|
-
columns={columns}
|
|
351
|
-
visibleColumns={visibleColumns}
|
|
352
|
-
onVisibilityChange={onVisibilityChange}
|
|
353
|
-
onShowAll={onShowAll}
|
|
354
|
-
onHideAll={onHideAll}
|
|
355
|
-
onColumnReorder={onColumnReorder}
|
|
356
|
-
onColumnOrderReset={onColumnOrderReset}
|
|
357
|
-
showNonHideableColumns={showNonHideableColumns}
|
|
358
|
-
/>
|
|
359
|
-
</div>
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function FilterPanelWrapper({
|
|
366
|
-
anchorRef,
|
|
367
|
-
columns,
|
|
368
|
-
filterModel,
|
|
369
|
-
onFilterModelChange,
|
|
370
|
-
onClose
|
|
371
|
-
}: {
|
|
372
|
-
anchorRef: React.RefObject<HTMLElement | null>;
|
|
373
|
-
columns: GridColDef[];
|
|
374
|
-
filterModel: GridFilterModel;
|
|
375
|
-
onFilterModelChange: (model: GridFilterModel) => void;
|
|
376
|
-
onClose: () => void;
|
|
377
|
-
}) {
|
|
378
|
-
const locale = useGridLocale();
|
|
379
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
380
|
-
const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
|
|
381
|
-
// Stable ref for onClose — prevents the mousedown effect from
|
|
382
|
-
// tearing down and re-registering on every filterModel update,
|
|
383
|
-
// which could fire the close callback during the re-register window.
|
|
384
|
-
const onCloseRef = useRef(onClose);
|
|
385
|
-
useLayoutEffect(() => { onCloseRef.current = onClose; }, [onClose]);
|
|
386
|
-
const [portalContainer, setPortalContainer] = useState<Element | null>(null);
|
|
387
|
-
|
|
388
|
-
useLayoutEffect(() => {
|
|
389
|
-
setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
|
|
390
|
-
}, [anchorRef]);
|
|
391
|
-
|
|
392
|
-
const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
|
|
393
|
-
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
setPos(computePos());
|
|
396
|
-
const update = () => setPos(computePos());
|
|
397
|
-
window.addEventListener('scroll', update, true);
|
|
398
|
-
window.addEventListener('resize', update);
|
|
399
|
-
return () => {
|
|
400
|
-
window.removeEventListener('scroll', update, true);
|
|
401
|
-
window.removeEventListener('resize', update);
|
|
402
|
-
};
|
|
403
|
-
}, [computePos]);
|
|
404
|
-
|
|
405
|
-
// Only Escape key closes the filter panel automatically.
|
|
406
|
-
// Click-outside is intentionally removed: users need to type in filter
|
|
407
|
-
// inputs without the panel closing. There is an explicit Close button.
|
|
408
|
-
useEffect(() => {
|
|
409
|
-
function handleKey(e: KeyboardEvent) {
|
|
410
|
-
if (e.key === 'Escape') onCloseRef.current();
|
|
411
|
-
}
|
|
412
|
-
document.addEventListener('keydown', handleKey);
|
|
413
|
-
return () => document.removeEventListener('keydown', handleKey);
|
|
414
|
-
// Empty deps — listener is registered once and uses the stable ref
|
|
415
|
-
}, []);
|
|
416
|
-
|
|
417
|
-
const activeFilterCount = filterModel.items?.length ?? 0;
|
|
418
|
-
|
|
419
|
-
const handleClearAll = () => {
|
|
420
|
-
onFilterModelChange({ ...filterModel, items: [], logicOperator: 'and' });
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
const panel = (
|
|
424
|
-
<div
|
|
425
|
-
ref={panelRef}
|
|
426
|
-
className="ogx-toolbar__panel ogx-toolbar__panel--filter"
|
|
427
|
-
role="dialog"
|
|
428
|
-
aria-label={locale.toolbarFiltersLabel}
|
|
429
|
-
style={{ top: pos.top, right: pos.right, left: pos.left, width: 540, maxWidth: 'calc(100vw - 24px)' }}
|
|
430
|
-
>
|
|
431
|
-
<div className="ogx-toolbar__panel-header">
|
|
432
|
-
<span className="ogx-toolbar__panel-title">
|
|
433
|
-
<FilterIcon />
|
|
434
|
-
{locale.toolbarFilters}
|
|
435
|
-
{activeFilterCount > 0 && (
|
|
436
|
-
<span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
|
|
437
|
-
)}
|
|
438
|
-
</span>
|
|
439
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
440
|
-
{activeFilterCount > 0 && (
|
|
441
|
-
<button
|
|
442
|
-
type="button"
|
|
443
|
-
className="ogx-toolbar__clear-btn"
|
|
444
|
-
onClick={handleClearAll}
|
|
445
|
-
title={locale.toolbarFiltersClearAllTooltip}
|
|
446
|
-
>
|
|
447
|
-
{locale.toolbarFiltersClearAll}
|
|
448
|
-
</button>
|
|
449
|
-
)}
|
|
450
|
-
<button
|
|
451
|
-
type="button"
|
|
452
|
-
className="ogx-toolbar__close-btn"
|
|
453
|
-
onClick={() => onCloseRef.current()}
|
|
454
|
-
title={locale.toolbarFiltersCloseTooltip}
|
|
455
|
-
aria-label={locale.toolbarFiltersCloseLabel}
|
|
456
|
-
>
|
|
457
|
-
{locale.toolbarFiltersClose}
|
|
458
|
-
</button>
|
|
459
|
-
</div>
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
<div className="ogx-toolbar__panel-body ogx-toolbar__panel-body--filter">
|
|
463
|
-
<FilterPanel
|
|
464
|
-
columns={columns}
|
|
465
|
-
filterModel={filterModel}
|
|
466
|
-
onFilterModelChange={onFilterModelChange}
|
|
467
|
-
/>
|
|
468
|
-
</div>
|
|
469
|
-
</div>
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
102
|
const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
|
|
476
103
|
// Module-level default, so `columns` is the same frozen value on every render (React Compiler).
|
|
477
104
|
const NO_COLUMNS: GridColDef[] = [];
|
|
@@ -576,7 +203,7 @@ export function GridToolbar({
|
|
|
576
203
|
const baseFields = baseColumns ? new Set(baseColumns.map((c) => c.field)) : null;
|
|
577
204
|
const aggregableColumns = columns.filter(
|
|
578
205
|
(c) => c.aggregable !== false && (c.type === 'number' || c.aggregable === true)
|
|
579
|
-
&&
|
|
206
|
+
&& getOfferedAggregationFunctions(c).length > 0
|
|
580
207
|
&& (!baseFields || baseFields.has(c.field))
|
|
581
208
|
);
|
|
582
209
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Icons of the toolbar buttons and panel titles.
|
|
2
|
+
|
|
3
|
+
export function SigmaIcon() {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width="18" height="18" viewBox="0 0 24 24"
|
|
7
|
+
fill="none" stroke="currentColor" strokeWidth="2"
|
|
8
|
+
strokeLinecap="round" strokeLinejoin="round"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
>
|
|
11
|
+
<path d="M18 4H6l6 8-6 8h12" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ViewColumnIcon() {
|
|
17
|
+
return (
|
|
18
|
+
<svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none">
|
|
19
|
+
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="1.8" />
|
|
20
|
+
<rect x="8.5" y="4" width="7" height="16" fill="currentColor" opacity="0.25" />
|
|
21
|
+
<line x1="8.5" y1="4" x2="8.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
|
|
22
|
+
<line x1="15.5" y1="4" x2="15.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
|
|
23
|
+
<line x1="2" y1="9" x2="22" y2="9" stroke="currentColor" strokeWidth="1.2" />
|
|
24
|
+
<line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" strokeWidth="1.2" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function FilterIcon() {
|
|
30
|
+
return (
|
|
31
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
32
|
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { getViewportWidth } from '../../utils/viewport';
|
|
3
|
+
import { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
|
|
4
|
+
|
|
5
|
+
export interface UseToolbarPanelOptions {
|
|
6
|
+
/** Close when the user presses the mouse outside the panel and its button. Default: true. */
|
|
7
|
+
closeOnOutsideClick?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseToolbarPanelResult {
|
|
11
|
+
panelRef: React.RefObject<HTMLDivElement | null>;
|
|
12
|
+
/** Where to place the panel: under its button, kept on screen (`getPanelPosition`). */
|
|
13
|
+
position: GridPanelPosition;
|
|
14
|
+
/** The element to portal into: the enclosing theme provider (so its tokens apply), else `document.body`. */
|
|
15
|
+
portalContainer: Element | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What every toolbar panel needs: a portal target, a position under its button that follows page
|
|
20
|
+
* scroll and resize, and closing on Escape (and, by default, on a press outside it). `onClose` is
|
|
21
|
+
* read through a ref, so the listeners are registered once and a re-render never leaves a window in
|
|
22
|
+
* which a close could be lost or fired twice.
|
|
23
|
+
*/
|
|
24
|
+
export function useToolbarPanel(
|
|
25
|
+
anchorRef: React.RefObject<HTMLElement | null>,
|
|
26
|
+
onClose: () => void,
|
|
27
|
+
{ closeOnOutsideClick = true }: UseToolbarPanelOptions = {},
|
|
28
|
+
): UseToolbarPanelResult {
|
|
29
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
const [position, setPosition] = useState<GridPanelPosition>({ top: 0, right: 0 });
|
|
31
|
+
const [portalContainer, setPortalContainer] = useState<Element | null>(null);
|
|
32
|
+
const onCloseRef = useRef(onClose);
|
|
33
|
+
useLayoutEffect(() => { onCloseRef.current = onClose; });
|
|
34
|
+
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
|
|
37
|
+
}, [anchorRef]);
|
|
38
|
+
|
|
39
|
+
const computePosition = useCallback(() => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setPosition(computePosition());
|
|
43
|
+
const update = () => setPosition(computePosition());
|
|
44
|
+
window.addEventListener('scroll', update, true);
|
|
45
|
+
window.addEventListener('resize', update);
|
|
46
|
+
return () => {
|
|
47
|
+
window.removeEventListener('scroll', update, true);
|
|
48
|
+
window.removeEventListener('resize', update);
|
|
49
|
+
};
|
|
50
|
+
}, [computePosition]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!closeOnOutsideClick) return undefined;
|
|
54
|
+
function handleMouseDown(e: MouseEvent) {
|
|
55
|
+
const target = e.target as Node;
|
|
56
|
+
if (panelRef.current && !panelRef.current.contains(target) && anchorRef.current && !anchorRef.current.contains(target)) {
|
|
57
|
+
onCloseRef.current();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
document.addEventListener('mousedown', handleMouseDown);
|
|
61
|
+
return () => document.removeEventListener('mousedown', handleMouseDown);
|
|
62
|
+
}, [anchorRef, closeOnOutsideClick]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
function handleKey(e: KeyboardEvent) { if (e.key === 'Escape') onCloseRef.current(); }
|
|
66
|
+
document.addEventListener('keydown', handleKey);
|
|
67
|
+
return () => document.removeEventListener('keydown', handleKey);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return { panelRef, position, portalContainer };
|
|
71
|
+
}
|