@topconsultnpm/sdkui-react 6.21.0-t3 → 6.21.0-t5

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.
Files changed (51) hide show
  1. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +23 -18
  2. package/lib/components/base/TMPopUp.js +3 -3
  3. package/lib/components/base/TMTreeView.d.ts +16 -13
  4. package/lib/components/base/TMTreeView.js +230 -64
  5. package/lib/components/choosers/TMDistinctValues.js +1 -1
  6. package/lib/components/editors/TMTextBox.d.ts +1 -0
  7. package/lib/components/editors/TMTextBox.js +2 -1
  8. package/lib/components/features/documents/TMDcmtForm.d.ts +2 -0
  9. package/lib/components/features/documents/TMDcmtForm.js +2 -1
  10. package/lib/components/features/documents/TMDcmtIcon.d.ts +3 -1
  11. package/lib/components/features/documents/TMDcmtIcon.js +5 -32
  12. package/lib/components/features/documents/TMFileUploader.js +1 -1
  13. package/lib/components/features/documents/TMMasterDetailDcmts.d.ts +2 -0
  14. package/lib/components/features/documents/TMMasterDetailDcmts.js +169 -14
  15. package/lib/components/features/documents/TMMergeToPdfForm.d.ts +2 -1
  16. package/lib/components/features/documents/TMMergeToPdfForm.js +95 -49
  17. package/lib/components/features/documents/TMRelationViewer.d.ts +15 -10
  18. package/lib/components/features/documents/TMRelationViewer.js +533 -179
  19. package/lib/components/features/documents/copyAndMergeDcmtsShared.d.ts +4 -3
  20. package/lib/components/features/documents/copyAndMergeDcmtsShared.js +46 -23
  21. package/lib/components/features/documents/mergePdfUtils.d.ts +52 -0
  22. package/lib/components/features/documents/mergePdfUtils.js +278 -0
  23. package/lib/components/features/search/TMSearch.d.ts +2 -0
  24. package/lib/components/features/search/TMSearch.js +2 -2
  25. package/lib/components/features/search/TMSearchResult.d.ts +2 -0
  26. package/lib/components/features/search/TMSearchResult.js +58 -9
  27. package/lib/components/features/workflow/diagram/xmlParser.js +2 -1
  28. package/lib/components/viewers/TMTidViewer.js +14 -2
  29. package/lib/helper/Enum_Localizator.js +1 -0
  30. package/lib/helper/SDKUI_Globals.d.ts +1 -0
  31. package/lib/helper/SDKUI_Globals.js +1 -0
  32. package/lib/helper/SDKUI_Localizator.d.ts +34 -0
  33. package/lib/helper/SDKUI_Localizator.js +352 -12
  34. package/lib/helper/TMIcons.d.ts +1 -0
  35. package/lib/helper/TMIcons.js +3 -0
  36. package/lib/helper/TMUtils.d.ts +33 -1
  37. package/lib/helper/TMUtils.js +104 -1
  38. package/lib/helper/certificateImportHelper.d.ts +43 -0
  39. package/lib/helper/certificateImportHelper.js +403 -0
  40. package/lib/helper/helpers.d.ts +3 -0
  41. package/lib/helper/helpers.js +29 -1
  42. package/lib/helper/index.d.ts +1 -0
  43. package/lib/helper/index.js +1 -0
  44. package/lib/hooks/useDcmtOperations.d.ts +2 -1
  45. package/lib/hooks/useDcmtOperations.js +10 -2
  46. package/lib/hooks/useDocumentOperations.d.ts +2 -0
  47. package/lib/hooks/useDocumentOperations.js +28 -6
  48. package/lib/services/platform_services.d.ts +1 -1
  49. package/lib/ts/types.d.ts +2 -1
  50. package/lib/ts/types.js +1 -0
  51. package/package.json +3 -4
@@ -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, LayoutModes } from "@topconsultnpm/sdk-ts";
4
+ import { genUniqueId, IconFolder, IconBackhandIndexPointingRight, IconCircleInfo, getDcmtCicoStatus, IconChevronDown, IconChevronRight, SDKUI_Localizator, buildDcmtDisplayName, SDKUI_Globals, searchResultToMetadataValues } 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
  */
@@ -109,40 +88,90 @@ export const getDisplayValueByColumn = (col, value) => {
109
88
  };
110
89
  /**
111
90
  * Convert SearchResultDescriptor to structured data source with metadata
91
+ * For each document, fetches complete metadata using GetMetadataAsync to ensure
92
+ * all metadata properties (isSpecialSearchOutput, permissions, etc.) are available
112
93
  */
