@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.
- package/lib/components/base/TMTreeView.d.ts +16 -13
- package/lib/components/base/TMTreeView.js +230 -64
- package/lib/components/features/documents/TMDcmtIcon.d.ts +3 -1
- package/lib/components/features/documents/TMDcmtIcon.js +5 -32
- package/lib/components/features/documents/TMFileUploader.js +1 -1
- package/lib/components/features/documents/TMMasterDetailDcmts.js +165 -11
- package/lib/components/features/documents/TMMergeToPdfForm.js +6 -3
- package/lib/components/features/documents/TMRelationViewer.d.ts +15 -10
- package/lib/components/features/documents/TMRelationViewer.js +532 -179
- package/lib/components/features/documents/copyAndMergeDcmtsShared.d.ts +4 -3
- package/lib/components/features/documents/copyAndMergeDcmtsShared.js +46 -23
- package/lib/components/features/documents/mergePdfUtils.js +21 -11
- package/lib/components/features/search/TMSearchResult.js +20 -5
- package/lib/components/viewers/TMTidViewer.js +14 -2
- package/lib/helper/SDKUI_Globals.d.ts +1 -0
- package/lib/helper/SDKUI_Globals.js +1 -0
- package/lib/helper/SDKUI_Localizator.d.ts +28 -0
- package/lib/helper/SDKUI_Localizator.js +280 -0
- package/lib/helper/TMIcons.d.ts +1 -0
- package/lib/helper/TMIcons.js +3 -0
- package/lib/helper/TMUtils.d.ts +33 -1
- package/lib/helper/TMUtils.js +104 -1
- package/lib/helper/helpers.d.ts +3 -0
- package/lib/helper/helpers.js +29 -1
- package/lib/hooks/useDocumentOperations.js +2 -2
- 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,
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 && (
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
|
1183
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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:
|
|
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:
|
|
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 && (
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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 ?
|
|
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
|
-
|
|
1602
|
+
expansionAbortController?.abort();
|
|
1250
1603
|
}, 100);
|
|
1251
1604
|
} }))] }));
|
|
1252
1605
|
};
|