@topconsultnpm/sdkui-react 6.20.0-dev2.47 → 6.20.0-dev2.49

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.
@@ -6,8 +6,9 @@ export interface ITMTreeItem {
6
6
  hidden?: boolean;
7
7
  isLoaded?: boolean;
8
8
  isContainer: boolean;
9
+ isExpandible?: boolean;
9
10
  items?: ITMTreeItem[];
10
- visibleItemsCount?: number;
11
+ currentPage?: number;
11
12
  totalItemsCount?: number;
12
13
  }
13
14
  interface ITMTreeViewProps<T extends ITMTreeItem> {
@@ -31,3 +32,4 @@ export default TMTreeView;
31
32
  export declare const StyledTreeNode: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
32
33
  $isSelected?: boolean;
33
34
  }>> & string;
35
+ export declare const StyledStickyPaginator: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
@@ -85,7 +85,9 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
85
85
  let newItems = node.items;
86
86
  // Solo se calculateItemsForNode è definito E (ctrl è premuto O non è già caricato)
87
87
  if (calculateItemsForNode && (ctrlKey || !node.isLoaded)) {
88
- newItems = await calculateItemsForNode(node);
88
+ // Se ctrl è premuto, passa un nodo con isLoaded resettato per forzare il refresh
89
+ const nodeForCalculation = ctrlKey ? { ...node, isLoaded: false } : node;
90
+ newItems = await calculateItemsForNode(nodeForCalculation);
89
91
  const updatedNode = { ...node, expanded: true, items: newItems, isLoaded: true };
90
92
  onNodeUpdate?.(updatedNode);
91
93
  return updatedNode;
@@ -274,53 +276,62 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
274
276
  return childKeys.every(key => selectedChildKeys.includes(key));
275
277
  };
276
278
  const hasVisibleItems = (node) => {
279
+ // If node has explicit isExpandible value, use that
280
+ if (node.isExpandible !== undefined) {
281
+ return node.isExpandible;
282
+ }
283
+ // Fallback: check if node has visible items
277
284
  if (!node.items)
278
285
  return false;
279
286
  if (node.items.length <= 0)
280
287
  return false;
281
288
  return node.items.filter(o => !o.hidden).length > 0;
282
289
  };
