@topconsultnpm/sdkui-react 6.22.0-dev2.22 → 6.22.0-dev2.24

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.
@@ -386,7 +386,7 @@ const TMDataGrid = React.forwardRef((props, ref) => {
386
386
  // Temporary: iOS-specific styles to prevent default long-press menu and text selection
387
387
  WebkitTouchCallout: 'none',
388
388
  WebkitUserSelect: 'none'
389
- }, children: [dataColumns.map((column, index) => (_jsx(Column, { ...column }, column.caption + index.toString()))), sorting && _jsx(Sorting, { ...sorting }), selection && _jsx(Selection, { ...selection }), scrolling && _jsx(Scrolling, { ...scrolling }), summary && _jsx(Summary, { ...summary }), showHeaderFilter && _jsx(HeaderFilter, { visible: true, ...headerFilter }), rowDragging && _jsx(RowDragging, { ...rowDragging }), filterRow && _jsx(FilterRow, { ...filterRow }), showFilterPanel && _jsx(FilterPanel, { visible: true }), showHeaderColumnChooser && _jsxs(ColumnChooser, { height: "400px", enabled: !showHeaderColumnChooser, mode: "select", children: [_jsx(Position, { my: "center", at: "center", of: window }), _jsx(ColumnChooserSearch, { enabled: true }), _jsx(ColumnChooserSelection, { allowSelectAll: false, selectByClick: true, recursive: true })] }), stateStoring && _jsx(StateStoring, { ...stateStoring }), groupPanel && _jsx(GroupPanel, { ...groupPanel }), _jsx(Grouping, { contextMenuEnabled: true, ...grouping }), _jsx(LoadPanel, { enabled: showLoadPanel }), _jsx(SearchPanel, { visible: showSearchPanel, searchVisibleColumnsOnly: true, highlightSearchText: true }), editing && _jsx(Editing, { ...editing }), paging && _jsx(Paging, { ...paging }), effectivePager && _jsx(Pager, { ...effectivePager, visible: totalRecordCount > pageSize }), masterDetail && _jsx(MasterDetail, { ...masterDetail })] }) }), counterConfig.show && _jsx("div", { style: { width: "100%", height: "25px", display: "flex", alignItems: "center", gap: "15px", backgroundColor: "#e0e0e0" }, children: _jsx(TMCounterContainer, { items: counterValues, bgColorContainer: counterConfig.bgColorContainer, bgColorItem: counterConfig.bgColorItem, hoverColorItem: counterConfig.hoverColorItem, textColorItem: counterConfig.textColorItem }) }), customContextMenuItems && (_jsx(TMContextMenu, { target: ".dx-data-row", items: processCustomContextMenuItems(customContextMenuItems, customContextMenuRowKey), externalControl: {
389
+ }, children: [dataColumns.map((column, index) => (_jsx(Column, { ...column }, column.caption + index.toString()))), sorting && _jsx(Sorting, { ...sorting }), selection && _jsx(Selection, { ...selection }), scrolling && _jsx(Scrolling, { ...scrolling }), summary && _jsx(Summary, { ...summary }), showHeaderFilter && _jsx(HeaderFilter, { visible: true, ...headerFilter, search: { enabled: true, ...headerFilter?.search } }), rowDragging && _jsx(RowDragging, { ...rowDragging }), filterRow && _jsx(FilterRow, { ...filterRow }), showFilterPanel && _jsx(FilterPanel, { visible: true }), showHeaderColumnChooser && _jsxs(ColumnChooser, { height: "400px", enabled: !showHeaderColumnChooser, mode: "select", children: [_jsx(Position, { my: "center", at: "center", of: window }), _jsx(ColumnChooserSearch, { enabled: true }), _jsx(ColumnChooserSelection, { allowSelectAll: false, selectByClick: true, recursive: true })] }), stateStoring && _jsx(StateStoring, { ...stateStoring }), groupPanel && _jsx(GroupPanel, { ...groupPanel }), _jsx(Grouping, { contextMenuEnabled: true, ...grouping }), _jsx(LoadPanel, { enabled: showLoadPanel }), _jsx(SearchPanel, { visible: showSearchPanel, searchVisibleColumnsOnly: true, highlightSearchText: true }), editing && _jsx(Editing, { ...editing }), paging && _jsx(Paging, { ...paging }), effectivePager && _jsx(Pager, { ...effectivePager, visible: totalRecordCount > pageSize }), masterDetail && _jsx(MasterDetail, { ...masterDetail })] }) }), counterConfig.show && _jsx("div", { style: { width: "100%", height: "25px", display: "flex", alignItems: "center", gap: "15px", backgroundColor: "#e0e0e0" }, children: _jsx(TMCounterContainer, { items: counterValues, bgColorContainer: counterConfig.bgColorContainer, bgColorItem: counterConfig.bgColorItem, hoverColorItem: counterConfig.hoverColorItem, textColorItem: counterConfig.textColorItem }) }), customContextMenuItems && (_jsx(TMContextMenu, { target: ".dx-data-row", items: processCustomContextMenuItems(customContextMenuItems, customContextMenuRowKey), externalControl: {
390
390
  visible: customContextMenuVisible,
391
391
  position: customContextMenuPosition,
392
392
  onClose: () => setCustomContextMenuVisible(false)
@@ -13,11 +13,17 @@ const styles = {
13
13
  wrapper: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, borderTop: `1px solid ${TMColors.card_background}` },
14
14
  body: { flex: 1, minHeight: 0, padding: '13px 10px 9px', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(185px, 1fr))', gridAutoRows: 'max-content', alignContent: 'start', columnGap: '10px', rowGap: '13px', overflow: 'auto' },
15
15
  item: { position: 'relative', display: 'flex', alignItems: 'center', minWidth: 0, minHeight: '30px', padding: '5px 10px 4px', borderRadius: '8px', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, transition: 'border-color 0.15s ease, box-shadow 0.15s ease' },
16
- label: { position: 'absolute', top: '-7px', left: '8px', maxWidth: 'calc(100% - 16px)', padding: '0 5px', background: TMColors.default_background, fontSize: '9px', fontWeight: 700, lineHeight: '13px', letterSpacing: '0.5px', textTransform: 'uppercase', color: TMColors.label_normal, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
16
+ // Come per il pulsante copia, la posizione sta sull'ancora del tooltip; il troncamento con i
17
+ // puntini deve invece stare sull'elemento che contiene il testo
18
+ label: { position: 'absolute', top: '-7px', left: '8px', maxWidth: 'calc(100% - 16px)', minWidth: 0, padding: '0 5px', background: TMColors.default_background },
19
+ labelText: { maxWidth: '100%', fontSize: '9px', fontWeight: 700, lineHeight: '13px', letterSpacing: '0.5px', textTransform: 'uppercase', color: TMColors.label_normal, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
17
20
  value: { display: 'block', width: '100%', fontSize: '12.5px', fontWeight: 500, color: TMColors.text_normal, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', userSelect: 'text', cursor: 'text', minHeight: '17px', lineHeight: '17px' },
18
21
  emptyValue: { color: TMColors.disabled, fontWeight: 400 },
19
22
  // Pulsante di copia: pastiglia sul bordo superiore della card, come la caption
20
- copyBtn: { position: 'absolute', top: '-9px', right: '6px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '18px', height: '18px', padding: 0, borderRadius: '50%', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, color: TMColors.label_normal, cursor: 'pointer', transition: 'opacity 0.15s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease' },
23
+ // La posizione sta sull'ancora e non sul pulsante: è il contenitore del tooltip, che deve stare
24
+ // dove si vede il pulsante, altrimenti il tooltip si aprirebbe altrove
25
+ copyBtnAnchor: { position: 'absolute', top: '-9px', right: '6px', transition: 'opacity 0.15s ease' },
26
+ copyBtn: { display: 'flex', alignItems: 'center', justifyContent: 'center', width: '18px', height: '18px', padding: 0, borderRadius: '50%', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, color: TMColors.label_normal, cursor: 'pointer', transition: 'background 0.15s ease, color 0.15s ease, border-color 0.15s ease' },
21
27
  placeholder: { gridColumn: '1 / -1', padding: '6px 2px', color: TMColors.label_normal, fontSize: '12px' },
22
28
  // Barra di ricerca
23
29
  searchBar: { display: 'flex', alignItems: 'center', gap: '8px', padding: '12px 14px 0', flexShrink: 0 },
@@ -58,7 +64,7 @@ const MetadataCard = ({ caption, copyText, children }) => {
58
64
  };
59
65
  // Il pulsante resta visibile durante il feedback e quando ha il focus (accessibilità da tastiera)
60
66
  const isCopyVisible = copyText !== '' && (!SUPPORTS_HOVER || isHovered || isCopied);
61
- return (_jsxs("div", { style: styles.item, onMouseEnter: (e) => { setIsHovered(true); Object.assign(e.currentTarget.style, { borderColor: TMColors.primary, boxShadow: '0 2px 8px rgba(37, 89, 165, 0.14)' }); }, onMouseLeave: (e) => { setIsHovered(false); Object.assign(e.currentTarget.style, { borderColor: TMColors.card_background, boxShadow: 'none' }); }, children: [_jsx("span", { style: { ...styles.label, maxWidth: isCopyVisible ? 'calc(100% - 38px)' : 'calc(100% - 16px)' }, title: caption, children: caption }), children, copyText !== '' && (_jsx("button", { type: 'button', style: { ...styles.copyBtn, ...(isCopied ? COPY_BTN_COPIED : COPY_BTN_IDLE), opacity: isCopyVisible ? 1 : 0, pointerEvents: isCopyVisible ? 'auto' : 'none' }, title: isCopied ? SDKUI_Localizator.CopiedSuccessfully : SDKUI_Localizator.CopyToClipboard, "aria-label": SDKUI_Localizator.CopyToClipboard, onClick: copyAsync, onFocus: () => setIsHovered(true), onBlur: () => setIsHovered(false), ...hoverStyle(isCopied ? COPY_BTN_COPIED : COPY_BTN_HOVER, isCopied ? COPY_BTN_COPIED : COPY_BTN_IDLE), children: isCopied ? _jsx(IconSuccess, { fontSize: 11 }) : _jsx(IconCopy, { fontSize: 11 }) }))] }));
67
+ return (_jsxs("div", { style: styles.item, onMouseEnter: (e) => { setIsHovered(true); Object.assign(e.currentTarget.style, { borderColor: TMColors.primary, boxShadow: '0 2px 8px rgba(37, 89, 165, 0.14)' }); }, onMouseLeave: (e) => { setIsHovered(false); Object.assign(e.currentTarget.style, { borderColor: TMColors.card_background, boxShadow: 'none' }); }, children: [_jsx(TMTooltip, { content: caption, parentStyle: { ...styles.label, maxWidth: isCopyVisible ? 'calc(100% - 38px)' : 'calc(100% - 16px)' }, childStyle: { maxWidth: '100%', minWidth: 0 }, children: _jsx("span", { style: styles.labelText, children: caption }) }), children, copyText !== '' && (_jsx(TMTooltip, { content: isCopied ? SDKUI_Localizator.CopiedSuccessfully : SDKUI_Localizator.CopyToClipboard, parentStyle: { ...styles.copyBtnAnchor, opacity: isCopyVisible ? 1 : 0, pointerEvents: isCopyVisible ? 'auto' : 'none' }, children: _jsx("button", { type: 'button', style: { ...styles.copyBtn, ...(isCopied ? COPY_BTN_COPIED : COPY_BTN_IDLE) }, "aria-label": SDKUI_Localizator.CopyToClipboard, onClick: copyAsync, onFocus: () => setIsHovered(true), onBlur: () => setIsHovered(false), ...hoverStyle(isCopied ? COPY_BTN_COPIED : COPY_BTN_HOVER, isCopied ? COPY_BTN_COPIED : COPY_BTN_IDLE), children: isCopied ? _jsx(IconSuccess, { fontSize: 11 }) : _jsx(IconCopy, { fontSize: 11 }) }) }))] }));
62
68
  };
63
69
  // Riepilogo dei metadati di un documento in card compatte
64
70
  const TMMasterInfoFields = ({ dtd, dcmt, getValuesByMidAsync, reloadToken, isExpandedView = false }) => {
@@ -157,7 +163,7 @@ const TMMasterInfoFields = ({ dtd, dcmt, getValuesByMidAsync, reloadToken, isExp
157
163
  e.preventDefault();
158
164
  e.stopPropagation();
159
165
  setFilter('');
160
- } }, onFocus: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.primary, boxShadow: '0 0 0 2px rgba(37, 89, 165, 0.14)' }), onBlur: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.card_background, boxShadow: 'none' }) }), filter !== '' && (_jsx("button", { type: 'button', style: styles.searchClear, title: SDKUI_Localizator.Clear, "aria-label": SDKUI_Localizator.Clear, onClick: () => setFilter(''), ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: _jsx(IconClearButton, { fontSize: 13 }) }))] }), _jsxs("span", { style: styles.searchCount, children: [filteredRows.length, "/", rows.length] })] }));
166
+ } }, onFocus: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.primary, boxShadow: '0 0 0 2px rgba(37, 89, 165, 0.14)' }), onBlur: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.card_background, boxShadow: 'none' }) }), filter !== '' && (_jsx(TMTooltip, { content: SDKUI_Localizator.Clear, parentStyle: { display: 'flex', flexShrink: 0 }, children: _jsx("button", { type: 'button', style: styles.searchClear, "aria-label": SDKUI_Localizator.Clear, onClick: () => setFilter(''), ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: _jsx(IconClearButton, { fontSize: 13 }) }) }))] }), _jsxs("span", { style: styles.searchCount, children: [filteredRows.length, "/", rows.length] })] }));
161
167
  // La ricerca sta solo nella vista ingrandita: in linea lo spazio è poco e l'elenco è limitato