113
94
  export const searchResultToDataSource = async (searchResult, hideSysMetadata) => {
114
95
  const rows = searchResult?.dtdResult?.rows ?? [];
115
- const tid = searchResult?.fromTID;
116
- const dtd = await DcmtTypeListCacheService.GetAsync(tid);
117
96
  const output = [];
97
+ // Find TID and DID column indices
98
+ const columns = searchResult?.dtdResult?.columns ?? [];
99
+ const tidColIndex = columns.findIndex(col => col.caption?.toUpperCase() === 'TID');
100
+ const didColIndex = columns.findIndex(col => col.caption?.toUpperCase() === 'DID');
118
101
  for (let index = 0; index < rows.length; index++) {
119
- const item = { rowIndex: index };
120
102
  const row = rows[index];
121
- for (let i = 0; i < row.length; i++) {
122
- const mid = Number(searchResult?.dtdResult?.columns?.[i]?.extendedProperties?.["MID"] ?? "0");
123
- if (hideSysMetadata && mid < 100)
124
- continue;
125
- let key = searchResult?.dtdResult?.columns?.[i].caption ?? '';
126
- if (mid <= 100)
127
- key = key.toUpperCase();
128
- const value = row[i];
129
- const md = dtd?.metadata?.find(o => o.id == mid);
130
- item[key] = {
131
- md: md,
132
- value: getDisplayValueByColumn(searchResult?.dtdResult?.columns?.[i], value)
133
- };
103
+ // Extract TID and DID from row
104
+ const tid = tidColIndex >= 0 ? Number(row[tidColIndex]) : searchResult?.fromTID;
105
+ const did = didColIndex >= 0 ? Number(row[didColIndex]) : undefined;
106
+ // Fetch complete metadata for this specific document (like getFilteredMetadata does)
107
+ const metadata = await SDK_Globals.tmSession?.NewSearchEngine().GetMetadataAsync(tid, did, true);
108
+ if (metadata) {
109
+ // Get full DTD with metadata for this document
110
+ const dtdResult = metadata.dtdResult;
111
+ const metadataRows = dtdResult?.rows?.[0] ?? [];
112
+ const mids = metadata.selectMIDs;
113
+ const dtdWithMetadata = await DcmtTypeListCacheService.GetWithNotGrantedAsync(tid, did, metadata);
114
+ const mdList = dtdWithMetadata?.metadata ?? [];
115
+ // Convert to MetadataValueDescriptorEx array
116
+ const metadataList = searchResultToMetadataValues(tid, dtdResult, metadataRows, mids, mdList, LayoutModes.Update);
117
+ // Convert to DcmtMetadataMap format (like getFilteredMetadata does)
118
+ const item = { rowIndex: index };
119
+ for (const mvd of metadataList) {
120
+ if (hideSysMetadata && mvd.mid !== undefined && mvd.mid < 100)
121
+ continue;
122
+ const key = mvd.mid !== undefined && mvd.mid <= 100
123
+ ? (mvd.md?.name ?? '').toUpperCase()
124
+ : (mvd.md?.name ?? '');
125
+ if (key) {
126
+ item[key] = {
127
+ md: mvd.md,
128
+ value: mvd.value
129
+ };
130
+ }
131
+ }
132
+ output.push(item);
133
+ }
134
+ else {
135
+ // Fallback: use original row data with basic parsing
136
+ const item = { rowIndex: index };
137
+ const fallbackTid = searchResult?.fromTID;
138
+ const dtd = await DcmtTypeListCacheService.GetWithNotGrantedAsync(fallbackTid, undefined, searchResult);
139
+ for (let i = 0; i < row.length; i++) {
140
+ const column = searchResult?.dtdResult?.columns?.[i];
141
+ const mid = Number(column?.extendedProperties?.["MID"] ?? "0");
142
+ if (hideSysMetadata && mid < 100)
143
+ continue;
144
+ let key = column?.caption ?? '';
145
+ if (mid <= 100) {
146
+ key = key.toUpperCase();
147
+ }
148
+ else {
149
+ if (item[key] !== undefined && item[key]?.md?.id !== mid) {
150
+ key = `${key}_${mid}`;
151
+ }
152
+ }
153
+ const value = row[i];
154
+ const md = dtd?.metadata?.find(o => o.id == mid);
155
+ item[key] = {
156
+ md: md,
157
+ value: getDisplayValueByColumn(column, value)
158
+ };
159
+ }
160
+ output.push(item);
134
161
  }
135
- output.push(item);
136
162
  }
137
163
  return output;
138
164
  };
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 }) => {
165
+ export const DEFAULT_RELATION_EXPAND_LEVEL = 4; // Default maximum depth level for expansion (can be adjusted)
166
+ 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
167
  // State
141
168
  const [dcmtTypes, setDcmtTypes] = useState([]);
142
169
  const [treeData, setTreeData] = useState([]);
143
170
  const [showZeroDcmts, setShowZeroDcmts] = useState(initialShowZeroDcmts);
144
171
  const [staticItemsState, setStaticItemsState] = useState([]);
145
- const [allExpanded, setAllExpanded] = useState(defaultExpandAll);
172
+ const getInitialExpandLevel = () => SDKUI_Globals.userSettings?.searchSettings?.relationExpandLevel ?? DEFAULT_RELATION_EXPAND_LEVEL;
173
+ const [expandLevel, setExpandLevel] = useState(getInitialExpandLevel);
174
+ const [isAllCollapsed, setIsAllCollapsed] = useState(false);
146
175
  const initialExpandAllAppliedRef = React.useRef(false);
147
176
  const [showWaitPanel, setShowWaitPanel] = useState(false);
148
177
  const [waitPanelTextPrimary, setWaitPanelTextPrimary] = useState('');
@@ -158,6 +187,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
158
187
  const lastLoadedInputRef = React.useRef('');
159
188
  // State to track loaded input key - triggers re-render for focus selection
160
189
  const [loadedInputKey, setLoadedInputKey] = React.useState('');
190
+ // State to track how many GetMetadataAsync calls failed
191
+ const [metadataErrorCount, setMetadataErrorCount] = React.useState(0);
192
+ // State to track total master documents found (used when isForMaster is true)
193
+ const [totalMasterDocuments, setTotalMasterDocuments] = React.useState(0);
161
194
  // Ref to track if user has manually expanded/collapsed static items
162
195
  const userInteractedWithStaticItemsRef = React.useRef(false);
163
196
  // Ref to track the last inputKey for which we set the focused item
@@ -172,6 +205,18 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
172
205
  };
173
206
  fetchAllUsers();
174
207
  }, []);
208
+ // Notify parent when loading state changes
209
+ useEffect(() => {
210
+ const isLoading = showWaitPanel || showExpansionWaitPanel;
211
+ onLoadingStateChanged?.(isLoading);
212
+ }, [showWaitPanel, showExpansionWaitPanel, onLoadingStateChanged]);
213
+ // Sincronizza expandLevel quando userSettings.searchSettings.relationExpandLevel cambia
214
+ useEffect(() => {
215
+ const globalExpandLevel = SDKUI_Globals.userSettings?.searchSettings?.relationExpandLevel;
216
+ if (globalExpandLevel !== undefined && globalExpandLevel !== expandLevel) {
217
+ setExpandLevel(globalExpandLevel);
218
+ }
219
+ }, [SDKUI_Globals.userSettings?.searchSettings?.relationExpandLevel]);
175
220
  /**
176
221
  * Generate a stable key from inputDcmts to detect real changes
177
222
  */
@@ -200,7 +245,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
200
245
  return [];
201
246
  if (!mDID)
202
247
  return [];
