@topconsultnpm/sdkui-react 6.22.0-dev3.14 → 6.22.0-dev3.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.
@@ -459,6 +459,12 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
459
459
  return selectedCount > 0 && selectedCount < childKeys.length;
460
460
  }, [flattenTree]);
461
461
  const hasVisibleItems = useCallback((node) => {
462
+ // Nodo già caricato: conta i figli visibili (isExpandible è solo una stima a priori)
463
+ if (node.isLoaded) {
464
+ if (!node.items)
465
+ return false;
466
+ return node.items.filter(o => !o.hidden).length > 0;
467
+ }
462
468
  // If node has explicit isExpandible value, use that
463
469
  if (node.isExpandible !== undefined) {
464
470
  return node.isExpandible;
@@ -34,7 +34,7 @@ const StyledTitle = styled.h2 ` margin: 0; font-size: 1.5em; color: #333; `;
34
34
  const StyledProgressBarContainer = styled.div ` width: 100%; height: 20px; background: #e0e0e0; border-radius: 10px; overflow: hidden; margin: 20px 0; `;
35
35
  const StyledProgressBar = styled.div ` height: 100%; background: #4caf50; transition: width 0.3s ease; `;
36
36
  const StyledProgressText = styled.p ` font-weight: bold; color: #333; margin: 0; `;
37
- const StyledMessage = styled.p ` color: #666; font-size: 0.9em; margin-top: 10px; `;
37
+ const StyledMessage = styled.p ` color: #666; font-size: 0.9em; margin-top: 10px; white-space: pre-line; `;
38
38
  const StyledAbortButton = styled.button ` background: #ff4d4d; color: white; border: none; border-radius: 5px; padding: 10px 20px; font-size: 1em; cursor: pointer; margin-top: 20px; &:hover { background: #ff6666; } `;
39
39
  export const TMWaitPanel = (props) => {
40
40
  const { title, showPrimary, textPrimary, valuePrimary, maxValuePrimary, showSecondary, textSecondary, valueSecondary, maxValueSecondary, isCancelable, abortController, onAbortClick, useHighZIndex } = props;
@@ -45,6 +45,13 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
45
45
  const [refreshKey, setRefreshKey] = useState(0);
46
46
  // Separate refresh key for TMFormOrResultWrapper only (doesn't affect tmTreeView)
47
47
  const [refreshKeyFormOrResult, setRefreshKeyFormOrResult] = useState(0);
48
+ // Stato dell'albero conservato qui perché il bump di refreshKey rimonta TMRelationViewer:
49
+ // una ref interna al viewer non sopravvivrebbe. Riletto dal useMemo di tmTreeView,
50
+ // che si ricalcola proprio al refresh.
51
+ const treeStateRef = useRef(undefined);
52
+ const handleTreeStateChange = useCallback((state) => {
53
+ treeStateRef.current = state;
54
+ }, []);
48
55
  /** State for transformed focusedItem metadata values (similar to formData in TMDcmtForm) */
49
56
  const [focusedItemFormData, setFocusedItemFormData] = useState([]);
50
57
  // Trigger operationItems refresh (after file substitution, etc.)
@@ -386,11 +393,11 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
386
393
  setContextMenuVisible(true);
387
394
  }, children: [_jsx(TMRelationViewerWrapper, { refreshKey: refreshKey, inputDcmts: inputDcmts, isForMaster: isForMaster, showCurrentDcmtIndicator: showCurrentDcmtIndicator, showZeroDcmts: showZeroDcmts,
388
395
  // customItemRender={customItemRender}
389
- allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: handleSelectedItemsChanged, onNoRelationsFound: handleNoRelationsFound, onItemContextMenu: isRelationViewerLoading ? undefined : onItemContextMenu, focusedItemFormData: focusedItemFormData, onLoadingStateChanged: setIsRelationViewerLoading, onLoadingProgress: handleRelationsProgress, onStopCalculationReady: handleStopCalculationReady, onCalculationStopped: handleRelationsCalculationStopped }), _jsx(TMContextMenu, { items: operationItems, externalControl: {
396
+ allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: handleSelectedItemsChanged, onNoRelationsFound: handleNoRelationsFound, onItemContextMenu: isRelationViewerLoading ? undefined : onItemContextMenu, focusedItemFormData: focusedItemFormData, onLoadingStateChanged: setIsRelationViewerLoading, onLoadingProgress: handleRelationsProgress, onStopCalculationReady: handleStopCalculationReady, onCalculationStopped: handleRelationsCalculationStopped, initialTreeState: treeStateRef.current, onTreeStateChange: handleTreeStateChange }), _jsx(TMContextMenu, { items: operationItems, externalControl: {
390
397
  visible: contextMenuVisible,
391
398
  position: contextMenuPosition,
392
399
  onClose: () => setContextMenuVisible(false)
393
- } })] }) }), [inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, allowMultipleSelection, focusedItem, selectedItems, handleFocusedItemChanged, handleSelectedItemsChanged, handleNoRelationsFound, onItemContextMenu, contextMenuVisible, contextMenuPosition, refreshKey, focusedItemFormData, isRelationViewerLoading, handleStopCalculationReady, handleRelationsCalculationStopped]);
400
+ } })] }) }), [inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, allowMultipleSelection, focusedItem, selectedItems, handleFocusedItemChanged, handleSelectedItemsChanged, handleNoRelationsFound, onItemContextMenu, contextMenuVisible, contextMenuPosition, refreshKey, focusedItemFormData, isRelationViewerLoading, handleStopCalculationReady, handleRelationsCalculationStopped, handleTreeStateChange]);
394
401
  const tmFormOrResult = useMemo(() => _jsx(TMFormOrResultWrapper, { refreshKey: refreshKeyFormOrResult, deviceType: deviceType, focusedItem: focusedItem, onTaskCreateRequest: onTaskCreateRequest, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, handleNavigateToReference: handleNavigateToReference, onRefreshAfterAddDcmtToFavs: onRefreshAfterAddDcmtToFavs, editPdfForm: editPdfForm, openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, onOpenPdfEditorRequest: onOpenPdfEditorRequest, onRefreshSearchResults: onRefreshAllPanels }), [focusedItem, deviceType, allTasks, handleNavigateToWGs, handleNavigateToDossiers, handleNavigateToReference, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onRefreshAfterAddDcmtToFavs, refreshKeyFormOrResult]);