162
168
  // all'anteprima, con il pulsante per aprirlo tutto
163
169
  const hasSearch = isExpandedView && rows.length > PREVIEW_COUNT;
@@ -1,5 +1,5 @@
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 { LayoutModes } from "@topconsultnpm/sdk-ts";
4
4
  import { LoadIndicator } from "devextreme-react/load-indicator";
5
5
  import { SearchResultContext } from "../../../ts";
@@ -19,7 +19,7 @@ import TMDcmtForm from "./TMDcmtForm";
19
19
  import TMDcmtIcon from "./TMDcmtIcon";
20
20
  import TMMasterInfoFields from "./TMMasterInfoFields";
21
21
  import { MultiDetailArchivePhase, useMultiMasterDetailDcmts } from "../../../hooks/useMultiMasterDetailDcmts";
22
- import { accordionStyles, getDtdDisplayName, INLINE_EDITABLE_CELL_CLASS, multiDetailStyles, PANEL_GUTTER_PX, renderAccordionHeader, renderSelectDcmtType, renderStepper, SECTION_HEADER_HEIGHT_PX, sectionPanelHeights, TMSectionPanelsHeights, TMSummarySectionSlot } from "./TMMultiMasterDetailDcmtsUtils";
22
+ import { accordionStyles, getDtdDisplayName, INLINE_EDITABLE_CELL_CLASS, multiDetailStyles, PANEL_GUTTER_PX, renderAccordionHeader, renderSelectDcmtType, renderStepper, SECTION_HEADER_HEIGHT_PX, sectionPanelHeights, TMSectionPanelsHeights, TMSummaryResizeTracker, TMSummarySectionSlot } from "./TMMultiMasterDetailDcmtsUtils";
23
23
  // Stili delle sezioni in TMMultiMasterDetailDcmtsUtils (vedi accordionStyles e multiDetailStyles)
