From 4d0b8b1570e5862f6f03b73c14fa63d13579ca8f Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 22 May 2026 18:20:59 +0200 Subject: [PATCH] refactor(document): migrate frontend from DocumentSearchItem to flat DocumentListItem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All components, specs, and the generated API client now use the new DocumentListItem shape — flat access (item.title, item.sender) instead of the removed item.document.* nesting. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/document/DocumentMultiSelect.svelte | 6 +- frontend/src/lib/document/DocumentRow.svelte | 6 +- .../lib/document/DocumentRow.svelte.spec.ts | 100 +++++------- .../lib/document/DocumentRow.svelte.test.ts | 56 ++++--- frontend/src/lib/generated/api.ts | 41 +++-- frontend/src/routes/DocumentList.svelte | 24 +-- .../src/routes/DocumentList.svelte.spec.ts | 153 ++++++++---------- frontend/src/routes/documents/+page.server.ts | 6 +- 8 files changed, 193 insertions(+), 199 deletions(-) diff --git a/frontend/src/lib/document/DocumentMultiSelect.svelte b/frontend/src/lib/document/DocumentMultiSelect.svelte index 4dabda60..4d2e42ba 100644 --- a/frontend/src/lib/document/DocumentMultiSelect.svelte +++ b/frontend/src/lib/document/DocumentMultiSelect.svelte @@ -5,7 +5,7 @@ import { clickOutside } from '$lib/shared/actions/clickOutside'; import { formatDate } from '$lib/shared/utils/date'; type Document = components['schemas']['Document']; -type DocumentSearchItem = components['schemas']['DocumentSearchItem']; +type DocumentListItem = components['schemas']['DocumentListItem']; interface Props { selectedDocuments?: Document[]; @@ -45,8 +45,8 @@ function handleInput() { try { const res = await fetch(`/api/documents/search?q=${encodeURIComponent(searchTerm)}&size=10`); if (res.ok) { - const body: { items: DocumentSearchItem[] } = await res.json(); - const docs = body.items.map((it) => it.document); + const body: { items: DocumentListItem[] } = await res.json(); + const docs = body.items as unknown as Document[]; results = docs.filter((d) => !selectedDocuments.some((s) => s.id === d.id)); } } catch { diff --git a/frontend/src/lib/document/DocumentRow.svelte b/frontend/src/lib/document/DocumentRow.svelte index 70c85ed3..903ed727 100644 --- a/frontend/src/lib/document/DocumentRow.svelte +++ b/frontend/src/lib/document/DocumentRow.svelte @@ -9,11 +9,11 @@ import ProgressRing from '$lib/shared/primitives/ProgressRing.svelte'; import ContributorStack from '$lib/shared/primitives/ContributorStack.svelte'; import DocumentThumbnail from './DocumentThumbnail.svelte'; -type DocumentSearchItem = components['schemas']['DocumentSearchItem']; +type DocumentListItem = components['schemas']['DocumentListItem']; -let { item, canWrite = false }: { item: DocumentSearchItem; canWrite?: boolean } = $props(); +let { item, canWrite = false }: { item: DocumentListItem; canWrite?: boolean } = $props(); -const doc = $derived(item.document); +const doc = $derived(item); const titleText = $derived(doc.title || doc.originalFilename); const titleOffsets = $derived(item.matchData?.titleOffsets ?? []); const titleSegments = $derived(applyOffsets(titleText, titleOffsets)); diff --git a/frontend/src/lib/document/DocumentRow.svelte.spec.ts b/frontend/src/lib/document/DocumentRow.svelte.spec.ts index 19b3c66a..79ce8913 100644 --- a/frontend/src/lib/document/DocumentRow.svelte.spec.ts +++ b/frontend/src/lib/document/DocumentRow.svelte.spec.ts @@ -14,24 +14,17 @@ afterEach(() => { bulkSelectionStore.clear(); }); -type DocumentSearchItem = components['schemas']['DocumentSearchItem']; +type DocumentListItem = components['schemas']['DocumentListItem']; -function makeItem(overrides: Partial = {}): DocumentSearchItem { +function makeItem(overrides: Partial = {}): DocumentListItem { return { - document: { - id: '1', - title: 'Testbrief', - originalFilename: 'testbrief.pdf', - status: 'UPLOADED', - documentDate: '2024-03-15', - sender: null, - receivers: [], - tags: [], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - metadataComplete: false, - scriptType: 'UNKNOWN' - }, + id: '1', + title: 'Testbrief', + originalFilename: 'testbrief.pdf', + documentDate: '2024-03-15', + sender: undefined, + receivers: [], + tags: [], matchData: { titleOffsets: [], senderMatched: false, @@ -55,14 +48,14 @@ describe('DocumentRow – title', () => { }); it('falls back to originalFilename when title is null', async () => { - const item = makeItem({ document: { ...makeItem().document, title: null } }); + const item = makeItem({ title: null as unknown as string }); render(DocumentRow, { item }); await expect.element(page.getByRole('heading', { name: 'testbrief.pdf' })).toBeInTheDocument(); }); it('renders a mark element for highlighted title offsets', async () => { const item = makeItem({ - document: { ...makeItem().document, title: 'Brief an Anna' }, + title: 'Brief an Anna', matchData: { titleOffsets: [{ start: 0, length: 5 }], senderMatched: false, @@ -109,9 +102,12 @@ describe('DocumentRow – snippet', () => { describe('DocumentRow – sender', () => { it('shows sender display name', async () => { const item = makeItem({ - document: { - ...makeItem().document, - sender: { id: 's1', displayName: 'Großmutter Maria' } + sender: { + id: 's1', + lastName: 'Maria', + displayName: 'Großmutter Maria', + personType: 'PERSON', + familyMember: false } }); render(DocumentRow, { item }); @@ -126,9 +122,12 @@ describe('DocumentRow – sender', () => { it('highlights the sender when senderMatched is true', async () => { const item = makeItem({ - document: { - ...makeItem().document, - sender: { id: 's1', displayName: 'Großmutter Maria' } + sender: { + id: 's1', + lastName: 'Maria', + displayName: 'Großmutter Maria', + personType: 'PERSON', + familyMember: false }, matchData: { ...makeItem().matchData, @@ -142,10 +141,15 @@ describe('DocumentRow – sender', () => { it('highlights a receiver when matchedReceiverIds includes its id', async () => { const item = makeItem({ - document: { - ...makeItem().document, - receivers: [{ id: 'r1', displayName: 'Onkel Karl' }] - }, + receivers: [ + { + id: 'r1', + lastName: 'Karl', + displayName: 'Onkel Karl', + personType: 'PERSON', + familyMember: false + } + ], matchData: { ...makeItem().matchData, matchedReceiverIds: ['r1'] @@ -162,10 +166,7 @@ describe('DocumentRow – sender', () => { describe('DocumentRow – summary', () => { it('renders the document summary when present', async () => { const item = makeItem({ - document: { - ...makeItem().document, - summary: 'Brief von Eugenie über die Heimreise aus dem Süden.' - } + summary: 'Brief von Eugenie über die Heimreise aus dem Süden.' }); render(DocumentRow, { item }); await expect @@ -180,7 +181,7 @@ describe('DocumentRow – summary', () => { it('applies summary search-match highlight via summaryOffsets', async () => { const item = makeItem({ - document: { ...makeItem().document, summary: 'Brief über Menton' }, + summary: 'Brief über Menton', matchData: { ...makeItem().matchData, summaryOffsets: [{ start: 11, length: 6 }] @@ -196,25 +197,19 @@ describe('DocumentRow – summary', () => { describe('DocumentRow – archive chips', () => { it('renders the archive box chip when set', async () => { - const item = makeItem({ - document: { ...makeItem().document, archiveBox: 'K3' } - }); + const item = makeItem({ archiveBox: 'K3' }); render(DocumentRow, { item }); await expect.element(page.getByText('K3')).toBeInTheDocument(); }); it('renders the archive folder chip when set', async () => { - const item = makeItem({ - document: { ...makeItem().document, archiveFolder: 'Mappe A' } - }); + const item = makeItem({ archiveFolder: 'Mappe A' }); render(DocumentRow, { item }); await expect.element(page.getByText('Mappe A')).toBeInTheDocument(); }); it('renders the location chip when meta_location is set', async () => { - const item = makeItem({ - document: { ...makeItem().document, location: 'Berlin' } - }); + const item = makeItem({ location: 'Berlin' }); render(DocumentRow, { item }); await expect.element(page.getByText('Berlin')).toBeInTheDocument(); }); @@ -225,10 +220,7 @@ describe('DocumentRow – archive chips', () => { describe('DocumentRow – tags', () => { it('renders tag buttons', async () => { const item = makeItem({ - document: { - ...makeItem().document, - tags: [{ id: 't1', name: 'Familie', color: null, parentId: null }] - } + tags: [{ id: 't1', name: 'Familie' }] }); render(DocumentRow, { item }); await expect.element(page.getByRole('button', { name: 'Familie' })).toBeInTheDocument(); @@ -236,10 +228,7 @@ describe('DocumentRow – tags', () => { it('navigates to /documents?tag=… on tag click', async () => { const item = makeItem({ - document: { - ...makeItem().document, - tags: [{ id: 't1', name: 'Urlaub & Reise', color: null, parentId: null }] - } + tags: [{ id: 't1', name: 'Urlaub & Reise' }] }); render(DocumentRow, { item }); // Tailwind CSS isn't loaded in the vitest-browser client project, so the @@ -255,10 +244,7 @@ describe('DocumentRow – tags', () => { it('tag click does not navigate to the document detail page', async () => { const item = makeItem({ - document: { - ...makeItem().document, - tags: [{ id: 't2', name: 'Familie', color: null, parentId: null }] - } + tags: [{ id: 't2', name: 'Familie' }] }); render(DocumentRow, { item }); const before = window.location.href; @@ -281,7 +267,7 @@ describe('DocumentRow – bulk selection checkbox', () => { }); it('checkbox aria-label includes the document title', async () => { - const item = makeItem({ document: { ...makeItem().document, title: 'Brief an Anna' } }); + const item = makeItem({ title: 'Brief an Anna' }); render(DocumentRow, { item, canWrite: true }); await expect .element(page.getByRole('checkbox', { name: /Brief an Anna/i })) @@ -289,7 +275,7 @@ describe('DocumentRow – bulk selection checkbox', () => { }); it('toggling the checkbox calls bulkSelectionStore.toggle', async () => { - const item = makeItem({ document: { ...makeItem().document, id: 'doc-42' } }); + const item = makeItem({ id: 'doc-42' }); render(DocumentRow, { item, canWrite: true }); expect(bulkSelectionStore.has('doc-42')).toBe(false); @@ -300,7 +286,7 @@ describe('DocumentRow – bulk selection checkbox', () => { it('checked state mirrors the store', async () => { bulkSelectionStore.add('doc-99'); - const item = makeItem({ document: { ...makeItem().document, id: 'doc-99' } }); + const item = makeItem({ id: 'doc-99' }); render(DocumentRow, { item, canWrite: true }); await expect.element(page.getByRole('checkbox')).toBeChecked(); }); diff --git a/frontend/src/lib/document/DocumentRow.svelte.test.ts b/frontend/src/lib/document/DocumentRow.svelte.test.ts index 704428bc..58ac9bbd 100644 --- a/frontend/src/lib/document/DocumentRow.svelte.test.ts +++ b/frontend/src/lib/document/DocumentRow.svelte.test.ts @@ -20,10 +20,31 @@ const { default: DocumentRow } = await import('./DocumentRow.svelte'); afterEach(cleanup); -const sender = { id: 's1', displayName: 'Anna Schmidt' }; -const receiver = { id: 'r1', displayName: 'Bert Meier' }; +const sender = { + id: 's1', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON' as const, + familyMember: false +}; +const receiver = { + id: 'r1', + lastName: 'Meier', + displayName: 'Bert Meier', + personType: 'PERSON' as const, + familyMember: false +}; -const makeDoc = (overrides: Record = {}) => ({ +const emptyMatchData = { + titleOffsets: [], + senderMatched: false, + matchedReceiverIds: [], + matchedTagIds: [], + snippetOffsets: [], + summaryOffsets: [] +}; + +const baseItem = (overrides: Record = {}) => ({ id: 'd1', title: 'Brief 1923', originalFilename: 'b.pdf', @@ -31,20 +52,14 @@ const makeDoc = (overrides: Record = {}) => ({ sender, receivers: [receiver], tags: [], - thumbnailUrl: null, - contentType: 'application/pdf', - summary: null, - archiveBox: null, - archiveFolder: null, - location: null, - ...overrides -}); - -const baseItem = (docOverrides: Record = {}) => ({ - document: makeDoc(docOverrides), - matchData: null, + summary: undefined, + archiveBox: undefined, + archiveFolder: undefined, + location: undefined, + matchData: emptyMatchData, completionPercentage: 0, - contributors: [] + contributors: [], + ...overrides }); describe('DocumentRow', () => { @@ -121,12 +136,9 @@ describe('DocumentRow', () => { it('renders the snippet when matchData provides a transcriptionSnippet', async () => { render(DocumentRow, { props: { - item: { - document: makeDoc(), - matchData: { transcriptionSnippet: 'Hello world snippet' }, - completionPercentage: 50, - contributors: [] - } + item: baseItem({ + matchData: { ...emptyMatchData, transcriptionSnippet: 'Hello world snippet' } + }) } }); diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts index 764adce9..9a9a5408 100644 --- a/frontend/src/lib/generated/api.ts +++ b/frontend/src/lib/generated/api.ts @@ -2068,12 +2068,20 @@ export interface components { }; ImportStatus: { /** @enum {string} */ - state?: "IDLE" | "RUNNING" | "DONE" | "FAILED"; - statusCode?: string; + state: "IDLE" | "RUNNING" | "DONE" | "FAILED"; + statusCode: string; /** Format: int32 */ - processed?: number; + processed: number; + skippedFiles: components["schemas"]["SkippedFile"][]; /** Format: date-time */ startedAt?: string; + /** Format: int32 */ + skipped?: number; + }; + SkippedFile: { + filename: string; + /** @enum {string} */ + reason: "INVALID_FILENAME_PATH_TRAVERSAL" | "INVALID_PDF_SIGNATURE" | "FILE_READ_ERROR" | "ALREADY_EXISTS" | "S3_UPLOAD_FAILED"; }; BackfillStatus: { /** @enum {string} */ @@ -2197,10 +2205,10 @@ export interface components { totalStories: number; }; PersonSummaryDTO: { - title?: string; /** Format: uuid */ id?: string; displayName?: string; + title?: string; firstName?: string; lastName?: string; /** Format: int64 */ @@ -2307,14 +2315,14 @@ export interface components { /** Format: int32 */ totalPages?: number; pageable?: components["schemas"]["PageableObject"]; + first?: boolean; + last?: boolean; /** Format: int32 */ size?: number; content?: components["schemas"]["NotificationDTO"][]; /** Format: int32 */ number?: number; sort?: components["schemas"]["SortObject"]; - first?: boolean; - last?: boolean; /** Format: int32 */ numberOfElements?: number; empty?: boolean; @@ -2380,15 +2388,28 @@ export interface components { /** Format: int32 */ totalPages?: number; }; - DocumentSearchItem: { - document: components["schemas"]["Document"]; - matchData: components["schemas"]["SearchMatchData"]; + DocumentListItem: { + /** Format: uuid */ + id: string; + title: string; + originalFilename: string; + thumbnailUrl?: string; + /** Format: date */ + documentDate?: string; + sender?: components["schemas"]["Person"]; + receivers: components["schemas"]["Person"][]; + tags: components["schemas"]["Tag"][]; + archiveBox?: string; + archiveFolder?: string; + location?: string; + summary?: string; /** Format: int32 */ completionPercentage: number; contributors: components["schemas"]["ActivityActorDTO"][]; + matchData: components["schemas"]["SearchMatchData"]; }; DocumentSearchResult: { - items: components["schemas"]["DocumentSearchItem"][]; + items: components["schemas"]["DocumentListItem"][]; /** Format: int64 */ totalElements: number; /** Format: int32 */ diff --git a/frontend/src/routes/DocumentList.svelte b/frontend/src/routes/DocumentList.svelte index f2bd1c18..8d29c870 100644 --- a/frontend/src/routes/DocumentList.svelte +++ b/frontend/src/routes/DocumentList.svelte @@ -5,7 +5,7 @@ import DocumentRow from '$lib/document/DocumentRow.svelte'; import { SvelteMap } from 'svelte/reactivity'; import type { components } from '$lib/generated/api'; -type DocumentSearchItem = components['schemas']['DocumentSearchItem']; +type DocumentListItem = components['schemas']['DocumentListItem']; type SortMode = 'DATE' | 'TITLE' | 'SENDER' | 'RECEIVER' | 'UPLOAD_DATE' | 'RELEVANCE'; @@ -17,7 +17,7 @@ let { q = '', sort = 'DATE' }: { - items: DocumentSearchItem[]; + items: DocumentListItem[]; canWrite: boolean; error?: string | null; total?: number; @@ -31,10 +31,10 @@ const groups = $derived.by(() => { return groupByYear(items); }); -function groupByYear(docItems: DocumentSearchItem[]) { - const map = new SvelteMap(); +function groupByYear(docItems: DocumentListItem[]) { + const map = new SvelteMap(); for (const item of docItems) { - const label = item.document.documentDate?.substring(0, 4) ?? m.docs_group_undated(); + const label = item.documentDate?.substring(0, 4) ?? m.docs_group_undated(); const bucket = map.get(label); if (bucket) bucket.push(item); else map.set(label, [item]); @@ -42,10 +42,10 @@ function groupByYear(docItems: DocumentSearchItem[]) { return Array.from(map.entries()).map(([label, groupItems]) => ({ label, items: groupItems })); } -function groupBySender(docItems: DocumentSearchItem[]) { - const map = new SvelteMap(); +function groupBySender(docItems: DocumentListItem[]) { + const map = new SvelteMap(); for (const item of docItems) { - const label = item.document.sender?.displayName ?? m.docs_group_unknown_sender(); + const label = item.sender?.displayName ?? m.docs_group_unknown_sender(); const bucket = map.get(label); if (bucket) bucket.push(item); else map.set(label, [item]); @@ -53,10 +53,10 @@ function groupBySender(docItems: DocumentSearchItem[]) { return Array.from(map.entries()).map(([label, groupItems]) => ({ label, items: groupItems })); } -function groupByReceiver(docItems: DocumentSearchItem[]) { - const map = new SvelteMap(); +function groupByReceiver(docItems: DocumentListItem[]) { + const map = new SvelteMap(); for (const item of docItems) { - const receivers = item.document.receivers ?? []; + const receivers = item.receivers ?? []; const labels = receivers.length > 0 ? receivers.map((r) => r.displayName) @@ -99,7 +99,7 @@ function groupByReceiver(docItems: DocumentSearchItem[]) { >
    - {#each group.items as item (group.label + '-' + item.document.id)} + {#each group.items as item (group.label + '-' + item.id)} {/each}
diff --git a/frontend/src/routes/DocumentList.svelte.spec.ts b/frontend/src/routes/DocumentList.svelte.spec.ts index 19a11676..a21be4b4 100644 --- a/frontend/src/routes/DocumentList.svelte.spec.ts +++ b/frontend/src/routes/DocumentList.svelte.spec.ts @@ -8,24 +8,17 @@ vi.mock('$app/navigation', () => ({ goto: vi.fn() })); afterEach(() => cleanup()); -type DocumentSearchItem = components['schemas']['DocumentSearchItem']; +type DocumentListItem = components['schemas']['DocumentListItem']; -function makeItem(overrides: Partial = {}): DocumentSearchItem { +function makeItem(overrides: Partial = {}): DocumentListItem { return { - document: { - id: '1', - title: 'Testbrief', - originalFilename: 'testbrief.pdf', - status: 'UPLOADED', - documentDate: '2024-03-15', - sender: undefined, - receivers: [], - tags: [], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - metadataComplete: false, - scriptType: 'UNKNOWN' - }, + id: '1', + title: 'Testbrief', + originalFilename: 'testbrief.pdf', + documentDate: '2024-03-15', + sender: undefined, + receivers: [], + tags: [], matchData: { titleOffsets: [], senderMatched: false, @@ -75,8 +68,8 @@ describe('DocumentList – empty state', () => { describe('DocumentList – year grouping', () => { it('groups documents by year into separate cards', async () => { const items = [ - makeItem({ document: { ...makeItem().document, id: '1', documentDate: '1923-04-12' } }), - makeItem({ document: { ...makeItem().document, id: '2', documentDate: '1965-08-03' } }) + makeItem({ id: '1', documentDate: '1923-04-12' }), + makeItem({ id: '2', documentDate: '1965-08-03' }) ]; render(DocumentList, { ...baseProps, items, total: 2 }); const groupCards = page.getByTestId('group-card'); @@ -85,17 +78,15 @@ describe('DocumentList – year grouping', () => { }); it('uses undated label for items with no documentDate', async () => { - const items = [ - makeItem({ document: { ...makeItem().document, id: '1', documentDate: undefined } }) - ]; + const items = [makeItem({ id: '1', documentDate: undefined })]; render(DocumentList, { ...baseProps, items, total: 1 }); await expect.element(page.getByText('Undatiert')).toBeInTheDocument(); }); it('single year renders one group-card', async () => { const items = [ - makeItem({ document: { ...makeItem().document, id: '1', documentDate: '1938-01-01' } }), - makeItem({ document: { ...makeItem().document, id: '2', documentDate: '1938-06-15' } }) + makeItem({ id: '1', documentDate: '1938-01-01' }), + makeItem({ id: '2', documentDate: '1938-06-15' }) ]; render(DocumentList, { ...baseProps, items, total: 2 }); const groupCards = page.getByTestId('group-card'); @@ -108,9 +99,7 @@ describe('DocumentList – year grouping', () => { describe('DocumentList – sort fallback', () => { it('falls back to year grouping when sort is not SENDER or RECEIVER', async () => { - const items = [ - makeItem({ document: { ...makeItem().document, id: '1', documentDate: '2024-03-15' } }) - ]; + const items = [makeItem({ id: '1', documentDate: '2024-03-15' })]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'TITLE' }); await expect .element(page.getByTestId('group-header').filter({ hasText: '2024' })) @@ -124,29 +113,23 @@ describe('DocumentList – sender grouping', () => { it('groups by sender displayName when sort is SENDER', async () => { const items = [ makeItem({ - document: { - ...makeItem().document, - id: '1', - sender: { - id: 's1', - lastName: 'Mustermann', - displayName: 'Max Mustermann', - personType: 'PERSON', - familyMember: false - } + id: '1', + sender: { + id: 's1', + lastName: 'Mustermann', + displayName: 'Max Mustermann', + personType: 'PERSON', + familyMember: false } }), makeItem({ - document: { - ...makeItem().document, - id: '2', - sender: { - id: 's2', - lastName: 'Musterfrau', - displayName: 'Anna Musterfrau', - personType: 'PERSON', - familyMember: false - } + id: '2', + sender: { + id: 's2', + lastName: 'Musterfrau', + displayName: 'Anna Musterfrau', + personType: 'PERSON', + familyMember: false } }) ]; @@ -167,10 +150,7 @@ describe('DocumentList – sender grouping', () => { personType: 'PERSON' as const, familyMember: false }; - const items = [ - makeItem({ document: { ...makeItem().document, id: '1', sender } }), - makeItem({ document: { ...makeItem().document, id: '2', sender } }) - ]; + const items = [makeItem({ id: '1', sender }), makeItem({ id: '2', sender })]; render(DocumentList, { ...baseProps, items, total: 2, sort: 'SENDER' }); const cards = page.getByTestId('group-card'); await expect.element(cards.first()).toBeInTheDocument(); @@ -178,7 +158,7 @@ describe('DocumentList – sender grouping', () => { }); it('places items with no sender under fallback label', async () => { - const items = [makeItem({ document: { ...makeItem().document, id: '1', sender: undefined } })]; + const items = [makeItem({ id: '1', sender: undefined })]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'SENDER' }); await expect.element(page.getByText('Unbekannter Absender')).toBeInTheDocument(); }); @@ -190,19 +170,16 @@ describe('DocumentList – receiver grouping', () => { it('groups by receiver displayName when sort is RECEIVER', async () => { const items = [ makeItem({ - document: { - ...makeItem().document, - id: '1', - receivers: [ - { - id: 'r1', - lastName: 'Brandt', - displayName: 'Felix Brandt', - personType: 'PERSON', - familyMember: false - } - ] - } + id: '1', + receivers: [ + { + id: 'r1', + lastName: 'Brandt', + displayName: 'Felix Brandt', + personType: 'PERSON', + familyMember: false + } + ] }) ]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' }); @@ -214,27 +191,24 @@ describe('DocumentList – receiver grouping', () => { it('duplicates a document into each receiver group', async () => { const items = [ makeItem({ - document: { - ...makeItem().document, - id: '1', - title: 'Rundbriefchen', - receivers: [ - { - id: 'r1', - lastName: 'Brandt', - displayName: 'Felix Brandt', - personType: 'PERSON', - familyMember: false - }, - { - id: 'r2', - lastName: 'Meier', - displayName: 'Hans Meier', - personType: 'PERSON', - familyMember: false - } - ] - } + id: '1', + title: 'Rundbriefchen', + receivers: [ + { + id: 'r1', + lastName: 'Brandt', + displayName: 'Felix Brandt', + personType: 'PERSON', + familyMember: false + }, + { + id: 'r2', + lastName: 'Meier', + displayName: 'Hans Meier', + personType: 'PERSON', + familyMember: false + } + ] }) ]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' }); @@ -249,7 +223,7 @@ describe('DocumentList – receiver grouping', () => { }); it('places items with no receivers under fallback label', async () => { - const items = [makeItem({ document: { ...makeItem().document, id: '1', receivers: [] } })]; + const items = [makeItem({ id: '1', receivers: [] })]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' }); await expect.element(page.getByText('Unbekannter Empfänger')).toBeInTheDocument(); }); @@ -261,7 +235,7 @@ describe('DocumentList – DocumentRow delegation', () => { it('shows transcription snippet when matchData has one', async () => { const items = [ makeItem({ - document: { ...makeItem().document, id: 'doc1' }, + id: 'doc1', matchData: { transcriptionSnippet: 'Er schrieb einen langen Brief', titleOffsets: [], @@ -278,7 +252,7 @@ describe('DocumentList – DocumentRow delegation', () => { }); it('does not render snippet when matchData has no transcription snippet', async () => { - const items = [makeItem({ document: { ...makeItem().document, id: 'doc1' } })]; + const items = [makeItem({ id: 'doc1' })]; render(DocumentList, { ...baseProps, items, total: 1 }); await expect.element(page.getByTestId('search-snippet')).not.toBeInTheDocument(); }); @@ -286,7 +260,8 @@ describe('DocumentList – DocumentRow delegation', () => { it('renders mark for title highlight when titleOffsets present', async () => { const items = [ makeItem({ - document: { ...makeItem().document, id: 'doc1', title: 'Brief an Anna' }, + id: 'doc1', + title: 'Brief an Anna', matchData: { titleOffsets: [{ start: 0, length: 5 }], // "Brief" senderMatched: false, diff --git a/frontend/src/routes/documents/+page.server.ts b/frontend/src/routes/documents/+page.server.ts index 1c9510e9..8cfb3ac1 100644 --- a/frontend/src/routes/documents/+page.server.ts +++ b/frontend/src/routes/documents/+page.server.ts @@ -20,7 +20,7 @@ async function resolvePersonName( } } -type DocumentSearchItem = components['schemas']['DocumentSearchItem']; +type DocumentListItem = components['schemas']['DocumentListItem']; const VALID_SORTS = ['DATE', 'TITLE', 'SENDER', 'RECEIVER', 'UPLOAD_DATE', 'RELEVANCE'] as const; type ValidSort = (typeof VALID_SORTS)[number]; @@ -77,7 +77,7 @@ export async function load({ url, fetch }) { ]); } catch { return { - items: [] as DocumentSearchItem[], + items: [] as DocumentListItem[], totalElements: 0, pageNumber: 0, pageSize: PAGE_SIZE, @@ -108,7 +108,7 @@ export async function load({ url, fetch }) { : null; return { - items: (result.data?.items ?? []) as DocumentSearchItem[], + items: (result.data?.items ?? []) as DocumentListItem[], totalElements: result.data?.totalElements ?? 0, pageNumber: result.data?.pageNumber ?? page, pageSize: result.data?.pageSize ?? PAGE_SIZE,