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