24
24
  const styles = {
25
25
  content: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 },
@@ -46,7 +46,7 @@ const styles = {
46
46
  const MOBILE_SECTION = 'archiveList';
47
47
  const TMMultiMasterDetailDcmtsForm = (props) => {
48
48
  const { dtd, masterDcmt, onClose, allTasks, getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, showDcmtFormSidebar, showTodoDcmtForm, openFileUploaderPdfEditor, graphometricManager, editPdfForm, openS4TViewer, showToppyDraggableHelpCenter, toppyHelpCenterUsePortal, fetchRemoteCertificates, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onReferenceClick, onTaskCreateRequest, onRefreshAfterAddDcmtToFavs, onFileOpened, passToArchiveCallback, openWGsCopyMoveForm } = props;
49
- const { isLoading, isSummaryReady, phase, setPhase, detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes, selectRelation, backToTypeSelection, isBackLocked, closeModal, sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken, canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken, isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync, archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync, detailColumns, detailSupportsFile, archiveRows, currentRows, focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys, isOpenAddForm, editingRowId, editingRow, formInputMids, openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues, stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync, waitPanel, abortController, } = useMultiMasterDetailDcmts({ dtd, masterDcmt, onClose });
49
+ const { isLoading, isSummaryReady, phase, setPhase, detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes, selectRelation, backToTypeSelection, isBackLocked, closeModal, sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken, hasSavedLayout, setHasSavedLayout, canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken, isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync, archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync, detailColumns, detailSupportsFile, archiveRows, currentRows, focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys, isOpenAddForm, editingRowId, editingRow, formInputMids, openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues, stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync, waitPanel, abortController, } = useMultiMasterDetailDcmts({ dtd, masterDcmt, onClose });
50
50
  const deviceType = useDeviceType();
51
51
  // Su mobile il panel manager mostra un pannello alla volta: le sezioni si raggiungono dalla sua barra
52
52
  const isMobile = useMemo(() => deviceType === DeviceType.MOBILE, [deviceType]);
@@ -56,6 +56,16 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
56
56
  if (isMobile && isSummaryReady)
57
57
  openAllSections();
58
58
  }, [isMobile, isSummaryReady]);
59
+ // Le altezze sono una scelta dell'utente solo da quando sposta un separatore: quelle calcolate all'apertura
60
+ // o al collasso di una sezione non vanno salvate. Con un layout già salvato lo sono comunque
61
+ const hasUserResizedRef = useRef(false);
62
+ useEffect(() => { hasUserResizedRef.current = hasMultiDetailPanelStates(); }, [layoutResetToken]);
63
+ const persistPanelStatesOnUserResize = useCallback((panelStates) => {
64
+ if (!hasUserResizedRef.current)
65
+ return;
66
+ saveMultiDetailPanelStates(panelStates);
67
+ setHasSavedLayout(true);
68
+ }, []);
59
69
  // Ricerca nella griglia dei dettagli da archiviare, come in TMSearch (vedi useDocumentOperations)
60
70
  const [showGridSearch, setShowGridSearch] = useState(false);
