refactor(document): migrate frontend from DocumentSearchItem to flat DocumentListItem
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, DocumentSearchItem[]>();
|
||||
function groupByYear(docItems: DocumentListItem[]) {
|
||||
const map = new SvelteMap<string, DocumentListItem[]>();
|
||||
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<string, DocumentSearchItem[]>();
|
||||
function groupBySender(docItems: DocumentListItem[]) {
|
||||
const map = new SvelteMap<string, DocumentListItem[]>();
|
||||
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<string, DocumentSearchItem[]>();
|
||||
function groupByReceiver(docItems: DocumentListItem[]) {
|
||||
const map = new SvelteMap<string, DocumentListItem[]>();
|
||||
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[]) {
|
||||
>
|
||||
</div>
|
||||
<ul class="divide-y divide-line">
|
||||
{#each group.items as item (group.label + '-' + item.document.id)}
|
||||
{#each group.items as item (group.label + '-' + item.id)}
|
||||
<DocumentRow item={item} canWrite={canWrite} />
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -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> = {}): DocumentSearchItem {
|
||||
function makeItem(overrides: Partial<DocumentListItem> = {}): 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,
|
||||
|
||||
@@ -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,
|
||||
@@ -107,7 +107,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,
|
||||
|
||||
Reference in New Issue
Block a user