@bit.rhplus/ag-grid 0.0.99 → 0.0.101

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,1116 +1,1121 @@
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
- // Nastavíme flag _rh_plus_ag_grid_signal_new (NE _rh_plus_ag_grid_new_item),
759
- // aby postSort přesunul řádek na začátek, ale renderery obsah nezahovaly
760
- const addRecords = records.map(r => ({ ...r, _rh_plus_ag_grid_signal_new: true }));
761
- event.api.applyTransaction({ add: addRecords, addIndex: 0 });
762
- // Flash efekt pro nové řádky + po 5s odebrat signal flag a re-sort
763
- setTimeout(() => {
764
- records.forEach(record => {
765
- const rowNode = event.api.getRowNode(record.id);
766
- if (rowNode) {
767
- if (rowNode.rowElement) {
768
- rowNode.rowElement.classList.add('ag-row-flash-created');
769
- setTimeout(() => {
770
- rowNode.rowElement.classList.remove('ag-row-flash-created');
771
- }, 1000);
772
- }
773
- // Po 5 sekundách odebrat signal flag a refreshnout sort
774
- setTimeout(() => {
775
- if (rowNode.data) {
776
- delete rowNode.data._rh_plus_ag_grid_signal_new;
777
- // Refresh sort, aby se řádek zařadil podle aktuálního řazení
778
- if (event.api && !event.api.isDestroyed?.()) {
779
- event.api.refreshClientSideRowModel('sort');
780
- }
781
- }
782
- }, 5000);
783
- }
784
- });
785
- }, 100);
786
- break;
787
-
788
- case 'update':
789
- event.api.applyTransaction({ update: records });
790
- // Flash efekt pro aktualizované řádky
791
- setTimeout(() => {
792
- records.forEach(record => {
793
- const rowNode = event.api.getRowNode(record.id);
794
- if (rowNode && rowNode.rowElement) {
795
- rowNode.rowElement.classList.add('ag-row-flash-updated');
796
- setTimeout(() => {
797
- rowNode.rowElement.classList.remove('ag-row-flash-updated');
798
- }, 1000);
799
- }
800
- });
801
- }, 100);
802
- break;
803
-
804
- case 'remove':
805
- // Flash efekt před smazáním
806
- records.forEach(record => {
807
- const rowNode = event.api.getRowNode(record.id);
808
- if (rowNode && rowNode.rowElement) {
809
- rowNode.rowElement.classList.add('ag-row-flash-deleted');
810
- }
811
- });
812
-
813
- setTimeout(() => {
814
- event.api.applyTransaction({ remove: records });
815
- }, 500); // Krátké zpoždění pro zobrazení flash efektu
816
- break;
817
- }
818
- } catch (error) {
819
- // Ignorujeme chyby
820
- }
821
- };
822
- }
823
-
824
- // Pak zavoláme parent onGridReady handler, pokud existuje
825
- // Toto je kritické pro správné fungování bit/ui/grid a GridLayout
826
- if (options.onGridReady) {
827
- try {
828
- options.onGridReady(event);
829
- } catch (error) {
830
- // Error handling without console output
831
- }
832
- }
833
-
834
- // Nakonec nastavíme grid ready state s timeout
835
- setTimeout(() => {
836
- setIsGridReady(true);
837
- }, 1000);
838
- };
839
-
840
- const components = React.useMemo(() => {
841
- return {
842
- checkboxRenderer: CheckboxRenderer,
843
- selectRenderer: SelectRenderer,
844
- countrySelectRenderer: CountrySelectRenderer,
845
- booleanRenderer: BooleanRenderer,
846
- buttonRenderer: ButtonRenderer,
847
- iconRenderer: IconRenderer,
848
- imageRenderer: ImageRenderer,
849
- stateRenderer: StateRenderer,
850
- objectRenderer: ObjectRenderer,
851
- linkRenderer: LinkRenderer,
852
- ...props.frameworkComponents,
853
- };
854
- }, [props.frameworkComponents]);
855
-
856
- // ========== PERFORMANCE OPTIMIZATION: Memoizované event handlers ==========
857
- const memoizedOnCellEditingStarted = React.useCallback(
858
- (event) => RhPlusOnCellEditingStarted(event, props),
859
- [props.onCellEditingStarted]
860
- );
861
-
862
- const memoizedOnCellDoubleClicked = React.useCallback(
863
- (event) => RhPlusOnCellDoubleClicked(event, props),
864
- [props.onCellDoubleClicked]
865
- );
866
-
867
- const memoizedOnCellValueChanged = React.useCallback(
868
- (event) => RhPlusOnCellValueChanged(event, props),
869
- [props.onCellValueChanged]
870
- );
871
-
872
- const memoizedPostSort = React.useCallback(
873
- (event) => AgGridPostSort(event, props),
874
- [props.postSort]
875
- );
876
-
877
- const memoizedOnGridReady = React.useCallback(
878
- (event, options) => AgGridOnGridReady(event, props),
879
- [onGridReady]
880
- );
881
-
882
- const memoizedOnRowDataChanged = React.useCallback(
883
- (event) => AgGridOnRowDataChanged(event, props),
884
- [props.onRowDataChanged]
885
- );
886
-
887
- const memoizedOnRowDataUpdated = React.useCallback(
888
- (event) => AgGridOnRowDataUpdated(event, props),
889
- [props.onRowDataUpdated]
890
- );
891
-
892
- // Memoizovaný context object
893
- // ✅ OPTIMALIZACE: Použít pouze relevantní props pro context - neměnit při změně props
894
- const memoizedContext = React.useMemo(() => ({
895
- componentParent: props
896
- }), [props.context, props.gridName, props.id]);
897
-
898
- // Memoizovaný defaultColDef
899
- const memoizedDefaultColDef = React.useMemo(() => ({
900
- ...props.defaultColDef,
901
- suppressHeaderMenuButton: true,
902
- suppressHeaderFilterButton: true,
903
- suppressMenu: true,
904
- // ✅ FIX AG-Grid v35: Bezpečné zpracování null hodnot v Quick Filter
905
- // AG-Grid v35 změnil implementaci Quick Filter - nyní volá .toString() na hodnotách buněk bez null check
906
- // Tento callback zajistí že nikdy nedojde k chybě "Cannot read properties of null (reading 'toString')"
907
- getQuickFilterText: (params) => {
908
- const value = params.value;
909
- // Null/undefined prázdný string (bez chyby)
910
- if (value == null) return '';
911
- // Objekty a pole JSON string
912
- if (typeof value === 'object') {
913
- try {
914
- return JSON.stringify(value);
915
- } catch {
916
- // Cirkulární reference nebo jiná chyba při serializaci prázdný string
917
- return '';
918
- }
919
- }
920
- // Primitivní typy → toString
921
- return value.toString();
922
- },
923
- }), [props.defaultColDef]);
924
-
925
- // ========== PERFORMANCE FIX: Memoizovat columnDefs ==========
926
- // AgGridColumns vrací nový array při každém volání, i když jsou vstupy stejné
927
- const memoizedColumnDefs = React.useMemo(() => {
928
- return AgGridColumns(props.columnDefs, props);
929
- }, [props.columnDefs, props.getRowId, props.frameworkComponents]);
930
-
931
- // ========== CRITICAL FIX: Stabilní allGridProps objekt pomocí ref ==========
932
- // Problém: Jakékoli použití useMemo vytváří nový objekt při změně dependencies
933
- // Řešení: Použít ref pro VŽDY stejný objekt
934
- // AG-Grid САМО detekuje změny rowData a columnDefs NENÍ potřeba forceUpdate!
935
-
936
- const allGridPropsRef = React.useRef(null);
937
-
938
- // Inicializace objektu
939
- if (!allGridPropsRef.current) {
940
- allGridPropsRef.current = {};
941
- }
942
-
943
- // Aktualizovat props v EXISTUJÍCÍM objektu (mutace)
944
- // AG Grid interně detekuje změny props, nepotřebuje nový objekt
945
- allGridPropsRef.current.ref = internalRef;
946
- allGridPropsRef.current.rowData = props.rowData;
947
- allGridPropsRef.current.getRowId = props.getRowId;
948
- allGridPropsRef.current.theme = themeObject;
949
- allGridPropsRef.current.columnDefs = memoizedColumnDefs;
950
- allGridPropsRef.current.defaultColDef = memoizedDefaultColDef;
951
- allGridPropsRef.current.onCellEditingStarted = memoizedOnCellEditingStarted;
952
- allGridPropsRef.current.onCellDoubleClicked = memoizedOnCellDoubleClicked;
953
- allGridPropsRef.current.onCellValueChanged = memoizedOnCellValueChanged;
954
- allGridPropsRef.current.postSort = memoizedPostSort;
955
- allGridPropsRef.current.onGridReady = memoizedOnGridReady;
956
- allGridPropsRef.current.onRowDataChanged = memoizedOnRowDataChanged;
957
- allGridPropsRef.current.onRowDataUpdated = memoizedOnRowDataUpdated;
958
- allGridPropsRef.current.onRangeSelectionChanged = RhPlusRangeSelectionChanged;
959
- allGridPropsRef.current.context = memoizedContext;
960
- allGridPropsRef.current.components = components;
961
-
962
- // Další AG Grid props
963
- allGridPropsRef.current.rowModelType = props.rowModelType;
964
- allGridPropsRef.current.rowSelection = props.rowSelection;
965
- allGridPropsRef.current.enableRangeSelection = props.enableRangeSelection;
966
- allGridPropsRef.current.enableRangeHandle = props.enableRangeHandle;
967
- allGridPropsRef.current.enableFillHandle = props.enableFillHandle;
968
- allGridPropsRef.current.suppressRowClickSelection = props.suppressRowClickSelection;
969
- allGridPropsRef.current.singleClickEdit = props.singleClickEdit;
970
- allGridPropsRef.current.stopEditingWhenCellsLoseFocus = props.stopEditingWhenCellsLoseFocus;
971
- allGridPropsRef.current.rowClass = props.rowClass;
972
- allGridPropsRef.current.rowStyle = props.rowStyle;
973
- allGridPropsRef.current.getRowClass = props.getRowClass;
974
- allGridPropsRef.current.getRowStyle = props.getRowStyle;
975
- allGridPropsRef.current.animateRows = props.animateRows;
976
- allGridPropsRef.current.suppressCellFocus = props.suppressCellFocus;
977
- allGridPropsRef.current.suppressMenuHide = props.suppressMenuHide;
978
- allGridPropsRef.current.enableCellTextSelection = props.enableCellTextSelection;
979
- allGridPropsRef.current.ensureDomOrder = props.ensureDomOrder;
980
- allGridPropsRef.current.suppressRowTransform = props.suppressRowTransform;
981
- allGridPropsRef.current.suppressColumnVirtualisation = props.suppressColumnVirtualisation;
982
- allGridPropsRef.current.suppressRowVirtualisation = props.suppressRowVirtualisation;
983
- allGridPropsRef.current.tooltipShowDelay = props.tooltipShowDelay;
984
- allGridPropsRef.current.tooltipHideDelay = props.tooltipHideDelay;
985
- allGridPropsRef.current.tooltipMouseTrack = props.tooltipMouseTrack;
986
- allGridPropsRef.current.gridId = props.gridId;
987
- allGridPropsRef.current.id = props.id;
988
- allGridPropsRef.current.gridName = props.gridName;
989
- allGridPropsRef.current.getContextMenuItems = props.getContextMenuItems;
990
-
991
- // Grid Layout event handlers - stabilní wrappery (eliminuje rerendery)
992
- // FIX #12: Používáme stabilní wrappery místo props → eliminuje rerendery při změně props
993
- allGridPropsRef.current.onColumnMoved = stableOnColumnMoved;
994
- allGridPropsRef.current.onDragStopped = stableOnDragStopped;
995
- allGridPropsRef.current.onColumnVisible = stableOnColumnVisible;
996
- allGridPropsRef.current.onColumnPinned = stableOnColumnPinned;
997
- allGridPropsRef.current.onColumnResized = stableOnColumnResized;
998
-
999
- // gridOptions support - spread additional AG-Grid props
1000
- // Přidává POUZE hodnoty které ještě NEJSOU nastaveny výše (nejnižší priorita)
1001
- if (props.gridOptions && typeof props.gridOptions === 'object') {
1002
- for (const key in props.gridOptions) {
1003
- if (props.gridOptions[key] !== undefined && allGridPropsRef.current[key] === undefined) {
1004
- allGridPropsRef.current[key] = props.gridOptions[key];
1005
- }
1006
- }
1007
- }
1008
-
1009
- // AG-Grid САМО detekuje změny rowData a columnDefs
1010
- // NENÍ potřeba forceUpdate - AG-Grid reaguje na změny v props automaticky!
1011
-
1012
- // ✅ Vracíme VŽDY stejný objekt (stabilní reference)
1013
- const allGridProps = allGridPropsRef.current;
1014
-
1015
- // State pro sledování, kdy je API ready
1016
- const [isApiReady, setIsApiReady] = React.useState(false);
1017
-
1018
- // Nastavení Quick Filter přes API (podle AG-Grid v33 dokumentace)
1019
- // Tento useEffect se volá při změně quickFilterText NEBO když API je ready
1020
- React.useEffect(() => {
1021
- if (internalRef.current?.api && !internalRef.current.api.isDestroyed?.()) {
1022
- // ✅ FIX AG-Grid v35: Zajistit že quickFilterText není null/undefined
1023
- // AG-Grid v35 interně volá .toString() na této hodnotě bez null checku
1024
- const safeQuickFilterText = quickFilterText ?? '';
1025
- internalRef.current.api.setGridOption("quickFilterText", safeQuickFilterText);
1026
- }
1027
- }, [quickFilterText, isApiReady]);
1028
-
1029
- // Status bar se zobrazuje pouze v simple mode
1030
- const showStatusBar = notificationMode === 'simple' && aggregationDataRef.current;
1031
-
1032
- // FIX: Rezervovat místo pro status bar pouze když má data
1033
- // Grid se dynamicky rozšíří/zmenší podle přítomnosti status baru
1034
- const shouldReserveSpace = showStatusBar;
1035
- const gridContainerStyle = shouldReserveSpace
1036
- ? { height: `calc(100% - ${statusBarHeight}px)`, width: '100%', position: 'relative' }
1037
- : { height: '100%', width: '100%', position: 'relative' };
1038
-
1039
- return (
1040
- <div style={{ height: '100%', width: '100%', position: 'relative' }}>
1041
- {/* AG Grid - fixní výška, nikdy se nemění */}
1042
- <div style={gridContainerStyle}>
1043
- <AgGridReact {...allGridProps} />
1044
- </div>
1045
-
1046
- {/* Aggregation Status Bar - absolutně pozicovaný na spodku */}
1047
- {shouldReserveSpace && (
1048
- <div style={{
1049
- position: 'absolute',
1050
- bottom: 0,
1051
- left: 0,
1052
- right: 0,
1053
- height: `${statusBarHeight}px`,
1054
- opacity: showStatusBar ? 1 : 0,
1055
- visibility: showStatusBar ? 'visible' : 'hidden',
1056
- pointerEvents: showStatusBar ? 'auto' : 'none',
1057
- zIndex: 10,
1058
- // GPU acceleration pro plynulejší zobrazení
1059
- transform: 'translateZ(0)',
1060
- willChange: 'opacity'
1061
- }}>
1062
- <AggregationStatusBar
1063
- data={aggregationDataRef.current || {}}
1064
- metrics={statusBarMetrics}
1065
- height={statusBarHeight}
1066
- onSwitchToFull={handleSwitchToFull}
1067
- />
1068
- </div>
1069
- )}
1070
-
1071
- {/* Bulk Edit Floating Button */}
1072
- {enableBulkEdit && floatingButton.visible && (
1073
- <BulkEditButton
1074
- visible={floatingButton.visible}
1075
- position={floatingButton.position}
1076
- range={floatingButton.range}
1077
- column={floatingButton.column}
1078
- cellCount={floatingButton.cellCount}
1079
- rowsContainer={floatingButton.rowsContainer}
1080
- gridApi={internalRef.current?.api}
1081
- editPopover={editPopover}
1082
- onOpenPopover={handleOpenPopover}
1083
- onValueChange={handleValueChange}
1084
- onSubmit={handleSubmitEdit}
1085
- onCancel={handleCancelEdit}
1086
- />
1087
- )}
1088
- </div>
1089
- );
1090
- });
1091
-
1092
- // ========== PERFORMANCE OPTIMIZATION: React.memo ==========
1093
- // Refactored: Používá createGridComparison utility místo manuálního deep comparison
1094
- // Eliminováno ~120 řádků duplicitního kódu, zachována stejná funkcionalita
1095
- // Diagnostic mode: zapnutý pouze ve development módu
1096
- export default React.memo(AgGrid, createGridComparison(
1097
- process.env.NODE_ENV !== 'production' // Diagnostic mode pouze ve development
1098
- ));
1099
-
1100
- export {
1101
- useBulkCellEdit,
1102
- BulkEditButton,
1103
- BulkEditPopover,
1104
- BulkEditSelect,
1105
- BulkEditDatePicker,
1106
- BulkEditModule,
1107
- BulkEditInput,
1108
- BulkEditTagsSelect
1109
- } from './BulkEdit';
1110
-
1111
- export {
1112
- default as CheckboxRenderer
1113
- } from './Renderers/CheckboxRenderer';
1114
-
1115
- 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
+ // Nastavíme flag _rh_plus_ag_grid_signal_new (NE _rh_plus_ag_grid_new_item),
759
+ // aby postSort přesunul řádek na začátek, ale renderery obsah nezahovaly
760
+ const addRecords = records.map(r => ({ ...r, _rh_plus_ag_grid_signal_new: true }));
761
+ event.api.applyTransaction({ add: addRecords, addIndex: 0 });
762
+ // Flash efekt pro nové řádky + po 5s odebrat signal flag a re-sort
763
+ setTimeout(() => {
764
+ records.forEach(record => {
765
+ const rowNode = event.api.getRowNode(record.id);
766
+ if (rowNode) {
767
+ if (rowNode.rowElement) {
768
+ rowNode.rowElement.classList.add('ag-row-flash-created');
769
+ setTimeout(() => {
770
+ rowNode.rowElement.classList.remove('ag-row-flash-created');
771
+ }, 1000);
772
+ }
773
+ // Po 5 sekundách odebrat signal flag a refreshnout sort
774
+ setTimeout(() => {
775
+ if (rowNode.data) {
776
+ delete rowNode.data._rh_plus_ag_grid_signal_new;
777
+ // Refresh sort, aby se řádek zařadil podle aktuálního řazení
778
+ if (event.api && !event.api.isDestroyed?.()) {
779
+ event.api.refreshClientSideRowModel('sort');
780
+ }
781
+ }
782
+ }, 5000);
783
+ }
784
+ });
785
+ }, 100);
786
+ break;
787
+
788
+ case 'update':
789
+ event.api.applyTransaction({ update: records });
790
+ // Flash efekt pro aktualizované řádky
791
+ setTimeout(() => {
792
+ records.forEach(record => {
793
+ const rowNode = event.api.getRowNode(record.id);
794
+ if (rowNode && rowNode.rowElement) {
795
+ rowNode.rowElement.classList.add('ag-row-flash-updated');
796
+ setTimeout(() => {
797
+ rowNode.rowElement.classList.remove('ag-row-flash-updated');
798
+ }, 1000);
799
+ }
800
+ });
801
+ }, 100);
802
+ break;
803
+
804
+ case 'remove':
805
+ // Flash efekt před smazáním
806
+ records.forEach(record => {
807
+ const rowNode = event.api.getRowNode(record.id);
808
+ if (rowNode && rowNode.rowElement) {
809
+ rowNode.rowElement.classList.add('ag-row-flash-deleted');
810
+ }
811
+ });
812
+
813
+ setTimeout(() => {
814
+ event.api.applyTransaction({ remove: records });
815
+ }, 500); // Krátké zpoždění pro zobrazení flash efektu
816
+ break;
817
+ }
818
+ } catch (error) {
819
+ // Ignorujeme chyby
820
+ }
821
+ };
822
+ }
823
+
824
+ // Pak zavoláme parent onGridReady handler, pokud existuje
825
+ // Toto je kritické pro správné fungování bit/ui/grid a GridLayout
826
+ if (options.onGridReady) {
827
+ try {
828
+ options.onGridReady(event);
829
+ } catch (error) {
830
+ // Error handling without console output
831
+ }
832
+ }
833
+
834
+ // Nakonec nastavíme grid ready state s timeout
835
+ setTimeout(() => {
836
+ setIsGridReady(true);
837
+ }, 1000);
838
+ };
839
+
840
+ const components = React.useMemo(() => {
841
+ return {
842
+ checkboxRenderer: CheckboxRenderer,
843
+ selectRenderer: SelectRenderer,
844
+ countrySelectRenderer: CountrySelectRenderer,
845
+ booleanRenderer: BooleanRenderer,
846
+ buttonRenderer: ButtonRenderer,
847
+ iconRenderer: IconRenderer,
848
+ imageRenderer: ImageRenderer,
849
+ stateRenderer: StateRenderer,
850
+ objectRenderer: ObjectRenderer,
851
+ linkRenderer: LinkRenderer,
852
+ ...props.frameworkComponents,
853
+ };
854
+ }, [props.frameworkComponents]);
855
+
856
+ // ========== PERFORMANCE OPTIMIZATION: Memoizované event handlers ==========
857
+ const memoizedOnCellEditingStarted = React.useCallback(
858
+ (event) => RhPlusOnCellEditingStarted(event, props),
859
+ [props.onCellEditingStarted]
860
+ );
861
+
862
+ const memoizedOnCellDoubleClicked = React.useCallback(
863
+ (event) => RhPlusOnCellDoubleClicked(event, props),
864
+ [props.onCellDoubleClicked]
865
+ );
866
+
867
+ const memoizedOnCellValueChanged = React.useCallback(
868
+ (event) => RhPlusOnCellValueChanged(event, props),
869
+ [props.onCellValueChanged]
870
+ );
871
+
872
+ const memoizedPostSort = React.useCallback(
873
+ (event) => AgGridPostSort(event, props),
874
+ [props.postSort]
875
+ );
876
+
877
+ const memoizedOnGridReady = React.useCallback(
878
+ (event, options) => AgGridOnGridReady(event, props),
879
+ [onGridReady]
880
+ );
881
+
882
+ const memoizedOnRowDataChanged = React.useCallback(
883
+ (event) => AgGridOnRowDataChanged(event, props),
884
+ [props.onRowDataChanged]
885
+ );
886
+
887
+ const memoizedOnRowDataUpdated = React.useCallback(
888
+ (event) => AgGridOnRowDataUpdated(event, props),
889
+ [props.onRowDataUpdated]
890
+ );
891
+
892
+ // Memoizovaný context object
893
+ // ✅ OPTIMALIZACE: Použít pouze relevantní props pro context - neměnit při změně props
894
+ const memoizedContext = React.useMemo(() => ({
895
+ componentParent: props
896
+ }), [props.context, props.gridName, props.id]);
897
+
898
+ // Memoizovaný defaultColDef
899
+ const memoizedDefaultColDef = React.useMemo(() => ({
900
+ filter: 'agTextColumnFilter',
901
+ floatingFilter: true,
902
+ filterParams: {
903
+ defaultOption: 'contains',
904
+ },
905
+ ...props.defaultColDef,
906
+ suppressHeaderMenuButton: true,
907
+ suppressHeaderFilterButton: true,
908
+ suppressMenu: true,
909
+ // FIX AG-Grid v35: Bezpečné zpracování null hodnot v Quick Filter
910
+ // AG-Grid v35 změnil implementaci Quick Filter - nyní volá .toString() na hodnotách buněk bez null check
911
+ // Tento callback zajistí že nikdy nedojde k chybě "Cannot read properties of null (reading 'toString')"
912
+ getQuickFilterText: (params) => {
913
+ const value = params.value;
914
+ // Null/undefined → prázdný string (bez chyby)
915
+ if (value == null) return '';
916
+ // Objekty a poleJSON string
917
+ if (typeof value === 'object') {
918
+ try {
919
+ return JSON.stringify(value);
920
+ } catch {
921
+ // Cirkulární reference nebo jiná chyba při serializaci → prázdný string
922
+ return '';
923
+ }
924
+ }
925
+ // Primitivní typy toString
926
+ return value.toString();
927
+ },
928
+ }), [props.defaultColDef]);
929
+
930
+ // ========== PERFORMANCE FIX: Memoizovat columnDefs ==========
931
+ // AgGridColumns vrací nový array při každém volání, i když jsou vstupy stejné
932
+ const memoizedColumnDefs = React.useMemo(() => {
933
+ return AgGridColumns(props.columnDefs, props);
934
+ }, [props.columnDefs, props.getRowId, props.frameworkComponents]);
935
+
936
+ // ========== CRITICAL FIX: Stabilní allGridProps objekt pomocí ref ==========
937
+ // Problém: Jakékoli použití useMemo vytváří nový objekt při změně dependencies
938
+ // Řešení: Použít ref pro VŽDY stejný objekt
939
+ // AG-Grid САМО detekuje změny rowData a columnDefs → NENÍ potřeba forceUpdate!
940
+
941
+ const allGridPropsRef = React.useRef(null);
942
+
943
+ // Inicializace objektu
944
+ if (!allGridPropsRef.current) {
945
+ allGridPropsRef.current = {};
946
+ }
947
+
948
+ // Aktualizovat props v EXISTUJÍCÍM objektu (mutace)
949
+ // AG Grid interně detekuje změny props, nepotřebuje nový objekt
950
+ allGridPropsRef.current.ref = internalRef;
951
+ allGridPropsRef.current.rowData = props.rowData;
952
+ allGridPropsRef.current.getRowId = props.getRowId;
953
+ allGridPropsRef.current.theme = themeObject;
954
+ allGridPropsRef.current.columnDefs = memoizedColumnDefs;
955
+ allGridPropsRef.current.defaultColDef = memoizedDefaultColDef;
956
+ allGridPropsRef.current.onCellEditingStarted = memoizedOnCellEditingStarted;
957
+ allGridPropsRef.current.onCellDoubleClicked = memoizedOnCellDoubleClicked;
958
+ allGridPropsRef.current.onCellValueChanged = memoizedOnCellValueChanged;
959
+ allGridPropsRef.current.postSort = memoizedPostSort;
960
+ allGridPropsRef.current.onGridReady = memoizedOnGridReady;
961
+ allGridPropsRef.current.onRowDataChanged = memoizedOnRowDataChanged;
962
+ allGridPropsRef.current.onRowDataUpdated = memoizedOnRowDataUpdated;
963
+ allGridPropsRef.current.onRangeSelectionChanged = RhPlusRangeSelectionChanged;
964
+ allGridPropsRef.current.context = memoizedContext;
965
+ allGridPropsRef.current.components = components;
966
+
967
+ // Další AG Grid props
968
+ allGridPropsRef.current.rowModelType = props.rowModelType;
969
+ allGridPropsRef.current.rowSelection = props.rowSelection;
970
+ allGridPropsRef.current.enableRangeSelection = props.enableRangeSelection;
971
+ allGridPropsRef.current.enableRangeHandle = props.enableRangeHandle;
972
+ allGridPropsRef.current.enableFillHandle = props.enableFillHandle;
973
+ allGridPropsRef.current.suppressRowClickSelection = props.suppressRowClickSelection;
974
+ allGridPropsRef.current.singleClickEdit = props.singleClickEdit;
975
+ allGridPropsRef.current.stopEditingWhenCellsLoseFocus = props.stopEditingWhenCellsLoseFocus;
976
+ allGridPropsRef.current.rowClass = props.rowClass;
977
+ allGridPropsRef.current.rowStyle = props.rowStyle;
978
+ allGridPropsRef.current.getRowClass = props.getRowClass;
979
+ allGridPropsRef.current.getRowStyle = props.getRowStyle;
980
+ allGridPropsRef.current.animateRows = props.animateRows;
981
+ allGridPropsRef.current.suppressCellFocus = props.suppressCellFocus;
982
+ allGridPropsRef.current.suppressMenuHide = props.suppressMenuHide;
983
+ allGridPropsRef.current.enableCellTextSelection = props.enableCellTextSelection;
984
+ allGridPropsRef.current.ensureDomOrder = props.ensureDomOrder;
985
+ allGridPropsRef.current.suppressRowTransform = props.suppressRowTransform;
986
+ allGridPropsRef.current.suppressColumnVirtualisation = props.suppressColumnVirtualisation;
987
+ allGridPropsRef.current.suppressRowVirtualisation = props.suppressRowVirtualisation;
988
+ allGridPropsRef.current.tooltipShowDelay = props.tooltipShowDelay;
989
+ allGridPropsRef.current.tooltipHideDelay = props.tooltipHideDelay;
990
+ allGridPropsRef.current.tooltipMouseTrack = props.tooltipMouseTrack;
991
+ allGridPropsRef.current.gridId = props.gridId;
992
+ allGridPropsRef.current.id = props.id;
993
+ allGridPropsRef.current.gridName = props.gridName;
994
+ allGridPropsRef.current.getContextMenuItems = props.getContextMenuItems;
995
+
996
+ // Grid Layout event handlers - stabilní wrappery (eliminuje rerendery)
997
+ // FIX #12: Používáme stabilní wrappery místo props → eliminuje rerendery při změně props
998
+ allGridPropsRef.current.onColumnMoved = stableOnColumnMoved;
999
+ allGridPropsRef.current.onDragStopped = stableOnDragStopped;
1000
+ allGridPropsRef.current.onColumnVisible = stableOnColumnVisible;
1001
+ allGridPropsRef.current.onColumnPinned = stableOnColumnPinned;
1002
+ allGridPropsRef.current.onColumnResized = stableOnColumnResized;
1003
+
1004
+ // gridOptions support - spread additional AG-Grid props
1005
+ // Přidává POUZE hodnoty které ještě NEJSOU nastaveny výše (nejnižší priorita)
1006
+ if (props.gridOptions && typeof props.gridOptions === 'object') {
1007
+ for (const key in props.gridOptions) {
1008
+ if (props.gridOptions[key] !== undefined && allGridPropsRef.current[key] === undefined) {
1009
+ allGridPropsRef.current[key] = props.gridOptions[key];
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ // ✅ AG-Grid САМО detekuje změny rowData a columnDefs
1015
+ // NENÍ potřeba forceUpdate - AG-Grid reaguje na změny v props automaticky!
1016
+
1017
+ // ✅ Vracíme VŽDY stejný objekt (stabilní reference)
1018
+ const allGridProps = allGridPropsRef.current;
1019
+
1020
+ // State pro sledování, kdy je API ready
1021
+ const [isApiReady, setIsApiReady] = React.useState(false);
1022
+
1023
+ // Nastavení Quick Filter přes API (podle AG-Grid v33 dokumentace)
1024
+ // Tento useEffect se volá při změně quickFilterText NEBO když API je ready
1025
+ React.useEffect(() => {
1026
+ if (internalRef.current?.api && !internalRef.current.api.isDestroyed?.()) {
1027
+ // ✅ FIX AG-Grid v35: Zajistit že quickFilterText není null/undefined
1028
+ // AG-Grid v35 interně volá .toString() na této hodnotě bez null checku
1029
+ const safeQuickFilterText = quickFilterText ?? '';
1030
+ internalRef.current.api.setGridOption("quickFilterText", safeQuickFilterText);
1031
+ }
1032
+ }, [quickFilterText, isApiReady]);
1033
+
1034
+ // Status bar se zobrazuje pouze v simple mode
1035
+ const showStatusBar = notificationMode === 'simple' && aggregationDataRef.current;
1036
+
1037
+ // FIX: Rezervovat místo pro status bar pouze když má data
1038
+ // Grid se dynamicky rozšíří/zmenší podle přítomnosti status baru
1039
+ const shouldReserveSpace = showStatusBar;
1040
+ const gridContainerStyle = shouldReserveSpace
1041
+ ? { height: `calc(100% - ${statusBarHeight}px)`, width: '100%', position: 'relative' }
1042
+ : { height: '100%', width: '100%', position: 'relative' };
1043
+
1044
+ return (
1045
+ <div style={{ height: '100%', width: '100%', position: 'relative' }}>
1046
+ {/* AG Grid - fixní výška, nikdy se nemění */}
1047
+ <div style={gridContainerStyle}>
1048
+ <AgGridReact {...allGridProps} />
1049
+ </div>
1050
+
1051
+ {/* Aggregation Status Bar - absolutně pozicovaný na spodku */}
1052
+ {shouldReserveSpace && (
1053
+ <div style={{
1054
+ position: 'absolute',
1055
+ bottom: 0,
1056
+ left: 0,
1057
+ right: 0,
1058
+ height: `${statusBarHeight}px`,
1059
+ opacity: showStatusBar ? 1 : 0,
1060
+ visibility: showStatusBar ? 'visible' : 'hidden',
1061
+ pointerEvents: showStatusBar ? 'auto' : 'none',
1062
+ zIndex: 10,
1063
+ // GPU acceleration pro plynulejší zobrazení
1064
+ transform: 'translateZ(0)',
1065
+ willChange: 'opacity'
1066
+ }}>
1067
+ <AggregationStatusBar
1068
+ data={aggregationDataRef.current || {}}
1069
+ metrics={statusBarMetrics}
1070
+ height={statusBarHeight}
1071
+ onSwitchToFull={handleSwitchToFull}
1072
+ />
1073
+ </div>
1074
+ )}
1075
+
1076
+ {/* Bulk Edit Floating Button */}
1077
+ {enableBulkEdit && floatingButton.visible && (
1078
+ <BulkEditButton
1079
+ visible={floatingButton.visible}
1080
+ position={floatingButton.position}
1081
+ range={floatingButton.range}
1082
+ column={floatingButton.column}
1083
+ cellCount={floatingButton.cellCount}
1084
+ rowsContainer={floatingButton.rowsContainer}
1085
+ gridApi={internalRef.current?.api}
1086
+ editPopover={editPopover}
1087
+ onOpenPopover={handleOpenPopover}
1088
+ onValueChange={handleValueChange}
1089
+ onSubmit={handleSubmitEdit}
1090
+ onCancel={handleCancelEdit}
1091
+ />
1092
+ )}
1093
+ </div>
1094
+ );
1095
+ });
1096
+
1097
+ // ========== PERFORMANCE OPTIMIZATION: React.memo ==========
1098
+ // Refactored: Používá createGridComparison utility místo manuálního deep comparison
1099
+ // Eliminováno ~120 řádků duplicitního kódu, zachována stejná funkcionalita
1100
+ // Diagnostic mode: zapnutý pouze ve development módu
1101
+ export default React.memo(AgGrid, createGridComparison(
1102
+ process.env.NODE_ENV !== 'production' // Diagnostic mode pouze ve development
1103
+ ));
1104
+
1105
+ export {
1106
+ useBulkCellEdit,
1107
+ BulkEditButton,
1108
+ BulkEditPopover,
1109
+ BulkEditSelect,
1110
+ BulkEditDatePicker,
1111
+ BulkEditModule,
1112
+ BulkEditInput,
1113
+ BulkEditTagsSelect
1114
+ } from './BulkEdit';
1115
+
1116
+ export {
1117
+ default as CheckboxRenderer
1118
+ } from './Renderers/CheckboxRenderer';
1119
+
1120
+ export * from './Renderers';
1116
1121
  export * from './Editors';