From dd13f736c0f6707c31779d06eec2e026da766755 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 18 Sep 2026 07:37:25 +0200 Subject: [PATCH] mutipart --- src/api/ApiClient.js | 6 ++- src/api/index.js | 8 ++- src/components/manifest/index.js | 1 + src/components/manifest/manifestConfig.js | 64 ++++++++++++++++++++++- src/pages/lieferung/neu.jsx | 2 + 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/api/ApiClient.js b/src/api/ApiClient.js index d613ee0..7ff57da 100644 --- a/src/api/ApiClient.js +++ b/src/api/ApiClient.js @@ -406,8 +406,10 @@ export class ApiClient { const headers = { ...extraHeaders }; let payload; if (body !== undefined) { - if (raw) { - headers['Content-Type'] = contentType; + if (typeof FormData !== 'undefined' && body instanceof FormData) { + payload = body; + } else if (raw) { + if (contentType) headers['Content-Type'] = contentType; payload = body; } else { headers['Content-Type'] = contentType || 'application/json'; diff --git a/src/api/index.js b/src/api/index.js index bfe7be7..6068e7c 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -20,7 +20,11 @@ function readableHeaders(headers) { } async function http(path, { method = 'GET', body } = {}) { - const headers = { 'Content-Type': 'application/json' }; + const isFormData = typeof FormData !== 'undefined' && body instanceof FormData; + const headers = {}; + if (!isFormData) { + headers['Content-Type'] = 'application/json'; + } const url = new URL(path, window.location.origin).href; logApi('request', { @@ -38,7 +42,7 @@ async function http(path, { method = 'GET', body } = {}) { method, headers, credentials: 'include', - body: body ? JSON.stringify(body) : undefined, + body: isFormData ? body : (body ? JSON.stringify(body) : undefined), }); const responseHeaders = readableHeaders(res.headers); diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js index 221c1d6..201091e 100644 --- a/src/components/manifest/index.js +++ b/src/components/manifest/index.js @@ -15,6 +15,7 @@ export { lineFromApi, buildManifestBody, buildFilesForUpload, + collectFilesForUpload, restoreAttachmentsFromApi, dateInputValue, } from './manifestConfig'; diff --git a/src/components/manifest/manifestConfig.js b/src/components/manifest/manifestConfig.js index 54ae567..cf5e564 100644 --- a/src/components/manifest/manifestConfig.js +++ b/src/components/manifest/manifestConfig.js @@ -344,6 +344,53 @@ export async function restoreAttachmentsFromApi(apiFiles = [], existingAttachmen return attachments; } +/** + * Collect new or modified attachments for multipart upload + * @param {Array<{file: File, name: string, type: string, thumbnail?: string, text?: string, parentPdfId?: string|number, isPersisted?: boolean, isDirty?: boolean}>} attachments + * @param {{ onlyNew?: boolean }} [options] + * @returns {Array<{file: File, name: string, mimetype: string, meta?: object}>} + */ +export function collectFilesForUpload(attachments = [], { onlyNew = false } = {}) { + const files = []; + for (const attachment of attachments) { + if (!attachment) continue; + // Skip already persisted attachments in update mode unless modified + if (onlyNew && attachment.isPersisted && !attachment.isDirty) { + continue; + } + if (attachment.isPersisted && !attachment.isDirty && (!attachment.file || attachment.file.size === 0)) { + continue; + } + + let fileToUpload = attachment.file; + if (!fileToUpload || fileToUpload.size === 0) { + if (attachment.text) { + fileToUpload = new File([attachment.text], attachment.name || 'file.txt', { + type: 'text/plain;charset=utf-8', + }); + } else { + continue; + } + } + + const meta = {}; + if (attachment.parentPdfId) { + meta.parentPdfId = String(attachment.parentPdfId); + } + if (attachment.text) { + meta.text = String(attachment.text); + } + + files.push({ + file: fileToUpload, + name: attachment.name || fileToUpload.name, + mimetype: attachment.type || fileToUpload.type, + meta: Object.keys(meta).length > 0 ? meta : undefined, + }); + } + return files; +} + /** * @param {{ number: string, externalNumber?: string, date: string, partyId: number|null, lines: object[], attachments?: object[] }} form * @param {{ partyField: 'supplierId'|'entityId', requireAtLeastOne: boolean, externalNumber?: boolean }} opts @@ -380,9 +427,22 @@ export async function buildManifestBody( // Add files if attachments exist if (form.attachments && form.attachments.length > 0) { const isUpdate = !requireAtLeastOne; - const filesToUpload = await buildFilesForUpload(form.attachments, { onlyNew: isUpdate }); + const filesToUpload = collectFilesForUpload(form.attachments, { onlyNew: isUpdate }); if (filesToUpload.length > 0) { - body.files = filesToUpload; + const filesMeta = filesToUpload + .filter((f) => f.meta) + .map((f) => ({ name: f.name, meta: f.meta })); + + if (filesMeta.length > 0) { + body.filesMeta = filesMeta; + } + + const formData = new FormData(); + formData.append('data', JSON.stringify(body)); + for (const item of filesToUpload) { + formData.append('files', item.file, item.name); + } + return formData; } } diff --git a/src/pages/lieferung/neu.jsx b/src/pages/lieferung/neu.jsx index 2b62237..bf9a271 100644 --- a/src/pages/lieferung/neu.jsx +++ b/src/pages/lieferung/neu.jsx @@ -379,6 +379,7 @@ class LieferungNeuPage extends PureComponent { file: txtFile, size: txtFile.size, text, + isDirty: true, }); } else { deliveryEditorAutomat.actions.addAttachment({ @@ -606,6 +607,7 @@ class LieferungNeuPage extends PureComponent { file: newFile, size: newFile.size, text: newText, + isDirty: true, }); this.setState({