61
71
  const toggleGridSearchItem = {
@@ -104,7 +114,7 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
104
114
  allowSorting: false, allowFiltering: false, allowHeaderFiltering: false, allowResizing: false,
105
115
  allowReordering: false, allowHiding: false, showInColumnChooser: false, allowEditing: false,
106
116
  cssClass: 'tm-row-actions',
107
- cellRender: (cell) => (_jsxs("div", { style: styles.actionsCell, children: [_jsx("span", { style: styles.actionIcon, title: SDKUI_Localizator.Update, onClick: () => openEditForm(cell.data), children: _jsx(IconEdit, { fontSize: 18, color: TMColors.primary }) }), _jsx("span", { style: styles.actionIcon, title: SDKUI_Localizator.Duplicate, onClick: () => duplicateRow(cell.data), children: _jsx(IconDuplicate, { fontSize: 18, color: TMColors.tertiary }) }), _jsx("span", { style: styles.actionIcon, title: SDKUI_Localizator.Deletion, onClick: () => deleteRow(cell.data), children: _jsx(IconDelete, { fontSize: 18, color: TMColors.error }) })] })),
117
+ cellRender: (cell) => (_jsxs("div", { style: styles.actionsCell, children: [_jsx(TMTooltip, { content: SDKUI_Localizator.Update, children: _jsx("span", { style: styles.actionIcon, onClick: () => openEditForm(cell.data), children: _jsx(IconEdit, { fontSize: 18, color: TMColors.primary }) }) }), _jsx(TMTooltip, { content: SDKUI_Localizator.Duplicate, children: _jsx("span", { style: styles.actionIcon, onClick: () => duplicateRow(cell.data), children: _jsx(IconDuplicate, { fontSize: 18, color: TMColors.tertiary }) }) }), _jsx(TMTooltip, { content: SDKUI_Localizator.Deletion, children: _jsx("span", { style: styles.actionIcon, onClick: () => deleteRow(cell.data), children: _jsx(IconDelete, { fontSize: 18, color: TMColors.error }) }) })] })),
108
118
  };
109
119
  return [actionsColumn, iconColumn, ...detailColumns, ...(detailSupportsFile ? [fileColumn] : [])];
110
120
  }, [detailColumns, detailSupportsFile]);
@@ -139,22 +149,24 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
139
149
  { name: SDKUI_Localizator.Deletion, icon: _jsx(IconDelete, { fontSize: 18, color: TMColors.error }), operationType: 'singleRow', beginGroup: true, onClick: (id) => deleteRow(archiveRows.find(r => r.id === id)) },
140
150
  toggleGridSearchItem,
141
151
  ], [archiveRows, showGridSearch]);
142
- // Menu delle opzioni del modale, aperto dal pulsante a tre punti nella barra del titolo
143
- const optionsMenuItems = useMemo(() => [
144
- { name: 'Cancella layout', icon: _jsx(IconEraser, { fontSize: 18, color: TMColors.error }), onClick: clearLayout },
145
- ], [clearLayout]);
152
+ // Menu delle opzioni del modale, aperto dal pulsante a tre punti nella barra del titolo.
153
+ // Con il layout di default non c'è niente da cancellare, quindi la voce non compare
154
+ const optionsMenuItems = useMemo(() => hasSavedLayout
155
+ ? [{ name: 'Cancella layout', icon: _jsx(IconEraser, { fontSize: 18, color: TMColors.error }), onClick: clearLayout }]
156
+ : [], [clearLayout, hasSavedLayout]);
146
157
  // Le opzioni riguardano i pannelli del riepilogo: fuori da quella fase non hanno effetto, e su mobile
147
158
  // non c'è un layout da cancellare (i pannelli si vedono uno alla volta, a tutta altezza)
148
159
  const renderOptionsMenu = () => {
149
- if (!isSummaryPhase || isMobile)
160
+ // Senza voci non si mostra il pulsante, per non aprire un menu vuoto
161
+ if (!isSummaryPhase || isMobile || optionsMenuItems.length === 0)
150
162
  return undefined;
151
- return (_jsx(ContextMenu, { trigger: 'left', items: optionsMenuItems, children: _jsx("span", { style: styles.titleMenuIcon, title: SDKUI_Localizator.Options, "aria-label": SDKUI_Localizator.Options, children: _jsx(IconMenuVertical, { fontSize: 16, color: "white" }) }) }));
163
+ return (_jsx(ContextMenu, { trigger: 'left', items: optionsMenuItems, children: _jsx(TMTooltip, { content: SDKUI_Localizator.Options, children: _jsx("span", { style: styles.titleMenuIcon, "aria-label": SDKUI_Localizator.Options, children: _jsx(IconMenuVertical, { fontSize: 16, color: "white" }) }) }) }));
152
164
  };
153
165
  // ----- Sezione: informazioni del master -----------------------------------------------------
154
166
  const renderMasterInfo = (panel) => {
155
167
  const isOpen = sectionsOpen.masterInfo;
156
168
  const editMasterButton = canUpdateMaster
157
- ? (_jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setIsOpenMasterForm(true); }, style: accordionStyles.accordionExpandBtn, title: `${SDKUI_Localizator.Update}: ${getDtdDisplayName(dtd)}`, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: _jsx(IconEdit, { fontSize: 16 }) }))
169
+ ? (_jsx(TMTooltip, { content: `${SDKUI_Localizator.Update}: ${getDtdDisplayName(dtd)}`, children: _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setIsOpenMasterForm(true); }, style: accordionStyles.accordionExpandBtn, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: _jsx(IconEdit, { fontSize: 16 }) }) }))
158
170
  : undefined;
