@bit.rhplus/ui.grid-layout 0.0.1

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.
@@ -0,0 +1,512 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Hlavní hook pro správu grid layout - automatické ukládání a načítání rozvržení sloupců
4
+ * Kombinuje AG-Grid API s Grid Layout službou pro persistence uživatelských preferencí
5
+ */
6
+
7
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
8
+ import { useGridLayoutApi } from './useGridLayoutApi';
9
+ import { debounce } from 'lodash';
10
+
11
+ /**
12
+ * Hook pro správu grid layout s automatickým ukládáním
13
+ * @param {Object} config - Konfigurace grid layout
14
+ * @param {string} config.userKey - Identifikátor uživatele
15
+ * @param {string} config.applicationName - Název aplikace
16
+ * @param {string} config.gridName - Název gridu
17
+ * @param {string} [config.filterName] - Název filtru (volitelné)
18
+ * @param {boolean} [config.enabled=true] - Zapnout/vypnout layout management
19
+ * @param {boolean} [config.autoSave=true] - Automatické ukládání při změnách
20
+ * @param {number} [config.autoSaveDelay=2000] - Zpoždění auto-save v ms
21
+ * @param {Array} config.columnDefs - AG-Grid column definitions
22
+ * @param {string} [config.accessToken] - Přístupový token
23
+ * @param {boolean} [config.waitForSavedFields=false] - Skrýt columnDefs dokud nejsou načtena savedFields
24
+ * @param {Function} [config.onLayoutLoaded] - Callback při načtení layoutu
25
+ * @param {Function} [config.onLayoutSaved] - Callback při uložení layoutu
26
+ * @param {Function} [config.onError] - Callback při chybě
27
+ * @returns {Object} Grid layout management interface
28
+ */
29
+ export const useGridLayout = ({
30
+ userKey,
31
+ applicationName,
32
+ gridName,
33
+ filterName,
34
+ enabled = true,
35
+ autoSave = true,
36
+ autoSaveDelay = 2000,
37
+ columnDefs = [],
38
+ accessToken,
39
+ waitForSavedFields = false, // Nový prop pro odložené zobrazení columnDefs
40
+ onLayoutLoaded,
41
+ onLayoutSaved,
42
+ onError
43
+ }) => {
44
+ // Validace columnDefs - musí být array
45
+ if (columnDefs !== undefined && columnDefs !== null && !Array.isArray(columnDefs)) {
46
+ console.error('useGridLayout: columnDefs musí být array, získán:', typeof columnDefs, columnDefs);
47
+ throw new Error('useGridLayout: columnDefs musí být array');
48
+ }
49
+
50
+ // Refs pro AG-Grid API
51
+ const gridApiRef = useRef(null);
52
+ const columnApiRef = useRef(null);
53
+
54
+ // State
55
+ const [isInitialized, setIsInitialized] = useState(false);
56
+ const [isLoading, setIsLoading] = useState(false);
57
+ const [isSaving, setIsSaving] = useState(false);
58
+ const [error, setError] = useState(null);
59
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
60
+ const [isColumnEditorOpen, setIsColumnEditorOpen] = useState(false);
61
+
62
+ // Grid Layout API hook
63
+ const gridLayoutApi = useGridLayoutApi({
64
+ userKey,
65
+ applicationName,
66
+ gridName,
67
+ filterName,
68
+ accessToken
69
+ });
70
+
71
+ // Query pro načtení saved layoutu
72
+ const {
73
+ data: savedFields,
74
+ isLoading: isFieldsLoading,
75
+ error: fieldsError,
76
+ refetch: refetchFields
77
+ } = gridLayoutApi.useUserFields(
78
+ (columnDefs || []).map((colDef, index) => ({
79
+ name: colDef.field || `column_${index}`,
80
+ displayName: colDef.headerName || colDef.field || `Column ${index + 1}`,
81
+ dataType: colDef.type || 'string',
82
+ isVisible: !colDef.hide,
83
+ width: colDef.width || 100,
84
+ order: index
85
+ })),
86
+ { enabled: enabled && !!userKey && !!gridName && Array.isArray(columnDefs) && columnDefs.length > 0 }
87
+ );
88
+
89
+ /**
90
+ * Error handler
91
+ */
92
+ const handleError = useCallback((error, context = '') => {
93
+ console.error(`Grid Layout Error ${context}:`, error);
94
+ setError(error);
95
+ if (onError) {
96
+ onError(error, context);
97
+ }
98
+ }, [onError]);
99
+
100
+ /**
101
+ * Uloží současný stav sloupců do API
102
+ */
103
+ const saveCurrentLayout = useCallback(async () => {
104
+ if (!enabled || !gridApiRef.current || !columnApiRef.current) return;
105
+
106
+ try {
107
+ setIsSaving(true);
108
+ setError(null);
109
+
110
+ // Získáme současný column state z AG-Grid
111
+ const columnState = columnApiRef.current.getColumnState();
112
+
113
+ // Transformujeme na Grid API format
114
+ const fields = gridLayoutApi.transformColumnStateToFields(columnState, columnDefs);
115
+
116
+ // Uložíme do API
117
+ const result = await gridLayoutApi.saveGridLayout(fields);
118
+
119
+ if (result.success) {
120
+ setHasUnsavedChanges(false);
121
+ if (onLayoutSaved) {
122
+ onLayoutSaved(fields, columnState);
123
+ }
124
+ }
125
+ } catch (error) {
126
+ handleError(error, 'při ukládání layoutu');
127
+ } finally {
128
+ setIsSaving(false);
129
+ }
130
+ }, [enabled, columnDefs, gridLayoutApi, onLayoutSaved, handleError]);
131
+
132
+ /**
133
+ * Debounced auto-save pro předcházení častým voláním API
134
+ */
135
+ const debouncedSave = useMemo(
136
+ () => debounce(saveCurrentLayout, autoSaveDelay),
137
+ [saveCurrentLayout, autoSaveDelay]
138
+ );
139
+
140
+ /**
141
+ * Aplikuje saved layout na AG-Grid
142
+ */
143
+ const applySavedLayout = useCallback(() => {
144
+ if (!savedFields?.records || !columnApiRef.current || isInitialized) return;
145
+
146
+ try {
147
+ setIsLoading(true);
148
+ // Transformujeme Grid API fields na AG-Grid column state
149
+ const columnState = gridLayoutApi.transformFieldsToColumnState(savedFields.records, columnDefs);
150
+ console.log("🚀 ~ useGridLayout ~ columnState:", savedFields.records, columnDefs, columnState)
151
+
152
+ if (columnState && columnState.length > 0) {
153
+ // Pokud je waitForSavedFields true, columnDefs jsou už pre-transformované,
154
+ // takže aplikujeme jen width a hide vlastnosti bez delay pro pořadí
155
+ const applyFunction = () => {
156
+ try {
157
+ console.log("🎯 Applying column state with delay:", {
158
+ columnState,
159
+ columnCount: columnState.length,
160
+ columnIds: columnState.map(c => c.colId),
161
+ hasOrder: true // pořadí je v poli implicitně
162
+ });
163
+
164
+ // Debug info o columnApiRef
165
+ console.log("🔍 columnApiRef debug:", {
166
+ hasColumnApiRef: !!columnApiRef.current,
167
+ columnApiType: typeof columnApiRef.current,
168
+ columnApiMethods: columnApiRef.current ? Object.getOwnPropertyNames(columnApiRef.current).filter(name => typeof columnApiRef.current[name] === 'function') : [],
169
+ hasApplyColumnState: columnApiRef.current && typeof columnApiRef.current.applyColumnState === 'function'
170
+ });
171
+
172
+ // Ověření, že columnApi podporuje applyColumnState
173
+ if (!columnApiRef.current) {
174
+ throw new Error('columnApiRef.current is null or undefined');
175
+ }
176
+
177
+ if (typeof columnApiRef.current.applyColumnState !== 'function') {
178
+ throw new Error(`applyColumnState is not a function. Available methods: ${Object.getOwnPropertyNames(columnApiRef.current).filter(name => typeof columnApiRef.current[name] === 'function').join(', ')}`);
179
+ }
180
+
181
+ console.log("test2");
182
+ // Aplikujeme column state na AG-Grid
183
+ let result;
184
+ try {
185
+ const applyOrderEnabled = !waitForSavedFields; // Při waitForSavedFields už je pořadí v columnDefs
186
+
187
+ result = columnApiRef.current.applyColumnState({
188
+ state: columnState,
189
+ applyOrder: applyOrderEnabled, // Pořadí jen když není waitForSavedFields
190
+ defaultState: {
191
+ sort: null, // Reset sorting na všech sloupcích
192
+ sortIndex: null, // Reset sort index
193
+ pivot: null, // Reset pivot
194
+ rowGroup: null // Reset row grouping
195
+ }
196
+ });
197
+ console.log("test2.5 - applyColumnState executed");
198
+ } catch (applyError) {
199
+ console.error("❌ Error during applyColumnState:", applyError);
200
+ throw applyError;
201
+ }
202
+
203
+ console.log("✅ Column state applied successfully:", {
204
+ result,
205
+ appliedColumns: columnState.length
206
+ });
207
+
208
+ // Ověření, že se sloupce skutečně přeuspořádaly
209
+ const newColumnState = columnApiRef.current.getColumnState();
210
+ console.log("🚀 ~ test3:", newColumnState)
211
+ console.log("🔍 Current column state after apply:", newColumnState.map(c => c.colId));
212
+
213
+ if (onLayoutLoaded) {
214
+ onLayoutLoaded(savedFields.records, columnState);
215
+ }
216
+ } catch (delayedError) {
217
+ console.error("❌ Error applying column state:", delayedError);
218
+ handleError(delayedError, 'při delayed aplikování layoutu');
219
+ }
220
+ };
221
+
222
+ // Pro waitForSavedFields aplikujeme okamžitě (pořadí je už v columnDefs)
223
+ // Pro normální režim použijeme delay
224
+ if (waitForSavedFields) {
225
+ applyFunction();
226
+ } else {
227
+ setTimeout(applyFunction, 100); // 100ms delay jen pro normální režim
228
+ }
229
+ }
230
+ } catch (error) {
231
+ handleError(error, 'při aplikování layoutu');
232
+ } finally {
233
+ setIsInitialized(true);
234
+ setIsLoading(false);
235
+ }
236
+ }, [savedFields, gridLayoutApi, isInitialized, onLayoutLoaded, handleError, columnDefs, waitForSavedFields]);
237
+
238
+ /**
239
+ * Event handlers pro AG-Grid
240
+ */
241
+ const handleColumnMoved = useCallback(() => {
242
+ if (!enabled || !isInitialized) return;
243
+ setHasUnsavedChanges(true);
244
+ // Neukládáme při každém pohybu - čekáme na onDragStopped
245
+ }, [enabled, isInitialized]);
246
+
247
+ const handleDragStopped = useCallback(() => {
248
+ if (!enabled || !isInitialized) return;
249
+ setHasUnsavedChanges(true);
250
+ if (autoSave) {
251
+ console.log("🎯 Drag stopped - spouštím debounced save");
252
+ debouncedSave();
253
+ }
254
+ }, [enabled, isInitialized, autoSave, debouncedSave]);
255
+
256
+ const handleColumnResized = useCallback(() => {
257
+ if (!enabled || !isInitialized) return;
258
+ setHasUnsavedChanges(true);
259
+ if (autoSave) {
260
+ debouncedSave();
261
+ }
262
+ }, [enabled, isInitialized, autoSave, debouncedSave]);
263
+
264
+ const handleColumnVisible = useCallback(() => {
265
+ if (!enabled || !isInitialized) return;
266
+ setHasUnsavedChanges(true);
267
+ if (autoSave) {
268
+ debouncedSave();
269
+ }
270
+ }, [enabled, isInitialized, autoSave, debouncedSave]);
271
+
272
+ const handleColumnPinned = useCallback(() => {
273
+ if (!enabled || !isInitialized) return;
274
+ setHasUnsavedChanges(true);
275
+ if (autoSave) {
276
+ debouncedSave();
277
+ }
278
+ }, [enabled, isInitialized, autoSave, debouncedSave]);
279
+
280
+ /**
281
+ * AG-Grid Ready handler
282
+ */
283
+ const handleGridReady = useCallback((params) => {
284
+ if (!enabled) return;
285
+
286
+ gridApiRef.current = params.api;
287
+ columnApiRef.current = params.columnApi;
288
+
289
+ // Pokud už máme saved data, aplikujeme je
290
+ if (savedFields?.records && !isInitialized) {
291
+ applySavedLayout();
292
+ }
293
+ }, [enabled, savedFields, isInitialized, applySavedLayout]);
294
+
295
+ /**
296
+ * Effect pro aplikování layoutu když se načtou data
297
+ */
298
+ useEffect(() => {
299
+ if (savedFields?.records && columnApiRef.current && !isInitialized) {
300
+ applySavedLayout();
301
+ }
302
+ }, [savedFields, applySavedLayout, isInitialized]);
303
+
304
+ /**
305
+ * Effect pro error handling
306
+ */
307
+ useEffect(() => {
308
+ if (fieldsError) {
309
+ handleError(fieldsError, 'při načítání saved layoutu');
310
+ }
311
+ }, [fieldsError, handleError]);
312
+
313
+ /**
314
+ * Cleanup effect
315
+ */
316
+ useEffect(() => {
317
+ return () => {
318
+ debouncedSave.cancel();
319
+ };
320
+ }, [debouncedSave]);
321
+
322
+ /**
323
+ * Resetuje layout na default
324
+ */
325
+ const resetToDefault = useCallback(async () => {
326
+ if (!enabled || !columnApiRef.current) return;
327
+
328
+ try {
329
+ setIsLoading(true);
330
+
331
+ // Resetujeme AG-Grid na původní column definitions
332
+ columnApiRef.current.resetColumnState();
333
+
334
+ // Pokud je autoSave zapnuté, uložíme resetovaný stav
335
+ if (autoSave) {
336
+ await saveCurrentLayout();
337
+ } else {
338
+ setHasUnsavedChanges(true);
339
+ }
340
+ } catch (error) {
341
+ handleError(error, 'při resetování layoutu');
342
+ } finally {
343
+ setIsLoading(false);
344
+ }
345
+ }, [enabled, autoSave, saveCurrentLayout, handleError]);
346
+
347
+ /**
348
+ * Manuální uložení
349
+ */
350
+ const saveLayout = useCallback(async () => {
351
+ await saveCurrentLayout();
352
+ }, [saveCurrentLayout]);
353
+
354
+ /**
355
+ * Manuální reload
356
+ */
357
+ const reloadLayout = useCallback(async () => {
358
+ try {
359
+ setIsInitialized(false);
360
+ await refetchFields();
361
+ } catch (error) {
362
+ handleError(error, 'při reload layoutu');
363
+ }
364
+ }, [refetchFields, handleError]);
365
+
366
+ /**
367
+ * Otevře Column Editor modal
368
+ */
369
+ const openColumnEditor = useCallback(() => {
370
+ setIsColumnEditorOpen(true);
371
+ }, []);
372
+
373
+ /**
374
+ * Zavře Column Editor modal
375
+ */
376
+ const closeColumnEditor = useCallback(() => {
377
+ setIsColumnEditorOpen(false);
378
+ }, []);
379
+
380
+ /**
381
+ * Uloží změny z Column Editoru
382
+ * @param {Array} editedColumns - Editované sloupce z modalu
383
+ */
384
+ const saveColumnEditorChanges = useCallback(async (editedColumns) => {
385
+ if (!enabled || !columnApiRef.current) return;
386
+
387
+ try {
388
+ setIsLoading(true);
389
+
390
+ // Převedeme editované sloupce na AG-Grid column state
391
+ const columnState = editedColumns.map((col, index) => ({
392
+ colId: col.field,
393
+ width: col.width,
394
+ hide: !col.visible,
395
+ pinned: null,
396
+ sort: null,
397
+ sortIndex: null
398
+ }));
399
+
400
+ // Aplikujeme změny na AG-Grid
401
+ columnApiRef.current.applyColumnState({
402
+ state: columnState,
403
+ applyOrder: true,
404
+ defaultState: {
405
+ sort: null,
406
+ sortIndex: null,
407
+ pivot: null,
408
+ rowGroup: null
409
+ }
410
+ });
411
+
412
+ // Uložíme změny pokud je autoSave zapnuté
413
+ if (autoSave) {
414
+ await saveCurrentLayout();
415
+ } else {
416
+ setHasUnsavedChanges(true);
417
+ }
418
+
419
+ console.log("✅ Column Editor changes applied successfully");
420
+ } catch (error) {
421
+ handleError(error, 'při aplikování změn z Column Editoru');
422
+ } finally {
423
+ setIsLoading(false);
424
+ }
425
+ }, [enabled, autoSave, saveCurrentLayout, handleError]);
426
+
427
+ // Logika pro zobrazení columnDefs
428
+ const shouldShowColumns = useMemo(() => {
429
+ if (!waitForSavedFields) return true; // Pokud není waitForSavedFields, vždy zobrazuj
430
+
431
+ // Pokud je waitForSavedFields true, čekáme na dokončení načítání
432
+ return !isFieldsLoading && !isLoading;
433
+ }, [waitForSavedFields, isFieldsLoading, isLoading]);
434
+
435
+ // Pre-transformované columnDefs podle savedFields pro waitForSavedFields mode
436
+ const preTransformedColumnDefs = useMemo(() => {
437
+ if (!waitForSavedFields || !savedFields?.records || !Array.isArray(columnDefs)) {
438
+ return columnDefs; // Normální columnDefs
439
+ }
440
+
441
+ // Transformujeme columnDefs podle savedFields PŘED zobrazením gridu
442
+ const columnState = gridLayoutApi.transformFieldsToColumnState(savedFields.records, columnDefs);
443
+
444
+ if (!columnState || columnState.length === 0) {
445
+ return columnDefs; // Pokud není co transformovat, vrátíme původní
446
+ }
447
+
448
+ // Vytvoříme mapu pro rychlé vyhledávání
449
+ const columnStateMap = new Map();
450
+ columnState.forEach((colState, index) => {
451
+ columnStateMap.set(colState.colId, { ...colState, __order: index });
452
+ });
453
+
454
+ // Seřadíme columnDefs podle pořadí z columnState
455
+ const sortedColumnDefs = [...columnDefs].sort((a, b) => {
456
+ const aState = columnStateMap.get(a.field);
457
+ const bState = columnStateMap.get(b.field);
458
+
459
+ const aOrder = aState?.__order ?? 999;
460
+ const bOrder = bState?.__order ?? 999;
461
+
462
+ return aOrder - bOrder;
463
+ });
464
+
465
+ console.log("🔄 Pre-transformed columnDefs:", {
466
+ original: columnDefs.map(cd => cd.field),
467
+ transformed: sortedColumnDefs.map(cd => cd.field),
468
+ columnStateOrder: columnState.map(cs => cs.colId)
469
+ });
470
+
471
+ return sortedColumnDefs;
472
+ }, [waitForSavedFields, savedFields?.records, columnDefs, gridLayoutApi]);
473
+
474
+ return {
475
+ // State
476
+ isLoading: isLoading || isFieldsLoading,
477
+ isSaving,
478
+ error,
479
+ hasUnsavedChanges,
480
+ isInitialized,
481
+ shouldShowColumns, // Nové pole pro řízení zobrazení columnDefs
482
+ preTransformedColumnDefs, // Pre-transformované columnDefs pro waitForSavedFields
483
+
484
+ // AG-Grid event handlers
485
+ onGridReady: handleGridReady,
486
+ onColumnMoved: handleColumnMoved,
487
+ onDragStopped: handleDragStopped, // Nový handler pro konec drag operace
488
+ onColumnResized: handleColumnResized,
489
+ onColumnVisible: handleColumnVisible,
490
+ onColumnPinned: handleColumnPinned,
491
+
492
+ // Manual actions
493
+ saveLayout,
494
+ resetToDefault,
495
+ reloadLayout,
496
+
497
+ // Column Editor
498
+ isColumnEditorOpen,
499
+ openColumnEditor,
500
+ closeColumnEditor,
501
+ saveColumnEditorChanges,
502
+
503
+ // Data
504
+ savedFields: savedFields?.records || [],
505
+ columnDefs, // Původní columnDefs pro Column Editor
506
+
507
+ // Utils
508
+ gridLayoutApi
509
+ };
510
+ };
511
+
512
+ export default useGridLayout;