@topconsultnpm/sdkui-react 6.22.0-dev3.7 → 6.22.0-dev3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/base/TMTabControl.d.ts +2 -0
- package/lib/components/base/TMTabControl.js +7 -6
- package/lib/components/base/TMTreeView.js +13 -1
- package/lib/components/features/documents/TMCopyToFolderForm.js +61 -22
- package/lib/components/features/documents/TMMasterDetailDcmts.js +14 -7
- package/lib/components/features/documents/TMMergeToPdfForm.js +15 -8
- package/lib/hooks/useDcmtOperations.d.ts +7 -0
- package/lib/hooks/useDcmtOperations.js +141 -71
- package/package.json +1 -1
|
@@ -36,6 +36,8 @@ export interface TMTabControlProps {
|
|
|
36
36
|
hideBarWhenSingle?: boolean;
|
|
37
37
|
/** Se valorizzata mostra la 'x' sui tab e notifica la richiesta di chiusura. */
|
|
38
38
|
onTabClose?: (id: string) => void;
|
|
39
|
+
/** Se true anche l'ultimo tab rimasto e chiudibile, lasciando il controllo senza tab. Default false. */
|
|
40
|
+
closableWhenSingle?: boolean;
|
|
39
41
|
/** Se valorizzata abilita la rinomina del titolo. */
|
|
40
42
|
onTabRename?: (id: string, label: string) => void;
|
|
41
43
|
/** Se valorizzata abilita lo spostamento con trascinamento: sposta draggedId nella posizione di targetId. */
|
|
@@ -341,7 +341,7 @@ const shortenLabel = (label) => {
|
|
|
341
341
|
/** Contenitore dichiarativo di un tab: figlio diretto di TMTabControl, non renderizza nulla di suo. */
|
|
342
342
|
export const TMTabPanel = ({ children }) => _jsx(_Fragment, { children: children });
|
|
343
343
|
const TMTabControl = (props) => {
|
|
344
|
-
const { selectedId, defaultSelectedId, onSelectedIdChange, hideBarWhenSingle = false, onTabClose, onTabRename, onTabReorder, onTabAdd, tabMenuItems, addCaption = SDKUI_Localizator.AddTab, renameCaption = SDKUI_Localizator.Rename, closeCaption = SDKUI_Localizator.Close, closeOtherCaption = SDKUI_Localizator.CloseOtherTabs, variant = 'module', textColor, idleTextColor, contentAnimation = 'none', animationDuration = 300, swipeEnabled = true, contentStyle, style, children } = props;
|
|
344
|
+
const { selectedId, defaultSelectedId, onSelectedIdChange, hideBarWhenSingle = false, onTabClose, closableWhenSingle = false, onTabRename, onTabReorder, onTabAdd, tabMenuItems, addCaption = SDKUI_Localizator.AddTab, renameCaption = SDKUI_Localizator.Rename, closeCaption = SDKUI_Localizator.Close, closeOtherCaption = SDKUI_Localizator.CloseOtherTabs, variant = 'module', textColor, idleTextColor, contentAnimation = 'none', animationDuration = 300, swipeEnabled = true, contentStyle, style, children } = props;
|
|
345
345
|
const v = useMemo(() => VARIANTS[variant](), [variant]);
|
|
346
346
|
const titleColor = textColor ?? v.textColor;
|
|
347
347
|
const titleIdleColor = idleTextColor ?? v.idleTextColor;
|
|
@@ -523,8 +523,8 @@ const TMTabControl = (props) => {
|
|
|
523
523
|
// L'input si apre dopo la chiusura del menu, altrimenti perde subito il focus
|
|
524
524
|
pushOwn({ name: renameCaption, icon: _jsx(IconEdit, { fontSize: 18 }), onClick: () => { closeMenu(); setTimeout(() => setEditingId(panel.props.id), 0); } });
|
|
525
525
|
}
|
|
526
|
-
// Come per la 'x': l'ultimo tab rimasto non si chiude
|
|
527
|
-
if (onTabClose && panel.props.closable !== false && panels.length > 1) {
|
|
526
|
+
// Come per la 'x': l'ultimo tab rimasto non si chiude, salvo closableWhenSingle
|
|
527
|
+
if (onTabClose && panel.props.closable !== false && (closableWhenSingle || panels.length > 1)) {
|
|
528
528
|
pushOwn({ name: closeCaption, icon: _jsx(IconCloseOutline, { fontSize: 18 }), onClick: () => { closeMenu(); onTabClose(panel.props.id); } });
|
|
529
529
|
}
|
|
530
530
|
// Chiude in blocco gli altri tab chiudibili, ma prima chiede conferma dicendo quanti sono
|
|
@@ -547,7 +547,7 @@ const TMTabControl = (props) => {
|
|
|
547
547
|
pushOwn({ name: addCaption, icon: _jsx(IconAdd, { fontSize: 18 }), onClick: () => { closeMenu(); onTabAdd(); } }, items.length > 0);
|
|
548
548
|
}
|
|
549
549
|
return items;
|
|
550
|
-
}, [panels, tabMenuItems, onTabRename, onTabClose, onTabAdd, renameCaption, closeCaption, closeOtherCaption, addCaption]);
|
|
550
|
+
}, [panels, tabMenuItems, onTabRename, onTabClose, closableWhenSingle, onTabAdd, renameCaption, closeCaption, closeOtherCaption, addCaption]);
|
|
551
551
|
// Tasto destro sul tab: niente menu del browser, si apre quello con Rinomina e Chiudi.
|
|
552
552
|
const openMenu = (e, id) => {
|
|
553
553
|
e.preventDefault();
|
|
@@ -562,8 +562,9 @@ const TMTabControl = (props) => {
|
|
|
562
562
|
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', minHeight: 0, ...style }, children: [showBar &&
|
|
563
563
|
_jsxs(TabsHeader, { $v: v, onContextMenu: (e) => e.preventDefault(), children: [arrows.left && _jsx(NavButton, { $v: v, type: 'button', tabIndex: -1, onClick: () => scrollTabs(-1), children: _jsx(IconLeft, { fontSize: 18 }) }), _jsxs(TabsStrip, { $v: v, ref: stripRef, role: 'tablist', onScroll: refreshArrows, onKeyDown: handleBarKeyDown, children: [panels.map((p, index) => {
|
|
564
564
|
const isActive = p.props.id === activeId;
|
|
565
|
-
// Con un solo tab la 'x' sparisce: non deve essere possibile restare senza contenuto
|
|
566
|
-
|
|
565
|
+
// Con un solo tab la 'x' sparisce: non deve essere possibile restare senza contenuto,
|
|
566
|
+
// a meno che closableWhenSingle non consenta di chiudere anche l'ultimo tab.
|
|
567
|
+
const canClose = !!onTabClose && p.props.closable !== false && (closableWhenSingle || panels.length > 1);
|
|
567
568
|
const canRename = !!onTabRename && p.props.renamable !== false && typeof p.props.label === 'string';
|
|
568
569
|
const isDropTarget = !!dragId && dropId === p.props.id && dragId !== p.props.id;
|
|
569
570
|
const draggedIndex = panels.findIndex(t => t.props.id === dragId);
|
|
@@ -147,15 +147,26 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
147
147
|
return [];
|
|
148
148
|
return flattenTreeWithDepth(dataSource);
|
|
149
149
|
}, [enableVirtualization, dataSource, flattenTreeWithDepth]);
|
|
150
|
+
// Chiave dell'ultimo elemento verso cui si è effettivamente scrollato.
|
|
151
|
+
// Evita di ripetere lo scroll quando cambia solo la struttura dell'albero
|
|
152
|
+
// (es. espansione di un nodo) a parità di elemento focalizzato.
|
|
153
|
+
const lastScrolledKeyRef = useRef(undefined);
|
|
150
154
|
// Scroll verso l'elemento focalizzato quando cambia (navigazione con frecce)
|
|
151
155
|
useEffect(() => {
|
|
152
|
-
if (!focusedItem?.key)
|
|
156
|
+
if (!focusedItem?.key) {
|
|
157
|
+
lastScrolledKeyRef.current = undefined;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Focus invariato: l'effect è stato rieseguito solo perché l'albero è cambiato
|
|
161
|
+
// (espansione/collasso di un nodo). La posizione di scroll non va toccata.
|
|
162
|
+
if (lastScrolledKeyRef.current === focusedItem.key)
|
|
153
163
|
return;
|
|
154
164
|
if (enableVirtualization) {
|
|
155
165
|
// Lista virtualizzata: usa scrollToRow per garantire che l'elemento sia visibile
|
|
156
166
|
const index = flattenedItems.findIndex(item => item.node.key === focusedItem.key);
|
|
157
167
|
if (index >= 0 && listRef.current) {
|
|
158
168
|
listRef.current.scrollToRow({ index, align: 'smart' });
|
|
169
|
+
lastScrolledKeyRef.current = focusedItem.key;
|
|
159
170
|
}
|
|
160
171
|
}
|
|
161
172
|
else {
|
|
@@ -164,6 +175,7 @@ const TMTreeView = ({ dataSource = [], focusedItem, selectedItems = [], allowMul
|
|
|
164
175
|
const element = containerRef.current.querySelector(`[data-node-key="${focusedItem.key}"]`);
|
|
165
176
|
if (element) {
|
|
166
177
|
element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
178
|
+
lastScrolledKeyRef.current = focusedItem.key;
|
|
167
179
|
}
|
|
168
180
|
}
|
|
169
181
|
}
|
|
@@ -20,6 +20,12 @@ import ShowAlert from '../../base/TMAlert';
|
|
|
20
20
|
import { TMColors } from '../../../utils/theme';
|
|
21
21
|
import TMDownloadRelationViewerSection from './TMDownloadRelationViewerSection';
|
|
22
22
|
import { buildInitialDownloadSettings, fileExists, generateTargetFileName, generateUniqueFileName, getDcmtInfosToDownload, getFloatingLabelStyle, isDirectoryPickerSupported, } from './copyAndMergeDcmtsShared';
|
|
23
|
+
/**
|
|
24
|
+
* Numero di documenti scaricati in parallelo.
|
|
25
|
+
* I file vengono consumati via callback (scrittura su cartella o raccolta per lo ZIP),
|
|
26
|
+
* quindi il parallelismo è sicuro; l'ordine finale è garantito a valle del download.
|
|
27
|
+
*/
|
|
28
|
+
const COPY_DOWNLOAD_CONCURRENCY = 5;
|
|
23
29
|
/**
|
|
24
30
|
* Form per la copia/zip dei documenti in una cartella locale.
|
|
25
31
|
* Logica condivisa con TMMergeToPdfForm è esternalizzata in copyAndMergeDcmtsShared
|
|
@@ -111,10 +117,24 @@ const TMCopyToFolderForm = ({ mode, selectedDcmtInfos, onClose, showTMRelationVi
|
|
|
111
117
|
const handleSeparatorChange = (e) => {
|
|
112
118
|
updateSettings('separatorChar', e.target.value.slice(0, 1));
|
|
113
119
|
};
|
|
120
|
+
// ---- Serializzazione delle scritture su filesystem ----
|
|
121
|
+
// I download sono paralleli, le scritture no: fileExists/generateUniqueFileName/
|
|
122
|
+
// getFileHandle sono check-then-act sulla stessa cartella e due scritture contemporanee
|
|
123
|
+
// potrebbero sovrascriversi o calcolare lo stesso nome "univoco".
|
|
124
|
+
const writeChainRef = useRef(Promise.resolve());
|
|
125
|
+
const runExclusive = (action) => {
|
|
126
|
+
const result = writeChainRef.current.then(action);
|
|
127
|
+
// La catena non deve interrompersi se una scrittura fallisce: l'errore viene
|
|
128
|
+
// comunque propagato al chiamante tramite il promise restituito.
|
|
129
|
+
writeChainRef.current = result.catch(() => undefined);
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
114
132
|
// ---- Scrittura file nel filesystem locale ----
|
|
115
133
|
const writeFileToFolder = async (file, dcmtInfo) => {
|
|
116
134
|
try {
|
|
117
|
-
|
|
135
|
+
// Fuori dal lock: con la denominazione da metadati questa fa una chiamata al
|
|
136
|
+
// server per documento e deve poter andare in parallelo agli altri download.
|
|
137
|
+
const baseFileName = await generateTargetFileName(file, dcmtInfo, {
|
|
118
138
|
fileNamingMode: settings.fileNamingMode,
|
|
119
139
|
separatorChar: settings.separatorChar,
|
|
120
140
|
});
|
|
@@ -123,7 +143,7 @@ const TMCopyToFolderForm = ({ mode, selectedDcmtInfos, onClose, showTMRelationVi
|
|
|
123
143
|
const url = URL.createObjectURL(file);
|
|
124
144
|
const a = document.createElement('a');
|
|
125
145
|
a.href = url;
|
|
126
|
-
a.download =
|
|
146
|
+
a.download = baseFileName;
|
|
127
147
|
document.body.appendChild(a);
|
|
128
148
|
a.target = '_blank';
|
|
129
149
|
a.rel = 'noreferrer';
|
|
@@ -132,23 +152,29 @@ const TMCopyToFolderForm = ({ mode, selectedDcmtInfos, onClose, showTMRelationVi
|
|
|
132
152
|
URL.revokeObjectURL(url);
|
|
133
153
|
return;
|
|
134
154
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
// Dentro il lock: controllo esistenza, calcolo del nome univoco e scrittura
|
|
156
|
+
// devono restare un'unica operazione atomica sulla cartella.
|
|
157
|
+
const folderHandle = folderHandleRef.current;
|
|
158
|
+
await runExclusive(async () => {
|
|
159
|
+
let targetFileName = baseFileName;
|
|
160
|
+
const exists = await fileExists(folderHandle, targetFileName);
|
|
161
|
+
if (exists) {
|
|
162
|
+
switch (settings.fileExistsMode) {
|
|
163
|
+
case 'skip':
|
|
164
|
+
return;
|
|
165
|
+
case 'rename':
|
|
166
|
+
targetFileName = await generateUniqueFileName(folderHandle, targetFileName);
|
|
167
|
+
break;
|
|
168
|
+
case 'overwrite':
|
|
169
|
+
default:
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
146
172
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
const fileHandle = await folderHandle.getFileHandle(targetFileName, { create: true });
|
|
174
|
+
const writable = await fileHandle.createWritable();
|
|
175
|
+
await writable.write(file);
|
|
176
|
+
await writable.close();
|
|
177
|
+
});
|
|
152
178
|
}
|
|
153
179
|
catch (err) {
|
|
154
180
|
console.error('Errore nella scrittura del file:', err);
|
|
@@ -176,10 +202,12 @@ const TMCopyToFolderForm = ({ mode, selectedDcmtInfos, onClose, showTMRelationVi
|
|
|
176
202
|
retrieveOptions.invoiceRetrieveFormat = settings.invoiceFormat ?? SDKUI_Globals.userSettings.searchSettings.invoiceRetrieveFormat;
|
|
177
203
|
retrieveOptions.orderRetrieveFormat = settings.orderFormat ?? SDKUI_Globals.userSettings.searchSettings.orderRetrieveFormat;
|
|
178
204
|
if (settings.exportMode === 'copy') {
|
|
179
|
-
|
|
205
|
+
// Senza handle della cartella ogni file viene salvato con un click su <a download>:
|
|
206
|
+
// in quel caso si resta sequenziali, perché il browser blocca i click ravvicinati.
|
|
207
|
+
const copyConcurrency = folderHandleRef.current ? COPY_DOWNLOAD_CONCURRENCY : 1;
|
|
208
|
+
await downloadDcmtsAsync({ inputDcmts: dcmtInfosToDownload, downloadType: DownloadTypes.Dcmt, downloadMode: 'download', onFileDownloaded: writeFileToFolder, skipConfirmation: true, retrieveOptions, useCache: false, downloadConcurrency: copyConcurrency });
|
|
180
209
|
}
|
|
181
210
|
else if (settings.exportMode === 'zip') {
|
|
182
|
-
const zipEntries = [];
|
|
183
211
|
const fileNameCounts = new Map();
|
|
184
212
|
const getUniqueZipFileName = (fileName) => {
|
|
185
213
|
const lowerName = fileName.toLowerCase();
|
|
@@ -192,14 +220,25 @@ const TMCopyToFolderForm = ({ mode, selectedDcmtInfos, onClose, showTMRelationVi
|
|
|
192
220
|
const ext = dotIndex > 0 ? fileName.substring(dotIndex) : '';
|
|
193
221
|
return `${baseName} (${count})${ext}`;
|
|
194
222
|
};
|
|
223
|
+
// Con i download paralleli i file arrivano in ordine di completamento: si raccolgono
|
|
224
|
+
// in mappa (nome incluso, calcolato in parallelo) e l'archivio viene costruito dopo
|
|
225
|
+
// nell'ordine di selezione, così i suffissi di deduplica restano quelli sequenziali.
|
|
226
|
+
const downloadedEntries = new Map();
|
|
195
227
|
const collectFileForZip = async (file, dcmtInfo) => {
|
|
196
228
|
const targetFileName = await generateTargetFileName(file, dcmtInfo, {
|
|
197
229
|
fileNamingMode: settings.fileNamingMode,
|
|
198
230
|
separatorChar: settings.separatorChar,
|
|
199
231
|
});
|
|
200
|
-
|
|
232
|
+
downloadedEntries.set(`${dcmtInfo.TID}_${dcmtInfo.DID}`, { file, targetFileName });
|
|
201
233
|
};
|
|
202
|
-
await downloadDcmtsAsync({ inputDcmts: dcmtInfosToDownload, downloadType: DownloadTypes.Dcmt, downloadMode: 'download', onFileDownloaded: collectFileForZip, skipConfirmation: true, retrieveOptions, useCache: false });
|
|
234
|
+
await downloadDcmtsAsync({ inputDcmts: dcmtInfosToDownload, downloadType: DownloadTypes.Dcmt, downloadMode: 'download', onFileDownloaded: collectFileForZip, skipConfirmation: true, retrieveOptions, useCache: false, downloadConcurrency: COPY_DOWNLOAD_CONCURRENCY });
|
|
235
|
+
const zipEntries = [];
|
|
236
|
+
for (const dcmtInfo of dcmtInfosToDownload) {
|
|
237
|
+
const entry = downloadedEntries.get(`${dcmtInfo.TID}_${dcmtInfo.DID}`);
|
|
238
|
+
if (!entry)
|
|
239
|
+
continue;
|
|
240
|
+
zipEntries.push({ filename: getUniqueZipFileName(entry.targetFileName), data: entry.file });
|
|
241
|
+
}
|
|
203
242
|
if (zipEntries.length > 0) {
|
|
204
243
|
const zipFileName = settings.zipFileName.trim().toLowerCase().endsWith('.zip')
|
|
205
244
|
? settings.zipFileName.trim()
|
|
@@ -472,7 +472,10 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
|
|
|
472
472
|
], [tmTreeView, tmFormOrResult, focusedItem?.isDcmt, dtdMaster]);
|
|
473
473
|
return (_jsxs("div", { style: { width: '100%', height: '100%', position: 'relative' }, children: [isCheckingFirstLoad && (_jsx(TMLoadingOverlay, { onCancel: handleCancelFirstLoad, onStop: handleStopRelationsCalculation, description: relationsProgress.max > 1
|
|
474
474
|
? `${isForMaster ? SDKUI_Localizator.LoadingMasterDocuments : SDKUI_Localizator.LoadingDetailDocuments} (${relationsProgress.value}/${relationsProgress.max})`
|
|
475
|
-
: undefined, progressValue: relationsProgress.value, progressMax: relationsProgress.max
|
|
475
|
+
: undefined, progressValue: relationsProgress.value, progressMax: relationsProgress.max,
|
|
476
|
+
// Con un solo documento in input la chiamata è unica: non ci sono
|
|
477
|
+
// risultati parziali da mostrare, quindi il pulsante resta disabilitato
|
|
478
|
+
stopDisabled: (inputDcmts?.length ?? 0) <= 1 })), _jsxs("div", { style: isCheckingFirstLoad ? { position: 'absolute', width: 0, height: 0, overflow: 'hidden', opacity: 0, pointerEvents: 'none' } : { width: '100%', height: '100%' }, 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(TMPanelManagerProvider, { panels: initialPanels, initialVisibility: allInitialPanelVisibility, defaultDimensions: initialPanelDimensions, initialDimensions: initialPanelDimensions, initialMobilePanelId: 'tmTreeView', children: _jsx(TMPanelManagerContainer, { panels: initialPanels, direction: "horizontal", showToolbar: true }) }) }), renderDcmtOperations, renderFloatingBar] })] }));
|
|
476
479
|
};
|
|
477
480
|
export default TMMasterDetailDcmts;
|
|
478
481
|
/**
|
|
@@ -551,7 +554,7 @@ const TMFormOrResultWrapper = ({ refreshKey, deviceType, focusedItem, onTaskCrea
|
|
|
551
554
|
}, fetchRemoteCertificates: fetchRemoteCertificates }, refreshKey) :
|
|
552
555
|
focusedItem?.searchResult === undefined ? (_jsx(TMPanel, { title: SDKUI_Localizator.SearchResult, children: _jsx(TMToppyMessage, { message: SDKUI_Localizator.SelectDocumentToViewSearchResults }) })) : (_jsx(TMSearchResult, { groupId: 'tmFormOrResult', isClosable: deviceType !== DeviceType.MOBILE, context: SearchResultContext.METADATA_SEARCH, allowFloatingBar: false, allowRelations: false, openDcmtFormAsModal: true, searchResults: focusedItem?.searchResult ?? [], showSearchResultSidebar: false, showDcmtFormSidebar: false, autoFocusFirstRow: false, onTaskCreateRequest: onTaskCreateRequest, onClose: () => { setPanelVisibilityById('tmTreeView', true); }, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, editPdfForm: editPdfForm, onOpenPdfEditorRequest: onOpenPdfEditorRequest, openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, enablePinIcons: false, onRefreshAfterAddDcmtToFavs: onRefreshAfterAddDcmtToFavs, showBackButton: false, onRefreshSearchAsyncDatagrid: onRefreshSearchResults, onReferenceClick: handleNavigateToReference, fetchRemoteCertificates: fetchRemoteCertificates }, refreshKey)) }));
|
|
553
556
|
};
|
|
554
|
-
const TMLoadingOverlay = ({ onCancel, onStop, description, cancelText, stopText, progressValue, progressMax }) => {
|
|
557
|
+
const TMLoadingOverlay = ({ onCancel, onStop, description, cancelText, stopText, progressValue, progressMax, stopDisabled }) => {
|
|
555
558
|
// Stesso calcolo di TMWaitPanel: 0 quando il massimo non è ancora noto
|
|
556
559
|
const progressPercent = (progressMax ?? 0) > 0
|
|
557
560
|
? Math.min(100, ((progressValue ?? 0) / (progressMax ?? 1)) * 100)
|
|
@@ -625,20 +628,24 @@ const TMLoadingOverlay = ({ onCancel, onStop, description, cancelText, stopText,
|
|
|
625
628
|
.tm-spinner-animation div:nth-child(7):after { top: 63px; left: 17px; background: #782b7d; }
|
|
626
629
|
.tm-spinner-animation div:nth-child(8) { animation-delay: -0.288s; }
|
|
627
630
|
.tm-spinner-animation div:nth-child(8):after { top: 56px; left: 12px; background: #782b7d; }
|
|
628
|
-
` }), _jsxs("div", { className: "tm-spinner-animation", children: [_jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {})] })] }), _jsx("span", { style: { fontSize: '14px', color: '#334155', textAlign: 'center' }, children: description ?? SDKUI_Localizator.Loading }), (progressMax ?? 0) > 1 && (_jsxs("div", { style: { width: '260px' }, children: [_jsx("div", { style: { width: '100%', height: '20px', background: '#e0e0e0', borderRadius: '10px', overflow: 'hidden' }, children: _jsx("div", { style: { width: `${progressPercent.toFixed(2)}%`, height: '100%', background: '#4caf50', transition: 'width 0.3s ease' } }) }), _jsxs("p", { style: { fontWeight: 'bold', color: '#333', margin: '8px 0 0', textAlign: 'center' }, children: [progressPercent.toFixed(2), "%"] })] })), (onStop || onCancel) && (_jsxs("div", { style: { marginTop: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexWrap: 'wrap', gap: '10px' }, children: [onStop && (_jsx("button", { onClick: onStop, title: SDKUI_Localizator.StopRelationsCalculation, style: {
|
|
631
|
+
` }), _jsxs("div", { className: "tm-spinner-animation", children: [_jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {}), _jsx("div", {})] })] }), _jsx("span", { style: { fontSize: '14px', color: '#334155', textAlign: 'center' }, children: description ?? SDKUI_Localizator.Loading }), (progressMax ?? 0) > 1 && (_jsxs("div", { style: { width: '260px' }, children: [_jsx("div", { style: { width: '100%', height: '20px', background: '#e0e0e0', borderRadius: '10px', overflow: 'hidden' }, children: _jsx("div", { style: { width: `${progressPercent.toFixed(2)}%`, height: '100%', background: '#4caf50', transition: 'width 0.3s ease' } }) }), _jsxs("p", { style: { fontWeight: 'bold', color: '#333', margin: '8px 0 0', textAlign: 'center' }, children: [progressPercent.toFixed(2), "%"] })] })), (onStop || onCancel) && (_jsxs("div", { style: { marginTop: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexWrap: 'wrap', gap: '10px' }, children: [onStop && (_jsx("button", { onClick: onStop, disabled: stopDisabled, title: SDKUI_Localizator.StopRelationsCalculation, style: {
|
|
629
632
|
padding: '8px 24px',
|
|
630
633
|
fontSize: '13px',
|
|
631
634
|
fontWeight: 500,
|
|
632
|
-
color: 'white',
|
|
633
|
-
background: '#0c448e',
|
|
634
|
-
border:
|
|
635
|
+
color: stopDisabled ? '#94a3b8' : 'white',
|
|
636
|
+
background: stopDisabled ? '#e2e8f0' : '#0c448e',
|
|
637
|
+
border: `1.5px solid ${stopDisabled ? '#e2e8f0' : '#0c448e'}`,
|
|
635
638
|
borderRadius: '6px',
|
|
636
|
-
cursor: 'pointer',
|
|
639
|
+
cursor: stopDisabled ? 'not-allowed' : 'pointer',
|
|
637
640
|
transition: 'all 0.2s ease',
|
|
638
641
|
}, onMouseEnter: (e) => {
|
|
642
|
+
if (stopDisabled)
|
|
643
|
+
return;
|
|
639
644
|
e.currentTarget.style.background = '#0a3872';
|
|
640
645
|
e.currentTarget.style.borderColor = '#0a3872';
|
|
641
646
|
}, onMouseLeave: (e) => {
|
|
647
|
+
if (stopDisabled)
|
|
648
|
+
return;
|
|
642
649
|
e.currentTarget.style.background = '#0c448e';
|
|
643
650
|
e.currentTarget.style.borderColor = '#0c448e';
|
|
644
651
|
}, children: stopText ?? SDKUI_Localizator.ShowPartialResults })), onCancel && (_jsx("button", { onClick: onCancel, style: {
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useRef, useState } from 'react';
|
|
3
3
|
import { DownloadTypes } from '../../../ts';
|
|
4
4
|
import { calcResponsiveSizes, getExceptionMessage, IconFolderOpen, IconPlay, IconUndo, SDKUI_Globals, SDKUI_Localizator, } from '../../../helper';
|
|
5
|
-
import { DcmtOpers, FileFormats, GeneralRetrieveFormats, InvoiceRetrieveFormats, OrderRetrieveFormats, ResultTypes,
|
|
5
|
+
import { DcmtOpers, FileFormats, GeneralRetrieveFormats, InvoiceRetrieveFormats, OrderRetrieveFormats, ResultTypes, ValidationItem, } from '@topconsultnpm/sdk-ts';
|
|
6
6
|
import TMModal from '../../base/TMModal';
|
|
7
7
|
import { useDeviceType } from '../../base/TMDeviceProvider';
|
|
8
8
|
import TMTextBox from '../../editors/TMTextBox';
|
|
@@ -18,6 +18,12 @@ import ShowAlert from '../../base/TMAlert';
|
|
|
18
18
|
import TMTooltip from '../../base/TMTooltip';
|
|
19
19
|
import MergePdfManager from '../../../helper/MergePdfManager';
|
|
20
20
|
import { TMResultManager } from '../../forms/TMResultDialog';
|
|
21
|
+
/**
|
|
22
|
+
* Numero di documenti scaricati in parallelo durante l'unione.
|
|
23
|
+
* I file vengono consumati via callback (nessun click su <a download>), quindi il
|
|
24
|
+
* parallelismo è sicuro; il riordino finale è garantito da pdfFilesMap.
|
|
25
|
+
*/
|
|
26
|
+
const MERGE_DOWNLOAD_CONCURRENCY = 5;
|
|
21
27
|
/**
|
|
22
28
|
* Converte il formato di visualizzazione fatture impostato dall'utente nel corrispondente formato PDF:
|
|
23
29
|
* il foglio di stile SDI resta SDI, in tutti gli altri casi si usa ASW.
|
|
@@ -190,16 +196,17 @@ const TMMergeToPdfForm = (props) => {
|
|
|
190
196
|
}
|
|
191
197
|
// Per i documenti con file, usa il download normale
|
|
192
198
|
if (dcmtsWithFile.length > 0) {
|
|
193
|
-
const rfo =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
const rfo = {
|
|
200
|
+
retrieveReason: DcmtOpers.None,
|
|
201
|
+
generalRetrieveFormat: GeneralRetrieveFormats.OriginalUnsigned,
|
|
202
|
+
cvtFormat: FileFormats.PDF,
|
|
203
|
+
invoiceRetrieveFormat: toPdfInvoiceRetrieveFormat(SDKUI_Globals.userSettings?.searchSettings.invoiceRetrieveFormat),
|
|
204
|
+
orderRetrieveFormat: toPdfOrderRetrieveFormat(SDKUI_Globals.userSettings?.searchSettings.orderRetrieveFormat),
|
|
205
|
+
};
|
|
199
206
|
const collectFileForMerge = async (file, dcmtInfo) => {
|
|
200
207
|
pdfFilesMap.set(`${dcmtInfo.TID}_${dcmtInfo.DID}`, file);
|
|
201
208
|
};
|
|
202
|
-
await downloadDcmtsAsync({ inputDcmts: dcmtsWithFile, downloadType: DownloadTypes.Dcmt, downloadMode: 'download', onFileDownloaded: collectFileForMerge, skipConfirmation: true, retrieveOptions: rfo, useCache: false });
|
|
209
|
+
await downloadDcmtsAsync({ inputDcmts: dcmtsWithFile, downloadType: DownloadTypes.Dcmt, downloadMode: 'download', onFileDownloaded: collectFileForMerge, skipConfirmation: true, retrieveOptions: rfo, useCache: false, downloadConcurrency: MERGE_DOWNLOAD_CONCURRENCY });
|
|
203
210
|
}
|
|
204
211
|
// Ricostruisci l'array pdfFiles nell'ordine originale di selezione
|
|
205
212
|
const pdfFiles = [];
|
|
@@ -24,6 +24,13 @@ export interface DownloadDcmtsAsyncParams {
|
|
|
24
24
|
* - false (default): usa il nome file recuperato dal backend (file.name) come priorità
|
|
25
25
|
*/
|
|
26
26
|
useInputFileName?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Numero di download eseguiti in parallelo (default 1 = sequenziale, comportamento preesistente).
|
|
29
|
+
* Valori > 1 vanno usati solo quando il chiamante consuma i file via
|
|
30
|
+
* `onFileDownloaded`: nel percorso senza callback ogni file genera un click su un
|
|
31
|
+
* tag <a download>, e più click ravvicinati vengono bloccati dal browser.
|
|
32
|
+
*/
|
|
33
|
+
downloadConcurrency?: number;
|
|
27
34
|
}
|
|
28
35
|
export interface UseDcmtOperationsReturn {
|
|
29
36
|
abortController: AbortController;
|
|
@@ -9,6 +9,10 @@ import { importCertificatesAsync } from '../helper/certificateImportHelper';
|
|
|
9
9
|
import { isXMLFileExt } from '../helper/dcmtsHelper';
|
|
10
10
|
import { ShowConfirm } from '../components/base/TMConfirm';
|
|
11
11
|
import { generateTargetFileName } from '../components/features/documents/copyAndMergeDcmtsShared';
|
|
12
|
+
/** Limite massimo di download paralleli accettato da `downloadDcmtsAsync`. */
|
|
13
|
+
const MAX_DOWNLOAD_CONCURRENCY = 8;
|
|
14
|
+
/** Intervallo minimo (ms) fra due aggiornamenti della progress bar di download. */
|
|
15
|
+
const DOWNLOAD_PROGRESS_THROTTLE_MS = 100;
|
|
12
16
|
const isScannerLicenseConfigured = () => {
|
|
13
17
|
try {
|
|
14
18
|
const scannerLicense = SDKUI_Globals.userSettings.advancedSettings.scannerLicense;
|
|
@@ -257,7 +261,7 @@ export const useDcmtOperations = () => {
|
|
|
257
261
|
ShowAlert({ message: SDKUI_Localizator.ZipCreatedSavedInFolder.replaceParams(zipFileName, "Zip creato con successo"), mode: 'success', duration: 5000, title: SDKUI_Localizator.CompressToZipFile });
|
|
258
262
|
}
|
|
259
263
|
};
|
|
260
|
-
const _downloadDcmtsAsync = async (inputDcmts, downloadMode = "download", onFileDownloaded, skipConfirmation = false, retrieveOptions, useCache = true, showSuccessAlert = true, useInputFileName = false) => {
|
|
264
|
+
const _downloadDcmtsAsync = async (inputDcmts, downloadMode = "download", onFileDownloaded, skipConfirmation = false, retrieveOptions, useCache = true, showSuccessAlert = true, useInputFileName = false, downloadConcurrency = 1) => {
|
|
261
265
|
if (inputDcmts === undefined)
|
|
262
266
|
return;
|
|
263
267
|
if (inputDcmts.length <= 0)
|
|
@@ -286,85 +290,150 @@ export const useDcmtOperations = () => {
|
|
|
286
290
|
setShowSecondary(true);
|
|
287
291
|
setWaitPanelTitle(operationTitle);
|
|
288
292
|
abortController = new AbortController();
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
293
|
+
// Riferimento const: il narrowing su inputDcmts non sopravvive dentro le closure del pool.
|
|
294
|
+
const dcmts = inputDcmts;
|
|
295
|
+
/**
|
|
296
|
+
* Ogni richiesta usa la propria istanza di RetrieveFileOptions: con il pool più
|
|
297
|
+
* download sono in volo contemporaneamente e non possono condividere lo stesso oggetto.
|
|
298
|
+
*/
|
|
299
|
+
const buildRetrieveFileOptions = (dcmt) => {
|
|
300
|
+
const rfo = new RetrieveFileOptions();
|
|
301
|
+
rfo.retrieveReason = retrieveOptions?.retrieveReason ?? DcmtOpers.ShowFile;
|
|
302
|
+
if (retrieveOptions?.cvtFormat !== undefined)
|
|
303
|
+
rfo.cvtFormat = retrieveOptions.cvtFormat;
|
|
304
|
+
if (retrieveOptions?.generalRetrieveFormat !== undefined)
|
|
305
|
+
rfo.generalRetrieveFormat = retrieveOptions.generalRetrieveFormat;
|
|
306
|
+
rfo.invoiceRetrieveFormat = retrieveOptions?.invoiceRetrieveFormat ?? SDKUI_Globals.userSettings.searchSettings.invoiceRetrieveFormat;
|
|
307
|
+
rfo.orderRetrieveFormat = retrieveOptions?.orderRetrieveFormat ?? SDKUI_Globals.userSettings.searchSettings.orderRetrieveFormat;
|
|
308
|
+
return rfo;
|
|
309
|
+
};
|
|
310
|
+
const results = new Array(dcmts.length);
|
|
311
|
+
setWaitPanelMaxValuePrimary(dcmts.length);
|
|
312
|
+
// ---- Progress secondaria: aggregata sui download in volo e throttlata ----
|
|
313
|
+
// Gli eventi di avanzamento di axios arrivano a ogni chunk: senza throttle ognuno
|
|
314
|
+
// provoca un re-render dell'intera form (relation viewer compreso).
|
|
315
|
+
const inFlightBytes = new Map();
|
|
316
|
+
let pendingSecondary;
|
|
317
|
+
let secondaryTimer;
|
|
318
|
+
const flushSecondaryProgress = () => {
|
|
319
|
+
secondaryTimer = undefined;
|
|
320
|
+
if (!pendingSecondary)
|
|
321
|
+
return;
|
|
322
|
+
setWaitPanelMaxValueSecondary(pendingSecondary.max);
|
|
323
|
+
setWaitPanelValueSecondary(pendingSecondary.value);
|
|
324
|
+
setWaitPanelTextSecondary(pendingSecondary.text);
|
|
325
|
+
pendingSecondary = undefined;
|
|
326
|
+
};
|
|
327
|
+
const publishSecondaryProgress = () => {
|
|
328
|
+
let loaded = 0;
|
|
329
|
+
let total = 0;
|
|
330
|
+
for (const bytes of inFlightBytes.values()) {
|
|
331
|
+
loaded += bytes.loaded;
|
|
332
|
+
total += bytes.total;
|
|
305
333
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
334
|
+
pendingSecondary = total > 0
|
|
335
|
+
? { value: loaded, max: total, text: `Downloading... ${Globalization.getNumberDisplayValue(loaded, true)} / ${Globalization.getNumberDisplayValue(total, true)}` }
|
|
336
|
+
: { value: 0, max: 0, text: '' };
|
|
337
|
+
if (secondaryTimer === undefined)
|
|
338
|
+
secondaryTimer = setTimeout(flushSecondaryProgress, DOWNLOAD_PROGRESS_THROTTLE_MS);
|
|
339
|
+
};
|
|
340
|
+
// ---- Pool di download ----
|
|
341
|
+
let nextIndex = 0;
|
|
342
|
+
let started = 0;
|
|
343
|
+
let completed = 0;
|
|
344
|
+
let abortReported = false;
|
|
345
|
+
const downloadOne = async (i) => {
|
|
346
|
+
started++;
|
|
347
|
+
setWaitPanelTextPrimary(`Download file ${started} di ${dcmts.length}`);
|
|
348
|
+
let file;
|
|
349
|
+
const cacheKey = `${dcmts[i].TID}-${dcmts[i].DID}`;
|
|
350
|
+
if (useCache && dcmtsFileCacheDownload.has(cacheKey)) {
|
|
351
|
+
file = dcmtsFileCacheDownload.get(cacheKey);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
inFlightBytes.set(i, { loaded: 0, total: 0 });
|
|
355
|
+
try {
|
|
356
|
+
file = await SDK_Globals.tmSession?.NewSearchEngine().RetrieveFileAsync(dcmts[i].TID, dcmts[i].DID, buildRetrieveFileOptions(dcmts[i]), abortController.signal, (pd) => {
|
|
357
|
+
inFlightBytes.set(i, { loaded: pd.ProgressBarValue, total: pd.ProgressBarMaximum ?? 0 });
|
|
358
|
+
publishSecondaryProgress();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
inFlightBytes.delete(i);
|
|
363
|
+
publishSecondaryProgress();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
completed++;
|
|
367
|
+
setWaitPanelValuePrimary(completed);
|
|
368
|
+
if (downloadMode === "openInNewWindow") {
|
|
369
|
+
if (onFileDownloaded && file) {
|
|
370
|
+
onFileDownloaded(file, dcmts[i]);
|
|
312
371
|
}
|
|
313
372
|
else {
|
|
314
|
-
|
|
315
|
-
if (firstBlock) {
|
|
316
|
-
maxFileSize = pd.ProgressBarMaximum ?? 0;
|
|
317
|
-
setWaitPanelMaxValueSecondary(maxFileSize);
|
|
318
|
-
firstBlock = false;
|
|
319
|
-
}
|
|
320
|
-
setWaitPanelValueSecondary(pd.ProgressBarValue);
|
|
321
|
-
setWaitPanelTextSecondary(`Downloading... ${Globalization.getNumberDisplayValue(pd.ProgressBarValue, true)} / ${Globalization.getNumberDisplayValue(maxFileSize, true)}`);
|
|
322
|
-
if (pd.ProgressBarValue === pd.ProgressBarMaximum) {
|
|
323
|
-
setWaitPanelMaxValueSecondary(0);
|
|
324
|
-
setWaitPanelValueSecondary(0);
|
|
325
|
-
setWaitPanelTextSecondary('');
|
|
326
|
-
firstBlock = true;
|
|
327
|
-
}
|
|
328
|
-
});
|
|
373
|
+
window.open(window.URL.createObjectURL(file), '_blank', 'noopener');
|
|
329
374
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
(
|
|
334
|
-
? onFileDownloaded(file, inputDcmts[i])
|
|
335
|
-
: window.open(fileURL, '_blank', 'noopener');
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
if (onFileDownloaded && file) {
|
|
378
|
+
await onFileDownloaded(file, dcmts[i]);
|
|
336
379
|
}
|
|
337
380
|
else {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
381
|
+
// L'object URL serve solo in questo ramo: creato qui e revocato dopo il
|
|
382
|
+
// click, così i blob non restano agganciati al documento per sempre.
|
|
383
|
+
const fileURL = window.URL.createObjectURL(file);
|
|
384
|
+
const alink2 = document.createElement('a');
|
|
385
|
+
alink2.href = fileURL;
|
|
386
|
+
// Usa useInputFileName per determinare la priorità del nome file
|
|
387
|
+
alink2.download = getDownloadFileName(file, dcmts[i], useInputFileName);
|
|
388
|
+
alink2.target = "_blank";
|
|
389
|
+
alink2.rel = "noreferrer";
|
|
390
|
+
alink2.click();
|
|
391
|
+
// 60s come per l'iframe di stampa: revocare troppo presto può far fallire
|
|
392
|
+
// il download se il browser non ha ancora risolto il blob URL.
|
|
393
|
+
setTimeout(() => window.URL.revokeObjectURL(fileURL), 60000);
|
|
350
394
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
dcmtsFileCacheDownload.set(cacheKey, file);
|
|
395
|
+
}
|
|
396
|
+
if (useCache) {
|
|
397
|
+
if (dcmtsFileCacheDownload.size >= CACHE_SIZE_LIMIT) {
|
|
398
|
+
const oldestKey = dcmtsFileCacheDownload.keys().next().value;
|
|
399
|
+
dcmtsFileCacheDownload.delete(oldestKey);
|
|
357
400
|
}
|
|
358
|
-
|
|
401
|
+
dcmtsFileCacheDownload.set(cacheKey, file);
|
|
359
402
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
403
|
+
results[i] = { rowIndex: i, id1: dcmts[i].TID, id2: dcmts[i].DID, resultType: ResultTypes.SUCCESS };
|
|
404
|
+
};
|
|
405
|
+
const runWorker = async () => {
|
|
406
|
+
while (true) {
|
|
407
|
+
const i = nextIndex++;
|
|
408
|
+
if (i >= dcmts.length)
|
|
409
|
+
return;
|
|
410
|
+
if (abortController.signal.aborted) {
|
|
411
|
+
// Un solo avviso di interruzione anche quando più worker si fermano insieme.
|
|
412
|
+
if (!abortReported) {
|
|
413
|
+
abortReported = true;
|
|
414
|
+
results[i] = { rowIndex: i, id1: dcmts[i].TID, id2: dcmts[i].DID, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${completed} documenti` };
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await downloadOne(i);
|
|
420
|
+
}
|
|
421
|
+
catch (ex) {
|
|
422
|
+
results[i] = { rowIndex: i, id1: dcmts[i].TID, id2: dcmts[i].DID, resultType: ResultTypes.ERROR, description: getExceptionMessage(ex) };
|
|
423
|
+
const err = ex;
|
|
424
|
+
if (err.name === 'CanceledError') {
|
|
425
|
+
ShowAlert({ message: err.message, mode: 'warning', duration: 3000, title: 'Abort' });
|
|
426
|
+
}
|
|
365
427
|
}
|
|
366
428
|
}
|
|
367
|
-
}
|
|
429
|
+
};
|
|
430
|
+
// `|| 1` intercetta NaN/0/negativi: senza, Array.from({length: NaN}) non creerebbe
|
|
431
|
+
// alcun worker e il download terminerebbe in silenzio senza scaricare nulla.
|
|
432
|
+
const workers = Math.max(1, Math.min(Math.trunc(downloadConcurrency) || 1, MAX_DOWNLOAD_CONCURRENCY, dcmts.length));
|
|
433
|
+
await Promise.all(Array.from({ length: workers }, () => runWorker()));
|
|
434
|
+
if (secondaryTimer !== undefined)
|
|
435
|
+
clearTimeout(secondaryTimer);
|
|
436
|
+
const result = results.filter((r) => r !== undefined);
|
|
368
437
|
setWaitPanelTextPrimary('');
|
|
369
438
|
setWaitPanelMaxValuePrimary(0);
|
|
370
439
|
setWaitPanelValuePrimary(0);
|
|
@@ -436,12 +505,13 @@ export const useDcmtOperations = () => {
|
|
|
436
505
|
};
|
|
437
506
|
const downloadDcmtsAsync = async (params) => {
|
|
438
507
|
const { inputDcmts, downloadType = DownloadTypes.Attachment, downloadMode = "download", onFileDownloaded, confirmAttachments, skipConfirmation = false, retrieveOptions, useCache = true, showSuccessAlert = true, useInputFileName = false, // Default: usa il nome dal backend
|
|
508
|
+
downloadConcurrency = 1, // Default: sequenziale
|
|
439
509
|
} = params;
|
|
440
510
|
switch (downloadType) {
|
|
441
511
|
// Per il download di documenti, passa useInputFileName per controllare la priorità del nome
|
|
442
|
-
case DownloadTypes.Dcmt: return await _downloadDcmtsAsync(inputDcmts, downloadMode, onFileDownloaded, skipConfirmation, retrieveOptions, useCache, showSuccessAlert, useInputFileName);
|
|
512
|
+
case DownloadTypes.Dcmt: return await _downloadDcmtsAsync(inputDcmts, downloadMode, onFileDownloaded, skipConfirmation, retrieveOptions, useCache, showSuccessAlert, useInputFileName, downloadConcurrency);
|
|
443
513
|
case DownloadTypes.Attachment: return await _downloadAttachmentsAsync(inputDcmts, confirmAttachments, showSuccessAlert);
|
|
444
|
-
default: return await _downloadDcmtsAsync(inputDcmts, undefined, undefined, skipConfirmation, retrieveOptions, useCache, showSuccessAlert, useInputFileName);
|
|
514
|
+
default: return await _downloadDcmtsAsync(inputDcmts, undefined, undefined, skipConfirmation, retrieveOptions, useCache, showSuccessAlert, useInputFileName, downloadConcurrency);
|
|
445
515
|
}
|
|
446
516
|
};
|
|
447
517
|
const uploadDcmtsAsync = async (inputDcmts, operationTitle, operType, actionAfterOperationAsync) => {
|