refactor(transcription): extract block CRUD into createTranscriptionBlocks hook

Pulls the transcription-block state (load, save, delete, reviewToggle,
markAllReviewed, createFromDraw, toggleTrainingLabel, deleteAnnotation
+ derived blockNumbers / hasBlocks / lastEditedAt / annotationReloadKey)
out of documents/[id]/+page.svelte into a reusable factory in
lib/document/transcription/useTranscriptionBlocks.svelte.ts.

The page now reads transcription.blocks / .blockNumbers / .hasBlocks /
.lastEditedAt / .annotationReloadKey reactively and delegates writes
to transcription.{load, save, delete, reviewToggle, markAllReviewed,
createFromDraw, toggleTrainingLabel, deleteAnnotation,
findByAnnotationId, bumpAnnotationReloadKey}. The confirm-then-delete
dialog stays in the page; the hook only handles the data ops.

24 unit tests cover initial state, load (success / non-OK / network /
empty-id), derived state (blockNumbers in sortOrder, lastEditedAt
recent-pick, lastEditedAt-null fallback), delete (success bumps key /
non-OK throws), reviewToggle (success updates / non-OK no-op), markAll
(success / non-OK), createFromDraw (success / non-OK / network all
return correct shape), toggleTrainingLabel (200 / 500), deleteAnnotation
(linked-block path / orphan-annotation path / orphan-fail throw),
findByAnnotationId match + miss, bumpAnnotationReloadKey.

Also bumps the polling-loop test waits in useOcrJob.svelte.test.ts to
150-200ms (from 60-80ms) so the suite is reliable when run in parallel.

Refs #496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-10 10:34:14 +02:00
committed by marcel
parent 878bb3843b
commit 27041a639d
4 changed files with 710 additions and 159 deletions

View File

@@ -261,7 +261,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(60);
await wait(150);
expect(job.progressMessage).not.toBe('');
job.destroy();
@@ -287,7 +287,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(60);
await wait(150);
expect(job.skippedPages).toBeGreaterThanOrEqual(0);
job.destroy();
@@ -317,7 +317,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
resetDelayMs: 10
});
await job.triggerOcr('KURRENT', false);
await wait(80);
await wait(200);
expect(onJobFinished).toHaveBeenCalledWith('DONE');
job.destroy();
@@ -347,7 +347,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
resetDelayMs: 10
});
await job.triggerOcr('KURRENT', false);
await wait(80);
await wait(200);
expect(onJobFinished).toHaveBeenCalledWith('FAILED');
expect(job.errorMessage).toBeTruthy();
@@ -372,7 +372,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(60);
await wait(150);
expect(job.running).toBe(true);
job.destroy();
@@ -399,7 +399,7 @@ describe('createOcrJob — polling loop (short interval, real timers)', () => {
pollIntervalMs: 20
});
await job.triggerOcr('KURRENT', false);
await wait(60);
await wait(150);
expect(job.running).toBe(true);
job.destroy();
@@ -437,7 +437,7 @@ describe('createOcrJob.destroy', () => {
job.destroy();
const callsAtDestroy = fetchImpl.mock.calls.length;
await wait(80);
await wait(200);
// No additional fetch calls after destroy
expect(fetchImpl.mock.calls.length).toBe(callsAtDestroy);
});