395
402
  const initialPanelDimensions = {
396
403
  'tmTreeView': { width: '50%', height: '100%' },
@@ -484,7 +491,7 @@ export default TMMasterDetailDcmts;
484
491
  * - Panel visibility toggling
485
492
  * - Focus delay handling
486
493
  */
487
- const TMRelationViewerWrapper = ({ refreshKey, inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, customItemRender, allowMultipleSelection, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onNoRelationsFound, onItemContextMenu, focusedItemFormData, onLoadingStateChanged, onLoadingProgress, onStopCalculationReady, onCalculationStopped }) => {
494
+ const TMRelationViewerWrapper = ({ refreshKey, inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, customItemRender, allowMultipleSelection, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onNoRelationsFound, onItemContextMenu, focusedItemFormData, onLoadingStateChanged, onLoadingProgress, onStopCalculationReady, onCalculationStopped, initialTreeState, onTreeStateChange }) => {
488
495
  const { setPanelVisibilityById, setToolbarButtonVisibility } = useTMPanelManagerContext();
489
496
  // Monitor device type changes to restore panel visibility when switching from mobile to desktop
490
497
  const deviceType = useDeviceType();
@@ -544,7 +551,7 @@ const TMRelationViewerWrapper = ({ refreshKey, inputDcmts, isForMaster, showCurr
544
551
  onItemContextMenu?.(item, e);
545
552
  }, 100);
546
553
  }, [onItemContextMenu, handleFocusedItemChanged]);
547
- return (_jsx(TMRelationViewer, { inputDcmts: inputDcmts, isForMaster: isForMaster, showCurrentDcmtIndicator: showCurrentDcmtIndicator, initialShowZeroDcmts: showZeroDcmts, customItemRender: customItemRender, allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: onSelectedItemsChanged, maxDepthLevel: 1, invertMasterNavigation: false, showExpandAllButton: true, onNoRelationsFound: onNoRelationsFound, onItemContextMenu: onContextMenu, focusedItemFormData: focusedItemFormData, onLoadingStateChanged: onLoadingStateChanged, onLoadingProgress: onLoadingProgress, onStopCalculationReady: onStopCalculationReady, onCalculationStopped: onCalculationStopped }, refreshKey));
554
+ return (_jsx(TMRelationViewer, { inputDcmts: inputDcmts, isForMaster: isForMaster, showCurrentDcmtIndicator: showCurrentDcmtIndicator, initialShowZeroDcmts: showZeroDcmts, customItemRender: customItemRender, allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: onSelectedItemsChanged, maxDepthLevel: 1, invertMasterNavigation: false, showExpandAllButton: true, onNoRelationsFound: onNoRelationsFound, onItemContextMenu: onContextMenu, focusedItemFormData: focusedItemFormData, onLoadingStateChanged: onLoadingStateChanged, onLoadingProgress: onLoadingProgress, onStopCalculationReady: onStopCalculationReady, onCalculationStopped: onCalculationStopped, initialTreeState: initialTreeState, onTreeStateChange: onTreeStateChange }, refreshKey));
548
555
  };