159
171
  return (_jsxs("div", { style: { ...accordionStyles.accordion, ...accordionStyles.accordionInPanel }, children: [renderAccordionHeader({
160
172
  section: 'masterInfo',
@@ -172,7 +184,7 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
172
184
  const isOpen = sectionsOpen.archivedDetails;
173
185
  const hasResults = archivedDetailResults.length > 0 && archivedDetailsCount > 0;
174
186
  // Ricarica i dettagli già archiviati: la sezione va aperta, altrimenti l'esito non sarebbe visibile
175
- const refreshArchivedButton = (_jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setSectionOpen('archivedDetails', true); refreshArchivedDetailsAsync(); }, disabled: isLoadingArchivedDetails, style: { ...accordionStyles.accordionExpandBtn, ...(isLoadingArchivedDetails ? { cursor: 'default', opacity: 0.5 } : {}) }, title: SDKUI_Localizator.Refresh, "aria-label": SDKUI_Localizator.Refresh, ...(isLoadingArchivedDetails ? {} : hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal })), children: _jsx(IconRefresh, { fontSize: 16 }) }));
187
+ const refreshArchivedButton = (_jsx(TMTooltip, { content: SDKUI_Localizator.Refresh, children: _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setSectionOpen('archivedDetails', true); refreshArchivedDetailsAsync(); }, disabled: isLoadingArchivedDetails, style: { ...accordionStyles.accordionExpandBtn, ...(isLoadingArchivedDetails ? { cursor: 'default', opacity: 0.5 } : {}) }, "aria-label": SDKUI_Localizator.Refresh, ...(isLoadingArchivedDetails ? {} : hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal })), children: _jsx(IconRefresh, { fontSize: 16 }) }) }));
176
188
  return (_jsxs("div", { style: { ...accordionStyles.accordion, ...accordionStyles.accordionInPanel }, children: [renderAccordionHeader({
177
189
  section: 'archivedDetails',
178
190
  isOpen,
@@ -245,7 +257,9 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
245
257
  // passaggio fra impilato e un pannello alla volta, i pannelli vanno rimontati
246
258
  , { panels: panels, initialVisibility: initialVisibility, defaultDimensions: initialDimensions, initialDimensions: initialDimensions, initialMobilePanelId: MOBILE_SECTION,
247
259
  // Su mobile il layout è imposto dal dispositivo: salvarlo sovrascriverebbe quello dell'utente
248
- isPersistenceEnabled: !isMobile, persistPanelStates: !isMobile ? saveMultiDetailPanelStates : undefined, children: [_jsx(TMSectionPanelsHeights, { sections: sections, sectionsOpen: sectionsOpen }), _jsx(TMPanelManagerContainer, { panels: panels, direction: 'vertical', showToolbar: isMobile, minPanelSizePx: SECTION_HEADER_HEIGHT_PX, gutterSizePx: isMobile ? undefined : PANEL_GUTTER_PX })] }, `${layoutResetToken}-${isMobile}`) }) }));
260
+ isPersistenceEnabled: !isMobile,
261
+ // Le altezze si salvano solo dopo che l'utente ha spostato un separatore (vedi TMSummaryResizeTracker)
262
+ persistPanelStates: !isMobile ? persistPanelStatesOnUserResize : undefined, children: [_jsx(TMSummaryResizeTracker, { onUserResize: () => { hasUserResizedRef.current = true; } }), _jsx(TMSectionPanelsHeights, { sections: sections, sectionsOpen: sectionsOpen }), _jsx(TMPanelManagerContainer, { panels: panels, direction: 'vertical', showToolbar: isMobile, minPanelSizePx: SECTION_HEADER_HEIGHT_PX, gutterSizePx: isMobile ? undefined : PANEL_GUTTER_PX })] }, `${layoutResetToken}-${isMobile}`) }) }));
249
263
  };
