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

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, 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)
@@ -342,6 +394,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
342
394
  values: row,
343
395
  searchResult: [searchResult],
344
396
  fileExt: row?.FILEEXT?.value,
397
+ fileCount: row?.FILECOUNT?.value,
345
398
  // Leave items and itemsCount undefined so TMTreeView shows expand arrow based on isExpandible
346
399
  // Children will be loaded lazily by calculateItemsForNode when expanded
347
400
  expanded: false,
@@ -387,7 +440,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
387
440
  updatedItems = [
388
441
  {
389
442
  key: `__info__${node.key}`,
390
- name: 'Nessun documento correlato da visualizzare',
443
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
391
444
  isContainer: false,
392
445
  isDcmt: false,
393
446
  isInfoMessage: true,
@@ -422,71 +475,82 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
422
475
  const setupInitialTreeExpansion = (tree) => {
423
476
  if (tree.length === 0)
424
477
  return tree;
425
- // ALWAYS expand the first container (even if empty, to show infoMessage)
426
- const firstRootContainer = tree[0];
427
- if (!firstRootContainer)
428
- return tree;
429
- // Create a deep copy of the first container with expanded=true, isRoot=true
430
- let newFirstContainer = {
431
- ...firstRootContainer,
432
- expanded: true,
433
- isRoot: true
434
- };
435
- // 2. Find first document/container and expand it
436
- const firstRootItems = newFirstContainer.items;
437
- if (firstRootItems && firstRootItems.length > 0) {
438
- const firstDocOrContainer = firstRootItems[0];
439
- if (firstDocOrContainer.isDcmt) {
440
- // First item is a document - expand it and mark as root for focus
441
- let newFirstDoc = {
442
- ...firstDocOrContainer,
443
- expanded: true,
444
- isRoot: true
445
- };
446
- // 3. Expand first correlation folder ONLY if there's exactly one
447
- const docItems = newFirstDoc.items;
448
- const containerChildren = docItems?.filter(child => child.isContainer) ?? [];
449
- if (containerChildren.length === 1 && docItems && docItems.length > 0 && docItems[0].isContainer) {
450
- const newFirstCorrelation = { ...docItems[0], expanded: true };
451
- newFirstDoc = {
452
- ...newFirstDoc,
453
- items: [newFirstCorrelation, ...docItems.slice(1)]
454
- };
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;
455
485
  }
456
- // Update the container's items
457
- newFirstContainer = {
458
- ...newFirstContainer,
459
- items: [newFirstDoc, ...firstRootItems.slice(1)]
460
- };
461
486
  }
462
- else if (firstDocOrContainer.isContainer) {
463
- // First item is a container (correlation folder)
464
- // Count how many sibling containers there are
465
- const siblingContainers = firstRootItems.filter(child => child.isContainer);
466
- const shouldExpand = siblingContainers.length === 1;
467
- let newFirstCorrelation = {
468
- ...firstDocOrContainer,
469
- expanded: shouldExpand
470
- };
471
- // Find first document inside this container and mark for focus
472
- const containerItems = newFirstCorrelation.items;
473
- if (containerItems && containerItems.length > 0 && containerItems[0].isDcmt) {
474
- const newFirstDoc = { ...containerItems[0], isRoot: true };
475
- newFirstCorrelation = {
476
- ...newFirstCorrelation,
477
- items: [newFirstDoc, ...containerItems.slice(1)]
478
- };
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
+ }
479
549
  }
480
- // Update the container's items
481
- newFirstContainer = {
482
- ...newFirstContainer,
483
- items: [newFirstCorrelation, ...firstRootItems.slice(1)]
484
- };
485
550
  }
486
- }
487
- // If firstRootItems is empty/undefined, the container is still expanded (will show infoMessage)
488
- // Return new tree with the modified first container
489
- return [newFirstContainer, ...tree.slice(1)];
551
+ return newContainer;
552
+ });
553
+ return result;
490
554
  };