549
556
  const TMFormOrResultWrapper = ({ refreshKey, deviceType, focusedItem, onTaskCreateRequest, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, onRefreshAfterAddDcmtToFavs, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onRefreshSearchAsyncDatagrid, onRefreshSearchResults, handleNavigateToReference, fetchRemoteCertificates }) => {
550
557
  const { setPanelVisibilityById } = useTMPanelManagerContext();
@@ -3,6 +3,7 @@ import { DcmtTypeDescriptor, SearchResultDescriptor, DataColumnDescriptor } from
3
3
  import { DcmtMetadataMap } from '../../../helper';
4
4
  import { DcmtInfo, MetadataValueDescriptorEx } from '../../../ts';
5
5
  import { ITMTreeItem } from '../../base/TMTreeView';
6
+ import { RelationTreeState } from './relationTreeState';
6
7
  /**
7
8
  * Tree item structure for relations
8
9
  */
@@ -26,6 +27,7 @@ export interface RelationTreeItem extends ITMTreeItem {
26
27
  fileExt?: string;
27
28
  fileCount?: number;
28
29
  }
30
+ export type { RelationTreeState } from './relationTreeState';
29
31
  /**
30
32
  * Props for TMRelationViewer component
31
33
  */
@@ -151,6 +153,14 @@ export interface TMRelationViewerProps {
151
153
  * Riceve quanti documenti di input sono stati completati sul totale.
152
154
  */
153
155
  onCalculationStopped?: (loadedCount: number, totalCount: number) => void;
156
+ /**
157
+ * Stato dell'albero da ripristinare al primo caricamento, al posto dell'espansione
158
+ * iniziale predefinita: permette al padre di rimontare il viewer dopo un'operazione
159
+ * senza che l'albero torni allo stato di partenza. Se assente vale il default.
160
+ */
161
+ initialTreeState?: RelationTreeState;
162
+ /** Notifica lo stato dell'albero, da restituire come `initialTreeState` dopo un refresh. */
163
+ onTreeStateChange?: (state: RelationTreeState) => void;
154
164
  }
155
165
  /**
156
166
  * Check if document type has detail relations
@@ -13,6 +13,7 @@ import TMDcmtIcon from './TMDcmtIcon';
13
13
  import TMDataUserIdItemViewer from '../../viewers/TMDataUserIdItemViewer';
14
14
  import TMTooltip from '../../base/TMTooltip';
15
15
  import TMSpinner from '../../base/TMSpinner';
16
+ import { applyRelationTreeState, collectRelationTreeState, findRelationNodeByPath } from './relationTreeState';
16
17
  /**
17
18
  * Check if document type has detail relations
18
19
  */
@@ -143,7 +144,7 @@ export const searchResultToDataSource = async (searchResult, hideSysMetadata, si
143
144
  return output;
144
145
  };
145
146
  export const DEFAULT_RELATION_EXPAND_LEVEL = 4; // Default maximum depth level for expansion (can be adjusted)
146
- const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndicator = true, allowShowZeroDcmts = true, initialShowZeroDcmts = false, allowedTIDs, allowMultipleSelection = false, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onAllItemsChanged, onDocumentDoubleClick, customItemRender, customDocumentStyle, customMainContainerContent, customDocumentContent, showMetadataNames = false, maxDepthLevel = 2, invertMasterNavigation = true, additionalStaticItems, showMainDocument = true, labelMainContainer, onNoRelationsFound, onItemContextMenu, focusedItemFormData = [], showExpandAllButton = false, defaultExpandAll = false, onLoadingStateChanged, onLoadingProgress, onStopCalculationReady, onCalculationStopped }) => {
147
+ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndicator = true, allowShowZeroDcmts = true, initialShowZeroDcmts = false, allowedTIDs, allowMultipleSelection = false, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onAllItemsChanged, onDocumentDoubleClick, customItemRender, customDocumentStyle, customMainContainerContent, customDocumentContent, showMetadataNames = false, maxDepthLevel = 2, invertMasterNavigation = true, additionalStaticItems, showMainDocument = true, labelMainContainer, onNoRelationsFound, onItemContextMenu, focusedItemFormData = [], showExpandAllButton = false, defaultExpandAll = false, onLoadingStateChanged, onLoadingProgress, onStopCalculationReady, onCalculationStopped, initialTreeState, onTreeStateChange }) => {
147
148
  // State
148
149
  const [dcmtTypes, setDcmtTypes] = useState([]);
149
150
  const [treeData, setTreeData] = useState([]);
@@ -177,6 +178,14 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
177
178
  const userInteractedWithStaticItemsRef = React.useRef(false);
178
179
  // Ref to track the last inputKey for which we set the focused item
179
180
  const lastFocusedInputRef = React.useRef('');
181
+ // Catturato al mount (il padre lo aggiorna di continuo) e azzerato dopo l'uso,
182
+ // così vale solo per il primo caricamento
183
+ const stateToRestoreRef = React.useRef(initialTreeState);
184
+ // Parte dallo stato ricevuto: il padre azzera il fuoco prima di rimontare, quindi
185
+ // la prima notifica dopo il rimontaggio lo perderebbe
186
+ const lastReportedStateRef = React.useRef(initialTreeState);
187
+ // Percorso su cui rimettere il fuoco dopo un ripristino
188
+ const pendingFocusPathRef = React.useRef(undefined);
180
189
  // State for all users (used for checkout status display)
181
190
  const [allUsers, setAllUsers] = useState([]);
182
191
  // Load all users for checkout status resolution
@@ -479,6 +488,26 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
479
488
  return { ...node, hidden: shouldHide, items: updatedItems };
480
489
  });
481
490
  }, [showZeroDcmts]);
