feat(transcription): swap plain textarea for PersonMentionEditor and thread mentionedPersons through autosave

- TranscriptionBlockData now carries mentionedPersons (matches backend
  schema added in PR-A).
- useBlockAutoSave.saveFn signature widens to (blockId, text, mentions);
  pendingMentions is tracked alongside pendingTexts and is preserved on
  failure so a retry resends the in-flight payload (B12).
- TranscriptionBlock.svelte renders <PersonMentionEditor>, exposing the
  textarea node back through a captureTextarea callback so the existing
  quote-selection feature still works.
- saveBlock in routes/documents/[id]/+page.svelte forwards mentions on
  PUT.
- flushOnUnload sends mentions in the keepalive payload too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-29 00:32:09 +02:00
parent c4ee2c666b
commit 02d3e2ab61
11 changed files with 207 additions and 78 deletions

View File

@@ -15,7 +15,8 @@ const block1 = {
sortOrder: 0,
version: 0,
source: 'MANUAL' as const,
reviewed: false
reviewed: false,
mentionedPersons: []
};
const block2 = {
id: 'b2',
@@ -26,7 +27,8 @@ const block2 = {
sortOrder: 1,
version: 0,
source: 'OCR' as const,
reviewed: true
reviewed: true,
mentionedPersons: []
};
function renderView(overrides: Record<string, unknown> = {}, service = createConfirmService()) {
@@ -141,7 +143,28 @@ describe('TranscriptionEditView — auto-save debounce', () => {
vi.advanceTimersByTime(1500);
await vi.runAllTimersAsync();
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Neue Zeile');
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Neue Zeile', []);
vi.useRealTimers();
});
it('passes the block mentionedPersons array as the 3rd save argument', async () => {
vi.useFakeTimers();
const onSaveBlock = vi.fn().mockResolvedValue(undefined);
const blockWithMention = {
...block1,
mentionedPersons: [{ personId: 'p-aug', displayName: 'Auguste Raddatz' }]
};
renderView({ blocks: [blockWithMention], onSaveBlock });
const textarea = page.getByRole('textbox').first();
await textarea.fill('Hallo @Auguste Raddatz');
vi.advanceTimersByTime(1500);
await vi.runAllTimersAsync();
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Hallo @Auguste Raddatz', [
{ personId: 'p-aug', displayName: 'Auguste Raddatz' }
]);
vi.useRealTimers();
});
@@ -165,7 +188,7 @@ describe('TranscriptionEditView — auto-save debounce', () => {
// Only one save with the final value
expect(onSaveBlock).toHaveBeenCalledTimes(1);
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Second');
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Second', []);
vi.useRealTimers();
});
});
@@ -220,7 +243,7 @@ describe('TranscriptionEditView — flush on blur', () => {
el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
await vi.runAllTimersAsync();
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Blur text');
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Blur text', []);
vi.useRealTimers();
});
});