283
- // Gestisce il caricamento di più items per un nodo
284
- const handleLoadMoreItems = useCallback((nodeKey) => {
285
- const updateNodeVisibleCount = (nodes) => {
290
+ // Gestisce il cambio di pagina per un nodo
291
+ const handlePageChange = useCallback((nodeKey, newPage) => {
292
+ const updateNodePage = (nodes) => {
286
293
  return nodes.map(node => {
287
294
  if (node.key === nodeKey) {
288
- const currentVisible = node.visibleItemsCount ?? itemsPerPage;
289
- const newVisible = Math.min(currentVisible + itemsPerPage, node.totalItemsCount ?? node.items?.length ?? 0);
290
- const updatedNode = { ...node, visibleItemsCount: newVisible };
295
+ const updatedNode = { ...node, currentPage: newPage };
291
296
  onNodeUpdate?.(updatedNode);
292
297
  return updatedNode;
293
298
  }
294
299
  if (node.items) {
295
- return { ...node, items: updateNodeVisibleCount(node.items) };
300
+ return { ...node, items: updateNodePage(node.items) };
296
301
  }
297
302
  return node;
298
303
  });
299
304
  };
300
- const updatedData = updateNodeVisibleCount(dataSource);
305
+ const updatedData = updateNodePage(dataSource);
301
306
  onDataChanged?.(updatedData);
302
- }, [dataSource, itemsPerPage, onDataChanged, onNodeUpdate]);
307
+ }, [dataSource, onDataChanged, onNodeUpdate]);
303
308
  // Filtra gli items da mostrare in base alla paginazione
304
309
  const getVisibleItems = useCallback((node) => {
305
310
  if (!node.items)
306
311
  return [];
307
312
  const totalItems = node.items.length;
308
- const visibleCount = node.visibleItemsCount ?? itemsPerPage;
309
313
  // Se non c'è paginazione attiva o gli items sono pochi, mostra tutti
310
314
  if (totalItems <= itemsPerPage || !showLoadMoreButton) {
311
315
  return node.items;
312
316
  }
313
- // Altrimenti mostra solo i primi N items
314
- return node.items.slice(0, visibleCount);
317
+ // Altrimenti mostra solo gli items della pagina corrente
318
+ const currentPage = node.currentPage ?? 0;
319
+ const startIndex = currentPage * itemsPerPage;
320
+ const endIndex = startIndex + itemsPerPage;
321
+ return node.items.slice(startIndex, endIndex);
315
322
  }, [itemsPerPage, showLoadMoreButton]);
316
- // Verifica se c'è bisogno del bottone "Load More"
317
- const needsLoadMoreButton = useCallback((node) => {
323
+ // Verifica se c'è bisogno del paginatore
324
+ const needsPagination = useCallback((node) => {
318
325
  if (!showLoadMoreButton || !node.items)
319
326
  return false;
320
- const totalItems = node.items.length;
321
- const visibleCount = node.visibleItemsCount ?? itemsPerPage;
322
- return totalItems > itemsPerPage && visibleCount < totalItems;
327
+ return node.items.length > itemsPerPage;
323
328
  }, [itemsPerPage, showLoadMoreButton]);
329
+ // Calcola il numero totale di pagine per un nodo
330
+ const getTotalPages = useCallback((node) => {
331
+ if (!node.items)
332
+ return 0;
333
+ return Math.ceil(node.items.length / itemsPerPage);
334
+ }, [itemsPerPage]);
324
335
  const renderTree = useCallback((nodes) => {
325
336
  return nodes.map(node => !node.hidden && (_jsxs("div", { style: { width: '100%', margin: 0, padding: 0 }, children: [_jsxs(StyledTreeNode, { "$isSelected": node.key === focusedItem?.key, children: [_jsx("div", { style: {
326
337
  display: 'flex',
@@ -339,8 +350,8 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
339
350
  if (input) {
340
351
  input.indeterminate = isIndeterminate(node);
341
352
  }
342
- } })), _jsx("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, onClick: (e) => { handleNodeClick(node, e); }, children: itemRender(node) })] }), node.expanded && node.items && (_jsxs("div", { style: { paddingLeft: 20, width: '100%' }, children: [renderTree(getVisibleItems(node)), needsLoadMoreButton(node) && (_jsx("div", { style: { display: 'flex', justifyContent: 'center', margin: '5px 0' }, children: _jsx(TMButton, { onClick: () => handleLoadMoreItems(node.key), showTooltip: false, caption: `Carica altri ${itemsPerPage}... (${node.visibleItemsCount ?? itemsPerPage} / ${node.items?.length ?? 0})` }) }))] }))] }, node.key)));
343
- }, [handleNodeClick, handleNodeToggle, handleCheckboxChange, focusedItem, selectedItems, allowMultipleSelection, getVisibleItems, needsLoadMoreButton, handleLoadMoreItems, itemsPerPage]);
353
+ } })), _jsx("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, onClick: (e) => { handleNodeClick(node, e); }, children: itemRender(node) })] }), node.expanded && node.items && (_jsxs("div", { style: { paddingLeft: 20, width: '100%' }, children: [renderTree(getVisibleItems(node)), needsPagination(node) && (_jsxs(StyledStickyPaginator, { children: [_jsx(TMButton, { onClick: () => handlePageChange(node.key, (node.currentPage ?? 0) - 1), showTooltip: false, caption: "\u25C4", disabled: (node.currentPage ?? 0) <= 0 }), _jsxs("span", { style: { fontSize: '12px', whiteSpace: 'nowrap', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0 }, children: ["Pagina ", (node.currentPage ?? 0) + 1, " di ", getTotalPages(node), " (", node.items?.length ?? 0, " elementi)"] }), _jsx(TMButton, { onClick: () => handlePageChange(node.key, (node.currentPage ?? 0) + 1), showTooltip: false, caption: "\u25BA", disabled: (node.currentPage ?? 0) >= getTotalPages(node) - 1 })] }))] }))] }, node.key)));
354
+ }, [handleNodeClick, handleNodeToggle, handleCheckboxChange, focusedItem, selectedItems, allowMultipleSelection, getVisibleItems, needsPagination, handlePageChange, getTotalPages]);
344
355
  return (_jsx("div", { style: { height: '100%', width: '100%', overflowY: 'auto', overflowX: 'hidden', padding: '0px 5px 2px 2px' }, children: renderTree(dataSource) }));
345
356
  };
346
357
  export default TMTreeView;
@@ -361,3 +372,35 @@ export const StyledTreeNode = styled.div `
361
372
  background: ${() => `oklch(from var(--dx-color-primary) l c h / .1)`};
362
373
  }
363
374
  `;
375
+ export const StyledStickyPaginator = styled.div `
376
+ position: sticky;
377
+ bottom: 0;
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ gap: 8px;
382
+ margin: 8px 0;
383
+ padding: 8px 12px;
384
+ background: oklch(from var(--dx-color-primary) l c h / .85);
385
+ border: 1px solid oklch(from var(--dx-color-primary) l c h / .9);
386
+ border-radius: 6px;
387
+ box-shadow: 0 -2px 8px oklch(from var(--dx-color-primary) l c h / .15);
388
+ transition: all 0.2s ease;
389
+ min-width: 0;
390
+ z-index: 10;
391
+
392
+ & > button {
393
+ flex-shrink: 0;
394
+ }
395
+
396
+ & > span {
397
+ flex: 1 1 auto;
398
+ min-width: 0;
399
+ }
400
+
401
+ &:hover {
402
+ background: oklch(from var(--dx-color-primary) l c h / .95);
403
+ border-color: oklch(from var(--dx-color-primary) l c h / 1);
404
+ box-shadow: 0 -2px 12px oklch(from var(--dx-color-primary) l c h / .2);
405
+ }
406
+ `;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { DcmtTypeListCacheService, SDK_Localizator } from '@topconsultnpm/sdk-ts';
4
+ import styled from 'styled-components';
4
5
  import TMRelationViewer from './TMRelationViewer';
5
6
  import TMContextMenu from '../../NewComponents/ContextMenu/TMContextMenu';
6
7
  import { genUniqueId, IconMultipleSelection, IconCheckFile, IconDetailDcmts, SDKUI_Localizator, IconMail, IconDcmtTypeOnlyMetadata, IconCopy, IconMenuVertical, IconDataList, IconPreview, IconSearchCheck, IconBoard, IconDcmtTypeSys, IconShow, getMoreInfoTasksForDocument } from '../../../helper';
@@ -14,6 +15,44 @@ import { TMPanelManagerProvider, useTMPanelManagerContext } from '../../layout/p
14
15
  import TMSearchResult from '../search/TMSearchResult';
15
16
  import TMDcmtForm from './TMDcmtForm';
16
17
  import { TMNothingToShow } from './TMDcmtPreview';
18
+ import TMButton from '../../base/TMButton';
19
+ const StyledNoRelationsOverlay = styled.div `
20
+ width: 100%;
21
+ height: 100%;
22
+ position: fixed;
23
+ top: 0;
24
+ left: 0;
25
+ z-index: 1501;
26
+ overflow: visible;
27
+ background-color: rgba(0, 0, 0, 0.5);
28
+ backdrop-filter: blur(8px);
29
+ `;
30
+ const StyledNoRelationsPanel = styled.div `
31
+ position: absolute;
32
+ top: 50%;
33
+ left: 50%;
34
+ transform: translate(-50%, -50%);
35
+ width: 400px;
36
+ background: white;
37
+ border-radius: 8px;
38
+ padding: 20px;
39
+ text-align: center;
40
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: center;
44
+ `;
45
+ const StyledNoRelationsTitle = styled.h2 `
46
+ margin: 0 0 20px 0;
47
+ font-size: 1.3em;
48
+ color: #333;
49
+ `;
50
+ const StyledNoRelationsMessage = styled.p `
51
+ color: #666;
52
+ font-size: 1em;
53
+ margin: 0 0 25px 0;
54
+ line-height: 1.4;
55
+ `;
17
56
  const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, deviceType, inputDcmts, isForMaster, showCurrentDcmtIndicator = true, allowNavigation, canNext, canPrev, onNext, onPrev, onBack, appendMasterDcmts, onTaskCreateRequest }) => {
18
57
  const [id, setID] = useState('');
19
58
  const [focusedItem, setFocusedItem] = useState();
@@ -21,6 +60,10 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
21
60
  const [showZeroDcmts, setShowZeroDcmts] = useState(false);
22
61
  const [allowMultipleSelection, setAllowMultipleSelection] = useState(false);
23
62
  const [dtdMaster, setDtdMaster] = useState();
63
+ const [showNoRelationsModal, setShowNoRelationsModal] = useState(false);
64
+ const [shouldGoBackOnClose, setShouldGoBackOnClose] = useState(false);
65
+ // Ref to track if this is the first load (vs navigation with onPrev/onNext)
66
+ const isFirstLoadRef = useRef(true);
24
67
  useEffect(() => { setID(genUniqueId()); }, []);
25
68
  // Load dtdMaster when inputDcmts changes
26
69
  useEffect(() => {
@@ -40,10 +83,23 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
40
83
  }, [inputDcmts?.length, inputDcmts?.[0]?.TID, isForMaster]);
41
84
  const handleFocusedItemChanged = useCallback((item) => {
42
85
  setFocusedItem(item);
86
+ // When data is loaded and an item is focused, mark as no longer first load
87
+ if (item) {
88
+ isFirstLoadRef.current = false;
89
+ }
43
90
  }, []);
44
91
  const handleSelectedItemsChanged = useCallback((items) => {
45
92
  setSelectedItems(items);
46
93
  }, []);
94
+ const handleNoRelationsFound = useCallback(() => {
95
+ // Show modal only on first load, not during navigation with onPrev/onNext
96
+ if (isFirstLoadRef.current) {
97
+ setShowNoRelationsModal(true);
98
+ setShouldGoBackOnClose(true);
99
+ }
100
+ // Mark as no longer first load
101
+ isFirstLoadRef.current = false;
102
+ }, []);
47
103
  const commandsMenuItems = [
48
104
  {
49
105
  icon: _jsx(IconMultipleSelection, { color: allowMultipleSelection ? TMColors.tertiary : TMColors.text_normal }),
@@ -153,7 +209,7 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
153
209
  :
154
210
  _jsx(TMRelationViewerWrapper, { inputDcmts: inputDcmts, isForMaster: isForMaster, showCurrentDcmtIndicator: showCurrentDcmtIndicator, showZeroDcmts: showZeroDcmts,
155
211
  // customItemRender={customItemRender}
156
- allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: handleSelectedItemsChanged }) }), [inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, allowMultipleSelection, focusedItem, selectedItems, handleFocusedItemChanged, handleSelectedItemsChanged]);
212
+ allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: handleSelectedItemsChanged, onNoRelationsFound: handleNoRelationsFound }) }), [inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, allowMultipleSelection, focusedItem, selectedItems, handleFocusedItemChanged, handleSelectedItemsChanged, handleNoRelationsFound]);
157
213
  const tmFormOrResult = useMemo(() => _jsx(TMFormOrResultWrapper, { deviceType: deviceType, focusedItem: focusedItem, onTaskCreateRequest: onTaskCreateRequest, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers }), [focusedItem, deviceType, allTasks, handleNavigateToWGs, handleNavigateToDossiers]);
