@bit.rhplus/ui.grid-layout 0.0.59 → 0.0.60

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/useGridLayout.js CHANGED
@@ -2,20 +2,28 @@
2
2
  /**
3
3
  * Hlavní hook pro správu grid layout - automatické ukládání a načítání rozvržení sloupců
4
4
  * Kombinuje AG-Grid API s Grid Layout službou pro persistence uživatelských preferencí
5
+ *
6
+ * ARCHITEKTURA - ZERO RE-RENDER PATTERN:
7
+ * ─────────────────────────────────────
8
+ * Při drag/resize/auto-save se NEPOUŽÍVÁ žádný useState.
9
+ * Všechny "pozadí" operace (save, detekce změn) běží přes useRef.
10
+ * Re-rendery se spouštějí POUZE při explicitních akcích (init, reload, editor save).
11
+ *
12
+ * preTransformedColumnDefs se po inicializaci ZAMKNE (stabilní reference).
13
+ * Zámek se uvolní jen při: změně columnDefs od parenta, editor save, reload.
14
+ * Tím se zabrání tomu, aby AG-Grid dostal columnDefs s původním pořadím.
5
15
  */
6
16
 
7
17
  import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
8
18
  import { useGridLayoutApi } from './useGridLayoutApi';
9
19
  import { debounce } from 'lodash';
10
- // Import tooltip komponent pro eliminaci circular dependency
11
- // import * as Tooltip from '../grid/tooltips';
20
+
21
+ // ============================================================================
22
+ // HELPER FUNKCE
23
+ // ============================================================================
12
24
 
13
25
  /**
14
- * Helper funkce pro porovnání column state
15
- * Porovnává klíčové vlastnosti sloupců pro detekci změn
16
- * @param {Array} state1 - První column state
17
- * @param {Array} state2 - Druhý column state
18
- * @returns {boolean} - true pokud jsou identické, false pokud se liší
26
+ * Porovnání dvou column state pro detekci skutečných změn
19
27
  */
