@topconsultnpm/sdkui-react 6.21.0-dev4.9 → 6.21.0-dev5.2

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.
@@ -1,14 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
3
- import { DcmtTypeListCacheService, SDK_Globals, DataColumnTypes, MetadataFormats, SystemMIDs, MetadataDataDomains, RelationCacheService, RelationTypes, UserListCacheService } from "@topconsultnpm/sdk-ts";
4
- import { genUniqueId, IconFolder, IconBackhandIndexPointingRight, IconCircleInfo, getDcmtCicoStatus, IconChevronDown, IconChevronRight, SDKUI_Localizator } from '../../../helper';
3
+ import { DcmtTypeListCacheService, SDK_Globals, DataColumnTypes, MetadataFormats, MetadataDataDomains, RelationCacheService, RelationTypes, UserListCacheService } from "@topconsultnpm/sdk-ts";
4
+ import { genUniqueId, IconFolder, IconBackhandIndexPointingRight, IconCircleInfo, getDcmtCicoStatus, IconChevronDown, IconChevronRight, SDKUI_Localizator, buildDcmtDisplayName, SDKUI_Globals } from '../../../helper';
5
+ import ShowAlert from '../../base/TMAlert';
6
+ import TMToppyMessage from '../../../helper/TMToppyMessage';
5
7
  import { TMColors } from '../../../utils/theme';
6
- import { StyledDivHorizontal, StyledBadge, StyledToolbarForm } from '../../base/Styled';
8
+ import { StyledDivHorizontal, StyledBadge } from '../../base/Styled';
7
9
  import TMTreeView from '../../base/TMTreeView';
8
10
  import { TMWaitPanel } from '../../base/TMWaitPanel';
9
11
  import TMDataListItemViewer from '../../viewers/TMDataListItemViewer';
10
12
  import TMDcmtIcon from './TMDcmtIcon';
11
13
  import TMDataUserIdItemViewer from '../../viewers/TMDataUserIdItemViewer';
14
+ import TMTooltip from '../../base/TMTooltip';
15
+ import TMSpinner from '../../base/TMSpinner';
12
16
  /**
13
17
  * Check if document type has detail relations
14
18
  */
@@ -41,31 +45,6 @@ const isManyToManyRelation = async (relationID) => {
41
45
  const rdlManyToMany = allRelations.filter(o => o.relationType == RelationTypes.ManyToMany && o.id === relationID) ?? [];
42
46
  return rdlManyToMany.length > 0;
43
47
  };
44
- /**
45
- * Get metadata keys excluding system metadata
46
- */
47
- export const getMetadataKeys = (obj) => {
48
- if (!obj)
49
- return [];
50
- const keys = Object.keys(obj);
51
- // Escludi metadati di sistema (MID < 100 che sono uppercase) e altri campi tecnici
52
- const sysMIDs = Object.values(SystemMIDs).map(o => o.toUpperCase());
53
- return keys.filter(k => obj?.[k].value && !sysMIDs.includes(k)).filter(o => o !== "rowIndex" && o !== "ISLEXPROT");
54
- };
55
- /**
56
- * Get display value keys for a document (max 5, prioritize SYS_Abstract)
57
- */
58
- export const getDcmtDisplayValue = (obj) => {
59
- if (!obj)
60
- return [];
61
- // Prima cerca SYS_Abstract
62
- if (obj['SYS_Abstract']?.value) {
63
- return ['SYS_Abstract'];
64
- }
65
- // Altrimenti prendi i primi 5 metadati non di sistema
66
- const mdKeys = getMetadataKeys(obj);
67
- return mdKeys.slice(0, 5);
68
- };
69
48
  /**
70
49
  * Get display value formatted by column type
71
50
  */