491
+ /** Messaggio informativo in testa se tutti i figli sono nascosti dal filtro; senza figli il nodo resta vuoto (nessuna freccina). */
492
+ const applyZeroFilterInfoMessage = useCallback((nodeKey, items) => {
493
+ const cleaned = items.filter(child => !(child.isInfoMessage && child.key?.startsWith('__info__')));
494
+ if (showZeroDcmts)
495
+ return cleaned;
496
+ const realChildren = cleaned.filter(child => !child.isInfoMessage);
497
+ if (realChildren.length === 0 || !realChildren.every(child => child.hidden))
498
+ return cleaned;
499
+ return [
500
+ {
501
+ key: `__info__${nodeKey}`,
502
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
503
+ isContainer: false,
504
+ isDcmt: false,
505
+ isInfoMessage: true,
506
+ isExpandible: false
507
+ },
508
+ ...cleaned // Keep hidden containers so they can be shown when toggling
509
+ ];
510
+ }, [showZeroDcmts]);
482
511
  /**
483
512
  * Helper function to set up initial tree expansion state.
484
513
  * Called after all data is loaded but before setTreeData.
@@ -566,6 +595,18 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
566
595
  });
567
596
  return result;
568
597
  };
598
+ /**
599
+ * Carica i figli di un documento durante il ripristino. Usa i loader e non
600
+ * calculateItemsForNode, che accenderebbe il wait panel di espansione mentre
601
+ * è già attivo quello del caricamento principale.
602
+ */
603
+ const loadChildrenForRestore = async (node) => {
604
+ if (!node.tid || !node.did)
605
+ return [];
606
+ if (isForMaster && invertMasterNavigation)
607
+ return await getMasterDcmtsAsync(node.tid, node.did, 1, abortController.signal);
608
+ return await getDetailDcmtsAsync(node.tid, node.did, 1, abortController.signal);
609
+ };
569
610
  /**
570
611
  * Main data loading function
571
612
  */
@@ -852,12 +893,20 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
852
893
  }
853
894
  // FIRST setup initial expansion state (root container expanded, first document expanded with focus, first correlation folder expanded)
854
895
  // This must run BEFORE updateHiddenProperty so that infoMessage logic sees expanded=true
855
- const expandedTree = setupInitialTreeExpansion(tree);
896
+ let expandedTree = setupInitialTreeExpansion(tree);
897
+ // Stato precedente da ripristinare (rimontaggio dopo un'operazione): si riapplica
898
+ // sopra l'espansione iniziale, che resta necessaria perché marca isRoot.
899
+ const stateToRestore = stateToRestoreRef.current;
900
+ stateToRestoreRef.current = undefined;
901
+ if (stateToRestore && stateToRestore.inputKey === getInputKey()) {
902
+ expandedTree = await applyRelationTreeState(expandedTree, stateToRestore, loadChildrenForRestore, () => abortController.signal.aborted);
903
+ pendingFocusPathRef.current = stateToRestore.focusedPath;
904
+ }
856
905
  // THEN apply hidden property transformations (creates new objects, adds infoMessage where needed)
857
906
  // Now it will correctly detect expanded nodes and add infoMessage if all children are hidden
858
907
  const processedTree = updateHiddenProperty(expandedTree);
859
908
  setTreeData(processedTree);
860
- }, [inputDcmts, dcmtTypes, maxDepthLevel, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, abortController, updateHiddenProperty, showMainDocument, labelMainContainer, onNoRelationsFound, onCalculationStopped]);
909
+ }, [inputDcmts, dcmtTypes, maxDepthLevel, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, abortController, updateHiddenProperty, showMainDocument, labelMainContainer, onNoRelationsFound, onCalculationStopped, getInputKey]);
861
910
  /**
862
911
  * Merge main tree data with additional static items
863
912
  */
@@ -872,6 +921,18 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
872
921
  // and may contain custom properties that would be lost by the spread operator
873
922
  return [...treeData, ...itemsToMerge];
874
923
  }, [treeData, additionalStaticItems, staticItemsState]);
924
+ /** Notifica al padre lo stato dell'albero, così che possa restituirlo dopo un refresh. */
925
+ useEffect(() => {
926
+ if (!onTreeStateChange)
927
+ return;
928
+ // Ad albero vuoto (caricamento in corso) si sovrascriverebbe lo stato da ripristinare
929
+ if (treeData.length === 0)
930
+ return;
931
+ // Il padre azzera il fuoco all'avvio di un refresh: si conserva l'ultimo noto
932
+ const state = collectRelationTreeState(treeData, getInputKey(), focusedItem?.key, lastReportedStateRef.current?.focusedPath);
933
+ lastReportedStateRef.current = state;
934
+ onTreeStateChange(state);
935
+ }, [treeData, focusedItem, getInputKey, onTreeStateChange]);
875
936
  /**
876
937
  * Notify parent with a flattened list of all items whenever the merged tree data changes.
877
938
  */
@@ -951,6 +1012,18 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
951
1012
  if (lastFocusedInputRef.current === currentInputKey) {
952
1013
  return;
953
1014
  }
