From 74b473e3d7a9196037a4ef632888f3f5e887f603 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 25 Apr 2026 01:15:41 +0200 Subject: [PATCH] fix(bulk-upload): match error chips by filename, not by chunk position save() was marking the first N files in a chunk as errored (where N = the error count returned by the backend), but the backend errors are keyed by filename. A failure for file[2] would incorrectly mark file[0] as the error. Now builds a Set of error filenames and matches chunk entries by file.name. Test added: save marks only the file whose filename matches the backend error. Co-Authored-By: Claude Sonnet 4.6 --- .../document/BulkDocumentEditLayout.svelte | 12 +++++--- .../BulkDocumentEditLayout.svelte.spec.ts | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte index da4e6d3b..3959a54e 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte @@ -109,10 +109,14 @@ async function save() { if (!res.ok) { hadErrors = true; const body = await res.json().catch(() => ({ errors: [] })); - const errorCount = (body.errors ?? []).length; - for (let j = 0; j < errorCount && j < chunk.length; j++) { - const e = files.get(chunk[j].id); - if (e) files.set(chunk[j].id, { ...e, status: 'error' }); + const errorFilenames = new Set( + (body.errors ?? []).map((err: { filename: string }) => err.filename) + ); + for (const entry of chunk) { + if (errorFilenames.has(entry.file.name)) { + const e = files.get(entry.id); + if (e) files.set(entry.id, { ...e, status: 'error' }); + } } } chunkProgress = { done: i + 1, total: chunks.length }; diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts index c5dc5e85..b800d218 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts @@ -159,6 +159,34 @@ describe('BulkDocumentEditLayout', () => { expect(goto).not.toHaveBeenCalled(); }); + it('save marks only the file whose filename matches the backend error, not adjacent files', async () => { + // backend returns error keyed to b.pdf — only b.pdf chip should get data-status="error" + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + json: async () => ({ errors: [{ filename: 'b.pdf', code: 'FILE_UPLOAD_FAILED' }] }) + }); + vi.stubGlobal('fetch', mockFetch); + + const { container } = render(BulkDocumentEditLayout, {}); + await addFilesViaInput(container, [makeFile('a.pdf'), makeFile('b.pdf'), makeFile('c.pdf')]); + + const saveBtn = container.querySelector( + 'button[data-testid="bulk-save-btn"]' + ) as HTMLButtonElement; + saveBtn.click(); + + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1), { timeout: 3000 }); + + await vi.waitFor( + () => { + const errorChips = container.querySelectorAll('[data-chip-id][data-status="error"]'); + expect(errorChips.length).toBe(1); + expect(errorChips[0].textContent).toContain('b'); + }, + { timeout: 1000 } + ); + }); + it('discard-all resets to N=0 state and shows drop zone', async () => { const { container } = render(BulkDocumentEditLayout, {}); await addFilesViaInput(container, [makeFile('a.pdf'), makeFile('b.pdf')]);