@@ -113,36 +92,50 @@ export const getDisplayValueByColumn = (col, value) => {
113
92
  export const searchResultToDataSource = async (searchResult, hideSysMetadata) => {
114
93
  const rows = searchResult?.dtdResult?.rows ?? [];
115
94
  const tid = searchResult?.fromTID;
116
- const dtd = await DcmtTypeListCacheService.GetAsync(tid);
95
+ // IMPORTANT: Pass true to get full metadata with all properties (isSpecialSearchOutput, etc.)
96
+ const dtd = await DcmtTypeListCacheService.GetAsync(tid, true);
117
97
  const output = [];
118
98
  for (let index = 0; index < rows.length; index++) {
119
99
  const item = { rowIndex: index };
120
100
  const row = rows[index];
121
101
  for (let i = 0; i < row.length; i++) {
122
- const mid = Number(searchResult?.dtdResult?.columns?.[i]?.extendedProperties?.["MID"] ?? "0");
102
+ const column = searchResult?.dtdResult?.columns?.[i];
103
+ const mid = Number(column?.extendedProperties?.["MID"] ?? "0");
123
104
  if (hideSysMetadata && mid < 100)
124
105
  continue;
125
- let key = searchResult?.dtdResult?.columns?.[i].caption ?? '';
126
- if (mid <= 100)
106
+ // For system metadata (MID <= 100), use UPPERCASE caption as key
107
+ // For custom metadata (MID > 100), use caption as key but handle duplicates
108
+ let key = column?.caption ?? '';
109
+ if (mid <= 100) {
127
110
  key = key.toUpperCase();
111
+ }
112
+ else {
113
+ // If key already exists with a different MID, append MID to make it unique
114
+ if (item[key] !== undefined && item[key]?.md?.id !== mid) {
115
+ key = `${key}_${mid}`;
116
+ }
117
+ }
128
118
  const value = row[i];
129
119
  const md = dtd?.metadata?.find(o => o.id == mid);
130
120
  item[key] = {
131
121
  md: md,
132
- value: getDisplayValueByColumn(searchResult?.dtdResult?.columns?.[i], value)
122
+ value: getDisplayValueByColumn(column, value)
133
123
  };
134
124
  }
135
125
  output.push(item);
136
126
  }
137
127
  return output;
138
128
  };
139
- 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 }) => {
129
+ export const DEFAULT_RELATION_EXPAND_LEVEL = 4; // Default maximum depth level for expansion (can be adjusted)
130
+ 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 }) => {
140
131
  // State
141
132
  const [dcmtTypes, setDcmtTypes] = useState([]);
142
133
  const [treeData, setTreeData] = useState([]);
143
134
  const [showZeroDcmts, setShowZeroDcmts] = useState(initialShowZeroDcmts);
144
135
  const [staticItemsState, setStaticItemsState] = useState([]);
145
- const [allExpanded, setAllExpanded] = useState(defaultExpandAll);
136
+ const getInitialExpandLevel = () => SDKUI_Globals.userSettings?.searchSettings?.relationExpandLevel ?? DEFAULT_RELATION_EXPAND_LEVEL;
137
+ const [expandLevel, setExpandLevel] = useState(getInitialExpandLevel);
138
+ const [isAllCollapsed, setIsAllCollapsed] = useState(false);
146
139
  const initialExpandAllAppliedRef = React.useRef(false);
147
140
  const [showWaitPanel, setShowWaitPanel] = useState(false);
148
141
  const [waitPanelTextPrimary, setWaitPanelTextPrimary] = useState('');
@@ -158,6 +151,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
158
151
  const lastLoadedInputRef = React.useRef('');
159
152
  // State to track loaded input key - triggers re-render for focus selection
160
153
  const [loadedInputKey, setLoadedInputKey] = React.useState('');
154
+ // State to track how many GetMetadataAsync calls failed
155
+ const [metadataErrorCount, setMetadataErrorCount] = React.useState(0);
156
+ // State to track total master documents found (used when isForMaster is true)
157
+ const [totalMasterDocuments, setTotalMasterDocuments] = React.useState(0);
161
158
  // Ref to track if user has manually expanded/collapsed static items
162
159
  const userInteractedWithStaticItemsRef = React.useRef(false);
163
160
  // Ref to track the last inputKey for which we set the focused item
@@ -172,6 +169,18 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
172
169
  };
173
170
  fetchAllUsers();
174
171
  }, []);
172
+ // Notify parent when loading state changes
173
+ useEffect(() => {
174
+ const isLoading = showWaitPanel || showExpansionWaitPanel;
175
+ onLoadingStateChanged?.(isLoading);
176
+ }, [showWaitPanel, showExpansionWaitPanel, onLoadingStateChanged]);
177
+ // Sincronizza expandLevel quando userSettings.searchSettings.relationExpandLevel cambia
178
+ useEffect(() => {
179
+ const globalExpandLevel = SDKUI_Globals.userSettings?.searchSettings?.relationExpandLevel;
180
+ if (globalExpandLevel !== undefined && globalExpandLevel !== expandLevel) {
181
+ setExpandLevel(globalExpandLevel);
182
+ }
183
+ }, [SDKUI_Globals.userSettings?.searchSettings?.relationExpandLevel]);
175
184
  /**
176
185
  * Generate a stable key from inputDcmts to detect real changes
177
186
  */
@@ -200,7 +209,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
200
209
  return [];
201
210
  if (!mDID)
202
211
  return [];
203
- if (maxLevel <= 0)
212
+ // maxLevel = 0 means unlimited depth (expand as much as possible)
213
+ // maxLevel > 0 means specific depth limit
214
+ // maxLevel < 0 means stop recursion
215
+ if (maxLevel < 0)
204
216
  return [];
205
217
  const dcmtTypeHasRel = await hasDetailRelations(mTID);
206
218
  if (!dcmtTypeHasRel)
@@ -283,7 +295,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
283
295
  return [];
284
296
  if (!dDID)
285
297
  return [];
286
- if (maxLevel <= 0)
298
+ // maxLevel = 0 means unlimited depth (expand as much as possible)
299
+ // maxLevel > 0 means specific depth limit
300
+ // maxLevel < 0 means stop recursion
301
+ if (maxLevel < 0)
287
302
  return [];
288
303
  const dcmtTypeHasRel = await hasMasterRelations(dTID);
289
304
  if (!dcmtTypeHasRel)