1015
+ // Ripristino dopo un refresh. Se il nodo non esiste più (es. documento cancellato
1016
+ // fisicamente) si prosegue con il fuoco predefinito sul root.
1017
+ const focusPathToRestore = pendingFocusPathRef.current;
1018
+ pendingFocusPathRef.current = undefined;
1019
+ if (focusPathToRestore) {
1020
+ const nodeToFocus = findRelationNodeByPath(treeData, focusPathToRestore);
1021
+ if (nodeToFocus) {
1022
+ onFocusedItemChanged(nodeToFocus);
1023
+ lastFocusedInputRef.current = currentInputKey;
1024
+ return;
1025
+ }
1026
+ }
954
1027
  // Helper function to recursively find the first document with isRoot=true
955
1028
  const findFirstRootDocument = (items) => {
956
1029
  for (const item of items) {
@@ -1028,29 +1101,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1028
1101
  if (node.isLoaded && node.items) {
1029
1102
  // Apply current show/hide logic based on showZeroDcmts
1030
1103
  const updatedItems = updateHiddenProperty(node.items);
1031
- // If this is a container, check if we need to show info message
1032
- if (node.isContainer) {
1033
- // Remove any existing info messages
1034
- const filteredItems = updatedItems.filter(child => !(child.isInfoMessage && child.key?.startsWith('__info__')));
1035
- // Check if all real children are hidden
1036
- const realChildren = filteredItems.filter(child => !child.isInfoMessage);
1037
- const allChildrenHidden = realChildren.length > 0 && realChildren.every(child => child.hidden);
1038
- // If showZeroDcmts is false and all items are hidden, show info message + keep hidden containers
1039
- if (!showZeroDcmts && allChildrenHidden) {
1040
- return [
1041
- {
1042
- key: `__info__${node.key}`,
1043
- name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
1044
- isContainer: false,
1045
- isDcmt: false,
1046
- isInfoMessage: true,
1047
- isExpandible: false
1048
- },
1049
- ...filteredItems // Keep hidden containers so they can be shown when toggling
1050
- ];
1051
- }
1052
- }
1053
- return updatedItems;
1104
+ return applyZeroFilterInfoMessage(node.key, updatedItems);
1054
1105
  }
1055
1106
  // ============================================
1056
1107
  // CONTAINER: Show items immediately (no API calls)
@@ -1064,21 +1115,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1064
1115
  if (!node.items)
1065
1116
  return [];
1066
1117
  const updatedItems = updateHiddenProperty(node.items);
1067
- // If showZeroDcmts is false and all items are hidden, add info message + keep hidden containers
1068
- if (!showZeroDcmts && updatedItems.length > 0 && updatedItems.every(item => item.hidden)) {
1069
- return [
1070
- {
1071
- key: `__info__${node.key}`,
1072
- name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
1073
- isContainer: false,
1074
- isDcmt: false,
1075
- isInfoMessage: true,
1076
- isExpandible: false
1077
- },
1078
- ...updatedItems // Keep hidden containers so they can be shown when toggling
1079
- ];
1080
- }
1081
- return updatedItems;
1118
+ return applyZeroFilterInfoMessage(node.key, updatedItems);
1082
1119
  }
1083
1120
  // ============================================
1084
1121
  // DOCUMENT: Load its children (relation containers) lazily
@@ -1119,21 +1156,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1119
1156
  // - Relation type containers are always visible (even if empty)
1120
1157
  // - Other containers with zero items can be hidden based on showZeroDcmts
1121
1158
  const updatedItems = updateHiddenProperty(loadedItems);
1122
- // If showZeroDcmts is false and all items are hidden, add info message + keep hidden containers
1123
- if (!showZeroDcmts && updatedItems.length > 0 && updatedItems.every(item => item.hidden)) {
1124
- return [
1125
- {
1126
- key: `__info__${node.key}`,
1127
- name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
1128
- isContainer: false,
1129
- isDcmt: false,
1130
- isInfoMessage: true,
1131
- isExpandible: false
1132
- },
1133
- ...updatedItems // Keep hidden containers so they can be shown when toggling
1134
- ];
1135
- }
1136
- return updatedItems;
1159
+ return applyZeroFilterInfoMessage(node.key, updatedItems);
1137
1160
  }
1138
1161
  catch (error) {
1139
1162
  console.error('Error loading document relations:', error);
@@ -1146,7 +1169,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1146
1169
  }
1147
1170
  // Default: return existing items
1148
1171
  return node.items;
1149
- }, [isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty, showZeroDcmts]);
1172
+ }, [isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty, applyZeroFilterInfoMessage, showZeroDcmts]);
1150
1173
  /**
1151
1174
  * Default item renderer with metadata display (adapted from TMMasterDetailDcmts.tsx)
1152
1175
  */
@@ -1179,8 +1202,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1179
1202
  gap: '10px',
1180
1203
  height: '32px',
1181
1204
  padding: '6px 0',
1182
- opacity: item.isZero ? 0.99 : 1,
1183
- transition: 'opacity 0.2s ease-in-out'
1205
+ opacity: item.isZero ? 0.4 : 1,
1184
1206
  };
1185
1207
  // Se è il container principale (root) e showMainDocument è false,
1186
1208
  // non mostrare il badge perché conterrebbe il numero di sotto-container, non di documenti
@@ -1210,8 +1232,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1210
1232
  alignItems: 'center',