491
555
  /**
492
556
  * Main data loading function
@@ -498,6 +562,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
498
562
  }
499
563
  const tree = [];
500
564
  let processedCount = 0;
565
+ let errorCount = 0;
501
566
  for (const dcmt of inputDcmts) {
502
567
  // Check for abort
503
568
  if (abortController.signal.aborted) {
@@ -510,7 +575,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
510
575
  if (!searchEngine)
511
576
  continue;
512
577
  // Load document metadata
513
- 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
+ }
514
596
  const dtd = await DcmtTypeListCacheService.GetAsync(dcmt.TID);
515
597
  // Parse document values
516
598
  const docValues = await searchResultToDataSource(result);
@@ -637,7 +719,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
637
719
  searchResult: result ? [result] : [],
638
720
  items: relatedDocs,
639
721
  itemsCount: relatedDocs.length,
640
- fileExt: docRow?.FILEEXT?.value
722
+ fileExt: docRow?.FILEEXT?.value,
723
+ fileCount: docRow?.FILECOUNT?.value
641
724
  };
642
725
  // Check if a type container for this TID already exists in the tree
643
726
  const existingContainer = tree.find(c => c.tid === dcmt.TID && c.isContainer);
@@ -715,6 +798,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
715
798
  if (hasNoRelations && onNoRelationsFound) {
716
799
  onNoRelationsFound();
717
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
+ }
718
819
  // FIRST setup initial expansion state (root container expanded, first document expanded with focus, first correlation folder expanded)
719
820
  // This must run BEFORE updateHiddenProperty so that infoMessage logic sees expanded=true
720
821
  const expandedTree = setupInitialTreeExpansion(tree);
@@ -905,7 +1006,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
905
1006
  return [
906
1007
  {
907
1008
  key: `__info__${node.key}`,
908
- name: 'Nessun documento correlato da visualizzare',
1009
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
909
1010
  isContainer: false,
910
1011
  isDcmt: false,
911
1012
  isInfoMessage: true,
@@ -934,7 +1035,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
934
1035
  return [
935
1036
  {
936
1037
  key: `__info__${node.key}`,
937
- name: 'Nessun documento correlato da visualizzare',
1038
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
938
1039
  isContainer: false,
939
1040
  isDcmt: false,
940
1041
  isInfoMessage: true,
@@ -989,7 +1090,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
989
1090
  return [
990
1091
  {
991
1092
  key: `__info__${node.key}`,
992
- name: 'Nessun documento correlato da visualizzare',
1093
+ name: SDKUI_Localizator.NoRelatedDocumentsToDisplay,
993
1094
  isContainer: false,
994
1095
  isDcmt: false,
995
1096
  isInfoMessage: true,
@@ -1079,42 +1180,40 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1079
1180
  transition: 'opacity 0.2s ease-in-out',
1080
1181
  textDecoration: isLogicallyDeleted ? 'line-through' : 'none'
1081
1182
  };
1082
- const documentStyle = customDocumentStyle
1083
- ? { ...defaultDocumentStyle, ...customDocumentStyle(item) }
1084
- : defaultDocumentStyle;
1183
+ const documentStyle = customDocumentStyle ? { ...defaultDocumentStyle, ...customDocumentStyle(item) } : defaultDocumentStyle;
1085
1184
  const textDecoration = isLogicallyDeleted ? 'line-through' : 'none';
1086
1185
  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
- }) }));
1115
- const metadataContent = customDocumentContent
1116
- ? customDocumentContent(item, defaultMetadataContent || _jsx(_Fragment, {}))
1117
- : 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;
1118
1217
  // Calculate checkout status for non-root documents
1119
1218
  let checkoutStatusIcon = null;
1120
1219
  if (item.values && item.dtd) {
@@ -1138,7 +1237,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1138
1237
  checkoutStatusIcon = checkoutStatus.icon;
1139
1238
  }
1140
1239
  }
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] }));
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] }));
1142
1241
  }, [onDocumentDoubleClick, showCurrentDcmtIndicator, inputDcmts, customMainContainerContent, customDocumentStyle, customDocumentContent, showMetadataNames, showMainDocument, allUsers]);
1143
1242
  /**
1144
1243
  * Wrapper renderer that handles custom rendering if provided
@@ -1172,6 +1271,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1172
1271
  /**
1173
1272
  * Recursively set the `expanded` property on every node that has children loaded.
1174
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.
1175
1276
  */
1176
1277
  const setExpandedRecursively = useCallback((nodes, expanded) => {
1177
1278
  if (!nodes || !Array.isArray(nodes))
@@ -1179,18 +1280,237 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1179
1280
  return nodes.map(node => {
1180
1281
  const hasChildren = Array.isArray(node.items) && node.items.length > 0;
1181
1282
  const newItems = hasChildren ? setExpandedRecursively(node.items, expanded) : node.items;
1182
- // Only toggle expansion if the node is expandable (container/doc with children or marked expandible)
1183
- 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;
1184
1286
  return { ...node, expanded: canExpand ? expanded : node.expanded, items: newItems };
1185
1287
  });
1186
1288
  }, []);
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]);
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]);
1194
1514
  /**
1195
1515
  * Apply defaultExpandAll once on initial tree load.
1196
1516
  */
@@ -1216,37 +1536,70 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1216
1536
  setStaticItemsState(staticItems); // Preserve static items state (expanded/collapsed)
1217
1537
  // Mark that user has interacted with the tree (expanded/collapsed nodes)
1218
1538
  userInteractedWithStaticItemsRef.current = true;
1539
+ // Reset collapsed state when user manually expands a node
1540
+ setIsAllCollapsed(false);
1219
1541
  }, []);
1220
1542
  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); } }) });
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); } }) });
1222
1544
  }
1223
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
+ }
1224
1550
  // If parent handles no-relations via callback, render nothing to avoid UI flash
1225
1551
  if (onNoRelationsFound)
1226
1552
  return null;
1227
- 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 });
1228
1558
  }
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) => {
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) => {
1243
1596
  // Ritarda il focus quando si clicca sull'icona del documento
1244
1597
  // per permettere al doppio click di funzionare
1245
1598
  const target = e.target;
1246
1599
  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) => {
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: () => {
1248
1601
  setTimeout(() => {
1249
- abortController?.abort();
1602
+ expansionAbortController?.abort();
1250
1603
  }, 100);
1251
1604
  } }))] }));
1252
1605
  };