@topconsultnpm/sdkui-react 6.22.0-dev2.14 → 6.22.0-dev2.15
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.
|
@@ -54,13 +54,16 @@ const VirtualRowComponent = memo(({ ariaAttributes, index, style, flattenedItems
|
|
|
54
54
|
return prevItem === nextItem;
|
|
55
55
|
const prevNode = prevItem.node;
|
|
56
56
|
const nextNode = nextItem.node;
|
|
57
|
-
// Controlla se questa riga specifica è cambiata
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Controlla se questa riga specifica è cambiata: confronta l'identità del nodo (non i soli
|
|
58
|
+
// campi key/expanded) per non lasciare visualizzato un nodo obsoleto. Gli handler non servono:
|
|
59
|
+
// hanno identità stabile e leggono lo stato dalle ref.
|
|
60
|
+
return (prevNode === nextNode &&
|
|
60
61
|
prevItem.depth === nextItem.depth &&
|
|
61
62
|
(prevNode.key === prevProps.focusedItemKey) === (nextNode.key === nextProps.focusedItemKey) &&
|
|
62
63
|
prevProps.selectedItemKeys.has(prevNode.key) === nextProps.selectedItemKeys.has(nextNode.key) &&
|
|
63
|
-
prevProps.allowMultipleSelection === nextProps.allowMultipleSelection
|
|
64
|
+
prevProps.allowMultipleSelection === nextProps.allowMultipleSelection &&
|
|
65
|
+
prevProps.itemRender === nextProps.itemRender &&
|
|
66
|
+
prevProps.onItemContextMenu === nextProps.onItemContextMenu);
|
|
64
67
|
});
|
|
65
68
|
const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMultipleSelection, onDataChanged, calculateItemsForNode, itemRender, onNodeUpdate, onFocusedItemChanged, onSelectionChanged, shouldDelayFocusOnEvent, onItemContextMenu, autoSelectChildren = true, itemsPerPage = 100, showLoadMoreButton = true, enableVirtualization = false }) => {
|
|
66
69
|
// Altezza fissa per ogni riga nella virtualizzazione
|
|
@@ -71,6 +74,24 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
71
74
|
// Ref per selectedItems - permette alle callback memoizzate di accedere sempre al valore più recente
|
|
72
75
|
const selectedItemsRef = useRef(selectedItems);
|
|
73
76
|
selectedItemsRef.current = selectedItems;
|
|
77
|
+
// Ref per props e callback usate dagli handler: una riga virtualizzata memoizzata conserva le
|
|
78
|
+
// props del render precedente, quindi gli handler non devono catturare lo stato nella closure.
|
|
79
|
+
const dataSourceRef = useRef(dataSource);
|
|
80
|
+
dataSourceRef.current = dataSource;
|
|
81
|
+
const autoSelectChildrenRef = useRef(autoSelectChildren);
|
|
82
|
+
autoSelectChildrenRef.current = autoSelectChildren;
|
|
83
|
+
const calculateItemsForNodeRef = useRef(calculateItemsForNode);
|
|
84
|
+
calculateItemsForNodeRef.current = calculateItemsForNode;
|
|
85
|
+
const onNodeUpdateRef = useRef(onNodeUpdate);
|
|
86
|
+
onNodeUpdateRef.current = onNodeUpdate;
|
|
87
|
+
const onDataChangedRef = useRef(onDataChanged);
|
|
88
|
+
onDataChangedRef.current = onDataChanged;
|
|
89
|
+
const onFocusedItemChangedRef = useRef(onFocusedItemChanged);
|
|
90
|
+
onFocusedItemChangedRef.current = onFocusedItemChanged;
|
|
91
|
+
const onSelectionChangedRef = useRef(onSelectionChanged);
|
|
92
|
+
onSelectionChangedRef.current = onSelectionChanged;
|
|
93
|
+
const shouldDelayFocusOnEventRef = useRef(shouldDelayFocusOnEvent);
|
|
94
|
+
shouldDelayFocusOnEventRef.current = shouldDelayFocusOnEvent;
|
|
74
95
|
// Ref per handleCheckboxChange - permette all'useEffect di accedere alla funzione prima che sia definita
|
|
75
96
|
const handleCheckboxChangeRef = useRef(() => { });
|
|
76
97
|
// Filtra gli items da mostrare in base alla paginazione (definito prima per essere usato da flattenTreeWithDepth)
|
|
@@ -216,7 +237,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
216
237
|
const currentIndex = flatList.findIndex(item => item.key === currentItem.key);
|
|
217
238
|
return flatList[currentIndex - 1] || null;
|
|
218
239
|
};
|
|
219
|
-
const flattenTree = (nodes) => {
|
|
240
|
+
const flattenTree = useCallback((nodes) => {
|
|
220
241
|
let flatList = [];
|
|
221
242
|
nodes.forEach(node => {
|
|
222
243
|
if (!node.hidden) {
|
|
@@ -227,45 +248,57 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
227
248
|
}
|
|
228
249
|
});
|
|
229
250
|
return flatList;
|
|
230
|
-
};
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
if (node.key === key) {
|
|
235
|
-
if (node.expanded) {
|
|
236
|
-
// Collassa il nodo
|
|
237
|
-
const updatedNode = { ...node, expanded: false };
|
|
238
|
-
onNodeUpdate?.(updatedNode);
|
|
239
|
-
return updatedNode;
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
// Espandi il nodo
|
|
243
|
-
let newItems = node.items;
|
|
244
|
-
// Solo se calculateItemsForNode è definito E (ctrl è premuto O non è già caricato)
|
|
245
|
-
if (calculateItemsForNode && (ctrlKey || !node.isLoaded)) {
|
|
246
|
-
// Se ctrl è premuto, passa un nodo con isLoaded resettato per forzare il refresh
|
|
247
|
-
const nodeForCalculation = ctrlKey ? { ...node, isLoaded: false } : node;
|
|
248
|
-
newItems = await calculateItemsForNode(nodeForCalculation);
|
|
249
|
-
const updatedNode = { ...node, expanded: true, items: newItems, isLoaded: true };
|
|
250
|
-
onNodeUpdate?.(updatedNode);
|
|
251
|
-
return updatedNode;
|
|
252
|
-
}
|
|
253
|
-
// Altrimenti usa semplicemente gli items esistenti
|
|
254
|
-
return { ...node, expanded: true };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (node.items) {
|
|
258
|
-
const updatedItems = await updateNodeRecursively(node.items);
|
|
259
|
-
return { ...node, items: updatedItems };
|
|
260
|
-
}
|
|
251
|
+
}, []);
|
|
252
|
+
const findNodeByKey = useCallback((nodes, key) => {
|
|
253
|
+
for (const node of nodes) {
|
|
254
|
+
if (node.key === key)
|
|
261
255
|
return node;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
256
|
+
if (node.items) {
|
|
257
|
+
const found = findNodeByKey(node.items, key);
|
|
258
|
+
if (found)
|
|
259
|
+
return found;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}, []);
|
|
264
|
+
const handleNodeToggle = useCallback(async (key, ctrlKey) => {
|
|
265
|
+
// dataSourceRef è letta alla chiamata, non alla creazione della closure
|
|
266
|
+
const target = findNodeByKey(dataSourceRef.current, key);
|
|
267
|
+
if (!target)
|
|
268
|
+
return;
|
|
269
|
+
const calculateItems = calculateItemsForNodeRef.current;
|
|
270
|
+
let updatedNode;
|
|
271
|
+
if (target.expanded) {
|
|
272
|
+
// Collassa il nodo
|
|
273
|
+
updatedNode = { ...target, expanded: false };
|
|
274
|
+
onNodeUpdateRef.current?.(updatedNode);
|
|
275
|
+
}
|
|
276
|
+
else if (calculateItems && (ctrlKey || !target.isLoaded)) {
|
|
277
|
+
// Espandi il nodo caricando gli items.
|
|
278
|
+
// Solo se calculateItemsForNode è definito E (ctrl è premuto O non è già caricato).
|
|
279
|
+
// Se ctrl è premuto, passa un nodo con isLoaded resettato per forzare il refresh.
|
|
280
|
+
const nodeForCalculation = ctrlKey ? { ...target, isLoaded: false } : target;
|
|
281
|
+
const newItems = await calculateItems(nodeForCalculation);
|
|
282
|
+
updatedNode = { ...target, expanded: true, items: newItems, isLoaded: true };
|
|
283
|
+
onNodeUpdateRef.current?.(updatedNode);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// Altrimenti usa semplicemente gli items esistenti
|
|
287
|
+
updatedNode = { ...target, expanded: true };
|
|
288
|
+
}
|
|
289
|
+
// Rimpiazzo sull'albero riletto dopo l'await: non sovrascrive i toggle fatti durante la fetch
|
|
290
|
+
let replaced = false;
|
|
291
|
+
const replaceNode = (nodes) => nodes.map(node => {
|
|
292
|
+
if (!replaced && node.key === key) {
|
|
293
|
+
replaced = true;
|
|
294
|
+
return updatedNode;
|
|
295
|
+
}
|
|
296
|
+
if (node.items)
|
|
297
|
+
return { ...node, items: replaceNode(node.items) };
|
|
298
|
+
return node;
|
|
267
299
|
});
|
|
268
|
-
|
|
300
|
+
onDataChangedRef.current?.(replaceNode(dataSourceRef.current));
|
|
301
|
+
}, [findNodeByKey]);
|
|
269
302
|
// Ref per distinguere click singolo/doppio sulla riga (esempio TMDcmtIcon)
|
|
270
303
|
const clickTimeoutRef = useRef(null);
|
|
271
304
|
const lastClickedNodeKeyRef = useRef(undefined);
|
|
@@ -276,7 +309,8 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
276
309
|
return;
|
|
277
310
|
}
|
|
278
311
|
// Se shouldDelayFocusOnEvent non è fornita, il comportamento predefinito è il focus immediato (false)
|
|
279
|
-
const
|
|
312
|
+
const shouldDelay = shouldDelayFocusOnEventRef.current;
|
|
313
|
+
const delayFocus = shouldDelay ? shouldDelay(node, e) : false;
|
|
280
314
|
// Reset del timer se il click è su un nodo diverso dal precedente
|
|
281
315
|
if (lastClickedNodeKeyRef.current && lastClickedNodeKeyRef.current !== node.key) {
|
|
282
316
|
if (clickTimeoutRef.current) {
|
|
@@ -292,7 +326,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
292
326
|
// Primo click: imposta il timer per il focus
|
|
293
327
|
lastClickedNodeKeyRef.current = node.key;
|
|
294
328
|
clickTimeoutRef.current = setTimeout(() => {
|
|
295
|
-
|
|
329
|
+
onFocusedItemChangedRef.current?.(node);
|
|
296
330
|
clickTimeoutRef.current = null;
|
|
297
331
|
lastClickedNodeKeyRef.current = undefined;
|
|
298
332
|
}, 200);
|
|
@@ -307,12 +341,14 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
307
341
|
clickTimeoutRef.current = null;
|
|
308
342
|
lastClickedNodeKeyRef.current = undefined;
|
|
309
343
|
}
|
|
310
|
-
|
|
344
|
+
onFocusedItemChangedRef.current?.(node); // Chiama onFocusedItemChanged immediatamente
|
|
311
345
|
}
|
|
312
|
-
}, [
|
|
346
|
+
}, []);
|
|
313
347
|
const handleCheckboxChange = useCallback((node, checked) => {
|
|
314
|
-
// Usa
|
|
348
|
+
// Usa le ref per accedere sempre ai valori più recenti
|
|
315
349
|
const currentSelectedItems = selectedItemsRef.current;
|
|
350
|
+
const currentDataSource = dataSourceRef.current;
|
|
351
|
+
const autoSelect = autoSelectChildrenRef.current;
|
|
316
352
|
// Funzione helper per trovare il parent di un nodo e verificare se tutti i suoi figli sono selezionati
|
|
317
353
|
const findAndCheckParents = (items, targetNode, currentSelection) => {
|
|
318
354
|
const parentsToAdd = [];
|
|
@@ -346,7 +382,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
346
382
|
};
|
|
347
383
|
let newSelectedItems;
|
|
348
384
|
if (checked) {
|
|
349
|
-
if (node.isContainer &&
|
|
385
|
+
if (node.isContainer && autoSelect) {
|
|
350
386
|
// Quando selezioni un container, aggiungi il container stesso + tutti i figli (se autoSelectChildren è true)
|
|
351
387
|
const allChildren = flattenTree(node.items || []);
|
|
352
388
|
// Rimuovi eventuali duplicati: filtra gli elementi già presenti nella selezione
|
|
@@ -358,8 +394,8 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
358
394
|
// Quando selezioni un figlio o un container con autoSelectChildren=false, aggiungi solo il nodo
|
|
359
395
|
newSelectedItems = [...currentSelectedItems, node];
|
|
360
396
|
// Verifica se selezionando questo figlio sono ora selezionati tutti i figli del parent (solo se autoSelectChildren è true)
|
|
361
|
-
if (
|
|
362
|
-
const parentsToAdd = findAndCheckParents(
|
|
397
|
+
if (autoSelect) {
|
|
398
|
+
const parentsToAdd = findAndCheckParents(currentDataSource, node, newSelectedItems);
|
|
363
399
|
newSelectedItems = [...newSelectedItems, ...parentsToAdd];
|
|
364
400
|
}
|
|
365
401
|
}
|
|
@@ -367,7 +403,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
367
403
|
newSelectedItems = currentSelectedItems;
|
|
368
404
|
}
|
|
369
405
|
}
|
|
370
|
-
else if (node.isContainer &&
|
|
406
|
+
else if (node.isContainer && autoSelect) {
|
|
371
407
|
// Quando deselezioni un container, rimuovi il container stesso + tutti i figli (solo se autoSelectChildren è true)
|
|
372
408
|
const childKeys = flattenTree(node.items || []).map(item => item.key);
|
|
373
409
|
newSelectedItems = currentSelectedItems.filter(item => item.key !== node.key && !childKeys.includes(item.key));
|
|
@@ -376,7 +412,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
376
412
|
// Quando deselezioni un figlio o un container con autoSelectChildren=false, rimuovi solo il nodo
|
|
377
413
|
newSelectedItems = currentSelectedItems.filter(item => item.key !== node.key);
|
|
378
414
|
// Se il figlio apparteneva a un parent che era selezionato, rimuovi anche il parent (solo se autoSelectChildren è true)
|
|
379
|
-
if (
|
|
415
|
+
if (autoSelect && !node.isContainer) {
|
|
380
416
|
const removeParentContainers = (items) => {
|
|
381
417
|
const parentsToRemove = [];
|
|
382
418
|
items.forEach(item => {
|
|
@@ -391,26 +427,26 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
391
427
|
});
|
|
392
428
|
return parentsToRemove;
|
|
393
429
|
};
|
|
394
|
-
const parentsToRemove = removeParentContainers(
|
|
430
|
+
const parentsToRemove = removeParentContainers(currentDataSource);
|
|
395
431
|
newSelectedItems = newSelectedItems.filter(item => !parentsToRemove.includes(item.key));
|
|
396
432
|
}
|
|
397
433
|
}
|
|
398
|
-
|
|
399
|
-
}, [
|
|
434
|
+
onSelectionChangedRef.current?.(newSelectedItems);
|
|
435
|
+
}, [flattenTree]);
|
|
400
436
|
// Aggiorna la ref con l'ultima versione di handleCheckboxChange
|
|
401
437
|
handleCheckboxChangeRef.current = handleCheckboxChange;
|
|
402
|
-
const hasPartialChildSelection = (node) => {
|
|
438
|
+
const hasPartialChildSelection = useCallback((node) => {
|
|
403
439
|
// Lo stato indeterminate ha senso solo quando autoSelectChildren è true
|
|
404
|
-
if (!
|
|
440
|
+
if (!autoSelectChildrenRef.current)
|
|
405
441
|
return false;
|
|
406
442
|
if (!node.isContainer || !node.items)
|
|
407
443
|
return false;
|
|
408
444
|
const childKeys = flattenTree(node.items).map(item => item.key);
|
|
409
|
-
const selectedChildKeys =
|
|
445
|
+
const selectedChildKeys = selectedItemsRef.current.map(item => item.key);
|
|
410
446
|
const selectedCount = childKeys.filter(key => selectedChildKeys.includes(key)).length;
|
|
411
447
|
return selectedCount > 0 && selectedCount < childKeys.length;
|
|
412
|
-
};
|
|
413
|
-
const hasVisibleItems = (node) => {
|
|
448
|
+
}, [flattenTree]);
|
|
449
|
+
const hasVisibleItems = useCallback((node) => {
|
|
414
450
|
// If node has explicit isExpandible value, use that
|
|
415
451
|
if (node.isExpandible !== undefined) {
|
|
416
452
|
return node.isExpandible;
|
|
@@ -421,14 +457,14 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
421
457
|
if (node.items.length <= 0)
|
|
422
458
|
return false;
|
|
423
459
|
return node.items.filter(o => !o.hidden).length > 0;
|
|
424
|
-
};
|
|
460
|
+
}, []);
|
|
425
461
|
// Gestisce il cambio di pagina per un nodo
|
|
426
462
|
const handlePageChange = useCallback((nodeKey, newPage) => {
|
|
427
463
|
const updateNodePage = (nodes) => {
|
|
428
464
|
return nodes.map(node => {
|
|
429
465
|
if (node.key === nodeKey) {
|
|
430
466
|
const updatedNode = { ...node, currentPage: newPage };
|
|
431
|
-
|
|
467
|
+
onNodeUpdateRef.current?.(updatedNode);
|
|
432
468
|
return updatedNode;
|
|
433
469
|
}
|
|
434
470
|
if (node.items) {
|
|
@@ -437,9 +473,9 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
437
473
|
return node;
|
|
438
474
|
});
|
|
439
475
|
};
|
|
440
|
-
const updatedData = updateNodePage(
|
|
441
|
-
|
|
442
|
-
}, [
|
|
476
|
+
const updatedData = updateNodePage(dataSourceRef.current);
|
|
477
|
+
onDataChangedRef.current?.(updatedData);
|
|
478
|
+
}, []);
|
|
443
479
|
// Verifica se c'è bisogno del paginatore
|
|
444
480
|
const needsPagination = useCallback((node) => {
|
|
445
481
|
if (!showLoadMoreButton || !node.items)
|
|
@@ -479,7 +515,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
479
515
|
onItemContextMenu(node, e);
|
|
480
516
|
}
|
|
481
517
|
}, children: itemRender(node) })] }), node.expanded && node.items && (_jsxs("div", { style: { paddingLeft: 20, width: 'fit-content', minWidth: '100%' }, children: [renderTree(getVisibleItems(node)), needsPagination(node) && (_jsxs(StyledStickyPaginator, { children: [_jsx(TMButton, { btnStyle: 'icon', onClick: () => handlePageChange(node.key, (node.currentPage ?? 0) - 1), showTooltip: false, caption: "\u25C4", icon: _jsx(IconArrowLeft, { color: 'white' }), disabled: (node.currentPage ?? 0) <= 0 }), _jsx("span", { style: { fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0, color: 'white' }, children: SDKUI_Localizator.PaginationInfo.replaceParams((node.currentPage ?? 0) + 1, getTotalPages(node), node.items?.length ?? 0) }), _jsx(TMButton, { btnStyle: 'icon', onClick: () => handlePageChange(node.key, (node.currentPage ?? 0) + 1), showTooltip: false, caption: "\u25BA", icon: _jsx(IconArrowRight, { color: 'white' }), disabled: (node.currentPage ?? 0) >= getTotalPages(node) - 1 })] }))] }))] }, node.key)));
|
|
482
|
-
}, [handleNodeClick, handleNodeToggle, handleCheckboxChange, focusedItem, selectedItems, allowMultipleSelection, getVisibleItems, needsPagination, handlePageChange, getTotalPages, onItemContextMenu]);
|
|
518
|
+
}, [handleNodeClick, handleNodeToggle, handleCheckboxChange, hasVisibleItems, hasPartialChildSelection, itemRender, focusedItem, selectedItems, allowMultipleSelection, getVisibleItems, needsPagination, handlePageChange, getTotalPages, onItemContextMenu]);
|
|
483
519
|
// Calcola selectedItemKeys ad ogni render per garantire sincronizzazione con selectedItems
|
|
484
520
|
const selectedItemKeys = new Set(selectedItems.map(s => s.key));
|
|
485
521
|
// Memoizza rowPropsData per evitare re-render della List
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { LayoutModes, MetadataDataDomains, AccessLevels, MetadataDataTypes, SDK_Globals, DcmtTypeListCacheService, DataColumnTypes, DataListViewModes } from '@topconsultnpm/sdk-ts';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { SDKUI_Localizator, stringIsNullOrEmpty } from '../../helper';
|
|
@@ -7,26 +7,30 @@ import TMDataGrid, { TMDataGridPageSize } from '../base/TMDataGrid';
|
|
|
7
7
|
import { TMExceptionBoxManager } from '../base/TMPopUp';
|
|
8
8
|
import TMSpinner from '../base/TMSpinner';
|
|
9
9
|
import TMCheckBox from '../editors/TMCheckBox';
|
|
10
|
+
import TMTextBox from '../editors/TMTextBox';
|
|
10
11
|
import TMPanel from '../base/TMPanel';
|
|
11
12
|
import TMModal from '../base/TMModal';
|
|
12
13
|
import { useDataListItem } from '../../hooks/useDataListItem';
|
|
13
14
|
import { useDataUserIdItem } from '../../hooks/useDataUserIdItem';
|
|
14
15
|
const StyledDistinctValues = styled.div `display: flex; flex-direction: column; height: 100%; overflow: hidden; gap: 10px; position: relative;`;
|
|
15
|
-
const
|
|
16
|
+
const StyledAppendBar = styled.div `display: flex; flex-direction: row; align-items: center; gap: 5px; position: absolute; right: 15px; top: 15px; z-index: 10000;`;
|
|
17
|
+
const TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppendMode = true, showHeader = true, isModal, separator = ",", onSelectionChanged, onClosePanelCallback }) => {
|
|
16
18
|
const [focusedItem, setFocusedItem] = useState();
|
|
17
19
|
const [dataSource, setDataSource] = useState([]);
|
|
18
20
|
const [isAppendMode, setIsAppendMode] = useState(false);
|
|
19
21
|
const [md, setMd] = useState();
|
|
20
|
-
const [
|
|
21
|
-
const { loadDataListsAsync, renderDataListCell,
|
|
22
|
-
const { loadUsersAsync, renderUserIdViewer,
|
|
22
|
+
const [appendedValues, setAppendedValues] = useState(new Set());
|
|
23
|
+
const { loadDataListsAsync, renderDataListCell, getDataListItem } = useDataListItem();
|
|
24
|
+
const { loadUsersAsync, renderUserIdViewer, getUserItem, getCompleteUserName } = useDataUserIdItem();
|
|
25
|
+
// Identifica la richiesta corrente: le risposte delle richieste obsolete vengono scartate
|
|
26
|
+
const requestIdRef = useRef(0);
|
|
23
27
|
useEffect(() => {
|
|
24
28
|
getDistictValuesAsync();
|
|
25
|
-
}, [mid]);
|
|
29
|
+
}, [tid, mid]);
|
|
26
30
|
useEffect(() => {
|
|
27
31
|
setIsAppendMode(false);
|
|
28
32
|
setFocusedItem(undefined);
|
|
29
|
-
|
|
33
|
+
setAppendedValues(new Set());
|
|
30
34
|
}, [md]);
|
|
31
35
|
useEffect(() => {
|
|
32
36
|
if (!focusedItem)
|
|
@@ -42,27 +46,39 @@ const TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppend
|
|
|
42
46
|
};
|
|
43
47
|
if (layoutMode in permissionCheck && permissionCheck[layoutMode] !== AccessLevels.Yes)
|
|
44
48
|
return;
|
|
45
|
-
if (
|
|
46
|
-
|
|
49
|
+
if (stringIsNullOrEmpty(focusedItem.value)) {
|
|
50
|
+
setAppendedValues(new Set());
|
|
47
51
|
onSelectionChanged?.({ tid: tid, mid: mid, newValue: undefined, isAppendMode: isAppendMode });
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
50
54
|
let value = md.dataType === MetadataDataTypes.Number ? parseFloat(focusedItem.value.replace(',', '.')).toString() : focusedItem.value;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
onSelectionChanged?.({ tid: tid, mid: mid, newValue:
|
|
55
|
+
// Fuori dalla modalità Accoda la selezione sostituisce sempre il valore precedente
|
|
56
|
+
const newAppendedValues = isAppendMode ? new Set(appendedValues) : new Set();
|
|
57
|
+
newAppendedValues.add(value);
|
|
58
|
+
setAppendedValues(newAppendedValues);
|
|
59
|
+
onSelectionChanged?.({ tid: tid, mid: mid, newValue: joinAppendedValues(newAppendedValues), isAppendMode: isAppendMode });
|
|
56
60
|
}, [focusedItem]);
|
|
61
|
+
const joinAppendedValues = (values) => Array.from(values).join(separator);
|
|
62
|
+
const onAppendModeChanged = () => {
|
|
63
|
+
const newIsAppendMode = !isAppendMode;
|
|
64
|
+
// Disattivando l'accodamento si riparte da zero: il prossimo click sostituirà il valore
|
|
65
|
+
if (!newIsAppendMode)
|
|
66
|
+
setAppendedValues(new Set());
|
|
67
|
+
setIsAppendMode(newIsAppendMode);
|
|
68
|
+
};
|
|
57
69
|
const getDistictValuesAsync = async () => {
|
|
58
70
|
if (!tid)
|
|
59
71
|
return;
|
|
72
|
+
const requestId = ++requestIdRef.current;
|
|
73
|
+
// Svuota subito la griglia: evita di mostrare i valori del tid/mid precedente durante il caricamento
|
|
74
|
+
setDataSource([]);
|
|
60
75
|
try {
|
|
61
76
|
TMSpinner.show({ description: 'Caricamento dei valori distinti...' });
|
|
62
77
|
let dtd = await DcmtTypeListCacheService.GetAsync(tid, true);
|
|
63
78
|
const currentMd = dtd?.metadata?.find(o => o.id === mid);
|
|
64
|
-
setMd(currentMd);
|
|
65
79
|
let result = await SDK_Globals.tmSession?.NewSearchEngine().GetDistinctValuesAsync(tid, mid, 10000);
|
|
80
|
+
if (requestId !== requestIdRef.current)
|
|
81
|
+
return;
|
|
66
82
|
// Load DataList or UserID cache based on metadata domain
|
|
67
83
|
if (currentMd?.dataDomain === MetadataDataDomains.DataList && currentMd.dataListID) {
|
|
68
84
|
await loadDataListsAsync(new Set([currentMd.dataListID]));
|
|
@@ -77,9 +93,17 @@ const TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppend
|
|
|
77
93
|
});
|
|
78
94
|
await loadUsersAsync(userIDs);
|
|
79
95
|
}
|
|
96
|
+
// Nel frattempo tid/mid sono cambiati: questa risposta (e le cache appena caricate) non è più valida
|
|
97
|
+
if (requestId !== requestIdRef.current)
|
|
98
|
+
return;
|
|
99
|
+
// md e dataSource vengono impostati insieme, dopo il popolamento delle cache,
|
|
100
|
+
// così il render delle celle usa sempre metadato e cache coerenti con i dati
|
|
101
|
+
setMd(currentMd);
|
|
80
102
|
setDataSource(convertDataTableToObject(result?.dtdResult));
|
|
81
103
|
}
|
|
82
104
|
catch (e) {
|
|
105
|
+
if (requestId !== requestIdRef.current)
|
|
106
|
+
return;
|
|
83
107
|
let err = e;
|
|
84
108
|
TMExceptionBoxManager.show({ exception: err });
|
|
85
109
|
}
|
|
@@ -101,13 +125,32 @@ const TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppend
|
|
|
101
125
|
}
|
|
102
126
|
return _jsx("div", { children: cellData.text });
|
|
103
127
|
}, [md, renderDataListCell, renderUserIdViewer]);
|
|
128
|
+
// Per DataList e UserID il testo mostrato in cella (descrizione, nome utente) è diverso dal valore
|
|
129
|
+
// grezzo del dataSource (codice, id): senza questa funzione la ricerca di DevExtreme filtrerebbe
|
|
130
|
+
// sul valore grezzo e non troverebbe quello che l'utente vede scritto nella griglia
|
|
131
|
+
const calculateValueDisplayValue = useCallback((rowData) => {
|
|
132
|
+
const value = rowData?.value;
|
|
133
|
+
if (stringIsNullOrEmpty(value))
|
|
134
|
+
return value;
|
|
135
|
+
if (md?.dataDomain === MetadataDataDomains.DataList && md.dataListID) {
|
|
136
|
+
return getDataListItem(md.dataListID, value)?.name ?? value;
|
|
137
|
+
}
|
|
138
|
+
if (md?.dataDomain === MetadataDataDomains.UserID) {
|
|
139
|
+
const userId = Number(value);
|
|
140
|
+
if (userId === 0)
|
|
141
|
+
return SDKUI_Localizator.SystemUser;
|
|
142
|
+
const ud = getUserItem(userId);
|
|
143
|
+
return ud ? getCompleteUserName(ud.domain, ud.name) ?? value : value;
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}, [md, getDataListItem, getUserItem, getCompleteUserName]);
|
|
104
147
|
const customColumns = useMemo(() => {
|
|
105
148
|
const needsCellRender = md?.dataDomain === MetadataDataDomains.DataList || md?.dataDomain === MetadataDataDomains.UserID;
|
|
106
149
|
return ([
|
|
107
|
-
{ dataField: 'value', dataType: md?.dataType === MetadataDataTypes.DateTime ? 'date' : 'string', caption: md?.nameLoc, width: 'min-content', ...(needsCellRender && { cellRender: valueCellRender }) },
|
|
150
|
+
{ dataField: 'value', dataType: md?.dataType === MetadataDataTypes.DateTime ? 'date' : 'string', caption: md?.nameLoc, width: 'min-content', ...(needsCellRender && { cellRender: valueCellRender, calculateDisplayValue: calculateValueDisplayValue }) },
|
|
108
151
|
{ dataField: 'Count', dataType: 'number', caption: 'N°' }
|
|
109
152
|
]);
|
|
110
|
-
}, [md, valueCellRender]);
|
|
153
|
+
}, [md, valueCellRender, calculateValueDisplayValue]);
|
|
111
154
|
const customSummary = useMemo(() => {
|
|
112
155
|
return ({
|
|
113
156
|
totalItems: [
|
|
@@ -119,17 +162,29 @@ const TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppend
|
|
|
119
162
|
const convertDataTableToObject = (dtdResult) => {
|
|
120
163
|
if (!dtdResult?.columns?.length || !dtdResult?.rows?.length)
|
|
121
164
|
return [];
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
165
|
+
// Descrittori di colonna calcolati una sola volta invece che per ogni riga
|
|
166
|
+
const columns = [];
|
|
167
|
+
dtdResult.columns.forEach((col, colIndex) => {
|
|
168
|
+
if (col?.caption) {
|
|
169
|
+
columns.push({ index: colIndex, key: colIndex === 0 ? "value" : col.caption, isNumber: col.dataType === DataColumnTypes.Number });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// Il conteggio delle occorrenze è la seconda colonna del risultato:
|
|
173
|
+
// la chiave di ordinamento viene calcolata una volta per riga, non a ogni confronto
|
|
174
|
+
const COUNT_COL_INDEX = 1;
|
|
175
|
+
const rows = dtdResult.rows.map((row) => {
|
|
176
|
+
let rowObject = { rowIndex: 0, sortValue: 0 };
|
|
177
|
+
for (let i = 0; i < columns.length; i++) {
|
|
178
|
+
const col = columns[i];
|
|
179
|
+
rowObject[col.key] = col.isNumber ? row?.[col.index]?.replace('.', ',') : row?.[col.index] ?? null;
|
|
180
|
+
}
|
|
181
|
+
rowObject.sortValue = Number(row?.[COUNT_COL_INDEX]) || 0;
|
|
131
182
|
return rowObject;
|
|
132
183
|
});
|
|
184
|
+
rows.sort((a, b) => b.sortValue - a.sortValue);
|
|
185
|
+
for (let i = 0; i < rows.length; i++)
|
|
186
|
+
rows[i].rowIndex = i;
|
|
187
|
+
return rows;
|
|
133
188
|
};
|
|
134
189
|
const isVisibleAppend = () => {
|
|
135
190
|
if (!allowAppendMode)
|
|
@@ -145,7 +200,7 @@ const TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppend
|
|
|
145
200
|
return md.dataDomain === undefined || md.dataDomain === MetadataDataDomains.None;
|
|
146
201
|
};
|
|
147
202
|
const renderContent = () => {
|
|
148
|
-
return (_jsxs(StyledDistinctValues, { children: [isVisibleAppend() &&
|
|
203
|
+
return (_jsxs(StyledDistinctValues, { children: [isVisibleAppend() && _jsxs(StyledAppendBar, { children: [_jsx(TMCheckBox, { label: 'Accoda e separa con', value: isAppendMode, onValueChanged: onAppendModeChanged }), _jsx(TMTextBox, { width: '30px', readOnly: true, disabled: !isAppendMode, value: separator })] }), _jsx(TMDataGrid, { focusedRowKey: focusedItem ? focusedItem.rowIndex : undefined, selection: { showCheckBoxesMode: 'none' }, searchPanel: { highlightCaseSensitive: true, visible: true }, dataColumns: customColumns, dataSource: dataSource, keyExpr: 'rowIndex', height: 'calc(100%)', onFocusedRowChanged: onFocusedRowChanged, paging: { pageSize: 30 }, summary: customSummary, onRowDblClick: () => onClosePanelCallback?.(), pageSize: TMDataGridPageSize.Medium })] }));
|
|
149
204
|
};
|
|
150
205
|
return (_jsx(_Fragment, { children: isModal
|
|
151
206
|
? _jsx(TMModal, { title: SDKUI_Localizator.DistinctValues + (md?.nameLoc ? ` (${md?.nameLoc})` : ''), height: '600px', width: '500px', resizable: true, onClose: onClosePanelCallback, children: renderContent() })
|
|
@@ -58,10 +58,12 @@ const TMSearchQueryEditor = ({ qd, dcmtTypesList = [], isExpertMode = SDKUI_Glob
|
|
|
58
58
|
const handleItemSelect = useCallback((tid, mid) => {
|
|
59
59
|
if (currentEditingMID !== mid)
|
|
60
60
|
setCurrentEditingMID(mid);
|
|
61
|
-
if (!focusedTidMid || mid !== focusedTidMid.mid) {
|
|
62
|
-
|
|
61
|
+
if (!focusedTidMid || mid !== focusedTidMid.mid || tid !== focusedTidMid.tid) {
|
|
62
|
+
const newFocusedTidMid = { tid: tid, mid: mid };
|
|
63
|
+
setFocusedTidMid(newFocusedTidMid);
|
|
64
|
+
onFocusedMetadataChanged?.(newFocusedTidMid);
|
|
63
65
|
}
|
|
64
|
-
}, [currentEditingMID, focusedTidMid]);
|
|
66
|
+
}, [currentEditingMID, focusedTidMid, onFocusedMetadataChanged]);
|
|
65
67
|
const handleAdvancedMenuClick = useCallback((e) => {
|
|
66
68
|
if (e.button === AdvancedMenuButtons.MakeEditable && e.mid) {
|
|
67
69
|
addOrRemoveEditableList(e.mid);
|
|
@@ -3,6 +3,7 @@ import { UserDescriptor } from '@topconsultnpm/sdk-ts';
|
|
|
3
3
|
export declare const useDataUserIdItem: () => {
|
|
4
4
|
loadUsersAsync: (userIDs: Set<number>) => Promise<void>;
|
|
5
5
|
getUserItem: (userId: number) => UserDescriptor | undefined;
|
|
6
|
+
getCompleteUserName: (domain: string | undefined, name: string | undefined) => string | undefined;
|
|
6
7
|
clearCache: () => void;
|
|
7
8
|
hasUser: (userId: number) => boolean;
|
|
8
9
|
usersCache: React.MutableRefObject<Map<number, UserDescriptor>>;
|