@topconsultnpm/sdkui-react 6.22.0-dev2.29 → 6.22.0-dev2.30

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.
@@ -26,13 +26,6 @@ export interface FileItem {
26
26
  version?: number;
27
27
  isSigned?: number;
28
28
  }
29
- /**
30
- * Completa i nomi utente (autore e utente di check-out) di un FileItem quando mancano, risolvendoli dagli ID.
31
- * buildFolderHierarchy popola i nomi solo se riceve la lista dei partecipanti: nei refresh parziali
32
- * (es. dopo la condivisione di un nuovo file) i partecipanti possono non essere disponibili e i nomi
33
- * arrivano vuoti, svuotando la colonna Autore fino al ricaricamento completo.
34
- * Restituisce lo stesso oggetto se non c'è nulla da risolvere, per non invalidare le reference inutilmente.
35
- */
36
29
  export declare const resolveFileItemUserNames: (item: FileItem, users: Array<UserDescriptor>) => FileItem;
37
30
  export interface TMFileManagerTreeViewDirectory {
38
31
  id: number;
@@ -12,13 +12,7 @@ export const isSigned = (ext) => {
12
12
  const parts = normalized.split('.');
13
13
  return parts.some(part => SIGNATURE_EXTENSIONS.has(part));
14
14
  };
15
- /**
16
- * Completa i nomi utente (autore e utente di check-out) di un FileItem quando mancano, risolvendoli dagli ID.
17
- * buildFolderHierarchy popola i nomi solo se riceve la lista dei partecipanti: nei refresh parziali
18
- * (es. dopo la condivisione di un nuovo file) i partecipanti possono non essere disponibili e i nomi
19
- * arrivano vuoti, svuotando la colonna Autore fino al ricaricamento completo.
20
- * Restituisce lo stesso oggetto se non c'è nulla da risolvere, per non invalidare le reference inutilmente.
21
- */
15
+ // Completa i nomi utente (autore e utente di check-out) di un FileItem quando mancano, risolvendoli dagli ID.
22
16
  export const resolveFileItemUserNames = (item, users) => {
23
17
  if (!users || users.length === 0)
24
18
  return item;
@@ -3,7 +3,10 @@ import { HomeBlogPost, TaskDescriptor } from '@topconsultnpm/sdk-ts';
3
3
  interface ITMDcmtBlogProps {
4
4
  tid: number | undefined;
5
5
  did: number | undefined;
6
+ /** Il pannello è aperto. La inietta TMPanelWrapper, non va passata dal chiamante */
6
7
  isVisible?: boolean;
8
+ /** La pagina che contiene il pannello è a video: quelle in secondo piano restano montate e non devono caricare */
9
+ isPageVisible?: boolean;
7
10
  fetchBlogDataTrigger?: number;
8
11
  onRefreshBlogDatagrid?: () => Promise<void>;
9
12
  showFloatingCommentButton?: boolean;
@@ -8,7 +8,7 @@ import TMBlogCommentForm from '../blog/TMBlogCommentForm';
8
8
  import TMBlogsPost from '../../grids/TMBlogsPost';
9
9
  import TMSpinner from '../../base/TMSpinner';
10
10
  import { TMExceptionBoxManager } from '../../base/TMPopUp';
11
- const TMDcmtBlog = ({ tid, did, isVisible, fetchBlogDataTrigger, onRefreshBlogDatagrid, showFloatingCommentButton = true, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
11
+ const TMDcmtBlog = ({ tid, did, isVisible, isPageVisible = true, fetchBlogDataTrigger, onRefreshBlogDatagrid, showFloatingCommentButton = true, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
12
12
  const [blogsDatasource, setBlogsDatasource] = useState([]);
13
13
  const [hasLoadedDataOnce, setHasLoadedDataOnce] = useState(false); //traccia se *qualsiasi* dato è stato caricato per la prima volta
14
14
  const [lastLoadedDid, setLastLoadedDid] = useState(undefined); // `lastLoadedDid` tiene traccia dell'ultimo `did` per cui abbiamo caricato i dati
@@ -47,15 +47,15 @@ const TMDcmtBlog = ({ tid, did, isVisible, fetchBlogDataTrigger, onRefreshBlogDa
47
47
  return;
48
48
  }
49
49
  // Condizione per eseguire il fetch:
50
- // 1. Il pannello è visibile
50
+ // 1. Il pannello è visibile in una pagina a video
51
51
  // 2. E (non abbiamo ancora caricato dati o il `did` è cambiato rispetto all'ultima volta)
52
- const shouldFetch = isVisible && (!hasLoadedDataOnce || did !== lastLoadedDid);
52
+ const shouldFetch = isVisible && isPageVisible && (!hasLoadedDataOnce || did !== lastLoadedDid);
53
53
  // Esegui la chiamata API solo se il pannello è visibile E i dati non sono già stati caricati
54
54
  // O, se vuoi ricaricare ogni volta che diventa visibile (ma è meno efficiente per "pesante")
55
55
  if (shouldFetch) {
56
56
  fetchBlogDataAsync(tid, did);
57
57
  }
58
- }, [tid, did, isVisible, hasLoadedDataOnce, lastLoadedDid]);
58
+ }, [tid, did, isVisible, isPageVisible, hasLoadedDataOnce, lastLoadedDid]);
59
59
  const refreshCallback = async () => {
60
60
  await fetchBlogDataAsync(tid, did);
61
61
  await onRefreshBlogDatagrid?.();
@@ -1504,7 +1504,7 @@ const TMDcmtForm = ({ TID, DID, groupId, layoutMode = LayoutModes.Update, formMo
1504
1504
  for (const key of Object.keys(persistedState)) {
1505
1505
  if (key === 'tmWF') {
1506
1506
  // Hide tmWF
1507
- newState[key] = { ...persistedState[key], visible: false };
1507
+ newState[key] = { ...persistedState[key], visible: false, width: '0%' };
1508
1508
  }
1509
1509
  else if (otherVisiblePanels.includes(key)) {
1510
1510
  // Add extra width to visible panels
@@ -1531,6 +1531,8 @@ const TMDcmtForm = ({ TID, DID, groupId, layoutMode = LayoutModes.Update, formMo
1531
1531
  const persistedState = invocationContext === InvocationContext.Todo ? settings?.layoutToDo : settings?.layout;
1532
1532
  return persistedState?.['tmWF']?.visible ?? false;
1533
1533
  };
1534
+ // Persisted tmWF visibility, read once at mount
1535
+ const persistedWFVisibleRef = useRef(getPersistedWFVisible());
1534
1536
  const onBlogCommentFormCustomSave = useCallback(async (blogPost) => {
1535
1537
  try {
1536
1538
  if (!moreInfoTasks || moreInfoTasks.length === 0) {
@@ -1618,7 +1620,7 @@ const TMDcmtForm = ({ TID, DID, groupId, layoutMode = LayoutModes.Update, formMo
1618
1620
  overflow: 'hidden'
1619
1621
  }, children: [_jsxs("div", { style: { width: '100%', height: '100%', display: isOpenDetails || isOpenMaster ? 'none' : 'flex' }, children: [isNavigating && _jsx(Spinner, { description: SDKUI_Localizator.Loading, flat: false }), (fromDTD) && _jsx(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: useWaitPanelLocalState ? showWaitPanelLocal : showWaitPanel, showWaitPanelPrimary: useWaitPanelLocalState ? showPrimaryLocal : showPrimary, showWaitPanelSecondary: useWaitPanelLocalState ? showSecondaryLocal : showSecondary, waitPanelTitle: useWaitPanelLocalState ? waitPanelTitleLocal : waitPanelTitle, waitPanelTextPrimary: useWaitPanelLocalState ? waitPanelTextPrimaryLocal : waitPanelTextPrimary, waitPanelValuePrimary: useWaitPanelLocalState ? waitPanelValuePrimaryLocal : waitPanelValuePrimary, waitPanelMaxValuePrimary: useWaitPanelLocalState ? waitPanelMaxValuePrimaryLocal : waitPanelMaxValuePrimary, waitPanelTextSecondary: useWaitPanelLocalState ? waitPanelTextSecondaryLocal : waitPanelTextSecondary, waitPanelValueSecondary: useWaitPanelLocalState ? waitPanelValueSecondaryLocal : waitPanelValueSecondary, waitPanelMaxValueSecondary: useWaitPanelLocalState ? waitPanelMaxValueSecondaryLocal : waitPanelMaxValueSecondary, isCancelable: useWaitPanelLocalState ? dcmtFile ? dcmtFile.size >= 1000000 : false : true, abortController: useWaitPanelLocalState ? abortControllerLocal : abortController, children: _jsxs(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: showCicoWaitPanel, showWaitPanelPrimary: showCicoPrimaryProgress, waitPanelTitle: cicoWaitPanelTitle, waitPanelTextPrimary: cicoPrimaryProgressText, waitPanelValuePrimary: cicoPrimaryProgressValue, waitPanelMaxValuePrimary: cicoPrimaryProgressMax, isCancelable: true, abortController: abortControllerLocal, children: [(groupId && groupId.length > 0)
1620
1622
  ? _jsxs(_Fragment, { children: [_jsx(PanelDisabledStateHandler, { isWFDisabled: isWFDisabled, isSysMetadataDisabled: isSysMetadataDisabled, isBoardDisabled: isBoardDisabled, isDcmtTasksDisabled: isDcmtTasksDisabled, isPreviewDisabled: isPreviewDisabled }), _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", parentId: groupId, showToolbar: showDcmtFormSidebar })] })
1621
- : _jsxs(TMPanelManagerWithPersistenceProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: defaultPanelDimensions, initialDimensions: defaultPanelDimensions, initialMobilePanelId: 'tmDcmtForm', isPersistenceEnabled: !isMobile && layoutMode !== LayoutModes.Ark ? hasSavedLayout() : false, persistPanelStates: !isMobile && layoutMode !== LayoutModes.Ark ? (state) => persistPanelStates(state) : undefined, persistedPanelStates: layoutMode !== LayoutModes.Ark ? getPersistedPanelStates() : undefined, children: [_jsx(PanelDisabledStateHandler, { isWFDisabled: isWFDisabled, isSysMetadataDisabled: isSysMetadataDisabled, isBoardDisabled: isBoardDisabled, isDcmtTasksDisabled: isDcmtTasksDisabled, isPreviewDisabled: isPreviewDisabled, persistedWFVisible: getPersistedWFVisible() }), _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", parentId: groupId, showToolbar: showDcmtFormSidebar })] }), isOpenDistinctValues &&
1623
+ : _jsxs(TMPanelManagerWithPersistenceProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: defaultPanelDimensions, initialDimensions: defaultPanelDimensions, initialMobilePanelId: 'tmDcmtForm', isPersistenceEnabled: !isMobile && layoutMode !== LayoutModes.Ark ? hasSavedLayout() : false, persistPanelStates: !isMobile && layoutMode !== LayoutModes.Ark ? (state) => persistPanelStates(state) : undefined, persistedPanelStates: layoutMode !== LayoutModes.Ark ? getPersistedPanelStates() : undefined, children: [_jsx(PanelDisabledStateHandler, { isWFDisabled: isWFDisabled, isSysMetadataDisabled: isSysMetadataDisabled, isBoardDisabled: isBoardDisabled, isDcmtTasksDisabled: isDcmtTasksDisabled, isPreviewDisabled: isPreviewDisabled, persistedWFVisible: persistedWFVisibleRef.current }), _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", parentId: groupId, showToolbar: showDcmtFormSidebar })] }), isOpenDistinctValues &&
1622
1624
  _jsx(TMDistinctValues, { tid: TID, mid: focusedMetadataValue?.mid, isModal: true, showHeader: false, layoutMode: layoutMode, onClosePanelCallback: () => setIsOpenDistinctValues(false), onSelectionChanged: (e) => {
1623
1625
  if (!e)
1624
1626
  return;
@@ -1781,6 +1783,8 @@ const PanelDisabledStateHandler = ({ isWFDisabled, isSysMetadataDisabled, isBoar
1781
1783
  setToolbarButtonDisabled('tmBlog', false);
1782
1784
  }
1783
1785
  }, [isBoardDisabled]);
1786
+ // Tracks whether tmWF has already been restored from the persisted state
1787
+ const didRestoreWF = useRef(false);
1784
1788
  useEffect(() => {
1785
1789
  if (isWFDisabled) {
1786
1790
  setToolbarButtonDisabled('tmWF', true);
@@ -1789,7 +1793,8 @@ const PanelDisabledStateHandler = ({ isWFDisabled, isSysMetadataDisabled, isBoar
1789
1793
  else {
1790
1794
  setToolbarButtonDisabled('tmWF', false);
1791
1795
  // Restore persisted visibility when WF becomes enabled
1792
- if (persistedWFVisible) {
1796
+ if (persistedWFVisible && !didRestoreWF.current) {
1797
+ didRestoreWF.current = true;
1793
1798
  setPanelVisibilityById('tmWF', true);
1794
1799
  }
1795
1800
  }
@@ -50,13 +50,17 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
50
50
  return;
51
51
  }
52
52
  const currentCacheKey = isBasketMode ? `basket-${dcmtData.btid}-${dcmtData.bid}-${dcmtData.bfid}` : `${dcmtData.tid}-${dcmtData.did}`;
53
- const shouldFetch = isVisible && (!hasLoadedDataOnce || currentCacheKey !== lastLoadedDid);
53
+ // Fuori campo non si carica: l'anteprima nativa creata in una pagina non a video resta rotta
54
+ if (!isVisible)
55
+ return;
56
+ // Documento già caricato: ricaricarlo ricreerebbe il visualizzatore senza motivo
57
+ if (hasLoadedDataOnce && currentCacheKey === lastLoadedDid)
58
+ return;
54
59
  if (isDcmtFileInCache(currentCacheKey)) {
55
60
  isBasketMode ? loadBasketFile() : loadDocumentWithCache();
56
61
  setShowPreview(true);
57
- return;
58
62
  }
59
- if (shouldFetch) {
63
+ else {
60
64
  setDcmtBlob(undefined);
61
65
  setError('');
62
66
  setIsAbortError(false);
@@ -71,10 +75,10 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
71
75
  else {
72
76
  setShowPreview(false);
73
77
  }
74
- setHasLoadedDataOnce(true);
75
- setLastLoadedDid(currentCacheKey);
76
78
  }
77
- }, [dcmtData?.did, isVisible, hasLoadedDataOnce, lastLoadedDid]);
79
+ setHasLoadedDataOnce(true);
80
+ setLastLoadedDid(currentCacheKey);
81
+ }, [dcmtData?.tid, dcmtData?.did, isVisible, hasLoadedDataOnce, lastLoadedDid]);
78
82
  const loadBasketFile = async () => {
79
83
  try {
80
84
  // Check cache first
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { LoadIndicator } from 'devextreme-react';
5
5
  import { AppModules, DataColumnTypes, DataListViewModes, DcmtTypeListCacheService, LayoutCacheService, LayoutModes, MetadataDataDomains, SDK_Globals, SystemMIDsAsNumber, UserListCacheService, } from '@topconsultnpm/sdk-ts';
6
- import { customizeSearchByDisplayText, deepCompare, formatDateTimeByMetadataFormat, formatNumberByMetadataFormat, generateUniqueColumnKeys, genUniqueId, getColumnFormatInfo, getDateDisplayText, getNumberDisplayText, getSearchToolbarVisibility, IconAll, IconBoard, IconDcmtTypeSys, IconDelete, IconDuplicate, IconEdit, IconMenuVertical, IconPlatform, IconRefresh, IconSearchCheck, IconShow, isApprovalWorkflowView, isSign4TopEnabled, searchResultDescriptorToSimpleArray, searchResultToMetadataValues, SDKUI_Globals, SDKUI_Localizator, IconCloseOutline } from '../../../helper';
6
+ import { customizeSearchByDisplayText, deepCompare, formatDateTimeByMetadataFormat, formatNumberByMetadataFormat, generateUniqueColumnKeys, genUniqueId, getColumnFormatInfo, getDateDisplayText, getNumberDisplayText, getSearchToolbarVisibility, IconAll, IconBoard, IconDcmtTypeSys, IconDelete, IconDuplicate, IconEdit, IconMenuVertical, IconPlatform, IconRefresh, IconSearchCheck, IconShow, isApprovalWorkflowView, isSign4TopEnabled, searchResultDescriptorToSimpleArray, searchResultToMetadataValues, SDKUI_Globals, SDKUI_Localizator, IconCloseOutline, getSearchResultPanelLayout, getSearchResultPanelLayoutSignature, hasSearchResultPanelLayout, saveSearchResultPanelLayout, subscribeSearchResultPanelLayout } from '../../../helper';
7
7
  import { CounterItemKey } from '../../base/TMCounterContainer';
8
8
  import { getDcmtCicoStatus } from '../../../helper/checkinCheckoutManager';
9
9
  import { DcmtOperationTypes, SearchResultContext, } from '../../../ts';
@@ -23,7 +23,8 @@ import { StyledMultiViewPanel } from '../../base/Styled';
23
23
  import { TMLayoutWaitingContainer } from '../../base/TMWaitPanel';
24
24
  import TMMetadataValues from '../../editors/TMMetadataValues';
25
25
  import TMTidViewer from '../../viewers/TMTidViewer';
26
- import { TMPanelManagerProvider, useTMPanelManagerContext } from '../../layout/panelManager/TMPanelManagerContext';
26
+ import { useTMPanelManagerContext } from '../../layout/panelManager/TMPanelManagerContext';
27
+ import { TMPanelManagerWithPersistenceProvider } from '../../layout/panelManager/TMPanelManagerWithPersistenceProvider';
27
28
  import TMPanelManagerContainer from '../../layout/panelManager/TMPanelManagerContainer';
28
29
  import TMContextMenu from '../../NewComponents/ContextMenu/TMContextMenu';
29
30
  import TMToppyDraggableHelpCenter from '../assistant/TMToppyDraggableHelpCenter';
@@ -51,6 +52,16 @@ const orderByName = (array) => {
51
52
  return 1;
52
53
  } return 0; });
53
54
  };
55
+ //#endregion Helper Methods
56
+ /**
57
+ * Contesti che ricordano il layout dei pannelli, condividendone uno solo.
58
+ * Gli altri sono esclusi di proposito: FREE_SEARCH ha un pannello in più, quindi il suo layout non vale
59
+ * per gli altri, e i restanti mostrano il risultato dentro contenitori con esigenze di spazio loro.
60
+ */
61
+ const PERSISTED_LAYOUT_CONTEXTS = [
62
+ SearchResultContext.METADATA_SEARCH,
63
+ SearchResultContext.WORKFLOW_APPROVE,
64
+ ];
54
65
  const TMSearchResult = ({
55
66
  // Data
56
67
  groupId, searchResults = [], context = SearchResultContext.METADATA_SEARCH, title, selectedSearchResultTID, floatingActionConfig, workingGroupContext = undefined, inputDID,
@@ -794,9 +805,14 @@ handleNavigateToWGs, handleNavigateToDossiers, fetchRemoteCertificates, graphome
794
805
  editPdfForm,
795
806
  showNoDcmtFoundMessage
796
807
  ]);
797
- const tmBlog = useMemo(() => _jsx(TMDcmtBlog, { tid: focusedItem?.TID, did: focusedItem?.DID, fetchBlogDataTrigger: refreshBlogTrigger, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers }), [focusedItem, allTasks, refreshBlogTrigger, handleNavigateToWGs, handleNavigateToDossiers, onRefreshBlogDatagrid]);
808
+ /**
809
+ * Il risultato è davvero a video: le pagine restano montate anche in secondo piano, e senza questo
810
+ * bacheca e anteprima scaricherebbero il documento fuori campo.
811
+ */
812
+ const isSearchResultOnScreen = isVisible && !isOpenDcmtForm && !isOpenDetails && !isOpenMaster;
813
+ const tmBlog = useMemo(() => _jsx(TMDcmtBlog, { tid: focusedItem?.TID, did: focusedItem?.DID, isPageVisible: isSearchResultOnScreen, fetchBlogDataTrigger: refreshBlogTrigger, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers }), [focusedItem, allTasks, refreshBlogTrigger, isSearchResultOnScreen, handleNavigateToWGs, handleNavigateToDossiers, onRefreshBlogDatagrid]);
798
814
  const tmSysMetadata = useMemo(() => _jsx(TMMetadataValues, { layoutMode: LayoutModes.Update, openChooserBySingleClick: true, TID: focusedItem?.TID, isReadOnly: true, deviceType: deviceType, metadataValues: currentMetadataValues.filter(o => (o.mid != undefined && o.mid <= 100)), metadataValuesOrig: currentMetadataValues.filter(o => (o.mid != undefined && o.mid <= 100)), validationItems: [] }), [focusedItem, currentMetadataValues, deviceType]);
799
- const tmDcmtPreview = useMemo(() => _jsx(TMDcmtPreviewWrapper, { refreshPreviewTrigger: refreshPreviewTrigger, currentDcmt: currentDcmt, onBack: backHandlerSecondary }), [currentDcmt, refreshPreviewTrigger, backHandlerSecondary]);
815
+ const tmDcmtPreview = useMemo(() => _jsx(TMDcmtPreviewWrapper, { refreshPreviewTrigger: refreshPreviewTrigger, currentDcmt: currentDcmt, isPageVisible: isSearchResultOnScreen, onBack: backHandlerSecondary }), [currentDcmt, refreshPreviewTrigger, isSearchResultOnScreen, backHandlerSecondary]);
800
816
  // Auto-fetch indexing info when drawer is open and focusedItem changes
801
817
  useEffect(() => {
802
818
  if (!focusedItem || !showIndexingInfo)
@@ -853,6 +869,22 @@ handleNavigateToWGs, handleNavigateToDossiers, fetchRemoteCertificates, graphome
853
869
  }
854
870
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', width: '100%' }, children: [_jsx("div", { style: { padding: '10px', overflow: 'auto', flex: 1 }, children: _jsx("div", { dangerouslySetInnerHTML: { __html: ftExplanation } }) }), _jsxs(StyledIndexingInfoSection, { children: [_jsxs(StyledIndexingToggle, { onClick: handleToggleIndexingInfo, disabled: loadingIndexingInfo, children: [_jsx(StyledLeftContent, { children: _jsx("span", { children: SDKUI_Localizator.IndexingInformation }) }), _jsx(StyledRightContent, { children: _jsx(StyledChevron, { "$isOpen": showIndexingInfo, children: "\u25BC" }) })] }), showIndexingInfo && indexingInfo && (_jsxs(StyledIndexingInfoBox, { children: [_jsx("div", { dangerouslySetInnerHTML: { __html: indexingInfo } }), loadingIndexingInfo && (_jsxs("div", { style: { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: 'rgba(255, 255, 255, 0.9)', padding: '10px', borderRadius: '4px', boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }, children: [SDKUI_Localizator.Loading, "..."] }))] }))] })] }));
855
871
  }, [selectedSearchResult, focusedItem, indexingInfo, showIndexingInfo, loadingIndexingInfo]);
872
+ /**
873
+ * Su mobile i pannelli si mostrano uno per volta, quindi il layout non va ricordato; con groupId
874
+ * i pannelli sono di un panel manager esterno, che ha un layout suo. Senza la barra laterale
875
+ * l'utente non può aprire/chiudere i pannelli: non ha senso applicargli il layout salvato (li
876
+ * troverebbe aperti senza poterli chiudere) né lasciargli sporcare quello condiviso.
877
+ * Il layout salvato vale solo per il login con Surfer: gli altri moduli hanno layout propri.
878
+ */
879
+ const isLayoutPersisted = SDK_Globals.tmSession?.SessionDescr?.appModuleID === AppModules.SURFER && !isMobile && !groupId?.length && showSearchResultSidebar && PERSISTED_LAYOUT_CONTEXTS.includes(context);
880
+ /**
881
+ * Pannelli che questo risultato non può mostrare (la bacheca è l'unico, vedi PanelDisabledStateHandler).
882
+ * Il layout è condiviso con gli altri risultati aperti: qui non vanno aperti quando il layout lo chiede,
883
+ * e il loro stato salvato non va toccato, altrimenti li si chiuderebbe dove invece sono attivi.
884
+ */
885
+ const disabledLayoutPanelIds = isBoardDisabled ? ['tmBlog'] : [];
886
+ /** Bacheca aperta nel layout salvato: da riaprire quando il tipo documento torna a prevederla */
887
+ const savedBlogVisible = isLayoutPersisted && (getSearchResultPanelLayout()['tmBlog']?.visible ?? false);
856
888
  const allInitialPanelVisibility = {
857
889
  'tmSearchResult': true,
858
890
  'tmBlog': false,
@@ -935,6 +967,8 @@ handleNavigateToWGs, handleNavigateToDossiers, fetchRemoteCertificates, graphome
935
967
  toolbarOptions: { icon: _jsx(IconShow, { fontSize: 24 }), visible: getSearchToolbarVisibility(SDK_Globals.tmSession?.SessionDescr?.appModuleID ?? AppModules.SURFER).tmDcmtPreview, orderNumber: context === SearchResultContext.FREE_SEARCH ? 5 : 4, isActive: allInitialPanelVisibility['tmDcmtPreview'] }
936
968
  }
937
969
  ], [tmSearchResult, tmBlog, tmSysMetadata, tmDcmtPreview, tmFullTextSearch, showToolbarHeader, context, isMobile, backHandler, backHandlerSecondary, isClosable, onBack]);
970
+ /** Contenuto del panel manager: con groupId sta nel provider del contenitore, altrimenti in quello qui sotto */
971
+ const panelManagerContent = (_jsxs(_Fragment, { children: [_jsx(PanelDisabledStateHandler, { isBoardDisabled: isBoardDisabled, savedBlogVisible: savedBlogVisible }), isLayoutPersisted && _jsx(SavedLayoutSyncHandler, { disabledPanelIds: disabledLayoutPanelIds }), _jsx(ShowMainPanelBridge, { showMainPanelRef: showMainPanelRef }), _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", parentId: groupId, showToolbar: showSearchResultSidebar })] }));
938
972
  return (_jsxs(StyledMultiViewPanel, { "$isVisible": isVisible, children: [_jsx(StyledMultiViewPanel, { "$isVisible": !isOpenDcmtForm && !isOpenDetails && !isOpenMaster, style: {
939
973
  display: 'flex',
940
974
  flexDirection: isMobile ? 'column' : 'row',
@@ -942,10 +976,10 @@ handleNavigateToWGs, handleNavigateToDossiers, fetchRemoteCertificates, graphome
942
976
  gap: Gutters.getGutters(),
943
977
  width: '100%',
944
978
  height: '100%',
945
- }, children: _jsx(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: showWaitPanel, showWaitPanelPrimary: showPrimary, showWaitPanelSecondary: showSecondary, waitPanelTitle: waitPanelTitle, waitPanelTextPrimary: waitPanelTextPrimary, waitPanelValuePrimary: waitPanelValuePrimary, waitPanelMaxValuePrimary: waitPanelMaxValuePrimary, waitPanelTextSecondary: waitPanelTextSecondary, waitPanelValueSecondary: waitPanelValueSecondary, waitPanelMaxValueSecondary: waitPanelMaxValueSecondary, isCancelable: true, abortController: abortController, children: _jsx(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: showCicoWaitPanel, showWaitPanelPrimary: showCicoPrimaryProgress, waitPanelTitle: cicoWaitPanelTitle, waitPanelTextPrimary: cicoPrimaryProgressText, waitPanelValuePrimary: cicoPrimaryProgressValue, waitPanelMaxValuePrimary: cicoPrimaryProgressMax, isCancelable: true, abortController: abortControllerLocal, children: (groupId && groupId.length > 0) ?
946
- _jsxs(_Fragment, { children: [_jsx(PanelDisabledStateHandler, { isBoardDisabled: isBoardDisabled }), _jsx(ShowMainPanelBridge, { showMainPanelRef: showMainPanelRef }), _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", parentId: groupId, showToolbar: showSearchResultSidebar })] })
947
- :
948
- _jsxs(TMPanelManagerProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: initialPanelDimensions, initialDimensions: initialPanelDimensions, initialMobilePanelId: 'tmSearchResult', children: [_jsx(PanelDisabledStateHandler, { isBoardDisabled: isBoardDisabled }), _jsx(ShowMainPanelBridge, { showMainPanelRef: showMainPanelRef }), _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", parentId: groupId, showToolbar: showSearchResultSidebar })] }) }) }) }), renderDcmtOperations] }));
979
+ }, children: _jsx(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: showWaitPanel, showWaitPanelPrimary: showPrimary, showWaitPanelSecondary: showSecondary, waitPanelTitle: waitPanelTitle, waitPanelTextPrimary: waitPanelTextPrimary, waitPanelValuePrimary: waitPanelValuePrimary, waitPanelMaxValuePrimary: waitPanelMaxValuePrimary, waitPanelTextSecondary: waitPanelTextSecondary, waitPanelValueSecondary: waitPanelValueSecondary, waitPanelMaxValueSecondary: waitPanelMaxValueSecondary, isCancelable: true, abortController: abortController, children: _jsx(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: showCicoWaitPanel, showWaitPanelPrimary: showCicoPrimaryProgress, waitPanelTitle: cicoWaitPanelTitle, waitPanelTextPrimary: cicoPrimaryProgressText, waitPanelValuePrimary: cicoPrimaryProgressValue, waitPanelMaxValuePrimary: cicoPrimaryProgressMax, isCancelable: true, abortController: abortControllerLocal, children: (groupId && groupId.length > 0) ? panelManagerContent
980
+ // Senza le prop di persistenza il provider si comporta come quello base:
981
+ // i contesti che non ricordano il layout restano come prima
982
+ : _jsx(TMPanelManagerWithPersistenceProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: initialPanelDimensions, initialDimensions: initialPanelDimensions, initialMobilePanelId: 'tmSearchResult', isPersistenceEnabled: isLayoutPersisted && hasSearchResultPanelLayout(), persistPanelStates: isLayoutPersisted ? (state) => saveSearchResultPanelLayout(state, disabledLayoutPanelIds) : undefined, persistedPanelStates: isLayoutPersisted ? getSearchResultPanelLayout() : undefined, children: panelManagerContent }) }) }) }), renderDcmtOperations] }));
949
983
  };
950
984
  export default TMSearchResult;
951
985
  /** Cerca una voce di operazione per id, anche dentro i sottomenu */
@@ -1608,24 +1642,70 @@ const ShowMainPanelBridge = ({ showMainPanelRef }) => {
1608
1642
  }, [setPanelVisibilityById, showMainPanelRef]);
1609
1643
  return null;
1610
1644
  };
1611
- const PanelDisabledStateHandler = ({ isBoardDisabled }) => {
1612
- const { setPanelVisibilityById, setToolbarButtonDisabled } = useTMPanelManagerContext();
1645
+ /** Senza UI: allinea i pannelli quando il layout viene salvato da un altro risultato di ricerca già aperto */
1646
+ const SavedLayoutSyncHandler = ({ disabledPanelIds }) => {
1647
+ const { panelVisibility, panelDimensions, maximizedPanels, isResizingActive, setPanelVisibilityById, setPanelDimensionsById } = useTMPanelManagerContext();
1648
+ // Il layout è condiviso fra i risultati aperti: si sottoscrive la firma, che è una stringa, perché
1649
+ // React confronta le letture per riferimento e su un oggetto ri-renderizzerebbe all'infinito
1650
+ const savedLayoutSignature = useSyncExternalStore(subscribeSearchResultPanelLayout, getSearchResultPanelLayoutSignature);
1613
1651
  useEffect(() => {
1614
- if (isBoardDisabled) {
1615
- setToolbarButtonDisabled('tmBlog', true);
1616
- setPanelVisibilityById('tmBlog', false);
1617
- }
1618
- else {
1619
- setToolbarButtonDisabled('tmBlog', false);
1620
- }
1652
+ // Con un pannello massimizzato o un separatore in movimento comanda l'utente di questa istanza
1653
+ if (maximizedPanels.length > 0 || isResizingActive)
1654
+ return;
1655
+ // Al mount panelVisibility è ancora vuota: lo stato iniziale lo applica il provider.
1656
+ // Un pannello disabilitato qui non va aperto, gli altri si allineano comunque
1657
+ const panelsToApply = Object.entries(getSearchResultPanelLayout())
1658
+ .filter(([id, state]) => panelVisibility[id] !== undefined && !(state?.visible && disabledPanelIds.includes(id)));
1659
+ panelsToApply.forEach(([id, state]) => {
1660
+ const isVisible = state?.visible ?? false;
1661
+ if (panelVisibility[id] !== isVisible)
1662
+ setPanelVisibilityById(id, isVisible);
1663
+ });
1664
+ // Le larghezze salvate valgono come pesi, non come percentuali: qui si normalizzano sui soli pannelli
1665
+ // che questo risultato apre davvero, così lo spazio di quelli scartati non resta vuoto
1666
+ const panelsToShow = panelsToApply.filter(([, state]) => state?.visible && parseFloat(state.width) > 0 && parseFloat(state.height) > 0);
1667
+ const weightTotal = panelsToShow.reduce((total, [, state]) => total + parseFloat(state.width), 0);
1668
+ if (weightTotal <= 0)
1669
+ return;
1670
+ // Dopo la visibilità, che accoda un suo ricalcolo delle quote: così restano quelle salvate.
1671
+ // Le quote sono arrotondate, altrimenti due risultati si rincorrono sugli ultimi decimali
1672
+ panelsToShow.forEach(([id, state]) => {
1673
+ const width = `${Math.round(parseFloat(state.width) / weightTotal * 10000) / 100}%`;
1674
+ if (panelDimensions[id]?.width === width && panelDimensions[id]?.height === state.height)
1675
+ return;
1676
+ setPanelDimensionsById(id, width, state.height);
1677
+ });
1678
+ }, [savedLayoutSignature]);
1679
+ return null;
1680
+ };
1681
+ const PanelDisabledStateHandler = ({ isBoardDisabled, savedBlogVisible = false }) => {
1682
+ const { panelVisibility, setPanelVisibilityById, setToolbarButtonDisabled } = useTMPanelManagerContext();
1683
+ const isBlogVisible = panelVisibility['tmBlog'] ?? false;
1684
+ useEffect(() => {
1685
+ setToolbarButtonDisabled('tmBlog', isBoardDisabled);
1621
1686
  }, [isBoardDisabled]);
1687
+ /**
1688
+ * Senza bacheca nel tipo documento il pannello va chiuso ogni volta che ricompare, non solo al cambio
1689
+ * di stato: il layout salvato viene applicato dopo questo effetto e può riportarlo a video.
1690
+ * È un effetto di layout per chiuderlo prima che venga disegnato, altrimenti lampeggia.
1691
+ */
1692
+ useLayoutEffect(() => {
1693
+ if (isBoardDisabled && isBlogVisible)
1694
+ setPanelVisibilityById('tmBlog', false);
1695
+ }, [isBoardDisabled, isBlogVisible]);
1696
+ // Bacheca di nuovo prevista: si riapre come da layout salvato. isBlogVisible non è fra le dipendenze,
1697
+ // altrimenti riaprirebbe il pannello appena chiuso dall'utente
1698
+ useEffect(() => {
1699
+ if (!isBoardDisabled && savedBlogVisible && !isBlogVisible)
1700
+ setPanelVisibilityById('tmBlog', true);
1701
+ }, [isBoardDisabled, savedBlogVisible]);
1622
1702
  return null;
1623
1703
  };
1624
- const TMDcmtPreviewWrapper = ({ refreshPreviewTrigger, currentDcmt, isVisible, onBack }) => {
1704
+ const TMDcmtPreviewWrapper = ({ refreshPreviewTrigger, currentDcmt, isVisible, isPageVisible = true, onBack }) => {
1625
1705
  const { setPanelVisibilityById, toggleMaximize, isResizingActive, countVisibleLeafPanels } = useTMPanelManagerContext();
1626
1706
  const deviceType = useDeviceType();
1627
1707
  const isMobile = deviceType === DeviceType.MOBILE;
1628
- return (_jsx(TMDcmtPreview, { dcmtData: currentDcmt, onClosePanel: (!isMobile && countVisibleLeafPanels() > 1) ? () => setPanelVisibilityById('tmDcmtPreview', false) : undefined, onBack: onBack, allowMaximize: !isMobile && countVisibleLeafPanels() > 1, onMaximizePanel: (!isMobile && countVisibleLeafPanels() > 1) ? () => toggleMaximize("tmDcmtPreview") : undefined, isResizingActive: isResizingActive, isVisible: isVisible }, refreshPreviewTrigger));
1708
+ return (_jsx(TMDcmtPreview, { dcmtData: currentDcmt, onClosePanel: (!isMobile && countVisibleLeafPanels() > 1) ? () => setPanelVisibilityById('tmDcmtPreview', false) : undefined, onBack: onBack, allowMaximize: !isMobile && countVisibleLeafPanels() > 1, onMaximizePanel: (!isMobile && countVisibleLeafPanels() > 1) ? () => toggleMaximize("tmDcmtPreview") : undefined, isResizingActive: isResizingActive, isVisible: isVisible && isPageVisible }, refreshPreviewTrigger));
1629
1709
  };
1630
1710
  // Styled Components
1631
1711
  const StyledPlaceholder = styled.div `
@@ -14,12 +14,14 @@ export const TMPanelManagerProvider = (props) => {
14
14
  const defaultPanelDimensionsRef = useRef(defaultDimensions);
15
15
  // Memoize the panel hierarchy map to avoid re-generating it on every render unless `panels` change
16
16
  const hierarchyMap = useMemo(() => generatePanelHierarchyMap(panels), [panels]);
17
- // State to track the dimensions (width and height) of each panel, initialized with props
18
- const [panelDimensions, setPanelDimensions] = useState(initialDimensions);
17
+ // State to track the visibility and the dimensions (width and height) of each panel, initialized with props
18
+ const [panelLayout, setPanelLayout] = useState({ visibility: {}, dimensions: initialDimensions });
19
+ // Visibility of each panel, keyed by panel ID
20
+ const panelVisibility = panelLayout.visibility;
21
+ // Dimensions of each panel, keyed by panel ID
22
+ const panelDimensions = panelLayout.dimensions;
19
23
  // IDs of maximized panels, including their parents if present
20
24
  const [maximizedPanels, setMaximizedPanels] = useState([]);
21
- // State to track the visibility of each panel, keyed by panel ID
22
- const [panelVisibility, setPanelVisibility] = useState({});
23
25
  // State to track visibility of toolbar buttons, keyed by panel ID
24
26
  const [toolbarButtonsVisibility, setToolbarButtonsVisibility] = useState({});
25
27
  // State to track disabled status of toolbar buttons, keyed by panel ID
@@ -31,93 +33,73 @@ export const TMPanelManagerProvider = (props) => {
31
33
  setToolbarButtonsVisibility(visibilityMap);
32
34
  setToolbarButtonsDisabled(disabledMap);
33
35
  }, []);
34
- // Callback to update the visibility state of a specific panel and its related hierarchy
35
- const adjustPanelVisibilityAndSize = useCallback((id, isVisible, prevVisibility) => {
36
+ // Callback to update the visibility state of a specific panel and its related hierarchy, redistributing the dimensions accordingly
37
+ const adjustPanelVisibilityAndSize = useCallback((id, isVisible, prevLayout) => {
36
38
  // Clone previous visibility state to work with
37
- let updatedVisibility = { ...prevVisibility };
38
- if (isMobile) {
39
- if (isVisible) {
40
- // On mobile, showing one panel hides all others first
41
- updatedVisibility = Object.keys(prevVisibility).reduce((acc, key) => { acc[key] = false; return acc; }, {});
42
- // Recursively determine and show parent panels
43
- const parentsToShow = showParentRecursively(id, hierarchyMap);
44
- parentsToShow.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
45
- // Show the target panel
46
- updatedVisibility[id] = true;
47
- // Redistribute dimensions to account for newly shown panel
48
- setPanelDimensions(prev => redistributeDimensionsOnShow(id, prev, defaultPanelDimensionsRef.current, hierarchyMap, updatedVisibility));
49
- }
50
- else {
51
- // Hide the panel
52
- updatedVisibility[id] = false;
53
- // Recursively hide parents
54
- const parentsToHide = hideParentRecursively(id, hierarchyMap, updatedVisibility);
55
- parentsToHide.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
56
- // Redistribute dimensions after hiding
57
- setPanelDimensions(prev => redistributeDimensionsOnHide(id, prev, hierarchyMap, updatedVisibility));
39
+ let updatedVisibility = { ...prevLayout.visibility };
40
+ let updatedDimensions;
41
+ if (isVisible) {
42
+ // On mobile, showing one panel hides all others first
43
+ if (isMobile) {
44
+ updatedVisibility = Object.keys(prevLayout.visibility).reduce((acc, key) => { acc[key] = false; return acc; }, {});
58
45
  }
46
+ // Recursively determine and show parent panels
47
+ const parentsToShow = showParentRecursively(id, hierarchyMap);
48
+ parentsToShow.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
49
+ // Show the target panel
50
+ updatedVisibility[id] = true;
51
+ // Redistribute dimensions to account for newly shown panel
52
+ updatedDimensions = redistributeDimensionsOnShow(id, prevLayout.dimensions, defaultPanelDimensionsRef.current, hierarchyMap, updatedVisibility);
59
53
  }
60
54
  else {
61
- if (isVisible) {
62
- // On desktop, show panel and its parents without hiding others
63
- const parentsToShow = showParentRecursively(id, hierarchyMap);
64
- parentsToShow.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
65
- // Show the panel
66
- updatedVisibility[id] = true;
67
- // Adjust panel dimensions accordingly
68
- setPanelDimensions(prev => redistributeDimensionsOnShow(id, prev, defaultPanelDimensionsRef.current, hierarchyMap, updatedVisibility));
69
- }
70
- else {
71
- // Hide the panel
72
- updatedVisibility[id] = false;
73
- // Recursively hide parent panels
74
- const parentsToHide = hideParentRecursively(id, hierarchyMap, updatedVisibility);
75
- parentsToHide.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
76
- // Adjust dimensions after hiding
77
- setPanelDimensions(prev => redistributeDimensionsOnHide(id, prev, hierarchyMap, updatedVisibility));
78
- }
55
+ // Hide the panel
56
+ updatedVisibility[id] = false;
57
+ // Recursively hide parents
58
+ const parentsToHide = hideParentRecursively(id, hierarchyMap, updatedVisibility);
59
+ parentsToHide.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
60
+ // Redistribute dimensions after hiding
61
+ updatedDimensions = redistributeDimensionsOnHide(id, prevLayout.dimensions, hierarchyMap, updatedVisibility);
79
62
  }
80
- return updatedVisibility;
63
+ return { visibility: updatedVisibility, dimensions: updatedDimensions };
81
64
  }, [hierarchyMap, isMobile]);
82
65
  // On initial mount: initialize panel visibility using the provided `initialVisibility` config
83
66
  // Each panel's visibility is processed through `updatePanelVisibility` to ensure hierarchy logic is applied
84
67
  useEffect(() => {
85
68
  if (isPersistenceEnabled) {
86
69
  // If persistence is enabled, visibility and dimensions are already synced from persisted state, so just set them directly
87
- setPanelVisibility(initialVisibility);
88
- setPanelDimensions(initialDimensions);
70
+ setPanelLayout({ visibility: initialVisibility, dimensions: initialDimensions });
89
71
  }
90
72
  else {
91
73
  // If persistence is not enabled, recalculate visibility and dimensions based on the initial visibility, applying the hierarchy logic
92
- let updated = { ...initialVisibility };
93
- Object.entries(initialVisibility).forEach(([id, isVisible]) => {
94
- updated = adjustPanelVisibilityAndSize(id, isVisible, updated);
74
+ setPanelLayout(prev => {
75
+ let updated = { visibility: { ...initialVisibility }, dimensions: prev.dimensions };
76
+ Object.entries(initialVisibility).forEach(([id, isVisible]) => {
77
+ updated = adjustPanelVisibilityAndSize(id, isVisible, updated);
78
+ });
79
+ return updated;
95
80
  });
96
- // Update panel visibility state with recalculated values
97
- setPanelVisibility(updated);
98
81
  }
99
82
  }, []);
100
83
  // On mobile devices: automatically show the initial mobile panel when layout switches to mobile
101
84
  // This ensures the correct panel is visible by default on smaller screens
102
85
  useEffect(() => {
103
86
  if (isMobile) {
104
- setPanelVisibility(prev => {
105
- return adjustPanelVisibilityAndSize(initialMobilePanelId, true, prev);
106
- });
87
+ setPanelLayout(prev => adjustPanelVisibilityAndSize(initialMobilePanelId, true, prev));
107
88
  }
108
89
  else {
109
90
  if (isPersistenceEnabled) {
110
91
  // If persistence is enabled, visibility and dimensions are already synced from persisted state, so just set them directly
111
- setPanelVisibility(initialVisibility);
92
+ setPanelLayout(prev => ({ ...prev, visibility: initialVisibility }));
112
93
  }
113
94
  else {
114
95
  // If persistence is not enabled, recalculate visibility and dimensions based on the initial visibility, applying the hierarchy logic
115
- let updated = { ...initialVisibility };
116
- Object.entries(initialVisibility).forEach(([id, isVisible]) => {
117
- updated = adjustPanelVisibilityAndSize(id, isVisible, updated);
96
+ setPanelLayout(prev => {
97
+ let updated = { visibility: { ...initialVisibility }, dimensions: prev.dimensions };
98
+ Object.entries(initialVisibility).forEach(([id, isVisible]) => {
99
+ updated = adjustPanelVisibilityAndSize(id, isVisible, updated);
100
+ });
101
+ return updated;
118
102
  });
119
- // Update panel visibility state with recalculated values
120
- setPanelVisibility(updated);
121
103
  }
122
104
  }
123
105
  }, [isMobile]);
@@ -130,9 +112,9 @@ export const TMPanelManagerProvider = (props) => {
130
112
  // Update state to track which panels are maximized
131
113
  setMaximizedPanels(toMaximize);
132
114
  // Update panel dimensions accordingly
133
- setPanelDimensions(prev => {
115
+ setPanelLayout(prev => {
134
116
  const newDimensions = {};
135
- Object.keys(prev).forEach(pid => {
117
+ Object.keys(prev.dimensions).forEach(pid => {
136
118
  if (toMaximize.includes(pid)) {
137
119
  // For maximized panels (target + parents), set width and height to 100%
138
120
  newDimensions[pid] = { width: '100%', height: '100%' };
@@ -142,31 +124,31 @@ export const TMPanelManagerProvider = (props) => {
142
124
  newDimensions[pid] = { width: '0%', height: '0%' };
143
125
  }
144
126
  });
145
- return newDimensions;
127
+ return { ...prev, dimensions: newDimensions };
146
128
  });
147
129
  }, [hierarchyMap]);
148
130
  // Restore all panels to their original dimensions
149
131
  const resetMaximization = useCallback(() => {
150
132
  // Clear the list of maximized panels to exit maximize mode
151
133
  setMaximizedPanels([]);
152
- // Clone the current visibility state of panels
153
- let updatedVisibility = { ...panelVisibility };
154
134
  // Start with the initial panel dimensions as the base for restoration
155
- let nextDimensions = { ...defaultPanelDimensionsRef.current };
156
- nextDimensions = Object.entries(updatedVisibility).reduce((acc, [id, isVisible]) => {
157
- if (isVisible) {
158
- // If the panel is visible, adjust dimensions to show it properly
159
- acc = redistributeDimensionsOnShow(id, acc, defaultPanelDimensionsRef.current, hierarchyMap, updatedVisibility);
160
- }
161
- else {
162
- // If the panel is hidden, adjust dimensions to hide it
163
- acc = redistributeDimensionsOnHide(id, acc, hierarchyMap, updatedVisibility);
164
- }
165
- return acc;
166
- }, defaultPanelDimensionsRef.current);
167
- // Apply the recalculated dimensions to the panel state
168
- setPanelDimensions(nextDimensions);
169
- }, [panelVisibility, hierarchyMap]);
135
+ setPanelLayout(prev => {
136
+ const updatedVisibility = prev.visibility;
137
+ const nextDimensions = Object.entries(updatedVisibility).reduce((acc, [id, isVisible]) => {
138
+ if (isVisible) {
139
+ // If the panel is visible, adjust dimensions to show it properly
140
+ acc = redistributeDimensionsOnShow(id, acc, defaultPanelDimensionsRef.current, hierarchyMap, updatedVisibility);
141
+ }
142
+ else {
143
+ // If the panel is hidden, adjust dimensions to hide it
144
+ acc = redistributeDimensionsOnHide(id, acc, hierarchyMap, updatedVisibility);
145
+ }
146
+ return acc;
147
+ }, defaultPanelDimensionsRef.current);
148
+ // Apply the recalculated dimensions to the panel state
149
+ return { ...prev, dimensions: nextDimensions };
150
+ });
151
+ }, [hierarchyMap]);
170
152
  // Toggle the maximized state of a panel by its ID
171
153
  const toggleMaximize = useCallback((id) => {
172
154
  if (maximizedPanels.includes(id)) {
@@ -185,17 +167,11 @@ export const TMPanelManagerProvider = (props) => {
185
167
  // If it is maximized, first reset all maximized panels to their original state
186
168
  resetMaximization();
187
169
  // Then update the visibility of the panel, toggling its current state
188
- setPanelVisibility(prev => {
189
- const isCurrentlyVisible = prev[id];
190
- return adjustPanelVisibilityAndSize(id, !isCurrentlyVisible, prev);
191
- });
170
+ setPanelLayout(prev => adjustPanelVisibilityAndSize(id, !prev.visibility[id], prev));
192
171
  }
193
172
  else {
194
173
  // If the panel is not maximized, simply toggle its visibility
195
- setPanelVisibility(prev => {
196
- const isCurrentlyVisible = prev[id];
197
- return adjustPanelVisibilityAndSize(id, !isCurrentlyVisible, prev);
198
- });
174
+ setPanelLayout(prev => adjustPanelVisibilityAndSize(id, !prev.visibility[id], prev));
199
175
  }
200
176
  }, [maximizedPanels, resetMaximization, adjustPanelVisibilityAndSize]);
201
177
  // Sets the visibility of a panel by its ID to a specific value (true = show, false = hide)
@@ -205,14 +181,14 @@ export const TMPanelManagerProvider = (props) => {
205
181
  resetMaximization();
206
182
  }
207
183
  // Then update the visibility state of the panel to the given value
208
- setPanelVisibility(prev => adjustPanelVisibilityAndSize(id, isVisible, prev));
184
+ setPanelLayout(prev => adjustPanelVisibilityAndSize(id, isVisible, prev));
209
185
  }, [maximizedPanels, resetMaximization, adjustPanelVisibilityAndSize]);
210
186
  // Sets the dimensions (width and height) of a specific panel by its ID
211
187
  const setPanelDimensionsById = useCallback((id, width, height) => {
212
188
  // Update the ref holding the initial dimensions
213
189
  defaultPanelDimensionsRef.current = { ...defaultPanelDimensionsRef.current, [id]: { width, height } };
214
190
  // Update the panel dimensions state
215
- setPanelDimensions(prev => ({ ...prev, [id]: { width, height } }));
191
+ setPanelLayout(prev => ({ ...prev, dimensions: { ...prev.dimensions, [id]: { width, height } } }));
216
192
  }, []);
217
193
  // Checks if there is at least one panel currently visible
218
194
  const hasVisiblePanels = useCallback(() => {
@@ -97,18 +97,20 @@ export const redistributeDimensionsOnHide = (hiddenPanelId, currentDimensions, h
97
97
  const hiddenWidthNum = parseFloat(hiddenDims.width);
98
98
  const hiddenHeightNum = parseFloat(hiddenDims.height);
99
99
  const visibleSiblings = siblings.filter(siblingId => panelVisibility[siblingId]);
100
- const widthIncrement = hiddenWidthNum / visibleSiblings.length;
101
- const heightIncrement = hiddenHeightNum / visibleSiblings.length;
102
100
  let newDimensions = { ...currentDimensions };
103
- visibleSiblings.forEach(siblingId => {
104
- const siblingDims = newDimensions[siblingId] || { width: '0%', height: '0%' };
105
- const siblingWidthNum = parseFloat(siblingDims.width) || 0;
106
- const siblingHeightNum = parseFloat(siblingDims.height) || 0;
107
- const newWidthNum = Math.min(siblingWidthNum + widthIncrement, 100);
108
- const newHeightNum = Math.min(siblingHeightNum + heightIncrement, 100);
109
- newDimensions[siblingId] = { width: `${newWidthNum}%`, height: `${newHeightNum}%` };
110
- });
111
- newDimensions[hiddenPanelId] = { width: '0%', height: '0%' };
101
+ if (visibleSiblings.length > 0) {
102
+ const widthIncrement = hiddenWidthNum / visibleSiblings.length;
103
+ const heightIncrement = hiddenHeightNum / visibleSiblings.length;
104
+ visibleSiblings.forEach(siblingId => {
105
+ const siblingDims = newDimensions[siblingId] || { width: '0%', height: '0%' };
106
+ const siblingWidthNum = parseFloat(siblingDims.width) || 0;
107
+ const siblingHeightNum = parseFloat(siblingDims.height) || 0;
108
+ const newWidthNum = Math.min(siblingWidthNum + widthIncrement, 100);
109
+ const newHeightNum = Math.min(siblingHeightNum + heightIncrement, 100);
110
+ newDimensions[siblingId] = { width: `${newWidthNum}%`, height: `${newHeightNum}%` };
111
+ });
112
+ newDimensions[hiddenPanelId] = { width: '0%', height: '0%' };
113
+ }
112
114
  // Ora controllo se il parent non ha più figli visibili e chiamo ricorsivamente
113
115
  if (parentId) {
114
116
  const parentInfo = hierarchyMap.get(parentId);
@@ -98,11 +98,33 @@ export declare class SearchSettings {
98
98
  };
99
99
  relationExpandLevel: number;
100
100
  relationShowZeroDcmts: boolean;
101
+ /** Layout dei pannelli del risultato di ricerca, condiviso dai contesti che lo gestiscono */
102
+ resultPanelLayout: SearchResultPanelStates;
101
103
  /** Ricerche rapide configurate per i valori distinti: una per ogni campo, con chiave "tid_mid" */
102
104
  distinctValuesQuickSearches: {
103
105
  [fieldKey: string]: DistinctValuesQuickSearchSettings;
104
106
  };
105
107
  }
108
+ /** Visibilità e dimensioni dei pannelli, per id pannello */
109
+ export type SearchResultPanelStates = Record<string, {
110
+ visible: boolean;
111
+ width: string;
112
+ height: string;
113
+ }>;
114
+ /** Avvisa i risultati di ricerca già montati quando un altro salva il layout; ritorna la disiscrizione */
115
+ export declare const subscribeSearchResultPanelLayout: (onLayoutChanged: () => void) => (() => void);
116
+ /** Layout salvato; vuoto se non ne è stato salvato nessuno (anche per le impostazioni di versioni precedenti) */
117
+ export declare const getSearchResultPanelLayout: () => SearchResultPanelStates;
118
+ /** Il panel manager applica il layout salvato solo se esiste, altrimenti riparte dalle quote di default */
119
+ export declare const hasSearchResultPanelLayout: () => boolean;
120
+ /** Firma del layout salvato: un primitivo confrontabile, come richiede useSyncExternalStore */
121
+ export declare const getSearchResultPanelLayoutSignature: () => string;
122
+ /**
123
+ * @param disabledPanelIds Pannelli che il risultato chiamante non può mostrare: il loro stato resta quello
124
+ * già salvato, perché sono chiusi per via del tipo documento e non per scelta
125
+ * dell'utente. Sovrascriverlo li chiuderebbe anche dove sono attivi.
126
+ */
127
+ export declare const saveSearchResultPanelLayout: (panelStates: SearchResultPanelStates, disabledPanelIds?: Array<string>) => void;
106
128
  /** Ricerca rapida configurata per il campo, se presente */
107
129
  export declare const getDistinctValuesQuickSearch: (tid: number | undefined, mid: number | undefined) => DistinctValuesQuickSearchSettings | undefined;
108
130
  export declare const saveDistinctValuesQuickSearch: (tid: number | undefined, mid: number | undefined, settings: DistinctValuesQuickSearchSettings) => void;
@@ -158,10 +158,48 @@ export class SearchSettings {
158
158
  this.panelLayout = {};
159
159
  this.relationExpandLevel = 4; // Livello di espansione predefinito per le correlazioni
160
160
  this.relationShowZeroDcmts = false;
161
+ /** Layout dei pannelli del risultato di ricerca, condiviso dai contesti che lo gestiscono */
162
+ this.resultPanelLayout = {};
161
163
  /** Ricerche rapide configurate per i valori distinti: una per ogni campo, con chiave "tid_mid" */
162
164
  this.distinctValuesQuickSearches = {};
163
165
  }
164
166
  }
167
+ const resultPanelLayoutSubscribers = new Set();
168
+ /** Avvisa i risultati di ricerca già montati quando un altro salva il layout; ritorna la disiscrizione */
169
+ export const subscribeSearchResultPanelLayout = (onLayoutChanged) => {
170
+ resultPanelLayoutSubscribers.add(onLayoutChanged);
171
+ return () => { resultPanelLayoutSubscribers.delete(onLayoutChanged); };
172
+ };
173
+ /** Layout salvato; vuoto se non ne è stato salvato nessuno (anche per le impostazioni di versioni precedenti) */
174
+ export const getSearchResultPanelLayout = () => SDKUI_Globals.userSettings.searchSettings?.resultPanelLayout ?? {};
175
+ /** Il panel manager applica il layout salvato solo se esiste, altrimenti riparte dalle quote di default */
176
+ export const hasSearchResultPanelLayout = () => Object.keys(getSearchResultPanelLayout()).length > 0;
177
+ /** Riassume un layout in una stringa: ordinata per id, così due layout uguali danno la stessa firma */
178
+ const searchResultPanelLayoutSignature = (panelStates) => Object.entries(panelStates)
179
+ .sort(([firstId], [secondId]) => firstId.localeCompare(secondId))
180
+ .map(([id, state]) => `${id}:${state?.visible ? 1 : 0}:${state?.width ?? ''}:${state?.height ?? ''}`)
181
+ .join('|');
182
+ /** Firma del layout salvato: un primitivo confrontabile, come richiede useSyncExternalStore */
183
+ export const getSearchResultPanelLayoutSignature = () => searchResultPanelLayoutSignature(getSearchResultPanelLayout());
184
+ /**
185
+ * @param disabledPanelIds Pannelli che il risultato chiamante non può mostrare: il loro stato resta quello
186
+ * già salvato, perché sono chiusi per via del tipo documento e non per scelta
187
+ * dell'utente. Sovrascriverlo li chiuderebbe anche dove sono attivi.
188
+ */
189
+ export const saveSearchResultPanelLayout = (panelStates, disabledPanelIds = []) => {
190
+ if (!panelStates || Object.keys(panelStates).length === 0)
191
+ return;
192
+ const savedLayout = getSearchResultPanelLayout();
193
+ const layout = { ...panelStates };
194
+ disabledPanelIds.forEach(id => { if (savedLayout[id])
195
+ layout[id] = { ...savedLayout[id] }; });
196
+ // Layout identico: senza questo controllo ogni scrittura avviserebbe a vuoto gli altri risultati aperti
197
+ if (searchResultPanelLayoutSignature(layout) === getSearchResultPanelLayoutSignature())
198
+ return;
199
+ // Il proxy persiste solo intercettando l'assegnazione di una proprietà: va riassegnata l'intera mappa
200
+ SDKUI_Globals.userSettings.searchSettings.resultPanelLayout = layout;
201
+ resultPanelLayoutSubscribers.forEach(onLayoutChanged => onLayoutChanged());
202
+ };
165
203
  /** Il campo da cui viene aperto il pannello dei valori distinti è identificato dalla coppia (tid, mid) */
166
204
  const distinctValuesFieldKey = (tid, mid) => `${tid}_${mid}`;
167
205
  /** Ricerca rapida configurata per il campo, se presente */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev2.29",
3
+ "version": "6.22.0-dev2.30",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",