250
264
  const renderArchiveSummary = () => (_jsxs("div", { style: styles.summaryContainer, children: [renderSectionsInPanels(), _jsxs("div", { style: styles.footer, children: [_jsxs("label", { style: styles.footerLabel, ...hoverStyle({ borderColor: TMColors.primary, color: TMColors.text_normal }, { borderColor: TMColors.card_background, color: TMColors.label_normal }), children: [_jsx("input", { type: "checkbox", checked: stopOnFirstError, onChange: (e) => setStopOnFirstError(e.target.checked), style: styles.checkbox }), SDKUI_Localizator.StopOnFirstError] }), _jsxs("button", { type: "button", onClick: archiveAllAsync, disabled: isArchiveDisabled, style: {
251
265
  ...styles.archiveButton,
@@ -211,7 +211,6 @@ export declare const multiDetailStyles: {
211
211
  lineHeight: number;
212
212
  };
213
213
  };
214
- /** Stili delle sezioni collassabili: qui perché la testata comune (renderAccordionHeader) vive in questo file */
215
214
  export declare const accordionStyles: {
216
215
  accordion: {
217
216
  display: "flex";
@@ -251,11 +250,6 @@ export declare const accordionStyles: {
251
250
  display: "inline-flex";
252
251
  alignItems: "center";
253
252
  justifyContent: "center";
254
- minWidth: string;
255
- height: string;
256
- padding: string;
257
- borderRadius: string;
258
- background: string;
259
253
  color: string;
260
254
  fontSize: string;
261
255
  fontWeight: number;
@@ -349,6 +343,15 @@ export declare const TMSummarySectionSlot: ({ section, render, onBeforeMaximize
349
343
  render: (panel: SectionPanelState) => React.ReactNode;
350
344
  onBeforeMaximize?: (section: AccordionSection) => void;
351
345
  }) => import("react/jsx-runtime").JSX.Element;
346
+ /**
347
+ * Segnala che l'utente ha spostato un separatore: è l'unico caso in cui le altezze dei pannelli sono
348
+ * una sua scelta, quindi la sola da salvare. Le quote calcolate all'apertura del riepilogo o al
349
+ * collasso di una sezione non vanno scritte nel local storage.
350
+ * Vive dentro il provider perché il contesto è leggibile solo lì
351
+ */
352
+ export declare const TMSummaryResizeTracker: ({ onUserResize }: {
353
+ onUserResize: () => void;
354
+ }) => null;
352
355
  /**
353
356
  * Tiene allineate le altezze dei pannelli al collasso delle sezioni: il panel manager non
354
357
  * conosce il collasso, quindi è qui che l'apertura di una sezione si traduce nelle nuove quote.
@@ -9,7 +9,11 @@ import TMTooltip from '../../base/TMTooltip';
9
9
  import { DeviceType, useDeviceType } from '../../base/TMDeviceProvider';
10
10
  import { useTMPanelManagerContext } from '../../layout/panelManager/TMPanelManagerContext';
11
11
  // l'elenco da archiviare è l'informazione più importante, quindi prende la fetta maggiore
12
- export const SECTION_WEIGHTS = { masterInfo: 25, archivedDetails: 30, archiveList: 45 };
12
+ export const SECTION_WEIGHTS = {
13
+ masterInfo: 25,
14
+ archivedDetails: 30,
15
+ archiveList: 45
16
+ };
13
17
  export const SECTION_HEADER_HEIGHT_PX = 30;
14
18
  // Spessore dei separatori fra i pannelli
15
19
  export const PANEL_GUTTER_PX = 10;
@@ -128,15 +132,13 @@ export const multiDetailStyles = {
128
132
  typeCardIcon: { width: '34px', height: '34px', borderRadius: '9px', background: TMColors.primary_container, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
129
133
  typeCardName: { flex: 1, fontSize: '14px', fontWeight: 500, color: TMColors.text_normal, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.3 },
130
134
  };
131
- /** Stili delle sezioni collassabili: qui perché la testata comune (renderAccordionHeader) vive in questo file */
135
+ // Stili delle sezioni collassabili
132
136
  export const accordionStyles = {
133
137
  accordion: { display: 'flex', flexDirection: 'column', minHeight: 0, borderRadius: '8px', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, overflow: 'hidden' },
134
- // Sezione dentro un pannello: l'altezza è quella del pannello, che la sezione riempie. Serve `flex`
135
- // e non `height: 100%`, perché il pannello chiuso ha altezza 0 e vive del suo ingombro minimo
136
138
  accordionInPanel: { flex: 1 },
137
139
  accordionHeader: { display: 'flex', alignItems: 'center', gap: '8px', padding: '4px 8px', cursor: 'pointer', userSelect: 'none', transition: 'background 0.15s ease', outline: 'none', flexShrink: 0, minHeight: '28px' },
138
140
  accordionTitle: { flex: 1, fontSize: '12px', fontWeight: 600, color: TMColors.text_normal, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 },
139
- accordionBadge: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: '18px', height: '16px', padding: '0 5px', borderRadius: '8px', background: TMColors.primary_container, color: TMColors.primary, fontSize: '10px', fontWeight: 700, flexShrink: 0 },
141
+ accordionBadge: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: TMColors.primary, fontSize: '13px', fontWeight: 700, flexShrink: 0 },
140
142
  accordionListBody: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', borderTop: `1px solid ${TMColors.card_background}` },
141
143
  accordionListPlaceholder: { display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '16px', color: TMColors.label_normal, fontSize: '13px' },
142
144
  accordionExpandBtn: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '20px', padding: 0, borderRadius: '6px', border: 'none', background: 'transparent', color: TMColors.label_normal, cursor: 'pointer', flexShrink: 0, transition: 'background 0.15s ease, color 0.15s ease' },
@@ -186,12 +188,10 @@ export const renderAccordionHeader = ({ section, isOpen, tooltip, count, title,
186
188
  const canMaximize = panel.canMaximize;
187
189
  const onToggle = () => onToggleOpen(section, !isOpen);
188
190
  const expandTooltip = panel.isMaximized ? SDKUI_Localizator.Minimize : SDKUI_Localizator.ZoomIn;
189
- return (_jsxs("div", { role: canToggle ? 'button' : undefined, tabIndex: canToggle ? 0 : undefined, "aria-expanded": canToggle ? isOpen : undefined, style: { ...accordionStyles.accordionHeader, background: restBackground, ...(canToggle ? {} : { cursor: 'default' }) }, onClick: canToggle ? onToggle : undefined,
190
- // Doppio click sulla testata: ingrandisce e riduce, come nei pannelli del prodotto
191
- onDoubleClick: canMaximize ? () => panel.toggleMaximize() : undefined, onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
191
+ return (_jsxs("div", { role: canToggle ? 'button' : undefined, tabIndex: canToggle ? 0 : undefined, "aria-expanded": canToggle ? isOpen : undefined, style: { ...accordionStyles.accordionHeader, background: restBackground, ...(canToggle ? {} : { cursor: 'default' }) }, onClick: canToggle ? onToggle : undefined, onDoubleClick: canMaximize ? () => panel.toggleMaximize() : undefined, onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
192
192
  e.preventDefault();
193
193
  onToggle();
194
- } } : undefined, title: canToggle ? tooltip : undefined, ...(canToggle ? hoverStyle({ background: TMColors.primary_container }, { background: restBackground }) : {}), children: [_jsxs("span", { style: accordionStyles.accordionTitle, children: [title, _jsx("span", { style: accordionStyles.accordionBadge, children: count })] }), actions, canMaximize && (_jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); panel.toggleMaximize(); }, style: accordionStyles.accordionExpandBtn, title: expandTooltip, "aria-label": expandTooltip, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: panel.isMaximized ? _jsx(IconWindowMinimize, { fontSize: 14 }) : _jsx(IconWindowMaximize, { fontSize: 14 }) })), canToggle && (_jsx(IconChevronRight, { fontSize: 16, color: TMColors.label_normal, style: { transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s ease', flexShrink: 0 } }))] }));
194
+ } } : undefined, ...(canToggle ? hoverStyle({ background: TMColors.primary_container }, { background: restBackground }) : {}), children: [_jsx(TMTooltip, { content: canToggle ? tooltip : undefined, parentStyle: { flex: 1, minWidth: 0 }, childStyle: { maxWidth: '100%' }, children: _jsxs("span", { style: accordionStyles.accordionTitle, children: [title, _jsx("span", { style: accordionStyles.accordionBadge, children: count })] }) }), actions, canMaximize && (_jsx(TMTooltip, { content: expandTooltip, children: _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); panel.toggleMaximize(); }, style: accordionStyles.accordionExpandBtn, "aria-label": expandTooltip, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: panel.isMaximized ? _jsx(IconWindowMinimize, { fontSize: 14 }) : _jsx(IconWindowMaximize, { fontSize: 14 }) }) })), canToggle && (_jsx(IconChevronRight, { fontSize: 16, color: TMColors.label_normal, style: { transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s ease', flexShrink: 0 } }))] }));
195
195
  };
196
196
  /**
197
197
  * Contenuto di un pannello: il contesto del panel manager è leggibile solo dentro l'albero del
@@ -213,6 +213,20 @@ export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
213
213
  };
214
214
  return _jsx(_Fragment, { children: render({ isMaximized, canMaximize: !isMobile, canCollapse: !isMobile, toggleMaximize: toggleMaximizeSection }) });
215
215
  };
216
+ /**
217
+ * Segnala che l'utente ha spostato un separatore: è l'unico caso in cui le altezze dei pannelli sono
218
+ * una sua scelta, quindi la sola da salvare. Le quote calcolate all'apertura del riepilogo o al
219
+ * collasso di una sezione non vanno scritte nel local storage.
220
+ * Vive dentro il provider perché il contesto è leggibile solo lì
221
+ */
222
+ export const TMSummaryResizeTracker = ({ onUserResize }) => {
223
+ const { isResizingActive } = useTMPanelManagerContext();
224
+ useEffect(() => {
225
+ if (isResizingActive)
226
+ onUserResize();
227
+ }, [isResizingActive]);
228
+ return null;
229
+ };
216
230
  /**
217
231
  * Tiene allineate le altezze dei pannelli al collasso delle sezioni: il panel manager non
218
232
  * conosce il collasso, quindi è qui che l'apertura di una sezione si traduce nelle nuove quote.
@@ -236,10 +250,7 @@ export const TMSectionPanelsHeights = ({ sections, sectionsOpen }) => {
236
250
  }
237
251
  if (hasMaximizedPanel)
238
252
  return;
239
- // Solo i pannelli a schermo si spartiscono l'altezza: contando anche i nascosti (su mobile ne
240
- // resta visibile uno solo) al pannello a schermo toccherebbe la sua sola quota nominale,
241
- // lasciando vuoto lo spazio dei pannelli che non ci sono. Chi è fuori schermo resta alle proprie
242
- // quote, che sono quelle da cui il panel manager riparte quando lo si torna a mostrare
253
+ // Solo i pannelli a schermo si spartiscono l'altezza, altrimenti resterebbe vuoto lo spazio dei nascosti
243
254
  const visibleSections = sections.filter(section => panelVisibility[section] !== false);
244
255
  const heights = sectionPanelHeights(visibleSections, sectionsOpen, dimensionsRef.current);
245
256
  visibleSections.forEach(section => setPanelDimensionsById(section, heights[section].width, heights[section].height));
@@ -53,7 +53,6 @@ export const TMPanelManagerProvider = (props) => {
53
53
  // Recursively hide parents
54
54
  const parentsToHide = hideParentRecursively(id, hierarchyMap, updatedVisibility);
55
55
  parentsToHide.forEach(([pid, visible]) => { updatedVisibility[pid] = visible; });
56
- 4;
57
56
  // Redistribute dimensions after hiding
58
57
  setPanelDimensions(prev => redistributeDimensionsOnHide(id, prev, hierarchyMap, updatedVisibility));
59
58
  }
@@ -133,15 +133,18 @@ export type MultiDetailPanelStates = Record<string, MultiDetailPanelState>;
133
133
  export declare class ArchivingSettings {
134
134
  mruTIDs: number[];
135
135
  defaultTree: number;
136
- /** Sezioni collassate dall'utente nel riepilogo, ripristinate alla riapertura */
137
- multiDetailSectionsOpen: MultiDetailSectionsOpen;
136
+ /**
137
+ * Sezioni collassate dall'utente nel riepilogo, ripristinate alla riapertura. Assente finché
138
+ * l'utente non ne chiude una: le impostazioni di default non vanno scritte nel local storage
139
+ */
140
+ multiDetailSectionsOpen?: MultiDetailSectionsOpen;
138
141
  /**
139
142
  * Altezze dei pannelli del riepilogo, gestite dal panel manager. Non comprende il collasso delle
140
- * sezioni, che è in multiDetailSectionsOpen: resta vuoto finché l'utente non sposta un separatore.
143
+ * sezioni, che è in multiDetailSectionsOpen: assente finché l'utente non sposta un separatore.
141
144
  */
142
- multiDetailPanelStates: MultiDetailPanelStates;
145
+ multiDetailPanelStates?: MultiDetailPanelStates;
143
146
  }
144
- /** Stato di apertura salvato delle sezioni: i default coprono le impostazioni scritte prima di questo campo */
147
+ /** Stato di apertura salvato delle sezioni: i default coprono l'impostazione mai modificata dall'utente */
145
148
  export declare const getMultiDetailSectionsOpen: () => MultiDetailSectionsOpen;
146
149
  export declare const saveMultiDetailSectionsOpen: (sectionsOpen: MultiDetailSectionsOpen) => void;
147
150
  /** Stato salvato dei pannelli: vuoto finché l'utente non ne ha ridimensionato nessuno */
@@ -149,6 +152,8 @@ export declare const getMultiDetailPanelStates: () => MultiDetailPanelStates;
149
152
  /** Il panel manager applica lo stato salvato solo se esiste: altrimenti riparte dalle quote di default */
150
153
  export declare const hasMultiDetailPanelStates: () => boolean;
151
154
  export declare const saveMultiDetailPanelStates: (panelStates: MultiDetailPanelStates) => void;
155
+ /** C'è un layout dell'utente da cancellare: nel local storage le due impostazioni ci sono solo se diverse dai default */
156
+ export declare const hasMultiDetailLayout: () => boolean;
152
157
  /**
153
158
  * Elimina dal local storage il layout configurato dall'utente per il riepilogo: altezze dei pannelli
154
159
  * e sezioni collassate. Alla riapertura le sezioni sono tutte espanse con le quote di default.
@@ -198,23 +198,19 @@ export class ArchivingSettings {
198
198
  constructor() {
199
199
  this.mruTIDs = [];
200
200
  this.defaultTree = -1;
201
- /** Sezioni collassate dall'utente nel riepilogo, ripristinate alla riapertura */
202
- this.multiDetailSectionsOpen = { ...DEFAULT_MULTI_DETAIL_SECTIONS_OPEN };
203
- /**
204
- * Altezze dei pannelli del riepilogo, gestite dal panel manager. Non comprende il collasso delle
205
- * sezioni, che è in multiDetailSectionsOpen: resta vuoto finché l'utente non sposta un separatore.
206
- */
207
- this.multiDetailPanelStates = {};
208
201
  }
209
202
  }
210
- /** Stato di apertura salvato delle sezioni: i default coprono le impostazioni scritte prima di questo campo */
203
+ /** Stato di apertura salvato delle sezioni: i default coprono l'impostazione mai modificata dall'utente */
211
204
  export const getMultiDetailSectionsOpen = () => ({
212
205
  ...DEFAULT_MULTI_DETAIL_SECTIONS_OPEN,
213
206
  ...SDKUI_Globals.userSettings.archivingSettings?.multiDetailSectionsOpen,
214
207
  });
215
208
  export const saveMultiDetailSectionsOpen = (sectionsOpen) => {
216
- // Il proxy persiste solo intercettando l'assegnazione di una proprietà: la mappa va riassegnata per intero
217
- SDKUI_Globals.userSettings.archivingSettings.multiDetailSectionsOpen = { ...sectionsOpen };
209
+ const isDefault = Object.keys(DEFAULT_MULTI_DETAIL_SECTIONS_OPEN)
210
+ .every(section => sectionsOpen[section] === DEFAULT_MULTI_DETAIL_SECTIONS_OPEN[section]);
211
+ // Il proxy persiste solo intercettando l'assegnazione di una proprietà: la mappa va riassegnata per intero.
212
+ // Tornata ai default l'impostazione va rimossa, così nel local storage restano solo le modifiche dell'utente
213
+ SDKUI_Globals.userSettings.archivingSettings.multiDetailSectionsOpen = isDefault ? undefined : { ...sectionsOpen };
218
214
  };
219
215
  /** Stato salvato dei pannelli: vuoto finché l'utente non ne ha ridimensionato nessuno */
220
216
  export const getMultiDetailPanelStates = () => SDKUI_Globals.userSettings.archivingSettings?.multiDetailPanelStates ?? {};
@@ -226,14 +222,17 @@ export const saveMultiDetailPanelStates = (panelStates) => {
226
222
  // Il proxy persiste solo intercettando l'assegnazione di una proprietà: la mappa va riassegnata per intero
227
223
  SDKUI_Globals.userSettings.archivingSettings.multiDetailPanelStates = { ...panelStates };
228
224
  };
225
+ /** C'è un layout dell'utente da cancellare: nel local storage le due impostazioni ci sono solo se diverse dai default */
226
+ export const hasMultiDetailLayout = () => hasMultiDetailPanelStates() || SDKUI_Globals.userSettings.archivingSettings?.multiDetailSectionsOpen !== undefined;
229
227
  /**
230
228
  * Elimina dal local storage il layout configurato dall'utente per il riepilogo: altezze dei pannelli
231
229
  * e sezioni collassate. Alla riapertura le sezioni sono tutte espanse con le quote di default.
232
230
  */
233
231
  export const clearMultiDetailLayout = () => {
234
- // Il proxy persiste solo intercettando l'assegnazione di una proprietà: entrambe le mappe vanno riassegnate
235
- SDKUI_Globals.userSettings.archivingSettings.multiDetailPanelStates = {};
236
- SDKUI_Globals.userSettings.archivingSettings.multiDetailSectionsOpen = { ...DEFAULT_MULTI_DETAIL_SECTIONS_OPEN };
232
+ // Il proxy persiste solo intercettando l'assegnazione di una proprietà: entrambi i campi vanno riassegnati.
233
+ // Senza valore spariscono dal local storage e alla riapertura tornano validi i default
234
+ SDKUI_Globals.userSettings.archivingSettings.multiDetailPanelStates = undefined;
235
+ SDKUI_Globals.userSettings.archivingSettings.multiDetailSectionsOpen = undefined;
237
236
  };
238
237
  export class FullTextSettings {
239
238
  constructor() {
@@ -59,6 +59,8 @@ export declare const useMultiMasterDetailDcmts: ({ dtd, masterDcmt, onClose }: U
59
59
  openAllSections: () => void;
60
60
  clearLayout: () => void;
61
61
  layoutResetToken: number;
62
+ hasSavedLayout: boolean;
63
+ setHasSavedLayout: import("react").Dispatch<import("react").SetStateAction<boolean>>;
62
64
  canUpdateMaster: boolean;
63
65
  masterFieldsCount: number;
64
66
  getMasterMetadataAsync: () => Promise<Map<number, MetadataScalarValue>>;
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { ArchiveConstraints, ArchiveEngineByID, MetadataDataDomains, MetadataDataTypes, RelationCacheService, RelationTypes, ResultTypes, SDK_Globals } from '@topconsultnpm/sdk-ts';
3
3
  import { MetadataValueDescriptorEx } from '../ts';
4
- import { clearMultiDetailLayout, Globalization, getExceptionMessage, getMultiDetailSectionsOpen, saveMultiDetailSectionsOpen, SDKUI_Localizator } from '../helper';
4
+ import { clearMultiDetailLayout, Globalization, getExceptionMessage, getMultiDetailSectionsOpen, hasMultiDetailLayout, saveMultiDetailSectionsOpen, SDKUI_Localizator } from '../helper';
5
5
  import { getDcmtMetadataByMid, getUserMetadataSorted, mapAssociationsFromMetadata } from '../helper/dcmtsHelper';
6
6
  import { ButtonNames, TMExceptionBoxManager, TMMessageBoxManager } from '../components/base/TMPopUp';
7
7
  import TMSpinner from '../components/base/TMSpinner';
@@ -54,6 +54,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
54
54
  const [sectionsOpen, setSectionsOpen] = useState(getMultiDetailSectionsOpen());
55
55
  // Cambia a ogni cancellazione del layout, per rimontare i pannelli sulle dimensioni di default
56
56
  const [layoutResetToken, setLayoutResetToken] = useState(0);
57
+ // Layout dell'utente nel local storage: senza non c'è niente da cancellare (è uno stato perché le altezze le salva il panel manager)
58
+ const [hasSavedLayout, setHasSavedLayout] = useState(hasMultiDetailLayout());
57
59
  // Master
58
60
  const [isOpenMasterForm, setIsOpenMasterForm] = useState(false);
59
61
  const [masterInfoReloadToken, setMasterInfoReloadToken] = useState(0);
@@ -222,6 +224,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
222
224
  const next = { ...sectionsOpen, [section]: isOpen };
223
225
  setSectionsOpen(next);
224
226
  saveMultiDetailSectionsOpen(next);
227
+ // Riaperte tutte le sezioni il collasso torna ai default: senza altezze salvate non c'è più layout
228
+ setHasSavedLayout(hasMultiDetailLayout());
225
229
  // I dettagli già archiviati si caricano alla prima apertura della sezione
226
230
  if (section === 'archivedDetails' && isOpen && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
227
231
  refreshArchivedDetailsAsync();
@@ -254,6 +258,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
254
258
  const restored = getMultiDetailSectionsOpen();
255
259
  setSectionsOpen(restored);
256
260
  setLayoutResetToken(prev => prev + 1);
261
+ setHasSavedLayout(false);
257
262
  // Le sezioni riaperte dal ripristino caricano i dettagli già archiviati come alla prima apertura
258
263
  if (restored.archivedDetails && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
259
264
  refreshArchivedDetailsAsync();
@@ -562,6 +567,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
562
567
  selectRelation, backToTypeSelection, isBackLocked, closeModal,
563
568
  // Sezioni
564
569
  sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken,
570
+ hasSavedLayout, setHasSavedLayout,
565
571
  // Master
566
572
  canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken,
567
573
  isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev2.22",
3
+ "version": "6.22.0-dev2.24",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",