@bit.rhplus/ui.grid 0.0.51 → 0.0.53

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/ColumnBuilder.jsx CHANGED
@@ -1,1032 +1,1294 @@
1
- /* eslint-disable */
2
- import * as React from 'react';
3
- import Menu from 'antd/es/menu';
4
- import Dropdown from 'antd/es/dropdown';
5
- import Button from 'antd/es/button';
6
- import Divider from 'antd/es/divider';
7
- import { prepareCellStyle, prepareColDef, getValueByPath } from './functions';
8
- import { currencySymbols } from './enums';
9
- import { ActionIcon } from './Icons';
10
- import { CircleHelp, Check, X } from 'lucide-react';
11
-
12
- // Vytvoříme React hook funkci pro link cell renderer
13
- const useLinkCellRenderer = (
14
- params,
15
- onClick,
16
- linkStyle,
17
- hoverStyle,
18
- overviewToggle
19
- ) => {
20
- const [isHovered, setIsHovered] = React.useState(false);
21
-
22
- const handleMouseEnter = React.useCallback(() => {
23
- setIsHovered(true);
24
- }, []);
25
-
26
- const handleMouseLeave = React.useCallback(() => {
27
- setIsHovered(false);
28
- }, []);
29
-
30
- const handleClick = React.useCallback(
31
- (event) => {
32
- event.stopPropagation();
33
-
34
- if (overviewToggle && params.api) {
35
- params.api.dispatchEvent({
36
- type: 'overviewToggle',
37
- data: params.data,
38
- params,
39
- });
40
- }
41
-
42
- if (onClick) {
43
- onClick(params);
44
- }
45
- },
46
- [onClick, overviewToggle, params]
47
- );
48
-
49
- // Přesunuto do useMemo, aby se vytvořilo pouze jednou
50
- const defaultLinkStyle = React.useMemo(
51
- () => ({
52
- color: '#1a73e8',
53
- textDecoration: 'none',
54
- cursor: 'pointer',
55
- }),
56
- []
57
- );
58
-
59
- // Přesunuto do useMemo, aby se vytvořilo pouze jednou
60
- const defaultHoverStyle = React.useMemo(
61
- () => ({
62
- textDecoration: 'underline',
63
- }),
64
- []
65
- );
66
-
67
- const computedLinkStyle = React.useMemo(
68
- () => ({
69
- ...defaultLinkStyle,
70
- ...(typeof linkStyle === 'function'
71
- ? linkStyle(params)
72
- : linkStyle || {}),
73
- }),
74
- [linkStyle, params, defaultLinkStyle]
75
- );
76
-
77
- const computedHoverStyle = React.useMemo(
78
- () => ({
79
- ...defaultLinkStyle,
80
- ...computedLinkStyle,
81
- ...defaultHoverStyle,
82
- ...(typeof hoverStyle === 'function'
83
- ? hoverStyle(params)
84
- : hoverStyle || {}),
85
- }),
86
- [computedLinkStyle, hoverStyle, params, defaultLinkStyle, defaultHoverStyle]
87
- );
88
-
89
- return {
90
- isHovered,
91
- handleMouseEnter,
92
- handleMouseLeave,
93
- handleClick,
94
- computedLinkStyle,
95
- computedHoverStyle,
96
- };
97
- };
98
-
99
- // Vytvoříme React hook funkci pro object cell renderer
100
- const useObjectCellRenderer = (params, editable, onEditClick) => {
101
- const [isHovered, setIsHovered] = React.useState(false);
102
-
103
- const handleMouseEnter = React.useCallback(() => {
104
- setIsHovered(true);
105
- }, []);
106
-
107
- const handleMouseLeave = React.useCallback(() => {
108
- setIsHovered(false);
109
- }, []);
110
-
111
- const handleEditClick = React.useCallback(
112
- (event) => {
113
- event.stopPropagation();
114
- if (onEditClick) {
115
- onEditClick(params);
116
- }
117
- },
118
- [onEditClick, params]
119
- );
120
-
121
- return {
122
- isHovered,
123
- handleMouseEnter,
124
- handleMouseLeave,
125
- handleEditClick,
126
- };
127
- };
128
-
129
- // Vytvoříme React hook funkci pro row hover action renderer
130
- const useRowHoverActionRenderer = (params) => {
131
- const [isRowHovered, setIsRowHovered] = React.useState(false);
132
- const rowRef = React.useRef(null);
133
-
134
- React.useEffect(() => {
135
- if (!params.node) return undefined;
136
-
137
- // Funkce pro kontrolu, zda je řádek v hover stavu
138
- const checkRowHoverState = () => {
139
- // Najdeme DOM element řádku pomocí rowIndex
140
- const rowElement = document.querySelector(
141
- `.ag-row[row-index="${params.rowIndex}"]`
142
- );
143
- if (rowElement) {
144
- rowRef.current = rowElement;
145
- // AG-Grid automaticky přidává třídu ag-row-hover k řádku při najetí myší
146
- const isHovered = rowElement.classList.contains('ag-row-hover');
147
- setIsRowHovered(isHovered);
148
- }
149
- };
150
-
151
- // Vytvoříme MutationObserver pro sledování změn tříd na řádku
152
- const observer = new MutationObserver((mutations) => {
153
- mutations.forEach((mutation) => {
154
- if (
155
- mutation.type === 'attributes' &&
156
- mutation.attributeName === 'class'
157
- ) {
158
- checkRowHoverState();
159
- }
160
- });
161
- });
162
-
163
- // Inicializujeme stav
164
- checkRowHoverState();
165
-
166
- // Přidáme observer k element řádku, pokud existuje
167
- if (rowRef.current) {
168
- observer.observe(rowRef.current, { attributes: true });
169
- }
170
-
171
- // Přidáme event listeners na grid container pro sledování pohybu myši
172
- const gridContainer = document.querySelector('.ag-root-wrapper');
173
- if (gridContainer) {
174
- gridContainer.addEventListener('mouseover', checkRowHoverState);
175
- gridContainer.addEventListener('mouseout', checkRowHoverState);
176
- }
177
-
178
- // Cleanup při odmontování komponenty
179
- return () => {
180
- if (rowRef.current) {
181
- observer.disconnect();
182
- }
183
- if (gridContainer) {
184
- gridContainer.removeEventListener('mouseover', checkRowHoverState);
185
- gridContainer.removeEventListener('mouseout', checkRowHoverState);
186
- }
187
- };
188
- }, [params.node, params.rowIndex]);
189
-
190
- return {
191
- isRowHovered,
192
- };
193
- };
194
-
195
- class ColumnBuilder {
196
- #addPreparedColumn(props) {
197
- this.columns.push(prepareColDef(this.intl, props));
198
- }
199
-
200
- constructor(intl) {
201
- this.columns = [];
202
- this.intl = intl;
203
- }
204
-
205
- addColumn({ colorField, ...props }) {
206
- // Získáme původní cellStyle, pokud existuje
207
- const originalCellStyle = props.cellStyle;
208
- const { cellAlign, ...restProps } = props;
209
-
210
- // Pokud je zadáno colorField, vytvoříme novou funkci pro cellStyle
211
- if (colorField) {
212
- const overrideCellStyle = (params) => {
213
- // Získáme základní styl
214
- let baseStyle = {};
215
- if (originalCellStyle) {
216
- if (typeof originalCellStyle === 'function') {
217
- baseStyle = originalCellStyle(params);
218
- } else {
219
- baseStyle = originalCellStyle;
220
- }
221
- }
222
-
223
- // Získáme hodnotu barvy z dat
224
- const colorValue =
225
- params.data && getValueByPath(params.data, colorField);
226
- const restStyle = prepareCellStyle(props);
227
- // Vrátíme kombinovaný styl
228
- return colorValue
229
- ? { ...baseStyle, ...restStyle, color: colorValue }
230
- : { ...baseStyle, ...restStyle };
231
- };
232
-
233
- // Předáme upravené props do addPreparedColumn
234
- this.#addPreparedColumn({ ...restProps, overrideCellStyle });
235
- } else {
236
- // Pokud není colorField zadán, použijeme původní implementaci
237
- this.#addPreparedColumn(props);
238
- }
239
-
240
- return this;
241
- }
242
-
243
- // Nová metoda pro přidání sloupce s fakturou pro overview mód
244
- addInvoiceCardColumn({
245
- field = 'invoice',
246
- headerName = '',
247
- width = 340,
248
- onCheckboxChange,
249
- ...restProps
250
- } = {}) {
251
- const cellRenderer = 'invoiceCardRenderer';
252
-
253
- this.#addPreparedColumn({
254
- headerName,
255
- field,
256
- width,
257
- cellRenderer,
258
- suppressSizeToFit: true,
259
- cellRendererParams: {
260
- onCheckboxChange:
261
- onCheckboxChange ||
262
- (() => {
263
- // Funkce bez console.log
264
- }),
265
- },
266
- ...restProps,
267
- });
268
-
269
- return this;
270
- }
271
-
272
- addCurrencyColumn({
273
- currency,
274
- position,
275
- currencyStyle,
276
- cellAlign = 'right',
277
- editable,
278
- ...restProps
279
- }) {
280
- const cellRenderer = (params) => {
281
- const currencyValue =
282
- typeof currency === 'function' ? currency(params) : currency;
283
- const symbol = currencySymbols[currencyValue] || currencyValue;
284
-
285
- // Použití Number.isNaN místo globální isNaN
286
- const value =
287
- params.value != null && !Number.isNaN(Number(params.value))
288
- ? params.value.toFixed(2)
289
- : '0.00';
290
-
291
- const style =
292
- typeof currencyStyle === 'function'
293
- ? currencyStyle(params)
294
- : currencyStyle;
295
-
296
- return (
297
- <span>
298
- {position === 'left' ? (
299
- <>
300
- <span style={style}>{symbol}</span> {value}
301
- </>
302
- ) : (
303
- <>
304
- {value} <span style={style}>{symbol}</span>
305
- </>
306
- )}
307
- </span>
308
- );
309
- };
310
-
311
- this.#addPreparedColumn({
312
- cellAlign,
313
- cellRenderer,
314
- editable: this.#resolveEditable(editable),
315
- ...restProps,
316
- });
317
- return this;
318
- }
319
-
320
- addDateColumn({ dateFormat = 'DD.MM.YYYY', editable, ...restProps }) {
321
- this.#addPreparedColumn({
322
- dateRenderer: dateFormat,
323
- editable: this.#resolveEditable(editable),
324
- ...restProps,
325
- });
326
- return this;
327
- }
328
-
329
- addLinkColumn({
330
- onClick,
331
- linkStyle,
332
- hoverStyle,
333
- cellAlign = 'left',
334
- editable,
335
- overviewToggle = false, // Přidán výchozí parametr pro overview
336
- ...restProps
337
- }) {
338
- // Vytvořím komponentu pro cell renderer, která používá hook
339
- const LinkCellRenderer = React.memo((params) => {
340
- const {
341
- isHovered,
342
- handleMouseEnter,
343
- handleMouseLeave,
344
- handleClick,
345
- computedLinkStyle,
346
- computedHoverStyle,
347
- } = useLinkCellRenderer(
348
- params,
349
- onClick,
350
- linkStyle,
351
- hoverStyle,
352
- overviewToggle
353
- );
354
-
355
- return (
356
- <div
357
- className="link-cell-container"
358
- style={{
359
- width: '100%',
360
- height: '100%',
361
- display: 'flex',
362
- alignItems: 'center',
363
- }}
364
- >
365
- <span
366
- onClick={handleClick}
367
- onMouseEnter={handleMouseEnter}
368
- onMouseLeave={handleMouseLeave}
369
- style={isHovered ? computedHoverStyle : computedLinkStyle}
370
- role="button"
371
- tabIndex={0}
372
- onKeyPress={(event) => {
373
- if (event.key === 'Enter' || event.key === ' ') {
374
- handleClick(event);
375
- }
376
- }}
377
- >
378
- {params.value}
379
- </span>
380
- </div>
381
- );
382
- });
383
-
384
- // Používáme funkci, která vrátí React komponentu
385
- const cellRenderer = (params) => {
386
- return <LinkCellRenderer {...params} />;
387
- };
388
-
389
- this.#addPreparedColumn({
390
- cellAlign,
391
- cellRenderer,
392
- editable: this.#resolveEditable(editable),
393
- overviewToggle, // Předání příznaku dál
394
- ...restProps,
395
- });
396
- return this;
397
- }
398
-
399
- // Přidání Helper metody pro overview link
400
- addOverviewLinkColumn({
401
- field = 'documentNumber',
402
- headerName = '',
403
- width = 130,
404
- cellAlign = 'left',
405
- linkStyle,
406
- hoverStyle,
407
- editable = false,
408
- ...rest
409
- } = {}) {
410
- return this.addLinkColumn({
411
- field,
412
- headerName: this?.intl?.formatMessage({ id: rest?.intlId }) || headerName,
413
- width,
414
- cellAlign,
415
- linkStyle,
416
- hoverStyle,
417
- editable,
418
- overviewToggle: true,
419
- ...rest,
420
- });
421
- }
422
-
423
- addObjectColumn({
424
- contentTooltip,
425
- tooltipField,
426
- tooltipInteraction,
427
- tooltipShowDelay = 100,
428
- editable,
429
- onEditClick,
430
- ...restProps
431
- }) {
432
- // Vytvořím komponentu pro cell renderer, která používá hook
433
- const ObjectCellRenderer = React.memo((params) => {
434
- const { isHovered, handleMouseEnter, handleMouseLeave, handleEditClick } =
435
- useObjectCellRenderer(params, editable, onEditClick);
436
-
437
- return (
438
- <div
439
- className="object-cell-container"
440
- style={{
441
- position: 'relative',
442
- width: '100%',
443
- height: '100%',
444
- display: 'flex',
445
- alignItems: 'center',
446
- }}
447
- onMouseEnter={handleMouseEnter}
448
- onMouseLeave={handleMouseLeave}
449
- >
450
- <div style={{ flexGrow: 1 }}>{params.value}</div>
451
- {isHovered && editable && (
452
- <button
453
- className="edit-object-button"
454
- style={{
455
- position: 'absolute',
456
- right: '4px',
457
- background: 'transparent',
458
- border: 'none',
459
- cursor: 'pointer',
460
- padding: '4px',
461
- display: 'flex',
462
- alignItems: 'center',
463
- justifyContent: 'center',
464
- }}
465
- onClick={handleEditClick}
466
- title="Upravit"
467
- >
468
- <span style={{ fontSize: '16px' }}>⋮</span>
469
- </button>
470
- )}
471
- </div>
472
- );
473
- });
474
-
475
- // Používáme funkci, která vrátí React komponentu
476
- const cellRenderer = (params) => {
477
- return <ObjectCellRenderer {...params} />;
478
- };
479
-
480
- this.#addPreparedColumn({
481
- contentTooltip,
482
- tooltipField,
483
- tooltipInteraction,
484
- tooltipShowDelay,
485
- cellRenderer,
486
- editable: this.#resolveEditable(editable),
487
- ...restProps,
488
- });
489
- return this;
490
- }
491
-
492
- addIconColumn({
493
- visibleGetter = () => true,
494
- cellAlign,
495
- icon,
496
- badge,
497
- showZero,
498
- ...restProps
499
- }) {
500
- const iconRendererParams = {
501
- cellAlign,
502
- visibleGetter,
503
- icon,
504
- badge,
505
- showZero,
506
- };
507
- this.#addPreparedColumn({
508
- iconRenderer: true,
509
- iconRendererParams,
510
- ...restProps,
511
- });
512
- return this;
513
- }
514
-
515
- addBooleanColumn({
516
- visibleFalse = true,
517
- visibleTrue = true,
518
- cellAlign = 'center',
519
- visibleGetter = () => true,
520
- defaultIcon = true,
521
- defaultIconColor = '#898989',
522
- size = 16,
523
- colorTrue = 'green',
524
- colorFalse = 'red',
525
- showOnGroup = false,
526
- editable,
527
- ...restProps
528
- }) {
529
- // Create a React component for boolean cell renderer
530
- const BooleanCellRenderer = React.memo((params) => {
531
- const { data, value } = params;
532
-
533
- const visibleResult = visibleGetter ? visibleGetter(data) : true;
534
-
535
- const Icon = ({ innerValue, ...iconProps }) => {
536
- if (
537
- innerValue === undefined ||
538
- (!visibleFalse && !innerValue) ||
539
- (!visibleTrue && innerValue)
540
- ) {
541
- if (defaultIcon)
542
- return <CircleHelp size={size} color={defaultIconColor} {...iconProps} />;
543
- return <div />;
544
- }
545
-
546
- if (innerValue)
547
- return <Check size={size} color={colorTrue} {...iconProps} />;
548
- return <X size={size} color={colorFalse} {...iconProps} />;
549
- };
550
-
551
- const showCondition = () => {
552
- const newItem = (data && data._rh_plus_ag_grid_new_item) || false;
553
- return !newItem && (showOnGroup || !!data) && visibleResult;
554
- };
555
-
556
- if (!showCondition()) return null;
557
-
558
- return (
559
- <span
560
- style={{
561
- width: '100%',
562
- display: 'flex',
563
- justifyContent: cellAlign,
564
- alignItems: 'center',
565
- height: '100%',
566
- }}
567
- >
568
- <Icon
569
- style={{
570
- display: 'inline-block',
571
- height: '100%',
572
- }}
573
- innerValue={value}
574
- visibleTrue={visibleTrue}
575
- visibleFalse={visibleFalse}
576
- />
577
- </span>
578
- );
579
- });
580
-
581
- // Use the function that returns React component
582
- const cellRenderer = (params) => {
583
- return <BooleanCellRenderer {...params} />;
584
- };
585
-
586
- this.#addPreparedColumn({
587
- cellRenderer,
588
- editable: this.#resolveEditable(editable),
589
- ...restProps,
590
- });
591
- return this;
592
- }
593
-
594
- // Řešení s izolovaným observer pro každý grid instance
595
- addPinnedActionButton({
596
- menuItems = [],
597
- onActionClick,
598
- onBeforeShow,
599
- ...restProps
600
- } = {}) {
601
- const ActionDropdownRenderer = (params) => {
602
- const [isRowHovered, setIsRowHovered] = React.useState(false);
603
- const [isDropdownVisible, setIsDropdownVisible] = React.useState(false);
604
- const [currentMenuItems, setCurrentMenuItems] = React.useState(menuItems);
605
-
606
- const isMountedRef = React.useRef(true);
607
- const rowIndexRef = React.useRef(null);
608
- const observerRef = React.useRef(null);
609
- const gridContainerRef = React.useRef(null);
610
- const currentHoveredRowRef = React.useRef(null);
611
- const mouseListenersSetupRef = React.useRef(false);
612
-
613
- React.useEffect(() => {
614
- return () => {
615
- isMountedRef.current = false;
616
- cleanup();
617
- };
618
- }, []);
619
-
620
- // Cleanup funkce
621
- const cleanup = () => {
622
- if (observerRef.current) {
623
- observerRef.current.disconnect();
624
- observerRef.current = null;
625
- }
626
-
627
- if (gridContainerRef.current && mouseListenersSetupRef.current) {
628
- gridContainerRef.current.removeEventListener(
629
- 'mousemove',
630
- handleMouseMove
631
- );
632
- gridContainerRef.current.removeEventListener(
633
- 'mouseleave',
634
- handleMouseLeave
635
- );
636
- // gridContainerRef.current.removeEventListener(
637
- // 'mouseenter',
638
- // handleMouseEnter
639
- // );
640
- mouseListenersSetupRef.current = false;
641
- }
642
- };
643
-
644
- // Získáme row index z params
645
- React.useEffect(() => {
646
- if (params.node && params.node.rowIndex !== undefined) {
647
- rowIndexRef.current = params.node.rowIndex.toString();
648
- } else if (params.rowIndex !== undefined) {
649
- rowIndexRef.current = params.rowIndex.toString();
650
- }
651
- }, [params.node, params.rowIndex]);
652
-
653
- // Mouse event handlers
654
- const handleMouseMove = React.useCallback((event) => {
655
- if (!isMountedRef.current) return;
656
-
657
- const target = event.target;
658
- if (!target) return;
659
-
660
- // Najdeme nejbližší AG-Grid row element
661
- let rowElement = target.closest('.ag-row');
662
-
663
- if (rowElement) {
664
- // Získáme row index z AG-Grid attributů
665
- const rowIndex =
666
- rowElement.getAttribute('row-index') ||
667
- rowElement.getAttribute('aria-rowindex') ||
668
- rowElement.dataset.rowIndex ||
669
- rowElement.getAttribute('data-ag-row-index');
670
-
671
- if (rowIndex !== null) {
672
- const normalizedRowIndex = rowIndex.toString();
673
-
674
- // KLÍČOVÁ ZMĚNA: Explicitně resetujeme předchozí řádek při změně
675
- if (normalizedRowIndex !== currentHoveredRowRef.current) {
676
- // Pokud jsme měli předchozí řádek, nastavíme mu hover false
677
- if (currentHoveredRowRef.current !== null) {
678
- notifyRowHoverChange(currentHoveredRowRef.current, false);
679
- }
680
-
681
- // Nastavíme nový řádek jako hovered
682
- currentHoveredRowRef.current = normalizedRowIndex;
683
- notifyRowHoverChange(normalizedRowIndex, true);
684
- }
685
- }
686
- } else {
687
- // Myš není nad žádným řádkem - resetujeme všechny
688
- if (currentHoveredRowRef.current !== null) {
689
- notifyRowHoverChange(currentHoveredRowRef.current, false);
690
- currentHoveredRowRef.current = null;
691
- }
692
- }
693
- }, []);
694
-
695
- const handleMouseLeave = React.useCallback((event) => {
696
- if (!isMountedRef.current) return;
697
-
698
- // Lepší detekce opuštění gridu
699
- const relatedTarget = event.relatedTarget;
700
-
701
- // Pokud myš opustí celý grid container NEBO jde mimo dokument
702
- if (
703
- gridContainerRef.current &&
704
- (!relatedTarget || !gridContainerRef.current.contains(relatedTarget))
705
- ) {
706
- if (currentHoveredRowRef.current !== null) {
707
- notifyRowHoverChange(currentHoveredRowRef.current, false);
708
- currentHoveredRowRef.current = null;
709
- }
710
- }
711
- }, []);
712
-
713
- // Notifikace o změně hover stavu pro tento specifický grid
714
- const notifyRowHoverChange = (rowIndex, isHovered) => {
715
- if (!isMountedRef.current) return;
716
-
717
- // EXPLICITNÍ ŘÍZENÍ: Pokud nastavujeme hover false, resetujeme stav pro všechny řádky
718
- if (!isHovered && rowIndexRef.current) {
719
- setIsRowHovered(false);
720
- }
721
-
722
- // Pokud je toto náš řádek a nastavujeme hover true, aktualizujeme stav
723
- if (isHovered && rowIndexRef.current === rowIndex) {
724
- setIsRowHovered(true);
725
- }
726
- };
727
-
728
- // Nastavení observer pro tento konkrétní grid
729
- const setupGridObserver = React.useCallback(() => {
730
- if (!params.api || observerRef.current || !isMountedRef.current) return;
731
-
732
- // Najdeme grid container pro tento konkrétní grid instance
733
- const findThisGridContainer = () => {
734
- if (params.eGridCell) {
735
- let current = params.eGridCell;
736
- while (current && current !== document.body) {
737
- if (
738
- current.classList &&
739
- (current.classList.contains('ag-root-wrapper') ||
740
- current.classList.contains('ag-theme-alpine') ||
741
- current.classList.contains('ag-theme-balham') ||
742
- current.classList.contains('ag-theme-material') ||
743
- current.classList.contains('ag-theme-fresh') ||
744
- current.classList.contains('ag-theme-dark') ||
745
- current.classList.contains('ag-theme-blue') ||
746
- current.classList.contains('ag-theme-bootstrap'))
747
- ) {
748
- return current;
749
- }
750
- current = current.parentElement;
751
- }
752
- }
753
- return null;
754
- };
755
-
756
- const gridContainer = findThisGridContainer();
757
- if (!gridContainer) {
758
- // Zkusíme znovu za chvíli
759
- setTimeout(setupGridObserver, 100);
760
- return;
761
- }
762
-
763
- gridContainerRef.current = gridContainer;
764
-
765
- // Přidáme mouse event listenery pouze pokud ještě nejsou
766
- if (!mouseListenersSetupRef.current) {
767
- gridContainer.addEventListener('mousemove', handleMouseMove, {
768
- passive: true,
769
- });
770
- gridContainer.addEventListener('mouseleave', handleMouseLeave, {
771
- passive: true,
772
- });
773
-
774
- // DŮLEŽITÉ: Přidáme také mouseenter pro reset při vstupu do gridu
775
- const handleMouseEnter = () => {
776
- if (isMountedRef.current) {
777
- // Reset stavu při vstupu do gridu
778
- currentHoveredRowRef.current = null;
779
- setIsRowHovered(false);
780
- }
781
- };
782
- gridContainer.addEventListener('mouseenter', handleMouseEnter, {
783
- passive: true,
784
- });
785
-
786
- mouseListenersSetupRef.current = true;
787
- }
788
-
789
- // Nastavíme MutationObserver pro tento grid
790
- observerRef.current = new MutationObserver((mutations) => {
791
- if (!isMountedRef.current) return;
792
-
793
- let shouldRecalculate = false;
794
-
795
- mutations.forEach((mutation) => {
796
- if (mutation.type === 'childList') {
797
- const addedRows = Array.from(mutation.addedNodes).some(
798
- (node) =>
799
- node.nodeType === Node.ELEMENT_NODE &&
800
- (node.classList?.contains('ag-row') ||
801
- node.querySelector?.('.ag-row'))
802
- );
803
-
804
- const removedRows = Array.from(mutation.removedNodes).some(
805
- (node) =>
806
- node.nodeType === Node.ELEMENT_NODE &&
807
- (node.classList?.contains('ag-row') ||
808
- node.querySelector?.('.ag-row'))
809
- );
810
-
811
- if (addedRows || removedRows) {
812
- shouldRecalculate = true;
813
- }
814
- }
815
- });
816
-
817
- if (shouldRecalculate) {
818
- // Reset hover stavu
819
- currentHoveredRowRef.current = null;
820
- setIsRowHovered(false);
821
-
822
- // Znovu detekujeme aktuální pozici myši po krátké době
823
- setTimeout(() => {
824
- if (isMountedRef.current && gridContainerRef.current) {
825
- const rect = gridContainerRef.current.getBoundingClientRect();
826
- const mouseEvent = new MouseEvent('mousemove', {
827
- clientX: window.lastMouseX || rect.left + rect.width / 2,
828
- clientY: window.lastMouseY || rect.top + rect.height / 2,
829
- bubbles: true,
830
- });
831
- gridContainerRef.current.dispatchEvent(mouseEvent);
832
- }
833
- }, 100);
834
- }
835
- });
836
-
837
- // Spustíme observer na grid container
838
- observerRef.current.observe(gridContainer, {
839
- childList: true,
840
- subtree: true,
841
- attributes: false,
842
- });
843
-
844
- // Sledujeme pozici myši globálně pro tento observer
845
- const trackMouse = (e) => {
846
- window.lastMouseX = e.clientX;
847
- window.lastMouseY = e.clientY;
848
- };
849
-
850
- document.addEventListener('mousemove', trackMouse, { passive: true });
851
-
852
- // Cleanup tracking při unmount
853
- return () => {
854
- document.removeEventListener('mousemove', trackMouse);
855
- };
856
- }, [params.api, params.eGridCell, handleMouseMove, handleMouseLeave]);
857
-
858
- // Spustíme setup při mount a při změně params
859
- React.useEffect(() => {
860
- setupGridObserver();
861
- }, [setupGridObserver]);
862
-
863
- const handleVisibleChange = React.useCallback(
864
- async (visible) => {
865
- if (!isMountedRef.current) return;
866
-
867
- if (visible && onBeforeShow) {
868
- try {
869
- const updatedMenuItems = await onBeforeShow(params, menuItems);
870
- if (
871
- updatedMenuItems &&
872
- Array.isArray(updatedMenuItems) &&
873
- isMountedRef.current
874
- ) {
875
- setCurrentMenuItems(updatedMenuItems);
876
- }
877
- } catch (error) {
878
- // Removed console.error for production
879
- if (isMountedRef.current) {
880
- setCurrentMenuItems(menuItems);
881
- }
882
- }
883
- }
884
-
885
- if (isMountedRef.current) {
886
- setIsDropdownVisible(visible);
887
- }
888
- },
889
- [params, menuItems, onBeforeShow]
890
- );
891
-
892
- React.useEffect(() => {
893
- if (isMountedRef.current) {
894
- setCurrentMenuItems(menuItems);
895
- }
896
- }, [menuItems]);
897
-
898
- const menu = React.useMemo(
899
- () => (
900
- <Menu
901
- onClick={(e) => {
902
- if (onActionClick) {
903
- const item = currentMenuItems.filter(f => f.key === e.key)[0];
904
- onActionClick(e.key, params, item);
905
- }
906
- setIsDropdownVisible(false);
907
- }}
908
- >
909
- {currentMenuItems.map((item) => {
910
- if (item.type === 'divider') {
911
- return <Divider key={item.key} style={{ margin: '4px 0' }} />;
912
- }
913
-
914
- let isDisabled = false;
915
- if (typeof item.disable === 'function') {
916
- isDisabled = item.disable(params);
917
- } else if (typeof item.disable === 'boolean') {
918
- isDisabled = item.disable;
919
- } else if (item.disabled !== undefined) {
920
- isDisabled = item.disabled;
921
- }
922
-
923
- return (
924
- <Menu.Item key={item.key} disabled={isDisabled}>
925
- {item.label}
926
- </Menu.Item>
927
- );
928
- })}
929
- </Menu>
930
- ),
931
- [currentMenuItems, onActionClick, params]
932
- );
933
-
934
- const containerStyle = {
935
- width: '32px',
936
- height: '32px',
937
- display: 'flex',
938
- alignItems: 'center',
939
- justifyContent: 'center',
940
- position: 'relative',
941
- };
942
-
943
- const buttonStyle = {
944
- minWidth: 'auto',
945
- width: '32px',
946
- height: '32px',
947
- padding: '0',
948
- background: 'transparent',
949
- border: 'none',
950
- cursor: 'pointer',
951
- display: 'flex',
952
- alignItems: 'center',
953
- justifyContent: 'center',
954
- opacity: isRowHovered || isDropdownVisible ? 1 : 0,
955
- visibility: isRowHovered || isDropdownVisible ? 'visible' : 'hidden',
956
- transition: 'opacity 0.15s ease-in-out, visibility 0.15s ease-in-out',
957
- pointerEvents: isRowHovered || isDropdownVisible ? 'auto' : 'none',
958
- };
959
-
960
- return (
961
- <div style={containerStyle}>
962
- <Dropdown
963
- overlayStyle={{ zIndex: 5000 }}
964
- overlay={menu}
965
- trigger={['click']}
966
- visible={isDropdownVisible}
967
- onVisibleChange={handleVisibleChange}
968
- getPopupContainer={() => document.body}
969
- >
970
- <div
971
- style={buttonStyle}
972
- onClick={(e) => {
973
- e.stopPropagation();
974
- }}
975
- >
976
- <ActionIcon />
977
- </div>
978
- </Dropdown>
979
- </div>
980
- );
981
- };
982
-
983
- this.#addPreparedColumn({
984
- headerName: '',
985
- pinned: 'right',
986
- maxWidth: 40,
987
- minWidth: 40,
988
- suppressSizeToFit: true,
989
- suppressMenu: true,
990
- sortable: false,
991
- filter: false,
992
- resizable: false,
993
- cellRenderer: ActionDropdownRenderer,
994
- cellClass: 'action-button-cell-observer',
995
- ...restProps,
996
- });
997
-
998
- return this;
999
- }
1000
-
1001
- addButtonColumn({
1002
- buttons,
1003
- visibleGetter = () => true,
1004
- editable,
1005
- ...restProps
1006
- }) {
1007
- const buttonRendererParams = {
1008
- buttons,
1009
- visibleGetter,
1010
- };
1011
- this.#addPreparedColumn({
1012
- buttonRenderer: true,
1013
- buttonRendererParams,
1014
- editable: this.#resolveEditable(editable),
1015
- ...restProps,
1016
- });
1017
- return this;
1018
- }
1019
-
1020
- // Pomocná metoda pro správné nastavení editable vlastnosti
1021
- #resolveEditable(editable) {
1022
- if (!editable) return false;
1023
- return editable;
1024
- }
1025
-
1026
- build() {
1027
- return this.columns;
1028
- }
1029
- }
1030
-
1031
- export default ColumnBuilder;
1
+ /* eslint-disable */
2
+ import * as React from 'react';
3
+ import Menu from 'antd/es/menu';
4
+ import Dropdown from 'antd/es/dropdown';
5
+ import Button from 'antd/es/button';
6
+ import Divider from 'antd/es/divider';
7
+ import { prepareCellStyle, prepareColDef, getValueByPath } from './functions';
8
+ import { currencySymbols } from './enums';
9
+ import { ActionIcon } from './Icons';
10
+ import { CircleHelp, Check, X } from 'lucide-react';
11
+
12
+ /**
13
+ * Custom React hook pro zpracování interakcí s link buňkou v AG Grid.
14
+ * Obsluhuje hover efekty, kliky a přepínání overview módu.
15
+ * @param {Object} params - AG Grid parametry buňky
16
+ * @param {Function} onClick - Callback funkce při kliku na link
17
+ * @param {Object|Function} linkStyle - CSS styl pro link nebo funkce vracející styl
18
+ * @param {Object|Function} hoverStyle - CSS styl pro hover stav nebo funkce vracející styl
19
+ * @param {boolean} overviewToggle - Příznak zda aktivovat overview toggle funkčnost
20
+ * @returns {Object} Objekt s handlry a styly pro link buňku
21
+ */
22
+ const useLinkCellRenderer = (
23
+ params,
24
+ onClick,
25
+ linkStyle,
26
+ hoverStyle,
27
+ overviewToggle
28
+ ) => {
29
+ const [isHovered, setIsHovered] = React.useState(false);
30
+
31
+ const handleMouseEnter = React.useCallback(() => {
32
+ setIsHovered(true);
33
+ }, []);
34
+
35
+ const handleMouseLeave = React.useCallback(() => {
36
+ setIsHovered(false);
37
+ }, []);
38
+
39
+ const handleClick = React.useCallback(
40
+ (event) => {
41
+ event.stopPropagation();
42
+
43
+ if (overviewToggle && params.api) {
44
+ params.api.dispatchEvent({
45
+ type: 'overviewToggle',
46
+ data: params.data,
47
+ params,
48
+ });
49
+ }
50
+
51
+ if (onClick) {
52
+ onClick(params);
53
+ }
54
+ },
55
+ [onClick, overviewToggle, params]
56
+ );
57
+
58
+ // Přesunuto do useMemo, aby se vytvořilo pouze jednou
59
+ const defaultLinkStyle = React.useMemo(
60
+ () => ({
61
+ color: '#1a73e8',
62
+ textDecoration: 'none',
63
+ cursor: 'pointer',
64
+ }),
65
+ []
66
+ );
67
+
68
+ // Přesunuto do useMemo, aby se vytvořilo pouze jednou
69
+ const defaultHoverStyle = React.useMemo(
70
+ () => ({
71
+ textDecoration: 'underline',
72
+ }),
73
+ []
74
+ );
75
+
76
+ const computedLinkStyle = React.useMemo(
77
+ () => ({
78
+ ...defaultLinkStyle,
79
+ ...(typeof linkStyle === 'function'
80
+ ? linkStyle(params)
81
+ : linkStyle || {}),
82
+ }),
83
+ [linkStyle, params, defaultLinkStyle]
84
+ );
85
+
86
+ const computedHoverStyle = React.useMemo(
87
+ () => ({
88
+ ...defaultLinkStyle,
89
+ ...computedLinkStyle,
90
+ ...defaultHoverStyle,
91
+ ...(typeof hoverStyle === 'function'
92
+ ? hoverStyle(params)
93
+ : hoverStyle || {}),
94
+ }),
95
+ [computedLinkStyle, hoverStyle, params, defaultLinkStyle, defaultHoverStyle]
96
+ );
97
+
98
+ return {
99
+ isHovered,
100
+ handleMouseEnter,
101
+ handleMouseLeave,
102
+ handleClick,
103
+ computedLinkStyle,
104
+ computedHoverStyle,
105
+ };
106
+ };
107
+
108
+ /**
109
+ * Custom React hook pro zpracování interakcí s object buňkou v AG Grid.
110
+ * Poskytuje funkcionalitu pro editovatelné objekty s hover efekty a edit tlačítkem.
111
+ * @param {Object} params - AG Grid parametry buňky
112
+ * @param {boolean} editable - Příznak zda je buňka editovatelná
113
+ * @param {Function} onEditClick - Callback funkce při kliku na edit tlačítko
114
+ * @returns {Object} Objekt s handlry pro object buňku
115
+ */
116
+ const useObjectCellRenderer = (params, editable, onEditClick) => {
117
+ const [isHovered, setIsHovered] = React.useState(false);
118
+
119
+ const handleMouseEnter = React.useCallback(() => {
120
+ setIsHovered(true);
121
+ }, []);
122
+
123
+ const handleMouseLeave = React.useCallback(() => {
124
+ setIsHovered(false);
125
+ }, []);
126
+
127
+ const handleEditClick = React.useCallback(
128
+ (event) => {
129
+ event.stopPropagation();
130
+ if (onEditClick) {
131
+ onEditClick(params);
132
+ }
133
+ },
134
+ [onEditClick, params]
135
+ );
136
+
137
+ return {
138
+ isHovered,
139
+ handleMouseEnter,
140
+ handleMouseLeave,
141
+ handleEditClick,
142
+ };
143
+ };
144
+
145
+ /**
146
+ * Custom React hook pro detekci hover stavu řádku v AG Grid.
147
+ * Používá MutationObserver pro sledování CSS tříd řádků a detekci hover stavu.
148
+ * Tento hook se používá pro akční tlačítka, která se zobrazují pouze při hoveru nad řádkem.
149
+ * @param {Object} params - AG Grid parametry buňky obsahující informace o řádku
150
+ * @returns {Object} Objekt s boolean příznakem isRowHovered
151
+ */
152
+ const useRowHoverActionRenderer = (params) => {
153
+ const [isRowHovered, setIsRowHovered] = React.useState(false);
154
+ const rowRef = React.useRef(null);
155
+
156
+ React.useEffect(() => {
157
+ if (!params.node) return undefined;
158
+
159
+ // Funkce pro kontrolu, zda je řádek v hover stavu
160
+ const checkRowHoverState = () => {
161
+ // Najdeme DOM element řádku pomocí rowIndex
162
+ const rowElement = document.querySelector(
163
+ `.ag-row[row-index="${params.rowIndex}"]`
164
+ );
165
+ if (rowElement) {
166
+ rowRef.current = rowElement;
167
+ // AG-Grid automaticky přidává třídu ag-row-hover k řádku při najetí myší
168
+ const isHovered = rowElement.classList.contains('ag-row-hover');
169
+ setIsRowHovered(isHovered);
170
+ }
171
+ };
172
+
173
+ // Vytvoříme MutationObserver pro sledování změn tříd na řádku
174
+ const observer = new MutationObserver((mutations) => {
175
+ mutations.forEach((mutation) => {
176
+ if (
177
+ mutation.type === 'attributes' &&
178
+ mutation.attributeName === 'class'
179
+ ) {
180
+ checkRowHoverState();
181
+ }
182
+ });
183
+ });
184
+
185
+ // Inicializujeme stav
186
+ checkRowHoverState();
187
+
188
+ // Přidáme observer k element řádku, pokud existuje
189
+ if (rowRef.current) {
190
+ observer.observe(rowRef.current, { attributes: true });
191
+ }
192
+
193
+ // Přidáme event listeners na grid container pro sledování pohybu myši
194
+ const gridContainer = document.querySelector('.ag-root-wrapper');
195
+ if (gridContainer) {
196
+ gridContainer.addEventListener('mouseover', checkRowHoverState);
197
+ gridContainer.addEventListener('mouseout', checkRowHoverState);
198
+ }
199
+
200
+ // Cleanup při odmontování komponenty
201
+ return () => {
202
+ if (rowRef.current) {
203
+ observer.disconnect();
204
+ }
205
+ if (gridContainer) {
206
+ gridContainer.removeEventListener('mouseover', checkRowHoverState);
207
+ gridContainer.removeEventListener('mouseout', checkRowHoverState);
208
+ }
209
+ };
210
+ }, [params.node, params.rowIndex]);
211
+
212
+ return {
213
+ isRowHovered,
214
+ };
215
+ };
216
+
217
+ /**
218
+ * ColumnBuilder třída pro builder pattern vytváření sloupců AG Grid.
219
+ * Poskytuje fluent API pro snadné a čitelné definování různých typů sloupců.
220
+ * Podporuje lokalizaci přes react-intl a automatickou konfiguraci sloupců.
221
+ */
222
+ class ColumnBuilder {
223
+ /**
224
+ * Privátní metoda pro přidání připraveného sloupce do kolekce.
225
+ * Aplikuje standardní konfiguraci sloupce včetně lokalizace.
226
+ * @param {Object} props - Konfigurace sloupce
227
+ */
228
+ #addPreparedColumn(props) {
229
+ this.columns.push(prepareColDef(this.intl, props));
230
+ }
231
+
232
+ /**
233
+ * Konstruktor ColumnBuilder třídy.
234
+ * @param {Object} intl - React-intl objekt pro lokalizaci
235
+ */
236
+ constructor(intl) {
237
+ this.columns = [];
238
+ this.intl = intl;
239
+ }
240
+
241
+ /**
242
+ * Přidá základní sloupec do AG Grid s podporou barevného pole.
243
+ * Umožňuje dynamické obarvení textu buňky na základě hodnoty z jiného pole.
244
+ * @param {Object} config - Konfigurace sloupce
245
+ * @param {string} config.colorField - Název pole obsahujícího barvu pro text buňky
246
+ * @param {string} config.bgColorField - Název pole obsahujícího barvu pozadí buňky
247
+ * @param {Object} ...props - Další AG Grid vlastnosti sloupce
248
+ * @returns {ColumnBuilder} Instance pro chaining
249
+ */
250
+ addColumn({ colorField, bgColorField, ...props }) {
251
+ // Získáme původní cellStyle, pokud existuje
252
+ const originalCellStyle = props.cellStyle;
253
+ const { cellAlign, ...restProps } = props;
254
+
255
+ // Pokud je zadáno colorField nebo bgColorField, vytvoříme novou funkci pro cellStyle
256
+ if (colorField || bgColorField) {
257
+ const overrideCellStyle = (params) => {
258
+ // Získáme základní styl
259
+ let baseStyle = {};
260
+ if (originalCellStyle) {
261
+ if (typeof originalCellStyle === 'function') {
262
+ baseStyle = originalCellStyle(params);
263
+ } else {
264
+ baseStyle = originalCellStyle;
265
+ }
266
+ }
267
+
268
+ // Získáme hodnotu barvy textu z dat
269
+ const colorValue =
270
+ colorField && params.data ? getValueByPath(params.data, colorField) : null;
271
+
272
+ // Získáme hodnotu barvy pozadí z dat
273
+ const bgColorValue =
274
+ bgColorField && params.data ? getValueByPath(params.data, bgColorField) : null;
275
+
276
+ const restStyle = prepareCellStyle(props);
277
+
278
+ // Vrátíme kombinovaný styl s barvou textu i pozadí
279
+ const newStyle = { ...baseStyle, ...restStyle };
280
+ if (colorValue) {
281
+ newStyle.color = colorValue;
282
+ }
283
+ if (bgColorValue) {
284
+ newStyle.backgroundColor = bgColorValue;
285
+ }
286
+
287
+ return newStyle;
288
+ };
289
+
290
+ // Předáme upravené props do addPreparedColumn
291
+ this.#addPreparedColumn({ ...restProps, overrideCellStyle });
292
+ } else {
293
+ // Pokud není colorField ani bgColorField zadán, použijeme původní implementaci
294
+ this.#addPreparedColumn(props);
295
+ }
296
+
297
+ return this;
298
+ }
299
+
300
+ /**
301
+ * Přidá sloupec s kartou faktury optimalizovaný pro overview mód.
302
+ * Zobrazuje fakturační informace v kartovém formátu s podporou checkboxu.
303
+ * @param {Object} config - Konfigurace sloupce
304
+ * @param {string} [config.field='invoice'] - Název datového pole
305
+ * @param {string} [config.headerName=''] - Název hlavičky sloupce
306
+ * @param {number} [config.width=340] - Šířka sloupce
307
+ * @param {Function} config.onCheckboxChange - Callback při změně stavu checkboxu
308
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
309
+ * @returns {ColumnBuilder} Instance pro chaining
310
+ */
311
+ addInvoiceCardColumn({
312
+ field = 'invoice',
313
+ headerName = '',
314
+ width = 340,
315
+ onCheckboxChange,
316
+ ...restProps
317
+ } = {}) {
318
+ const cellRenderer = 'invoiceCardRenderer';
319
+
320
+ this.#addPreparedColumn({
321
+ headerName,
322
+ field,
323
+ width,
324
+ cellRenderer,
325
+ suppressSizeToFit: true,
326
+ cellRendererParams: {
327
+ onCheckboxChange:
328
+ onCheckboxChange ||
329
+ (() => {
330
+ // Funkce bez console.log
331
+ }),
332
+ },
333
+ ...restProps,
334
+ });
335
+
336
+ return this;
337
+ }
338
+
339
+ /**
340
+ * Přidá sloupec pro zobrazení měny s podporou různých formátů a pozicování symbolu.
341
+ * Automaticky formátuje hodnoty jako měnu s dvěma desetinnými místy.
342
+ * @param {Object} config - Konfigurace měnového sloupce
343
+ * @param {string|Function} config.currency - Kód měny nebo funkce vracející kód měny
344
+ * @param {string} [config.position] - Pozice symbolu měny ('left' nebo 'right')
345
+ * @param {Object|Function} config.currencyStyle - CSS styl pro symbol měny
346
+ * @param {string} [config.cellAlign='right'] - Zarovnání obsahu buňky
347
+ * @param {boolean} config.editable - Zda je sloupec editovatelný
348
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
349
+ * @returns {ColumnBuilder} Instance pro chaining
350
+ */
351
+ addCurrencyColumn({
352
+ currency,
353
+ position,
354
+ currencyStyle,
355
+ cellAlign = 'right',
356
+ editable,
357
+ ...restProps
358
+ }) {
359
+ const cellRenderer = (params) => {
360
+ const currencyValue =
361
+ typeof currency === 'function' ? currency(params) : currency;
362
+ const symbol = currencySymbols[currencyValue] || currencyValue;
363
+
364
+ // Použití Number.isNaN místo globální isNaN
365
+ const value =
366
+ params.value != null && !Number.isNaN(Number(params.value))
367
+ ? params.value.toFixed(2)
368
+ : '0.00';
369
+
370
+ const style =
371
+ typeof currencyStyle === 'function'
372
+ ? currencyStyle(params)
373
+ : currencyStyle;
374
+
375
+ return (
376
+ <span>
377
+ {position === 'left' ? (
378
+ <>
379
+ <span style={style}>{symbol}</span> {value}
380
+ </>
381
+ ) : (
382
+ <>
383
+ {value} <span style={style}>{symbol}</span>
384
+ </>
385
+ )}
386
+ </span>
387
+ );
388
+ };
389
+
390
+ this.#addPreparedColumn({
391
+ cellAlign,
392
+ cellRenderer,
393
+ currency,
394
+ editable: this.#resolveEditable(editable),
395
+ ...restProps,
396
+ });
397
+ return this;
398
+ }
399
+
400
+ /**
401
+ * Přidá sloupec pro zobrazení data s možností specifikace formátu.
402
+ * Používá standardní AG Grid date renderer s českým formátováním.
403
+ * @param {Object} config - Konfigurace datového sloupce
404
+ * @param {string} [config.dateFormat='DD.MM.YYYY'] - Formát zobrazení data
405
+ * @param {boolean} config.editable - Zda je sloupec editovatelný
406
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
407
+ * @returns {ColumnBuilder} Instance pro chaining
408
+ */
409
+ addDateColumn({ dateFormat = 'DD.MM.YYYY', editable, ...restProps }) {
410
+ this.#addPreparedColumn({
411
+ dateRenderer: dateFormat,
412
+ editable: this.#resolveEditable(editable),
413
+ ...restProps,
414
+ });
415
+ return this;
416
+ }
417
+
418
+ /**
419
+ * Přidá sloupec s klikatelným linkem podporujícím hover efekty.
420
+ * Link může spouštět akce nebo přepínat overview mód v gridu.
421
+ * @param {Object} config - Konfigurace linkového sloupce
422
+ * @param {Function} config.onClick - Callback funkce při kliku na link
423
+ * @param {Object|Function} config.linkStyle - CSS styl pro link nebo funkce vracející styl
424
+ * @param {Object|Function} config.hoverStyle - CSS styl pro hover stav
425
+ * @param {string} [config.cellAlign='left'] - Zarovnání obsahu buňky
426
+ * @param {boolean} config.editable - Zda je sloupec editovatelný
427
+ * @param {boolean} [config.overviewToggle=false] - Zda aktivovat overview toggle
428
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
429
+ * @returns {ColumnBuilder} Instance pro chaining
430
+ */
431
+ addLinkColumn({
432
+ onClick,
433
+ linkStyle,
434
+ hoverStyle,
435
+ cellAlign = 'left',
436
+ editable,
437
+ overviewToggle = false, // Přidán výchozí parametr pro overview
438
+ ...restProps
439
+ }) {
440
+ // Vytvořím komponentu pro cell renderer, která používá hook
441
+ const LinkCellRenderer = React.memo((params) => {
442
+ const {
443
+ isHovered,
444
+ handleMouseEnter,
445
+ handleMouseLeave,
446
+ handleClick,
447
+ computedLinkStyle,
448
+ computedHoverStyle,
449
+ } = useLinkCellRenderer(
450
+ params,
451
+ onClick,
452
+ linkStyle,
453
+ hoverStyle,
454
+ overviewToggle
455
+ );
456
+
457
+ return (
458
+ <div
459
+ className="link-cell-container"
460
+ style={{
461
+ width: '100%',
462
+ height: '100%',
463
+ display: 'flex',
464
+ alignItems: 'center',
465
+ }}
466
+ >
467
+ <span
468
+ onClick={handleClick}
469
+ onMouseEnter={handleMouseEnter}
470
+ onMouseLeave={handleMouseLeave}
471
+ style={isHovered ? computedHoverStyle : computedLinkStyle}
472
+ role="button"
473
+ tabIndex={0}
474
+ onKeyPress={(event) => {
475
+ if (event.key === 'Enter' || event.key === ' ') {
476
+ handleClick(event);
477
+ }
478
+ }}
479
+ >
480
+ {params.value}
481
+ </span>
482
+ </div>
483
+ );
484
+ });
485
+
486
+ // Používáme funkci, která vrátí React komponentu
487
+ const cellRenderer = (params) => {
488
+ return <LinkCellRenderer {...params} />;
489
+ };
490
+
491
+ this.#addPreparedColumn({
492
+ cellAlign,
493
+ cellRenderer,
494
+ editable: this.#resolveEditable(editable),
495
+ overviewToggle, // Předání příznaku dál
496
+ ...restProps,
497
+ });
498
+ return this;
499
+ }
500
+
501
+ /**
502
+ * Helper metoda pro přidání overview link sloupce s přednastavenými hodnotami.
503
+ * Specializovaný link sloupec pro přepínání overview módu v gridu.
504
+ * @param {Object} config - Konfigurace overview link sloupce
505
+ * @param {string} [config.field='documentNumber'] - Název datového pole
506
+ * @param {string} [config.headerName=''] - Název hlavičky sloupce
507
+ * @param {number} [config.width=130] - Šířka sloupce
508
+ * @param {string} [config.cellAlign='left'] - Zarovnání obsahu buňky
509
+ * @param {Object|Function} config.linkStyle - CSS styl pro link
510
+ * @param {Object|Function} config.hoverStyle - CSS styl pro hover stav
511
+ * @param {boolean} [config.editable=false] - Zda je sloupec editovatelný
512
+ * @param {Object} ...rest - Další AG Grid vlastnosti sloupce včetně intlId
513
+ * @returns {ColumnBuilder} Instance pro chaining
514
+ */
515
+ addOverviewLinkColumn({
516
+ field = 'documentNumber',
517
+ headerName = '',
518
+ width = 130,
519
+ cellAlign = 'left',
520
+ linkStyle,
521
+ hoverStyle,
522
+ editable = false,
523
+ ...rest
524
+ } = {}) {
525
+ return this.addLinkColumn({
526
+ field,
527
+ headerName: this?.intl?.formatMessage({ id: rest?.intlId }) || headerName,
528
+ width,
529
+ cellAlign,
530
+ linkStyle,
531
+ hoverStyle,
532
+ editable,
533
+ overviewToggle: true,
534
+ ...rest,
535
+ });
536
+ }
537
+
538
+ /**
539
+ * Přidá sloupec pro zobrazení objektů s možností editace a tooltip podporou.
540
+ * Při hoveru zobrazuje edit tlačítko pro editovatelné objekty.
541
+ * @param {Object} config - Konfigurace objektového sloupce
542
+ * @param {boolean} config.contentTooltip - Zda zobrazovat tooltip s obsahem
543
+ * @param {string} config.tooltipField - Pole obsahující text pro tooltip
544
+ * @param {boolean} config.tooltipInteraction - Zda povolit interakci s tooltipem
545
+ * @param {number} [config.tooltipShowDelay=100] - Zpoždění zobrazení tooltipu v ms
546
+ * @param {boolean} config.editable - Zda je objekt editovatelný
547
+ * @param {Function} config.onEditClick - Callback při kliku na edit tlačítko
548
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
549
+ * @returns {ColumnBuilder} Instance pro chaining
550
+ */
551
+ addObjectColumn({
552
+ contentTooltip,
553
+ tooltipField,
554
+ tooltipInteraction,
555
+ tooltipShowDelay = 100,
556
+ editable,
557
+ onEditClick,
558
+ ...restProps
559
+ }) {
560
+ // Vytvořím komponentu pro cell renderer, která používá hook
561
+ const ObjectCellRenderer = React.memo((params) => {
562
+ const { isHovered, handleMouseEnter, handleMouseLeave, handleEditClick } =
563
+ useObjectCellRenderer(params, editable, onEditClick);
564
+
565
+ return (
566
+ <div
567
+ className="object-cell-container"
568
+ style={{
569
+ position: 'relative',
570
+ width: '100%',
571
+ height: '100%',
572
+ display: 'flex',
573
+ alignItems: 'center',
574
+ }}
575
+ onMouseEnter={handleMouseEnter}
576
+ onMouseLeave={handleMouseLeave}
577
+ >
578
+ <div style={{ flexGrow: 1 }}>{params.value}</div>
579
+ {isHovered && editable && (
580
+ <button
581
+ className="edit-object-button"
582
+ style={{
583
+ position: 'absolute',
584
+ right: '4px',
585
+ background: 'transparent',
586
+ border: 'none',
587
+ cursor: 'pointer',
588
+ padding: '4px',
589
+ display: 'flex',
590
+ alignItems: 'center',
591
+ justifyContent: 'center',
592
+ }}
593
+ onClick={handleEditClick}
594
+ title="Upravit"
595
+ >
596
+ <span style={{ fontSize: '16px' }}>⋮</span>
597
+ </button>
598
+ )}
599
+ </div>
600
+ );
601
+ });
602
+
603
+ // Používáme funkci, která vrátí React komponentu
604
+ const cellRenderer = (params) => {
605
+ return <ObjectCellRenderer {...params} />;
606
+ };
607
+
608
+ this.#addPreparedColumn({
609
+ contentTooltip,
610
+ tooltipField,
611
+ tooltipInteraction,
612
+ tooltipShowDelay,
613
+ cellRenderer,
614
+ editable: this.#resolveEditable(editable),
615
+ ...restProps,
616
+ });
617
+ return this;
618
+ }
619
+
620
+ /**
621
+ * Přidá sloupec pro zobrazení ikon s podporou badge (číselného označení).
622
+ * Umožňuje podmíněné zobrazování ikon a jejich stylování.
623
+ * @param {Object} config - Konfigurace ikonového sloupce
624
+ * @param {Function} [config.visibleGetter=() => true] - Funkce určující viditelnost ikony
625
+ * @param {string} config.cellAlign - Zarovnání ikony v buňce
626
+ * @param {string|Function} config.icon - Název ikony nebo funkce vracející ikonu
627
+ * @param {string|Function} [config.iconColor] - Barva ikony nebo funkce vracející barvu podle parametrů
628
+ * @param {number} [config.size=16] - Velikost ikony v pixelech
629
+ * @param {boolean} [config.iconLeft=false] - Zda zobrazit ikonu vlevo od textu
630
+ * @param {boolean} [config.iconRight=false] - Zda zobrazit ikonu vpravo od textu
631
+ * @param {boolean} [config.showText=true] - Zda zobrazit text (hodnotu pole)
632
+ * @param {number} [config.textGap=5] - Mezera mezi ikonou a textem v pixelech
633
+ * @param {boolean} [config.showOnGroup=false] - Zda zobrazit ikonu na skupinových řádcích
634
+ * @param {string|number|Function} config.badge - Text/číslo badge nebo funkce vracející badge
635
+ * @param {boolean} config.showZero - Zda zobrazit badge s hodnotou 0
636
+ * @param {string} [config.badgeColor='#dc3545'] - Barva pozadí badge
637
+ * @param {string} [config.badgeTextColor='white'] - Barva textu badge
638
+ * @param {string} [config.badgePosition='top-right'] - Pozice badge ('top-right', 'top-left', 'bottom-right', 'bottom-left')
639
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
640
+ * @returns {ColumnBuilder} Instance pro chaining
641
+ */
642
+ addIconColumn({
643
+ visibleGetter = () => true,
644
+ cellAlign,
645
+ icon,
646
+ iconColor,
647
+ size = 16,
648
+ iconLeft = false,
649
+ iconRight = false,
650
+ showText = true,
651
+ textGap = 5,
652
+ showOnGroup = false,
653
+ badge,
654
+ showZero,
655
+ badgeColor = '#dc3545',
656
+ badgeTextColor = 'white',
657
+ badgePosition = 'top-right',
658
+ ...restProps
659
+ }) {
660
+ const iconRendererParams = {
661
+ cellAlign,
662
+ visibleGetter,
663
+ icon,
664
+ iconColor,
665
+ size,
666
+ iconLeft,
667
+ iconRight,
668
+ showText,
669
+ textGap,
670
+ showOnGroup,
671
+ badge,
672
+ showZero,
673
+ badgeColor,
674
+ badgeTextColor,
675
+ badgePosition,
676
+ };
677
+ this.#addPreparedColumn({
678
+ iconRenderer: true,
679
+ iconRendererParams,
680
+ ...restProps,
681
+ });
682
+ return this;
683
+ }
684
+
685
+ /**
686
+ * Přidá sloupec pro zobrazování stavů s kruhovým pozadím.
687
+ * Zobrazuje text stavu v kruhovém pozadí s definovanou barvou.
688
+ * @param {Object} config - Konfigurace stavového sloupce
689
+ * @param {Function} [config.visibleGetter=() => true] - Funkce určující viditelnost
690
+ * @param {string} [config.cellAlign='center'] - Zarovnání v buňce
691
+ * @param {string|Function} [config.bgColor] - Barva pozadí nebo funkce vracející barvu podle parametrů
692
+ * @param {string|Function} [config.textColor='white'] - Barva textu nebo funkce vracející barvu
693
+ * @param {string|Function} [config.displayField] - Pole pro zobrazení textu (pokud se liší od field)
694
+ * @param {number} [config.minWidth=80] - Minimální šířka kruhového pozadí
695
+ * @param {number} [config.fontSize=12] - Velikost fontu
696
+ * @param {boolean} [config.showOnGroup=false] - Zda zobrazit na skupinových řádcích
697
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
698
+ * @returns {ColumnBuilder} Instance pro chaining
699
+ */
700
+ addStateColumn({
701
+ visibleGetter = () => true,
702
+ cellAlign = 'center',
703
+ bgColor,
704
+ textColor = 'white',
705
+ displayField,
706
+ minWidth = 80,
707
+ fontSize = 12,
708
+ showOnGroup = false,
709
+ ...restProps
710
+ }) {
711
+ const stateRendererParams = {
712
+ visibleGetter,
713
+ cellAlign,
714
+ bgColor,
715
+ textColor,
716
+ displayField,
717
+ minWidth,
718
+ fontSize,
719
+ showOnGroup,
720
+ };
721
+ this.#addPreparedColumn({
722
+ stateRenderer: true,
723
+ stateRendererParams,
724
+ ...restProps,
725
+ });
726
+ return this;
727
+ }
728
+
729
+ /**
730
+ * Přidá sloupec pro zobrazení boolean hodnot pomocí ikon (zaškrtnutí/křížek).
731
+ * Podporuje různé ikony pro true/false stav a podmíněnou viditelnost.
732
+ * @param {Object} config - Konfigurace boolean sloupce
733
+ * @param {boolean} [config.visibleFalse=true] - Zda zobrazit ikonu pro false hodnotu
734
+ * @param {boolean} [config.visibleTrue=true] - Zda zobrazit ikonu pro true hodnotu
735
+ * @param {string} [config.cellAlign='center'] - Zarovnání ikony v buňce
736
+ * @param {Function} [config.visibleGetter=() => true] - Funkce určující viditelnost ikony
737
+ * @param {boolean} [config.defaultIcon=true] - Zda zobrazit výchozí ikonu pro undefined hodnoty
738
+ * @param {string} [config.defaultIconColor='#898989'] - Barva výchozí ikony
739
+ * @param {number} [config.size=16] - Velikost ikony v pixelech
740
+ * @param {string} [config.colorTrue='green'] - Barva ikony pro true hodnotu
741
+ * @param {string} [config.colorFalse='red'] - Barva ikony pro false hodnotu
742
+ * @param {boolean} [config.showOnGroup=false] - Zda zobrazit ikonu na skupinových řádcích
743
+ * @param {boolean} config.editable - Zda je sloupec editovatelný
744
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
745
+ * @returns {ColumnBuilder} Instance pro chaining
746
+ */
747
+ addBooleanColumn({
748
+ visibleFalse = true,
749
+ visibleTrue = true,
750
+ cellAlign = 'center',
751
+ visibleGetter = () => true,
752
+ defaultIcon = true,
753
+ defaultIconColor = '#898989',
754
+ size = 16,
755
+ colorTrue = 'green',
756
+ colorFalse = 'red',
757
+ showOnGroup = false,
758
+ editable,
759
+ ...restProps
760
+ }) {
761
+ // Create a React component for boolean cell renderer
762
+ const BooleanCellRenderer = React.memo((params) => {
763
+ const { data, value } = params;
764
+
765
+ const visibleResult = visibleGetter ? visibleGetter(data) : true;
766
+
767
+ const Icon = ({ innerValue, ...iconProps }) => {
768
+ if (
769
+ innerValue === undefined ||
770
+ (!visibleFalse && !innerValue) ||
771
+ (!visibleTrue && innerValue)
772
+ ) {
773
+ if (defaultIcon)
774
+ return <CircleHelp size={size} color={defaultIconColor} {...iconProps} />;
775
+ return <div />;
776
+ }
777
+
778
+ if (innerValue)
779
+ return <Check size={size} color={colorTrue} {...iconProps} />;
780
+ return <X size={size} color={colorFalse} {...iconProps} />;
781
+ };
782
+
783
+ const showCondition = () => {
784
+ const newItem = (data && data._rh_plus_ag_grid_new_item) || false;
785
+ return !newItem && (showOnGroup || !!data) && visibleResult;
786
+ };
787
+
788
+ if (!showCondition()) return null;
789
+
790
+ return (
791
+ <span
792
+ style={{
793
+ width: '100%',
794
+ display: 'flex',
795
+ justifyContent: cellAlign,
796
+ alignItems: 'center',
797
+ height: '100%',
798
+ }}
799
+ >
800
+ <Icon
801
+ style={{
802
+ display: 'inline-block',
803
+ height: '100%',
804
+ }}
805
+ innerValue={value}
806
+ visibleTrue={visibleTrue}
807
+ visibleFalse={visibleFalse}
808
+ />
809
+ </span>
810
+ );
811
+ });
812
+
813
+ // Use the function that returns React component
814
+ const cellRenderer = (params) => {
815
+ return <BooleanCellRenderer {...params} />;
816
+ };
817
+
818
+ this.#addPreparedColumn({
819
+ cellRenderer,
820
+ editable: this.#resolveEditable(editable),
821
+ ...restProps,
822
+ });
823
+ return this;
824
+ }
825
+
826
+ /**
827
+ * Přidá připnutý akční sloupec s dropdown menu, který se zobrazuje pouze při hoveru nad řádkem.
828
+ * Používa pokročilou detekci hover stavu s MutationObserver pro každou instanci gridu zvlášť.
829
+ * Sloupec je automaticky připnut vpravo a obsahuje akční tlačítko s kontextovým menu.
830
+ * @param {Object} config - Konfigurace akčního sloupce
831
+ * @param {Array} [config.menuItems=[]] - Array položek menu s key, label, disable vlastnostmi
832
+ * @param {Function} config.onActionClick - Callback při kliku na položku menu (key, params, item)
833
+ * @param {Function} config.onBeforeShow - Async callback před zobrazením menu pro dynamické položky
834
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
835
+ * @returns {ColumnBuilder} Instance pro chaining
836
+ */
837
+ addPinnedActionButton({
838
+ menuItems = [],
839
+ onActionClick,
840
+ onBeforeShow,
841
+ ...restProps
842
+ } = {}) {
843
+ const ActionDropdownRenderer = (params) => {
844
+ const [isRowHovered, setIsRowHovered] = React.useState(false);
845
+ const [isDropdownVisible, setIsDropdownVisible] = React.useState(false);
846
+ const [currentMenuItems, setCurrentMenuItems] = React.useState(menuItems);
847
+
848
+ const isMountedRef = React.useRef(true);
849
+ const rowIndexRef = React.useRef(null);
850
+ const observerRef = React.useRef(null);
851
+ const gridContainerRef = React.useRef(null);
852
+ const currentHoveredRowRef = React.useRef(null);
853
+ const mouseListenersSetupRef = React.useRef(false);
854
+
855
+ React.useEffect(() => {
856
+ return () => {
857
+ isMountedRef.current = false;
858
+ cleanup();
859
+ };
860
+ }, []);
861
+
862
+ // Cleanup funkce
863
+ const cleanup = () => {
864
+ if (observerRef.current) {
865
+ observerRef.current.disconnect();
866
+ observerRef.current = null;
867
+ }
868
+
869
+ if (gridContainerRef.current && mouseListenersSetupRef.current) {
870
+ gridContainerRef.current.removeEventListener(
871
+ 'mousemove',
872
+ handleMouseMove
873
+ );
874
+ gridContainerRef.current.removeEventListener(
875
+ 'mouseleave',
876
+ handleMouseLeave
877
+ );
878
+ // gridContainerRef.current.removeEventListener(
879
+ // 'mouseenter',
880
+ // handleMouseEnter
881
+ // );
882
+ mouseListenersSetupRef.current = false;
883
+ }
884
+ };
885
+
886
+ // Získáme row index z params
887
+ React.useEffect(() => {
888
+ if (params.node && params.node.rowIndex !== undefined) {
889
+ rowIndexRef.current = params.node.rowIndex.toString();
890
+ } else if (params.rowIndex !== undefined) {
891
+ rowIndexRef.current = params.rowIndex.toString();
892
+ }
893
+ }, [params.node, params.rowIndex]);
894
+
895
+ // Mouse event handlers
896
+ const handleMouseMove = React.useCallback((event) => {
897
+ if (!isMountedRef.current) return;
898
+
899
+ const target = event.target;
900
+ if (!target) return;
901
+
902
+ // Najdeme nejbližší AG-Grid row element
903
+ let rowElement = target.closest('.ag-row');
904
+
905
+ if (rowElement) {
906
+ // Získáme row index z AG-Grid attributů
907
+ const rowIndex =
908
+ rowElement.getAttribute('row-index') ||
909
+ rowElement.getAttribute('aria-rowindex') ||
910
+ rowElement.dataset.rowIndex ||
911
+ rowElement.getAttribute('data-ag-row-index');
912
+
913
+ if (rowIndex !== null) {
914
+ const normalizedRowIndex = rowIndex.toString();
915
+
916
+ // KLÍČOVÁ ZMĚNA: Explicitně resetujeme předchozí řádek při změně
917
+ if (normalizedRowIndex !== currentHoveredRowRef.current) {
918
+ // Pokud jsme měli předchozí řádek, nastavíme mu hover false
919
+ if (currentHoveredRowRef.current !== null) {
920
+ notifyRowHoverChange(currentHoveredRowRef.current, false);
921
+ }
922
+
923
+ // Nastavíme nový řádek jako hovered
924
+ currentHoveredRowRef.current = normalizedRowIndex;
925
+ notifyRowHoverChange(normalizedRowIndex, true);
926
+ }
927
+ }
928
+ } else {
929
+ // Myš není nad žádným řádkem - resetujeme všechny
930
+ if (currentHoveredRowRef.current !== null) {
931
+ notifyRowHoverChange(currentHoveredRowRef.current, false);
932
+ currentHoveredRowRef.current = null;
933
+ }
934
+ }
935
+ }, []);
936
+
937
+ const handleMouseLeave = React.useCallback((event) => {
938
+ if (!isMountedRef.current) return;
939
+
940
+ // Lepší detekce opuštění gridu
941
+ const relatedTarget = event.relatedTarget;
942
+
943
+ // Pokud myš opustí celý grid container NEBO jde mimo dokument
944
+ if (
945
+ gridContainerRef.current &&
946
+ (!relatedTarget || !gridContainerRef.current.contains(relatedTarget))
947
+ ) {
948
+ if (currentHoveredRowRef.current !== null) {
949
+ notifyRowHoverChange(currentHoveredRowRef.current, false);
950
+ currentHoveredRowRef.current = null;
951
+ }
952
+ }
953
+ }, []);
954
+
955
+ // Notifikace o změně hover stavu pro tento specifický grid
956
+ const notifyRowHoverChange = (rowIndex, isHovered) => {
957
+ if (!isMountedRef.current) return;
958
+
959
+ // EXPLICITNÍ ŘÍZENÍ: Pokud nastavujeme hover false, resetujeme stav pro všechny řádky
960
+ if (!isHovered && rowIndexRef.current) {
961
+ setIsRowHovered(false);
962
+ }
963
+
964
+ // Pokud je toto náš řádek a nastavujeme hover true, aktualizujeme stav
965
+ if (isHovered && rowIndexRef.current === rowIndex) {
966
+ setIsRowHovered(true);
967
+ }
968
+ };
969
+
970
+ // Nastavení observer pro tento konkrétní grid
971
+ const setupGridObserver = React.useCallback(() => {
972
+ if (!params.api || observerRef.current || !isMountedRef.current) return;
973
+
974
+ // Najdeme grid container pro tento konkrétní grid instance
975
+ const findThisGridContainer = () => {
976
+ if (params.eGridCell) {
977
+ let current = params.eGridCell;
978
+ while (current && current !== document.body) {
979
+ if (
980
+ current.classList &&
981
+ (current.classList.contains('ag-root-wrapper') ||
982
+ current.classList.contains('ag-theme-alpine') ||
983
+ current.classList.contains('ag-theme-balham') ||
984
+ current.classList.contains('ag-theme-material') ||
985
+ current.classList.contains('ag-theme-fresh') ||
986
+ current.classList.contains('ag-theme-dark') ||
987
+ current.classList.contains('ag-theme-blue') ||
988
+ current.classList.contains('ag-theme-bootstrap'))
989
+ ) {
990
+ return current;
991
+ }
992
+ current = current.parentElement;
993
+ }
994
+ }
995
+ return null;
996
+ };
997
+
998
+ const gridContainer = findThisGridContainer();
999
+ if (!gridContainer) {
1000
+ // Zkusíme znovu za chvíli
1001
+ setTimeout(setupGridObserver, 100);
1002
+ return;
1003
+ }
1004
+
1005
+ gridContainerRef.current = gridContainer;
1006
+
1007
+ // Přidáme mouse event listenery pouze pokud ještě nejsou
1008
+ if (!mouseListenersSetupRef.current) {
1009
+ gridContainer.addEventListener('mousemove', handleMouseMove, {
1010
+ passive: true,
1011
+ });
1012
+ gridContainer.addEventListener('mouseleave', handleMouseLeave, {
1013
+ passive: true,
1014
+ });
1015
+
1016
+ // DŮLEŽITÉ: Přidáme také mouseenter pro reset při vstupu do gridu
1017
+ const handleMouseEnter = () => {
1018
+ if (isMountedRef.current) {
1019
+ // Reset stavu při vstupu do gridu
1020
+ currentHoveredRowRef.current = null;
1021
+ setIsRowHovered(false);
1022
+ }
1023
+ };
1024
+ gridContainer.addEventListener('mouseenter', handleMouseEnter, {
1025
+ passive: true,
1026
+ });
1027
+
1028
+ mouseListenersSetupRef.current = true;
1029
+ }
1030
+
1031
+ // Nastavíme MutationObserver pro tento grid
1032
+ observerRef.current = new MutationObserver((mutations) => {
1033
+ if (!isMountedRef.current) return;
1034
+
1035
+ let shouldRecalculate = false;
1036
+
1037
+ mutations.forEach((mutation) => {
1038
+ if (mutation.type === 'childList') {
1039
+ const addedRows = Array.from(mutation.addedNodes).some(
1040
+ (node) =>
1041
+ node.nodeType === Node.ELEMENT_NODE &&
1042
+ (node.classList?.contains('ag-row') ||
1043
+ node.querySelector?.('.ag-row'))
1044
+ );
1045
+
1046
+ const removedRows = Array.from(mutation.removedNodes).some(
1047
+ (node) =>
1048
+ node.nodeType === Node.ELEMENT_NODE &&
1049
+ (node.classList?.contains('ag-row') ||
1050
+ node.querySelector?.('.ag-row'))
1051
+ );
1052
+
1053
+ if (addedRows || removedRows) {
1054
+ shouldRecalculate = true;
1055
+ }
1056
+ }
1057
+ });
1058
+
1059
+ if (shouldRecalculate) {
1060
+ // Reset hover stavu
1061
+ currentHoveredRowRef.current = null;
1062
+ setIsRowHovered(false);
1063
+
1064
+ // Znovu detekujeme aktuální pozici myši po krátké době
1065
+ setTimeout(() => {
1066
+ if (isMountedRef.current && gridContainerRef.current) {
1067
+ const rect = gridContainerRef.current.getBoundingClientRect();
1068
+ const mouseEvent = new MouseEvent('mousemove', {
1069
+ clientX: window.lastMouseX || rect.left + rect.width / 2,
1070
+ clientY: window.lastMouseY || rect.top + rect.height / 2,
1071
+ bubbles: true,
1072
+ });
1073
+ gridContainerRef.current.dispatchEvent(mouseEvent);
1074
+ }
1075
+ }, 100);
1076
+ }
1077
+ });
1078
+
1079
+ // Spustíme observer na grid container
1080
+ observerRef.current.observe(gridContainer, {
1081
+ childList: true,
1082
+ subtree: true,
1083
+ attributes: false,
1084
+ });
1085
+
1086
+ // Sledujeme pozici myši globálně pro tento observer
1087
+ const trackMouse = (e) => {
1088
+ window.lastMouseX = e.clientX;
1089
+ window.lastMouseY = e.clientY;
1090
+ };
1091
+
1092
+ document.addEventListener('mousemove', trackMouse, { passive: true });
1093
+
1094
+ // Cleanup tracking při unmount
1095
+ return () => {
1096
+ document.removeEventListener('mousemove', trackMouse);
1097
+ };
1098
+ }, [params.api, params.eGridCell, handleMouseMove, handleMouseLeave]);
1099
+
1100
+ // Spustíme setup při mount a při změně params
1101
+ React.useEffect(() => {
1102
+ setupGridObserver();
1103
+ }, [setupGridObserver]);
1104
+
1105
+ const handleVisibleChange = React.useCallback(
1106
+ async (visible) => {
1107
+ if (!isMountedRef.current) return;
1108
+
1109
+ if (visible && onBeforeShow) {
1110
+ try {
1111
+ const updatedMenuItems = await onBeforeShow(params, menuItems);
1112
+ if (
1113
+ updatedMenuItems &&
1114
+ Array.isArray(updatedMenuItems) &&
1115
+ isMountedRef.current
1116
+ ) {
1117
+ setCurrentMenuItems(updatedMenuItems);
1118
+ }
1119
+ } catch (error) {
1120
+ // Removed console.error for production
1121
+ if (isMountedRef.current) {
1122
+ setCurrentMenuItems(menuItems);
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ if (isMountedRef.current) {
1128
+ setIsDropdownVisible(visible);
1129
+ }
1130
+ },
1131
+ [params, menuItems, onBeforeShow]
1132
+ );
1133
+
1134
+ React.useEffect(() => {
1135
+ if (isMountedRef.current) {
1136
+ setCurrentMenuItems(menuItems);
1137
+ }
1138
+ }, [menuItems]);
1139
+
1140
+ const menu = React.useMemo(
1141
+ () => (
1142
+ <Menu
1143
+ onClick={(e) => {
1144
+ if (onActionClick) {
1145
+ const item = currentMenuItems.filter(f => f.key === e.key)[0];
1146
+ onActionClick(e.key, params, item);
1147
+ }
1148
+ setIsDropdownVisible(false);
1149
+ }}
1150
+ >
1151
+ {currentMenuItems.map((item) => {
1152
+ if (item.type === 'divider') {
1153
+ return <Divider key={item.key} style={{ margin: '4px 0' }} />;
1154
+ }
1155
+
1156
+ let isDisabled = false;
1157
+ if (typeof item.disable === 'function') {
1158
+ isDisabled = item.disable(params);
1159
+ } else if (typeof item.disable === 'boolean') {
1160
+ isDisabled = item.disable;
1161
+ } else if (item.disabled !== undefined) {
1162
+ isDisabled = item.disabled;
1163
+ }
1164
+
1165
+ return (
1166
+ <Menu.Item key={item.key} disabled={isDisabled}>
1167
+ {item.label}
1168
+ </Menu.Item>
1169
+ );
1170
+ })}
1171
+ </Menu>
1172
+ ),
1173
+ [currentMenuItems, onActionClick, params]
1174
+ );
1175
+
1176
+ const containerStyle = {
1177
+ width: '32px',
1178
+ height: '32px',
1179
+ display: 'flex',
1180
+ alignItems: 'center',
1181
+ justifyContent: 'center',
1182
+ position: 'relative',
1183
+ };
1184
+
1185
+ const buttonStyle = {
1186
+ minWidth: 'auto',
1187
+ width: '32px',
1188
+ height: '32px',
1189
+ padding: '0',
1190
+ background: 'transparent',
1191
+ border: 'none',
1192
+ cursor: 'pointer',
1193
+ display: 'flex',
1194
+ alignItems: 'center',
1195
+ justifyContent: 'center',
1196
+ opacity: isRowHovered || isDropdownVisible ? 1 : 0,
1197
+ visibility: isRowHovered || isDropdownVisible ? 'visible' : 'hidden',
1198
+ transition: 'opacity 0.15s ease-in-out, visibility 0.15s ease-in-out',
1199
+ pointerEvents: isRowHovered || isDropdownVisible ? 'auto' : 'none',
1200
+ };
1201
+
1202
+ return (
1203
+ <div style={containerStyle}>
1204
+ <Dropdown
1205
+ overlayStyle={{ zIndex: 5000 }}
1206
+ overlay={menu}
1207
+ trigger={['click']}
1208
+ visible={isDropdownVisible}
1209
+ onVisibleChange={handleVisibleChange}
1210
+ getPopupContainer={() => document.body}
1211
+ >
1212
+ <div
1213
+ style={buttonStyle}
1214
+ onClick={(e) => {
1215
+ e.stopPropagation();
1216
+ }}
1217
+ >
1218
+ <ActionIcon />
1219
+ </div>
1220
+ </Dropdown>
1221
+ </div>
1222
+ );
1223
+ };
1224
+
1225
+ this.#addPreparedColumn({
1226
+ headerName: '',
1227
+ pinned: 'right',
1228
+ maxWidth: 40,
1229
+ minWidth: 40,
1230
+ suppressSizeToFit: true,
1231
+ suppressMenu: true,
1232
+ sortable: false,
1233
+ filter: false,
1234
+ resizable: false,
1235
+ cellRenderer: ActionDropdownRenderer,
1236
+ cellClass: 'action-button-cell-observer',
1237
+ ...restProps,
1238
+ });
1239
+
1240
+ return this;
1241
+ }
1242
+
1243
+ /**
1244
+ * Přidá sloupec s custom tlačítky s podmíněnou viditelností.
1245
+ * Každé tlačítko může mít vlastní styl, akci a podmínku viditelnosti.
1246
+ * @param {Object} config - Konfigurace tlačítkového sloupce
1247
+ * @param {Array} config.buttons - Array definic tlačítek s vlastnostmi jako text, onClick, style
1248
+ * @param {Function} [config.visibleGetter=() => true] - Funkce určující viditelnost tlačítek
1249
+ * @param {boolean} config.editable - Zda je sloupec editovatelný
1250
+ * @param {Object} ...restProps - Další AG Grid vlastnosti sloupce
1251
+ * @returns {ColumnBuilder} Instance pro chaining
1252
+ */
1253
+ addButtonColumn({
1254
+ buttons,
1255
+ visibleGetter = () => true,
1256
+ editable,
1257
+ ...restProps
1258
+ }) {
1259
+ const buttonRendererParams = {
1260
+ buttons,
1261
+ visibleGetter,
1262
+ };
1263
+ this.#addPreparedColumn({
1264
+ buttonRenderer: true,
1265
+ buttonRendererParams,
1266
+ editable: this.#resolveEditable(editable),
1267
+ ...restProps,
1268
+ });
1269
+ return this;
1270
+ }
1271
+
1272
+ /**
1273
+ * Privátní pomocná metoda pro správné nastavení editable vlastnosti sloupce.
1274
+ * Zajišťuje konzistentní zpracování boolean hodnot pro editovatelnost.
1275
+ * @param {boolean|Function} editable - Editovatelnost sloupce nebo funkce vracející boolean
1276
+ * @returns {boolean|Function} Upravená hodnota editovatelnosti
1277
+ */
1278
+ #resolveEditable(editable) {
1279
+ if (!editable) return false;
1280
+ return editable;
1281
+ }
1282
+
1283
+ /**
1284
+ * Finální metoda pro sestavení pole sloupců AG Grid.
1285
+ * Vrací kompletní konfiguraci všech přidaných sloupců připravených pro AG Grid.
1286
+ * @returns {Array} Array připravených definic sloupců pro AG Grid
1287
+ */
1288
+ build() {
1289
+ return this.columns;
1290
+ }
1291
+ }
1292
+
1293
+ export default ColumnBuilder;
1032
1294
  /* eslint-enable */