fix(bulk-upload): skip navigation when any chunk fails to upload
goto('/documents') fired unconditionally, discarding error chips and
leaving the user with no feedback on which files failed. Now only
navigates when hadErrors is false after all chunks complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
chunkProgress = { done: 0, total: chunks.length };
|
chunkProgress = { done: 0, total: chunks.length };
|
||||||
|
|
||||||
|
let hadErrors = false;
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
const chunk = chunks[i];
|
const chunk = chunks[i];
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -106,6 +107,7 @@ async function save() {
|
|||||||
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
||||||
const res = await fetch('/api/documents/quick-upload', { method: 'POST', body: formData });
|
const res = await fetch('/api/documents/quick-upload', { method: 'POST', body: formData });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
hadErrors = true;
|
||||||
const body = await res.json().catch(() => ({ errors: [] }));
|
const body = await res.json().catch(() => ({ errors: [] }));
|
||||||
const errorCount = (body.errors ?? []).length;
|
const errorCount = (body.errors ?? []).length;
|
||||||
for (let j = 0; j < errorCount && j < chunk.length; j++) {
|
for (let j = 0; j < errorCount && j < chunk.length; j++) {
|
||||||
@@ -115,7 +117,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
chunkProgress = { done: i + 1, total: chunks.length };
|
chunkProgress = { done: i + 1, total: chunks.length };
|
||||||
}
|
}
|
||||||
goto('/documents');
|
if (!hadErrors) goto('/documents');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page, userEvent } from 'vitest/browser';
|
import { page, userEvent } from 'vitest/browser';
|
||||||
import BulkDocumentEditLayout from './BulkDocumentEditLayout.svelte';
|
import BulkDocumentEditLayout from './BulkDocumentEditLayout.svelte';
|
||||||
|
|
||||||
|
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeFile(name: string): File {
|
function makeFile(name: string): File {
|
||||||
@@ -80,9 +84,6 @@ describe('BulkDocumentEditLayout', () => {
|
|||||||
});
|
});
|
||||||
vi.stubGlobal('fetch', mockFetch);
|
vi.stubGlobal('fetch', mockFetch);
|
||||||
|
|
||||||
// Also stub goto to prevent navigation errors in test
|
|
||||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
|
||||||
|
|
||||||
const { container } = render(BulkDocumentEditLayout, {});
|
const { container } = render(BulkDocumentEditLayout, {});
|
||||||
const files = Array.from({ length: 12 }, (_, i) => makeFile(`f${i}.pdf`));
|
const files = Array.from({ length: 12 }, (_, i) => makeFile(`f${i}.pdf`));
|
||||||
await addFilesViaInput(container, files);
|
await addFilesViaInput(container, files);
|
||||||
@@ -103,7 +104,6 @@ describe('BulkDocumentEditLayout', () => {
|
|||||||
json: async () => ({ errors: [{ filename: 'f0.pdf', code: 'FILE_UPLOAD_FAILED' }] })
|
json: async () => ({ errors: [{ filename: 'f0.pdf', code: 'FILE_UPLOAD_FAILED' }] })
|
||||||
});
|
});
|
||||||
vi.stubGlobal('fetch', mockFetch);
|
vi.stubGlobal('fetch', mockFetch);
|
||||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
|
||||||
|
|
||||||
const { container } = render(BulkDocumentEditLayout, {});
|
const { container } = render(BulkDocumentEditLayout, {});
|
||||||
await addFilesViaInput(container, [makeFile('f0.pdf')]);
|
await addFilesViaInput(container, [makeFile('f0.pdf')]);
|
||||||
@@ -140,6 +140,25 @@ describe('BulkDocumentEditLayout', () => {
|
|||||||
expect(metadataJson).toHaveProperty('tagNames');
|
expect(metadataJson).toHaveProperty('tagNames');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('save() does not navigate when chunk returns non-ok response', async () => {
|
||||||
|
const mockFetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
json: async () => ({ errors: [{ filename: 'f0.pdf', code: 'FILE_UPLOAD_FAILED' }] })
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', mockFetch);
|
||||||
|
|
||||||
|
const { container } = render(BulkDocumentEditLayout, {});
|
||||||
|
await addFilesViaInput(container, [makeFile('f0.pdf')]);
|
||||||
|
|
||||||
|
const saveBtn = container.querySelector(
|
||||||
|
'button[data-testid="bulk-save-btn"]'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
saveBtn.click();
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1), { timeout: 3000 });
|
||||||
|
expect(goto).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('discard-all resets to N=0 state and shows drop zone', async () => {
|
it('discard-all resets to N=0 state and shows drop zone', async () => {
|
||||||
const { container } = render(BulkDocumentEditLayout, {});
|
const { container } = render(BulkDocumentEditLayout, {});
|
||||||
await addFilesViaInput(container, [makeFile('a.pdf'), makeFile('b.pdf')]);
|
await addFilesViaInput(container, [makeFile('a.pdf'), makeFile('b.pdf')]);
|
||||||
|
|||||||
Reference in New Issue
Block a user