203
- if (maxLevel <= 0)
248
+ // maxLevel = 0 means unlimited depth (expand as much as possible)
249
+ // maxLevel > 0 means specific depth limit
250
+ // maxLevel < 0 means stop recursion
251
+ if (maxLevel < 0)
204
252
  return [];
205
253
  const dcmtTypeHasRel = await hasDetailRelations(mTID);
206
254
  if (!dcmtTypeHasRel)
@@ -261,6 +309,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
261
309
  hidden: false,
262
310
  name: row?.SYS_Abstract?.value || row?.SYS_SUBJECT?.value || `Documento ${did}`,
263
311
  fileExt: row?.FILEEXT?.value,
312
+ fileCount: row?.FILECOUNT?.value,
264
313
  // Note: Recursive loading on expansion is handled by calculateItemsForNode
265
314
  });
266
315
  }
@@ -270,7 +319,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
270
319
  }
271
320
  }
272
321
  catch (error) {
273
- console.error('Error loading detail documents:', error);
322
+ console.error('Error loading detail documents:', error);
274
323
  }
275
324
  return items;
276
325
  }, [allowedTIDs]);
@@ -283,7 +332,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
283
332
  return [];
284
333
  if (!dDID)
285
334
  return [];
286
- if (maxLevel <= 0)
335
+ // maxLevel = 0 means unlimited depth (expand as much as possible)
336
+ // maxLevel > 0 means specific depth limit
337
+ // maxLevel < 0 means stop recursion
338
+ if (maxLevel < 0)
287
339
  return [];
288
340
  const dcmtTypeHasRel = await hasMasterRelations(dTID);
289
341
  if (!dcmtTypeHasRel)
@@ -341,6 +393,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
341
393
  isExpandible: canExpand, // Can this doc be expanded?
342
394
  values: row,
343
395
  searchResult: [searchResult],
396
+ fileExt: row?.FILEEXT?.value,
397
+ fileCount: row?.FILECOUNT?.value,
344
398
  // Leave items and itemsCount undefined so TMTreeView shows expand arrow based on isExpandible
345
399
  // Children will be loaded lazily by calculateItemsForNode when expanded
346
400
  expanded: false,