158
214
  const initialPanelDimensions = {
159
215
  'tmTreeView': { width: '50%', height: '100%' },
@@ -231,7 +287,12 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
231
287
  toolbarOptions: { icon: _jsx(IconSearchCheck, { fontSize: 24 }), visible: false, orderNumber: 2, isActive: allInitialPanelVisibility['tmFormOrResult'] }
232
288
  }
233
289
  ], [tmTreeView, tmFormOrResult, focusedItem?.isDcmt, dtdMaster]);
234
- return (_jsx(TMPanelManagerProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: initialPanelDimensions, initialDimensions: initialPanelDimensions, initialMobilePanelId: 'tmTreeView', children: _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", showToolbar: true }) }));
290
+ return (_jsxs(_Fragment, { children: [_jsx(TMPanelManagerProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: initialPanelDimensions, initialDimensions: initialPanelDimensions, initialMobilePanelId: 'tmTreeView', children: _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", showToolbar: true }) }), showNoRelationsModal && (_jsx(StyledNoRelationsOverlay, { children: _jsxs(StyledNoRelationsPanel, { children: [_jsx(StyledNoRelationsTitle, { children: SDKUI_Localizator.RelationsNotFound }), _jsx(StyledNoRelationsMessage, { children: SDKUI_Localizator.RelatedDcmtsNotFound }), _jsx(TMButton, { caption: "OK", showTooltip: false, onClick: () => {
291
+ setShowNoRelationsModal(false);
292
+ if (shouldGoBackOnClose) {
293
+ onBack?.();
294
+ }
295
+ }, btnStyle: "normal" })] }) }))] }));
235
296
  };
236
297
  export default TMMasterDetailDcmts;
237
298
  /**
@@ -240,7 +301,7 @@ export default TMMasterDetailDcmts;
240
301
  * - Panel visibility toggling
241
302
  * - Focus delay handling
242
303
  */
243
- const TMRelationViewerWrapper = ({ inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, customItemRender, allowMultipleSelection, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged }) => {
304
+ const TMRelationViewerWrapper = ({ inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, customItemRender, allowMultipleSelection, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onNoRelationsFound }) => {
244
305
  const { setPanelVisibilityById, setToolbarButtonVisibility } = useTMPanelManagerContext();
245
306
  // Handle focused item changes with panel visibility management
246
307
  const handleFocusedItemChanged = useCallback((item) => {
@@ -259,7 +320,7 @@ const TMRelationViewerWrapper = ({ inputDcmts, isForMaster, showCurrentDcmtIndic
259
320
  setToolbarButtonVisibility('tmDcmtForm', false);
260
321
  }
261
322
  }, [onFocusedItemChanged, setPanelVisibilityById, setToolbarButtonVisibility]);
262
- 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 }));
323
+ 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, onNoRelationsFound: onNoRelationsFound }));
263
324
  };