1211
1233
  cursor: 'pointer',
1212
1234
  userSelect: 'none',
1213
- opacity: item.isZero ? 0.99 : 1,
1214
- transition: 'opacity 0.2s ease-in-out',
1235
+ opacity: item.isZero ? 0.4 : 1,
1215
1236
  textDecoration: isLogicallyDeleted ? 'line-through' : 'none'
1216
1237
  };
1217
1238
  const documentStyle = customDocumentStyle ? { ...defaultDocumentStyle, ...customDocumentStyle(item) } : defaultDocumentStyle;
@@ -1386,14 +1407,17 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1386
1407
  const targetLevel = expandLevel;
1387
1408
  const newAbortController = new AbortController();
1388
1409
  setExpansionAbortController(newAbortController);
1410
+ // Denominatore subito: con maxValue = 0 TMWaitPanel disegna una barra ferma a 0%
1411
+ setExpansionWaitPanelMaxValue(100);
1412
+ setExpansionWaitPanelValue(0);
1389
1413
  // CONTATORI DINAMICI:
1390
1414
  // - totalNodes: cresce quando scopriamo nuovi nodi (evita che percentuale > 100%)
1391
1415
  // - processedNodes: quanti nodi abbiamo già processato
1392
- // - maxReachedProgress: percentuale massima raggiunta (evita che la barra torni indietro)
1416
+ // - shownProgress: percentuale mostrata, che non torna mai indietro
1393
1417
  let totalNodes = 0;
1394
1418
  let processedNodes = 0;
1395
1419
  let hasApiCalls = false;
1396
- let maxReachedProgress = 0;
1420
+ let shownProgress = 0;
1397
1421
  /**
1398
1422
  * Conta i nodi espandibili in un array (solo primo livello, non ricorsivo).
1399
1423
  * Usato per aggiornare totalNodes man mano che scopriamo nuovi figli.
@@ -1409,6 +1433,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1409
1433
  };
1410
1434
  // Conta iniziale: solo i nodi al livello root
1411
1435
  totalNodes = countNodesAtLevel([...workingTreeData, ...workingStaticData]);
1436
+ const initialTotalNodes = totalNodes;
1412
1437
  /**
1413
1438
  * FUNZIONE RICORSIVA DI ESPANSIONE
1414
1439
  *
@@ -1479,7 +1504,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1479
1504
  loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1, newAbortController.signal);
1480
1505
  }
1481
1506
  // Applica le regole di visibilità (showZeroDcmts)
1482
- expandedNode.items = updateHiddenProperty(loadedItems);
1507
+ // Stesse regole dell'espansione manuale (messaggio informativo se tutto nascosto)
1508
+ expandedNode.items = applyZeroFilterInfoMessage(node.key, updateHiddenProperty(loadedItems));
1483
1509
  expandedNode.isLoaded = true;
1484
1510
  expandedNode.itemsCount = loadedItems.length;
1485
1511
  // IMPORTANTE: Aggiungi i nuovi nodi scoperti al contatore totale
@@ -1506,14 +1532,15 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1506
1532
  }
1507
1533
  // Aggiorna il progresso DOPO aver processato questo nodo
1508
1534
  processedNodes++;
1509
- // Calcola percentuale e aggiorna solo se è più alta di prima
1510
- // (evita che la barra di progresso torni indietro)
1511
- const currentProgress = totalNodes > 0 ? Math.floor((processedNodes / totalNodes) * 100) : 0;
1512
- if (currentProgress > maxReachedProgress) {
1513
- maxReachedProgress = currentProgress;
1514
- setExpansionWaitPanelValue(maxReachedProgress);
1515
- }
1516
- setExpansionWaitPanelText(SDKUI_Localizator.ExpansionProgress.replaceParams(processedNodes.toString(), totalNodes.toString()));
1535
+ // La barra non torna mai indietro: segue il valore reale quando sale, altrimenti avanza verso il 99%
1536
+ const realProgress = totalNodes > 0 ? (processedNodes / totalNodes) * 100 : 0;
1537
+ shownProgress = Math.min(99, realProgress > shownProgress
1538
+ ? realProgress
1539
+ : shownProgress + (99 - shownProgress) * 0.01);
1540
+ setExpansionWaitPanelValue(shownProgress);
1541
+ // Seconda riga: spiega perché percentuale e conteggio non vanno di pari passo
1542
+ setExpansionWaitPanelText(SDKUI_Localizator.ExpansionProgress.replaceParams(processedNodes.toString(), totalNodes.toString())
1543
+ + (totalNodes > initialTotalNodes ? `\n${SDKUI_Localizator.ExpansionProgressIsEstimated}` : ''));
1517
1544
  // ═══════════════════════════════════════════════════════════════════
1518
1545
  // PASSO 3: CHIAMATA RICORSIVA SUI FIGLI
1519
1546
  // Questo è il punto chiave: i nuovi nodi appena caricati vengono
@@ -1544,7 +1571,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1544
1571
  setShowExpansionWaitPanel(false);
1545
1572
  setExpansionAbortController(undefined);
1546
1573
  }
1547
- }, [expandLevel, treeData, staticItemsState, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty]);
1574
+ }, [expandLevel, treeData, staticItemsState, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty, applyZeroFilterInfoMessage]);
1548
1575
  /**
1549
1576
  * Apply defaultExpandAll once on initial tree load.
1550
1577
  */