@@ -387,7 +402,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
387
402
  updatedItems = [
388
403
  {
389
404
  key: `__info__${node.key}`,
390
- name: 'Nessun documento correlato da visualizzare',
405
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
391
406
  isContainer: false,
392
407
  isDcmt: false,
393
408
  isInfoMessage: true,
@@ -498,6 +513,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
498
513
  }
499
514
  const tree = [];
500
515
  let processedCount = 0;
516
+ let errorCount = 0;
501
517
  for (const dcmt of inputDcmts) {
502
518
  // Check for abort
503
519
  if (abortController.signal.aborted) {
@@ -510,7 +526,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
510
526
  if (!searchEngine)
511
527
  continue;
512
528
  // Load document metadata
513
- const result = await searchEngine.GetMetadataAsync(dcmt.TID, dcmt.DID, false);
529
+ let result;
530
+ try {
531
+ result = await searchEngine.GetMetadataAsync(dcmt.TID, dcmt.DID, false);
532
+ }
533
+ catch (metadataError) {
534
+ console.warn('[TMRelationViewer] GetMetadataAsync failed for TID:', dcmt.TID, 'DID:', dcmt.DID, metadataError);
535
+ errorCount++;
536
+ ShowAlert({
537
+ title: SDKUI_Localizator.Warning,
538
+ message: `Il documento (TID: ${dcmt.TID}, DID: ${dcmt.DID}) non esiste più o non è accessibile.`,
539
+ mode: 'warning',
540
+ duration: 5000
541
+ });
542
+ // Skip this document and continue with the next one
543
+ processedCount++;
544
+ setWaitPanelValuePrimary(processedCount);
545
+ continue;
546
+ }
514
547
  const dtd = await DcmtTypeListCacheService.GetAsync(dcmt.TID);
515
548
  // Parse document values
516
549
  const docValues = await searchResultToDataSource(result);
@@ -715,6 +748,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
715
748
  if (hasNoRelations && onNoRelationsFound) {
716
749
  onNoRelationsFound();
717
750
  }
751
+ // Update metadata error count state
752
+ setMetadataErrorCount(errorCount);
753
+ // Count total master documents (only relevant when isForMaster is true)
754
+ if (isForMaster) {
755
+ const countMasterDocuments = (items) => {
756
+ let count = 0;
757
+ for (const item of items) {
758
+ if (item.isDcmt && !item.isZero) {
759
+ count++;
760
+ }
761
+ if (item.items && Array.isArray(item.items)) {
762
+ count += countMasterDocuments(item.items);
763
+ }
764
+ }
765
+ return count;
766
+ };
767
+ setTotalMasterDocuments(countMasterDocuments(tree));
768
+ }
718
769
  // FIRST setup initial expansion state (root container expanded, first document expanded with focus, first correlation folder expanded)
719
770
  // This must run BEFORE updateHiddenProperty so that infoMessage logic sees expanded=true
720
771
  const expandedTree = setupInitialTreeExpansion(tree);
@@ -905,7 +956,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
905
956
  return [
906
957
  {
907
958
  key: `__info__${node.key}`,
908
- name: 'Nessun documento correlato da visualizzare',
959
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
909
960
  isContainer: false,
910
961
  isDcmt: false,
911
962
  isInfoMessage: true,
@@ -934,7 +985,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
934
985
  return [
935
986
  {
936
987
  key: `__info__${node.key}`,
937
- name: 'Nessun documento correlato da visualizzare',
988
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
938
989
  isContainer: false,
939
990
  isDcmt: false,
940
991
  isInfoMessage: true,
@@ -989,7 +1040,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
989
1040
  return [
990
1041
  {
991
1042
  key: `__info__${node.key}`,
992
- name: 'Nessun documento correlato da visualizzare',
1043
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
993
1044
  isContainer: false,
994
1045
  isDcmt: false,
995
1046
  isInfoMessage: true,
@@ -1084,34 +1135,36 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1084
1135
  : defaultDocumentStyle;
1085
1136
  const textDecoration = isLogicallyDeleted ? 'line-through' : 'none';
1086
1137
  const textColor = isLogicallyDeleted ? 'gray' : undefined;
1087
- const defaultMetadataContent = item.values && (_jsx(StyledDivHorizontal, { style: {
1088
- fontSize: '1rem',
1089
- overflow: 'hidden',
1090
- flex: 1,
1091
- minWidth: 0,
1092
- whiteSpace: 'nowrap',
1093
- textDecoration: textDecoration,
1094
- color: textColor
1095
- }, children: getDcmtDisplayValue(item.values).map((key, index) => {
1096
- const md = item.values?.[key]?.md;
1097
- const value = item.values?.[key]?.value;
1098
- const displayValues = getDcmtDisplayValue(item.values);
1099
- const isLast = index === displayValues.length - 1;
1100
- return (_jsxs(StyledDivHorizontal, { style: {
1101
- flexShrink: isLast ? 1 : 0,
1102
- minWidth: isLast ? 0 : 'auto',
1103
- overflow: isLast ? 'hidden' : 'visible',
1104
- textDecoration: textDecoration,
1105
- color: textColor
1106
- }, children: [index > 0 && _jsx("span", { style: { margin: '0 5px', color: textColor || '#999', textDecoration: textDecoration }, children: "\u2022" }), showMetadataNames && (_jsxs("span", { style: { color: textColor || '#666', marginRight: '5px', textDecoration: textDecoration }, children: [md?.name || key, ":"] })), md?.dataDomain === MetadataDataDomains.DataList ? (_jsx("span", { style: { textDecoration: textDecoration, color: textColor }, children: _jsx(TMDataListItemViewer, { dataListId: md.dataListID, viewMode: md.dataListViewMode, value: value }) })) : md?.dataDomain === MetadataDataDomains.UserID ? (_jsx("span", { style: { textDecoration: textDecoration, color: textColor }, children: _jsx(TMDataUserIdItemViewer, { userId: value, showIcon: true }) })) : (_jsx("span", { style: {
1107
- fontWeight: 500,
1108
- overflow: isLast ? 'hidden' : 'visible',
1109
- textOverflow: isLast ? 'ellipsis' : 'clip',
1110
- whiteSpace: 'nowrap',
1111
- textDecoration: textDecoration,
1112
- color: textColor
1113
- }, children: value }))] }, `${key}_${index}`));
1114
- }) }));
1138
+ const defaultMetadataContent = item.values && (() => {
1139
+ const displayKeys = buildDcmtDisplayName(item.values);
1140
+ return (_jsx(StyledDivHorizontal, { style: {
1141
+ fontSize: '1rem',
1142
+ overflow: 'hidden',
1143
+ flex: 1,
1144
+ minWidth: 0,
1145
+ whiteSpace: 'nowrap',
1146
+ textDecoration: textDecoration,
1147
+ color: textColor
1148
+ }, children: displayKeys.map((key, index) => {
1149
+ const md = item.values?.[key]?.md;
1150
+ const value = item.values?.[key]?.value;
1151
+ const isLast = index === displayKeys.length - 1;
1152
+ return (_jsxs(StyledDivHorizontal, { style: {
1153
+ flexShrink: isLast ? 1 : 0,
1154
+ minWidth: isLast ? 0 : 'auto',
1155
+ overflow: isLast ? 'hidden' : 'visible',
1156
+ textDecoration: textDecoration,
1157
+ color: textColor
1158
+ }, children: [index > 0 && _jsx("span", { style: { margin: '0 5px', color: textColor || '#999', textDecoration: textDecoration }, children: "\u2022" }), showMetadataNames && (_jsxs("span", { style: { color: textColor || '#666', marginRight: '5px', textDecoration: textDecoration }, children: [md?.name || key, ":"] })), md?.dataDomain === MetadataDataDomains.DataList ? (_jsx("span", { style: { textDecoration: textDecoration, color: textColor }, children: _jsx(TMDataListItemViewer, { dataListId: md.dataListID, viewMode: md.dataListViewMode, value: value }) })) : md?.dataDomain === MetadataDataDomains.UserID ? (_jsx("span", { style: { textDecoration: textDecoration, color: textColor }, children: _jsx(TMDataUserIdItemViewer, { userId: value, showIcon: true }) })) : (_jsx("span", { style: {
1159
+ fontWeight: 500,
1160
+ overflow: isLast ? 'hidden' : 'visible',
1161
+ textOverflow: isLast ? 'ellipsis' : 'clip',
1162
+ whiteSpace: 'nowrap',
1163
+ textDecoration: textDecoration,
1164
+ color: textColor
1165
+ }, children: value }))] }, `${key}_${index}`));
1166
+ }) }));
1167
+ })();
1115
1168
  const metadataContent = customDocumentContent
1116
1169
  ? customDocumentContent(item, defaultMetadataContent || _jsx(_Fragment, {}))
1117
1170
  : defaultMetadataContent;
@@ -1138,7 +1191,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1138
1191
  checkoutStatusIcon = checkoutStatus.icon;
1139
1192
  }
1140
1193
  }
1141
- return (_jsxs("div", { onDoubleClick: handleDoubleClick, style: documentStyle, children: [item.did && item.tid && showCurrentDcmtIndicator && inputDcmts?.some(d => d.DID === item.did && d.TID === item.tid) ? _jsx(IconBackhandIndexPointingRight, { fontSize: 22, overflow: 'visible' }) : _jsx(_Fragment, {}), item.values && (_jsx(TMDcmtIcon, { tid: item.values?.TID?.value, did: item.values?.DID?.value, fileExtension: item.values?.FILEEXT?.value, fileCount: item.values?.FILECOUNT?.value, isLexProt: item.values?.IsLexProt?.value, isMail: item.values?.ISMAIL?.value, isShared: item.values?.ISSHARED?.value, isSigned: item.values?.ISSIGNED?.value, downloadMode: 'openInNewWindow' })), checkoutStatusIcon, metadataContent] }));
1194
+ return (_jsxs("div", { onDoubleClick: handleDoubleClick, style: documentStyle, children: [item.did && item.tid && showCurrentDcmtIndicator && inputDcmts?.some(d => d.DID === item.did && d.TID === item.tid) ? _jsx(IconBackhandIndexPointingRight, { fontSize: 22, overflow: 'visible' }) : _jsx(_Fragment, {}), item.values && (_jsx(TMDcmtIcon, { tid: item.values?.TID?.value, did: item.values?.DID?.value, fileExtension: item.values?.FILEEXT?.value, fileCount: item.values?.FILECOUNT?.value, isLexProt: item.values?.IsLexProt?.value, isMail: item.values?.ISMAIL?.value, isShared: item.values?.ISSHARED?.value, isSigned: item.values?.ISSIGNED?.value, downloadMode: 'openInNewWindow', usePortal: true })), checkoutStatusIcon, metadataContent] }));
1142
1195
  }, [onDocumentDoubleClick, showCurrentDcmtIndicator, inputDcmts, customMainContainerContent, customDocumentStyle, customDocumentContent, showMetadataNames, showMainDocument, allUsers]);
1143
1196
  /**
1144
1197
  * Wrapper renderer that handles custom rendering if provided
@@ -1184,13 +1237,231 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1184
1237
  return { ...node, expanded: canExpand ? expanded : node.expanded, items: newItems };
1185
1238
  });
1186
1239
  }, []);
1187
- const handleToggleExpandAll = useCallback(() => {
1188
- const next = !allExpanded;
1189
- setAllExpanded(next);
1190
- setTreeData(prev => setExpandedRecursively(prev, next));
1191
- setStaticItemsState(prev => setExpandedRecursively(prev, next));
1192
- userInteractedWithStaticItemsRef.current = true;
1193
- }, [allExpanded, setExpandedRecursively]);
1240
+ // Collassa l'albero impostando tutti i nodi come non espansi
1241
+ const handleCollapseTree = useCallback(() => {
1242
+ // Funzione helper per collassare ricorsivamente tutti i nodi
1243
+ const collapseAllNodes = (nodes) => {
1244
+ return nodes.map(node => ({
1245
+ ...node,
1246
+ expanded: false,
1247
+ items: node.items ? collapseAllNodes(node.items) : node.items
1248
+ }));
1249
+ };
1250
+ // Collassa tutti i nodi dell'albero
1251
+ setTreeData(prevData => collapseAllNodes(prevData));
1252
+ setStaticItemsState(prevData => collapseAllNodes(prevData));
1253
+ setIsAllCollapsed(true);
1254
+ }, []);
1255
+ /**
1256
+ * Espande ricorsivamente l'albero fino a `expandLevel`.
1257
+ * Carica i figli lazy (API) solo se non già in memoria (isLoaded).
1258
+ * Il contatore totalNodes cresce dinamicamente per una % progresso accurata.
1259
+ *
1260
+ * @param forceReload - Se true (Ctrl+click), ricarica tutto ignorando la cache
1261
+ */
1262
+ const handleExpandToLevel = useCallback(async (forceReload = false) => {
1263
+ // Mostra spinner per feedback immediato all'utente
1264
+ TMSpinner.show({ description: forceReload ? SDKUI_Localizator.FullReloadInProgress : SDKUI_Localizator.ExpansionInProgress });
1265
+ // Piccolo delay per permettere allo spinner di renderizzarsi prima del processing pesante
1266
+ await new Promise(resolve => setTimeout(resolve, 50));
1267
+ /**
1268
+ * Se forceReload è true, prima resettiamo tutti i nodi come se non fossero mai stati caricati.
1269
+ * Questo significa rimuovere isLoaded e items da tutti i nodi documento,
1270
+ * così l'espansione successiva li ricaricherà tutti da API.
1271
+ */
1272
+ const resetLoadedState = (nodes) => {
1273
+ return nodes.map(node => {
1274
+ const resetNode = { ...node };
1275
+ // Reset isLoaded per forzare il ricaricamento
1276
+ if (resetNode.isDcmt || resetNode.isExpandible) {
1277
+ resetNode.isLoaded = false;
1278
+ // Manteniamo items solo per i container (cartelle),
1279
+ // perché i container non vengono ricaricati via API
1280
+ // I documenti invece devono ricaricare i loro figli (correlazioni)
1281
+ if (resetNode.isDcmt) {
1282
+ resetNode.items = undefined;
1283
+ resetNode.itemsCount = undefined;
1284
+ }
1285
+ }
1286
+ // Reset ricorsivo sui figli
1287
+ if (resetNode.items && Array.isArray(resetNode.items)) {
1288
+ resetNode.items = resetLoadedState(resetNode.items);
1289
+ }
1290
+ return resetNode;
1291
+ });
1292
+ };
1293
+ // Se forceReload, reset dello stato prima di iniziare
1294
+ let workingTreeData = treeData;
1295
+ let workingStaticData = staticItemsState;
1296
+ if (forceReload) {
1297
+ workingTreeData = resetLoadedState(treeData);
1298
+ workingStaticData = resetLoadedState(staticItemsState);
1299
+ // Aggiorna subito lo stato per riflettere il reset
1300
+ setTreeData(workingTreeData);
1301
+ setStaticItemsState(workingStaticData);
1302
+ }
1303
+ const targetLevel = expandLevel;
1304
+ const newAbortController = new AbortController();
1305
+ setExpansionAbortController(newAbortController);
1306
+ // CONTATORI DINAMICI:
1307
+ // - totalNodes: cresce quando scopriamo nuovi nodi (evita che percentuale > 100%)
1308
+ // - processedNodes: quanti nodi abbiamo già processato
1309
+ // - maxReachedProgress: percentuale massima raggiunta (evita che la barra torni indietro)
1310
+ let totalNodes = 0;
1311
+ let processedNodes = 0;
1312
+ let hasApiCalls = false;
1313
+ let maxReachedProgress = 0;
1314
+ /**
1315
+ * Conta i nodi espandibili in un array (solo primo livello, non ricorsivo).
1316
+ * Usato per aggiornare totalNodes man mano che scopriamo nuovi figli.
1317
+ */
1318
+ const countNodesAtLevel = (nodes) => {
1319
+ let count = 0;
1320
+ for (const node of nodes) {
1321
+ if (node.isContainer || node.isExpandible || node.isDcmt) {
1322
+ count++;
1323
+ }
1324
+ }
1325
+ return count;
1326
+ };
1327
+ // Conta iniziale: solo i nodi al livello root
1328
+ totalNodes = countNodesAtLevel([...workingTreeData, ...workingStaticData]);
1329
+ /**
1330
+ * FUNZIONE RICORSIVA DI ESPANSIONE
1331
+ *
1332
+ * @param nodes - Array di nodi da processare
1333
+ * @param currentLevel - Livello corrente (0 = root)
1334
+ * @returns Array di nodi con expanded=true e figli caricati
1335
+ *
1336
+ * FLUSSO:
1337
+ * 1. Se targetLevel > 0 e currentLevel >= targetLevel → STOP, ritorna i nodi così come sono
1338
+ * (Se targetLevel = 0 → espandi senza limiti, non applicare mai il limite)
1339
+ * 2. Per ogni nodo espandibile:
1340
+ * a) Imposta expanded = true
1341
+ * b) Se i figli non sono caricati → chiama API per caricarli
1342
+ * c) Aggiorna totalNodes con i nuovi figli scoperti
1343
+ * d) CHIAMATA RICORSIVA su expandedNode.items con currentLevel + 1
1344
+ * → QUESTO È IL PUNTO CHIAVE: i nuovi nodi vengono espansi!
1345
+ */
1346
+ const expandToLevel = async (nodes, currentLevel) => {
1347
+ // CONDIZIONE DI USCITA: abbiamo raggiunto il livello target
1348
+ // targetLevel = 0 significa espandi senza limiti (non applicare mai il limite)
1349
+ // targetLevel > 0 significa applica il limite normalmente
1350
+ if (targetLevel > 0 && currentLevel >= targetLevel)
1351
+ return nodes;
1352
+ // Controlla se l'utente ha annullato l'operazione
1353
+ if (newAbortController.signal.aborted)
1354
+ return nodes;
1355
+ const result = [];
1356
+ for (const node of nodes) {
1357
+ // Controlla abort per ogni nodo (permette cancellazione veloce)
1358
+ if (newAbortController.signal.aborted) {
1359
+ result.push(node);
1360
+ continue;
1361
+ }
1362
+ // Crea copia del nodo (immutabilità React)
1363
+ let expandedNode = { ...node };
1364
+ // Verifica se questo nodo può essere espanso
1365
+ const canExpand = node.isContainer || node.isExpandible || node.isDcmt;
1366
+ if (canExpand) {
1367
+ // PASSO 1: Imposta il nodo come espanso
1368
+ expandedNode.expanded = true;
1369
+ // PASSO 2: Verifica se i figli sono già caricati o vanno caricati via API
1370
+ // Se forceReload è true, ricarica sempre (ignora isLoaded e items esistenti)
1371
+ const hasLoadedItems = !forceReload && node.items && Array.isArray(node.items) && node.items.length > 0;
1372
+ const actualHasItems = node.items && Array.isArray(node.items) && node.items.length > 0;
1373
+ const needsLoading = (forceReload || (!hasLoadedItems && !node.isLoaded)) && (node.isExpandible || node.isDcmt);
1374
+ if (needsLoading && node.tid && node.did) {
1375
+ // ═══════════════════════════════════════════════════════════════
1376
+ // CASO A: I figli NON sono in memoria → CHIAMATA API
1377
+ // ═══════════════════════════════════════════════════════════════
1378
+ // Mostra pannello di attesa solo quando ci sono chiamate API
1379
+ if (!hasApiCalls) {
1380
+ hasApiCalls = true;
1381
+ TMSpinner.hide();
1382
+ setShowExpansionWaitPanel(true);
1383
+ setExpansionWaitPanelMaxValue(100);
1384
+ setExpansionWaitPanelValue(0);
1385
+ }
1386
+ try {
1387
+ let loadedItems = [];
1388
+ // Carica i documenti correlati (detail o master in base alla modalità)
1389
+ if (isForMaster && !invertMasterNavigation) {
1390
+ loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1);
1391
+ }
1392
+ else if (isForMaster) {
1393
+ loadedItems = await getMasterDcmtsAsync(node.tid, node.did, 1);
1394
+ }
1395
+ else {
1396
+ loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1);
1397
+ }
1398
+ // Applica le regole di visibilità (showZeroDcmts)
1399
+ expandedNode.items = updateHiddenProperty(loadedItems);
1400
+ expandedNode.isLoaded = true;
1401
+ expandedNode.itemsCount = loadedItems.length;
1402
+ // IMPORTANTE: Aggiungi i nuovi nodi scoperti al contatore totale
1403
+ // Questo viene fatto SOLO se non siamo all'ultimo livello
1404
+ // (perché i nodi dell'ultimo livello non verranno espansi)
1405
+ // targetLevel = 0 significa illimitato, quindi aggiungi sempre
1406
+ if (targetLevel === 0 || currentLevel + 1 < targetLevel) {
1407
+ const newExpandableCount = countNodesAtLevel(loadedItems);
1408
+ totalNodes += newExpandableCount;
1409
+ }
1410
+ }
1411
+ catch (error) {
1412
+ console.error('Errore nel caricamento dei figli del nodo:', error);
1413
+ }
1414
+ }
1415
+ else if (actualHasItems && (targetLevel === 0 || currentLevel + 1 < targetLevel)) {
1416
+ // ═══════════════════════════════════════════════════════════════
1417
+ // CASO B: I figli esistono già in memoria (non ricaricati via API)
1418
+ // Aggiungiamo comunque il loro conteggio a totalNodes
1419
+ // Usiamo actualHasItems invece di hasLoadedItems per gestire anche forceReload
1420
+ // ═══════════════════════════════════════════════════════════════
1421
+ const childCount = countNodesAtLevel(node.items);
1422
+ totalNodes += childCount;
1423
+ }
1424
+ // Aggiorna il progresso DOPO aver processato questo nodo
1425
+ processedNodes++;
1426
+ // Calcola percentuale e aggiorna solo se è più alta di prima
1427
+ // (evita che la barra di progresso torni indietro)
1428
+ const currentProgress = totalNodes > 0 ? Math.floor((processedNodes / totalNodes) * 100) : 0;
1429
+ if (currentProgress > maxReachedProgress) {
1430
+ maxReachedProgress = currentProgress;
1431
+ setExpansionWaitPanelValue(maxReachedProgress);
1432
+ }
1433
+ setExpansionWaitPanelText(SDKUI_Localizator.ExpansionProgress.replaceParams(processedNodes.toString(), totalNodes.toString()));
1434
+ // ═══════════════════════════════════════════════════════════════════
1435
+ // PASSO 3: CHIAMATA RICORSIVA SUI FIGLI
1436
+ // Questo è il punto chiave: i nuovi nodi appena caricati vengono
1437
+ // passati a expandToLevel con currentLevel + 1, quindi anche loro
1438
+ // verranno espansi (se non abbiamo raggiunto il livello target)
1439
+ // ═══════════════════════════════════════════════════════════════════
1440
+ if (expandedNode.items && Array.isArray(expandedNode.items) && expandedNode.items.length > 0) {
1441
+ expandedNode.items = await expandToLevel(expandedNode.items, currentLevel + 1);
1442
+ }
1443
+ }
1444
+ result.push(expandedNode);
1445
+ }
1446
+ return result;
1447
+ };
1448
+ try {
1449
+ // Avvia l'espansione dal livello 0 (root)
1450
+ const expandedTree = await expandToLevel(workingTreeData, 0);
1451
+ setTreeData(expandedTree);
1452
+ // Espandi anche gli item statici (se presenti)
1453
+ const expandedStatic = await expandToLevel(workingStaticData, 0);
1454
+ setStaticItemsState(expandedStatic);
1455
+ setIsAllCollapsed(false);
1456
+ userInteractedWithStaticItemsRef.current = true;
1457
+ }
1458
+ finally {
1459
+ // Cleanup: nascondi spinner e pannello di attesa
1460
+ TMSpinner.hide();
1461
+ setShowExpansionWaitPanel(false);
1462
+ setExpansionAbortController(undefined);
1463
+ }
1464
+ }, [expandLevel, treeData, staticItemsState, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty]);
1194
1465
  /**
1195
1466
  * Apply defaultExpandAll once on initial tree load.
1196
1467
  */
@@ -1216,37 +1487,70 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1216
1487
  setStaticItemsState(staticItems); // Preserve static items state (expanded/collapsed)
1217
1488
  // Mark that user has interacted with the tree (expanded/collapsed nodes)
1218
1489
  userInteractedWithStaticItemsRef.current = true;
1490
+ // Reset collapsed state when user manually expands a node
1491
+ setIsAllCollapsed(false);
1219
1492
  }, []);
1220
1493
  if (showWaitPanel) {
1221
- return _jsx("div", { style: { padding: '20px', textAlign: 'center' }, children: _jsx(TMWaitPanel, { title: 'Caricamento documenti dettaglio', showPrimary: true, textPrimary: waitPanelTextPrimary, valuePrimary: waitPanelValuePrimary, maxValuePrimary: waitPanelMaxValuePrimary, isCancelable: true, abortController: abortController, onAbortClick: (abortController) => { setTimeout(() => { abortController?.abort(); }, 1000); } }) });
1494
+ return _jsx("div", { style: { padding: '20px', textAlign: 'center' }, children: _jsx(TMWaitPanel, { title: SDKUI_Localizator.LoadingDetailDocuments, showPrimary: true, textPrimary: waitPanelTextPrimary, valuePrimary: waitPanelValuePrimary, maxValuePrimary: waitPanelMaxValuePrimary, isCancelable: true, abortController: abortController, onAbortClick: () => { setTimeout(() => { abortController.abort(); }, 1000); } }) });
1222
1495
  }
1223
1496
  if (mergedTreeData.length === 0) {
1497
+ // Se NON isForMaster e tutti i GetMetadataAsync sono falliti
1498
+ if (!isForMaster && metadataErrorCount === inputDcmts.length) {
1499
+ return _jsx(TMToppyMessage, { message: SDKUI_Localizator.DocumentsNotAvailableOrNoCorrelations });
1500
+ }
1224
1501
  // If parent handles no-relations via callback, render nothing to avoid UI flash
1225
1502
  if (onNoRelationsFound)
1226
1503
  return null;
1227
- return _jsx("div", { style: { padding: '20px', textAlign: 'center', color: '#666' }, children: "Nessuna relazione disponibile." });
1504
+ return _jsx("div", { style: { padding: '20px', textAlign: 'center', color: '#666' }, children: SDKUI_Localizator.NoRelationsAvailable });
1505
+ }
1506
+ // Se isForMaster e non ci sono master documents disponibili
1507
+ if (isForMaster && totalMasterDocuments === 0) {
1508
+ return _jsx(TMToppyMessage, { message: SDKUI_Localizator.NoMasterDocumentsAvailable });
1228
1509
  }
1229
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0, width: '100%' }, children: [showExpandAllButton && (_jsx(StyledToolbarForm, { style: { flexShrink: 0 }, children: _jsxs("button", { type: "button", onClick: handleToggleExpandAll, title: allExpanded ? SDKUI_Localizator.CollapseAll : SDKUI_Localizator.ExpandAll, style: {
1230
- display: 'inline-flex',
1231
- alignItems: 'center',
1232
- gap: '4px',
1233
- padding: '4px 8px',
1234
- fontSize: '0.8rem',
1235
- color: TMColors.primary,
1236
- background: 'transparent',
1237
- border: `1px solid ${TMColors.border_normal}`,
1238
- borderRadius: '4px',
1239
- cursor: 'pointer',
1240
- whiteSpace: 'nowrap',
1241
- outline: 'none'
1242
- }, children: [_jsx("span", { style: { display: 'flex', alignItems: 'center' }, children: allExpanded ? _jsx(IconChevronRight, { fontSize: 14 }) : _jsx(IconChevronDown, { fontSize: 14 }) }), _jsx("span", { children: allExpanded ? SDKUI_Localizator.CollapseAll : SDKUI_Localizator.ExpandAll })] }) })), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto' }, children: _jsx(TMTreeView, { dataSource: mergedTreeData, itemRender: finalItemRender, calculateItemsForNode: calculateItemsForNode, onDataChanged: handleDataChanged, focusedItem: focusedItem, onFocusedItemChanged: handleFocusedItemChanged, allowMultipleSelection: allowMultipleSelection, selectedItems: selectedItems, itemsPerPage: 100, onSelectionChanged: handleSelectedItemsChanged, onItemContextMenu: onItemContextMenu, shouldDelayFocusOnEvent: (node, e) => {
1510
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0, width: '100%' }, children: [showExpandAllButton && (_jsxs("div", { style: { flexShrink: 0, display: 'flex', alignItems: 'center', gap: '6px', height: '40px', paddingLeft: '10px', backgroundColor: TMColors.toolbar_background }, children: [_jsx(TMTooltip, { content: expandLevel === 0
1511
+ ? `${SDKUI_Localizator.ExpandAllLevels} (${SDKUI_Localizator.CtrlClickToReloadAll})`
1512
+ : `${SDKUI_Localizator.ExpandToLevel.replaceParams(expandLevel.toString())} (${SDKUI_Localizator.CtrlClickToReloadAll})`, children: _jsxs("button", { type: "button", onClick: (e) => handleExpandToLevel(e.ctrlKey), style: {
1513
+ display: 'inline-flex',
1514
+ alignItems: 'center',
1515
+ justifyContent: 'center',
1516
+ gap: '5px',
1517
+ height: '28px',
1518
+ padding: '0 10px',
1519
+ fontSize: '0.8rem',
1520
+ fontWeight: 500,
1521
+ color: 'white',
1522
+ background: TMColors.primary,
1523
+ border: 'none',
1524
+ borderRadius: '4px',
1525
+ cursor: 'pointer',
1526
+ whiteSpace: 'nowrap',
1527
+ outline: 'none',
1528
+ transition: 'background 0.2s'
1529
+ }, children: [_jsx(IconChevronDown, { fontSize: 14 }), _jsx("span", { children: expandLevel === 0 ? SDKUI_Localizator.ExpandAllLevels : SDKUI_Localizator.ExpandLevels.replaceParams(expandLevel.toString()) })] }) }), _jsx(TMTooltip, { content: SDKUI_Localizator.CollapseTree, children: _jsxs("button", { type: "button", onClick: handleCollapseTree, disabled: isAllCollapsed, style: {
1530
+ display: 'inline-flex',
1531
+ alignItems: 'center',
1532
+ justifyContent: 'center',
1533
+ gap: '5px',
1534
+ height: '28px',
1535
+ padding: '0 10px',
1536
+ fontSize: '0.8rem',
1537
+ fontWeight: 500,
1538
+ color: isAllCollapsed ? '#999' : 'white',
1539
+ background: isAllCollapsed ? '#e0e0e0' : TMColors.secondary || '#6c757d',
1540
+ border: 'none',
1541
+ borderRadius: '4px',
1542
+ cursor: isAllCollapsed ? 'not-allowed' : 'pointer',
1543
+ whiteSpace: 'nowrap',
1544
+ outline: 'none',
1545
+ transition: 'background 0.2s'
1546
+ }, children: [_jsx(IconChevronRight, { fontSize: 14 }), _jsx("span", { children: SDKUI_Localizator.CollapseAll })] }) })] })), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto' }, children: _jsx(TMTreeView, { dataSource: mergedTreeData, itemRender: finalItemRender, calculateItemsForNode: calculateItemsForNode, onDataChanged: handleDataChanged, focusedItem: focusedItem, onFocusedItemChanged: handleFocusedItemChanged, allowMultipleSelection: allowMultipleSelection, selectedItems: selectedItems, itemsPerPage: 100, onSelectionChanged: handleSelectedItemsChanged, onItemContextMenu: showExpansionWaitPanel ? undefined : onItemContextMenu, enableVirtualization: true, shouldDelayFocusOnEvent: (node, e) => {
1243
1547
  // Ritarda il focus quando si clicca sull'icona del documento
1244
1548
  // per permettere al doppio click di funzionare
1245
1549
  const target = e.target;
1246
1550
  return !!target.closest('.tm-dcmt-icon');
1247
- } }) }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? 'Caricamento documenti master' : 'Caricamento documenti dettaglio', showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: (abortController) => {
1551
+ } }) }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? SDKUI_Localizator.LoadingMasterDocuments : SDKUI_Localizator.LoadingDetailDocuments, showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: () => {
1248
1552
  setTimeout(() => {
1249
- abortController?.abort();
1553
+ expansionAbortController?.abort();
1250
1554
  }, 100);
1251
1555
  } }))] }));
1252
1556
  };