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

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