@@ -386,7 +440,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
386
440
  updatedItems = [
387
441
  {
388
442
  key: `__info__${node.key}`,
389
- name: 'Nessun documento correlato da visualizzare',
443
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
390
444
  isContainer: false,
391
445
  isDcmt: false,
392
446
  isInfoMessage: true,
@@ -421,71 +475,82 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
421
475
  const setupInitialTreeExpansion = (tree) => {
422
476
  if (tree.length === 0)
423
477
  return tree;
424
- // ALWAYS expand the first container (even if empty, to show infoMessage)
425
- const firstRootContainer = tree[0];
426
- if (!firstRootContainer)
427
- return tree;
428
- // Create a deep copy of the first container with expanded=true, isRoot=true
429
- let newFirstContainer = {
430
- ...firstRootContainer,
431
- expanded: true,
432
- isRoot: true
433
- };
434
- // 2. Find first document/container and expand it
435
- const firstRootItems = newFirstContainer.items;
436
- if (firstRootItems && firstRootItems.length > 0) {
437
- const firstDocOrContainer = firstRootItems[0];
438
- if (firstDocOrContainer.isDcmt) {
439
- // First item is a document - expand it and mark as root for focus
440
- let newFirstDoc = {
441
- ...firstDocOrContainer,
442
- expanded: true,
443
- isRoot: true
444
- };
445
- // 3. Expand first correlation folder ONLY if there's exactly one
446
- const docItems = newFirstDoc.items;
447
- const containerChildren = docItems?.filter(child => child.isContainer) ?? [];
448
- if (containerChildren.length === 1 && docItems && docItems.length > 0 && docItems[0].isContainer) {
449
- const newFirstCorrelation = { ...docItems[0], expanded: true };
450
- newFirstDoc = {
451
- ...newFirstDoc,
452
- items: [newFirstCorrelation, ...docItems.slice(1)]
453
- };
478
+ // Find the first container with documents (not empty)
479
+ // This ensures we focus on an actual document, not an empty container
480
+ const findFirstNonEmptyContainerIndex = () => {
481
+ for (let i = 0; i < tree.length; i++) {
482
+ const container = tree[i];
483
+ if (!container.isZero && container.items && Array.isArray(container.items) && container.items.length > 0) {
484
+ return i;
454
485
  }
455
- // Update the container's items
456
- newFirstContainer = {
457
- ...newFirstContainer,
458
- items: [newFirstDoc, ...firstRootItems.slice(1)]
459
- };
460
486
  }
461
- else if (firstDocOrContainer.isContainer) {
462
- // First item is a container (correlation folder)
463
- // Count how many sibling containers there are
464
- const siblingContainers = firstRootItems.filter(child => child.isContainer);
465
- const shouldExpand = siblingContainers.length === 1;
466
- let newFirstCorrelation = {
467
- ...firstDocOrContainer,
468
- expanded: shouldExpand
469
- };
470
- // Find first document inside this container and mark for focus
471
- const containerItems = newFirstCorrelation.items;
472
- if (containerItems && containerItems.length > 0 && containerItems[0].isDcmt) {
473
- const newFirstDoc = { ...containerItems[0], isRoot: true };
474
- newFirstCorrelation = {
475
- ...newFirstCorrelation,
476
- items: [newFirstDoc, ...containerItems.slice(1)]
477
- };
487
+ return 0; // Fallback to first container if all are empty
488
+ };
489
+ const targetContainerIndex = findFirstNonEmptyContainerIndex();
490
+ // Process all containers, but only mark the target one for focus
491
+ const result = tree.map((container, index) => {
492
+ const isTargetContainer = index === targetContainerIndex;
493
+ const isFirstContainer = index === 0;
494
+ // Always expand the first container (even if empty, to show infoMessage)
495
+ // Also expand the target container if different
496
+ const shouldExpand = isFirstContainer || isTargetContainer;
497
+ if (!shouldExpand && !isTargetContainer) {
498
+ return container; // Keep non-target containers as-is
499
+ }
500
+ // Create a copy with expanded state
501
+ let newContainer = {
502
+ ...container,
503
+ expanded: shouldExpand,
504
+ isRoot: isTargetContainer // Only mark target container as root
505
+ };
506
+ // Only process items for the target container (the one with documents)
507
+ if (isTargetContainer) {
508
+ const containerItems = newContainer.items;
509
+ if (containerItems && containerItems.length > 0) {
510
+ const firstDocOrContainer = containerItems[0];
511
+ if (firstDocOrContainer.isDcmt) {
512
+ // First item is a document - expand it and mark as root for focus
513
+ let newFirstDoc = {
514
+ ...firstDocOrContainer,
515
+ expanded: true,
516
+ isRoot: true
517
+ };
518
+ // NOTE: Le cartelle di correlazione rimangono con expanded: false.
519
+ // Verranno espanse solo tramite il pulsante "espandi n livelli" o espansione manuale.
520
+ // Update the container's items
521
+ newContainer = {
522
+ ...newContainer,
523
+ items: [newFirstDoc, ...containerItems.slice(1)]
524
+ };
525
+ }
526
+ else if (firstDocOrContainer.isContainer) {
527
+ // First item is a container (correlation folder)
528
+ // NOTE: Le cartelle di correlazione rimangono con expanded: false.
529
+ // Verranno espanse solo tramite il pulsante "espandi n livelli" o espansione manuale.
530
+ let newFirstCorrelation = {
531
+ ...firstDocOrContainer,
532
+ expanded: false
533
+ };
534
+ // Find first document inside this container and mark for focus
535
+ const correlationItems = newFirstCorrelation.items;
536
+ if (correlationItems && correlationItems.length > 0 && correlationItems[0].isDcmt) {
537
+ const newFirstDoc = { ...correlationItems[0], isRoot: true };
538
+ newFirstCorrelation = {
539
+ ...newFirstCorrelation,
540
+ items: [newFirstDoc, ...correlationItems.slice(1)]
541
+ };
542
+ }
543
+ // Update the container's items
544
+ newContainer = {
545
+ ...newContainer,
546
+ items: [newFirstCorrelation, ...containerItems.slice(1)]
547
+ };
548
+ }
478
549
  }
479
- // Update the container's items
480
- newFirstContainer = {
481
- ...newFirstContainer,
482
- items: [newFirstCorrelation, ...firstRootItems.slice(1)]
483
- };
484
550
  }
485
- }
486
- // If firstRootItems is empty/undefined, the container is still expanded (will show infoMessage)
487
- // Return new tree with the modified first container
488
- return [newFirstContainer, ...tree.slice(1)];
551
+ return newContainer;
552
+ });
553
+ return result;
489
554
  };
490
555
  /**
491
556
  * Main data loading function
@@ -497,6 +562,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
497
562
  }
498
563
  const tree = [];
499
564
  let processedCount = 0;
565
+ let errorCount = 0;
500
566
  for (const dcmt of inputDcmts) {
501
567
  // Check for abort
502
568
  if (abortController.signal.aborted) {
@@ -509,7 +575,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
509
575
  if (!searchEngine)
510
576
  continue;
511
577
  // Load document metadata
512
- const result = await searchEngine.GetMetadataAsync(dcmt.TID, dcmt.DID, false);
578
+ let result;
579
+ try {
580
+ result = await searchEngine.GetMetadataAsync(dcmt.TID, dcmt.DID, false);
581
+ }
582
+ catch (metadataError) {
583
+ console.warn('[TMRelationViewer] GetMetadataAsync failed for TID:', dcmt.TID, 'DID:', dcmt.DID, metadataError);
584
+ errorCount++;
585
+ ShowAlert({
586
+ title: SDKUI_Localizator.Warning,
587
+ message: `Il documento (TID: ${dcmt.TID}, DID: ${dcmt.DID}) non esiste più o non è accessibile.`,
588
+ mode: 'warning',
589
+ duration: 5000
590
+ });
591
+ // Skip this document and continue with the next one
592
+ processedCount++;
593
+ setWaitPanelValuePrimary(processedCount);
594
+ continue;
595
+ }
513
596
  const dtd = await DcmtTypeListCacheService.GetAsync(dcmt.TID);
514
597
  // Parse document values
515
598
  const docValues = await searchResultToDataSource(result);
@@ -636,7 +719,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
636
719
  searchResult: result ? [result] : [],
637
720
  items: relatedDocs,
638
721
  itemsCount: relatedDocs.length,
639
- fileExt: docRow?.FILEEXT?.value
722
+ fileExt: docRow?.FILEEXT?.value,
723
+ fileCount: docRow?.FILECOUNT?.value
640
724
  };
641
725
  // Check if a type container for this TID already exists in the tree
642
726
  const existingContainer = tree.find(c => c.tid === dcmt.TID && c.isContainer);
@@ -714,6 +798,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
714
798
  if (hasNoRelations && onNoRelationsFound) {
715
799
  onNoRelationsFound();
716
800
  }
801
+ // Update metadata error count state
802
+ setMetadataErrorCount(errorCount);
803
+ // Count total master documents (only relevant when isForMaster is true)
804
+ if (isForMaster) {
805
+ const countMasterDocuments = (items) => {
806
+ let count = 0;
807
+ for (const item of items) {
808
+ if (item.isDcmt && !item.isZero) {
809
+ count++;
810
+ }
811
+ if (item.items && Array.isArray(item.items)) {
812
+ count += countMasterDocuments(item.items);
813
+ }
814
+ }
815
+ return count;
816
+ };
817
+ setTotalMasterDocuments(countMasterDocuments(tree));
818
+ }
717
819
  // FIRST setup initial expansion state (root container expanded, first document expanded with focus, first correlation folder expanded)
718
820
  // This must run BEFORE updateHiddenProperty so that infoMessage logic sees expanded=true
719
821
  const expandedTree = setupInitialTreeExpansion(tree);
@@ -904,7 +1006,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
904
1006
  return [
905
1007
  {
906
1008
  key: `__info__${node.key}`,
907
- name: 'Nessun documento correlato da visualizzare',
1009
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
908
1010
  isContainer: false,
909
1011
  isDcmt: false,
910
1012
  isInfoMessage: true,
@@ -933,7 +1035,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
933
1035
  return [
934
1036
  {
935
1037
  key: `__info__${node.key}`,
936
- name: 'Nessun documento correlato da visualizzare',
1038
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
937
1039
  isContainer: false,
938
1040
  isDcmt: false,
939
1041
  isInfoMessage: true,
@@ -988,7 +1090,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
988
1090
  return [
989
1091
  {
990
1092
  key: `__info__${node.key}`,
991
- name: 'Nessun documento correlato da visualizzare',
1093
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
992
1094
  isContainer: false,
993
1095
  isDcmt: false,
994
1096
  isInfoMessage: true,
@@ -1078,42 +1180,40 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1078
1180
  transition: 'opacity 0.2s ease-in-out',
1079
1181
  textDecoration: isLogicallyDeleted ? 'line-through' : 'none'
1080
1182
  };
1081
- const documentStyle = customDocumentStyle
1082
- ? { ...defaultDocumentStyle, ...customDocumentStyle(item) }
1083
- : defaultDocumentStyle;
1183
+ const documentStyle = customDocumentStyle ? { ...defaultDocumentStyle, ...customDocumentStyle(item) } : defaultDocumentStyle;
1084
1184
  const textDecoration = isLogicallyDeleted ? 'line-through' : 'none';
1085
1185
  const textColor = isLogicallyDeleted ? 'gray' : undefined;
1086
- const defaultMetadataContent = item.values && (_jsx(StyledDivHorizontal, { style: {
1087
- fontSize: '1rem',
1088
- overflow: 'hidden',
1089
- flex: 1,
1090
- minWidth: 0,
1091
- whiteSpace: 'nowrap',
1092
- textDecoration: textDecoration,
1093
- color: textColor
1094
- }, children: getDcmtDisplayValue(item.values).map((key, index) => {
1095
- const md = item.values?.[key]?.md;
1096
- const value = item.values?.[key]?.value;
1097
- const displayValues = getDcmtDisplayValue(item.values);
1098
- const isLast = index === displayValues.length - 1;
1099
- return (_jsxs(StyledDivHorizontal, { style: {
1100
- flexShrink: isLast ? 1 : 0,
1101
- minWidth: isLast ? 0 : 'auto',
1102
- overflow: isLast ? 'hidden' : 'visible',
1103
- textDecoration: textDecoration,
1104
- color: textColor
1105
- }, 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: {
1106
- fontWeight: 500,
1107
- overflow: isLast ? 'hidden' : 'visible',
1108
- textOverflow: isLast ? 'ellipsis' : 'clip',
1109
- whiteSpace: 'nowrap',
1110
- textDecoration: textDecoration,
1111
- color: textColor
1112
- }, children: value }))] }, `${key}_${index}`));
1113
- }) }));
1114
- const metadataContent = customDocumentContent
1115
- ? customDocumentContent(item, defaultMetadataContent || _jsx(_Fragment, {}))
1116
- : defaultMetadataContent;
1186
+ const defaultMetadataContent = item.values && (() => {
1187
+ const displayKeys = buildDcmtDisplayName(item.values);
1188
+ return (_jsx(StyledDivHorizontal, { style: {
1189
+ fontSize: '1rem',
1190
+ overflow: 'hidden',
1191
+ flex: 1,
1192
+ minWidth: 0,
1193
+ whiteSpace: 'nowrap',
1194
+ textDecoration: textDecoration,
1195
+ color: textColor
1196
+ }, children: displayKeys.map((key, index) => {
1197
+ const md = item.values?.[key]?.md;
1198
+ const value = item.values?.[key]?.value;
1199
+ const isLast = index === displayKeys.length - 1;
1200
+ return (_jsxs(StyledDivHorizontal, { style: {
1201
+ flexShrink: isLast ? 1 : 0,
1202
+ minWidth: isLast ? 0 : 'auto',
1203
+ overflow: isLast ? 'hidden' : 'visible',
1204
+ textDecoration: textDecoration,
1205
+ color: textColor
1206
+ }, 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: {
1207
+ fontWeight: 500,
1208
+ overflow: isLast ? 'hidden' : 'visible',
1209
+ textOverflow: isLast ? 'ellipsis' : 'clip',
1210
+ whiteSpace: 'nowrap',
1211
+ textDecoration: textDecoration,
1212
+ color: textColor
1213
+ }, children: value }))] }, `${key}_${index}`));
1214
+ }) }));
1215
+ })();
1216
+ const metadataContent = customDocumentContent ? customDocumentContent(item, defaultMetadataContent || _jsx(_Fragment, {})) : defaultMetadataContent;
1117
1217
  // Calculate checkout status for non-root documents
1118
1218
  let checkoutStatusIcon = null;
1119
1219
  if (item.values && item.dtd) {
@@ -1137,7 +1237,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1137
1237
  checkoutStatusIcon = checkoutStatus.icon;
1138
1238
  }
1139
1239
  }
1140
- 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] }));
1240
+ 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] }));
1141
1241
  }, [onDocumentDoubleClick, showCurrentDcmtIndicator, inputDcmts, customMainContainerContent, customDocumentStyle, customDocumentContent, showMetadataNames, showMainDocument, allUsers]);
1142
1242
  /**
1143
1243
  * Wrapper renderer that handles custom rendering if provided
@@ -1171,6 +1271,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1171
1271
  /**
1172
1272
  * Recursively set the `expanded` property on every node that has children loaded.
1173
1273
  * Nodes without `items` are left untouched (will be loaded lazily on user click).
1274
+ * NOTE: Solo i nodi con figli già caricati (isLoaded o hasChildren) vengono espansi.
1275
+ * I nodi con isExpandible ma senza figli caricati rimangono chiusi.
1174
1276
  */
1175
1277
  const setExpandedRecursively = useCallback((nodes, expanded) => {
1176
1278
  if (!nodes || !Array.isArray(nodes))
@@ -1178,18 +1280,237 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1178
1280
  return nodes.map(node => {
1179
1281
  const hasChildren = Array.isArray(node.items) && node.items.length > 0;
1180
1282
  const newItems = hasChildren ? setExpandedRecursively(node.items, expanded) : node.items;
1181
- // Only toggle expansion if the node is expandable (container/doc with children or marked expandible)
1182
- const canExpand = hasChildren || node.isExpandible;
1283
+ // Only toggle expansion if the node has children already loaded.
1284
+ // Nodes with isExpandible but no loaded children stay collapsed (will be loaded lazily).
1285
+ const canExpand = hasChildren || node.isLoaded;
1183
1286
  return { ...node, expanded: canExpand ? expanded : node.expanded, items: newItems };
1184
1287
  });
1185
1288
  }, []);
1186
- const handleToggleExpandAll = useCallback(() => {
1187
- const next = !allExpanded;
1188
- setAllExpanded(next);
1189
- setTreeData(prev => setExpandedRecursively(prev, next));
1190
- setStaticItemsState(prev => setExpandedRecursively(prev, next));
1191
- userInteractedWithStaticItemsRef.current = true;
1192
- }, [allExpanded, setExpandedRecursively]);
1289
+ // Collassa l'albero impostando tutti i nodi come non espansi
1290
+ const handleCollapseTree = useCallback(() => {
1291
+ // Funzione helper per collassare ricorsivamente tutti i nodi
1292
+ const collapseAllNodes = (nodes) => {
1293
+ return nodes.map(node => ({
1294
+ ...node,
1295
+ expanded: false,
1296
+ items: node.items ? collapseAllNodes(node.items) : node.items
1297
+ }));
1298
+ };
1299
+ // Collassa tutti i nodi dell'albero
1300
+ setTreeData(prevData => collapseAllNodes(prevData));
1301
+ setStaticItemsState(prevData => collapseAllNodes(prevData));
1302
+ setIsAllCollapsed(true);
1303
+ }, []);
1304
+ /**
1305
+ * Espande ricorsivamente l'albero fino a `expandLevel`.
1306
+ * Carica i figli lazy (API) solo se non già in memoria (isLoaded).
1307
+ * Il contatore totalNodes cresce dinamicamente per una % progresso accurata.
1308
+ *
1309
+ * @param forceReload - Se true (Ctrl+click), ricarica tutto ignorando la cache
1310
+ */
1311
+ const handleExpandToLevel = useCallback(async (forceReload = false) => {
1312
+ // Mostra spinner per feedback immediato all'utente
1313
+ TMSpinner.show({ description: forceReload ? SDKUI_Localizator.FullReloadInProgress : SDKUI_Localizator.ExpansionInProgress });
1314
+ // Piccolo delay per permettere allo spinner di renderizzarsi prima del processing pesante
1315
+ await new Promise(resolve => setTimeout(resolve, 50));
1316
+ /**
1317
+ * Se forceReload è true, prima resettiamo tutti i nodi come se non fossero mai stati caricati.
1318
+ * Questo significa rimuovere isLoaded e items da tutti i nodi documento,
1319
+ * così l'espansione successiva li ricaricherà tutti da API.
1320
+ */
1321
+ const resetLoadedState = (nodes) => {
1322
+ return nodes.map(node => {
1323
+ const resetNode = { ...node };
1324
+ // Reset isLoaded per forzare il ricaricamento
1325
+ if (resetNode.isDcmt || resetNode.isExpandible) {
1326
+ resetNode.isLoaded = false;
1327
+ // Manteniamo items solo per i container (cartelle),
1328
+ // perché i container non vengono ricaricati via API
1329
+ // I documenti invece devono ricaricare i loro figli (correlazioni)
1330
+ if (resetNode.isDcmt) {
1331
+ resetNode.items = undefined;
1332
+ resetNode.itemsCount = undefined;
1333
+ }
1334
+ }
1335
+ // Reset ricorsivo sui figli
1336
+ if (resetNode.items && Array.isArray(resetNode.items)) {
1337
+ resetNode.items = resetLoadedState(resetNode.items);
1338
+ }
1339
+ return resetNode;
1340
+ });
1341
+ };
1342
+ // Se forceReload, reset dello stato prima di iniziare
1343
+ let workingTreeData = treeData;
1344
+ let workingStaticData = staticItemsState;
1345
+ if (forceReload) {
1346
+ workingTreeData = resetLoadedState(treeData);
1347
+ workingStaticData = resetLoadedState(staticItemsState);
1348
+ // Aggiorna subito lo stato per riflettere il reset
1349
+ setTreeData(workingTreeData);
1350
+ setStaticItemsState(workingStaticData);
1351
+ }
1352
+ const targetLevel = expandLevel;
1353
+ const newAbortController = new AbortController();
1354
+ setExpansionAbortController(newAbortController);
1355
+ // CONTATORI DINAMICI:
1356
+ // - totalNodes: cresce quando scopriamo nuovi nodi (evita che percentuale > 100%)
1357
+ // - processedNodes: quanti nodi abbiamo già processato
1358
+ // - maxReachedProgress: percentuale massima raggiunta (evita che la barra torni indietro)
1359
+ let totalNodes = 0;
1360
+ let processedNodes = 0;
1361
+ let hasApiCalls = false;
1362
+ let maxReachedProgress = 0;
1363
+ /**
1364
+ * Conta i nodi espandibili in un array (solo primo livello, non ricorsivo).
1365
+ * Usato per aggiornare totalNodes man mano che scopriamo nuovi figli.
1366
+ */
1367
+ const countNodesAtLevel = (nodes) => {
1368
+ let count = 0;
1369
+ for (const node of nodes) {
1370
+ if (node.isContainer || node.isExpandible || node.isDcmt) {
1371
+ count++;
1372
+ }
1373
+ }
1374
+ return count;
1375
+ };
1376
+ // Conta iniziale: solo i nodi al livello root
1377
+ totalNodes = countNodesAtLevel([...workingTreeData, ...workingStaticData]);
1378
+ /**
1379
+ * FUNZIONE RICORSIVA DI ESPANSIONE
1380
+ *
1381
+ * @param nodes - Array di nodi da processare
1382
+ * @param currentLevel - Livello corrente (0 = root)
1383
+ * @returns Array di nodi con expanded=true e figli caricati
1384
+ *
1385
+ * FLUSSO:
1386
+ * 1. Se targetLevel > 0 e currentLevel >= targetLevel → STOP, ritorna i nodi così come sono
1387
+ * (Se targetLevel = 0 → espandi senza limiti, non applicare mai il limite)
1388
+ * 2. Per ogni nodo espandibile:
1389
+ * a) Imposta expanded = true
1390
+ * b) Se i figli non sono caricati → chiama API per caricarli
1391
+ * c) Aggiorna totalNodes con i nuovi figli scoperti
1392
+ * d) CHIAMATA RICORSIVA su expandedNode.items con currentLevel + 1
1393
+ * → QUESTO È IL PUNTO CHIAVE: i nuovi nodi vengono espansi!
1394
+ */
1395
+ const expandToLevel = async (nodes, currentLevel) => {
1396
+ // CONDIZIONE DI USCITA: abbiamo raggiunto il livello target
1397
+ // targetLevel = 0 significa espandi senza limiti (non applicare mai il limite)
1398
+ // targetLevel > 0 significa applica il limite normalmente
1399
+ if (targetLevel > 0 && currentLevel >= targetLevel)
1400
+ return nodes;
1401
+ // Controlla se l'utente ha annullato l'operazione
1402
+ if (newAbortController.signal.aborted)
1403
+ return nodes;
1404
+ const result = [];
1405
+ for (const node of nodes) {
1406
+ // Controlla abort per ogni nodo (permette cancellazione veloce)
1407
+ if (newAbortController.signal.aborted) {
1408
+ result.push(node);
1409
+ continue;
1410
+ }
1411
+ // Crea copia del nodo (immutabilità React)
1412
+ let expandedNode = { ...node };
1413
+ // Verifica se questo nodo può essere espanso
1414
+ const canExpand = node.isContainer || node.isExpandible || node.isDcmt;
1415
+ if (canExpand) {
1416
+ // PASSO 1: Imposta il nodo come espanso
1417
+ expandedNode.expanded = true;
1418
+ // PASSO 2: Verifica se i figli sono già caricati o vanno caricati via API
1419
+ // Se forceReload è true, ricarica sempre (ignora isLoaded e items esistenti)
1420
+ const hasLoadedItems = !forceReload && node.items && Array.isArray(node.items) && node.items.length > 0;
1421
+ const actualHasItems = node.items && Array.isArray(node.items) && node.items.length > 0;
1422
+ const needsLoading = (forceReload || (!hasLoadedItems && !node.isLoaded)) && (node.isExpandible || node.isDcmt);
1423
+ if (needsLoading && node.tid && node.did) {
1424
+ // ═══════════════════════════════════════════════════════════════
1425
+ // CASO A: I figli NON sono in memoria → CHIAMATA API
1426
+ // ═══════════════════════════════════════════════════════════════
1427
+ // Mostra pannello di attesa solo quando ci sono chiamate API
1428
+ if (!hasApiCalls) {
1429
+ hasApiCalls = true;
1430
+ TMSpinner.hide();
1431
+ setShowExpansionWaitPanel(true);
1432
+ setExpansionWaitPanelMaxValue(100);
1433
+ setExpansionWaitPanelValue(0);
1434
+ }
1435
+ try {
1436
+ let loadedItems = [];
1437
+ // Carica i documenti correlati (detail o master in base alla modalità)
1438
+ if (isForMaster && !invertMasterNavigation) {
1439
+ loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1);
1440
+ }
1441
+ else if (isForMaster) {
1442
+ loadedItems = await getMasterDcmtsAsync(node.tid, node.did, 1);
1443
+ }
1444
+ else {
1445
+ loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1);
1446
+ }
1447
+ // Applica le regole di visibilità (showZeroDcmts)
1448
+ expandedNode.items = updateHiddenProperty(loadedItems);
1449
+ expandedNode.isLoaded = true;
1450
+ expandedNode.itemsCount = loadedItems.length;
1451
+ // IMPORTANTE: Aggiungi i nuovi nodi scoperti al contatore totale
1452
+ // Questo viene fatto SOLO se non siamo all'ultimo livello
1453
+ // (perché i nodi dell'ultimo livello non verranno espansi)
1454
+ // targetLevel = 0 significa illimitato, quindi aggiungi sempre
1455
+ if (targetLevel === 0 || currentLevel + 1 < targetLevel) {
1456
+ const newExpandableCount = countNodesAtLevel(loadedItems);
1457
+ totalNodes += newExpandableCount;
1458
+ }
1459
+ }
1460
+ catch (error) {
1461
+ console.error('Errore nel caricamento dei figli del nodo:', error);
1462
+ }
1463
+ }
1464
+ else if (actualHasItems && (targetLevel === 0 || currentLevel + 1 < targetLevel)) {
1465
+ // ═══════════════════════════════════════════════════════════════
1466
+ // CASO B: I figli esistono già in memoria (non ricaricati via API)
1467
+ // Aggiungiamo comunque il loro conteggio a totalNodes
1468
+ // Usiamo actualHasItems invece di hasLoadedItems per gestire anche forceReload
1469
+ // ═══════════════════════════════════════════════════════════════
1470
+ const childCount = countNodesAtLevel(node.items);
1471
+ totalNodes += childCount;
1472
+ }
1473
+ // Aggiorna il progresso DOPO aver processato questo nodo
1474
+ processedNodes++;
1475
+ // Calcola percentuale e aggiorna solo se è più alta di prima
1476
+ // (evita che la barra di progresso torni indietro)
1477
+ const currentProgress = totalNodes > 0 ? Math.floor((processedNodes / totalNodes) * 100) : 0;
1478
+ if (currentProgress > maxReachedProgress) {
1479
+ maxReachedProgress = currentProgress;
1480
+ setExpansionWaitPanelValue(maxReachedProgress);
1481
+ }
1482
+ setExpansionWaitPanelText(SDKUI_Localizator.ExpansionProgress.replaceParams(processedNodes.toString(), totalNodes.toString()));
1483
+ // ═══════════════════════════════════════════════════════════════════
1484
+ // PASSO 3: CHIAMATA RICORSIVA SUI FIGLI
1485
+ // Questo è il punto chiave: i nuovi nodi appena caricati vengono
1486
+ // passati a expandToLevel con currentLevel + 1, quindi anche loro
1487
+ // verranno espansi (se non abbiamo raggiunto il livello target)
1488
+ // ═══════════════════════════════════════════════════════════════════
1489
+ if (expandedNode.items && Array.isArray(expandedNode.items) && expandedNode.items.length > 0) {
1490
+ expandedNode.items = await expandToLevel(expandedNode.items, currentLevel + 1);
1491
+ }
1492
+ }
1493
+ result.push(expandedNode);
1494
+ }
1495
+ return result;
1496
+ };
1497
+ try {
1498
+ // Avvia l'espansione dal livello 0 (root)
1499
+ const expandedTree = await expandToLevel(workingTreeData, 0);
1500
+ setTreeData(expandedTree);
1501
+ // Espandi anche gli item statici (se presenti)
1502
+ const expandedStatic = await expandToLevel(workingStaticData, 0);
1503
+ setStaticItemsState(expandedStatic);
1504
+ setIsAllCollapsed(false);
1505
+ userInteractedWithStaticItemsRef.current = true;
1506
+ }
1507
+ finally {
1508
+ // Cleanup: nascondi spinner e pannello di attesa
1509
+ TMSpinner.hide();
1510
+ setShowExpansionWaitPanel(false);
1511
+ setExpansionAbortController(undefined);
1512
+ }
1513
+ }, [expandLevel, treeData, staticItemsState, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty]);
1193
1514
  /**
1194
1515
  * Apply defaultExpandAll once on initial tree load.
1195
1516
  */
@@ -1215,37 +1536,70 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1215
1536
  setStaticItemsState(staticItems); // Preserve static items state (expanded/collapsed)
1216
1537
  // Mark that user has interacted with the tree (expanded/collapsed nodes)
1217
1538
  userInteractedWithStaticItemsRef.current = true;
1539
+ // Reset collapsed state when user manually expands a node
1540
+ setIsAllCollapsed(false);
1218
1541
  }, []);
1219
1542
  if (showWaitPanel) {
1220
- 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); } }) });
1543
+ 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); } }) });
1221
1544
  }
