@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
- return (prevNode.key === nextNode.key &&
59
- prevNode.expanded === nextNode.expanded &&
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 handleNodeToggle = async (key, ctrlKey) => {
232
- const updateNodeRecursively = async (nodes) => {
233
- const updatedNodes = await Promise.all(nodes.map(async (node) => {
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
- return updatedNodes;
264
- };
265
- updateNodeRecursively(dataSource).then(updatedData => {
266
- onDataChanged?.(updatedData);
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 delayFocus = shouldDelayFocusOnEvent ? shouldDelayFocusOnEvent(node, e) : false;
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
- onFocusedItemChanged?.(node);
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
- onFocusedItemChanged?.(node); // Chiama onFocusedItemChanged immediatamente
344
+ onFocusedItemChangedRef.current?.(node); // Chiama onFocusedItemChanged immediatamente
311
345
  }
312
- }, [onFocusedItemChanged, calculateItemsForNode, onNodeUpdate]);
346
+ }, []);
313
347
  const handleCheckboxChange = useCallback((node, checked) => {
314
- // Usa la ref per accedere sempre al valore più recente di selectedItems
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 && autoSelectChildren) {
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 (autoSelectChildren) {
362
- const parentsToAdd = findAndCheckParents(dataSource, node, newSelectedItems);
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 && autoSelectChildren) {
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 (autoSelectChildren && !node.isContainer) {
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(dataSource);
430
+ const parentsToRemove = removeParentContainers(currentDataSource);
395
431
  newSelectedItems = newSelectedItems.filter(item => !parentsToRemove.includes(item.key));
396
432
  }
397
433
  }
398
- onSelectionChanged?.(newSelectedItems);
399
- }, [autoSelectChildren, dataSource, onSelectionChanged]);
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 (!autoSelectChildren)
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 = selectedItems.map(item => item.key);
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
- onNodeUpdate?.(updatedNode);
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(dataSource);
441
- onDataChanged?.(updatedData);
442
- }, [dataSource, onDataChanged, onNodeUpdate]);
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 TMDistinctValues = ({ tid, mid, layoutMode = LayoutModes.None, allowAppendMode = true, showHeader = true, isModal, separator = " ", onSelectionChanged, onClosePanelCallback }) => {
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 [currentValue, setCurrentValue] = useState('');
21
- const { loadDataListsAsync, renderDataListCell, dataListsCache } = useDataListItem();
22
- const { loadUsersAsync, renderUserIdViewer, usersCache } = useDataUserIdItem();
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
- setCurrentValue('');
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 (!focusedItem.value) {
46
- setCurrentValue('');
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
- let newValue = stringIsNullOrEmpty(currentValue)
52
- ? value
53
- : isAppendMode ? currentValue + separator + value : value;
54
- setCurrentValue(newValue);
55
- onSelectionChanged?.({ tid: tid, mid: mid, newValue: newValue, isAppendMode: isAppendMode });
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
- return dtdResult.rows.map((row, index) => {
123
- let rowObject = { rowIndex: index };
124
- dtdResult.columns && dtdResult.columns.forEach((col, colIndex) => {
125
- if (col?.caption) {
126
- rowObject[colIndex === 0 ? "value" : col.caption] = col.dataType === DataColumnTypes.Number
127
- ? row?.[colIndex]?.replace('.', ',')
128
- : row?.[colIndex] ?? null;
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() && _jsx(TMCheckBox, { elementStyle: { position: 'absolute', right: '15px', top: '15px', zIndex: 10000 }, label: 'Accoda', value: isAppendMode, onValueChanged: () => { setIsAppendMode(!isAppendMode); } }), _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: () => !isAppendMode && onClosePanelCallback?.(), pageSize: TMDataGridPageSize.Medium })] }));
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
- setFocusedTidMid({ tid: tid, mid: mid });
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>>;
@@ -90,6 +90,7 @@ export const useDataUserIdItem = () => {
90
90
  return {
91
91
  loadUsersAsync,
92
92
  getUserItem,
93
+ getCompleteUserName,
93
94
  clearCache,
94
95
  hasUser,
95
96
  usersCache: usersCacheRef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev2.14",
3
+ "version": "6.22.0-dev2.15",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",