From b07f9efa9c5e0976e62bf9b85463dcf824c43dd0 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 21 Apr 2026 15:22:38 +0200 Subject: [PATCH] fix(document-detail): force edit panel on notification deep-link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comments only render inside TranscriptionEditView, so a deep-link into a document with existing reviewed transcriptions landed the user in read mode with no comment element in the DOM — the scroll target silently missed. scrollToCommentFromQuery now takes a setPanelMode callback and calls it with 'edit' whenever both query params are present. The page's own transcribe-mode $effect checks a skipInitialPanelMode flag the deep-link flow sets, so its default-panel-mode logic doesn't race against the explicit override. Two new helper tests pin the contract: panel mode is forced to 'edit' both when transcribe mode is off (entering fresh) and when it is already on (same-page notification click). Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/lib/utils/deepLinkScroll.spec.ts | 23 +++++++++++++++++++ frontend/src/lib/utils/deepLinkScroll.ts | 6 +++++ .../src/routes/documents/[id]/+page.svelte | 14 ++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/utils/deepLinkScroll.spec.ts b/frontend/src/lib/utils/deepLinkScroll.spec.ts index 20f0cf19..df8d9156 100644 --- a/frontend/src/lib/utils/deepLinkScroll.spec.ts +++ b/frontend/src/lib/utils/deepLinkScroll.spec.ts @@ -18,6 +18,7 @@ function buildOpts(overrides: Overrides = {}): DeepLinkScrollOptions { return { transcribeMode: true, setTranscribeMode: vi.fn(), + setPanelMode: vi.fn(), loadBlocks: vi.fn().mockResolvedValue(undefined), setActiveAnnotationId: vi.fn(), flashAnnotation: vi.fn(), @@ -126,4 +127,26 @@ describe('scrollToCommentFromQuery', () => { expect(opts.onStripUrl).toHaveBeenCalled(); }); + + it('forces panel mode to "edit" so the comment DOM exists on reviewed documents', async () => { + const url = new URL( + `https://app/documents/doc-1?commentId=${COMMENT_ID}&annotationId=${ANNOTATION_ID}` + ); + const opts = buildOpts(); + + await scrollToCommentFromQuery(url, opts); + + expect(opts.setPanelMode).toHaveBeenCalledWith('edit'); + }); + + it('forces panel mode to "edit" even when transcribe mode is already on', async () => { + const url = new URL( + `https://app/documents/doc-1?commentId=${COMMENT_ID}&annotationId=${ANNOTATION_ID}` + ); + const opts = buildOpts({ transcribeMode: true }); + + await scrollToCommentFromQuery(url, opts); + + expect(opts.setPanelMode).toHaveBeenCalledWith('edit'); + }); }); diff --git a/frontend/src/lib/utils/deepLinkScroll.ts b/frontend/src/lib/utils/deepLinkScroll.ts index cc0d49c9..f221cdc4 100644 --- a/frontend/src/lib/utils/deepLinkScroll.ts +++ b/frontend/src/lib/utils/deepLinkScroll.ts @@ -1,6 +1,7 @@ export type DeepLinkScrollOptions = { transcribeMode: boolean; setTranscribeMode: (value: boolean) => void; + setPanelMode: (mode: 'read' | 'edit') => void; loadBlocks: () => Promise; setActiveAnnotationId: (id: string) => void; flashAnnotation: (annotationId: string) => void; @@ -25,6 +26,11 @@ export async function scrollToCommentFromQuery( await opts.loadBlocks(); } + // Comments only render in edit mode — force it so the deep-link target + // exists in the DOM even if the document already has reviewed transcriptions + // (which default the panel to read mode). + opts.setPanelMode('edit'); + opts.setActiveAnnotationId(annotationId); await opts.afterTick(); diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index c0e67381..06a38484 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -40,6 +40,10 @@ let activeAnnotationId = $state(null); let highlightBlockId = $state(null); let flashAnnotationId = $state(null); let pdfStripExpanded = $state(false); +// Flag set by the deep-link helper so the transcribe-mode $effect does not +// overwrite the panelMode it picked (e.g. forcing 'edit' on notification +// click-through). One-shot: consumed after the effect's loadBlocks resolves. +let skipInitialPanelMode = $state(false); const prefersReducedMotion = $derived( typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches @@ -281,7 +285,11 @@ async function checkOcrStatus() { $effect(() => { if (transcribeMode) { loadTranscriptionBlocks().then(() => { - panelMode = transcriptionBlocks.length > 0 ? 'read' : 'edit'; + if (skipInitialPanelMode) { + skipInitialPanelMode = false; + } else { + panelMode = transcriptionBlocks.length > 0 ? 'read' : 'edit'; + } }); checkOcrStatus(); } @@ -309,6 +317,10 @@ onMount(() => { scrollToCommentFromQuery(new URL(page.url), { transcribeMode, setTranscribeMode: (v) => (transcribeMode = v), + setPanelMode: (m) => { + skipInitialPanelMode = true; + panelMode = m; + }, loadBlocks: loadTranscriptionBlocks, setActiveAnnotationId: (id) => (activeAnnotationId = id), flashAnnotation: (annotationId) => {