1222
1545
  if (mergedTreeData.length === 0) {
1546
+ // Se NON isForMaster e tutti i GetMetadataAsync sono falliti
1547
+ if (!isForMaster && metadataErrorCount === inputDcmts.length) {
1548
+ return _jsx(TMToppyMessage, { message: SDKUI_Localizator.DocumentsNotAvailableOrNoCorrelations });
1549
+ }
1223
1550
  // If parent handles no-relations via callback, render nothing to avoid UI flash
1224
1551
  if (onNoRelationsFound)
1225
1552
  return null;
1226
- return _jsx("div", { style: { padding: '20px', textAlign: 'center', color: '#666' }, children: "Nessuna relazione disponibile." });
1553
+ return _jsx("div", { style: { padding: '20px', textAlign: 'center', color: '#666' }, children: SDKUI_Localizator.NoRelationsAvailable });
1554
+ }
1555
+ // Se isForMaster e non ci sono master documents disponibili
1556
+ if (isForMaster && totalMasterDocuments === 0) {
1557
+ return _jsx(TMToppyMessage, { message: SDKUI_Localizator.NoMasterDocumentsAvailable });
1227
1558
  }
1228
- 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: {
1229
- display: 'inline-flex',
1230
- alignItems: 'center',
1231
- gap: '4px',
1232
- padding: '4px 8px',
1233
- fontSize: '0.8rem',
1234
- color: TMColors.primary,
1235
- background: 'transparent',
1236
- border: `1px solid ${TMColors.border_normal}`,
1237
- borderRadius: '4px',
1238
- cursor: 'pointer',
1239
- whiteSpace: 'nowrap',
1240
- outline: 'none'
1241
- }, 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) => {
1559
+ 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
1560
+ ? `${SDKUI_Localizator.ExpandAllLevels} (${SDKUI_Localizator.CtrlClickToReloadAll})`
1561
+ : `${SDKUI_Localizator.ExpandToLevel.replaceParams(expandLevel.toString())} (${SDKUI_Localizator.CtrlClickToReloadAll})`, children: _jsxs("button", { type: "button", onClick: (e) => handleExpandToLevel(e.ctrlKey), style: {
1562
+ display: 'inline-flex',
1563
+ alignItems: 'center',
1564
+ justifyContent: 'center',
1565
+ gap: '5px',
1566
+ height: '28px',
1567
+ padding: '0 10px',
1568
+ fontSize: '0.8rem',
1569
+ fontWeight: 500,
1570
+ color: 'white',
1571
+ background: TMColors.primary,
1572
+ border: 'none',
1573
+ borderRadius: '4px',
1574
+ cursor: 'pointer',
1575
+ whiteSpace: 'nowrap',
1576
+ outline: 'none',
1577
+ transition: 'background 0.2s'
1578
+ }, 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: {
1579
+ display: 'inline-flex',
1580
+ alignItems: 'center',
1581
+ justifyContent: 'center',
1582
+ gap: '5px',
1583
+ height: '28px',
1584
+ padding: '0 10px',
1585
+ fontSize: '0.8rem',
1586
+ fontWeight: 500,
1587
+ color: isAllCollapsed ? '#999' : 'white',
1588
+ background: isAllCollapsed ? '#e0e0e0' : TMColors.secondary || '#6c757d',
1589
+ border: 'none',
1590
+ borderRadius: '4px',
1591
+ cursor: isAllCollapsed ? 'not-allowed' : 'pointer',
1592
+ whiteSpace: 'nowrap',
1593
+ outline: 'none',
1594
+ transition: 'background 0.2s'
1595
+ }, 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) => {
1242
1596
  // Ritarda il focus quando si clicca sull'icona del documento
1243
1597
  // per permettere al doppio click di funzionare
1244
1598
  const target = e.target;
1245
1599
  return !!target.closest('.tm-dcmt-icon');
1246
- } }) }), 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) => {
1600
+ } }) }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? SDKUI_Localizator.LoadingMasterDocuments : SDKUI_Localizator.LoadingDetailDocuments, showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: () => {
1247
1601
  setTimeout(() => {
1248
- abortController?.abort();
1602
+ expansionAbortController?.abort();
1249
1603
  }, 100);
1250
1604
  } }))] }));
1251
1605
  };