feat(dropzone): emit onUploadComplete callback with created count
Optional callback lets the parent route pop a post-upload banner without lifting state into a store. Dashboard uses it to drive UploadSuccessBanner (issue #296). Only fires when the server actually created new documents — duplicates and errors do not trigger it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,12 @@ import { getErrorMessage } from '$lib/errors';
|
||||
|
||||
const ACCEPTED_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/tiff'];
|
||||
|
||||
interface Props {
|
||||
onUploadComplete?: (count: number) => void;
|
||||
}
|
||||
|
||||
let { onUploadComplete }: Props = $props();
|
||||
|
||||
let isDragging = $state(false);
|
||||
let windowDragging = $state(false);
|
||||
let dragCounter = 0;
|
||||
@@ -80,6 +86,7 @@ async function uploadFiles(files: File[]) {
|
||||
const result = JSON.parse(body);
|
||||
if (result.created?.length > 0) {
|
||||
messages.push({ text: m.upload_success({ count: result.created.length }), isError: false });
|
||||
onUploadComplete?.(result.created.length);
|
||||
}
|
||||
for (const doc of result.updated ?? []) {
|
||||
messages.push({
|
||||
|
||||
84
frontend/src/routes/DropZone.svelte.spec.ts
Normal file
84
frontend/src/routes/DropZone.svelte.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
|
||||
import DropZone from './DropZone.svelte';
|
||||
|
||||
vi.mock('$app/navigation', () => ({ invalidateAll: vi.fn(async () => {}) }));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function stubXhrWith(responseBody: string) {
|
||||
class FakeXhr {
|
||||
upload = { addEventListener: vi.fn() };
|
||||
status = 200;
|
||||
responseText = responseBody;
|
||||
private loadHandler: (() => void) | null = null;
|
||||
open = vi.fn();
|
||||
addEventListener = vi.fn((event: string, handler: () => void) => {
|
||||
if (event === 'load') this.loadHandler = handler;
|
||||
});
|
||||
send = vi.fn(() => {
|
||||
queueMicrotask(() => this.loadHandler?.());
|
||||
});
|
||||
}
|
||||
vi.stubGlobal('XMLHttpRequest', FakeXhr);
|
||||
}
|
||||
|
||||
describe('DropZone onUploadComplete', () => {
|
||||
it('invokes callback with created.length after a successful upload', async () => {
|
||||
stubXhrWith(JSON.stringify({ created: [{ id: 'd1' }, { id: 'd2' }], updated: [], errors: [] }));
|
||||
|
||||
const onUploadComplete = vi.fn();
|
||||
render(DropZone, { onUploadComplete });
|
||||
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement | null;
|
||||
expect(input).not.toBeNull();
|
||||
const file = new File(['%PDF-1.4'], 'test.pdf', { type: 'application/pdf' });
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
input!.files = dataTransfer.files;
|
||||
input!.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(onUploadComplete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(onUploadComplete).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('does not invoke callback when no files were created', async () => {
|
||||
stubXhrWith(JSON.stringify({ created: [], updated: [], errors: [] }));
|
||||
|
||||
const onUploadComplete = vi.fn();
|
||||
render(DropZone, { onUploadComplete });
|
||||
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const file = new File(['%PDF-1.4'], 'dupe.pdf', { type: 'application/pdf' });
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
input.files = dt.files;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
// Wait a tick to let the microtask flush
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
expect(onUploadComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('works when the onUploadComplete prop is not supplied', async () => {
|
||||
stubXhrWith(JSON.stringify({ created: [{ id: 'x' }], updated: [], errors: [] }));
|
||||
|
||||
render(DropZone, {});
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const file = new File(['%PDF-1.4'], 'x.pdf', { type: 'application/pdf' });
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
input.files = dt.files;
|
||||
// Should not throw
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await expect.element(page.getByText(/1 Dokument/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user