264
325
  const TMFormOrResultWrapper = ({ deviceType, focusedItem, onTaskCreateRequest, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
265
326
  const { setPanelVisibilityById } = useTMPanelManagerContext();
@@ -16,8 +16,9 @@ export interface RelationTreeItem extends ITMTreeItem {
16
16
  isRoot?: boolean;
17
17
  isMaster?: boolean;
18
18
  isCorrelated?: boolean;
19
- isLoaded?: boolean;
20
19
  isSeparator?: boolean;
20
+ isInfoMessage?: boolean;
21
+ isLogDel?: number;
21
22
  values?: any;
22
23
  searchResult?: SearchResultDescriptor[];
23
24
  itemsCount?: number;
@@ -92,6 +93,11 @@ export interface TMRelationViewerProps {
92
93
  * If not provided, uses the document type name.
93
94
  */
94
95
  labelMainContainer?: string;
96
+ /**
97
+ * Callback invoked when no relations are found for any of the input documents.
98
+ * Useful to notify parent component that there are no correlated documents to display.
99
+ */
100
+ onNoRelationsFound?: () => void;
95
101
  }
96
102
  /**
97
103
  * Check if document type has detail relations
@@ -1,7 +1,7 @@
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
3
  import { DcmtTypeListCacheService, SDK_Globals, DataColumnTypes, MetadataFormats, SystemMIDs, MetadataDataDomains, RelationCacheService, RelationTypes } from "@topconsultnpm/sdk-ts";
4
- import { genUniqueId, IconFolder, IconBackhandIndexPointingRight } from '../../../helper';
4
+ import { genUniqueId, IconFolder, IconBackhandIndexPointingRight, IconCircleInfo } from '../../../helper';
5
5
  import { TMColors } from '../../../utils/theme';
6
6
  import { StyledDivHorizontal, StyledBadge } from '../../base/Styled';
7
7
  import TMTreeView from '../../base/TMTreeView';
@@ -136,7 +136,7 @@ export const searchResultToDataSource = async (searchResult, hideSysMetadata) =>
136
136
  }
137
137
  return output;
138
138
  };
139
- const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndicator = true, allowShowZeroDcmts = true, initialShowZeroDcmts = false, allowedTIDs, allowMultipleSelection = false, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onDocumentDoubleClick, customItemRender, customDocumentStyle, customMainContainerContent, customDocumentContent, showMetadataNames = false, maxDepthLevel = 2, invertMasterNavigation = true, additionalStaticItems, showMainDocument = true, labelMainContainer, }) => {
139
+ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndicator = true, allowShowZeroDcmts = true, initialShowZeroDcmts = false, allowedTIDs, allowMultipleSelection = false, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onDocumentDoubleClick, customItemRender, customDocumentStyle, customMainContainerContent, customDocumentContent, showMetadataNames = false, maxDepthLevel = 2, invertMasterNavigation = true, additionalStaticItems, showMainDocument = true, labelMainContainer, onNoRelationsFound, }) => {
140
140
  // State
141
141
  const [dcmtTypes, setDcmtTypes] = useState([]);
142
142
  const [treeData, setTreeData] = useState([]);
@@ -156,6 +156,8 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
156
156
  const lastLoadedInputRef = React.useRef('');
157
157
  // Ref to track if user has manually expanded/collapsed static items
158
158
  const userInteractedWithStaticItemsRef = React.useRef(false);
159
+ // Ref to track if we've already set the initial focused item
160
+ const initialFocusSetRef = React.useRef(false);
159
161
  /**
160
162
  * Generate a stable key from inputDcmts to detect real changes
161
163
  */
@@ -219,20 +221,27 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
219
221
  const source = await searchResultToDataSource(searchResult);
220
222
  if (source && source.length > 0) {
221
223
  const dcmtDetails = [];
224
+ // Check once if this document type can have detail relations
225
+ // (optimization: avoid checking for each document)
226
+ const canHaveDetails = await hasDetailRelations(searchResult.fromTID);
222
227
  for (const row of source) {
223
228
  const rowGUID = genUniqueId();
224
229
  const tid = row?.TID?.value;
225
230
  const did = row?.DID?.value;
231
+ const isLogDel = row?.ISLOGDEL?.value;
226
232
  dcmtDetails.push({
227
233
  tid: tid,
228
234
  did: did,
235
+ isLogDel: isLogDel,
229
236
  key: `${tid}_${did}_${searchResult.relationID}_${mTID}_${mDID}_${rowGUID}`,
230
237
  isDcmt: true,
231
238
  isContainer: false,
232
239
  isZero: false, // Documents are never zero (they exist)
240
+ isExpandible: canHaveDetails, // Can this doc be expanded?
233
241
  values: row,
234
242
  searchResult: [searchResult],
235
- itemsCount: 0,
243
+ // Leave items and itemsCount undefined so TMTreeView shows expand arrow based on isExpandible
244
+ // Children will be loaded lazily by calculateItemsForNode when expanded
236
245
  expanded: false,
237
246
  hidden: false,
238
247
  name: row?.SYS_Abstract?.value || row?.SYS_SUBJECT?.value || `Documento ${did}`
@@ -293,20 +302,30 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
293
302
  const source = await searchResultToDataSource(searchResult);
294
303
  if (source && source.length > 0) {
295
304
  const dcmtMasters = [];
305
+ // Check once if this document type can have master relations (for inverted mode)
306
+ // or detail relations (for standard mode when expanding masters)
307
+ // (optimization: avoid checking for each document)
308
+ const canExpand = isForMaster && invertMasterNavigation
309
+ ? await hasMasterRelations(searchResult.fromTID)
310
+ : await hasDetailRelations(searchResult.fromTID);
296
311
  for (const row of source) {
297
312
  const rowGUID = genUniqueId();
298
313
  const tid = row?.TID?.value;
299
314
  const did = row?.DID?.value;
315
+ const isLogDel = row?.ISLOGDEL?.value;
300
316
  dcmtMasters.push({
301
317
  tid: tid,
302
318
  did: did,
319
+ isLogDel: isLogDel,
303
320
  key: `${tid}_${did}_${searchResult.relationID}_${dTID}_${dDID}_${rowGUID}`,
304
321
  isDcmt: true,
305
322
  isContainer: false,
306
323
  isZero: false, // Documents are never zero (they exist)
324
+ isExpandible: canExpand, // Can this doc be expanded?
307
325
  values: row,
308
326
  searchResult: [searchResult],
309
- itemsCount: 0,
327
+ // Leave items and itemsCount undefined so TMTreeView shows expand arrow based on isExpandible
328
+ // Children will be loaded lazily by calculateItemsForNode when expanded
310
329
  expanded: false,
311
330
  hidden: false,
312
331
  name: row?.SYS_Abstract?.value || row?.SYS_SUBJECT?.value || `Documento ${did}`
@@ -324,17 +343,128 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
324
343
  return items;
325
344
  }, [allowedTIDs]);
326
345
  /**
327
- * Update hidden property based on showZeroDcmts (original simple logic)
346
+ * Update hidden property based on showZeroDcmts
347
+ * Updates items in-place without resetting isLoaded (no API calls needed)
328
348
  */
329
349
  const updateHiddenProperty = useCallback((nodes) => {
350
+ // Safety check: if nodes is undefined or not an array, return empty array
351
+ if (!nodes || !Array.isArray(nodes)) {
352
+ return [];
353
+ }
330
354
  return nodes.map(node => {
331
- const updatedNode = { ...node, hidden: !showZeroDcmts && node.isZero };
332
- if (node.items) {
333
- updatedNode.items = updateHiddenProperty(node.items);
355
+ let shouldHide = false;
356
+ let updatedItems = undefined;
357
+ if (node.items && Array.isArray(node.items)) {
358
+ // Recursively update children first - this creates a new array
359
+ let updatedChildren = updateHiddenProperty(node.items);
360
+ // Remove any existing auto-generated info messages (with key starting with __info__)
361
+ const filteredChildren = updatedChildren.filter(child => !(child.isInfoMessage && child.key?.startsWith('__info__')));
362
+ // Check if all real children (excluding info messages) are hidden
363
+ const realChildren = filteredChildren.filter(child => !child.isInfoMessage);
364
+ const allChildrenHidden = realChildren.length > 0 && realChildren.every(child => child.hidden);
365
+ // If node is expanded AND showZeroDcmts is false AND all children are hidden, add info message
366
+ // This applies to both containers and document nodes with children
367
+ if (node.expanded && !showZeroDcmts && allChildrenHidden) {
368
+ // Add info message at the beginning, keep original children (hidden but preserved)
369
+ updatedItems = [
370
+ {
371
+ key: `__info__${node.key}`,
372
+ name: 'Nessun documento correlato da visualizzare',
373
+ isContainer: false,
374
+ isDcmt: false,
375
+ isInfoMessage: true,
376
+ isExpandible: false
377
+ },
378
+ ...filteredChildren
379
+ ];
380
+ // Don't hide the node itself if it's showing an info message
381
+ shouldHide = false;
382
+ }
383
+ else {
384
+ updatedItems = filteredChildren;
385
+ // Hide zero-item containers when showZeroDcmts is false (only if not showing info message)
386
+ shouldHide = !showZeroDcmts && (node.isZero ?? false);
387
+ }
388
+ }
389
+ else {
390
+ // No items, apply normal hide logic
391
+ shouldHide = !showZeroDcmts && (node.isZero ?? false);
334
392
  }
335
- return updatedNode;
393
+ // IMPORTANT: Always create new object to trigger React re-render
394
+ return { ...node, hidden: shouldHide, items: updatedItems };
336
395
  });
337
396
  }, [showZeroDcmts]);
397
+ /**
398
+ * Helper function to set up initial tree expansion state.
399
+ * Called after all data is loaded but before setTreeData.
400
+ * Ensures: root container expanded, first document expanded with isRoot=true (for focus), first correlation folder expanded
401
+ * Returns a NEW tree with the modifications (immutable approach)
402
+ */
403
+ const setupInitialTreeExpansion = (tree) => {
404
+ if (tree.length === 0)
405
+ return tree;
406
+ // ALWAYS expand the first container (even if empty, to show infoMessage)
407
+ const firstRootContainer = tree[0];
408
+ if (!firstRootContainer)
409
+ return tree;
410
+ // Create a deep copy of the first container with expanded=true, isRoot=true
411
+ let newFirstContainer = {
412
+ ...firstRootContainer,
413
+ expanded: true,
414
+ isRoot: true
415
+ };
416
+ // 2. Find first document/container and expand it
417
+ const firstRootItems = newFirstContainer.items;
418
+ if (firstRootItems && firstRootItems.length > 0) {
419
+ const firstDocOrContainer = firstRootItems[0];
420
+ if (firstDocOrContainer.isDcmt) {
421
+ // First item is a document - expand it and mark as root for focus
422
+ let newFirstDoc = {
423
+ ...firstDocOrContainer,
424
+ expanded: true,
425
+ isRoot: true
426
+ };
427
+ // 3. Expand first correlation folder (child of the document)
428
+ const docItems = newFirstDoc.items;
429
+ if (docItems && docItems.length > 0 && docItems[0].isContainer) {
430
+ const newFirstCorrelation = { ...docItems[0], expanded: true };
431
+ newFirstDoc = {
432
+ ...newFirstDoc,
433
+ items: [newFirstCorrelation, ...docItems.slice(1)]
434
+ };
435
+ }
436
+ // Update the container's items
437
+ newFirstContainer = {
438
+ ...newFirstContainer,
439
+ items: [newFirstDoc, ...firstRootItems.slice(1)]
440
+ };
441
+ }
442
+ else if (firstDocOrContainer.isContainer) {
443
+ // First item is a container (correlation folder) - expand it
444
+ let newFirstCorrelation = {
445
+ ...firstDocOrContainer,
446
+ expanded: true
447
+ };
448
+ // Find first document inside this container and mark for focus
449
+ const containerItems = newFirstCorrelation.items;
450
+ if (containerItems && containerItems.length > 0 && containerItems[0].isDcmt) {
451
+ const newFirstDoc = { ...containerItems[0], isRoot: true };
452
+ newFirstCorrelation = {
453
+ ...newFirstCorrelation,
454
+ items: [newFirstDoc, ...containerItems.slice(1)]
455
+ };
456
+ }
457
+ // Update the container's items
458
+ newFirstContainer = {
459
+ ...newFirstContainer,
460
+ items: [newFirstCorrelation, ...firstRootItems.slice(1)]
461
+ };
462
+ }
463
+ }
464
+ // If firstRootItems is empty/undefined, the container is still expanded (will show infoMessage)
465
+ // Return new tree with the modified first container
466
+ return [newFirstContainer, ...tree.slice(1)];
467
+ };
338
468
  /**
339
469
  * Main data loading function
340
470
  */
@@ -405,13 +535,12 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
405
535
  tid: dcmt.TID,
406
536
  dtd,
407
537
  isContainer: true,
408
- isRoot: true,
409
538
  isLoaded: true,
410
539
  isZero: false,
411
540
  searchResult: result ? [result] : [],
412
541
  items: relatedDocs, // Directly show detail containers
413
542
  itemsCount: relatedDocs.length,
414
- expanded: tree.length === 0,
543
+ expanded: false,
415
544
  hidden: false
416
545
  };
417
546
  tree.push(typeContainer);
@@ -426,10 +555,9 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
426
555
  did: dcmt.DID,
427
556
  isDcmt: true,
428
557
  isContainer: false,
429
- expanded: tree.length === 0,
558
+ expanded: false,
430
559
  isZero: dcmt.DID === 0,
431
560
  isMaster: !isForMaster,
432
- isRoot: true,
433
561
  isLoaded: true,
434
562
  hidden: false,
435
563
  values: docRow,
@@ -444,13 +572,12 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
444
572
  tid: dcmt.TID,
445
573
  dtd,
446
574
  isContainer: true,
447
- isRoot: true,
448
575
  isLoaded: true,
449
576
  isZero: false, // Type container is never zero (contains documents)
450
577
  searchResult: result ? [result] : [],
451
578
  items: [docNode],
452
579
  itemsCount: 1,
453
- expanded: tree.length === 0,
580
+ expanded: false,
454
581
  hidden: false
455
582
  };
456
583
  tree.push(typeContainer);
@@ -460,8 +587,55 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
460
587
  processedCount++;
461
588
  setWaitPanelValuePrimary(processedCount);
462
589
  }
463
- setTreeData(updateHiddenProperty(tree));
464
- }, [inputDcmts, dcmtTypes, maxDepthLevel, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, abortController, updateHiddenProperty, showMainDocument, labelMainContainer]);
590
+ /**
591
+ * Helper function to check if a node has any actual correlated documents
592
+ * Returns true if there are NO documents (relations are empty)
593
+ */
594
+ const hasNoActualDocuments = (items) => {
595
+ if (!items || items.length === 0)
596
+ return true;
597
+ // Check each item
598
+ return items.every(item => {
599
+ // If it's a container, check if it's zero or has no documents
600
+ if (item.isContainer) {
601
+ // Container with isZero=true has no documents
602
+ if (item.isZero)
603
+ return true;
604
+ // Container with no items has no documents
605
+ if (!item.items || item.items.length === 0)
606
+ return true;
607
+ // Otherwise, it has documents
608
+ return false;
609
+ }
610
+ // If it's a document node, check its children (relation containers)
611
+ if (item.isDcmt) {
612
+ // Check if all child containers are empty
613
+ return hasNoActualDocuments(item.items);
614
+ }
615
+ // For other types, assume no documents
616
+ return true;
617
+ });
618
+ };
619
+ // Check if there are no relations for any document
620
+ const hasNoRelations = tree.length === 0 || tree.every(container => {
621
+ // Check if container has no items
622
+ if (!container.items || container.items.length === 0)
623
+ return true;
624
+ // Check recursively if there are any actual documents
625
+ return hasNoActualDocuments(container.items);
626
+ });
627
+ // If no relations found, notify parent
628
+ if (hasNoRelations && onNoRelationsFound) {
629
+ onNoRelationsFound();
630
+ }
631
+ // FIRST setup initial expansion state (root container expanded, first document expanded with focus, first correlation folder expanded)
632
+ // This must run BEFORE updateHiddenProperty so that infoMessage logic sees expanded=true
633
+ const expandedTree = setupInitialTreeExpansion(tree);
634
+ // THEN apply hidden property transformations (creates new objects, adds infoMessage where needed)
635
+ // Now it will correctly detect expanded nodes and add infoMessage if all children are hidden
636
+ const processedTree = updateHiddenProperty(expandedTree);
637
+ setTreeData(processedTree);
638
+ }, [inputDcmts, dcmtTypes, maxDepthLevel, isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, abortController, updateHiddenProperty, showMainDocument, labelMainContainer, onNoRelationsFound]);
465
639
  /**
466
640
  * Merge main tree data with additional static items
467
641
  */
@@ -509,10 +683,56 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
509
683
  }, [inputDcmts, dcmtTypes, maxDepthLevel, getInputKey, loadData, treeData.length]);
510
684
  /**
511
685
  * Update tree when showZeroDcmts changes
686
+ * Updates the visualization without re-fetching data (isLoaded stays true)
512
687
  */
513
688
  useEffect(() => {
514
689
  setTreeData(prevData => updateHiddenProperty(prevData));
515
690
  }, [showZeroDcmts, updateHiddenProperty]);
691
+ /**
692
+ * Set initial focused item when showMainDocument = true
693
+ * Focuses on the main document (first inputDcmts) when data is loaded
694
+ * For master mode with invertMasterNavigation=false, focuses on the first master document
695
+ */
696
+ useEffect(() => {
697
+ // Only execute if:
698
+ // 1. showMainDocument is true
699
+ // 2. onFocusedItemChanged callback exists
700
+ // 3. We have tree data
701
+ // 4. We have input documents
702
+ // 5. We haven't already set the initial focus
703
+ if (!showMainDocument || !onFocusedItemChanged || !treeData.length || !inputDcmts?.length || initialFocusSetRef.current) {
704
+ return;
705
+ }
706
+ // Helper function to recursively find the first document with isRoot=true
707
+ const findFirstRootDocument = (items) => {
708
+ for (const item of items) {
709
+ // Check if this item is a document with isRoot=true
710
+ if (item.isDcmt && item.isRoot) {
711
+ return item;
712
+ }
713
+ // Recursively search in children
714
+ if (item.items && Array.isArray(item.items)) {
715
+ const found = findFirstRootDocument(item.items);
716
+ if (found)
717
+ return found;
718
+ }
719
+ }
720
+ return null;
721
+ };
722
+ // Find the first document marked as root (set by setupInitialTreeExpansion)
723
+ const docNode = findFirstRootDocument(treeData);
724
+ if (docNode) {
725
+ // Set the focused item
726
+ onFocusedItemChanged(docNode);
727
+ initialFocusSetRef.current = true;
728
+ }
729
+ }, [treeData, showMainDocument, onFocusedItemChanged, inputDcmts]);
730
+ /**
731
+ * Reset initial focus flag when input documents change
732
+ */
733
+ useEffect(() => {
734
+ initialFocusSetRef.current = false;
735
+ }, [getInputKey()]);
516
736
  /**
517
737
  * Sync static items state when additionalStaticItems change
518
738
  * IMPORTANT: Only update if user hasn't manually interacted with the tree,
@@ -548,6 +768,10 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
548
768
  /**
549
769
  * Calculate items for node when expanded (lazy loading)
550
770
  * Note: additionalStaticItems are already fully loaded, so skip calculation for them
771
+ *
772
+ * PERFORMANCE OPTIMIZATION:
773
+ * - Containers: Return items immediately (no API calls) - items are already loaded
774
+ * - Documents: Load children lazily only when the specific document is expanded (1 API call)
551
775
  */
552
776
  const calculateItemsForNode = useCallback(async (node) => {
553
777
  // Skip calculation for separator (it has no children)
@@ -558,62 +782,129 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
558
782
  if (node.isStaticItem || node.isAdditionalContainer || node.isAdditional) {
559
783
  return node.items;
560
784
  }
561
- // If it's a document, return existing items
562
- if (node.isDcmt)
563
- return node.items;
564
- // If container is already loaded, return items
565
- if (node.isLoaded)
566
- return node.items;
567
- const newAbortController = new AbortController();
568
- setExpansionAbortController(newAbortController);
569
- const itemsToLoad = node.items?.length ?? 0;
570
- setShowExpansionWaitPanel(true);
571
- setExpansionWaitPanelMaxValue(itemsToLoad);
572
- setExpansionWaitPanelValue(0);
573
- setExpansionWaitPanelText(`Caricamento documenti correlati...`);
574
- try {
575
- const newItems = [];
576
- let processedCount = 0;
577
- for (const dcmt of node.items ?? []) {
578
- if (newAbortController.signal.aborted) {
579
- console.log('Folder expansion aborted by user');
580
- return node.items;
581
- }
582
- const item = { ...dcmt };
583
- if (item.tid && item.did && !item.isLoaded) {
584
- // Update progress
585
- processedCount++;
586
- setExpansionWaitPanelValue(processedCount);
587
- setExpansionWaitPanelText(`Caricamento ${processedCount} di ${itemsToLoad}...`);
588
- // Nella modalità originale (invertMasterNavigation=false),
589
- // i documenti detail non devono caricare i master come figli
590
- if (isForMaster && !invertMasterNavigation) {
591
- // Carica i detail dei detail (navigazione naturale detail→detail)
592
- const loadedItems = await getDetailDcmtsAsync(item.tid, item.did, 1);
593
- item.items = updateHiddenProperty(loadedItems);
594
- }
595
- else {
596
- // Modalità standard o invertita
597
- const loadedItems = isForMaster
598
- ? await getMasterDcmtsAsync(item.tid, item.did, 1)
599
- : await getDetailDcmtsAsync(item.tid, item.did, 1);
600
- item.items = updateHiddenProperty(loadedItems);
601
- }
602
- item.isLoaded = true;
785
+ // If already loaded, apply current visualization logic without re-fetching
786
+ if (node.isLoaded && node.items) {
787
+ // Apply current show/hide logic based on showZeroDcmts
788
+ const updatedItems = updateHiddenProperty(node.items);
789
+ // If this is a container, check if we need to show info message
790
+ if (node.isContainer) {
791
+ // Remove any existing info messages
792
+ const filteredItems = updatedItems.filter(child => !(child.isInfoMessage && child.key?.startsWith('__info__')));
793
+ // Check if all real children are hidden
794
+ const realChildren = filteredItems.filter(child => !child.isInfoMessage);
795
+ const allChildrenHidden = realChildren.length > 0 && realChildren.every(child => child.hidden);
796
+ // If showZeroDcmts is false and all items are hidden, show info message + keep hidden containers
797
+ if (!showZeroDcmts && allChildrenHidden) {
798
+ return [
799
+ {
800
+ key: `__info__${node.key}`,
801
+ name: 'Nessun documento correlato da visualizzare',
802
+ isContainer: false,
803
+ isDcmt: false,
804
+ isInfoMessage: true,
805
+ isExpandible: false
806
+ },
807
+ ...filteredItems // Keep hidden containers so they can be shown when toggling
808
+ ];
603
809
  }
604
- newItems.push(item);
605
810
  }
606
- return newItems;
811
+ return updatedItems;
607
812
  }
608
- catch (error) {
609
- console.error('Error loading folder contents:', error);
610
- return node.items;
813
+ // ============================================
814
+ // CONTAINER: Show items immediately (no API calls)
815
+ // Items are already loaded from initial load or parent expansion
816
+ // ============================================
817
+ if (node.isContainer) {
818
+ // No API calls needed - just return existing items
819
+ // Children of these items will be loaded lazily when user expands each document
820
+ // Apply updateHiddenProperty to respect current showZeroDcmts setting
821
+ // If node has no items, return empty array
822
+ if (!node.items)
823
+ return [];
824
+ const updatedItems = updateHiddenProperty(node.items);
825
+ // If showZeroDcmts is false and all items are hidden, add info message + keep hidden containers
826
+ if (!showZeroDcmts && updatedItems.length > 0 && updatedItems.every(item => item.hidden)) {
827
+ return [
828
+ {
829
+ key: `__info__${node.key}`,
830
+ name: 'Nessun documento correlato da visualizzare',
831
+ isContainer: false,
832
+ isDcmt: false,
833
+ isInfoMessage: true,
834
+ isExpandible: false
835
+ },
836
+ ...updatedItems // Keep hidden containers so they can be shown when toggling
837
+ ];
838
+ }
839
+ return updatedItems;
611
840
  }
612
- finally {
613
- setShowExpansionWaitPanel(false);
614
- setExpansionAbortController(undefined);
841
+ // ============================================
842
+ // DOCUMENT: Load its children (relation containers) lazily
843
+ // Only makes ONE API call for this specific document
844
+ // ============================================
845
+ if (node.isDcmt && node.tid && node.did) {
846
+ const newAbortController = new AbortController();
847
+ setExpansionAbortController(newAbortController);
848
+ setShowExpansionWaitPanel(true);
849
+ setExpansionWaitPanelMaxValue(1);
850
+ setExpansionWaitPanelValue(0);
851
+ setExpansionWaitPanelText(`Caricamento documenti correlati...`);
852
+ try {
853
+ // Check for abort
854
+ if (newAbortController.signal.aborted) {
855
+ return [];
856
+ }
857
+ // Determine which load function to use based on mode
858
+ let loadedItems = [];
859
+ if (isForMaster && !invertMasterNavigation) {
860
+ // Original mode: detail documents load detail documents (natural detail→detail navigation)
861
+ loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1);
862
+ }
863
+ else if (isForMaster) {
864
+ // Inverted master mode: load master documents
865
+ loadedItems = await getMasterDcmtsAsync(node.tid, node.did, 1);
866
+ }
867
+ else {
868
+ // Standard mode: load detail documents
869
+ loadedItems = await getDetailDcmtsAsync(node.tid, node.did, 1);
870
+ }
871
+ // Espandi automaticamente il primo container se ci sono documenti correlati
872
+ if (loadedItems.length > 0 && loadedItems[0].isContainer) {
873
+ loadedItems[0].expanded = true;
874
+ }
875
+ // Apply updateHiddenProperty to respect current showZeroDcmts setting
876
+ // This ensures that dynamically loaded nodes respect the visibility rules:
877
+ // - Relation type containers are always visible (even if empty)
878
+ // - Other containers with zero items can be hidden based on showZeroDcmts
879
+ const updatedItems = updateHiddenProperty(loadedItems);
880
+ // If showZeroDcmts is false and all items are hidden, add info message + keep hidden containers
881
+ if (!showZeroDcmts && updatedItems.length > 0 && updatedItems.every(item => item.hidden)) {
882
+ return [
883
+ {
884
+ key: `__info__${node.key}`,
885
+ name: 'Nessun documento correlato da visualizzare',
886
+ isContainer: false,
887
+ isDcmt: false,
888
+ isInfoMessage: true,
889
+ isExpandible: false
890
+ },
891
+ ...updatedItems // Keep hidden containers so they can be shown when toggling
892
+ ];
893
+ }
894
+ return updatedItems;
895
+ }
896
+ catch (error) {
897
+ console.error('Error loading document relations:', error);
898
+ return [];
899
+ }
900
+ finally {
901
+ setShowExpansionWaitPanel(false);
902
+ setExpansionAbortController(undefined);
903
+ }
615
904
  }
616
- }, [isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty]);
905
+ // Default: return existing items
906
+ return node.items;
907
+ }, [isForMaster, invertMasterNavigation, getDetailDcmtsAsync, getMasterDcmtsAsync, updateHiddenProperty, showZeroDcmts]);
617
908
  /**
618
909
  * Default item renderer with metadata display (adapted from TMMasterDetailDcmts.tsx)
619
910
  */
@@ -626,6 +917,18 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
626
917
  e.stopPropagation();
627
918
  onDocumentDoubleClick?.(item.tid, item.did, item.name);
628
919
  };
920
+ // Info message rendering
921
+ if (item.isInfoMessage) {
922
+ return (_jsxs("div", { style: {
923
+ display: 'flex',
924
+ alignItems: 'center',
925
+ gap: '10px',
926
+ height: '32px',
927
+ padding: '6px 0',
928
+ color: '#666',
929
+ fontStyle: 'italic'
930
+ }, children: [_jsx(IconCircleInfo, { fontSize: 20, color: TMColors.iconLight }), _jsx("span", { children: item.name })] }));
931
+ }
629
932
  // Container rendering
630
933
  if (item.isContainer || !item.isDcmt) {
631
934
  const defaultContainerStyle = {
@@ -634,7 +937,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
634
937
  gap: '10px',
635
938
  height: '32px',
636
939
  padding: '6px 0',
637
- opacity: item.isZero ? 0.4 : 1,
940
+ opacity: item.isZero ? 0.99 : 1,
638
941
  transition: 'opacity 0.2s ease-in-out'
639
942
  };
640
943
  // Se è il container principale (root) e showMainDocument è false,
@@ -653,6 +956,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
653
956
  return (_jsx("div", { style: defaultContainerStyle, children: content }));
654
957
  }
655
958
  // Document rendering with full metadata display
959
+ const isLogicallyDeleted = Number(item.isLogDel) === 1;
656
960
  const defaultDocumentStyle = {
657
961
  minWidth: '90px',
658
962
  width: '100%',
@@ -664,18 +968,23 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
664
968
  alignItems: 'center',
665
969
  cursor: 'pointer',
666
970
  userSelect: 'none',
667
- opacity: item.isZero ? 0.4 : 1,
668
- transition: 'opacity 0.2s ease-in-out'
971
+ opacity: item.isZero ? 0.99 : 1,
972
+ transition: 'opacity 0.2s ease-in-out',
973
+ textDecoration: isLogicallyDeleted ? 'line-through' : 'none'
669
974
  };
670
975
  const documentStyle = customDocumentStyle
671
976
  ? { ...defaultDocumentStyle, ...customDocumentStyle(item) }
672
977
  : defaultDocumentStyle;
978
+ const textDecoration = isLogicallyDeleted ? 'line-through' : 'none';
979
+ const textColor = isLogicallyDeleted ? 'gray' : undefined;
673
980
  const defaultMetadataContent = item.values && (_jsx(StyledDivHorizontal, { style: {
674
981
  fontSize: '1rem',
675
982
  overflow: 'hidden',
676
983
  flex: 1,
677
984
  minWidth: 0,
678
- whiteSpace: 'nowrap'
985
+ whiteSpace: 'nowrap',
986
+ textDecoration: textDecoration,
987
+ color: textColor
679
988
  }, children: getDcmtDisplayValue(item.values).map((key, index) => {
680
989
  const md = item.values?.[key]?.md;
681
990
  const value = item.values?.[key]?.value;
@@ -684,12 +993,16 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
684
993
  return (_jsxs(StyledDivHorizontal, { style: {
685
994
  flexShrink: isLast ? 1 : 0,
686
995
  minWidth: isLast ? 0 : 'auto',
687
- overflow: isLast ? 'hidden' : 'visible'
688
- }, children: [index > 0 && _jsx("span", { style: { margin: '0 5px', color: '#999' }, children: "\u2022" }), showMetadataNames && (_jsxs("span", { style: { color: '#666', marginRight: '5px' }, children: [md?.name || key, ":"] })), md?.dataDomain === MetadataDataDomains.DataList ? (_jsx(TMDataListItemViewer, { dataListId: md.dataListID, viewMode: md.dataListViewMode, value: value })) : md?.dataDomain === MetadataDataDomains.UserID ? (_jsx(TMDataUserIdItemViewer, { userId: value, showIcon: true })) : (_jsx("span", { style: {
996
+ overflow: isLast ? 'hidden' : 'visible',
997
+ textDecoration: textDecoration,
998
+ color: textColor
999
+ }, 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: {
689
1000
  fontWeight: 500,
690
1001
  overflow: isLast ? 'hidden' : 'visible',
691
1002
  textOverflow: isLast ? 'ellipsis' : 'clip',
692
- whiteSpace: 'nowrap'
1003
+ whiteSpace: 'nowrap',
1004
+ textDecoration: textDecoration,
1005
+ color: textColor
693
1006
  }, children: value }))] }, `${key}_${index}`));
694
1007
  }) }));
695
1008
  const metadataContent = customDocumentContent
@@ -744,7 +1057,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
744
1057
  if (mergedTreeData.length === 0) {
745
1058
  return _jsx("div", { style: { padding: '20px', textAlign: 'center', color: '#666' }, children: "Nessuna relazione disponibile." });
746
1059
  }
747
- return (_jsxs(_Fragment, { children: [_jsx(TMTreeView, { dataSource: mergedTreeData, itemRender: finalItemRender, calculateItemsForNode: calculateItemsForNode, onDataChanged: handleDataChanged, focusedItem: focusedItem, onFocusedItemChanged: handleFocusedItemChanged, allowMultipleSelection: allowMultipleSelection, selectedItems: selectedItems, onSelectionChanged: handleSelectedItemsChanged }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? 'Caricamento documenti master' : 'Caricamento documenti dettaglio', showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: (abortController) => {
1060
+ return (_jsxs(_Fragment, { children: [_jsx(TMTreeView, { dataSource: mergedTreeData, itemRender: finalItemRender, calculateItemsForNode: calculateItemsForNode, onDataChanged: handleDataChanged, focusedItem: focusedItem, onFocusedItemChanged: handleFocusedItemChanged, allowMultipleSelection: allowMultipleSelection, selectedItems: selectedItems, itemsPerPage: 100, onSelectionChanged: handleSelectedItemsChanged }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? 'Caricamento documenti master' : 'Caricamento documenti dettaglio', showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: (abortController) => {
748
1061
  setTimeout(() => {
749
1062
  abortController?.abort();
750
1063
  }, 100);
@@ -274,9 +274,9 @@ export const RenderTaskFormStateField = (props) => {
274
274
  const closeTaskCallback = () => {
275
275
  if (!formData || !formData.id)
276
276
  return;
277
- const msg = `Sei sicuro voler chiudere l'attività ${formData.name ?? 'N/A'}?`;
277
+ const msg = _jsxs("span", { children: ["Sei sicuro voler chiudere l'attivit\u00E0 \"", _jsx("b", { children: formData.name ?? "N/A" }), "\"?"] });
278
278
  TMMessageBoxManager.show({
279
- title: SDKUI_Localizator.MarkAs, message: msg, buttons: [ButtonNames.YES, ButtonNames.NO],
279
+ title: SDKUI_Localizator.CloseTask, message: msg, buttons: [ButtonNames.YES, ButtonNames.NO],
280
280
  onButtonClick: async (e) => {
281
281
  if (e !== ButtonNames.YES)
282
282
  return;
@@ -345,7 +345,7 @@ export const TMChangeStateForm = (props) => {
345
345
  const [dropdownValues, setDropdownValues] = React.useState([]);
346
346
  useEffect(() => {
347
347
  const availableTransitions = getAvailableTaskTransitions(formData?.state ?? Task_States.None, taskRole);
348
- const values = getDropdownValuesFromTaskStates(availableTransitions, formData?.state ?? Task_States.None);
348
+ const values = getDropdownValuesFromTaskStates(availableTransitions);
349
349
  setDropdownValues(values);
350
350
  if (values.length > 0) {
351
351
  const currentState = formData?.state ?? Task_States.None;
@@ -416,7 +416,7 @@ const getAvailableTaskTransitions = (currentStatus, role) => {
416
416
  const transitions = STATUS_TRANSITIONS[role];
417
417
  return transitions?.[currentStatus] ?? [];
418
418
  };
419
- const getDropdownValuesFromTaskStates = (states, currentState) => {
419
+ const getDropdownValuesFromTaskStates = (states) => {
420
420
  const map = {
421
421
  [Task_States.Completed]: SDKUI_Localizator.Completed,
422
422
  [Task_States.Closed]: SDKUI_Localizator.Closed,
@@ -427,6 +427,6 @@ const getDropdownValuesFromTaskStates = (states, currentState) => {
427
427
  [Task_States.None]: "",
428
428
  };
429
429
  return states
430
- .filter(state => state !== Task_States.None && state !== currentState)
430
+ .filter(state => state !== Task_States.None)
431
431
  .map(state => ({ value: state, display: map[state] }));
432
432
  };
@@ -100,6 +100,7 @@ export declare class SDKUI_Localizator {
100
100
  static get ClearOTP(): "OTP löschen" | "Clear OTP" | "Borrar OTP" | "Effacer l'OTP" | "Limpar OTP" | "Cancella OTP";
101
101
  static get Close(): "Ausgang" | "Close" | "Salida" | "Sortie" | "Saída" | "Chiudi";
102
102
  static get Closed(): "Geschlossen" | "Closed" | "Cerrada" | "Fermée" | "Fechada" | "Chiusa";
103
+ static get CloseTask(): string;
103
104
  static get Columns_All_Hide(): "Alle Spalten ausblenden" | "Hide all columns" | "Ocultar todas las columnas" | "Masquer toutes les colonnes" | "Ocultar todas as colunas" | "Nascondi tutte le colonne";
104
105
  static get Columns_All_Show(): "Alle Spalten anzeigen" | "Show all columns" | "Mostrar todas las columnas" | "Afficher toutes les colonnes" | "Mostrar todas as colunas" | "Visualizza tutte le colonne";
105
106
  static get Comment(): string;
@@ -517,6 +518,8 @@ export declare class SDKUI_Localizator {
517
518
  static get Redo(): "Wiederherstellen" | "Redo" | "Rehacer" | "Rétablir" | "Refazer" | "Rifai";
518
519
  static get Reject(): "Ablehnen" | "Reject" | "Rechazar" | "Rejeter" | "Rejeitar" | "Rifiuta";
519
520
  static get Relations(): "Korrelationen" | "Correlations" | "Correlaciones" | "Relations" | "Correlacionados" | "Correlazioni";
521
+ static get RelationsNotFound(): "Keine Korrelationen gefunden" | "No correlations found" | "No se encontraron correlaciones" | "Aucune corrélation trouvée" | "Nenhuma correlação encontrada" | "Nessuna correlazione trovata";
522
+ static get RelatedDcmtsNotFound(): "Für das ausgewählte Dokument wurden keine zugehörigen Dokumente gefunden." | "No related documents were found for the selected document." | "No se encontraron documentos relacionados para el documento seleccionado." | "Aucun document associé n'a été trouvé pour le document sélectionné." | "Nenhum documento relacionado foi encontrado para o documento selecionado." | "Non sono stati trovati documenti correlati per il documento selezionato.";
520
523
  static get RelationManyToMany(): "Folge viele mit vielen" | "Relation many to many" | "Correlación muchos a muchos" | "Corrélation plusieurs à plusieurs" | "Muitos para muitos relação" | "Correlazione molti a molti";
521
524
  static get RelationType(): "Art der Beziehung" | "Relation type" | "Tipo de relación" | "Type de relation" | "Tipo de relacionamento" | "Tipo di relazione";
522
525
  static get RemoveContextualFilter(): "Kontextbezogenen Filter entfernen" | "Remove contextual filter" | "Eliminar filtro contextual" | "Supprimer le filtre contextuel" | "Remover filtro contextual" | "Rimuovi filtro contestuale";
@@ -980,6 +980,16 @@ export class SDKUI_Localizator {
980
980
  default: return "Chiusa";
981
981
  }
982
982
  }
983
+ static get CloseTask() {
984
+ switch (this._cultureID) {
985
+ case CultureIDs.De_DE: return "Aufgabe schließen";
986
+ case CultureIDs.En_US: return "Close task";
987
+ case CultureIDs.Es_ES: return "Cerrar tarea";
988
+ case CultureIDs.Fr_FR: return "Fermer la tâche";
989
+ case CultureIDs.Pt_PT: return "Fechar tarefa";
990
+ default: return "Chiudi attività";
991
+ }
992
+ }
983
993
  static get Columns_All_Hide() {
984
994
  switch (this._cultureID) {
985
995
  case CultureIDs.De_DE: return "Alle Spalten ausblenden";
@@ -5135,6 +5145,26 @@ export class SDKUI_Localizator {
5135
5145
  }
5136
5146
  ;
5137
5147
  }
5148
+ static get RelationsNotFound() {
5149
+ switch (this._cultureID) {
5150
+ case CultureIDs.De_DE: return "Keine Korrelationen gefunden";
5151
+ case CultureIDs.En_US: return "No correlations found";
5152
+ case CultureIDs.Es_ES: return "No se encontraron correlaciones";
5153
+ case CultureIDs.Fr_FR: return "Aucune corrélation trouvée";
5154
+ case CultureIDs.Pt_PT: return "Nenhuma correlação encontrada";
5155
+ default: return "Nessuna correlazione trovata";
5156
+ }
5157
+ }
5158
+ static get RelatedDcmtsNotFound() {
5159
+ switch (this._cultureID) {
5160
+ case CultureIDs.De_DE: return "Für das ausgewählte Dokument wurden keine zugehörigen Dokumente gefunden.";
5161
+ case CultureIDs.En_US: return "No related documents were found for the selected document.";
5162
+ case CultureIDs.Es_ES: return "No se encontraron documentos relacionados para el documento seleccionado.";
5163
+ case CultureIDs.Fr_FR: return "Aucun document associé n'a été trouvé pour le document sélectionné.";
5164
+ case CultureIDs.Pt_PT: return "Nenhum documento relacionado foi encontrado para o documento selecionado.";
5165
+ default: return "Non sono stati trovati documenti correlati per il documento selezionato.";
5166
+ }
5167
+ }
5138
5168
  static get RelationManyToMany() {
5139
5169
  switch (this._cultureID) {
5140
5170
  case CultureIDs.De_DE: return "Folge viele mit vielen";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.20.0-dev2.47",
3
+ "version": "6.20.0-dev2.49",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",