@@ -0,0 +1,32 @@
1
+ import type { RelationTreeItem } from './TMRelationViewer';
2
+ /**
3
+ * Conserva lo stato di un albero delle correlazioni (nodi espansi e fuoco) attraverso
4
+ * un refresh che ricarica i dati da zero. I nodi non sono confrontabili per `key`, che
5
+ * contiene un GUID rigenerato ad ogni caricamento: si usa un percorso costruito sui campi.
6
+ */
7
+ export interface RelationTreeState {
8
+ /** Documenti di input a cui lo stato si riferisce: non va applicato ad altri */
9
+ inputKey: string;
10
+ expandedPaths: string[];
11
+ /**
12
+ * Documenti espansi che avevano già i figli caricati. Quelli espansi ma vuoti
13
+ * (es. il root) non vanno ricaricati: mostrerebbero più di quanto c'era prima.
14
+ */
15
+ loadedPaths: string[];
16
+ focusedPath?: string;
17
+ }
18
+ /** Carica i figli di un documento durante il ripristino. */
19
+ export type LoadChildrenFn = (node: RelationTreeItem) => Promise<RelationTreeItem[]>;
20
+ /**
21
+ * Fotografa lo stato dell'albero.
22
+ * `previousFocusedPath` viene usato quando il fuoco è già stato azzerato dall'esterno.
23
+ */
24
+ export declare const collectRelationTreeState: (nodes: RelationTreeItem[], inputKey: string, focusedKey: string | undefined, previousFocusedPath?: string) => RelationTreeState;
25
+ /**
26
+ * Riapplica lo stato all'albero appena caricato. I nodi che non compaiono nello stato,
27
+ * o che non esistono più (es. documento cancellato), restano chiusi: il confronto per
28
+ * percorso è tollerante per costruzione.
29
+ */
30
+ export declare const applyRelationTreeState: (nodes: RelationTreeItem[], state: RelationTreeState, loadChildren: LoadChildrenFn, isAborted: () => boolean) => Promise<RelationTreeItem[]>;
31
+ /** Cerca un nodo per percorso, per rimettere il fuoco dov'era. */
32
+ export declare const findRelationNodeByPath: (nodes: RelationTreeItem[], path: string) => RelationTreeItem | null;
@@ -0,0 +1,89 @@
1
+ /** I nodi generati (messaggi informativi, separatori) non hanno identità stabile. */
2
+ const hasStableIdentity = (node) => !node.isInfoMessage && !node.isSeparator;
3
+ const getNodeIdentity = (node) => node.isDcmt
4
+ ? `d:${node.tid ?? ''}:${node.did ?? ''}:${node.relationId ?? ''}`
5
+ : `c:${node.tid ?? ''}:${node.relationId ?? ''}:${node.name ?? ''}`;
6
+ /** Il percorso include i padri: lo stesso documento può comparire sotto padri diversi. */
7
+ const getNodePath = (node, parentPath) => {
8
+ const identity = getNodeIdentity(node);
9
+ return parentPath ? `${parentPath}>${identity}` : identity;
10
+ };
11
+ /**
12
+ * Fotografa lo stato dell'albero.
13
+ * `previousFocusedPath` viene usato quando il fuoco è già stato azzerato dall'esterno.
14
+ */
15
+ export const collectRelationTreeState = (nodes, inputKey, focusedKey, previousFocusedPath) => {
16
+ const expandedPaths = [];
17
+ const loadedPaths = [];
18
+ let focusedPath;
19
+ const collect = (items, parentPath) => {
20
+ for (const node of items) {
21
+ if (!hasStableIdentity(node))
22
+ continue;
23
+ const path = getNodePath(node, parentPath);
24
+ if (node.expanded)
25
+ expandedPaths.push(path);
26
+ if (node.expanded && node.isDcmt && node.items)
27
+ loadedPaths.push(path);
28
+ // Entro la stessa istanza montata la key è univoca
29
+ if (focusedKey && node.key === focusedKey)
30
+ focusedPath = path;
31
+ if (node.items)
32
+ collect(node.items, path);
33
+ }
34
+ };
35
+ collect(nodes, '');
36
+ return { inputKey, expandedPaths, loadedPaths, focusedPath: focusedPath ?? previousFocusedPath };
37
+ };
38
+ /**
39
+ * Riapplica lo stato all'albero appena caricato. I nodi che non compaiono nello stato,
40
+ * o che non esistono più (es. documento cancellato), restano chiusi: il confronto per
41
+ * percorso è tollerante per costruzione.
42
+ */
43
+ export const applyRelationTreeState = async (nodes, state, loadChildren, isAborted) => {
44
+ const expandedPaths = new Set(state.expandedPaths);
45
+ const loadedPaths = new Set(state.loadedPaths);
46
+ const apply = async (items, parentPath) => {
47
+ const result = [];
48
+ for (const node of items) {
49
+ if (!hasStableIdentity(node)) {
50
+ result.push(node);
51
+ continue;
52
+ }
53
+ const path = getNodePath(node, parentPath);
54
+ const wasExpanded = expandedPaths.has(path);
55
+ let children = node.items;
56
+ let isLoaded = node.isLoaded;
57
+ let itemsCount = node.itemsCount;
58
+ // I figli dei documenti sono lazy: vanno ricaricati se l'utente li aveva aperti
59
+ if (wasExpanded && node.isDcmt && !children && loadedPaths.has(path) && !isAborted()) {
60
+ children = await loadChildren(node);
61
+ isLoaded = true;
62
+ itemsCount = children.length;
63
+ }
64
+ const updatedItems = children ? await apply(children, path) : children;
65
+ result.push({ ...node, expanded: wasExpanded, items: updatedItems, isLoaded, itemsCount });
66
+ }
67
+ return result;
68
+ };
69
+ return await apply(nodes, '');
70
+ };
71
+ /** Cerca un nodo per percorso, per rimettere il fuoco dov'era. */
72
+ export const findRelationNodeByPath = (nodes, path) => {
73
+ const find = (items, parentPath) => {
74
+ for (const node of items) {
75
+ if (!hasStableIdentity(node))
76
+ continue;
77
+ const currentPath = getNodePath(node, parentPath);
78
+ if (currentPath === path)
79
+ return node;
80
+ if (node.items) {
81
+ const found = find(node.items, currentPath);
82
+ if (found)
83
+ return found;
84
+ }
85
+ }
86
+ return null;
87
+ };
88
+ return find(nodes, '');
89
+ };
@@ -318,6 +318,7 @@ export declare class SDKUI_Localizator {
318
318
  static get ExpansionInProgress(): "Erweiterung läuft..." | "Espansione in corso..." | "Expansion en cours..." | "Expansion in progress..." | "Expansión en curso..." | "Expansão em andamento...";
319
319
  static get FullReloadInProgress(): "Full reload in progress..." | "Recarga completa en curso..." | "Recarregamento completo em andamento..." | "Rechargement complet en cours..." | "Ricaricamento completo in corso..." | "Vollständiges Neuladen läuft...";
320
320
  static get ExpansionProgress(): "Erweiterung {{0}} von {{1}}..." | "Espansione {{0}} di {{1}}..." | "Expansion {{0}} of {{1}}..." | "Expansion {{0}} sur {{1}}..." | "Expansión {{0}} de {{1}}..." | "Expansão {{0}} de {{1}}...";
321
+ static get ExpansionProgressIsEstimated(): "D'autres nœuds sont trouvés pendant l'expansion : le total augmente et le pourcentage est indicatif." | "Durante a expansão são encontrados mais nós: o total aumenta e a percentagem é indicativa." | "Durante l'espansione vengono trovati altri nodi: il totale aumenta e la percentuale è indicativa." | "Durante la expansión se encuentran más nodos: el total aumenta y el porcentaje es orientativo." | "More nodes are found during the expansion: the total increases and the percentage is approximate." | "Während der Erweiterung werden weitere Knoten gefunden: Der Gesamtwert steigt und der Prozentsatz ist ein Richtwert.";
321
322
  static get Expected(): "Attendu" | "Atteso" | "Erwartet" | "Esperado" | "Expected";
322
323
  static get Expiration(): "Ablaufdatum" | "Data de expiração" | "Date d'expiration" | "Expiration" | "Fecha de expiración" | "Scadenza";
323
324
  static get ExpirationDate(): "Ablaufdatum" | "Data de validade" | "Data di scadenza" | "Date d'échéance" | "Expiration date" | "Fecha de vencimiento";
@@ -3147,6 +3147,16 @@ export class SDKUI_Localizator {
3147
3147
  default: return "Espansione {{0}} di {{1}}...";
3148
3148
  }
3149
3149
  }
3150
+ static get ExpansionProgressIsEstimated() {
3151
+ switch (this._cultureID) {
3152
+ case CultureIDs.De_DE: return "Während der Erweiterung werden weitere Knoten gefunden: Der Gesamtwert steigt und der Prozentsatz ist ein Richtwert.";
3153
+ case CultureIDs.En_US: return "More nodes are found during the expansion: the total increases and the percentage is approximate.";
3154
+ case CultureIDs.Es_ES: return "Durante la expansión se encuentran más nodos: el total aumenta y el porcentaje es orientativo.";
3155
+ case CultureIDs.Fr_FR: return "D'autres nœuds sont trouvés pendant l'expansion : le total augmente et le pourcentage est indicatif.";
3156
+ case CultureIDs.Pt_PT: return "Durante a expansão são encontrados mais nós: o total aumenta e a percentagem é indicativa.";
3157
+ default: return "Durante l'espansione vengono trovati altri nodi: il totale aumenta e la percentuale è indicativa.";
3158
+ }
3159
+ }
3150
3160
  static get Expected() {
3151
3161
  switch (this._cultureID) {
3152
3162
  case CultureIDs.De_DE: return "Erwartet";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev3.14",
3
+ "version": "6.22.0-dev3.15",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",