From a7efb0044cd4e1a100a0ac6b36e6362e1d551bdf Mon Sep 17 00:00:00 2001
From: Marcel
Date: Thu, 23 Apr 2026 08:44:49 +0200
Subject: [PATCH] =?UTF-8?q?feat(documents):=20rebalance=20list=20row=20?=
=?UTF-8?q?=E2=80=94=20summary=20+=20archive=20chips,=20restored=20sender/?=
=?UTF-8?q?receiver?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refill the columns that went visually empty after the previous dedup
commit (`fc0fc57`):
- Middle column gains the document `summary` (line-clamp-2, italic,
with `summaryOffsets` highlighting — the backend already populates
the offsets, the frontend just wasn't rendering them) and a row of
thin neutral chips for `archiveBox`, `archiveFolder`, and `location`
(~99% of docs in the corpus carry these). Chips are desktop-only
and skip empty values.
- Right column restores `VON sender` and `AN receivers`, now with
`` highlighting that the previous right-column copy lacked,
so search matches stay visible there.
Co-Authored-By: Claude Opus 4.7
---
.../src/lib/components/DocumentRow.svelte | 110 ++++++++++++------
.../lib/components/DocumentRow.svelte.spec.ts | 83 ++++++++++++-
2 files changed, 150 insertions(+), 43 deletions(-)
diff --git a/frontend/src/lib/components/DocumentRow.svelte b/frontend/src/lib/components/DocumentRow.svelte
index b6d97762..c5659eb7 100644
--- a/frontend/src/lib/components/DocumentRow.svelte
+++ b/frontend/src/lib/components/DocumentRow.svelte
@@ -20,6 +20,15 @@ const snippet = $derived(item.matchData?.transcriptionSnippet ?? null);
const snippetSegments = $derived(
snippet ? applyOffsets(snippet, item.matchData?.snippetOffsets ?? []) : null
);
+const summary = $derived(doc.summary?.trim() ? doc.summary : null);
+const summarySegments = $derived(
+ summary ? applyOffsets(summary, item.matchData?.summaryOffsets ?? []) : null
+);
+const archiveChips = $derived(
+ [doc.archiveBox, doc.archiveFolder, doc.location].filter(
+ (c): c is string => !!c && c.trim().length > 0
+ )
+);
const senderMatched = $derived(item.matchData?.senderMatched ?? false);
const matchedReceiverIds = $derived(new Set(item.matchData?.matchedReceiverIds ?? []));
const matchedTagIds = $derived(new Set(item.matchData?.matchedTagIds ?? []));
@@ -77,46 +86,36 @@ function safeTagColor(color: string | null | undefined): string {
{/if}
-
-
-
-
{m.docs_list_from()}
-
- {#if doc.sender}
- {#if senderMatched}
- {doc.sender.displayName}
- {:else}
- {doc.sender.displayName}
- {/if}
+
+ {#if summarySegments}
+
+ {#each summarySegments as seg, i (i)}
+ {#if seg.highlight}
+ {seg.text}
{:else}
- {m.docs_list_unknown()}
+ {seg.text}
{/if}
-
+ {/each}
+
+ {/if}
+
+
+ {#if archiveChips.length > 0}
+
+ {#each archiveChips as chip, i (i)}
+ {chip}
+ {/each}
-
- {m.docs_list_to()}
-
- {#if doc.receivers && doc.receivers.length > 0}
- {#each doc.receivers as receiver, i (receiver.id)}
- {#if i > 0}, {/if}
- {#if matchedReceiverIds.has(receiver.id)}
- {receiver.displayName}
- {:else}
- {receiver.displayName}
- {/if}
- {/each}
- {:else}
- {m.docs_list_unknown()}
- {/if}
-
-
-
+ {/if}
{#if doc.tags && doc.tags.length > 0}
@@ -158,6 +157,43 @@ function safeTagColor(color: string | null | undefined): string {
{doc.documentDate ? formatDate(doc.documentDate) : '—'}
+
+ {m.docs_list_from()}
+
+ {#if doc.sender}
+ {#if senderMatched}
+ {doc.sender.displayName}
+ {:else}
+ {doc.sender.displayName}
+ {/if}
+ {:else}
+ {m.docs_list_unknown()}
+ {/if}
+
+
+
+ {m.docs_list_to()}
+
+ {#if doc.receivers && doc.receivers.length > 0}
+ {#each doc.receivers as receiver, i (receiver.id)}
+ {#if i > 0}, {/if}
+ {#if matchedReceiverIds.has(receiver.id)}
+ {receiver.displayName}
+ {:else}
+ {receiver.displayName}
+ {/if}
+ {/each}
+ {:else}
+ {m.docs_list_unknown()}
+ {/if}
+
+
diff --git a/frontend/src/lib/components/DocumentRow.svelte.spec.ts b/frontend/src/lib/components/DocumentRow.svelte.spec.ts
index 19dcbf4a..f71c9179 100644
--- a/frontend/src/lib/components/DocumentRow.svelte.spec.ts
+++ b/frontend/src/lib/components/DocumentRow.svelte.spec.ts
@@ -118,28 +118,99 @@ describe('DocumentRow – sender', () => {
await expect.element(unknownElements.first()).toBeInTheDocument();
});
- it('renders the sender display name only once across the row', async () => {
+ it('highlights the sender when senderMatched is true', async () => {
const item = makeItem({
document: {
...makeItem().document,
sender: { id: 's1', displayName: 'Großmutter Maria' }
+ },
+ matchData: {
+ ...makeItem().matchData,
+ senderMatched: true
}
});
render(DocumentRow, { item });
- const matches = await page.getByText('Großmutter Maria').all();
- expect(matches.length).toBe(1);
+ const mark = page.getByRole('mark').first();
+ await expect.element(mark).toHaveTextContent('Großmutter Maria');
});
- it('renders each receiver display name only once across the row', async () => {
+ it('highlights a receiver when matchedReceiverIds includes its id', async () => {
const item = makeItem({
document: {
...makeItem().document,
receivers: [{ id: 'r1', displayName: 'Onkel Karl' }]
+ },
+ matchData: {
+ ...makeItem().matchData,
+ matchedReceiverIds: ['r1']
}
});
render(DocumentRow, { item });
- const matches = await page.getByText('Onkel Karl').all();
- expect(matches.length).toBe(1);
+ const mark = page.getByRole('mark').first();
+ await expect.element(mark).toHaveTextContent('Onkel Karl');
+ });
+});
+
+// ─── Summary ─────────────────────────────────────────────────────────────────
+
+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.'
+ }
+ });
+ render(DocumentRow, { item });
+ await expect
+ .element(page.getByTestId('doc-summary'))
+ .toHaveTextContent('Brief von Eugenie über die Heimreise aus dem Süden.');
+ });
+
+ it('does not render the summary block when summary is empty', async () => {
+ render(DocumentRow, { item: makeItem() });
+ await expect.element(page.getByTestId('doc-summary')).not.toBeInTheDocument();
+ });
+
+ it('applies summary search-match highlight via summaryOffsets', async () => {
+ const item = makeItem({
+ document: { ...makeItem().document, summary: 'Brief über Menton' },
+ matchData: {
+ ...makeItem().matchData,
+ summaryOffsets: [{ start: 11, length: 6 }]
+ }
+ });
+ render(DocumentRow, { item });
+ const mark = page.getByRole('mark').first();
+ await expect.element(mark).toHaveTextContent('Menton');
+ });
+});
+
+// ─── Archive chips ───────────────────────────────────────────────────────────
+
+describe('DocumentRow – archive chips', () => {
+ it('renders the archive box chip when set', async () => {
+ const item = makeItem({
+ document: { ...makeItem().document, 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' }
+ });
+ 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' }
+ });
+ render(DocumentRow, { item });
+ await expect.element(page.getByText('Berlin')).toBeInTheDocument();
});
});