@topconsultnpm/sdkui-react 6.22.0-dev2.20 → 6.22.0-dev2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +35 -14
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +13 -5
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +44 -17
- package/lib/helper/SDKUI_Localizator.d.ts +1 -0
- package/lib/helper/SDKUI_Localizator.js +10 -0
- package/lib/hooks/useMultiMasterDetailDcmts.d.ts +1 -0
- package/lib/hooks/useMultiMasterDetailDcmts.js +32 -8
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, 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";
|
|
6
|
-
import { getFullFileExtension, getMultiDetailPanelStates, hasMultiDetailPanelStates, hoverStyle, IconArchive, IconDelete, IconDuplicate, IconEdit, IconEraser, IconMenuVertical, IconRefresh, IconSearch, IconSearchCheck, saveMultiDetailPanelStates, SDKUI_Localizator } from "../../../helper";
|
|
6
|
+
import { getFullFileExtension, getMultiDetailPanelStates, hasMultiDetailPanelStates, hoverStyle, IconArchive, IconDelete, IconDuplicate, IconEdit, IconEraser, IconInfo, IconMenuVertical, IconRefresh, IconSearch, IconSearchCheck, saveMultiDetailPanelStates, SDKUI_Localizator } from "../../../helper";
|
|
7
7
|
import { TMColors } from "../../../utils/theme";
|
|
8
|
+
import { DeviceType, useDeviceType } from "../../base/TMDeviceProvider";
|
|
8
9
|
import { TMLayoutWaitingContainer } from "../../base/TMWaitPanel";
|
|
9
10
|
import TMModal from "../../base/TMModal";
|
|
10
11
|
import TMDataGrid from "../../base/TMDataGrid";
|
|
@@ -41,9 +42,20 @@ const styles = {
|
|
|
41
42
|
archiveButton: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', height: '38px', padding: '0 22px', border: 'none', borderRadius: '10px', fontSize: '14px', fontWeight: 600, letterSpacing: '0.2px', color: '#ffffff', cursor: 'pointer', transition: 'transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease' },
|
|
42
43
|
checkbox: { width: '16px', height: '16px', accentColor: TMColors.primary, cursor: 'pointer', margin: 0 },
|
|
43
44
|
};
|
|
45
|
+
/** Sezione mostrata per prima su mobile, dove i pannelli si vedono uno alla volta */
|
|
46
|
+
const MOBILE_SECTION = 'archiveList';
|
|
44
47
|
const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
45
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;
|
|
46
|
-
const { isLoading, isSummaryReady, phase, setPhase, detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes, selectRelation, backToTypeSelection, isBackLocked, closeModal, sectionsOpen, setSectionOpen, 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, 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
|
+
const deviceType = useDeviceType();
|
|
51
|
+
// Su mobile il panel manager mostra un pannello alla volta: le sezioni si raggiungono dalla sua barra
|
|
52
|
+
const isMobile = useMemo(() => deviceType === DeviceType.MOBILE, [deviceType]);
|
|
53
|
+
// Un pannello alla volta non lascia spazio da cedere: le sezioni chiuse nella vista impilata si
|
|
54
|
+
// aprono qui, altrimenti il pannello a schermo mostrerebbe la sola testata
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (isMobile && isSummaryReady)
|
|
57
|
+
openAllSections();
|
|
58
|
+
}, [isMobile, isSummaryReady]);
|
|
47
59
|
// Ricerca nella griglia dei dettagli da archiviare, come in TMSearch (vedi useDocumentOperations)
|
|
48
60
|
const [showGridSearch, setShowGridSearch] = useState(false);
|
|
49
61
|
const toggleGridSearchItem = {
|
|
@@ -131,9 +143,10 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
131
143
|
const optionsMenuItems = useMemo(() => [
|
|
132
144
|
{ name: 'Cancella layout', icon: _jsx(IconEraser, { fontSize: 18, color: TMColors.error }), onClick: clearLayout },
|
|
133
145
|
], [clearLayout]);
|
|
134
|
-
// Le opzioni riguardano i pannelli del riepilogo: fuori da quella fase non hanno effetto
|
|
146
|
+
// Le opzioni riguardano i pannelli del riepilogo: fuori da quella fase non hanno effetto, e su mobile
|
|
147
|
+
// non c'è un layout da cancellare (i pannelli si vedono uno alla volta, a tutta altezza)
|
|
135
148
|
const renderOptionsMenu = () => {
|
|
136
|
-
if (!isSummaryPhase)
|
|
149
|
+
if (!isSummaryPhase || isMobile)
|
|
137
150
|
return undefined;
|
|
138
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" }) }) }));
|
|
139
152
|
};
|
|
@@ -191,13 +204,13 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
191
204
|
// ----- Pannelli del riepilogo, uno per sezione ----------------------------------------------
|
|
192
205
|
const renderSectionsInPanels = () => {
|
|
193
206
|
const definitions = [
|
|
194
|
-
{ section: 'masterInfo', name: `Master: ${getDtdDisplayName(dtd)}`, render: renderMasterInfo },
|
|
207
|
+
{ section: 'masterInfo', name: `Master: ${getDtdDisplayName(dtd)}`, icon: _jsx(IconInfo, { fontSize: 22 }), count: masterFieldsCount, render: renderMasterInfo },
|
|
195
208
|
...(selectedRelation?.detailTID !== undefined
|
|
196
|
-
? [{ section: 'archivedDetails', name: SDKUI_Localizator.ArchivedDetails, render: renderArchivedDetails }]
|
|
209
|
+
? [{ section: 'archivedDetails', name: SDKUI_Localizator.ArchivedDetails, icon: _jsx(IconSearchCheck, { fontSize: 22 }), count: archivedDetailsCount, render: renderArchivedDetails }]
|
|
197
210
|
: []),
|
|
198
|
-
{ section: 'archiveList', name: SDKUI_Localizator.DetailsToArchive, render: renderArchiveList },
|
|
211
|
+
{ section: 'archiveList', name: SDKUI_Localizator.DetailsToArchive, icon: _jsx(IconArchive, { fontSize: 22 }), count: currentRows.length, render: renderArchiveList },
|
|
199
212
|
];
|
|
200
|
-
const panels = definitions.map(definition => ({
|
|
213
|
+
const panels = definitions.map((definition, index) => ({
|
|
201
214
|
id: definition.section,
|
|
202
215
|
name: definition.name,
|
|
203
216
|
currentGroupDirection: 'vertical',
|
|
@@ -209,6 +222,8 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
209
222
|
// La sezione collassata è alta quanto la sua testata: i separatori non la ingrandiscono
|
|
210
223
|
isCollapsed: !sectionsOpen[definition.section],
|
|
211
224
|
},
|
|
225
|
+
// Barra dei pannelli: su mobile è l'unico modo per passare da una sezione all'altra
|
|
226
|
+
toolbarOptions: { icon: definition.icon, visible: true, isActive: true, orderNumber: index + 1, count: definition.count, tooltip: definition.name },
|
|
212
227
|
}));
|
|
213
228
|
const sections = panels.map(panel => panel.id);
|
|
214
229
|
// Le altezze salvate sono il punto di partenza, ma è il collasso corrente a decidere le quote
|
|
@@ -220,13 +235,19 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
220
235
|
return acc;
|
|
221
236
|
}, {})
|
|
222
237
|
: undefined;
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
// Con un pannello alla volta non ci sono quote da spartire: quello a schermo prende tutta l'altezza.
|
|
239
|
+
// Le quote proporzionali (e il layout salvato) valgono solo per i pannelli impilati
|
|
240
|
+
const fullHeights = sections.reduce((acc, section) => { acc[section] = { width: '100%', height: '100%' }; return acc; }, {});
|
|
241
|
+
const initialDimensions = isMobile ? fullHeights : sectionPanelHeights(sections, sectionsOpen, savedDimensions);
|
|
242
|
+
const initialVisibility = sections.reduce((acc, section) => { acc[section] = isMobile ? section === MOBILE_SECTION : true; return acc; }, {});
|
|
225
243
|
return (_jsx("div", { style: styles.panelsHost, children: _jsx("div", { style: styles.panelsHostFill, children: _jsxs(TMPanelManagerWithPersistenceProvider
|
|
226
|
-
// Le dimensioni sono lette solo al montaggio: dopo la cancellazione del layout
|
|
227
|
-
|
|
244
|
+
// Le dimensioni sono lette solo al montaggio: dopo la cancellazione del layout, e al
|
|
245
|
+
// passaggio fra impilato e un pannello alla volta, i pannelli vanno rimontati
|
|
246
|
+
, { panels: panels, initialVisibility: initialVisibility, defaultDimensions: initialDimensions, initialDimensions: initialDimensions, initialMobilePanelId: MOBILE_SECTION,
|
|
247
|
+
// 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}`) }) }));
|
|
228
249
|
};
|
|
229
|
-
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 }),
|
|
250
|
+
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: {
|
|
230
251
|
...styles.archiveButton,
|
|
231
252
|
background: isArchiveDisabled ? TMColors.disabled : TMColors.success,
|
|
232
253
|
boxShadow: isArchiveDisabled ? 'none' : `0 4px 14px ${TMColors.success}55`,
|
|
@@ -19,6 +19,8 @@ export type AccordionSection = 'masterInfo' | 'archivedDetails' | 'archiveList';
|
|
|
19
19
|
*/
|
|
20
20
|
export type SectionPanelState = {
|
|
21
21
|
isMaximized: boolean;
|
|
22
|
+
canMaximize: boolean;
|
|
23
|
+
canCollapse: boolean;
|
|
22
24
|
toggleMaximize: () => void;
|
|
23
25
|
};
|
|
24
26
|
/** Valore di un metadato letto da un documento: una cella del risultato di ricerca */
|
|
@@ -44,13 +46,14 @@ export interface ArchiveRow {
|
|
|
44
46
|
* stringhe, quindi il `value` generico di DevExtreme si restringe
|
|
45
47
|
*/
|
|
46
48
|
export type ArchiveRowCell = Omit<DataGridTypes.ColumnCellTemplateData<ArchiveRow, number>, 'value'> & {
|
|
47
|
-
readonly value?: string;
|
|
49
|
+
readonly value?: string | number;
|
|
48
50
|
};
|
|
49
51
|
/**
|
|
50
52
|
* Modifiche di una riga in arrivo dalla griglia (mid -> nuovo valore): in modifica diretta ci sono
|
|
51
|
-
*
|
|
53
|
+
* i metadati testuali e numerici (vedi isInlineEditableMetadata), quindi stringhe e numeri,
|
|
54
|
+
* e l'editor svuotato restituisce null
|
|
52
55
|
*/
|
|
53
|
-
export type ArchiveRowChanges = Record<string, string | null | undefined>;
|
|
56
|
+
export type ArchiveRowChanges = Record<string, string | number | null | undefined>;
|
|
54
57
|
/** Aggiornamento di una riga della griglia, con le sole modifiche possibili in linea */
|
|
55
58
|
export type ArchiveRowUpdatingEvent = Omit<DataGridTypes.RowUpdatingEvent<ArchiveRow, number>, 'newData'> & {
|
|
56
59
|
newData: ArchiveRowChanges;
|
|
@@ -68,10 +71,15 @@ export declare const canArchive: (dtd?: DcmtTypeDescriptor) => boolean;
|
|
|
68
71
|
*/
|
|
69
72
|
export declare const INLINE_EDITABLE_CELL_CLASS = "tm-cell-inline-editable";
|
|
70
73
|
/**
|
|
71
|
-
* Modifica diretta in griglia:
|
|
72
|
-
* hanno editor e vincoli propri, date
|
|
74
|
+
* Modifica diretta in griglia: testo libero e numeri. Liste dati, calcolati, numeratori e speciali
|
|
75
|
+
* hanno editor e vincoli propri, le date una formattazione dedicata: restano al form
|
|
73
76
|
*/
|
|
74
77
|
export declare const isInlineEditableMetadata: (md: MetadataDescriptor) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Valore di una cella numerica: l'editor della griglia vuole un numero, non la stringa che
|
|
80
|
+
* la riga porta in `__values` per l'archiviazione. Vuoto o non numerico: cella vuota
|
|
81
|
+
*/
|
|
82
|
+
export declare const toGridNumber: (value: unknown) => number | undefined;
|
|
75
83
|
/** Colonne della griglia di riepilogo dai metadati (utente) del tipo documento */
|
|
76
84
|
export declare const buildColumnsFromDtd: (dtd: DcmtTypeDescriptor | undefined, renderDataListCell: (value: string | Date | number | undefined, dataListID: number, viewMode: DataListViewModes) => React.ReactElement) => Array<IColumnProps>;
|
|
77
85
|
export declare const getDetailDtdAsync: (detailTID: number | undefined) => Promise<DcmtTypeDescriptor | undefined>;
|
|
@@ -6,6 +6,7 @@ import { formatScalarValue, getUserMetadataSorted } from '../../../helper/dcmtsH
|
|
|
6
6
|
import { TMColors } from '../../../utils/theme';
|
|
7
7
|
import { TMDcmtTypeIcon } from '../../viewers/TMTidViewer';
|
|
8
8
|
import TMTooltip from '../../base/TMTooltip';
|
|
9
|
+
import { DeviceType, useDeviceType } from '../../base/TMDeviceProvider';
|
|
9
10
|
import { useTMPanelManagerContext } from '../../layout/panelManager/TMPanelManagerContext';
|
|
10
11
|
// l'elenco da archiviare è l'informazione più importante, quindi prende la fetta maggiore
|
|
11
12
|
export const SECTION_WEIGHTS = { masterInfo: 25, archivedDetails: 30, archiveList: 45 };
|
|
@@ -26,12 +27,22 @@ export const canArchive = (dtd) => dtd?.perm?.canArchive === AccessLevelsEx.Yes
|
|
|
26
27
|
*/
|
|
27
28
|
export const INLINE_EDITABLE_CELL_CLASS = 'tm-cell-inline-editable';
|
|
28
29
|
/**
|
|
29
|
-
* Modifica diretta in griglia:
|
|
30
|
-
* hanno editor e vincoli propri, date
|
|
30
|
+
* Modifica diretta in griglia: testo libero e numeri. Liste dati, calcolati, numeratori e speciali
|
|
31
|
+
* hanno editor e vincoli propri, le date una formattazione dedicata: restano al form
|
|
31
32
|
*/
|
|
32
|
-
export const isInlineEditableMetadata = (md) => md.dataType === MetadataDataTypes.Varchar
|
|
33
|
+
export const isInlineEditableMetadata = (md) => (md.dataType === MetadataDataTypes.Varchar || md.dataType === MetadataDataTypes.Number)
|
|
33
34
|
&& (md.dataDomain === undefined || md.dataDomain === MetadataDataDomains.None)
|
|
34
35
|
&& md.isSystem !== 1 && md.isSystemDerived !== 1;
|
|
36
|
+
/**
|
|
37
|
+
* Valore di una cella numerica: l'editor della griglia vuole un numero, non la stringa che
|
|
38
|
+
* la riga porta in `__values` per l'archiviazione. Vuoto o non numerico: cella vuota
|
|
39
|
+
*/
|
|
40
|
+
export const toGridNumber = (value) => {
|
|
41
|
+
if (value === undefined || value === null || value === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
45
|
+
};
|
|
35
46
|
/** Colonne della griglia di riepilogo dai metadati (utente) del tipo documento */
|
|
36
47
|
export const buildColumnsFromDtd = (dtd, renderDataListCell) => getUserMetadataSorted(dtd).map(md => {
|
|
37
48
|
const dataListID = md.dataListID ?? 0;
|
|
@@ -40,21 +51,25 @@ export const buildColumnsFromDtd = (dtd, renderDataListCell) => getUserMetadataS
|
|
|
40
51
|
const dataType = md.dataType;
|
|
41
52
|
const format = md.format?.format;
|
|
42
53
|
const formatCulture = md.format?.formatCulture;
|
|
43
|
-
// Date e numeri usano la formattazione scalare; le liste dati il render comune
|
|
44
|
-
const needsScalarFormat = !isDataList && (dataType === MetadataDataTypes.DateTime || dataType === MetadataDataTypes.Number);
|
|
45
54
|
const isInlineEditable = isInlineEditableMetadata(md);
|
|
55
|
+
const isNumber = dataType === MetadataDataTypes.Number;
|
|
56
|
+
// Date e numeri usano la formattazione scalare; le liste dati il render comune. La cella
|
|
57
|
+
// modificabile mostra sempre l'editor, quindi non ha un proprio render
|
|
58
|
+
const needsScalarFormat = !isDataList && !isInlineEditable && (dataType === MetadataDataTypes.DateTime || isNumber);
|
|
46
59
|
return {
|
|
47
60
|
dataField: String(md.id),
|
|
48
61
|
caption: (SDK_Globals.useLocalizedName ? md.nameLoc : md.name) ?? md.name ?? '',
|
|
49
62
|
// La colonna modificabile dichiara il tipo: senza valori DevExtreme non saprebbe che editor usare
|
|
50
63
|
...(isInlineEditable
|
|
51
64
|
? {
|
|
52
|
-
|
|
65
|
+
// I numeri arrivano in cella già come numeri (vedi buildArchiveRow): l'editor è la casella numerica
|
|
66
|
+
dataType: isNumber ? 'number' : 'string',
|
|
53
67
|
allowEditing: true,
|
|
54
68
|
showEditorAlways: true,
|
|
55
69
|
cssClass: INLINE_EDITABLE_CELL_CLASS,
|
|
56
|
-
// Lunghezza e obbligatorietà del metadato valgono anche in griglia
|
|
57
|
-
|
|
70
|
+
// Lunghezza e obbligatorietà del metadato valgono anche in griglia; sui numeri
|
|
71
|
+
// i limiti restano all'API, in cella basta che sia un numero
|
|
72
|
+
editorOptions: !isNumber && md.length ? { maxLength: md.length } : undefined,
|
|
58
73
|
validationRules: String(md.isRequired) === '1' ? [{ type: 'required', message: SDKUI_Localizator.RequiredField }] : undefined,
|
|
59
74
|
}
|
|
60
75
|
: { allowEditing: false }),
|
|
@@ -164,16 +179,19 @@ export const renderSelectDcmtType = ({ detailRelationItems, selectedRelation, on
|
|
|
164
179
|
}) })] }));
|
|
165
180
|
export const renderAccordionHeader = ({ section, isOpen, tooltip, count, title, actions, panel, onToggleOpen }) => {
|
|
166
181
|
const restBackground = isOpen ? TMColors.toolbar_background : 'transparent';
|
|
167
|
-
// Un pannello ingrandito non si collassa: resterebbe la sola testata al posto di tutto il riepilogo
|
|
168
|
-
|
|
182
|
+
// Un pannello ingrandito non si collassa: resterebbe la sola testata al posto di tutto il riepilogo.
|
|
183
|
+
// Dove i pannelli si vedono uno alla volta (mobile) il collasso è precluso per lo stesso motivo
|
|
184
|
+
const canToggle = panel.canCollapse && !panel.isMaximized;
|
|
185
|
+
// Con un pannello alla volta l'ingrandimento non ha nulla da nascondere
|
|
186
|
+
const canMaximize = panel.canMaximize;
|
|
169
187
|
const onToggle = () => onToggleOpen(section, !isOpen);
|
|
170
188
|
const expandTooltip = panel.isMaximized ? SDKUI_Localizator.Minimize : SDKUI_Localizator.ZoomIn;
|
|
171
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,
|
|
172
190
|
// Doppio click sulla testata: ingrandisce e riduce, come nei pannelli del prodotto
|
|
173
|
-
onDoubleClick: () => panel.toggleMaximize(), onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
|
|
191
|
+
onDoubleClick: canMaximize ? () => panel.toggleMaximize() : undefined, onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
|
|
174
192
|
e.preventDefault();
|
|
175
193
|
onToggle();
|
|
176
|
-
} } : 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, _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, 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 } }))] }));
|
|
177
195
|
};
|
|
178
196
|
/**
|
|
179
197
|
* Contenuto di un pannello: il contesto del panel manager è leggibile solo dentro l'albero del
|
|
@@ -182,6 +200,10 @@ export const renderAccordionHeader = ({ section, isOpen, tooltip, count, title,
|
|
|
182
200
|
export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
|
|
183
201
|
const { maximizedPanels, toggleMaximize } = useTMPanelManagerContext();
|
|
184
202
|
const isMaximized = maximizedPanels.includes(section);
|
|
203
|
+
// Su mobile il panel manager mostra un pannello alla volta: la sezione è già a tutto schermo, e
|
|
204
|
+
// ingrandirla nasconderebbe la barra con cui si passa da una sezione all'altra; chiuderla lascerebbe
|
|
205
|
+
// a schermo la sola testata, perché non ci sono altre sezioni a cui cedere lo spazio
|
|
206
|
+
const isMobile = useDeviceType() === DeviceType.MOBILE;
|
|
185
207
|
// Una sezione collassata non ha corpo: ingrandirla mostrerebbe un pannello vuoto, senza
|
|
186
208
|
// nemmeno poterla riaprire (da ingrandita il collasso è bloccato), quindi la si apre prima
|
|
187
209
|
const toggleMaximizeSection = () => {
|
|
@@ -189,7 +211,7 @@ export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
|
|
|
189
211
|
onBeforeMaximize?.(section);
|
|
190
212
|
toggleMaximize(section);
|
|
191
213
|
};
|
|
192
|
-
return _jsx(_Fragment, { children: render({ isMaximized, toggleMaximize: toggleMaximizeSection }) });
|
|
214
|
+
return _jsx(_Fragment, { children: render({ isMaximized, canMaximize: !isMobile, canCollapse: !isMobile, toggleMaximize: toggleMaximizeSection }) });
|
|
193
215
|
};
|
|
194
216
|
/**
|
|
195
217
|
* Tiene allineate le altezze dei pannelli al collasso delle sezioni: il panel manager non
|
|
@@ -197,7 +219,7 @@ export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
|
|
|
197
219
|
* Vive dentro il provider perché il contesto è leggibile solo lì
|
|
198
220
|
*/
|
|
199
221
|
export const TMSectionPanelsHeights = ({ sections, sectionsOpen }) => {
|
|
200
|
-
const { panelDimensions, setPanelDimensionsById, maximizedPanels } = useTMPanelManagerContext();
|
|
222
|
+
const { panelDimensions, panelVisibility, setPanelDimensionsById, maximizedPanels } = useTMPanelManagerContext();
|
|
201
223
|
// Con un pannello ingrandito le quote sono quelle dell'ingrandimento (100% e 0%): riallinearle al
|
|
202
224
|
// collasso lo svuoterebbe, quindi si aspetta il ritorno alla vista normale per rifare i conti
|
|
203
225
|
const hasMaximizedPanel = maximizedPanels.length > 0;
|
|
@@ -214,8 +236,13 @@ export const TMSectionPanelsHeights = ({ sections, sectionsOpen }) => {
|
|
|
214
236
|
}
|
|
215
237
|
if (hasMaximizedPanel)
|
|
216
238
|
return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
243
|
+
const visibleSections = sections.filter(section => panelVisibility[section] !== false);
|
|
244
|
+
const heights = sectionPanelHeights(visibleSections, sectionsOpen, dimensionsRef.current);
|
|
245
|
+
visibleSections.forEach(section => setPanelDimensionsById(section, heights[section].width, heights[section].height));
|
|
246
|
+
}, [sectionsOpen, hasMaximizedPanel, panelVisibility]);
|
|
220
247
|
return null;
|
|
221
248
|
};
|
|
@@ -820,6 +820,7 @@ export declare class SDKUI_Localizator {
|
|
|
820
820
|
static get Statistics(): "Statistiken" | "Statistics" | "Estadística" | "Statistiques" | "Estatísticas" | "Statistiche";
|
|
821
821
|
static get Status(): "Status" | "Estado" | "Statut" | "Stato";
|
|
822
822
|
static get StatusAndAnswer(): "Status und Antwort" | "Status and answer" | "Estado y respuesta" | "Statut et réponse" | "Estado e resposta" | "Stato e risposta";
|
|
823
|
+
static get StopOnFirstError(): "Beim ersten Fehler abbrechen" | "Stop at the first error" | "Detener en el primer error" | "Arrêter à la première erreur" | "Parar no primeiro erro" | "Interrompi al primo errore";
|
|
823
824
|
static get Subject(): "Betreff" | "Subject" | "Asunto" | "Objet" | "Assunto" | "Oggetto";
|
|
824
825
|
static get Summary(): "Zusammenfassung" | "Summary" | "Resumen" | "Résumé" | "Resumo" | "Riepilogo";
|
|
825
826
|
static get SwitchUser(): "Benutzer wechseln" | "Switch user" | "Cambiar usuario" | "Changer d'utilisateur" | "Mudar de usuário" | "Cambia utente";
|
|
@@ -8180,6 +8180,16 @@ export class SDKUI_Localizator {
|
|
|
8180
8180
|
default: return "Stato e risposta";
|
|
8181
8181
|
}
|
|
8182
8182
|
}
|
|
8183
|
+
static get StopOnFirstError() {
|
|
8184
|
+
switch (this._cultureID) {
|
|
8185
|
+
case CultureIDs.De_DE: return "Beim ersten Fehler abbrechen";
|
|
8186
|
+
case CultureIDs.En_US: return "Stop at the first error";
|
|
8187
|
+
case CultureIDs.Es_ES: return "Detener en el primer error";
|
|
8188
|
+
case CultureIDs.Fr_FR: return "Arrêter à la première erreur";
|
|
8189
|
+
case CultureIDs.Pt_PT: return "Parar no primeiro erro";
|
|
8190
|
+
default: return "Interrompi al primo errore";
|
|
8191
|
+
}
|
|
8192
|
+
}
|
|
8183
8193
|
static get Subject() {
|
|
8184
8194
|
switch (this._cultureID) {
|
|
8185
8195
|
case CultureIDs.De_DE: return "Betreff";
|
|
@@ -56,6 +56,7 @@ export declare const useMultiMasterDetailDcmts: ({ dtd, masterDcmt, onClose }: U
|
|
|
56
56
|
closeModal: () => void;
|
|
57
57
|
sectionsOpen: Record<AccordionSection, boolean>;
|
|
58
58
|
setSectionOpen: (section: AccordionSection, isOpen: boolean) => void;
|
|
59
|
+
openAllSections: () => void;
|
|
59
60
|
clearLayout: () => void;
|
|
60
61
|
layoutResetToken: number;
|
|
61
62
|
canUpdateMaster: boolean;
|
|
@@ -8,7 +8,7 @@ import TMSpinner from '../components/base/TMSpinner';
|
|
|
8
8
|
import ShowAlert from '../components/base/TMAlert';
|
|
9
9
|
import { TMResultManager } from '../components/forms/TMResultDialog';
|
|
10
10
|
import { useDataListItem } from './useDataListItem';
|
|
11
|
-
import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
|
|
11
|
+
import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync, toGridNumber } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
|
|
12
12
|
/** Fasi del flusso di archiviazione multipla dei dettagli */
|
|
13
13
|
export var MultiDetailArchivePhase;
|
|
14
14
|
(function (MultiDetailArchivePhase) {
|
|
@@ -44,6 +44,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
44
44
|
// Struttura del tipo documento selezionato
|
|
45
45
|
const [detailColumns, setDetailColumns] = useState([]);
|
|
46
46
|
const [detailSupportsFile, setDetailSupportsFile] = useState(false);
|
|
47
|
+
// Mid numerici del tipo di dettaglio: in griglia le loro celle portano il numero (vedi buildArchiveRow)
|
|
48
|
+
const numericMidsRef = useRef(new Set());
|
|
47
49
|
// Dettagli già archiviati del tipo selezionato, collegati al master
|
|
48
50
|
const [archivedDetailResults, setArchivedDetailResults] = useState([]);
|
|
49
51
|
const [isLoadingArchivedDetails, setIsLoadingArchivedDetails] = useState(false);
|
|
@@ -146,6 +148,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
146
148
|
if (!selectedRelation?.detailTID) {
|
|
147
149
|
setDetailColumns([]);
|
|
148
150
|
setDetailSupportsFile(false);
|
|
151
|
+
numericMidsRef.current = new Set();
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
154
|
try {
|
|
@@ -153,16 +156,21 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
153
156
|
setDetailSupportsFile(detailDtd?.archiveConstraint !== ArchiveConstraints.OnlyMetadata);
|
|
154
157
|
// Liste dati referenziate dai metadati, servono al render delle celle
|
|
155
158
|
const dataListIDs = new Set();
|
|
159
|
+
const numericMids = new Set();
|
|
156
160
|
(detailDtd?.metadata ?? []).forEach(md => {
|
|
157
161
|
if (md.dataDomain === MetadataDataDomains.DataList && md.dataListID)
|
|
158
162
|
dataListIDs.add(md.dataListID);
|
|
163
|
+
if (md.dataType === MetadataDataTypes.Number && md.id !== undefined)
|
|
164
|
+
numericMids.add(md.id);
|
|
159
165
|
});
|
|
166
|
+
numericMidsRef.current = numericMids;
|
|
160
167
|
await loadDataListsAsync(dataListIDs);
|
|
161
168
|
setDetailColumns(buildColumnsFromDtd(detailDtd, renderDataListCell));
|
|
162
169
|
}
|
|
163
170
|
catch {
|
|
164
171
|
setDetailColumns([]);
|
|
165
172
|
setDetailSupportsFile(false);
|
|
173
|
+
numericMidsRef.current = new Set();
|
|
166
174
|
}
|
|
167
175
|
};
|
|
168
176
|
loadDetailStructureAsync();
|
|
@@ -218,6 +226,18 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
218
226
|
if (section === 'archivedDetails' && isOpen && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
|
|
219
227
|
refreshArchivedDetailsAsync();
|
|
220
228
|
};
|
|
229
|
+
/**
|
|
230
|
+
* Apre tutte le sezioni senza salvare: serve dove il collasso non è una scelta dell'utente ma una
|
|
231
|
+
* conseguenza del layout (su mobile si vede un pannello alla volta, e una sezione chiusa lascerebbe
|
|
232
|
+
* a schermo la sola testata). Non si persiste per non sovrascrivere le sezioni che l'utente ha
|
|
233
|
+
* chiuso nella vista a pannelli impilati.
|
|
234
|
+
*/
|
|
235
|
+
const openAllSections = () => {
|
|
236
|
+
setSectionsOpen(prev => Object.values(prev).every(Boolean) ? prev : { masterInfo: true, archivedDetails: true, archiveList: true });
|
|
237
|
+
// I dettagli già archiviati si caricano come alla prima apertura della sezione
|
|
238
|
+
if (!areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
|
|
239
|
+
refreshArchivedDetailsAsync();
|
|
240
|
+
};
|
|
221
241
|
/**
|
|
222
242
|
* Cancella il layout configurato dall'utente (altezze dei pannelli e sezioni collassate), previa
|
|
223
243
|
* conferma. Il token cambia per rimontare il panel manager, che legge le dimensioni solo all'avvio.
|
|
@@ -303,11 +323,13 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
303
323
|
},
|
|
304
324
|
});
|
|
305
325
|
}, []);
|
|
326
|
+
// I valori del form sono stringhe: le celle numeriche vogliono il numero, richiesto dal loro editor
|
|
306
327
|
const buildArchiveRow = useCallback((id, tid, values, file) => {
|
|
307
328
|
const row = { id, __tid: tid, __values: values, __file: file };
|
|
308
329
|
for (const v of values) {
|
|
309
|
-
if (v.mid
|
|
310
|
-
|
|
330
|
+
if (v.mid === undefined)
|
|
331
|
+
continue;
|
|
332
|
+
row[String(v.mid)] = numericMidsRef.current.has(v.mid) ? toGridNumber(v.value) : (v.value ?? '');
|
|
311
333
|
}
|
|
312
334
|
return row;
|
|
313
335
|
}, []);
|
|
@@ -326,7 +348,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
326
348
|
closeAddForm();
|
|
327
349
|
};
|
|
328
350
|
/**
|
|
329
|
-
* Modifica diretta in griglia di uno o più metadati testuali (changes: mid -> valore).
|
|
351
|
+
* Modifica diretta in griglia di uno o più metadati testuali o numerici (changes: mid -> valore).
|
|
330
352
|
* La riga e i suoi __values sono ricreati: il duplicato, che condivide __values per
|
|
331
353
|
* riferimento, resta invariato
|
|
332
354
|
*/
|
|
@@ -334,9 +356,10 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
334
356
|
const changedMids = Object.keys(changes);
|
|
335
357
|
if (changedMids.length === 0)
|
|
336
358
|
return;
|
|
337
|
-
//
|
|
359
|
+
// I __values vanno a sistema come stringhe, anche quelli che in griglia sono numeri. La griglia
|
|
360
|
+
// restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
|
|
338
361
|
const normalized = {};
|
|
339
|
-
changedMids.forEach(mid => { normalized[mid] = changes[mid]
|
|
362
|
+
changedMids.forEach(mid => { normalized[mid] = changes[mid] === null || changes[mid] === undefined ? '' : String(changes[mid]); });
|
|
340
363
|
setArchiveRows(prev => prev.map(r => {
|
|
341
364
|
if (r.id !== rowId)
|
|
342
365
|
return r;
|
|
@@ -352,7 +375,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
352
375
|
values.push(added);
|
|
353
376
|
});
|
|
354
377
|
const updated = { ...r, __values: values };
|
|
355
|
-
|
|
378
|
+
// In cella il numero resta un numero, come lo vuole il suo editor
|
|
379
|
+
changedMids.forEach(mid => { updated[mid] = numericMidsRef.current.has(Number(mid)) ? toGridNumber(normalized[mid]) : normalized[mid]; });
|
|
356
380
|
return updated;
|
|
357
381
|
}));
|
|
358
382
|
}, []);
|
|
@@ -537,7 +561,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
537
561
|
detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes,
|
|
538
562
|
selectRelation, backToTypeSelection, isBackLocked, closeModal,
|
|
539
563
|
// Sezioni
|
|
540
|
-
sectionsOpen, setSectionOpen, clearLayout, layoutResetToken,
|
|
564
|
+
sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken,
|
|
541
565
|
// Master
|
|
542
566
|
canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken,
|
|
543
567
|
isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync,
|