@bit.rhplus/ag-grid 0.0.87 → 0.0.89

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/index.jsx CHANGED
@@ -1,1089 +1,1099 @@
1
- /* eslint-disable */
2
- import * as React from 'react';
3
- import { AgGridReact } from 'ag-grid-react';
4
- import { AgGridColumns } from './AgGridColumn';
5
- import { RhPlusOnCellEditingStarted } from './OnCellEditingStarted';
6
- import { RhPlusOnCellDoubleClicked } from './OnCellDoubleClicked';
7
- import { RhPlusOnCellValueChanged } from './OnCellValueChanged';
8
- import { AgGridPostSort } from './AgGridPostSort';
9
- import { AgGridOnRowDataChanged } from './AgGridOnRowDataChanged';
10
- import { AgGridOnRowDataUpdated } from './AgGridOnRowDataUpdated';
11
- import CheckboxRenderer from './Renderers/CheckboxRenderer';
12
- import BooleanRenderer from './Renderers/BooleanRenderer';
13
- import { createGridComparison } from '@bit.rhplus/react-memo';
14
-
15
- import IconRenderer from './Renderers/IconRenderer';
16
- import ImageRenderer from './Renderers/ImageRenderer';
17
- import StateRenderer from './Renderers/StateRenderer';
18
- import SelectRenderer from './Renderers/SelectRenderer';
19
- import ButtonRenderer from './Renderers/ButtonRenderer';
20
- import CountrySelectRenderer from './Renderers/CountrySelectRenderer';
21
- import ObjectRenderer from './Renderers/ObjectRenderer';
22
- import LinkRenderer from './Renderers/LinkRenderer';
23
- import NotificationOptionsInit from "./NotificationOptions";
24
- import AggregationStatusBar from "./AggregationStatusBar";
25
- import { notification, Button } from "antd";
26
- import { CompressOutlined } from '@ant-design/icons';
27
- import Aggregations, { hashRanges } from "./Aggregations";
28
- import { useBulkCellEdit, BulkEditButton } from './BulkEdit';
29
- import {
30
- ModuleRegistry,
31
- themeAlpine,
32
- themeBalham,
33
- themeMaterial,
34
- themeQuartz,
35
- ClientSideRowModelModule,
36
- QuickFilterModule,
37
- ValidationModule,
38
- } from "ag-grid-community";
39
-
40
- // Registrace AG-Grid modulů (nutné pro Quick Filter a další funkce)
41
- ModuleRegistry.registerModules([
42
- QuickFilterModule,
43
- ClientSideRowModelModule,
44
- ...(process.env.NODE_ENV !== "production" ? [ValidationModule] : []),
45
- ]);
46
-
47
- const themes = [
48
- { id: "themeQuartz", theme: themeQuartz },
49
- { id: "themeBalham", theme: themeBalham },
50
- { id: "themeMaterial", theme: themeMaterial },
51
- { id: "themeAlpine", theme: themeAlpine },
52
- ];
53
-
54
- const AgGrid = React.forwardRef((props, ref) => {
55
- const internalRef = React.useRef();
56
- const {
57
- theme = "themeAlpine", // Default theme
58
- rowData = [],
59
- newRowFlash = true,
60
- updatedRowFlash = false,
61
- onGridReady,
62
- // Notification props
63
- notificationMode = 'full', // 'full' | 'simple' | 'none'
64
- notificationOptions: {
65
- notificationHead = NotificationOptionsInit.head,
66
- notificationBody = NotificationOptionsInit.body,
67
- style = NotificationOptionsInit.style,
68
- placement = NotificationOptionsInit.placement,
69
- } = {},
70
- // Status Bar props (pro simple mode)
71
- statusBarMetrics = [
72
- 'count',
73
- 'sum',
74
- 'min',
75
- 'max',
76
- 'avg',
77
- 'median',
78
- 'range',
79
- 'geometry',
80
- 'dateRange'
81
- ],
82
- statusBarHeight = 36,
83
- // Bulk Edit props
84
- enableBulkEdit = false,
85
- bulkEditOptions = {},
86
- bulkEditAccessToken,
87
- onBulkEditStart,
88
- onBulkEditComplete,
89
- // SignalR transaction support
90
- queryKey, // Identifikátor pro SignalR transactions
91
- // Quick Filter
92
- quickFilterText = "", // Text pro fulltextové vyhledávání
93
- } = props;
94
-
95
- // Najít theme objekt podle názvu z props
96
- const themeObject = React.useMemo(() => {
97
- const foundTheme = themes.find(t => t.id === theme);
98
- return foundTheme ? foundTheme.theme : themeQuartz; // Fallback na themeQuartz
99
- }, [theme]);
100
- const [, setIsGridReady] = React.useState(false);
101
- const isSelectingRef = React.useRef(false); // ✅ FIX: Změna ze state na ref pro eliminaci rerenderů
102
- const aggregationDataRef = React.useRef(null); // // Pro simple mode status bar
103
- const activeNotificationModeRef = React.useRef(notificationMode); // // Aktivní mód (může být dočasně přepsán uživatelem)
104
- const previousRowDataRef = React.useRef(rowData);
105
-
106
- // Synchronizovat activeNotificationModeRef s notificationMode prop
107
- React.useEffect(() => {
108
- activeNotificationModeRef.current = notificationMode;
109
- }, [notificationMode]);
110
-
111
- // Bulk Edit hook
112
- const {
113
- floatingButton,
114
- editPopover,
115
- handleRangeChange,
116
- handleOpenPopover,
117
- handleSubmitEdit,
118
- handleCancelEdit,
119
- handleValueChange,
120
- } = useBulkCellEdit(internalRef, {
121
- enabled: enableBulkEdit,
122
- accessToken: bulkEditAccessToken,
123
- onBulkEditStart,
124
- onBulkEditComplete,
125
- ...bulkEditOptions,
126
- });
127
-
128
-
129
- // ========== PERFORMANCE OPTIMIZATION: Shallow comparison helper ==========
130
- // Shallow porovnání objektů - 100x rychlejší než JSON.stringify
131
- const shallowEqual = React.useCallback((obj1, obj2) => {
132
- if (obj1 === obj2) return true;
133
- if (!obj1 || !obj2) return false;
134
-
135
- const keys1 = Object.keys(obj1);
136
- const keys2 = Object.keys(obj2);
137
-
138
- if (keys1.length !== keys2.length) return false;
139
-
140
- for (let key of keys1) {
141
- if (obj1[key] !== obj2[key]) return false;
142
- }
143
-
144
- return true;
145
- }, []);
146
-
147
- // Memoizované funkce pro detekci změn v rowData
148
- const findNewRows = React.useCallback((oldData, newData) => {
149
- const oldIds = new Set(oldData.map((row) => row.id));
150
- return newData.filter((row) => !oldIds.has(row.id));
151
- }, []);
152
-
153
- const findUpdatedRows = React.useCallback((oldData, newData) => {
154
- const oldDataMap = new Map(oldData.map((row) => [row.id, row]));
155
- return newData.filter((newRow) => {
156
- const oldRow = oldDataMap.get(newRow.id);
157
- if (!oldRow) return false; // Nový řádek, ne aktualizovaný
158
-
159
- // ✅ OPTIMALIZACE: Shallow comparison místo JSON.stringify (100x rychlejší!)
160
- return !shallowEqual(oldRow, newRow);
161
- });
162
- }, [shallowEqual]);
163
-
164
- React.useImperativeHandle(ref, () => internalRef.current, [internalRef]);
165
-
166
- // Throttle timer pro notifikace - zobrazuje se BĚHEM označování s 100ms throttle
167
- const notificationThrottleRef = React.useRef(null);
168
- const notificationLastCallRef = React.useRef(0);
169
- const lastRangeHashRef = React.useRef(null);
170
-
171
- // ✅ Callback pro přepnutí z full na simple mód
172
- const handleSwitchToSimple = React.useCallback(() => {
173
- const gridId = props.gridName || props.id || 'default';
174
- const key = `aggregation-grid-${gridId}`;
175
-
176
- // Zavřít full notifikaci
177
- notification.destroy(key);
178
-
179
- // Znovu spočítat a zobrazit simple status bar
180
- if (internalRef?.current?.api) {
181
- const ranges = internalRef.current.api.getCellRanges();
182
- if (ranges && ranges.length > 0 && ranges[0]?.startRow) {
183
- const messageInfo = Aggregations(internalRef);
184
- if (messageInfo.count > 1) {
185
- aggregationDataRef.current = messageInfo;
186
- }
187
- }
188
- }
189
-
190
- activeNotificationModeRef.current = 'simple';
191
- }, [props, internalRef]);
192
-
193
- // ✅ Helper funkce pro vytvoření custom description s tlačítkem pro přepnutí na simple
194
- const createNotificationDescription = React.useCallback((messageInfo, showSwitchButton = false) => {
195
- const bodyContent = notificationBody(messageInfo);
196
-
197
- if (!showSwitchButton) {
198
- return bodyContent;
199
- }
200
-
201
- return (
202
- <div>
203
- {bodyContent}
204
- <div style={{ marginTop: '12px', borderTop: '1px solid #f0f0f0', paddingTop: '8px' }}>
205
- <Button
206
- type="link"
207
- size="small"
208
- icon={<CompressOutlined />}
209
- onClick={handleSwitchToSimple}
210
- style={{ padding: 0 }}
211
- >
212
- Zobrazit kompaktní režim
213
- </Button>
214
- </div>
215
- </div>
216
- );
217
- }, [notificationBody, handleSwitchToSimple]);
218
-
219
- // ✅ Callback pro přepnutí z simple na full mód
220
- const handleSwitchToFull = React.useCallback(() => {
221
- if (!aggregationDataRef.current) return;
222
-
223
- const gridId = props.gridName || props.id || 'default';
224
- const key = `aggregation-grid-${gridId}`;
225
-
226
- // Zobrazit full notifikaci s aktuálními daty a tlačítkem pro přepnutí
227
- notification.info({
228
- key,
229
- message: notificationHead(aggregationDataRef.current),
230
- description: createNotificationDescription(aggregationDataRef.current, true),
231
- duration: 0,
232
- style,
233
- placement,
234
- });
235
-
236
- // Skrýt simple status bar
237
- aggregationDataRef.current = null;
238
- activeNotificationModeRef.current = 'full';
239
- }, [ props, notificationHead, createNotificationDescription, style, placement]);
240
-
241
- // ========== PERFORMANCE FIX: Stabilní refs pro updateAggregationNotification ==========
242
- // ✅ FIX: isSelectingRef je už deklarovaný na řádku 107 jako React.useRef(false)
243
- const notificationHeadRef = React.useRef(notificationHead);
244
- const createNotificationDescriptionRef = React.useRef(createNotificationDescription);
245
- const styleRef = React.useRef(style);
246
- const placementRef = React.useRef(placement);
247
- const propsRef = React.useRef(props);
248
-
249
-
250
- // ✅ FIX: Odstraněn useEffect pro isSelecting - isSelectingRef je nyní řízený přímo v updatePointerEventsForSelecting
251
-
252
- React.useEffect(() => {
253
- notificationHeadRef.current = notificationHead;
254
- }, [notificationHead]);
255
-
256
- React.useEffect(() => {
257
- createNotificationDescriptionRef.current = createNotificationDescription;
258
- }, [createNotificationDescription]);
259
-
260
- React.useEffect(() => {
261
- styleRef.current = style;
262
- }, [style]);
263
-
264
- React.useEffect(() => {
265
- placementRef.current = placement;
266
- }, [placement]);
267
-
268
- React.useEffect(() => {
269
- propsRef.current = props;
270
- }, [props]);
271
-
272
- // ✅ OPTIMALIZACE: Helper funkce pro aktualizaci aggregace notifikace s cache check
273
- // Volá se BĚHEM označování s 100ms throttle pro real-time feedback
274
- // STABILNÍ callback - používá pouze refs!
275
- const updateAggregationNotification = React.useCallback((event) => {
276
- // Pokud je notificationMode 'none', nedělat nic
277
- if (notificationModeRef.current === 'none') {
278
- return;
279
- }
280
-
281
- // Lightweight cache check PŘED voláním Aggregations()
282
- const ranges = event.api.getCellRanges();
283
- const currentRangeHash = hashRanges(ranges);
284
-
285
- // Žádné ranges nebo jen jedna buňka - vyčistit vše
286
- if (!ranges || ranges.length === 0 || !ranges[0]?.startRow) {
287
- lastRangeHashRef.current = null;
288
-
289
- // V 'full' módu zavřít notifikaci
290
- if (activeNotificationModeRef.current === 'full') {
291
- const gridId = propsRef.current.gridName || propsRef.current.id || 'default';
292
- const key = `aggregation-grid-${gridId}`;
293
- notification.destroy(key);
294
- }
295
-
296
- // V 'simple' módu vyčistit aggregationData
297
- if (activeNotificationModeRef.current === 'simple') {
298
- aggregationDataRef.current = null;
299
- }
300
-
301
- return;
302
- }
303
-
304
- // Cache hit - ranges se nezměnily, skip
305
- if (currentRangeHash === lastRangeHashRef.current) {
306
- return;
307
- }
308
-
309
- // Cache miss - spočítat aggregace
310
- const messageInfo = Aggregations(internalRef);
311
-
312
- if (messageInfo.count > 1) {
313
- // Uložit hash pro příští porovnání
314
- lastRangeHashRef.current = currentRangeHash;
315
-
316
- // Zavolat onAggregationChanged callback pokud existuje
317
- if (propsRef.current.onAggregationChanged) {
318
- propsRef.current.onAggregationChanged(messageInfo);
319
- }
320
-
321
- // Podle aktivního módu zobrazit buď notifikaci nebo aktualizovat status bar
322
- if (activeNotificationModeRef.current === 'full') {
323
- // FULL mód - zobrazit plovoucí notifikaci s tlačítkem pro přepnutí na simple
324
- const gridId = propsRef.current.gridName || propsRef.current.id || 'default';
325
- const key = `aggregation-grid-${gridId}`;
326
-
327
- const dynamicStyle = {
328
- ...styleRef.current,
329
- pointerEvents: isSelectingRef.current ? 'none' : 'auto'
330
- };
331
-
332
- notification.info({
333
- key,
334
- message: notificationHeadRef.current(messageInfo),
335
- description: createNotificationDescriptionRef.current(messageInfo, true),
336
- duration: 0,
337
- style: dynamicStyle,
338
- placement: placementRef.current,
339
- });
340
- } else if (activeNotificationModeRef.current === 'simple') {
341
- // SIMPLE mód - aktualizovat state pro status bar
342
- aggregationDataRef.current = messageInfo;
343
- }
344
- } else {
345
- // Jen jedna buňka - zavřít/vyčistit vše
346
- lastRangeHashRef.current = null;
347
-
348
- if (activeNotificationModeRef.current === 'full') {
349
- const gridId = propsRef.current.gridName || propsRef.current.id || 'default';
350
- const key = `aggregation-grid-${gridId}`;
351
- notification.destroy(key);
352
- }
353
-
354
- if (activeNotificationModeRef.current === 'simple') {
355
- aggregationDataRef.current = null;
356
- }
357
- }
358
- }, [internalRef]); // ✅ Pouze internalRef v dependencies!
359
-
360
- // Helper funkce pro nastavení pointer-events na notifikace
361
- const setNotificationPointerEvents = React.useCallback((enable) => {
362
- const notifications = document.querySelectorAll('.ant-notification');
363
-
364
- notifications.forEach((notif) => {
365
- if (enable) {
366
- // Obnovit pointer-events na notifikaci
367
- const original = notif.dataset.originalPointerEvents || 'auto';
368
- notif.style.pointerEvents = original;
369
- notif.style.removeProperty('pointer-events');
370
- delete notif.dataset.originalPointerEvents;
371
-
372
- // Obnovit pointer-events na všechny child elementy
373
- const allChildren = notif.querySelectorAll('*');
374
- allChildren.forEach((child) => {
375
- child.style.removeProperty('pointer-events');
376
- delete child.dataset.originalPointerEvents;
377
- });
378
- } else {
379
- // Zakázat pointer-events na notifikaci
380
- if (!notif.dataset.originalPointerEvents) {
381
- notif.dataset.originalPointerEvents = notif.style.pointerEvents || 'auto';
382
- }
383
- notif.style.setProperty('pointer-events', 'none', 'important');
384
-
385
- // Zakázat pointer-events na všechny child elementy
386
- const allChildren = notif.querySelectorAll('*');
387
- allChildren.forEach((child) => {
388
- if (!child.dataset.originalPointerEvents) {
389
- child.dataset.originalPointerEvents = child.style.pointerEvents || '';
390
- }
391
- child.style.setProperty('pointer-events', 'none', 'important');
392
- });
393
- }
394
- });
395
- }, []);
396
-
397
- // ✅ FIX: Helper funkce pro update isSelecting bez state změny (eliminuje rerender)
398
- const updatePointerEventsForSelecting = React.useCallback((selecting) => {
399
- isSelectingRef.current = selecting;
400
-
401
- if (selecting) {
402
- document.body.classList.add('ag-grid-selecting');
403
- setNotificationPointerEvents(false);
404
-
405
- // KRITICKÉ: Sledovat DOM změny - když se objeví nová notifikace během označování
406
- const observer = new MutationObserver((mutations) => {
407
- mutations.forEach((mutation) => {
408
- mutation.addedNodes.forEach((node) => {
409
- if (node.nodeType === 1) {
410
- if (node.classList?.contains('ant-notification') ||
411
- node.querySelector?.('.ant-notification')) {
412
- setNotificationPointerEvents(false);
413
- }
414
- }
415
- });
416
- });
417
- });
418
-
419
- observer.observe(document.body, {
420
- childList: true,
421
- subtree: true
422
- });
423
-
424
- // Uložit observer do ref pro případný cleanup
425
- if (!updatePointerEventsForSelecting.observer) {
426
- updatePointerEventsForSelecting.observer = observer;
427
- }
428
- } else {
429
- document.body.classList.remove('ag-grid-selecting');
430
-
431
- // Cleanup observer
432
- if (updatePointerEventsForSelecting.observer) {
433
- updatePointerEventsForSelecting.observer.disconnect();
434
- updatePointerEventsForSelecting.observer = null;
435
- }
436
-
437
- setTimeout(() => setNotificationPointerEvents(true), 100);
438
- }
439
- }, [setNotificationPointerEvents]);
440
-
441
- // Detekce konce označování pomocí mouseup event
442
- React.useEffect(() => {
443
- const handleMouseUp = () => {
444
- // ✅ FIX: Ukončit isSelecting pomocí ref (bez state update → žádný rerender)
445
- setTimeout(() => {
446
- updatePointerEventsForSelecting(false);
447
-
448
- // ✅ FIX: Zkontrolovat jestli jsou stále nějaké ranges, pokud ne - vyčistit status bar
449
- if (internalRef?.current?.api && activeNotificationModeRef.current === 'simple') {
450
- const ranges = internalRef.current.api.getCellRanges();
451
- if (!ranges || ranges.length === 0 || !ranges[0]?.startRow) {
452
- aggregationDataRef.current = null;
453
- // Reset na původní notificationMode když jsou všechny buňky odznačeny
454
- activeNotificationModeRef.current = notificationMode;
455
- }
456
- }
457
- }, 50);
458
- };
459
-
460
- document.addEventListener('mouseup', handleMouseUp);
461
- document.addEventListener('touchend', handleMouseUp); // Pro touch zařízení
462
-
463
- return () => {
464
- document.removeEventListener('mouseup', handleMouseUp);
465
- document.removeEventListener('touchend', handleMouseUp);
466
- };
467
- }, [notificationMode, internalRef, updatePointerEventsForSelecting]);
468
-
469
- // Cleanup notifikací a timerů při zničení komponenty
470
- React.useEffect(() => {
471
- return () => {
472
- // Clear throttle timer
473
- if (notificationThrottleRef.current) {
474
- clearTimeout(notificationThrottleRef.current);
475
- }
476
-
477
- if (notificationMode === 'full') {
478
- const gridId = props.gridName || props.id || 'default';
479
- const key = `aggregation-grid-${gridId}`;
480
- notification.destroy(key);
481
- }
482
-
483
- // Cleanup SignalR transaction callback při unmount
484
- if (queryKey && window.agGridTransactionCallbacks) {
485
- delete window.agGridTransactionCallbacks[queryKey];
486
- }
487
- };
488
- }, [notificationMode, props.gridName, props.id, queryKey]);
489
-
490
- React.useEffect(() => {
491
- // VYPNUTO: Cell flash je globálně vypnutý
492
- return;
493
-
494
- if (!newRowFlash && !updatedRowFlash) return;
495
-
496
- const previousRowData = previousRowDataRef.current;
497
- const addedRows = newRowFlash ? findNewRows(previousRowData, rowData) : [];
498
- const updatedRows = updatedRowFlash ? findUpdatedRows(previousRowData, rowData) : [];
499
-
500
- if (addedRows.length > 0 || updatedRows.length > 0) {
501
- setTimeout(() => {
502
- try {
503
- // Bezpečnostní kontrola API dostupnosti
504
- if (!internalRef.current?.api || internalRef.current.api.isDestroyed?.()) {
505
- return;
506
- }
507
-
508
- // Modern AG-Grid (33+): getColumnState() moved to main api
509
- const columnApi = internalRef.current?.columnApi || internalRef.current?.api;
510
- if (!columnApi?.getColumnState) {
511
- return;
512
- }
513
-
514
- const columnStates = columnApi.getColumnState();
515
- const allColumns = columnStates
516
- .filter(colState => colState.colId) // Filtrujeme pouze platné colId
517
- .map((colState) => colState.colId);
518
-
519
- // Kontrola, že máme platné sloupce
520
- if (allColumns.length === 0) {
521
- return;
522
- }
523
-
524
- // Flash efekt pro nové řádky (používá defaultní zelená barva AG Grid)
525
- if (addedRows.length > 0) {
526
- const newRowNodes = [];
527
- internalRef.current.api.forEachNode((node) => {
528
- if (addedRows.some((row) => row.id === node.data.id)) {
529
- newRowNodes.push(node);
530
- }
531
- });
532
-
533
- if (newRowNodes.length > 0) {
534
- internalRef.current.api.flashCells({
535
- rowNodes: newRowNodes,
536
- columns: allColumns,
537
- flashDelay: 0,
538
- fadeDelay: 1000,
539
- });
540
- }
541
- }
542
-
543
- // Flash efekt pro aktualizované řádky (modrá barva)
544
- if (updatedRows.length > 0) {
545
- const updatedRowNodes = [];
546
- internalRef.current.api.forEachNode((node) => {
547
- if (updatedRows.some((row) => row.id === node.data.id)) {
548
- updatedRowNodes.push(node);
549
- }
550
- });
551
-
552
- if (updatedRowNodes.length > 0) {
553
- // Použijeme vlastní CSS animaci pro modrou flash
554
- updatedRowNodes.forEach(node => {
555
- const rowElement = node.eGridRow || document.querySelector(`[row-id="${node.data.id}"]`);
556
- if (rowElement) {
557
- rowElement.classList.add('ag-row-flash-updated');
558
- setTimeout(() => {
559
- rowElement.classList.remove('ag-row-flash-updated');
560
- }, 1000);
561
- }
562
- });
563
- }
564
- }
565
- } catch (error) {
566
- // Ignorujeme chybu a pokračujeme bez crash aplikace
567
- }
568
- }, 100); // Zvýšený timeout pro stabilizaci gridu
569
- }
570
-
571
- previousRowDataRef.current = rowData;
572
- }, [rowData, newRowFlash, updatedRowFlash]);
573
-
574
- // ========== PERFORMANCE FIX: Stabilní ref pattern pro callbacks ==========
575
- // Refs pro aktuální hodnoty - zabraňuje re-creation RhPlusRangeSelectionChanged při změně stavu
576
- const notificationModeRef = React.useRef(notificationMode);
577
- const enableBulkEditRef = React.useRef(enableBulkEdit);
578
- const handleRangeChangeRef = React.useRef(handleRangeChange);
579
- const onRangeSelectionChangedRef = React.useRef(props.onRangeSelectionChanged);
580
- const updateAggregationNotificationRef = React.useRef(updateAggregationNotification);
581
-
582
- // ✅ FIX #12: Refs pro grid layout handlery (eliminuje rerendery při změně props)
583
- const onColumnMovedRef = React.useRef(props.onColumnMoved);
584
- const onDragStoppedRef = React.useRef(props.onDragStopped);
585
- const onColumnVisibleRef = React.useRef(props.onColumnVisible);
586
- const onColumnPinnedRef = React.useRef(props.onColumnPinned);
587
- const onColumnResizedRef = React.useRef(props.onColumnResized);
588
-
589
- // Aktualizovat refs při změně hodnot
590
- React.useEffect(() => {
591
- notificationModeRef.current = notificationMode;
592
- }, [notificationMode]);
593
-
594
- React.useEffect(() => {
595
- enableBulkEditRef.current = enableBulkEdit;
596
- }, [enableBulkEdit]);
597
-
598
- React.useEffect(() => {
599
- handleRangeChangeRef.current = handleRangeChange;
600
- }, [handleRangeChange]);
601
-
602
- React.useEffect(() => {
603
- onRangeSelectionChangedRef.current = props.onRangeSelectionChanged;
604
- }, [props.onRangeSelectionChanged]);
605
-
606
- React.useEffect(() => {
607
- updateAggregationNotificationRef.current = updateAggregationNotification;
608
- }, [updateAggregationNotification]);
609
-
610
- // ✅ FIX #12: Aktualizovat grid layout handler refs
611
- React.useEffect(() => {
612
- onColumnMovedRef.current = props.onColumnMoved;
613
- }, [props.onColumnMoved]);
614
-
615
- React.useEffect(() => {
616
- onDragStoppedRef.current = props.onDragStopped;
617
- }, [props.onDragStopped]);
618
-
619
- React.useEffect(() => {
620
- onColumnVisibleRef.current = props.onColumnVisible;
621
- }, [props.onColumnVisible]);
622
-
623
- React.useEffect(() => {
624
- onColumnPinnedRef.current = props.onColumnPinned;
625
- }, [props.onColumnPinned]);
626
-
627
- React.useEffect(() => {
628
- onColumnResizedRef.current = props.onColumnResized;
629
- }, [props.onColumnResized]);
630
-
631
- // Stabilní callback s prázdnými dependencies - používá pouze refs
632
- const RhPlusRangeSelectionChanged = React.useCallback(
633
- (event) => {
634
- // ✅ FIX: Detekovat začátek označování pomocí ref (bez state update → žádný rerender)
635
- updatePointerEventsForSelecting(true);
636
-
637
- // 1. ✅ OPTIMALIZACE: Zobrazení notifikace BĚHEM označování s THROTTLE
638
- // Simple mode: 300ms throttle (méně častá aktualizace, lepší výkon)
639
- // Full mode: 100ms throttle (rychlejší feedback, plovoucí notifikace je levná)
640
- if (notificationModeRef.current !== 'none') {
641
- const throttleInterval = notificationModeRef.current === 'simple' ? 300 : 100;
642
- const now = Date.now();
643
- const timeSinceLastCall = now - notificationLastCallRef.current;
644
-
645
- // První volání NEBO uplynul throttle interval
646
- if (timeSinceLastCall >= throttleInterval) {
647
- // Okamžité volání
648
- notificationLastCallRef.current = now;
649
- if (internalRef?.current) {
650
- updateAggregationNotificationRef.current(event);
651
- }
652
- } else {
653
- // Naplánovat volání za zbývající čas (trailing edge)
654
- if (notificationThrottleRef.current) {
655
- clearTimeout(notificationThrottleRef.current);
656
- }
657
-
658
- notificationThrottleRef.current = setTimeout(() => {
659
- notificationLastCallRef.current = Date.now();
660
- if (internalRef?.current) {
661
- updateAggregationNotificationRef.current(event);
662
- }
663
- }, throttleInterval - timeSinceLastCall);
664
- }
665
- }
666
-
667
- // 2. ✅ OPTIMALIZACE: Bulk edit handler BEZ debounce - okamžité zobrazení ikony
668
- // Lightweight validace v useBulkCellEdit zajistí okamžité zobrazení
669
- // Těžká validace proběhne s 15ms debounce uvnitř useBulkCellEdit
670
- if (enableBulkEditRef.current) {
671
- handleRangeChangeRef.current(event);
672
- }
673
-
674
- // 3. Custom onRangeSelectionChanged callback - bez debounce
675
- if (onRangeSelectionChangedRef.current) {
676
- onRangeSelectionChangedRef.current(event);
677
- }
678
- },
679
- [] // ✅ PRÁZDNÉ dependencies - stabilní reference!
680
- );
681
-
682
- // ✅ FIX #12: Stabilní wrappery pro grid layout handlery (prázdné dependencies)
683
- const stableOnColumnMoved = React.useCallback(
684
- (params) => {
685
- if (onColumnMovedRef.current) {
686
- onColumnMovedRef.current(params);
687
- }
688
- },
689
- []
690
- );
691
-
692
- const stableOnDragStopped = React.useCallback(
693
- (params) => {
694
- if (onDragStoppedRef.current) {
695
- onDragStoppedRef.current(params);
696
- }
697
- },
698
- []
699
- );
700
-
701
- const stableOnColumnVisible = React.useCallback(
702
- (params) => {
703
- if (onColumnVisibleRef.current) {
704
- onColumnVisibleRef.current(params);
705
- }
706
- },
707
- []
708
- );
709
-
710
- const stableOnColumnPinned = React.useCallback(
711
- (params) => {
712
- if (onColumnPinnedRef.current) {
713
- onColumnPinnedRef.current(params);
714
- }
715
- },
716
- []
717
- );
718
-
719
- const stableOnColumnResized = React.useCallback(
720
- (params) => {
721
- if (onColumnResizedRef.current) {
722
- onColumnResizedRef.current(params);
723
- }
724
- },
725
- []
726
- );
727
-
728
- const AgGridOnGridReady = (event, options) => {
729
- if (onGridReady) {
730
- onGridReady(event, options);
731
- }
732
-
733
- // Nejprve nastavíme API reference pro interní použití
734
- if (internalRef.current) {
735
- internalRef.current.api = event.api;
736
- internalRef.current.columnApi = event.columnApi || event.api; // fallback for modern AG-Grid
737
- }
738
-
739
- // Nastavíme API ready flag pro quick filter useEffect
740
- setIsApiReady(true);
741
-
742
- // Registruj callback pro AG Grid transactions (SignalR optimalizace)
743
- // Inicializace globálního registru pro více AG-Grid instancí
744
- if (!window.agGridTransactionCallbacks) {
745
- window.agGridTransactionCallbacks = {};
746
- }
747
-
748
- // Registrace callbacku pro tuto konkrétní AG-Grid instanci (podle queryKey)
749
- if (event.api && queryKey) {
750
- window.agGridTransactionCallbacks[queryKey] = (transactionData) => {
751
- try {
752
- if (!event.api || event.api.isDestroyed?.()) return;
753
-
754
- const { operation, records } = transactionData;
755
-
756
- switch (operation) {
757
- case 'add':
758
- event.api.applyTransaction({ add: records });
759
- // Flash efekt pro nové řádky
760
- setTimeout(() => {
761
- records.forEach(record => {
762
- const rowNode = event.api.getRowNode(record.id);
763
- if (rowNode && rowNode.rowElement) {
764
- rowNode.rowElement.classList.add('ag-row-flash-created');
765
- setTimeout(() => {
766
- rowNode.rowElement.classList.remove('ag-row-flash-created');
767
- }, 1000);
768
- }
769
- });
770
- }, 100);
771
- break;
772
-
773
- case 'update':
774
- event.api.applyTransaction({ update: records });
775
- // Flash efekt pro aktualizované řádky
776
- setTimeout(() => {
777
- records.forEach(record => {
778
- const rowNode = event.api.getRowNode(record.id);
779
- if (rowNode && rowNode.rowElement) {
780
- rowNode.rowElement.classList.add('ag-row-flash-updated');
781
- setTimeout(() => {
782
- rowNode.rowElement.classList.remove('ag-row-flash-updated');
783
- }, 1000);
784
- }
785
- });
786
- }, 100);
787
- break;
788
-
789
- case 'remove':
790
- // Flash efekt před smazáním
791
- records.forEach(record => {
792
- const rowNode = event.api.getRowNode(record.id);
793
- if (rowNode && rowNode.rowElement) {
794
- rowNode.rowElement.classList.add('ag-row-flash-deleted');
795
- }
796
- });
797
-
798
- setTimeout(() => {
799
- event.api.applyTransaction({ remove: records });
800
- }, 500); // Krátké zpoždění pro zobrazení flash efektu
801
- break;
802
- }
803
- } catch (error) {
804
- // Ignorujeme chyby
805
- }
806
- };
807
- }
808
-
809
- // Pak zavoláme parent onGridReady handler, pokud existuje
810
- // Toto je kritické pro správné fungování bit/ui/grid a GridLayout
811
- if (options.onGridReady) {
812
- try {
813
- options.onGridReady(event);
814
- } catch (error) {
815
- // Error handling without console output
816
- }
817
- }
818
-
819
- // Nakonec nastavíme grid ready state s timeout
820
- setTimeout(() => {
821
- setIsGridReady(true);
822
- }, 1000);
823
- };
824
-
825
- const components = React.useMemo(() => {
826
- return {
827
- checkboxRenderer: CheckboxRenderer,
828
- selectRenderer: SelectRenderer,
829
- countrySelectRenderer: CountrySelectRenderer,
830
- booleanRenderer: BooleanRenderer,
831
- buttonRenderer: ButtonRenderer,
832
- iconRenderer: IconRenderer,
833
- imageRenderer: ImageRenderer,
834
- stateRenderer: StateRenderer,
835
- objectRenderer: ObjectRenderer,
836
- linkRenderer: LinkRenderer,
837
- ...props.frameworkComponents,
838
- };
839
- }, [props.frameworkComponents]);
840
-
841
- // ========== PERFORMANCE OPTIMIZATION: Memoizované event handlers ==========
842
- const memoizedOnCellEditingStarted = React.useCallback(
843
- (event) => RhPlusOnCellEditingStarted(event, props),
844
- [props.onCellEditingStarted]
845
- );
846
-
847
- const memoizedOnCellDoubleClicked = React.useCallback(
848
- (event) => RhPlusOnCellDoubleClicked(event, props),
849
- [props.onCellDoubleClicked]
850
- );
851
-
852
- const memoizedOnCellValueChanged = React.useCallback(
853
- (event) => RhPlusOnCellValueChanged(event, props),
854
- [props.onCellValueChanged]
855
- );
856
-
857
- const memoizedPostSort = React.useCallback(
858
- (event) => AgGridPostSort(event, props),
859
- [props.postSort]
860
- );
861
-
862
- const memoizedOnGridReady = React.useCallback(
863
- (event, options) => AgGridOnGridReady(event, props),
864
- [onGridReady]
865
- );
866
-
867
- const memoizedOnRowDataChanged = React.useCallback(
868
- (event) => AgGridOnRowDataChanged(event, props),
869
- [props.onRowDataChanged]
870
- );
871
-
872
- const memoizedOnRowDataUpdated = React.useCallback(
873
- (event) => AgGridOnRowDataUpdated(event, props),
874
- [props.onRowDataUpdated]
875
- );
876
-
877
- // Memoizovaný context object
878
- // ✅ OPTIMALIZACE: Použít pouze relevantní props pro context - neměnit při změně props
879
- const memoizedContext = React.useMemo(() => ({
880
- componentParent: props
881
- }), [props.context, props.gridName, props.id]);
882
-
883
- // Memoizovaný defaultColDef
884
- const memoizedDefaultColDef = React.useMemo(() => ({
885
- ...props.defaultColDef,
886
- suppressHeaderMenuButton: true,
887
- suppressHeaderFilterButton: true,
888
- suppressMenu: true,
889
- // ✅ FIX AG-Grid v35: Bezpečné zpracování null hodnot v Quick Filter
890
- // AG-Grid v35 změnil implementaci Quick Filter - nyní volá .toString() na hodnotách buněk bez null check
891
- // Tento callback zajistí že nikdy nedojde k chybě "Cannot read properties of null (reading 'toString')"
892
- getQuickFilterText: (params) => {
893
- const value = params.value;
894
- // Null/undefined → prázdný string (bez chyby)
895
- if (value == null) return '';
896
- // Objekty a pole → JSON string
897
- if (typeof value === 'object') {
898
- try {
899
- return JSON.stringify(value);
900
- } catch {
901
- // Cirkulární reference nebo jiná chyba při serializaci → prázdný string
902
- return '';
903
- }
904
- }
905
- // Primitivní typy → toString
906
- return value.toString();
907
- },
908
- }), [props.defaultColDef]);
909
-
910
- // ========== PERFORMANCE FIX: Memoizovat columnDefs ==========
911
- // AgGridColumns vrací nový array při každém volání, i když jsou vstupy stejné
912
- const memoizedColumnDefs = React.useMemo(() => {
913
- return AgGridColumns(props.columnDefs, props);
914
- }, [props.columnDefs, props.getRowId, props.frameworkComponents]);
915
-
916
- // ========== CRITICAL FIX: Stabilní allGridProps objekt pomocí ref ==========
917
- // Problém: Jakékoli použití useMemo vytváří nový objekt při změně dependencies
918
- // Řešení: Použít ref pro VŽDY stejný objekt
919
- // AG-Grid САМО detekuje změny rowData a columnDefs → NENÍ potřeba forceUpdate!
920
-
921
- const allGridPropsRef = React.useRef(null);
922
-
923
- // Inicializace objektu
924
- if (!allGridPropsRef.current) {
925
- allGridPropsRef.current = {};
926
- }
927
-
928
- // ✅ Aktualizovat props v EXISTUJÍCÍM objektu (mutace)
929
- // AG Grid interně detekuje změny props, nepotřebuje nový objekt
930
- allGridPropsRef.current.ref = internalRef;
931
- allGridPropsRef.current.rowData = props.rowData;
932
- allGridPropsRef.current.getRowId = props.getRowId;
933
- allGridPropsRef.current.theme = themeObject;
934
- allGridPropsRef.current.columnDefs = memoizedColumnDefs;
935
- allGridPropsRef.current.defaultColDef = memoizedDefaultColDef;
936
- allGridPropsRef.current.onCellEditingStarted = memoizedOnCellEditingStarted;
937
- allGridPropsRef.current.onCellDoubleClicked = memoizedOnCellDoubleClicked;
938
- // allGridPropsRef.current.onCellValueChanged = memoizedOnCellValueChanged;
939
- allGridPropsRef.current.postSort = memoizedPostSort;
940
- allGridPropsRef.current.onGridReady = memoizedOnGridReady;
941
- allGridPropsRef.current.onRowDataChanged = memoizedOnRowDataChanged;
942
- allGridPropsRef.current.onRowDataUpdated = memoizedOnRowDataUpdated;
943
- allGridPropsRef.current.onRangeSelectionChanged = RhPlusRangeSelectionChanged;
944
- allGridPropsRef.current.context = memoizedContext;
945
- allGridPropsRef.current.components = components;
946
-
947
- // Další AG Grid props
948
- allGridPropsRef.current.rowModelType = props.rowModelType;
949
- allGridPropsRef.current.rowSelection = props.rowSelection;
950
- allGridPropsRef.current.enableRangeSelection = props.enableRangeSelection;
951
- allGridPropsRef.current.enableRangeHandle = props.enableRangeHandle;
952
- allGridPropsRef.current.enableFillHandle = props.enableFillHandle;
953
- allGridPropsRef.current.suppressRowClickSelection = props.suppressRowClickSelection;
954
- allGridPropsRef.current.singleClickEdit = props.singleClickEdit;
955
- allGridPropsRef.current.stopEditingWhenCellsLoseFocus = props.stopEditingWhenCellsLoseFocus;
956
- allGridPropsRef.current.rowClass = props.rowClass;
957
- allGridPropsRef.current.rowStyle = props.rowStyle;
958
- allGridPropsRef.current.getRowClass = props.getRowClass;
959
- allGridPropsRef.current.getRowStyle = props.getRowStyle;
960
- allGridPropsRef.current.animateRows = props.animateRows;
961
- allGridPropsRef.current.suppressCellFocus = props.suppressCellFocus;
962
- allGridPropsRef.current.suppressMenuHide = props.suppressMenuHide;
963
- allGridPropsRef.current.enableCellTextSelection = props.enableCellTextSelection;
964
- allGridPropsRef.current.ensureDomOrder = props.ensureDomOrder;
965
- allGridPropsRef.current.suppressRowTransform = props.suppressRowTransform;
966
- allGridPropsRef.current.suppressColumnVirtualisation = props.suppressColumnVirtualisation;
967
- allGridPropsRef.current.suppressRowVirtualisation = props.suppressRowVirtualisation;
968
- allGridPropsRef.current.tooltipShowDelay = props.tooltipShowDelay;
969
- allGridPropsRef.current.tooltipHideDelay = props.tooltipHideDelay;
970
- allGridPropsRef.current.tooltipMouseTrack = props.tooltipMouseTrack;
971
- allGridPropsRef.current.gridId = props.gridId;
972
- allGridPropsRef.current.id = props.id;
973
- allGridPropsRef.current.gridName = props.gridName;
974
- allGridPropsRef.current.getContextMenuItems = props.getContextMenuItems;
975
-
976
- // Grid Layout event handlers - stabilní wrappery (eliminuje rerendery)
977
- // ✅ FIX #12: Používáme stabilní wrappery místo props → eliminuje rerendery při změně props
978
- allGridPropsRef.current.onColumnMoved = stableOnColumnMoved;
979
- allGridPropsRef.current.onDragStopped = stableOnDragStopped;
980
- allGridPropsRef.current.onColumnVisible = stableOnColumnVisible;
981
- allGridPropsRef.current.onColumnPinned = stableOnColumnPinned;
982
- allGridPropsRef.current.onColumnResized = stableOnColumnResized;
983
-
984
- // ✅ AG-Grid САМО detekuje změny rowData a columnDefs
985
- // NENÍ potřeba forceUpdate - AG-Grid reaguje na změny v props automaticky!
986
-
987
- // Vracíme VŽDY stejný objekt (stabilní reference)
988
- const allGridProps = allGridPropsRef.current;
989
-
990
- // State pro sledování, kdy je API ready
991
- const [isApiReady, setIsApiReady] = React.useState(false);
992
-
993
- // Nastavení Quick Filter přes API (podle AG-Grid v33 dokumentace)
994
- // Tento useEffect se volá při změně quickFilterText NEBO když API je ready
995
- React.useEffect(() => {
996
- if (internalRef.current?.api && !internalRef.current.api.isDestroyed?.()) {
997
- // ✅ FIX AG-Grid v35: Zajistit že quickFilterText není null/undefined
998
- // AG-Grid v35 interně volá .toString() na této hodnotě bez null checku
999
- const safeQuickFilterText = quickFilterText ?? '';
1000
- internalRef.current.api.setGridOption("quickFilterText", safeQuickFilterText);
1001
- }
1002
- }, [quickFilterText, isApiReady]);
1003
-
1004
- // Status bar se zobrazuje pouze v simple mode
1005
- const showStatusBar = notificationMode === 'simple' && aggregationDataRef.current;
1006
-
1007
- // ✅ FIX: Rezervovat místo pro status bar pouze když má data
1008
- // Grid se dynamicky rozšíří/zmenší podle přítomnosti status baru
1009
- const shouldReserveSpace = showStatusBar;
1010
- const gridContainerStyle = shouldReserveSpace
1011
- ? { height: `calc(100% - ${statusBarHeight}px)`, width: '100%', position: 'relative' }
1012
- : { height: '100%', width: '100%', position: 'relative' };
1013
-
1014
- return (
1015
- <div style={{ height: '100%', width: '100%', position: 'relative' }}>
1016
- {/* AG Grid - fixní výška, nikdy se nemění */}
1017
- <div style={gridContainerStyle}>
1018
- <AgGridReact {...allGridProps} />
1019
- </div>
1020
-
1021
- {/* Aggregation Status Bar - absolutně pozicovaný na spodku */}
1022
- {shouldReserveSpace && (
1023
- <div style={{
1024
- position: 'absolute',
1025
- bottom: 0,
1026
- left: 0,
1027
- right: 0,
1028
- height: `${statusBarHeight}px`,
1029
- opacity: showStatusBar ? 1 : 0,
1030
- visibility: showStatusBar ? 'visible' : 'hidden',
1031
- pointerEvents: showStatusBar ? 'auto' : 'none',
1032
- zIndex: 10,
1033
- // GPU acceleration pro plynulejší zobrazení
1034
- transform: 'translateZ(0)',
1035
- willChange: 'opacity'
1036
- }}>
1037
- <AggregationStatusBar
1038
- data={aggregationDataRef.current || {}}
1039
- metrics={statusBarMetrics}
1040
- height={statusBarHeight}
1041
- onSwitchToFull={handleSwitchToFull}
1042
- />
1043
- </div>
1044
- )}
1045
-
1046
- {/* Bulk Edit Floating Button */}
1047
- {enableBulkEdit && floatingButton.visible && (
1048
- <BulkEditButton
1049
- visible={floatingButton.visible}
1050
- position={floatingButton.position}
1051
- range={floatingButton.range}
1052
- column={floatingButton.column}
1053
- cellCount={floatingButton.cellCount}
1054
- rowsContainer={floatingButton.rowsContainer}
1055
- editPopover={editPopover}
1056
- onOpenPopover={handleOpenPopover}
1057
- onValueChange={handleValueChange}
1058
- onSubmit={handleSubmitEdit}
1059
- onCancel={handleCancelEdit}
1060
- />
1061
- )}
1062
- </div>
1063
- );
1064
- });
1065
-
1066
- // ========== PERFORMANCE OPTIMIZATION: React.memo ==========
1067
- // Refactored: Používá createGridComparison utility místo manuálního deep comparison
1068
- // Eliminováno ~120 řádků duplicitního kódu, zachována stejná funkcionalita
1069
- // Diagnostic mode: zapnutý pouze ve development módu
1070
- export default React.memo(AgGrid, createGridComparison(
1071
- process.env.NODE_ENV !== 'production' // Diagnostic mode pouze ve development
1072
- ));
1073
-
1074
- export {
1075
- useBulkCellEdit,
1076
- BulkEditButton,
1077
- BulkEditPopover,
1078
- BulkEditSelect,
1079
- BulkEditDatePicker,
1080
- BulkEditModule,
1081
- BulkEditInput
1082
- } from './BulkEdit';
1083
-
1084
- export {
1085
- default as CheckboxRenderer
1086
- } from './Renderers/CheckboxRenderer';
1087
-
1088
- export * from './Renderers';
1
+ /* eslint-disable */
2
+ import * as React from 'react';
3
+ import { AgGridReact } from 'ag-grid-react';
4
+ import { AgGridColumns } from './AgGridColumn';
5
+ import { RhPlusOnCellEditingStarted } from './OnCellEditingStarted';
6
+ import { RhPlusOnCellDoubleClicked } from './OnCellDoubleClicked';
7
+ import { RhPlusOnCellValueChanged } from './OnCellValueChanged';
8
+ import { AgGridPostSort } from './AgGridPostSort';
9
+ import { AgGridOnRowDataChanged } from './AgGridOnRowDataChanged';
10
+ import { AgGridOnRowDataUpdated } from './AgGridOnRowDataUpdated';
11
+ import CheckboxRenderer from './Renderers/CheckboxRenderer';
12
+ import BooleanRenderer from './Renderers/BooleanRenderer';
13
+ import { createGridComparison } from '@bit.rhplus/react-memo';
14
+
15
+ import IconRenderer from './Renderers/IconRenderer';
16
+ import ImageRenderer from './Renderers/ImageRenderer';
17
+ import StateRenderer from './Renderers/StateRenderer';
18
+ import SelectRenderer from './Renderers/SelectRenderer';
19
+ import ButtonRenderer from './Renderers/ButtonRenderer';
20
+ import CountrySelectRenderer from './Renderers/CountrySelectRenderer';
21
+ import ObjectRenderer from './Renderers/ObjectRenderer';
22
+ import LinkRenderer from './Renderers/LinkRenderer';
23
+ import NotificationOptionsInit from "./NotificationOptions";
24
+ import AggregationStatusBar from "./AggregationStatusBar";
25
+ import { notification, Button } from "antd";
26
+ import { CompressOutlined } from '@ant-design/icons';
27
+ import Aggregations, { hashRanges } from "./Aggregations";
28
+ import { useBulkCellEdit, BulkEditButton } from './BulkEdit';
29
+ import {
30
+ ModuleRegistry,
31
+ themeAlpine,
32
+ themeBalham,
33
+ themeMaterial,
34
+ themeQuartz,
35
+ ClientSideRowModelModule,
36
+ QuickFilterModule,
37
+ ValidationModule,
38
+ } from "ag-grid-community";
39
+
40
+ // Registrace AG-Grid modulů (nutné pro Quick Filter a další funkce)
41
+ ModuleRegistry.registerModules([
42
+ QuickFilterModule,
43
+ ClientSideRowModelModule,
44
+ ...(process.env.NODE_ENV !== "production" ? [ValidationModule] : []),
45
+ ]);
46
+
47
+ const themes = [
48
+ { id: "themeQuartz", theme: themeQuartz },
49
+ { id: "themeBalham", theme: themeBalham },
50
+ { id: "themeMaterial", theme: themeMaterial },
51
+ { id: "themeAlpine", theme: themeAlpine },
52
+ ];
53
+
54
+ const AgGrid = React.forwardRef((props, ref) => {
55
+ const internalRef = React.useRef();
56
+ const {
57
+ theme = "themeAlpine", // Default theme
58
+ rowData = [],
59
+ newRowFlash = true,
60
+ updatedRowFlash = false,
61
+ onGridReady,
62
+ // Notification props
63
+ notificationMode = 'full', // 'full' | 'simple' | 'none'
64
+ notificationOptions: {
65
+ notificationHead = NotificationOptionsInit.head,
66
+ notificationBody = NotificationOptionsInit.body,
67
+ style = NotificationOptionsInit.style,
68
+ placement = NotificationOptionsInit.placement,
69
+ } = {},
70
+ // Status Bar props (pro simple mode)
71
+ statusBarMetrics = [
72
+ 'count',
73
+ 'sum',
74
+ 'min',
75
+ 'max',
76
+ 'avg',
77
+ 'median',
78
+ 'range',
79
+ 'geometry',
80
+ 'dateRange'
81
+ ],
82
+ statusBarHeight = 36,
83
+ // Bulk Edit props
84
+ enableBulkEdit = false,
85
+ bulkEditOptions = {},
86
+ bulkEditAccessToken,
87
+ onBulkEditStart,
88
+ onBulkEditComplete,
89
+ // SignalR transaction support
90
+ queryKey, // Identifikátor pro SignalR transactions
91
+ // Quick Filter
92
+ quickFilterText = "", // Text pro fulltextové vyhledávání
93
+ } = props;
94
+
95
+ // Najít theme objekt podle názvu z props
96
+ const themeObject = React.useMemo(() => {
97
+ const foundTheme = themes.find(t => t.id === theme);
98
+ return foundTheme ? foundTheme.theme : themeQuartz; // Fallback na themeQuartz
99
+ }, [theme]);
100
+ const [, setIsGridReady] = React.useState(false);
101
+ const isSelectingRef = React.useRef(false); // ✅ FIX: Změna ze state na ref pro eliminaci rerenderů
102
+ const aggregationDataRef = React.useRef(null); // // Pro simple mode status bar
103
+ const activeNotificationModeRef = React.useRef(notificationMode); // // Aktivní mód (může být dočasně přepsán uživatelem)
104
+ const previousRowDataRef = React.useRef(rowData);
105
+
106
+ // Synchronizovat activeNotificationModeRef s notificationMode prop
107
+ React.useEffect(() => {
108
+ activeNotificationModeRef.current = notificationMode;
109
+ }, [notificationMode]);
110
+
111
+ // Bulk Edit hook
112
+ const {
113
+ floatingButton,
114
+ editPopover,
115
+ handleRangeChange,
116
+ handleOpenPopover,
117
+ handleSubmitEdit,
118
+ handleCancelEdit,
119
+ handleValueChange,
120
+ } = useBulkCellEdit(internalRef, {
121
+ enabled: enableBulkEdit,
122
+ accessToken: bulkEditAccessToken,
123
+ onBulkEditStart,
124
+ onBulkEditComplete,
125
+ ...bulkEditOptions,
126
+ });
127
+
128
+
129
+ // ========== PERFORMANCE OPTIMIZATION: Shallow comparison helper ==========
130
+ // Shallow porovnání objektů - 100x rychlejší než JSON.stringify
131
+ const shallowEqual = React.useCallback((obj1, obj2) => {
132
+ if (obj1 === obj2) return true;
133
+ if (!obj1 || !obj2) return false;
134
+
135
+ const keys1 = Object.keys(obj1);
136
+ const keys2 = Object.keys(obj2);
137
+
138
+ if (keys1.length !== keys2.length) return false;
139
+
140
+ for (let key of keys1) {
141
+ if (obj1[key] !== obj2[key]) return false;
142
+ }
143
+
144
+ return true;
145
+ }, []);
146
+
147
+ // Memoizované funkce pro detekci změn v rowData
148
+ const findNewRows = React.useCallback((oldData, newData) => {
149
+ const oldIds = new Set(oldData.map((row) => row.id));
150
+ return newData.filter((row) => !oldIds.has(row.id));
151
+ }, []);
152
+
153
+ const findUpdatedRows = React.useCallback((oldData, newData) => {
154
+ const oldDataMap = new Map(oldData.map((row) => [row.id, row]));
155
+ return newData.filter((newRow) => {
156
+ const oldRow = oldDataMap.get(newRow.id);
157
+ if (!oldRow) return false; // Nový řádek, ne aktualizovaný
158
+
159
+ // ✅ OPTIMALIZACE: Shallow comparison místo JSON.stringify (100x rychlejší!)
160
+ return !shallowEqual(oldRow, newRow);
161
+ });
162
+ }, [shallowEqual]);
163
+
164
+ React.useImperativeHandle(ref, () => internalRef.current, [internalRef]);
165
+
166
+ // Throttle timer pro notifikace - zobrazuje se BĚHEM označování s 100ms throttle
167
+ const notificationThrottleRef = React.useRef(null);
168
+ const notificationLastCallRef = React.useRef(0);
169
+ const lastRangeHashRef = React.useRef(null);
170
+
171
+ // ✅ Callback pro přepnutí z full na simple mód
172
+ const handleSwitchToSimple = React.useCallback(() => {
173
+ const gridId = props.gridName || props.id || 'default';
174
+ const key = `aggregation-grid-${gridId}`;
175
+
176
+ // Zavřít full notifikaci
177
+ notification.destroy(key);
178
+
179
+ // Znovu spočítat a zobrazit simple status bar
180
+ if (internalRef?.current?.api) {
181
+ const ranges = internalRef.current.api.getCellRanges();
182
+ if (ranges && ranges.length > 0 && ranges[0]?.startRow) {
183
+ const messageInfo = Aggregations(internalRef);
184
+ if (messageInfo.count > 1) {
185
+ aggregationDataRef.current = messageInfo;
186
+ }
187
+ }
188
+ }
189
+
190
+ activeNotificationModeRef.current = 'simple';
191
+ }, [props, internalRef]);
192
+
193
+ // ✅ Helper funkce pro vytvoření custom description s tlačítkem pro přepnutí na simple
194
+ const createNotificationDescription = React.useCallback((messageInfo, showSwitchButton = false) => {
195
+ const bodyContent = notificationBody(messageInfo);
196
+
197
+ if (!showSwitchButton) {
198
+ return bodyContent;
199
+ }
200
+
201
+ return (
202
+ <div>
203
+ {bodyContent}
204
+ <div style={{ marginTop: '12px', borderTop: '1px solid #f0f0f0', paddingTop: '8px' }}>
205
+ <Button
206
+ type="link"
207
+ size="small"
208
+ icon={<CompressOutlined />}
209
+ onClick={handleSwitchToSimple}
210
+ style={{ padding: 0 }}
211
+ >
212
+ Zobrazit kompaktní režim
213
+ </Button>
214
+ </div>
215
+ </div>
216
+ );
217
+ }, [notificationBody, handleSwitchToSimple]);
218
+
219
+ // ✅ Callback pro přepnutí z simple na full mód
220
+ const handleSwitchToFull = React.useCallback(() => {
221
+ if (!aggregationDataRef.current) return;
222
+
223
+ const gridId = props.gridName || props.id || 'default';
224
+ const key = `aggregation-grid-${gridId}`;
225
+
226
+ // Zobrazit full notifikaci s aktuálními daty a tlačítkem pro přepnutí
227
+ notification.info({
228
+ key,
229
+ message: notificationHead(aggregationDataRef.current),
230
+ description: createNotificationDescription(aggregationDataRef.current, true),
231
+ duration: 0,
232
+ style,
233
+ placement,
234
+ });
235
+
236
+ // Skrýt simple status bar
237
+ aggregationDataRef.current = null;
238
+ activeNotificationModeRef.current = 'full';
239
+ }, [ props, notificationHead, createNotificationDescription, style, placement]);
240
+
241
+ // ========== PERFORMANCE FIX: Stabilní refs pro updateAggregationNotification ==========
242
+ // ✅ FIX: isSelectingRef je už deklarovaný na řádku 107 jako React.useRef(false)
243
+ const notificationHeadRef = React.useRef(notificationHead);
244
+ const createNotificationDescriptionRef = React.useRef(createNotificationDescription);
245
+ const styleRef = React.useRef(style);
246
+ const placementRef = React.useRef(placement);
247
+ const propsRef = React.useRef(props);
248
+
249
+
250
+ // ✅ FIX: Odstraněn useEffect pro isSelecting - isSelectingRef je nyní řízený přímo v updatePointerEventsForSelecting
251
+
252
+ React.useEffect(() => {
253
+ notificationHeadRef.current = notificationHead;
254
+ }, [notificationHead]);
255
+
256
+ React.useEffect(() => {
257
+ createNotificationDescriptionRef.current = createNotificationDescription;
258
+ }, [createNotificationDescription]);
259
+
260
+ React.useEffect(() => {
261
+ styleRef.current = style;
262
+ }, [style]);
263
+
264
+ React.useEffect(() => {
265
+ placementRef.current = placement;
266
+ }, [placement]);
267
+
268
+ React.useEffect(() => {
269
+ propsRef.current = props;
270
+ }, [props]);
271
+
272
+ // ✅ OPTIMALIZACE: Helper funkce pro aktualizaci aggregace notifikace s cache check
273
+ // Volá se BĚHEM označování s 100ms throttle pro real-time feedback
274
+ // STABILNÍ callback - používá pouze refs!
275
+ const updateAggregationNotification = React.useCallback((event) => {
276
+ // Pokud je notificationMode 'none', nedělat nic
277
+ if (notificationModeRef.current === 'none') {
278
+ return;
279
+ }
280
+
281
+ // Lightweight cache check PŘED voláním Aggregations()
282
+ const ranges = event.api.getCellRanges();
283
+ const currentRangeHash = hashRanges(ranges);
284
+
285
+ // Žádné ranges nebo jen jedna buňka - vyčistit vše
286
+ if (!ranges || ranges.length === 0 || !ranges[0]?.startRow) {
287
+ lastRangeHashRef.current = null;
288
+
289
+ // V 'full' módu zavřít notifikaci
290
+ if (activeNotificationModeRef.current === 'full') {
291
+ const gridId = propsRef.current.gridName || propsRef.current.id || 'default';
292
+ const key = `aggregation-grid-${gridId}`;
293
+ notification.destroy(key);
294
+ }
295
+
296
+ // V 'simple' módu vyčistit aggregationData
297
+ if (activeNotificationModeRef.current === 'simple') {
298
+ aggregationDataRef.current = null;
299
+ }
300
+
301
+ return;
302
+ }
303
+
304
+ // Cache hit - ranges se nezměnily, skip
305
+ if (currentRangeHash === lastRangeHashRef.current) {
306
+ return;
307
+ }
308
+
309
+ // Cache miss - spočítat aggregace
310
+ const messageInfo = Aggregations(internalRef);
311
+
312
+ if (messageInfo.count > 1) {
313
+ // Uložit hash pro příští porovnání
314
+ lastRangeHashRef.current = currentRangeHash;
315
+
316
+ // Zavolat onAggregationChanged callback pokud existuje
317
+ if (propsRef.current.onAggregationChanged) {
318
+ propsRef.current.onAggregationChanged(messageInfo);
319
+ }
320
+
321
+ // Podle aktivního módu zobrazit buď notifikaci nebo aktualizovat status bar
322
+ if (activeNotificationModeRef.current === 'full') {
323
+ // FULL mód - zobrazit plovoucí notifikaci s tlačítkem pro přepnutí na simple
324
+ const gridId = propsRef.current.gridName || propsRef.current.id || 'default';
325
+ const key = `aggregation-grid-${gridId}`;
326
+
327
+ const dynamicStyle = {
328
+ ...styleRef.current,
329
+ pointerEvents: isSelectingRef.current ? 'none' : 'auto'
330
+ };
331
+
332
+ notification.info({
333
+ key,
334
+ message: notificationHeadRef.current(messageInfo),
335
+ description: createNotificationDescriptionRef.current(messageInfo, true),
336
+ duration: 0,
337
+ style: dynamicStyle,
338
+ placement: placementRef.current,
339
+ });
340
+ } else if (activeNotificationModeRef.current === 'simple') {
341
+ // SIMPLE mód - aktualizovat state pro status bar
342
+ aggregationDataRef.current = messageInfo;
343
+ }
344
+ } else {
345
+ // Jen jedna buňka - zavřít/vyčistit vše
346
+ lastRangeHashRef.current = null;
347
+
348
+ if (activeNotificationModeRef.current === 'full') {
349
+ const gridId = propsRef.current.gridName || propsRef.current.id || 'default';
350
+ const key = `aggregation-grid-${gridId}`;
351
+ notification.destroy(key);
352
+ }
353
+
354
+ if (activeNotificationModeRef.current === 'simple') {
355
+ aggregationDataRef.current = null;
356
+ }
357
+ }
358
+ }, [internalRef]); // ✅ Pouze internalRef v dependencies!
359
+
360
+ // Helper funkce pro nastavení pointer-events na notifikace
361
+ const setNotificationPointerEvents = React.useCallback((enable) => {
362
+ const notifications = document.querySelectorAll('.ant-notification');
363
+
364
+ notifications.forEach((notif) => {
365
+ if (enable) {
366
+ // Obnovit pointer-events na notifikaci
367
+ const original = notif.dataset.originalPointerEvents || 'auto';
368
+ notif.style.pointerEvents = original;
369
+ notif.style.removeProperty('pointer-events');
370
+ delete notif.dataset.originalPointerEvents;
371
+
372
+ // Obnovit pointer-events na všechny child elementy
373
+ const allChildren = notif.querySelectorAll('*');
374
+ allChildren.forEach((child) => {
375
+ child.style.removeProperty('pointer-events');
376
+ delete child.dataset.originalPointerEvents;
377
+ });
378
+ } else {
379
+ // Zakázat pointer-events na notifikaci
380
+ if (!notif.dataset.originalPointerEvents) {
381
+ notif.dataset.originalPointerEvents = notif.style.pointerEvents || 'auto';
382
+ }
383
+ notif.style.setProperty('pointer-events', 'none', 'important');
384
+
385
+ // Zakázat pointer-events na všechny child elementy
386
+ const allChildren = notif.querySelectorAll('*');
387
+ allChildren.forEach((child) => {
388
+ if (!child.dataset.originalPointerEvents) {
389
+ child.dataset.originalPointerEvents = child.style.pointerEvents || '';
390
+ }
391
+ child.style.setProperty('pointer-events', 'none', 'important');
392
+ });
393
+ }
394
+ });
395
+ }, []);
396
+
397
+ // ✅ FIX: Helper funkce pro update isSelecting bez state změny (eliminuje rerender)
398
+ const updatePointerEventsForSelecting = React.useCallback((selecting) => {
399
+ isSelectingRef.current = selecting;
400
+
401
+ if (selecting) {
402
+ document.body.classList.add('ag-grid-selecting');
403
+ setNotificationPointerEvents(false);
404
+
405
+ // KRITICKÉ: Sledovat DOM změny - když se objeví nová notifikace během označování
406
+ const observer = new MutationObserver((mutations) => {
407
+ mutations.forEach((mutation) => {
408
+ mutation.addedNodes.forEach((node) => {
409
+ if (node.nodeType === 1) {
410
+ if (node.classList?.contains('ant-notification') ||
411
+ node.querySelector?.('.ant-notification')) {
412
+ setNotificationPointerEvents(false);
413
+ }
414
+ }
415
+ });
416
+ });
417
+ });
418
+
419
+ observer.observe(document.body, {
420
+ childList: true,
421
+ subtree: true
422
+ });
423
+
424
+ // Uložit observer do ref pro případný cleanup
425
+ if (!updatePointerEventsForSelecting.observer) {
426
+ updatePointerEventsForSelecting.observer = observer;
427
+ }
428
+ } else {
429
+ document.body.classList.remove('ag-grid-selecting');
430
+
431
+ // Cleanup observer
432
+ if (updatePointerEventsForSelecting.observer) {
433
+ updatePointerEventsForSelecting.observer.disconnect();
434
+ updatePointerEventsForSelecting.observer = null;
435
+ }
436
+
437
+ setTimeout(() => setNotificationPointerEvents(true), 100);
438
+ }
439
+ }, [setNotificationPointerEvents]);
440
+
441
+ // Detekce konce označování pomocí mouseup event
442
+ React.useEffect(() => {
443
+ const handleMouseUp = () => {
444
+ // ✅ FIX: Ukončit isSelecting pomocí ref (bez state update → žádný rerender)
445
+ setTimeout(() => {
446
+ updatePointerEventsForSelecting(false);
447
+
448
+ // ✅ FIX: Zkontrolovat jestli jsou stále nějaké ranges, pokud ne - vyčistit status bar
449
+ if (internalRef?.current?.api && activeNotificationModeRef.current === 'simple') {
450
+ const ranges = internalRef.current.api.getCellRanges();
451
+ if (!ranges || ranges.length === 0 || !ranges[0]?.startRow) {
452
+ aggregationDataRef.current = null;
453
+ // Reset na původní notificationMode když jsou všechny buňky odznačeny
454
+ activeNotificationModeRef.current = notificationMode;
455
+ }
456
+ }
457
+ }, 50);
458
+ };
459
+
460
+ document.addEventListener('mouseup', handleMouseUp);
461
+ document.addEventListener('touchend', handleMouseUp); // Pro touch zařízení
462
+
463
+ return () => {
464
+ document.removeEventListener('mouseup', handleMouseUp);
465
+ document.removeEventListener('touchend', handleMouseUp);
466
+ };
467
+ }, [notificationMode, internalRef, updatePointerEventsForSelecting]);
468
+
469
+ // Cleanup notifikací a timerů při zničení komponenty
470
+ React.useEffect(() => {
471
+ return () => {
472
+ // Clear throttle timer
473
+ if (notificationThrottleRef.current) {
474
+ clearTimeout(notificationThrottleRef.current);
475
+ }
476
+
477
+ if (notificationMode === 'full') {
478
+ const gridId = props.gridName || props.id || 'default';
479
+ const key = `aggregation-grid-${gridId}`;
480
+ notification.destroy(key);
481
+ }
482
+
483
+ // Cleanup SignalR transaction callback při unmount
484
+ if (queryKey && window.agGridTransactionCallbacks) {
485
+ delete window.agGridTransactionCallbacks[queryKey];
486
+ }
487
+ };
488
+ }, [notificationMode, props.gridName, props.id, queryKey]);
489
+
490
+ React.useEffect(() => {
491
+ // VYPNUTO: Cell flash je globálně vypnutý
492
+ return;
493
+
494
+ if (!newRowFlash && !updatedRowFlash) return;
495
+
496
+ const previousRowData = previousRowDataRef.current;
497
+ const addedRows = newRowFlash ? findNewRows(previousRowData, rowData) : [];
498
+ const updatedRows = updatedRowFlash ? findUpdatedRows(previousRowData, rowData) : [];
499
+
500
+ if (addedRows.length > 0 || updatedRows.length > 0) {
501
+ setTimeout(() => {
502
+ try {
503
+ // Bezpečnostní kontrola API dostupnosti
504
+ if (!internalRef.current?.api || internalRef.current.api.isDestroyed?.()) {
505
+ return;
506
+ }
507
+
508
+ // Modern AG-Grid (33+): getColumnState() moved to main api
509
+ const columnApi = internalRef.current?.columnApi || internalRef.current?.api;
510
+ if (!columnApi?.getColumnState) {
511
+ return;
512
+ }
513
+
514
+ const columnStates = columnApi.getColumnState();
515
+ const allColumns = columnStates
516
+ .filter(colState => colState.colId) // Filtrujeme pouze platné colId
517
+ .map((colState) => colState.colId);
518
+
519
+ // Kontrola, že máme platné sloupce
520
+ if (allColumns.length === 0) {
521
+ return;
522
+ }
523
+
524
+ // Flash efekt pro nové řádky (používá defaultní zelená barva AG Grid)
525
+ if (addedRows.length > 0) {
526
+ const newRowNodes = [];
527
+ internalRef.current.api.forEachNode((node) => {
528
+ if (addedRows.some((row) => row.id === node.data.id)) {
529
+ newRowNodes.push(node);
530
+ }
531
+ });
532
+
533
+ if (newRowNodes.length > 0) {
534
+ internalRef.current.api.flashCells({
535
+ rowNodes: newRowNodes,
536
+ columns: allColumns,
537
+ flashDelay: 0,
538
+ fadeDelay: 1000,
539
+ });
540
+ }
541
+ }
542
+
543
+ // Flash efekt pro aktualizované řádky (modrá barva)
544
+ if (updatedRows.length > 0) {
545
+ const updatedRowNodes = [];
546
+ internalRef.current.api.forEachNode((node) => {
547
+ if (updatedRows.some((row) => row.id === node.data.id)) {
548
+ updatedRowNodes.push(node);
549
+ }
550
+ });
551
+
552
+ if (updatedRowNodes.length > 0) {
553
+ // Použijeme vlastní CSS animaci pro modrou flash
554
+ updatedRowNodes.forEach(node => {
555
+ const rowElement = node.eGridRow || document.querySelector(`[row-id="${node.data.id}"]`);
556
+ if (rowElement) {
557
+ rowElement.classList.add('ag-row-flash-updated');
558
+ setTimeout(() => {
559
+ rowElement.classList.remove('ag-row-flash-updated');
560
+ }, 1000);
561
+ }
562
+ });
563
+ }
564
+ }
565
+ } catch (error) {
566
+ // Ignorujeme chybu a pokračujeme bez crash aplikace
567
+ }
568
+ }, 100); // Zvýšený timeout pro stabilizaci gridu
569
+ }
570
+
571
+ previousRowDataRef.current = rowData;
572
+ }, [rowData, newRowFlash, updatedRowFlash]);
573
+
574
+ // ========== PERFORMANCE FIX: Stabilní ref pattern pro callbacks ==========
575
+ // Refs pro aktuální hodnoty - zabraňuje re-creation RhPlusRangeSelectionChanged při změně stavu
576
+ const notificationModeRef = React.useRef(notificationMode);
577
+ const enableBulkEditRef = React.useRef(enableBulkEdit);
578
+ const handleRangeChangeRef = React.useRef(handleRangeChange);
579
+ const onRangeSelectionChangedRef = React.useRef(props.onRangeSelectionChanged);
580
+ const updateAggregationNotificationRef = React.useRef(updateAggregationNotification);
581
+
582
+ // ✅ FIX #12: Refs pro grid layout handlery (eliminuje rerendery při změně props)
583
+ const onColumnMovedRef = React.useRef(props.onColumnMoved);
584
+ const onDragStoppedRef = React.useRef(props.onDragStopped);
585
+ const onColumnVisibleRef = React.useRef(props.onColumnVisible);
586
+ const onColumnPinnedRef = React.useRef(props.onColumnPinned);
587
+ const onColumnResizedRef = React.useRef(props.onColumnResized);
588
+
589
+ // Aktualizovat refs při změně hodnot
590
+ React.useEffect(() => {
591
+ notificationModeRef.current = notificationMode;
592
+ }, [notificationMode]);
593
+
594
+ React.useEffect(() => {
595
+ enableBulkEditRef.current = enableBulkEdit;
596
+ }, [enableBulkEdit]);
597
+
598
+ React.useEffect(() => {
599
+ handleRangeChangeRef.current = handleRangeChange;
600
+ }, [handleRangeChange]);
601
+
602
+ React.useEffect(() => {
603
+ onRangeSelectionChangedRef.current = props.onRangeSelectionChanged;
604
+ }, [props.onRangeSelectionChanged]);
605
+
606
+ React.useEffect(() => {
607
+ updateAggregationNotificationRef.current = updateAggregationNotification;
608
+ }, [updateAggregationNotification]);
609
+
610
+ // ✅ FIX #12: Aktualizovat grid layout handler refs
611
+ React.useEffect(() => {
612
+ onColumnMovedRef.current = props.onColumnMoved;
613
+ }, [props.onColumnMoved]);
614
+
615
+ React.useEffect(() => {
616
+ onDragStoppedRef.current = props.onDragStopped;
617
+ }, [props.onDragStopped]);
618
+
619
+ React.useEffect(() => {
620
+ onColumnVisibleRef.current = props.onColumnVisible;
621
+ }, [props.onColumnVisible]);
622
+
623
+ React.useEffect(() => {
624
+ onColumnPinnedRef.current = props.onColumnPinned;
625
+ }, [props.onColumnPinned]);
626
+
627
+ React.useEffect(() => {
628
+ onColumnResizedRef.current = props.onColumnResized;
629
+ }, [props.onColumnResized]);
630
+
631
+ // Stabilní callback s prázdnými dependencies - používá pouze refs
632
+ const RhPlusRangeSelectionChanged = React.useCallback(
633
+ (event) => {
634
+ // ✅ FIX: Detekovat začátek označování pomocí ref (bez state update → žádný rerender)
635
+ updatePointerEventsForSelecting(true);
636
+
637
+ // 1. ✅ OPTIMALIZACE: Zobrazení notifikace BĚHEM označování s THROTTLE
638
+ // Simple mode: 300ms throttle (méně častá aktualizace, lepší výkon)
639
+ // Full mode: 100ms throttle (rychlejší feedback, plovoucí notifikace je levná)
640
+ if (notificationModeRef.current !== 'none') {
641
+ const throttleInterval = notificationModeRef.current === 'simple' ? 300 : 100;
642
+ const now = Date.now();
643
+ const timeSinceLastCall = now - notificationLastCallRef.current;
644
+
645
+ // První volání NEBO uplynul throttle interval
646
+ if (timeSinceLastCall >= throttleInterval) {
647
+ // Okamžité volání
648
+ notificationLastCallRef.current = now;
649
+ if (internalRef?.current) {
650
+ updateAggregationNotificationRef.current(event);
651
+ }
652
+ } else {
653
+ // Naplánovat volání za zbývající čas (trailing edge)
654
+ if (notificationThrottleRef.current) {
655
+ clearTimeout(notificationThrottleRef.current);
656
+ }
657
+
658
+ notificationThrottleRef.current = setTimeout(() => {
659
+ notificationLastCallRef.current = Date.now();
660
+ if (internalRef?.current) {
661
+ updateAggregationNotificationRef.current(event);
662
+ }
663
+ }, throttleInterval - timeSinceLastCall);
664
+ }
665
+ }
666
+
667
+ // 2. ✅ OPTIMALIZACE: Bulk edit handler BEZ debounce - okamžité zobrazení ikony
668
+ // Lightweight validace v useBulkCellEdit zajistí okamžité zobrazení
669
+ // Těžká validace proběhne s 15ms debounce uvnitř useBulkCellEdit
670
+ if (enableBulkEditRef.current) {
671
+ handleRangeChangeRef.current(event);
672
+ }
673
+
674
+ // 3. Custom onRangeSelectionChanged callback - bez debounce
675
+ if (onRangeSelectionChangedRef.current) {
676
+ onRangeSelectionChangedRef.current(event);
677
+ }
678
+ },
679
+ [] // ✅ PRÁZDNÉ dependencies - stabilní reference!
680
+ );
681
+
682
+ // ✅ FIX #12: Stabilní wrappery pro grid layout handlery (prázdné dependencies)
683
+ const stableOnColumnMoved = React.useCallback(
684
+ (params) => {
685
+ if (onColumnMovedRef.current) {
686
+ onColumnMovedRef.current(params);
687
+ }
688
+ },
689
+ []
690
+ );
691
+
692
+ const stableOnDragStopped = React.useCallback(
693
+ (params) => {
694
+ if (onDragStoppedRef.current) {
695
+ onDragStoppedRef.current(params);
696
+ }
697
+ },
698
+ []
699
+ );
700
+
701
+ const stableOnColumnVisible = React.useCallback(
702
+ (params) => {
703
+ if (onColumnVisibleRef.current) {
704
+ onColumnVisibleRef.current(params);
705
+ }
706
+ },
707
+ []
708
+ );
709
+
710
+ const stableOnColumnPinned = React.useCallback(
711
+ (params) => {
712
+ if (onColumnPinnedRef.current) {
713
+ onColumnPinnedRef.current(params);
714
+ }
715
+ },
716
+ []
717
+ );
718
+
719
+ const stableOnColumnResized = React.useCallback(
720
+ (params) => {
721
+ if (onColumnResizedRef.current) {
722
+ onColumnResizedRef.current(params);
723
+ }
724
+ },
725
+ []
726
+ );
727
+
728
+ const AgGridOnGridReady = (event, options) => {
729
+ if (onGridReady) {
730
+ onGridReady(event, options);
731
+ }
732
+
733
+ // Nejprve nastavíme API reference pro interní použití
734
+ if (internalRef.current) {
735
+ internalRef.current.api = event.api;
736
+ internalRef.current.columnApi = event.columnApi || event.api; // fallback for modern AG-Grid
737
+ }
738
+
739
+ // Nastavíme API ready flag pro quick filter useEffect
740
+ setIsApiReady(true);
741
+
742
+ // Registruj callback pro AG Grid transactions (SignalR optimalizace)
743
+ // Inicializace globálního registru pro více AG-Grid instancí
744
+ if (!window.agGridTransactionCallbacks) {
745
+ window.agGridTransactionCallbacks = {};
746
+ }
747
+
748
+ // Registrace callbacku pro tuto konkrétní AG-Grid instanci (podle queryKey)
749
+ if (event.api && queryKey) {
750
+ window.agGridTransactionCallbacks[queryKey] = (transactionData) => {
751
+ try {
752
+ if (!event.api || event.api.isDestroyed?.()) return;
753
+
754
+ const { operation, records } = transactionData;
755
+
756
+ switch (operation) {
757
+ case 'add':
758
+ event.api.applyTransaction({ add: records });
759
+ // Flash efekt pro nové řádky
760
+ setTimeout(() => {
761
+ records.forEach(record => {
762
+ const rowNode = event.api.getRowNode(record.id);
763
+ if (rowNode && rowNode.rowElement) {
764
+ rowNode.rowElement.classList.add('ag-row-flash-created');
765
+ setTimeout(() => {
766
+ rowNode.rowElement.classList.remove('ag-row-flash-created');
767
+ }, 1000);
768
+ }
769
+ });
770
+ }, 100);
771
+ break;
772
+
773
+ case 'update':
774
+ event.api.applyTransaction({ update: records });
775
+ // Flash efekt pro aktualizované řádky
776
+ setTimeout(() => {
777
+ records.forEach(record => {
778
+ const rowNode = event.api.getRowNode(record.id);
779
+ if (rowNode && rowNode.rowElement) {
780
+ rowNode.rowElement.classList.add('ag-row-flash-updated');
781
+ setTimeout(() => {
782
+ rowNode.rowElement.classList.remove('ag-row-flash-updated');
783
+ }, 1000);
784
+ }
785
+ });
786
+ }, 100);
787
+ break;
788
+
789
+ case 'remove':
790
+ // Flash efekt před smazáním
791
+ records.forEach(record => {
792
+ const rowNode = event.api.getRowNode(record.id);
793
+ if (rowNode && rowNode.rowElement) {
794
+ rowNode.rowElement.classList.add('ag-row-flash-deleted');
795
+ }
796
+ });
797
+
798
+ setTimeout(() => {
799
+ event.api.applyTransaction({ remove: records });
800
+ }, 500); // Krátké zpoždění pro zobrazení flash efektu
801
+ break;
802
+ }
803
+ } catch (error) {
804
+ // Ignorujeme chyby
805
+ }
806
+ };
807
+ }
808
+
809
+ // Pak zavoláme parent onGridReady handler, pokud existuje
810
+ // Toto je kritické pro správné fungování bit/ui/grid a GridLayout
811
+ if (options.onGridReady) {
812
+ try {
813
+ options.onGridReady(event);
814
+ } catch (error) {
815
+ // Error handling without console output
816
+ }
817
+ }
818
+
819
+ // Nakonec nastavíme grid ready state s timeout
820
+ setTimeout(() => {
821
+ setIsGridReady(true);
822
+ }, 1000);
823
+ };
824
+
825
+ const components = React.useMemo(() => {
826
+ return {
827
+ checkboxRenderer: CheckboxRenderer,
828
+ selectRenderer: SelectRenderer,
829
+ countrySelectRenderer: CountrySelectRenderer,
830
+ booleanRenderer: BooleanRenderer,
831
+ buttonRenderer: ButtonRenderer,
832
+ iconRenderer: IconRenderer,
833
+ imageRenderer: ImageRenderer,
834
+ stateRenderer: StateRenderer,
835
+ objectRenderer: ObjectRenderer,
836
+ linkRenderer: LinkRenderer,
837
+ ...props.frameworkComponents,
838
+ };
839
+ }, [props.frameworkComponents]);
840
+
841
+ // ========== PERFORMANCE OPTIMIZATION: Memoizované event handlers ==========
842
+ const memoizedOnCellEditingStarted = React.useCallback(
843
+ (event) => RhPlusOnCellEditingStarted(event, props),
844
+ [props.onCellEditingStarted]
845
+ );
846
+
847
+ const memoizedOnCellDoubleClicked = React.useCallback(
848
+ (event) => RhPlusOnCellDoubleClicked(event, props),
849
+ [props.onCellDoubleClicked]
850
+ );
851
+
852
+ const memoizedOnCellValueChanged = React.useCallback(
853
+ (event) => RhPlusOnCellValueChanged(event, props),
854
+ [props.onCellValueChanged]
855
+ );
856
+
857
+ const memoizedPostSort = React.useCallback(
858
+ (event) => AgGridPostSort(event, props),
859
+ [props.postSort]
860
+ );
861
+
862
+ const memoizedOnGridReady = React.useCallback(
863
+ (event, options) => AgGridOnGridReady(event, props),
864
+ [onGridReady]
865
+ );
866
+
867
+ const memoizedOnRowDataChanged = React.useCallback(
868
+ (event) => AgGridOnRowDataChanged(event, props),
869
+ [props.onRowDataChanged]
870
+ );
871
+
872
+ const memoizedOnRowDataUpdated = React.useCallback(
873
+ (event) => AgGridOnRowDataUpdated(event, props),
874
+ [props.onRowDataUpdated]
875
+ );
876
+
877
+ // Memoizovaný context object
878
+ // ✅ OPTIMALIZACE: Použít pouze relevantní props pro context - neměnit při změně props
879
+ const memoizedContext = React.useMemo(() => ({
880
+ componentParent: props
881
+ }), [props.context, props.gridName, props.id]);
882
+
883
+ // Memoizovaný defaultColDef
884
+ const memoizedDefaultColDef = React.useMemo(() => ({
885
+ ...props.defaultColDef,
886
+ suppressHeaderMenuButton: true,
887
+ suppressHeaderFilterButton: true,
888
+ suppressMenu: true,
889
+ // ✅ FIX AG-Grid v35: Bezpečné zpracování null hodnot v Quick Filter
890
+ // AG-Grid v35 změnil implementaci Quick Filter - nyní volá .toString() na hodnotách buněk bez null check
891
+ // Tento callback zajistí že nikdy nedojde k chybě "Cannot read properties of null (reading 'toString')"
892
+ getQuickFilterText: (params) => {
893
+ const value = params.value;
894
+ // Null/undefined → prázdný string (bez chyby)
895
+ if (value == null) return '';
896
+ // Objekty a pole → JSON string
897
+ if (typeof value === 'object') {
898
+ try {
899
+ return JSON.stringify(value);
900
+ } catch {
901
+ // Cirkulární reference nebo jiná chyba při serializaci → prázdný string
902
+ return '';
903
+ }
904
+ }
905
+ // Primitivní typy → toString
906
+ return value.toString();
907
+ },
908
+ }), [props.defaultColDef]);
909
+
910
+ // ========== PERFORMANCE FIX: Memoizovat columnDefs ==========
911
+ // AgGridColumns vrací nový array při každém volání, i když jsou vstupy stejné
912
+ const memoizedColumnDefs = React.useMemo(() => {
913
+ return AgGridColumns(props.columnDefs, props);
914
+ }, [props.columnDefs, props.getRowId, props.frameworkComponents]);
915
+
916
+ // ========== CRITICAL FIX: Stabilní allGridProps objekt pomocí ref ==========
917
+ // Problém: Jakékoli použití useMemo vytváří nový objekt při změně dependencies
918
+ // Řešení: Použít ref pro VŽDY stejný objekt
919
+ // AG-Grid САМО detekuje změny rowData a columnDefs → NENÍ potřeba forceUpdate!
920
+
921
+ const allGridPropsRef = React.useRef(null);
922
+
923
+ // Inicializace objektu
924
+ if (!allGridPropsRef.current) {
925
+ allGridPropsRef.current = {};
926
+ }
927
+
928
+ // ✅ Aktualizovat props v EXISTUJÍCÍM objektu (mutace)
929
+ // AG Grid interně detekuje změny props, nepotřebuje nový objekt
930
+ allGridPropsRef.current.ref = internalRef;
931
+ allGridPropsRef.current.rowData = props.rowData;
932
+ allGridPropsRef.current.getRowId = props.getRowId;
933
+ allGridPropsRef.current.theme = themeObject;
934
+ allGridPropsRef.current.columnDefs = memoizedColumnDefs;
935
+ allGridPropsRef.current.defaultColDef = memoizedDefaultColDef;
936
+ allGridPropsRef.current.onCellEditingStarted = memoizedOnCellEditingStarted;
937
+ allGridPropsRef.current.onCellDoubleClicked = memoizedOnCellDoubleClicked;
938
+ // allGridPropsRef.current.onCellValueChanged = memoizedOnCellValueChanged;
939
+ allGridPropsRef.current.postSort = memoizedPostSort;
940
+ allGridPropsRef.current.onGridReady = memoizedOnGridReady;
941
+ allGridPropsRef.current.onRowDataChanged = memoizedOnRowDataChanged;
942
+ allGridPropsRef.current.onRowDataUpdated = memoizedOnRowDataUpdated;
943
+ allGridPropsRef.current.onRangeSelectionChanged = RhPlusRangeSelectionChanged;
944
+ allGridPropsRef.current.context = memoizedContext;
945
+ allGridPropsRef.current.components = components;
946
+
947
+ // Další AG Grid props
948
+ allGridPropsRef.current.rowModelType = props.rowModelType;
949
+ allGridPropsRef.current.rowSelection = props.rowSelection;
950
+ allGridPropsRef.current.enableRangeSelection = props.enableRangeSelection;
951
+ allGridPropsRef.current.enableRangeHandle = props.enableRangeHandle;
952
+ allGridPropsRef.current.enableFillHandle = props.enableFillHandle;
953
+ allGridPropsRef.current.suppressRowClickSelection = props.suppressRowClickSelection;
954
+ allGridPropsRef.current.singleClickEdit = props.singleClickEdit;
955
+ allGridPropsRef.current.stopEditingWhenCellsLoseFocus = props.stopEditingWhenCellsLoseFocus;
956
+ allGridPropsRef.current.rowClass = props.rowClass;
957
+ allGridPropsRef.current.rowStyle = props.rowStyle;
958
+ allGridPropsRef.current.getRowClass = props.getRowClass;
959
+ allGridPropsRef.current.getRowStyle = props.getRowStyle;
960
+ allGridPropsRef.current.animateRows = props.animateRows;
961
+ allGridPropsRef.current.suppressCellFocus = props.suppressCellFocus;
962
+ allGridPropsRef.current.suppressMenuHide = props.suppressMenuHide;
963
+ allGridPropsRef.current.enableCellTextSelection = props.enableCellTextSelection;
964
+ allGridPropsRef.current.ensureDomOrder = props.ensureDomOrder;
965
+ allGridPropsRef.current.suppressRowTransform = props.suppressRowTransform;
966
+ allGridPropsRef.current.suppressColumnVirtualisation = props.suppressColumnVirtualisation;
967
+ allGridPropsRef.current.suppressRowVirtualisation = props.suppressRowVirtualisation;
968
+ allGridPropsRef.current.tooltipShowDelay = props.tooltipShowDelay;
969
+ allGridPropsRef.current.tooltipHideDelay = props.tooltipHideDelay;
970
+ allGridPropsRef.current.tooltipMouseTrack = props.tooltipMouseTrack;
971
+ allGridPropsRef.current.gridId = props.gridId;
972
+ allGridPropsRef.current.id = props.id;
973
+ allGridPropsRef.current.gridName = props.gridName;
974
+ allGridPropsRef.current.getContextMenuItems = props.getContextMenuItems;
975
+
976
+ // Grid Layout event handlers - stabilní wrappery (eliminuje rerendery)
977
+ // ✅ FIX #12: Používáme stabilní wrappery místo props → eliminuje rerendery při změně props
978
+ allGridPropsRef.current.onColumnMoved = stableOnColumnMoved;
979
+ allGridPropsRef.current.onDragStopped = stableOnDragStopped;
980
+ allGridPropsRef.current.onColumnVisible = stableOnColumnVisible;
981
+ allGridPropsRef.current.onColumnPinned = stableOnColumnPinned;
982
+ allGridPropsRef.current.onColumnResized = stableOnColumnResized;
983
+
984
+ // ✅ gridOptions support - spread additional AG-Grid props
985
+ // Přidává POUZE hodnoty které ještě NEJSOU nastaveny výše (nejnižší priorita)
986
+ if (props.gridOptions && typeof props.gridOptions === 'object') {
987
+ for (const key in props.gridOptions) {
988
+ if (props.gridOptions[key] !== undefined && allGridPropsRef.current[key] === undefined) {
989
+ allGridPropsRef.current[key] = props.gridOptions[key];
990
+ }
991
+ }
992
+ }
993
+
994
+ // AG-Grid САМО detekuje změny rowData a columnDefs
995
+ // NENÍ potřeba forceUpdate - AG-Grid reaguje na změny v props automaticky!
996
+
997
+ // ✅ Vracíme VŽDY stejný objekt (stabilní reference)
998
+ const allGridProps = allGridPropsRef.current;
999
+
1000
+ // State pro sledování, kdy je API ready
1001
+ const [isApiReady, setIsApiReady] = React.useState(false);
1002
+
1003
+ // Nastavení Quick Filter přes API (podle AG-Grid v33 dokumentace)
1004
+ // Tento useEffect se volá při změně quickFilterText NEBO když API je ready
1005
+ React.useEffect(() => {
1006
+ if (internalRef.current?.api && !internalRef.current.api.isDestroyed?.()) {
1007
+ // ✅ FIX AG-Grid v35: Zajistit že quickFilterText není null/undefined
1008
+ // AG-Grid v35 interně volá .toString() na této hodnotě bez null checku
1009
+ const safeQuickFilterText = quickFilterText ?? '';
1010
+ internalRef.current.api.setGridOption("quickFilterText", safeQuickFilterText);
1011
+ }
1012
+ }, [quickFilterText, isApiReady]);
1013
+
1014
+ // Status bar se zobrazuje pouze v simple mode
1015
+ const showStatusBar = notificationMode === 'simple' && aggregationDataRef.current;
1016
+
1017
+ // ✅ FIX: Rezervovat místo pro status bar pouze když má data
1018
+ // Grid se dynamicky rozšíří/zmenší podle přítomnosti status baru
1019
+ const shouldReserveSpace = showStatusBar;
1020
+ const gridContainerStyle = shouldReserveSpace
1021
+ ? { height: `calc(100% - ${statusBarHeight}px)`, width: '100%', position: 'relative' }
1022
+ : { height: '100%', width: '100%', position: 'relative' };
1023
+
1024
+ return (
1025
+ <div style={{ height: '100%', width: '100%', position: 'relative' }}>
1026
+ {/* AG Grid - fixní výška, nikdy se nemění */}
1027
+ <div style={gridContainerStyle}>
1028
+ <AgGridReact {...allGridProps} />
1029
+ </div>
1030
+
1031
+ {/* Aggregation Status Bar - absolutně pozicovaný na spodku */}
1032
+ {shouldReserveSpace && (
1033
+ <div style={{
1034
+ position: 'absolute',
1035
+ bottom: 0,
1036
+ left: 0,
1037
+ right: 0,
1038
+ height: `${statusBarHeight}px`,
1039
+ opacity: showStatusBar ? 1 : 0,
1040
+ visibility: showStatusBar ? 'visible' : 'hidden',
1041
+ pointerEvents: showStatusBar ? 'auto' : 'none',
1042
+ zIndex: 10,
1043
+ // GPU acceleration pro plynulejší zobrazení
1044
+ transform: 'translateZ(0)',
1045
+ willChange: 'opacity'
1046
+ }}>
1047
+ <AggregationStatusBar
1048
+ data={aggregationDataRef.current || {}}
1049
+ metrics={statusBarMetrics}
1050
+ height={statusBarHeight}
1051
+ onSwitchToFull={handleSwitchToFull}
1052
+ />
1053
+ </div>
1054
+ )}
1055
+
1056
+ {/* Bulk Edit Floating Button */}
1057
+ {enableBulkEdit && floatingButton.visible && (
1058
+ <BulkEditButton
1059
+ visible={floatingButton.visible}
1060
+ position={floatingButton.position}
1061
+ range={floatingButton.range}
1062
+ column={floatingButton.column}
1063
+ cellCount={floatingButton.cellCount}
1064
+ rowsContainer={floatingButton.rowsContainer}
1065
+ editPopover={editPopover}
1066
+ onOpenPopover={handleOpenPopover}
1067
+ onValueChange={handleValueChange}
1068
+ onSubmit={handleSubmitEdit}
1069
+ onCancel={handleCancelEdit}
1070
+ />
1071
+ )}
1072
+ </div>
1073
+ );
1074
+ });
1075
+
1076
+ // ========== PERFORMANCE OPTIMIZATION: React.memo ==========
1077
+ // Refactored: Používá createGridComparison utility místo manuálního deep comparison
1078
+ // Eliminováno ~120 řádků duplicitního kódu, zachována stejná funkcionalita
1079
+ // Diagnostic mode: zapnutý pouze ve development módu
1080
+ export default React.memo(AgGrid, createGridComparison(
1081
+ process.env.NODE_ENV !== 'production' // Diagnostic mode pouze ve development
1082
+ ));
1083
+
1084
+ export {
1085
+ useBulkCellEdit,
1086
+ BulkEditButton,
1087
+ BulkEditPopover,
1088
+ BulkEditSelect,
1089
+ BulkEditDatePicker,
1090
+ BulkEditModule,
1091
+ BulkEditInput
1092
+ } from './BulkEdit';
1093
+
1094
+ export {
1095
+ default as CheckboxRenderer
1096
+ } from './Renderers/CheckboxRenderer';
1097
+
1098
+ export * from './Renderers';
1089
1099
  export * from './Editors';