20
28
  const isColumnStateEqual = (state1, state2) => {
21
29
  if (!state1 || !state2) return false;
@@ -24,35 +32,49 @@ const isColumnStateEqual = (state1, state2) => {
24
32
  for (let i = 0; i < state1.length; i++) {
25
33
  const col1 = state1[i];
26
34
  const col2 = state2[i];
27
-
28
- // Porovnat klíčové vlastnosti
29
- if (col1.colId !== col2.colId) return false; // Pořadí
30
- if (col1.width !== col2.width) return false; // Šířka
31
- if (col1.hide !== col2.hide) return false; // Viditelnost
32
- if (col1.pinned !== col2.pinned) return false; // Pinning
35
+ if (col1.colId !== col2.colId) return false;
36
+ if (col1.width !== col2.width) return false;
37
+ if (col1.hide !== col2.hide) return false;
38
+ if (col1.pinned !== col2.pinned) return false;
33
39
  }
34
-
35
40
  return true;
36
41
  };
37
42
 
38
43
  /**
39
- * Hook pro správu grid layout s automatickým ukládáním
40
- * @param {Object} config - Konfigurace grid layout
41
- * @param {string} config.userKey - Identifikátor uživatele
42
- * @param {string} config.applicationName - Název aplikace
43
- * @param {string} config.gridName - Název gridu
44
- * @param {string} [config.filterName] - Název filtru (volitelné)
45
- * @param {boolean} [config.enabled=true] - Zapnout/vypnout layout management
46
- * @param {boolean} [config.autoSave=true] - Automatické ukládání při změnách
47
- * @param {number} [config.autoSaveDelay=500] - Zpoždění auto-save v ms
48
- * @param {Array} config.columnDefs - AG-Grid column definitions
49
- * @param {string} [config.accessToken] - Přístupový token
50
- * @param {boolean} [config.waitForSavedFields=false] - Skrýt columnDefs dokud nejsou načtena savedFields
51
- * @param {Function} [config.onLayoutLoaded] - Callback při načtení layoutu
52
- * @param {Function} [config.onLayoutSaved] - Callback při uložení layoutu
53
- * @param {Function} [config.onError] - Callback při chybě
54
- * @returns {Object} Grid layout management interface
44
+ * Získá API objekt s metodou applyColumnState (AG Grid v31+ nebo starší)
45
+ */
46
+ const getApplyApi = (gridApiRef, columnApiRef) => {
47
+ if (gridApiRef.current?.applyColumnState) return gridApiRef.current;
48
+ if (columnApiRef.current?.applyColumnState) return columnApiRef.current;
49
+ return null;
50
+ };
51
+
52
+ /**
53
+ * Získá aktuální column state z AG-Grid (AG Grid v31+ nebo starší)
54
+ */
55
+ const getColumnState = (gridApiRef, columnApiRef) => {
56
+ try {
57
+ if (gridApiRef.current?.getColumnState) return gridApiRef.current.getColumnState();
58
+ if (columnApiRef.current?.getColumnState) return columnApiRef.current.getColumnState();
59
+ } catch (e) {
60
+ console.error('[GridLayout] Chyba při čtení column state:', e);
61
+ }
62
+ return null;
63
+ };
64
+
65
+ /**
66
+ * Získá API objekt s metodou resetColumnState
55
67
  */
68
+ const getResetApi = (gridApiRef, columnApiRef) => {
69
+ if (gridApiRef.current?.resetColumnState) return gridApiRef.current;
70
+ if (columnApiRef.current?.resetColumnState) return columnApiRef.current;
71
+ return null;
72
+ };
73
+
74
+ // ============================================================================
75
+ // HLAVNÍ HOOK
76
+ // ============================================================================
77
+
56
78
  export const useGridLayout = ({
57
79
  userKey,
58
80
  applicationName,
@@ -63,51 +85,62 @@ export const useGridLayout = ({
63
85
  autoSaveDelay = 500,
64
86
  columnDefs = [],
65
87
  accessToken,
66
- waitForSavedFields = false, // Nový prop pro odložené zobrazení columnDefs
88
+ waitForSavedFields = false,
67
89
  onLayoutLoaded,
68
90
  onLayoutSaved,
69
91
  onError,
70
92
  }) => {
71
93
 
72
- // Validace columnDefs - musí být array
73
- if (
74
- columnDefs !== undefined &&
75
- columnDefs !== null &&
76
- !Array.isArray(columnDefs)
77
- ) {
78
- console.error(
79
- '[GridLayout] columnDefs is not an array:',
80
- typeof columnDefs,
81
- columnDefs
82
- );
94
+ // Validace columnDefs
95
+ if (columnDefs !== undefined && columnDefs !== null && !Array.isArray(columnDefs)) {
96
+ console.error('[GridLayout] columnDefs is not an array:', typeof columnDefs, columnDefs);
83
97
  throw new Error('useGridLayout: columnDefs musí být array');
84
98
  }
85
99
 
86
- // Refs pro AG-Grid API
100
+ // ==========================================================================
101
+ // REFS - stabilní reference pro event handlery a save funkce
102
+ // ==========================================================================
87
103
  const gridApiRef = useRef(null);
88
104
  const columnApiRef = useRef(null);
89
- // Ref mapa pro ukládání aktuálních šířek sloupců (fieldId -> šířka)
90
- const columnWidthRefsMap = useRef(new Map());
91
- // Ref pro isInitialized - stabilní reference pro event handlery
105
+ const columnWidthRefsMap = useRef(new Map()); // fieldId -> aktuální šířka
106
+ const headerNameMapRef = useRef(new Map()); // fieldId -> aktuální headerName
107
+ const lastKnownColumnStateRef = useRef(null); // Poslední známý stav pro detekci změn
92
108
  const isInitializedRef = useRef(false);
93
- // FIX: Ref pro sledování posledního známého column state (pro detekci změn v onDragStopped)
94
- const lastKnownColumnStateRef = useRef(null);
95
-
96
- // State
109
+ const enabledRef = useRef(enabled);
110
+ const isGridReadyRef = useRef(false);
111
+ const autoSaveRef = useRef(autoSave);
112
+ const stableColumnDefsRef = useRef(columnDefs);
113
+ const stableGridLayoutApiRef = useRef(null);
114
+ const stableOnLayoutSavedRef = useRef(onLayoutSaved);
115
+ const stableOnErrorRef = useRef(onError);
116
+ const stableOnLayoutLoadedRef = useRef(onLayoutLoaded);
117
+
118
+ // "Pozadí" refs - ŽÁDNÉ useState pro tyto hodnoty (zero re-render pattern)
119
+ const isSavingRef = useRef(false);
120
+ const hasUnsavedChangesRef = useRef(false);
121
+ const isApplyingLayoutRef = useRef(false);
122
+
123
+ // Zámek pro preTransformedColumnDefs (stabilní reference po inicializaci)
124
+ const lockedColumnDefsRef = useRef(null);
125
+ const lockedForColumnDefsRef = useRef(null);
126
+ const lockedForFrozenRecordsRef = useRef(null);
127
+
128
+ // ==========================================================================
129
+ // STATE - POUZE pro hodnoty kde re-render je žádoucí
130
+ // ==========================================================================
97
131
  const [isInitialized, setIsInitialized] = useState(false);
98
132
  const [isGridReady, setIsGridReady] = useState(false);
99
133
  const [isLoading, setIsLoading] = useState(false);
100
- const [isSaving, setIsSaving] = useState(false);
101
- const [isApplyingLayout, setIsApplyingLayout] = useState(false);
102
134
  const [error, setError] = useState(null);
103
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
104
135
  const [isColumnEditorOpen, setIsColumnEditorOpen] = useState(false);
105
- const [lastKnownColumnState, setLastKnownColumnState] = useState(null);
106
- const [columnWidthsVersion, setColumnWidthsVersion] = useState(0); // Pro trigger preTransformedColumnDefs
136
+ const [columnWidthsVersion, setColumnWidthsVersion] = useState(0);
107
137
 
138
+ // Zamrazené savedFields records - aktualizují se POUZE při init, reload a editor save
139
+ const [frozenSavedRecords, setFrozenSavedRecords] = useState(null);
108
140
 
109
-
110
- // Grid Layout API hook
141
+ // ==========================================================================
142
+ // API HOOK & QUERY
143
+ // ==========================================================================
111
144
  const gridLayoutApi = useGridLayoutApi({
112
145
  userKey,
113
146
  applicationName,
@@ -116,7 +149,6 @@ export const useGridLayout = ({
116
149
  accessToken,
117
150
  });
118
151
 
119
- // Query pro načtení saved layoutu
120
152
  const {
121
153
  data: savedFields,
122
154
  isLoading: isFieldsLoading,
@@ -125,11 +157,7 @@ export const useGridLayout = ({
125
157
  } = gridLayoutApi.useUserFields(
126
158
  (columnDefs || []).map((colDef, index) => ({
127
159
  name: colDef.field || colDef.colId || `column_${index}`,
128
- displayName:
129
- colDef.headerName ||
130
- colDef.field ||
131
- colDef.colId ||
132
- `Column ${index + 1}`,
160
+ displayName: colDef.headerName || colDef.field || colDef.colId || `Column ${index + 1}`,
133
161
  dataType: colDef.type || 'string',
134
162
  isVisible: !colDef.hide,
135
163
  width: colDef.width || 100,
@@ -145,874 +173,425 @@ export const useGridLayout = ({
145
173
  }
146
174
  );
147
175
 
148
- /**
149
- * Error handler
150
- */
151
- const handleError = useCallback(
152
- (error, context = '') => {
153
- setError(error);
154
- if (onError) {
155
- onError(error, context);
156
- }
157
- },
158
- [onError]
159
- );
160
-
161
- // Stabilní reference pro debouncedSave
162
- const stableColumnDefsRef = useRef(columnDefs);
163
- const stableGridLayoutApiRef = useRef(gridLayoutApi);
164
- const stableOnLayoutSavedRef = useRef(onLayoutSaved);
165
- const stableHandleErrorRef = useRef(handleError);
166
- // Reference pro ukládání stavu sloupců bez state update v render cyklu
167
- const stableCurrentColumnsRef = useRef(null);
176
+ // ==========================================================================
177
+ // SYNCHRONIZACE REFS
178
+ // ==========================================================================
179
+ useEffect(() => { enabledRef.current = enabled; }, [enabled]);
180
+ useEffect(() => { isGridReadyRef.current = isGridReady; }, [isGridReady]);
181
+ useEffect(() => { autoSaveRef.current = autoSave; }, [autoSave]);
182
+ useEffect(() => { isInitializedRef.current = isInitialized; }, [isInitialized]);
183
+ useEffect(() => { stableGridLayoutApiRef.current = gridLayoutApi; }, [gridLayoutApi]);
184
+ useEffect(() => {
185
+ stableOnLayoutSavedRef.current = onLayoutSaved;
186
+ stableOnErrorRef.current = onError;
187
+ stableOnLayoutLoadedRef.current = onLayoutLoaded;
188
+ }, [onLayoutSaved, onError, onLayoutLoaded]);
168
189
 
169
- // Jednoduchá aktualizace ref hodnot - BEZ aktualizace columnDefs
190
+ // Synchronizace columnDefs ref + zachování existujících šířek
170
191
  useEffect(() => {
171
- // Při změně columnDefs vyčistíme columnWidthRefsMap a přeneseme existující šířky
172
192
  if (columnDefs !== stableColumnDefsRef.current) {
173
193
  const newWidthMap = new Map();
174
-
175
- // Zachováme šířky pro sloupce, které stále existují
176
194
  if (Array.isArray(columnDefs)) {
177
195
  columnDefs.forEach(colDef => {
178
196
  const fieldId = colDef.field || colDef.colId;
179
197
  if (fieldId) {
180
- // Zkusíme najít existující šířku v ref map
181
198
  const existingWidth = columnWidthRefsMap.current.get(fieldId);
182
199
  if (existingWidth !== undefined) {
183
200
  newWidthMap.set(fieldId, existingWidth);
184
201
  } else if (colDef.width) {
185
- // Použijeme šířku z nového columnDef
186
202
  newWidthMap.set(fieldId, colDef.width);
187
203
  }
188
204
  }
189
205
  });
190
206
  }
191
-
192
207
  columnWidthRefsMap.current = newWidthMap;
193
- setColumnWidthsVersion(prev => prev + 1); // Trigger preTransformedColumnDefs přepočet
208
+ setColumnWidthsVersion(prev => prev + 1);
194
209
  }
195
-
196
210
  stableColumnDefsRef.current = columnDefs;
197
- stableGridLayoutApiRef.current = gridLayoutApi;
198
- stableOnLayoutSavedRef.current = onLayoutSaved;
199
- stableHandleErrorRef.current = handleError;
200
- }, [columnDefs, gridLayoutApi, onLayoutSaved, handleError]);
211
+ }, [columnDefs]);
212
+
213
+ // ==========================================================================
214
+ // HEADER NAME MAPA - plní se z savedFields při načtení
215
+ // ==========================================================================
216
+ useEffect(() => {
217
+ if (savedFields?.records && Array.isArray(savedFields.records)) {
218
+ savedFields.records.forEach(field => {
219
+ if (field.fieldName && field.headerName) {
220
+ headerNameMapRef.current.set(field.fieldName, field.headerName);
221
+ }
222
+ });
223
+ }
224
+ }, [savedFields?.records]);
201
225
 
202
- // Synchronizace isInitializedRef se state pro stabilní event handlery
226
+ // ==========================================================================
227
+ // FROZEN SAVED RECORDS - aktualizují se pouze při prvním načtení
228
+ // ==========================================================================
203
229
  useEffect(() => {
204
- isInitializedRef.current = isInitialized;
205
- }, [isInitialized]);
206
-
207
- // Efekt pro bezpečnou aktualizaci lastKnownColumnState z ref
208
- // useEffect(() => {
209
- // if (stableCurrentColumnsRef.current) {
210
- // setLastKnownColumnState(stableCurrentColumnsRef.current);
211
- // console.log('[GridLayout] Updated lastKnownColumnState from ref');
212
- // // Vymazat po použití
213
- // stableCurrentColumnsRef.current = null;
214
- // }
215
- // }, [stableCurrentColumnsRef.current]);
216
-
217
- /**
218
- * Uloží současný stav sloupců do API
219
- * KRITICKÉ: Ukládá POUZE pokud už byl layout načten z API (isInitialized === true)
220
- * Tím se zabrání přepsání uloženého layoutu default hodnotami při inicializaci
221
- */
230
+ if (!frozenSavedRecords && savedFields?.records?.length > 0) {
231
+ setFrozenSavedRecords(savedFields.records);
232
+ }
233
+ }, [savedFields?.records, frozenSavedRecords]);
234
+
235
+ // ==========================================================================
236
+ // ERROR HANDLER (pouze pro explicitní akce - ne auto-save)
237
+ // ==========================================================================
238
+ const handleError = useCallback((error, context = '') => {
239
+ setError(error);
240
+ if (stableOnErrorRef.current) {
241
+ stableOnErrorRef.current(error, context);
242
+ }
243
+ }, []);
244
+
245
+ // ==========================================================================
246
+ // SAVE CURRENT LAYOUT
247
+ // ZERO RE-RENDER: používá POUZE refs, žádné setState
248
+ // ==========================================================================
222
249
  const saveCurrentLayout = useCallback(async () => {
223
- // OCHRANA: Neuložit dokud není načten a aplikován layout z API
224
- if (!enabled || !gridApiRef.current || !isInitialized) {
250
+ if (!enabledRef.current || !gridApiRef.current || !isInitializedRef.current) {
225
251
  return;
226
252
  }
227
- try {
228
- setIsSaving(true);
229
- setError(null);
230
-
231
- // Získáme současný column state z AG-Grid s fallbackem pro AG Grid v31+
232
- let columnState = null;
233
- if (
234
- gridApiRef.current &&
235
- typeof gridApiRef.current.getColumnState === 'function'
236
- ) {
237
- // AG Grid v31+ - getColumnState je přímo v main API
238
- try {
239
- columnState = gridApiRef.current.getColumnState();
240
-
241
- // Získáme aktuální headerName hodnoty z DOM pro každý sloupec
242
- if (columnState && Array.isArray(columnState)) {
243
- columnState = columnState.map(colState => {
244
- // Pokusíme se získat aktuální headerName z DOM
245
- try {
246
- const headerCell = document.querySelector(`[col-id="${colState.colId}"]`);
247
- if (headerCell) {
248
- const headerTextEl = headerCell.querySelector('.ag-header-cell-text');
249
- if (headerTextEl && headerTextEl.textContent) {
250
- return {
251
- ...colState,
252
- headerName: headerTextEl.textContent
253
- };
254
- }
255
- }
256
- } catch (headerError) {
257
- console.log(`❌ [GridLayout] Could not get headerName from DOM for ${colState.colId}`);
258
- }
259
- return colState;
260
- });
261
- }
262
253
 
263
- // Pokud getColumnState vrátí undefined, grid je v nekonzistentním stavu
264
- // Zkusíme alternativní metodu přes getColumnDefs()
265
- if (columnState === undefined || columnState === null) {
266
- console.warn(
267
- '[GridLayout] getColumnState returned undefined/null, trying getColumnDefs() alternative'
268
- );
269
- console.warn(
270
- '[GridLayout] Full gridApiRef.current object:',
271
- gridApiRef.current
272
- );
273
- console.warn(
274
- '[GridLayout] Available methods on gridApiRef.current:',
275
- gridApiRef.current
276
- ? Object.getOwnPropertyNames(gridApiRef.current).filter(
277
- (name) => typeof gridApiRef.current[name] === 'function'
278
- )
279
- : 'NO_GRID_API'
280
- );
254
+ // Ochrana proti souběžným uložením
255
+ if (isSavingRef.current) return;
281
256
 
282
- try {
283
- // Alternativní přístup: použijeme getColumnDefs() a vytvoříme fake column state
284
- const columnDefs = gridApiRef.current.getColumnDefs();
285
-
286
- if (
287
- columnDefs &&
288
- Array.isArray(columnDefs) &&
289
- columnDefs.length > 0
290
- ) {
291
- // Vytvoříme column state z column defs
292
- columnState = columnDefs.map((colDef, index) => {
293
- // Zkusíme získat aktuální šířku sloupce z DOM
294
- let currentWidth = colDef.width || 100;
295
- try {
296
- const fieldId = colDef.field || colDef.colId;
297
- const headerElement = document.querySelector(
298
- `[col-id="${fieldId}"]`
299
- );
300
- if (headerElement && headerElement.offsetWidth) {
301
- currentWidth = headerElement.offsetWidth;
302
- }
303
- } catch (domError) {
304
- console.log('❌ [GridLayout] Could not get width from DOM for',
305
- colDef.field
306
- );
307
- }
257
+ try {
258
+ isSavingRef.current = true; // ref only žádný re-render
308
259
 
309
- return {
310
- colId: colDef.field || colDef.colId,
311
- hide: colDef.hide || false,
312
- width: currentWidth,
313
- headerName: colDef.headerName, // Přidáme aktuální headerName
314
- sort: null, // Nebudeme zachovávat sort při této fallback metodě
315
- sortIndex: null,
316
- };
317
- });
260
+ const columnState = getColumnState(gridApiRef, columnApiRef);
261
+ if (!columnState || !Array.isArray(columnState) || columnState.length === 0) {
262
+ return;
263
+ }
318
264
 
319
- } else {
320
- console.error(
321
- '[GridLayout] getColumnDefs() also failed or returned empty array'
322
- );
323
- const cachedColumnDefs = stableColumnDefsRef.current;
324
- if (
325
- cachedColumnDefs &&
326
- Array.isArray(cachedColumnDefs) &&
327
- cachedColumnDefs.length > 0
328
- ) {
329
- columnState = cachedColumnDefs.map((colDef, index) => {
330
- // Použijeme cached definice
331
- let currentWidth = colDef.width || 100;
332
-
333
- return {
334
- colId: colDef.field || colDef.colId,
335
- hide: colDef.hide || false,
336
- width: currentWidth,
337
- headerName: colDef.headerName, // Přidáme aktuální headerName
338
- sort: null,
339
- sortIndex: null,
340
- };
341
- });
342
- } else {
343
- console.error(
344
- '[GridLayout] All fallback methods failed - no column data available'
345
- );
346
- return;
347
- }
348
- }
349
- } catch (columnDefError) {
350
- console.error(
351
- '[GridLayout] getColumnDefs() alternative failed:',
352
- columnDefError
353
- );
354
- return;
355
- }
356
- }
357
- } catch (error) {
358
- console.error(
359
- '[GridLayout] Error calling gridApiRef.current.getColumnState():',
360
- error
361
- );
362
- return;
363
- }
364
- } else if (
365
- columnApiRef.current &&
366
- typeof columnApiRef.current.getColumnState === 'function'
367
- ) {
368
- // Starší verze AG Grid - getColumnState je v columnApi
369
- try {
370
- columnState = columnApiRef.current.getColumnState();
371
-
372
- // Získáme aktuální headerName hodnoty z DOM pro každý sloupec
373
- if (columnState && Array.isArray(columnState)) {
374
- columnState = columnState.map(colState => {
375
- // Pokusíme se získat aktuální headerName z DOM
376
- try {
377
- const headerCell = document.querySelector(`[col-id="${colState.colId}"]`);
378
- if (headerCell) {
379
- const headerTextEl = headerCell.querySelector('.ag-header-cell-text');
380
- if (headerTextEl && headerTextEl.textContent) {
381
- return {
382
- ...colState,
383
- headerName: headerTextEl.textContent
384
- };
385
- }
386
- }
387
- } catch (headerError) {
388
- console.log(`❌ [GridLayout] Could not get headerName from DOM for ${colState.colId}`);
389
- }
390
- return colState;
391
- });
392
- }
393
-
394
- } catch (error) {
395
- console.error(
396
- '❌ [GridLayout] Error calling columnApiRef.current.getColumnState():',
397
- error
398
- );
399
- return;
265
+ // Obohatíme column state o headerName z ref mapy (BEZ čtení z DOM)
266
+ const enrichedColumnState = columnState.map(colState => {
267
+ const savedHeaderName = headerNameMapRef.current.get(colState.colId);
268
+ if (savedHeaderName) {
269
+ return { ...colState, headerName: savedHeaderName };
400
270
  }
401
- } else {
271
+ return colState;
272
+ });
273
+
274
+ const columnDefsToUse = stableColumnDefsRef.current;
275
+ if (!columnDefsToUse || !Array.isArray(columnDefsToUse) || columnDefsToUse.length === 0) {
402
276
  return;
403
277
  }
404
278
 
405
- // Kontrola validity columnDefs a použití fallback pokud je potřeba
406
- let columnDefsToUse = stableColumnDefsRef.current;
407
- if (
408
- !columnDefsToUse ||
409
- !Array.isArray(columnDefsToUse) ||
410
- columnDefsToUse.length === 0
411
- ) {
412
- console.warn(
413
- '[GridLayout] stableColumnDefsRef is empty, using fallback from stableCurrentColumnsRef'
414
- );
415
- if (
416
- stableCurrentColumnsRef.current &&
417
- Array.isArray(stableCurrentColumnsRef.current)
418
- ) {
419
- // Převedeme cached sloupce zpět na columnDefs format
420
- columnDefsToUse = stableCurrentColumnsRef.current.map((col) => ({
421
- field: col.field,
422
- headerName: col.headerName || col.field,
423
- width: col.width || 100,
424
- hide: !col.visible,
425
- }));
426
- } else {
427
- console.error(
428
- '[GridLayout] No valid columnDefs available for saving'
429
- );
430
- return;
431
- }
279
+ const fields = stableGridLayoutApiRef.current.transformColumnStateToFields(
280
+ enrichedColumnState,
281
+ columnDefsToUse
282
+ );
283
+
284
+ if (!fields || fields.length === 0) {
285
+ return;
432
286
  }
433
287
 
434
- // Transformujeme na Grid API format
435
- const fields =
436
- stableGridLayoutApiRef.current.transformColumnStateToFields(
437
- columnState,
438
- columnDefsToUse
439
- );
288
+ const result = await stableGridLayoutApiRef.current.saveGridLayout(fields);
440
289
 
441
- // Uložíme do API
442
- const result = stableGridLayoutApiRef.current
443
- .saveGridLayout(fields)
444
- .then((result) => {
445
- if (result.success) {
446
- setHasUnsavedChanges(false);
447
- if (stableOnLayoutSavedRef.current) {
448
- stableOnLayoutSavedRef.current(fields, columnState);
449
- }
450
- }
451
- })
452
- .catch((error) => {
453
- stableHandleErrorRef.current(error, 'při ukládání layoutu');
454
- });
290
+ if (result.success) {
291
+ hasUnsavedChangesRef.current = false; // ref only → žádný re-render
292
+ if (stableOnLayoutSavedRef.current) {
293
+ stableOnLayoutSavedRef.current(fields, enrichedColumnState);
294
+ }
295
+ }
455
296
  } catch (error) {
456
- stableHandleErrorRef.current(error, 'při ukládání layoutu');
297
+ console.error('[GridLayout] Chyba při ukládání layoutu:', error);
298
+ // Při auto-save NENASTAVUJEME error state (žádný re-render)
299
+ // Error se loguje do konzole a volá se onError callback
300
+ if (stableOnErrorRef.current) {
301
+ stableOnErrorRef.current(error, 'při ukládání layoutu');
302
+ }
457
303
  } finally {
458
- setIsSaving(false);
304
+ isSavingRef.current = false; // ref only → žádný re-render
459
305
  }
460
- }, [enabled, isInitialized]);
306
+ }, []); // PRÁZDNÉ DEPS → stabilní funkce
307
+
308
+ // ==========================================================================
309
+ // DEBOUNCED SAVE
310
+ // ==========================================================================
311
+ const saveCurrentLayoutRef = useRef(saveCurrentLayout);
312
+ useEffect(() => {
313
+ saveCurrentLayoutRef.current = saveCurrentLayout;
314
+ }, [saveCurrentLayout]);
461
315
 
462
- /**
463
- * Debounced auto-save pro předcházení častým voláním API
464
- */
465
316
  const debouncedSave = useMemo(() => {
466
- const debouncedFn = debounce((...args) => {
467
- return saveCurrentLayout(...args);
317
+ return debounce(() => {
318
+ saveCurrentLayoutRef.current();
468
319
  }, autoSaveDelay);
469
- return debouncedFn;
470
- }, [saveCurrentLayout, autoSaveDelay]);
320
+ }, [autoSaveDelay]);
471
321
 
472
- // ✅ FIX #3: Stabilní ref pro debouncedSave (eliminuje regeneraci event handlerů)
473
322
  const debouncedSaveRef = useRef(debouncedSave);
474
-
475
- // Aktualizovat ref když se debouncedSave změní
476
323
  useEffect(() => {
477
324
  debouncedSaveRef.current = debouncedSave;
478
325
  }, [debouncedSave]);
479
326
 
480
- // ✅ FIX #3+: Stabilní refs pro enabled, isGridReady, autoSave (KRITICKÉ pro stabilitu handlerů)
481
- const enabledRef = useRef(enabled);
482
- const isGridReadyRef = useRef(isGridReady);
483
- const autoSaveRef = useRef(autoSave);
484
-
485
- // Aktualizovat refs při změnách
486
327
  useEffect(() => {
487
- enabledRef.current = enabled;
488
- }, [enabled]);
328
+ return () => { debouncedSave.cancel(); };
329
+ }, [debouncedSave]);
489
330
 
490
- useEffect(() => {
491
- isGridReadyRef.current = isGridReady;
492
- }, [isGridReady]);
331
+ // ==========================================================================
332
+ // APPLY SAVED LAYOUT (pouze při inicializaci - re-render je OK)
333
+ // ==========================================================================
334
+ const applySavedLayout = useCallback((forceApply = false) => {
335
+ if (!savedFields?.records || savedFields.records.length === 0 || (!forceApply && isInitialized)) {
336
+ return;
337
+ }
493
338
 
494
- useEffect(() => {
495
- autoSaveRef.current = autoSave;
496
- }, [autoSave]);
497
-
498
- /**
499
- * Aplikuje saved layout na AG-Grid
500
- * @param {boolean} forceApply - Vynucené aplikování ignorující isInitialized stav
501
- */
502
- const applySavedLayout = useCallback(
503
- (forceApply = false) => {
504
- // Ověříme dostupnost API a inicializaci
505
- if (
506
- !savedFields?.records ||
507
- savedFields.records.length === 0 ||
508
- (!forceApply && isInitialized)
509
- ) {
339
+ const applyApi = getApplyApi(gridApiRef, columnApiRef);
340
+ if (!applyApi) {
341
+ return;
342
+ }
343
+
344
+ try {
345
+ setIsLoading(true);
346
+ isApplyingLayoutRef.current = true;
347
+
348
+ const columnDefsToUse = stableColumnDefsRef.current || columnDefs;
349
+ const columnState = gridLayoutApi.transformFieldsToColumnState(
350
+ savedFields.records,
351
+ columnDefsToUse
352
+ );
353
+
354
+ if (!columnState || columnState.length === 0) {
510
355
  return;
511
356
  }
512
357
 
513
- // Ověříme dostupnost applyColumnState metody (AG Grid v31+ má ji v main API)
514
- let applyColumnStateApi = null;
515
- if (
516
- gridApiRef.current &&
517
- typeof gridApiRef.current.applyColumnState === 'function'
518
- ) {
519
- applyColumnStateApi = gridApiRef.current;
520
- } else if (
521
- columnApiRef.current &&
522
- typeof columnApiRef.current.applyColumnState === 'function'
523
- ) {
524
- applyColumnStateApi = columnApiRef.current;
358
+ // Inicializace width ref mapy s API šířkami
359
+ if (!isInitialized) {
360
+ columnState.forEach(colState => {
361
+ if (colState.colId && colState.width) {
362
+ columnWidthRefsMap.current.set(colState.colId, colState.width);
363
+ }
364
+ });
525
365
  }
526
366
 
527
- if (!applyColumnStateApi) {
528
- console.warn('[GridLayout] applyColumnState method not available');
529
- return;
530
- }
367
+ // Sestavení headerName mapy z API dat
368
+ const headerNameMap = new Map();
369
+ savedFields.records.forEach(field => {
370
+ if (field.fieldName && field.headerName) {
371
+ headerNameMap.set(field.fieldName, field.headerName);
372
+ headerNameMapRef.current.set(field.fieldName, field.headerName);
373
+ }
374
+ });
531
375
 
532
- try {
533
- setIsLoading(true);
534
- setIsApplyingLayout(true);
535
-
536
- // Transformujeme Grid API fields na AG-Grid column state
537
- // Použijeme stableColumnDefsRef.current místo columnDefs pro zachování aktuálních šířek
538
- const columnDefsToUse = stableColumnDefsRef.current || columnDefs;
539
- const columnState = gridLayoutApi.transformFieldsToColumnState(
540
- savedFields.records,
541
- columnDefsToUse
542
- );
376
+ // Adjusted column state (s ref šířkami pro re-apply)
377
+ const adjustedColumnState = columnState.map(colState => {
378
+ const refWidth = columnWidthRefsMap.current.get(colState.colId);
379
+ if (refWidth !== undefined && isInitialized) {
380
+ return { ...colState, width: refWidth };
381
+ }
382
+ return colState;
383
+ });
543
384
 
544
- if (columnState && columnState.length > 0) {
545
- // Při prvotním načítání inicializujeme ref mapu s API šířkami
546
- if (!isInitialized) {
547
- columnState.forEach(colState => {
548
- if (colState.colId && colState.width) {
549
- columnWidthRefsMap.current.set(colState.colId, colState.width);
550
- }
551
- });
552
- }
553
-
554
- // Pokud je waitForSavedFields true, columnDefs jsou už pre-transformované,
555
- // takže aplikujeme jen width a hide vlastnosti bez delay pro pořadí
556
- const applyFunction = () => {
385
+ const applyFunction = () => {
386
+ try {
387
+ // KROK 1: Aktualizace headerName přes setColumnDefs PRVNÍ
388
+ if (headerNameMap.size > 0 && gridApiRef.current) {
557
389
  try {
558
- // Aplikujeme column state na AG-Grid
559
- let result;
560
- try {
561
- const applyOrderEnabled = !waitForSavedFields; // Při waitForSavedFields už je pořadí v columnDefs
562
-
563
- // Upravíme columnState s aktuálními šířkami z ref map
564
- // POUZE pokud ref mapa obsahuje hodnoty (tj. uživatel už manipuloval s šířkami)
565
- // Při prvotním načítání z API zachováme API šířky
566
- let adjustedColumnState = columnState.map(colState => {
567
- // Zkontrolujeme, zda máme ref hodnotu pro tento sloupec
568
- const refWidth = columnWidthRefsMap.current.get(colState.colId);
569
- // Použijeme ref hodnotu POUZE pokud existuje A není to prvotní načítání
570
- if (refWidth !== undefined && isInitialized) {
571
- return {
572
- ...colState,
573
- width: refWidth
574
- };
575
- }
576
- // Při prvotním načítání nebo pokud nemáme ref hodnotu, zachováme API šířku
577
- return colState;
578
- });
579
- result = applyColumnStateApi.applyColumnState({
580
- state: adjustedColumnState,
581
- applyOrder: applyOrderEnabled, // Pořadí jen když není waitForSavedFields
582
- defaultState: {
583
- sort: null, // Reset sorting na všech sloupcích
584
- sortIndex: null, // Reset sort index
585
- pivot: null, // Reset pivot
586
- rowGroup: null, // Reset row grouping
587
- },
588
- });
589
-
590
- // ✅ FIX: Aktualizovat lastKnownColumnStateRef po aplikování layoutu z API
591
- // Tím zajistíme, že handleDragStopped má správnou referenci pro porovnání
592
- lastKnownColumnStateRef.current = adjustedColumnState;
593
-
594
- // Explicitně aktualizujeme headerName pro každý sloupec, protože AG-Grid
595
- // nepodporuje nastavení headerName přes applyColumnState
596
- if (
597
- savedFields.records &&
598
- Array.isArray(savedFields.records) &&
599
- gridApiRef.current
600
- ) {
601
- // Nejprve zkusíme použít refreshHeader funkci, pokud je dostupná
602
- try {
603
- if (
604
- typeof gridApiRef.current.refreshHeader === 'function'
605
- ) {
606
- gridApiRef.current.refreshHeader();
390
+ const currentColDefs = gridApiRef.current.getColumnDefs?.();
391
+ if (currentColDefs && Array.isArray(currentColDefs)) {
392
+ let hasHeaderChanges = false;
393
+ const updatedColDefs = currentColDefs.map(colDef => {
394
+ const fieldName = colDef.field;
395
+ if (fieldName && headerNameMap.has(fieldName)) {
396
+ const newName = headerNameMap.get(fieldName);
397
+ if (colDef.headerName !== newName) {
398
+ hasHeaderChanges = true;
399
+ return { ...colDef, headerName: newName };
607
400
  }
608
- } catch (refreshError) {
609
- console.error(
610
- '[GridLayout] Error in refreshHeader:',
611
- refreshError
612
- );
613
- }
614
- // Získáme aktuální definice sloupců z gridu
615
- let currentColDefs;
616
- try {
617
- currentColDefs = gridApiRef.current.getColumnDefs
618
- ? gridApiRef.current.getColumnDefs()
619
- : null;
620
- } catch (error) {
621
- console.error(
622
- '[GridLayout] Error getting column definitions:',
623
- error
624
- );
625
- currentColDefs = null;
626
401
  }
627
- if (currentColDefs && Array.isArray(currentColDefs)) {
628
- // Vytvoříme mapu fieldName -> headerName z API dat
629
- const headerNameMap = new Map();
630
- savedFields.records.forEach((field) => {
631
- if (field.fieldName && field.headerName) {
632
- headerNameMap.set(field.fieldName, field.headerName);
633
- }
634
- });
635
-
636
- // Aktualizujeme headerName pro každý sloupec
637
- const updatedColDefs = currentColDefs.map((colDef) => {
638
- const fieldName = colDef.field;
639
- if (fieldName && headerNameMap.has(fieldName)) {
640
- const newHeaderName = headerNameMap.get(fieldName);
641
- // Vytvoříme novou kopii definice sloupce s aktualizovaným headerName
642
- return {
643
- ...colDef,
644
- headerName: newHeaderName,
645
- };
646
- }
647
- return colDef;
648
- });
649
- // Aplikujeme aktualizované definice sloupců zpět do gridu
650
- try {
651
- if (
652
- typeof gridApiRef.current.setColumnDefs === 'function'
653
- ) {
654
- gridApiRef.current.setColumnDefs(updatedColDefs);
402
+ return colDef;
403
+ });
655
404
 
656
- // Pro jistotu zkontrolujeme, zda byly změny aplikovány
657
- setTimeout(() => {
658
- try {
659
- // DOM operace dokončeny, můžeme ukončit loading
660
- setIsApplyingLayout(false);
661
- } catch (checkError) {
662
- console.error(
663
- '[GridLayout] Error checking updated columns:',
664
- checkError
665
- );
666
- // I při chybě ukončíme loading
667
- setIsApplyingLayout(false);
668
- }
669
- }, 100);
670
- } else {
671
- // Alternativní řešení - přímá manipulace s DOM
672
-
673
- // Počkáme, až se grid vyrenderuje
674
- setTimeout(() => {
675
- try {
676
- // Vytvoříme mapu pro rychlou identifikaci
677
- const headerUpdates = new Map();
678
- updatedColDefs.forEach((colDef) => {
679
- if (colDef.field && colDef.headerName) {
680
- headerUpdates.set(
681
- colDef.field,
682
- colDef.headerName
683
- );
684
- }
685
- });
686
-
687
- // Najdeme všechny hlavičky sloupců pomocí DOM
688
- const headerCells =
689
- document.querySelectorAll('.ag-header-cell');
690
-
691
- headerCells.forEach((headerCell) => {
692
- try {
693
- // Získáme ID sloupce z DOM atributů
694
- const colId = headerCell.getAttribute('col-id');
695
- if (colId && headerUpdates.has(colId)) {
696
- // Najdeme element s textem hlavičky
697
- const headerTextEl = headerCell.querySelector(
698
- '.ag-header-cell-text'
699
- );
700
- if (headerTextEl) {
701
- const newHeaderName =
702
- headerUpdates.get(colId);
703
- headerTextEl.textContent = newHeaderName;
704
- }
705
- }
706
- } catch (cellError) {
707
- console.error(
708
- '[GridLayout] Error updating header cell:',
709
- cellError
710
- );
711
- }
712
- });
713
-
714
- // DOM manipulace dokončena, ukončíme loading
715
- setIsApplyingLayout(false);
716
- } catch (domError) {
717
- console.error(
718
- '[GridLayout] Error in DOM manipulation:',
719
- domError
720
- );
721
- // I při chybě ukončíme loading
722
- setIsApplyingLayout(false);
723
- }
724
- }, 200);
725
- }
726
- } catch (setError) {
727
- console.error(
728
- '[GridLayout] Error applying column definitions:',
729
- setError
730
- );
731
- }
732
- }
405
+ if (hasHeaderChanges && typeof gridApiRef.current.setColumnDefs === 'function') {
406
+ gridApiRef.current.setColumnDefs(updatedColDefs);
733
407
  }
734
- } catch (applyError) {
735
- throw applyError;
736
408
  }
737
-
738
- if (onLayoutLoaded) {
739
- onLayoutLoaded(savedFields.records, columnState);
740
- }
741
- } catch (delayedError) {
742
- handleError(delayedError, 'při delayed aplikování layoutu');
743
- } finally {
744
- // Bezpečnostní ukončení loading pokud se nedokončilo jinde
745
- setTimeout(() => {
746
- setIsApplyingLayout(false);
747
- }, 300);
409
+ } catch (e) {
410
+ console.error('[GridLayout] Chyba při aktualizaci headerName:', e);
748
411
  }
749
- };
750
-
751
- // Pro waitForSavedFields aplikujeme okamžitě (pořadí je už v columnDefs)
752
- // Pro normální režim použijeme delay
753
- if (waitForSavedFields) {
754
- applyFunction();
755
- } else {
756
- setTimeout(applyFunction, 100); // 100ms delay jen pro normální režim
757
412
  }
758
- }
759
- } catch (error) {
760
- handleError(error, 'při aplikování layoutu');
761
- } finally {
762
- setIsInitialized(true);
763
- setIsGridReady(true); // Obnovíme také isGridReady pro event handlery
764
- setIsLoading(false);
765
- // setIsApplyingLayout(false) se nastaví až po dokončení všech DOM operací
766
- }
767
- },
768
- [
769
- savedFields,
770
- gridLayoutApi,
771
- isInitialized,
772
- onLayoutLoaded,
773
- handleError,
774
- columnDefs,
775
- waitForSavedFields,
776
- ]
777
- );
778
413
 
779
- /**
780
- * Odložené akce pro případ rychlé interakce před dokončením inicializace
781
- */
782
- const [pendingActions, setPendingActions] = useState([]);
414
+ // KROK 2: Aplikovat column state (pořadí, šířky) PO setColumnDefs
415
+ const applyOrderEnabled = !waitForSavedFields;
416
+ applyApi.applyColumnState({
417
+ state: adjustedColumnState,
418
+ applyOrder: applyOrderEnabled,
419
+ defaultState: { sort: null, sortIndex: null, pivot: null, rowGroup: null },
420
+ });
783
421
 
784
- // Effect pro zpracování pending actions po dokončení inicializace
785
- useEffect(() => {
786
- if (isInitialized && pendingActions.length > 0) {
787
- // Zpracujeme pending akce s krátkým delay
788
- setTimeout(() => {
789
- pendingActions.forEach((action) => {
790
- switch (action.type) {
791
- case 'dragStopped':
792
- if (autoSaveRef.current && debouncedSaveRef.current) { // ✅ FIX #3+: Použít refs
793
- debouncedSaveRef.current();
794
- }
795
- break;
422
+ lastKnownColumnStateRef.current = adjustedColumnState;
423
+
424
+ try { gridApiRef.current?.refreshHeader?.(); } catch (e) { /* ignorujeme */ }
425
+
426
+ if (stableOnLayoutLoadedRef.current) {
427
+ stableOnLayoutLoadedRef.current(savedFields.records, columnState);
796
428
  }
797
- });
798
- setPendingActions([]);
799
- }, 100);
800
- }
801
- }, [isInitialized, pendingActions]); // ✅ FIX #3+: Odstranit autoSave z dependencies
429
+ } catch (error) {
430
+ console.error('[GridLayout] Chyba při aplikování layoutu:', error);
431
+ handleError(error, 'při aplikování layoutu');
432
+ } finally {
433
+ isApplyingLayoutRef.current = false;
434
+ }
435
+ };
802
436
 
803
- /**
804
- * Event handlers pro AG-Grid
805
- */
806
- const handleColumnMoved = useCallback(() => {
807
- if (!enabledRef.current || !isGridReadyRef.current) { // ✅ FIX #3+: Použít refs místo dependencies
808
- return;
437
+ if (waitForSavedFields) {
438
+ applyFunction();
439
+ } else {
440
+ setTimeout(applyFunction, 100);
441
+ }
442
+ } catch (error) {
443
+ console.error('[GridLayout] Chyba při přípravě layoutu:', error);
444
+ handleError(error, 'při aplikování layoutu');
445
+ isApplyingLayoutRef.current = false;
446
+ } finally {
447
+ setIsInitialized(true);
448
+ setIsGridReady(true);
449
+ setIsLoading(false);
809
450
  }
451
+ }, [savedFields, gridLayoutApi, isInitialized, handleError, columnDefs, waitForSavedFields]);
810
452
 
811
- setHasUnsavedChanges(true);
812
- // Neukládáme při každém pohybu - čekáme na onDragStopped
813
- }, []); // FIX #3+: ŽÁDNÉ dependencies handler se nikdy neregeneruje
453
+ // ==========================================================================
454
+ // AG-GRID EVENT HANDLERS
455
+ // ZERO RE-RENDER: všechny mají prázdné deps a používají POUZE refs
456
+ // ==========================================================================
457
+
458
+ const handleColumnMoved = useCallback(() => {
459
+ if (!enabledRef.current || !isGridReadyRef.current) return;
460
+ hasUnsavedChangesRef.current = true; // ref only → žádný re-render
461
+ }, []);
814
462
 
815
463
  const handleDragStopped = useCallback(() => {
816
- if (!enabledRef.current || !isGridReadyRef.current) { // ✅ FIX #3+: Použít refs místo dependencies
817
- return;
818
- }
464
+ if (!enabledRef.current || !isGridReadyRef.current) return;
819
465
 
820
- // FIX: Detekovat, zda se skutečně něco změnilo (pořadí/šířky/viditelnost/pinning)
821
- // Pokud se nic nezměnilo, SKIP celý handler (eliminuje zbytečné re-rendery při range selection)
466
+ // Detekce skutečné změny
822
467
  try {
823
- if (gridApiRef.current && typeof gridApiRef.current.getColumnState === 'function') {
824
- const currentColumnState = gridApiRef.current.getColumnState();
825
-
826
- if (currentColumnState && Array.isArray(currentColumnState)) {
827
- // Porovnat s posledním známým stavem
828
- if (isColumnStateEqual(lastKnownColumnStateRef.current, currentColumnState)) {
829
- // Žádná změna - SKIP celý handler (nejčastější případ při range selection)
830
- return;
831
- }
468
+ const currentColumnState = getColumnState(gridApiRef, columnApiRef);
469
+ if (currentColumnState && Array.isArray(currentColumnState)) {
470
+ if (isColumnStateEqual(lastKnownColumnStateRef.current, currentColumnState)) {
471
+ return; // Žádná změna → skip
472
+ }
832
473
 
833
- // Uložit nový stav jako referenci pro příští porovnání
834
- lastKnownColumnStateRef.current = currentColumnState;
474
+ lastKnownColumnStateRef.current = currentColumnState;
835
475
 
836
- // Uložit šířky do ref map pro každý sloupec (pro API save a další načtení)
837
- currentColumnState.forEach(colState => {
838
- if (colState.colId && colState.width) {
839
- columnWidthRefsMap.current.set(colState.colId, colState.width);
840
- }
841
- });
842
- // NEPOUŽÍVÁME setColumnWidthsVersion zde - způsobilo by reset pořadí/šířek!
843
- }
476
+ currentColumnState.forEach(colState => {
477
+ if (colState.colId && colState.width) {
478
+ columnWidthRefsMap.current.set(colState.colId, colState.width);
479
+ }
480
+ });
844
481
  }
845
482
  } catch (error) {
846
- console.error('[GridLayout] Error updating columnWidthRefsMap in handleDragStopped:', error);
483
+ console.error('[GridLayout] Chyba při detekci změn v handleDragStopped:', error);
847
484
  }
848
485
 
849
- // OCHRANA: Pokud ještě není načten layout z API, přidáme akci do pending queue
850
- // Pending actions se zpracují až po dokončení inicializace (načtení + aplikování layoutu)
851
- if (!isInitializedRef.current && autoSaveRef.current) { // ✅ FIX #3+: Použít refs
852
- setPendingActions((prev) => [
853
- ...prev,
854
- { type: 'dragStopped', timestamp: Date.now() },
855
- ]);
486
+ hasUnsavedChangesRef.current = true; // ref only žádný re-render
487
+
488
+ if (!isInitializedRef.current) {
489
+ const checkInterval = setInterval(() => {
490
+ if (isInitializedRef.current) {
491
+ clearInterval(checkInterval);
492
+ if (autoSaveRef.current && debouncedSaveRef.current) {
493
+ debouncedSaveRef.current();
494
+ }
495
+ }
496
+ }, 100);
497
+ setTimeout(() => clearInterval(checkInterval), 5000);
856
498
  return;
857
499
  }
858
500
 
859
- // Uložit do API pouze pokud už je layout načten a aplikován
860
- if (autoSaveRef.current && debouncedSaveRef.current) { // ✅ FIX #3+: Použít refs
501
+ if (autoSaveRef.current && debouncedSaveRef.current) {
861
502
  debouncedSaveRef.current();
862
503
  }
863
- }, []); // ✅ FIX #3+: ŽÁDNÉ dependencies → handler se nikdy neregeneruje
504
+ }, []);
864
505
 
865
- // Handler pro DOKONČENÍ resize - spouští auto-save
866
- // DŮLEŽITÉ: Reaguje pouze na finished === true (po uvolnění myši)
867
506
  const handleColumnResized = useCallback((event) => {
868
- if (!enabledRef.current || !isGridReadyRef.current) return; // ✅ FIX #3+: Použít refs
869
-
870
- // Reagujeme POUZE na dokončení resize operace (po uvolnění myši)
871
- // To zabrání poskakování sloupců během tažení
507
+ if (!enabledRef.current || !isGridReadyRef.current) return;
872
508
  if (!event || event.finished !== true) return;
873
509
 
874
- setHasUnsavedChanges(true);
510
+ hasUnsavedChangesRef.current = true; // ref only → žádný re-render
875
511
 
876
- // Uložit aktuální šířky sloupců do ref map pro persistenci
877
- // DŮLEŽITÉ: NEMĚNÍME columnWidthsVersion - AG-Grid už má správnou šířku ve svém interním stavu
878
- // Změna columnWidthsVersion by triggerovala přepočet preTransformedColumnDefs a reset šířek
879
512
  try {
880
- if (gridApiRef.current && typeof gridApiRef.current.getColumnState === 'function') {
881
- const currentColumnState = gridApiRef.current.getColumnState();
882
-
883
- if (currentColumnState && Array.isArray(currentColumnState)) {
884
- // Uložit šířky do ref map pro každý sloupec (pro API save a další načtení)
885
- currentColumnState.forEach(colState => {
886
- if (colState.colId && colState.width) {
887
- columnWidthRefsMap.current.set(colState.colId, colState.width);
888
- }
889
- });
890
- // NEPOUŽÍVÁME setColumnWidthsVersion zde - způsobilo by reset šířek!
891
- }
513
+ const currentColumnState = getColumnState(gridApiRef, columnApiRef);
514
+ if (currentColumnState && Array.isArray(currentColumnState)) {
515
+ currentColumnState.forEach(colState => {
516
+ if (colState.colId && colState.width) {
517
+ columnWidthRefsMap.current.set(colState.colId, colState.width);
518
+ }
519
+ });
892
520
  }
893
521
  } catch (error) {
894
- console.error('[GridLayout] Error updating columnWidthRefsMap in handleColumnResized:', error);
522
+ console.error('[GridLayout] Chyba při aktualizaci šířek v handleColumnResized:', error);
895
523
  }
896
524
 
897
- // Spustit debounced save do API
898
- // OCHRANA: Ukládáme pouze pokud už byl layout načten z API (isInitialized)
899
- if (autoSaveRef.current && isInitializedRef.current && debouncedSaveRef.current) { // ✅ FIX #3+: Použít refs
525
+ if (autoSaveRef.current && isInitializedRef.current && debouncedSaveRef.current) {
900
526
  debouncedSaveRef.current();
901
527
  }
902
- }, []); // ✅ FIX #3+: ŽÁDNÉ dependencies → handler se nikdy neregeneruje
528
+ }, []);
903
529
 
904
530
  const handleColumnVisible = useCallback(() => {
905
- if (!enabledRef.current || !isGridReadyRef.current) return; // ✅ FIX #3+: Použít refs místo přímých hodnot
906
- setHasUnsavedChanges(true);
907
- // OCHRANA: Ukládáme pouze pokud byl layout načten z API (isInitialized)
908
- if (autoSaveRef.current && isInitializedRef.current && debouncedSaveRef.current) { // ✅ FIX #3+: Použít refs
531
+ if (!enabledRef.current || !isGridReadyRef.current) return;
532
+ hasUnsavedChangesRef.current = true; // ref only → žádný re-render
533
+ if (autoSaveRef.current && isInitializedRef.current && debouncedSaveRef.current) {
909
534
  debouncedSaveRef.current();
910
535
  }
911
- }, []); // ✅ FIX #3+: ŽÁDNÉ dependencies → handler se nikdy neregeneruje
536
+ }, []);
912
537
 
913
538
  const handleColumnPinned = useCallback(() => {
914
- if (!enabledRef.current || !isGridReadyRef.current) return; // ✅ FIX #3+: Použít refs místo přímých hodnot
915
- setHasUnsavedChanges(true);
916
- // OCHRANA: Ukládáme pouze pokud byl layout načten z API (isInitialized)
917
- if (autoSaveRef.current && isInitializedRef.current && debouncedSaveRef.current) { // ✅ FIX #3+: Použít refs
539
+ if (!enabledRef.current || !isGridReadyRef.current) return;
540
+ hasUnsavedChangesRef.current = true; // ref only → žádný re-render
541
+ if (autoSaveRef.current && isInitializedRef.current && debouncedSaveRef.current) {
918
542
  debouncedSaveRef.current();
919
543
  }
920
- }, []); // ✅ FIX #3+: ŽÁDNÉ dependencies → handler se nikdy neregeneruje
921
-
922
- /**
923
- * AG-Grid Ready handler
924
- */
925
- const handleGridReady = useCallback(
926
- (params) => {
927
- if (!enabledRef.current) { // ✅ FIX #3+: Použít ref místo přímé hodnoty
928
- return;
929
- }
544
+ }, []);
930
545
 
931
- gridApiRef.current = params.api;
932
- // AG Grid v31+ - columnApi je deprecated, všechny metody jsou v hlavním api
933
- // Pokud columnApi neexistuje, použijeme main api jako fallback
934
- columnApiRef.current = params.columnApi || params.api;
935
- // Okamžitě označíme grid jako připravený pro event handlery
936
- setIsGridReady(true);
546
+ // ==========================================================================
547
+ // AG-GRID READY HANDLER
548
+ // ==========================================================================
549
+ const handleGridReady = useCallback((params) => {
550
+ if (!enabledRef.current) return;
551
+ gridApiRef.current = params.api;
552
+ columnApiRef.current = params.columnApi || params.api;
553
+ setIsGridReady(true);
554
+ }, []);
937
555
 
938
- // POZOR: Nenastavujeme isInitialized zde, protože by to zabránilo aplikování layoutu!
939
- // isInitialized se nastaví až po aplikování layoutu v applySavedLayout
940
- },
941
- [] // ✅ FIX #3+: ŽÁDNÉ dependencies → handler se nikdy neregeneruje (savedFields, isInitialized, applySavedLayout se nepoužívají)
942
- );
556
+ // ==========================================================================
557
+ // EFFECTS
558
+ // ==========================================================================
943
559
 
944
- /**
945
- * Effect pro aplikování layoutu při prvotním načtení savedFields
946
- */
947
560
  useEffect(() => {
948
- // Aplikujeme layout pokud:
949
- // 1. Máme savedFields z API (i když prázdné - první zobrazení modulu)
950
- // 2. Grid je ready (má API references)
951
- // 3. Ještě jsme neinicializovali layout
952
561
  if (savedFields?.records !== undefined && gridApiRef.current && !isInitialized) {
953
562
  if (savedFields.records.length > 0) {
954
563
  applySavedLayout();
955
564
  } else {
956
- // Pro prázdné savedFields jen nastavíme inicializaci
957
565
  setIsInitialized(true);
958
566
  setIsGridReady(true);
959
567
  }
960
568
  }
961
569
  }, [savedFields?.records, isInitialized, applySavedLayout]);
962
570
 
963
-
964
- /**
965
- * Effect pro error handling
966
- */
967
571
  useEffect(() => {
968
572
  if (fieldsError) {
969
573
  handleError(fieldsError, 'při načítání saved layoutu');
970
574
  }
971
575
  }, [fieldsError, handleError]);
972
576
 
973
- /**
974
- * Cleanup effect
975
- */
976
- useEffect(() => {
977
- return () => {
978
- debouncedSave.cancel();
979
- };
980
- }, [debouncedSave]);
577
+ // ==========================================================================
578
+ // MANUÁLNÍ AKCE (re-render je zde OK - explicitní uživatelská akce)
579
+ // ==========================================================================
981
580
 
982
- /**
983
- * Resetuje layout na default
984
- */
985
581
  const resetToDefault = useCallback(async () => {
986
582
  if (!enabled) return;
987
-
988
- // Ověříme dostupnost resetColumnState metody (AG Grid v31+ má ji v main API)
989
- let resetColumnStateApi = null;
990
- if (
991
- gridApiRef.current &&
992
- typeof gridApiRef.current.resetColumnState === 'function'
993
- ) {
994
- resetColumnStateApi = gridApiRef.current;
995
- } else if (
996
- columnApiRef.current &&
997
- typeof columnApiRef.current.resetColumnState === 'function'
998
- ) {
999
- resetColumnStateApi = columnApiRef.current;
1000
- } else {
1001
- console.warn('[GridLayout] resetColumnState method not available');
1002
- return;
1003
- }
583
+ const resetApi = getResetApi(gridApiRef, columnApiRef);
584
+ if (!resetApi) return;
1004
585
 
1005
586
  try {
1006
587
  setIsLoading(true);
588
+ resetApi.resetColumnState();
589
+ headerNameMapRef.current.clear();
1007
590
 
1008
- // Resetujeme AG-Grid na původní column definitions
1009
- resetColumnStateApi.resetColumnState();
1010
-
1011
- // Pokud je autoSave zapnuté, uložíme resetovaný stav
1012
591
  if (autoSave) {
1013
592
  await saveCurrentLayout();
1014
593
  } else {
1015
- setHasUnsavedChanges(true);
594
+ hasUnsavedChangesRef.current = true;
1016
595
  }
1017
596
  } catch (error) {
1018
597
  handleError(error, 'při resetování layoutu');
@@ -1021,36 +600,29 @@ export const useGridLayout = ({
1021
600
  }
1022
601
  }, [enabled, autoSave, saveCurrentLayout, handleError]);
1023
602
 
1024
- /**
1025
- * Manuální uložení
1026
- */
1027
603
  const saveLayout = useCallback(async () => {
1028
604
  await saveCurrentLayout();
1029
605
  }, [saveCurrentLayout]);
1030
606
 
1031
- /**
1032
- * Manuální reload
1033
- */
1034
607
  const reloadLayout = useCallback(async () => {
1035
608
  try {
1036
- // Save a reference to the current grid API before resetting states
1037
- const savedGridApiRef = gridApiRef.current;
1038
- const savedColumnApiRef = columnApiRef.current;
609
+ const savedGridApi = gridApiRef.current;
610
+ const savedColumnApi = columnApiRef.current;
611
+
612
+ // Reset zámku - při reloadu chceme přepočítat preTransformedColumnDefs
613
+ lockedColumnDefsRef.current = null;
1039
614
 
1040
615
  setIsInitialized(false);
1041
616
  setIsGridReady(false);
1042
- await refetchFields();
617
+ const freshData = await refetchFields();
1043
618
 
1044
- // Restore saved references if they were lost during the reload process
1045
- if (!gridApiRef.current && savedGridApiRef) {
1046
- gridApiRef.current = savedGridApiRef;
1047
- }
619
+ if (!gridApiRef.current && savedGridApi) gridApiRef.current = savedGridApi;
620
+ if (!columnApiRef.current && savedColumnApi) columnApiRef.current = savedColumnApi;
1048
621
 
1049
- if (!columnApiRef.current && savedColumnApiRef) {
1050
- columnApiRef.current = savedColumnApiRef;
622
+ if (freshData?.data?.records) {
623
+ setFrozenSavedRecords(freshData.data.records);
1051
624
  }
1052
625
 
1053
- // Po refetch dat obnovíme oba stavy (layout je už v DB)
1054
626
  setIsInitialized(true);
1055
627
  setIsGridReady(true);
1056
628
  } catch (error) {
@@ -1058,45 +630,18 @@ export const useGridLayout = ({
1058
630
  }
1059
631
  }, [refetchFields, handleError]);
1060
632
 
1061
- /**
1062
- * Získá aktuální column state pro Column Editor
1063
- */
633
+ // ==========================================================================
634
+ // COLUMN EDITOR
635
+ // ==========================================================================
636
+
1064
637
  const getCurrentColumnsForEditor = useCallback(() => {
1065
- // Zajistíme, že máme validní columnDefs
1066
638
  const validColumnDefs = Array.isArray(columnDefs) ? columnDefs : [];
1067
639
 
1068
640
  try {
1069
- let currentColumnState = null;
1070
-
1071
- // Pokusíme se získat column state s různými fallbacky pro AG Grid v31+
1072
- if (
1073
- gridApiRef.current &&
1074
- typeof gridApiRef.current.getColumnState === 'function'
1075
- ) {
1076
- // AG Grid v31+ - getColumnState je přímo v main API
1077
- currentColumnState = gridApiRef.current.getColumnState();
1078
- } else if (
1079
- columnApiRef.current &&
1080
- typeof columnApiRef.current.getColumnState === 'function'
1081
- ) {
1082
- // Starší verze AG Grid - getColumnState je v columnApi
1083
- currentColumnState = columnApiRef.current.getColumnState();
1084
- } else {
1085
- throw new Error(
1086
- 'getColumnState method is not available on gridApiRef or columnApiRef'
1087
- );
1088
- }
1089
-
1090
- if (
1091
- !Array.isArray(currentColumnState) ||
1092
- currentColumnState.length === 0
1093
- ) {
1094
- // Fallback pokud getColumnState() nevrátí validní array
1095
- if (lastKnownColumnState) {
1096
- return lastKnownColumnState;
1097
- }
641
+ const currentColumnState = getColumnState(gridApiRef, columnApiRef);
1098
642
 
1099
- const defaultColumns = validColumnDefs.map((col, index) => ({
643
+ if (!Array.isArray(currentColumnState) || currentColumnState.length === 0) {
644
+ return validColumnDefs.map((col, index) => ({
1100
645
  id: col.field,
1101
646
  field: col.field,
1102
647
  headerName: col.headerName || col.field,
@@ -1105,27 +650,24 @@ export const useGridLayout = ({
1105
650
  originalWidth: col.width || 100,
1106
651
  visible: !col.hide,
1107
652
  order: index,
1108
- originalOrder: index, // Původní pořadí z columnDefs
653
+ originalOrder: index,
1109
654
  }));
1110
-
1111
- // POZN: Neukládáme do state, aby nedocházelo k infinite loop
1112
- return defaultColumns;
1113
655
  }
1114
656
 
1115
- const result = currentColumnState.map((columnStateItem, index) => {
1116
- // Najdeme odpovídající column definition podle colId (zohledníme field i colId)
1117
- const colDef =
1118
- validColumnDefs.find(
1119
- (cd) =>
1120
- cd.field === columnStateItem.colId ||
1121
- cd.colId === columnStateItem.colId
1122
- ) || {};
1123
- // Pokusíme se najít saved hodnotu pro headerName
657
+ return currentColumnState.map((columnStateItem, index) => {
658
+ const colDef = validColumnDefs.find(
659
+ (cd) => cd.field === columnStateItem.colId || cd.colId === columnStateItem.colId
660
+ ) || {};
661
+
1124
662
  const savedField = savedFields?.records?.find(
1125
663
  (sf) => sf.fieldName === columnStateItem.colId
1126
664
  );
665
+ const headerName =
666
+ headerNameMapRef.current.get(columnStateItem.colId) ||
667
+ savedField?.headerName ||
668
+ colDef.headerName ||
669
+ columnStateItem.colId;
1127
670
 
1128
- // Najdeme původní index z columnDefs
1129
671
  const originalIndex = validColumnDefs.findIndex(
1130
672
  (cd) => (cd.field || cd.colId) === columnStateItem.colId
1131
673
  );
@@ -1133,284 +675,212 @@ export const useGridLayout = ({
1133
675
  return {
1134
676
  id: columnStateItem.colId,
1135
677
  field: columnStateItem.colId,
1136
- headerName:
1137
- savedField?.headerName ||
1138
- colDef.headerName ||
1139
- columnStateItem.colId,
1140
- // Původní headerName z columnDefs (ne z uložených uživatelských preferencí)
678
+ headerName,
1141
679
  originalHeaderName: colDef.headerName || columnStateItem.colId,
1142
680
  width: columnStateItem.width || colDef.width || 100,
1143
681
  originalWidth: colDef.width || 100,
1144
682
  visible: !columnStateItem.hide,
1145
683
  order: index,
1146
- originalOrder: originalIndex !== -1 ? originalIndex : index, // Původní pořadí z columnDefs
684
+ originalOrder: originalIndex !== -1 ? originalIndex : index,
1147
685
  };
1148
686
  });
1149
-
1150
- // POZN: Neukládáme do state, aby nedocházelo k infinite loop
1151
- // Tato aktualizace proběhne v useEffect
1152
- return result;
1153
687
  } catch (error) {
1154
- console.error('[GridLayout] Error in getCurrentColumnsForEditor:', error);
1155
-
1156
- // Použijeme lastKnownColumnState, pokud existuje
1157
- if (lastKnownColumnState) {
1158
- return lastKnownColumnState;
1159
- }
1160
-
1161
- // Poslední fallback na columnDefs
1162
- const defaultColumns = validColumnDefs.map((col, index) => ({
688
+ console.error('[GridLayout] Chyba v getCurrentColumnsForEditor:', error);
689
+ return validColumnDefs.map((col, index) => ({
1163
690
  id: col.field || col.colId,
1164
691
  field: col.field || col.colId,
1165
692
  headerName: col.headerName || col.field || col.colId,
1166
- // Vždy používáme původní definici jako referenci pro porovnání
1167
693
  originalHeaderName: col.headerName || col.field || col.colId,
1168
694
  width: col.width || 100,
1169
695
  originalWidth: col.width || 100,
1170
696
  visible: !col.hide,
1171
697
  order: index,
1172
- originalOrder: index, // Původní pořadí z columnDefs
698
+ originalOrder: index,
1173
699
  }));
1174
-
1175
- // POZN: Neukládáme do state, aby nedocházelo k infinite loop
1176
- return defaultColumns;
1177
700
  }
1178
- }, [columnDefs, savedFields, lastKnownColumnState]);
701
+ }, [columnDefs, savedFields]);
1179
702
 
1180
- /**
1181
- * Otevře Column Editor modal
1182
- */
1183
703
  const openColumnEditor = useCallback(() => {
1184
- // Před otevřením editoru si pouze zaznamenáme, že chceme aktualizovat stav sloupců
1185
- // Samotná aktualizace proběhne v useEffect, ne během renderu
1186
704
  setIsColumnEditorOpen(true);
1187
705
  }, []);
1188
706
 
1189
- /**
1190
- * Zavře Column Editor modal
1191
- */
1192
707
  const closeColumnEditor = useCallback(() => {
1193
708
  setIsColumnEditorOpen(false);
1194
709
  }, []);
1195
710
 
1196
- /**
1197
- * Uloží změny z Column Editoru
1198
- * @param {Array} editedColumns - Editované sloupce z modalu
1199
- */
1200
711
  const saveColumnEditorChanges = async (editedColumns) => {
1201
712
  if (!enabled) return;
1202
713
 
1203
714
  try {
1204
715
  setIsLoading(true);
1205
- setIsSaving(true);
716
+ isSavingRef.current = true;
1206
717
 
1207
- // Filtrujeme pouze validní sloupce (s definovaným field)
1208
- const validColumns = editedColumns.filter(
1209
- (col) => col.field && col.field !== undefined
1210
- );
718
+ const validColumns = editedColumns.filter(col => col.field && col.field !== undefined);
1211
719
 
1212
- // Připravíme UserFields pro API podle C# SaveUserFieldModel struktury
1213
720
  const completeUserFields = validColumns.map((col, index) => ({
1214
- Id: 0, // Nové pole má ID = 0, existující budou mít správné ID z API
721
+ Id: 0,
1215
722
  UserKey: userKey,
1216
723
  ApplicationName: applicationName,
1217
724
  GridName: gridName,
1218
725
  FilterName: filterName || null,
1219
726
  FieldName: col.field,
1220
727
  HeaderName: col.headerName || col.field,
1221
- Order: index, // Pořadí podle pozice v editedColumns
728
+ Order: index,
1222
729
  Show: col.visible,
1223
- Width: col.width != null ? col.width : null, // Zachováváme i nulovou šířku
730
+ Width: col.width != null ? col.width : null,
1224
731
  System: false,
1225
732
  }));
1226
733
 
1227
- // Uložení do API
1228
- gridLayoutApi
1229
- .saveGridLayout(completeUserFields)
1230
- .then((result) => {
1231
- if (result.success) {
1232
- setHasUnsavedChanges(false);
1233
-
1234
- // Znovunačtení layoutu z API a aplikování na grid
1235
- reloadLayout().then(async () => {
1236
- // Po reloadLayout() počkáme na aktualizaci savedFields a aplikujeme layout
1237
- try {
1238
- // Znovu načteme fieldy pro zajištění synchronizace
1239
- const freshFields = await refetchFields();
1240
-
1241
- setTimeout(() => {
1242
- if (gridApiRef.current && freshFields?.data?.records) {
1243
-
1244
- // Aplikujeme layout s novými daty
1245
- const columnState = gridLayoutApi.transformFieldsToColumnState(
1246
- freshFields.data.records,
1247
- columnDefs
1248
- );
1249
-
1250
- if (columnState && columnState.length > 0) {
1251
- // Najdeme správné API pro applyColumnState
1252
- let applyColumnStateApi = null;
1253
- if (gridApiRef.current?.applyColumnState) {
1254
- applyColumnStateApi = gridApiRef.current;
1255
- } else if (columnApiRef.current?.applyColumnState) {
1256
- applyColumnStateApi = columnApiRef.current;
1257
- }
1258
-
1259
- if (applyColumnStateApi) {
1260
- applyColumnStateApi.applyColumnState({
1261
- state: columnState,
1262
- applyOrder: true,
1263
- defaultState: {
1264
- sort: null,
1265
- sortIndex: null,
1266
- pivot: null,
1267
- rowGroup: null,
1268
- },
1269
- });
1270
-
1271
- // Aktualizujeme headerName pro každý sloupec
1272
- const headerNameMap = new Map();
1273
- freshFields.data.records.forEach((field) => {
1274
- if (field.fieldName && field.headerName) {
1275
- headerNameMap.set(field.fieldName, field.headerName);
1276
- }
1277
- });
1278
-
1279
- // Aplikujeme nové headerName hodnoty přes DOM
1280
- setTimeout(() => {
1281
- try {
1282
- const headerCells = document.querySelectorAll('.ag-header-cell');
1283
- headerCells.forEach((headerCell) => {
1284
- const colId = headerCell.getAttribute('col-id');
1285
- if (colId && headerNameMap.has(colId)) {
1286
- const headerTextEl = headerCell.querySelector('.ag-header-cell-text');
1287
- if (headerTextEl) {
1288
- headerTextEl.textContent = headerNameMap.get(colId);
1289
- }
1290
- }
1291
- });
1292
-
1293
- // Vynutíme refresh hlavičky
1294
- if (gridApiRef.current?.refreshHeader) {
1295
- gridApiRef.current.refreshHeader();
1296
- }
1297
- } catch (headerError) {
1298
- console.error('[GridLayout] Error updating headers:', headerError);
1299
- }
1300
- }, 50);
734
+ // Aktualizace headerName ref mapy
735
+ validColumns.forEach(col => {
736
+ if (col.field && col.headerName) {
737
+ headerNameMapRef.current.set(col.field, col.headerName);
738
+ }
739
+ });
740
+
741
+ const result = await gridLayoutApi.saveGridLayout(completeUserFields);
742
+
743
+ if (result.success) {
744
+ hasUnsavedChangesRef.current = false;
745
+
746
+ try {
747
+ const freshFields = await refetchFields();
748
+
749
+ // Reset zámku a aktualizace frozenSavedRecords → uvolní zámek v preTransformedColumnDefs
750
+ lockedColumnDefsRef.current = null;
751
+ if (freshFields?.data?.records) {
752
+ setFrozenSavedRecords(freshFields.data.records);
753
+ }
754
+
755
+ // Aplikujeme nový layout na grid
756
+ setTimeout(() => {
757
+ if (gridApiRef.current && freshFields?.data?.records) {
758
+ const columnState = gridLayoutApi.transformFieldsToColumnState(
759
+ freshFields.data.records,
760
+ columnDefs
761
+ );
762
+
763
+ if (columnState && columnState.length > 0) {
764
+ const applyApi = getApplyApi(gridApiRef, columnApiRef);
765
+ if (applyApi) {
766
+ // KROK 1: Aktualizace headerName
767
+ try {
768
+ const currentColDefs = gridApiRef.current.getColumnDefs?.();
769
+ if (currentColDefs && Array.isArray(currentColDefs)) {
770
+ const updatedColDefs = currentColDefs.map(colDef => {
771
+ const fieldName = colDef.field;
772
+ const newName = headerNameMapRef.current.get(fieldName);
773
+ if (newName && colDef.headerName !== newName) {
774
+ return { ...colDef, headerName: newName };
775
+ }
776
+ return colDef;
777
+ });
778
+ if (typeof gridApiRef.current.setColumnDefs === 'function') {
779
+ gridApiRef.current.setColumnDefs(updatedColDefs);
1301
780
  }
1302
781
  }
782
+ } catch (e) {
783
+ console.error('[GridLayout] Chyba při aktualizaci headerName v editoru:', e);
1303
784
  }
1304
- }, 150);
1305
- } catch (refreshError) {
1306
- console.error('[GridLayout] Error in post-save refresh:', refreshError);
1307
- // Fallback na standardní applySavedLayout
1308
- setTimeout(() => {
1309
- if (gridApiRef.current) {
1310
- applySavedLayout(true);
1311
- }
1312
- }, 200);
1313
- }
1314
- });
1315
785
 
1316
- if (onLayoutSaved) {
1317
- onLayoutSaved(completeUserFields);
786
+ // KROK 2: Aplikovat column state
787
+ applyApi.applyColumnState({
788
+ state: columnState,
789
+ applyOrder: true,
790
+ defaultState: { sort: null, sortIndex: null, pivot: null, rowGroup: null },
791
+ });
792
+
793
+ try { gridApiRef.current?.refreshHeader?.(); } catch (e) { /* ignorujeme */ }
794
+ }
795
+ }
1318
796
  }
1319
- }
1320
- })
1321
- .catch((error) => {
1322
- handleError(error, 'při ukládání změn z Column Editoru');
1323
- });
797
+ }, 150);
798
+ } catch (refreshError) {
799
+ console.error('[GridLayout] Chyba při obnově po uložení z editoru:', refreshError);
800
+ setTimeout(() => {
801
+ if (gridApiRef.current) applySavedLayout(true);
802
+ }, 200);
803
+ }
804
+
805
+ if (onLayoutSaved) {
806
+ onLayoutSaved(completeUserFields);
807
+ }
808
+ }
1324
809
  } catch (error) {
1325
810
  handleError(error, 'při ukládání změn z Column Editoru');
1326
811
  } finally {
1327
812
  setIsLoading(false);
1328
- setIsSaving(false);
813
+ isSavingRef.current = false;
1329
814
  }
1330
815
  };
1331
- //, [enabled, gridLayoutApi, onLayoutSaved, handleError, userKey, applicationName, gridName, filterName, reloadLayout]);
1332
816
 
1333
- // Logika pro zobrazení columnDefs
817
+ // ==========================================================================
818
+ // PRE-TRANSFORMED COLUMN DEFS
819
+ // ZAMKNUTÍ PO INICIALIZACI → stabilní reference, žádné zbytečné přepočty
820
+ // ==========================================================================
1334
821
  const shouldShowColumns = useMemo(() => {
1335
- if (!waitForSavedFields) return true; // Pokud není waitForSavedFields, vždy zobrazuj
1336
-
1337
- // Pokud je waitForSavedFields true, čekáme na dokončení načítání
822
+ if (!waitForSavedFields) return true;
1338
823
  return !isFieldsLoading && !isLoading;
1339
824
  }, [waitForSavedFields, isFieldsLoading, isLoading]);
1340
825
 
1341
- // Pre-transformované columnDefs podle savedFields pro waitForSavedFields mode
1342
- // DŮLEŽITÉ: Toto useMemo nyní obsahuje i AgGridColumns transformaci pro eliminaci circular dependency
1343
826
  const preTransformedColumnDefs = useMemo(() => {
1344
- // Použijeme aktualizované columnDefs ze stableColumnDefsRef pokud existují
1345
- const columnDefsToUse = stableColumnDefsRef.current; // || columnDefs; //HLO
1346
-
827
+ // ── ZÁMEK: Po inicializaci vrátíme zamknutou referenci ──
828
+ // Zámek se uvolní jen při: změně columnDefs od parenta, frozenSavedRecords (editor/reload)
829
+ // Tím se zabrání tomu, aby AG-Grid dostal nové columnDefs při jakémkoliv re-renderu
830
+ if (isInitialized
831
+ && lockedColumnDefsRef.current
832
+ && lockedForColumnDefsRef.current === columnDefs
833
+ && lockedForFrozenRecordsRef.current === frozenSavedRecords) {
834
+ return lockedColumnDefsRef.current;
835
+ }
836
+
837
+ // ── Normální výpočet ──
838
+ const columnDefsToUse = stableColumnDefsRef.current;
1347
839
  let baseColumnDefs = columnDefsToUse;
1348
840
 
1349
- // Pro waitForSavedFields aplikujeme grid layout transformace
1350
- if (
1351
- waitForSavedFields &&
1352
- savedFields?.records &&
1353
- Array.isArray(columnDefsToUse)
1354
- ) {
1355
- // Transformujeme columnDefs podle savedFields PŘED zobrazením gridu
841
+ // Pro waitForSavedFields: seřadíme columnDefs podle frozenSavedRecords
842
+ if (waitForSavedFields && frozenSavedRecords && Array.isArray(columnDefsToUse)) {
1356
843
  const columnState = gridLayoutApi.transformFieldsToColumnState(
1357
- savedFields.records,
844
+ frozenSavedRecords,
1358
845
  columnDefsToUse
1359
846
  );
1360
847
 
1361
848
  if (columnState && columnState.length > 0) {
1362
- // Vytvoříme mapu pro rychlé vyhledávání
1363
849
  const columnStateMap = new Map();
1364
850
  columnState.forEach((colState, index) => {
1365
851
  columnStateMap.set(colState.colId, { ...colState, __order: index });
1366
852
  });
1367
853
 
1368
- // Vytvoříme mapu fieldName -> headerName z API dat
1369
854
  const headerNameMap = new Map();
1370
- savedFields.records.forEach((field) => {
855
+ frozenSavedRecords.forEach(field => {
1371
856
  if (field.fieldName && field.headerName) {
1372
857
  headerNameMap.set(field.fieldName, field.headerName);
1373
858
  }
1374
859
  });
1375
860
 
1376
- // Seřadíme columnDefs podle pořadí z columnState a upravíme headerName podle savedFields
1377
861
  baseColumnDefs = [...columnDefsToUse]
1378
862
  .sort((a, b) => {
1379
863
  const fieldA = a.field || a.colId;
1380
864
  const fieldB = b.field || b.colId;
1381
- const aState = columnStateMap.get(fieldA);
1382
- const bState = columnStateMap.get(fieldB);
1383
-
1384
- const aOrder = aState?.__order ?? 999;
1385
- const bOrder = bState?.__order ?? 999;
1386
-
865
+ const aOrder = columnStateMap.get(fieldA)?.__order ?? 999;
866
+ const bOrder = columnStateMap.get(fieldB)?.__order ?? 999;
1387
867
  return aOrder - bOrder;
1388
868
  })
1389
- .map((colDef) => {
869
+ .map(colDef => {
1390
870
  const fieldId = colDef.field || colDef.colId;
1391
871
  const columnStateItem = columnStateMap.get(fieldId);
1392
-
1393
- // Aplikujeme headerName, hide i width z API pro waitForSavedFields režim
1394
- if (fieldId && (headerNameMap.has(fieldId) || columnStateItem)) {
1395
- return {
1396
- ...colDef,
1397
- // Aplikujeme headerName z savedFields
1398
- ...(headerNameMap.has(fieldId) && { headerName: headerNameMap.get(fieldId) }),
1399
- // Aplikujeme hide hodnotu z columnState (z API show hodnoty)
1400
- ...(columnStateItem && { hide: columnStateItem.hide }),
1401
- // Aplikujeme také width pro konzistentní zobrazení
1402
- ...(columnStateItem && columnStateItem.width && { width: columnStateItem.width }),
1403
- };
1404
- }
1405
- return colDef;
872
+ return {
873
+ ...colDef,
874
+ ...(headerNameMap.has(fieldId) && { headerName: headerNameMap.get(fieldId) }),
875
+ ...(columnStateItem && { hide: columnStateItem.hide }),
876
+ ...(columnStateItem?.width && { width: columnStateItem.width }),
877
+ };
1406
878
  });
1407
879
  }
1408
880
  }
1409
881
 
1410
- // Nyní VŽDY aplikujeme AgGridColumns transformaci PŘÍMO zde místo v props
1411
- // Tím eliminujeme circular dependency pro všechny případy použití
882
+ // Konverze ColumnBuilder na array
1412
883
  if (!Array.isArray(baseColumnDefs)) {
1413
- // Pokud baseColumnDefs není array, mohlo by to být ColumnBuilder
1414
884
  if (baseColumnDefs && typeof baseColumnDefs.build === 'function') {
1415
885
  baseColumnDefs = baseColumnDefs.build();
1416
886
  } else {
@@ -1418,63 +888,45 @@ export const useGridLayout = ({
1418
888
  }
1419
889
  }
1420
890
 
1421
- // Aplikujeme ref šířky jako poslední krok POUZE pokud už byla provedena inicializace
1422
- // Při prvotním načítání zachováváme API šířky (které jsou už v baseColumnDefs pro waitForSavedFields)
891
+ // Aplikace ref šířek (POUZE po inicializaci)
1423
892
  const finalColumnDefs = baseColumnDefs.map(colDef => {
1424
893
  const fieldId = colDef.field || colDef.colId;
1425
894
  const refWidth = columnWidthRefsMap.current.get(fieldId);
1426
-
1427
- // Použijeme ref šířku POUZE pokud už je grid inicializovaný (uživatel manipuloval s šířkami)
1428
895
  if (refWidth !== undefined && isInitialized) {
1429
- return {
1430
- ...colDef,
1431
- width: refWidth
1432
- };
896
+ return { ...colDef, width: refWidth };
1433
897
  }
1434
898
  return colDef;
1435
899
  });
1436
900
 
901
+ // ── Zamknout po inicializaci ──
902
+ if (isInitialized) {
903
+ lockedColumnDefsRef.current = finalColumnDefs;
904
+ lockedForColumnDefsRef.current = columnDefs;
905
+ lockedForFrozenRecordsRef.current = frozenSavedRecords;
906
+ }
907
+
1437
908
  return finalColumnDefs;
909
+ }, [waitForSavedFields, frozenSavedRecords, columnDefs, gridLayoutApi, columnWidthsVersion, isInitialized]);
1438
910
 
1439
- // // Replikujeme AgGridColumns logiku pro všechny column definitions:
1440
- // const processedColumns = baseColumnDefs?.map((column) => {
1441
- // // Zkopírujeme column aby se předešlo mutacím
1442
- // const processedColumn = { ...column };
1443
-
1444
- // // Aplikujeme tooltip logiku pokud je potřeba
1445
- // if (processedColumn.contentTooltip) {
1446
- // // Statické mapování tooltipů pro lepší performance a eliminaci circular dependency
1447
- // switch (processedColumn.contentTooltip) {
1448
- // case 'User':
1449
- // processedColumn.tooltipComponent = Tooltip.User;
1450
- // break;
1451
- // default:
1452
- // console.warn(`[GridLayout] Unknown tooltip component: ${processedColumn.contentTooltip}`);
1453
- // }
1454
- // }
1455
-
1456
- // return processedColumn;
1457
- // }) || [];
1458
-
1459
- // return processedColumns;
1460
- }, [waitForSavedFields, savedFields?.records, columnDefs, gridLayoutApi, columnWidthsVersion, isInitialized]);
1461
-
1462
- // ✅ FIX #10: NEPOUŽÍVAT memoizaci - spoléháme se na refs v Grid komponentě
1463
- // Memoizace s dependencies způsobuje rerender při změně JAKÉKOLIV hodnoty (např. hasUnsavedChanges)
1464
- // Refs v Grid komponentě zajišťují, že i když se tento objekt změní, Grid nevidí změnu
911
+ // ==========================================================================
912
+ // RETURN - API interface
913
+ // Pozadí hodnoty se čtou z refs (aktuální hodnota v okamžiku renderu)
914
+ // ==========================================================================
1465
915
  return {
1466
- // State
916
+ // State (re-render pouze při explicitních akcích)
1467
917
  isLoading: isLoading || isFieldsLoading,
1468
- isSaving,
1469
- isApplyingLayout,
1470
918
  error,
1471
- hasUnsavedChanges,
1472
919
  isInitialized,
1473
920
  isGridReady,
1474
921
  shouldShowColumns,
1475
922
  preTransformedColumnDefs,
1476
923
 
1477
- // AG-Grid event handlers (stabilní díky prázdným dependencies)
924
+ // Refs čtené při renderu (ŽÁDNÝ re-render při změně)
925
+ isSaving: isSavingRef.current,
926
+ hasUnsavedChanges: hasUnsavedChangesRef.current,
927
+ isApplyingLayout: isApplyingLayoutRef.current,
928
+
929
+ // AG-Grid event handlers (stabilní - prázdné deps)
1478
930
  onGridReady: handleGridReady,
1479
931
  onColumnMoved: handleColumnMoved,
1480
932
  onDragStopped: handleDragStopped,
@@ -1482,7 +934,7 @@ export const useGridLayout = ({
1482
934
  onColumnVisible: handleColumnVisible,
1483
935
  onColumnPinned: handleColumnPinned,
1484
936
 
1485
- // Manual actions (stabilní)
937
+ // Manuální akce
1486
938
  saveLayout,
1487
939
  resetToDefault,
1488
940
  reloadLayout,