diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte index 7a171e55..7160ad32 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte @@ -31,6 +31,7 @@ let { let files = new SvelteMap(); let activeId = $state(null); let chunkProgress = $state<{ done: number; total: number } | undefined>(undefined); +let saving = $state(false); // --- Shared metadata --- let senderId = $state(untrack(() => initialSenderId)); @@ -84,6 +85,8 @@ onDestroy(() => { // --- Save --- async function save() { + if (saving) return; + saving = true; const entries = Array.from(files.values()); const chunkSize = 10; const chunks: FileEntry[][] = []; @@ -124,6 +127,7 @@ async function save() { } chunkProgress = { done: i + 1, total: chunks.length }; } + saving = false; if (!hadErrors) goto('/documents'); } @@ -276,6 +280,7 @@ async function save() { chunkProgress={chunkProgress} onSave={save} onDiscard={discardAll} + disabled={saving} /> diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts index 32e8fdef..521f83a1 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts @@ -206,6 +206,34 @@ describe('BulkDocumentEditLayout', () => { ); }); + it('save() does not call fetch a second time when already saving', async () => { + let resolveFirst: (() => void) | undefined; + const mockFetch = vi.fn().mockImplementation( + () => + new Promise((resolve) => { + resolveFirst = () => + resolve({ + ok: true, + json: async () => ({ created: [], updated: [], errors: [] }) + } as Response); + }) + ); + vi.stubGlobal('fetch', mockFetch); + + const { container } = render(BulkDocumentEditLayout, {}); + await addFilesViaInput(container, [makeFile('a.pdf')]); + + const saveBtn = container.querySelector( + 'button[data-testid="bulk-save-btn"]' + ) as HTMLButtonElement; + saveBtn.click(); // first click — fetch is in-flight + saveBtn.click(); // second click — should be a no-op + + resolveFirst?.(); + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1), { timeout: 3000 }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + 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')]); diff --git a/frontend/src/lib/components/document/UploadSaveBar.svelte b/frontend/src/lib/components/document/UploadSaveBar.svelte index 4a1f65e0..d404e864 100644 --- a/frontend/src/lib/components/document/UploadSaveBar.svelte +++ b/frontend/src/lib/components/document/UploadSaveBar.svelte @@ -5,12 +5,14 @@ let { fileCount, chunkProgress, onSave, - onDiscard + onDiscard, + disabled = false }: { fileCount: number; chunkProgress?: { done: number; total: number }; onSave: () => void; - onDiscard: () => void; + onDiscard: () => void | Promise; + disabled?: boolean; } = $props(); @@ -37,7 +39,7 @@ let {