feat(frontend): add discussion sub-tab navigation for annotation threads
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m34s
CI / Backend Unit Tests (pull_request) Successful in 2m16s
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 24m11s
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m34s
CI / Backend Unit Tests (pull_request) Successful in 2m16s
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 24m11s
Within the Diskussion panel tab, show two sub-tabs when an annotation is active: «Diskussion» (document-level thread, with comment-count badge) and «Annotation · Seite N» (annotation-specific thread). Behaviour: - Clicking an annotation auto-switches to the Annotation sub-tab - Clicking the Diskussion sub-tab deselects the annotation and returns to the document thread - Escape clears the active annotation (or collapses the panel if none) - activeAnnotationPage is now lifted from PdfViewer → DocumentViewer → page → DocumentBottomPanel → PanelDiscussion so the tab label shows the correct page number Closes #60 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -257,5 +257,6 @@
|
|||||||
"doc_panel_tab_history": "Verlauf",
|
"doc_panel_tab_history": "Verlauf",
|
||||||
"doc_panel_annotate": "Annotieren",
|
"doc_panel_annotate": "Annotieren",
|
||||||
"doc_panel_annotate_stop": "Fertig",
|
"doc_panel_annotate_stop": "Fertig",
|
||||||
"doc_panel_annotation_thread_title": "Annotation"
|
"doc_panel_annotation_thread_title": "Annotation",
|
||||||
|
"doc_panel_discussion_annotation_tab": "Annotation · Seite {page}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,5 +257,6 @@
|
|||||||
"doc_panel_tab_history": "History",
|
"doc_panel_tab_history": "History",
|
||||||
"doc_panel_annotate": "Annotate",
|
"doc_panel_annotate": "Annotate",
|
||||||
"doc_panel_annotate_stop": "Done",
|
"doc_panel_annotate_stop": "Done",
|
||||||
"doc_panel_annotation_thread_title": "Annotation"
|
"doc_panel_annotation_thread_title": "Annotation",
|
||||||
|
"doc_panel_discussion_annotation_tab": "Annotation · Page {page}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,5 +257,6 @@
|
|||||||
"doc_panel_tab_history": "Historial",
|
"doc_panel_tab_history": "Historial",
|
||||||
"doc_panel_annotate": "Anotar",
|
"doc_panel_annotate": "Anotar",
|
||||||
"doc_panel_annotate_stop": "Listo",
|
"doc_panel_annotate_stop": "Listo",
|
||||||
"doc_panel_annotation_thread_title": "Anotación"
|
"doc_panel_annotation_thread_title": "Anotación",
|
||||||
|
"doc_panel_discussion_annotation_tab": "Anotación · Página {page}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ type Props = {
|
|||||||
height: number;
|
height: number;
|
||||||
activeTab: Tab;
|
activeTab: Tab;
|
||||||
activeAnnotationId: string | null;
|
activeAnnotationId: string | null;
|
||||||
|
activeAnnotationPage: number | null;
|
||||||
|
onClearAnnotation?: () => void;
|
||||||
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
|
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,6 +64,8 @@ let {
|
|||||||
height = $bindable(),
|
height = $bindable(),
|
||||||
activeTab = $bindable(),
|
activeTab = $bindable(),
|
||||||
activeAnnotationId,
|
activeAnnotationId,
|
||||||
|
activeAnnotationPage,
|
||||||
|
onClearAnnotation,
|
||||||
onAnnotationCommentCountChange
|
onAnnotationCommentCountChange
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -182,10 +186,12 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
|
|||||||
<PanelDiscussion
|
<PanelDiscussion
|
||||||
documentId={doc.id}
|
documentId={doc.id}
|
||||||
activeAnnotationId={activeAnnotationId}
|
activeAnnotationId={activeAnnotationId}
|
||||||
|
activeAnnotationPage={activeAnnotationPage}
|
||||||
initialComments={comments}
|
initialComments={comments}
|
||||||
canComment={canComment}
|
canComment={canComment}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
canAdmin={canAdmin}
|
canAdmin={canAdmin}
|
||||||
|
onClearAnnotation={onClearAnnotation}
|
||||||
onAnnotationCommentCountChange={onAnnotationCommentCountChange}
|
onAnnotationCommentCountChange={onAnnotationCommentCountChange}
|
||||||
/>
|
/>
|
||||||
{:else if activeTab === 'history'}
|
{:else if activeTab === 'history'}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Props = {
|
|||||||
error: string;
|
error: string;
|
||||||
annotateMode: boolean;
|
annotateMode: boolean;
|
||||||
activeAnnotationId: string | null;
|
activeAnnotationId: string | null;
|
||||||
|
activeAnnotationPage: number | null;
|
||||||
onAnnotationClick: (id: string) => void;
|
onAnnotationClick: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ let {
|
|||||||
error,
|
error,
|
||||||
annotateMode = $bindable(),
|
annotateMode = $bindable(),
|
||||||
activeAnnotationId = $bindable(),
|
activeAnnotationId = $bindable(),
|
||||||
|
activeAnnotationPage = $bindable(),
|
||||||
onAnnotationClick
|
onAnnotationClick
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -79,6 +81,7 @@ let {
|
|||||||
documentId={doc.id}
|
documentId={doc.id}
|
||||||
bind:annotateMode={annotateMode}
|
bind:annotateMode={annotateMode}
|
||||||
bind:activeAnnotationId={activeAnnotationId}
|
bind:activeAnnotationId={activeAnnotationId}
|
||||||
|
bind:activeAnnotationPage={activeAnnotationPage}
|
||||||
onAnnotationClick={onAnnotationClick}
|
onAnnotationClick={onAnnotationClick}
|
||||||
/>
|
/>
|
||||||
{:else if fileUrl}
|
{:else if fileUrl}
|
||||||
|
|||||||
@@ -24,33 +24,98 @@ type Comment = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
activeAnnotationId: string | null;
|
activeAnnotationId: string | null;
|
||||||
|
activeAnnotationPage: number | null;
|
||||||
initialComments: Comment[];
|
initialComments: Comment[];
|
||||||
canComment: boolean;
|
canComment: boolean;
|
||||||
currentUserId: string | null;
|
currentUserId: string | null;
|
||||||
canAdmin: boolean;
|
canAdmin: boolean;
|
||||||
|
onClearAnnotation?: () => void;
|
||||||
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
|
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
documentId,
|
documentId,
|
||||||
activeAnnotationId,
|
activeAnnotationId,
|
||||||
|
activeAnnotationPage,
|
||||||
initialComments,
|
initialComments,
|
||||||
canComment,
|
canComment,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
canAdmin,
|
canAdmin,
|
||||||
|
onClearAnnotation,
|
||||||
onAnnotationCommentCountChange
|
onAnnotationCommentCountChange
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
// Sub-tab within the discussion panel: 'document' or 'annotation'
|
||||||
|
type DiscussionTab = 'document' | 'annotation';
|
||||||
|
let activeSubTab = $state<DiscussionTab>('document');
|
||||||
|
|
||||||
|
// Track document-level comment count for badge.
|
||||||
|
// CommentThread calls onCountChange immediately on mount with the accurate total.
|
||||||
|
let docCommentCount = $state(0);
|
||||||
|
|
||||||
|
// When an annotation becomes active, switch to the annotation sub-tab automatically.
|
||||||
|
$effect(() => {
|
||||||
|
if (activeAnnotationId) {
|
||||||
|
activeSubTab = 'annotation';
|
||||||
|
} else {
|
||||||
|
activeSubTab = 'document';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectDocumentTab() {
|
||||||
|
activeSubTab = 'document';
|
||||||
|
onClearAnnotation?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAnnotationTab() {
|
||||||
|
activeSubTab = 'annotation';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-8 p-6">
|
<div class="flex h-full flex-col">
|
||||||
<!-- Annotation thread (shown when an annotation is active) -->
|
<!-- Sub-tab bar (only shown when annotation is active) -->
|
||||||
{#if activeAnnotationId}
|
{#if activeAnnotationId}
|
||||||
<div>
|
<div class="flex shrink-0 border-b border-brand-sand/70 bg-gray-50 px-4">
|
||||||
<h4
|
<button
|
||||||
class="mb-3 border-b border-brand-sand pb-2 font-sans text-xs font-bold tracking-widest text-brand-navy uppercase"
|
onclick={selectDocumentTab}
|
||||||
|
class="mr-1 px-3 py-2 font-sans text-xs font-medium transition-colors {activeSubTab === 'document'
|
||||||
|
? 'border-b-2 border-brand-navy text-brand-navy'
|
||||||
|
: 'text-gray-400 hover:text-brand-navy'}"
|
||||||
>
|
>
|
||||||
{m.doc_panel_annotation_thread_title()}
|
{m.doc_panel_tab_discussion()}
|
||||||
</h4>
|
{#if docCommentCount > 0}
|
||||||
|
<span
|
||||||
|
class="ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-brand-mint px-1 font-mono text-[10px] text-brand-navy"
|
||||||
|
>
|
||||||
|
{docCommentCount}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={selectAnnotationTab}
|
||||||
|
class="px-3 py-2 font-sans text-xs font-medium transition-colors {activeSubTab === 'annotation'
|
||||||
|
? 'border-b-2 border-brand-navy text-brand-navy'
|
||||||
|
: 'text-gray-400 hover:text-brand-navy'}"
|
||||||
|
>
|
||||||
|
{m.doc_panel_discussion_annotation_tab({ page: String(activeAnnotationPage ?? '?') })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Content area -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
|
{#if !activeAnnotationId || activeSubTab === 'document'}
|
||||||
|
<!-- Document-level thread -->
|
||||||
|
<CommentThread
|
||||||
|
documentId={documentId}
|
||||||
|
initialComments={initialComments}
|
||||||
|
canComment={canComment}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
canAdmin={canAdmin}
|
||||||
|
onCountChange={(count) => (docCommentCount = count)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<!-- Annotation-level thread -->
|
||||||
{#key activeAnnotationId}
|
{#key activeAnnotationId}
|
||||||
<CommentThread
|
<CommentThread
|
||||||
documentId={documentId}
|
documentId={documentId}
|
||||||
@@ -62,24 +127,6 @@ let {
|
|||||||
onCountChange={(count) => onAnnotationCommentCountChange?.(activeAnnotationId, count)}
|
onCountChange={(count) => onAnnotationCommentCountChange?.(activeAnnotationId, count)}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- General document discussion -->
|
|
||||||
<div>
|
|
||||||
{#if activeAnnotationId}
|
|
||||||
<h4
|
|
||||||
class="mb-3 border-b border-brand-sand pb-2 font-sans text-xs font-bold tracking-widest text-brand-navy uppercase"
|
|
||||||
>
|
|
||||||
{m.comment_section_title()}
|
|
||||||
</h4>
|
|
||||||
{/if}
|
{/if}
|
||||||
<CommentThread
|
|
||||||
documentId={documentId}
|
|
||||||
initialComments={initialComments}
|
|
||||||
canComment={canComment}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
canAdmin={canAdmin}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ let {
|
|||||||
documentId = '',
|
documentId = '',
|
||||||
annotateMode = $bindable(false),
|
annotateMode = $bindable(false),
|
||||||
activeAnnotationId = $bindable<string | null>(null),
|
activeAnnotationId = $bindable<string | null>(null),
|
||||||
|
activeAnnotationPage = $bindable<number | null>(null),
|
||||||
onAnnotationClick
|
onAnnotationClick
|
||||||
}: {
|
}: {
|
||||||
url: string;
|
url: string;
|
||||||
documentId?: string;
|
documentId?: string;
|
||||||
annotateMode?: boolean;
|
annotateMode?: boolean;
|
||||||
activeAnnotationId?: string | null;
|
activeAnnotationId?: string | null;
|
||||||
|
activeAnnotationPage?: number | null;
|
||||||
onAnnotationClick?: (id: string) => void;
|
onAnnotationClick?: (id: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -213,6 +215,7 @@ async function handleAnnotationDraw(rect: { x: number; y: number; width: number;
|
|||||||
const created: Annotation = await res.json();
|
const created: Annotation = await res.json();
|
||||||
annotations = [...annotations, created];
|
annotations = [...annotations, created];
|
||||||
activeAnnotationId = created.id;
|
activeAnnotationId = created.id;
|
||||||
|
activeAnnotationPage = created.pageNumber;
|
||||||
onAnnotationClick?.(created.id);
|
onAnnotationClick?.(created.id);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -236,6 +239,8 @@ async function handleAnnotationDelete(annotationId: string) {
|
|||||||
|
|
||||||
function handleAnnotationClick(id: string) {
|
function handleAnnotationClick(id: string) {
|
||||||
activeAnnotationId = id;
|
activeAnnotationId = id;
|
||||||
|
const ann = annotations.find((a) => a.id === id);
|
||||||
|
activeAnnotationPage = ann?.pageNumber ?? null;
|
||||||
onAnnotationClick?.(id);
|
onAnnotationClick?.(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ async function loadFile(id: string) {
|
|||||||
|
|
||||||
let annotateMode = $state(false);
|
let annotateMode = $state(false);
|
||||||
let activeAnnotationId = $state<string | null>(null);
|
let activeAnnotationId = $state<string | null>(null);
|
||||||
|
let activeAnnotationPage = $state<number | null>(null);
|
||||||
|
|
||||||
// When an annotation is clicked, open the Diskussion tab.
|
// When an annotation is clicked, open the Diskussion tab.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -97,6 +98,19 @@ onMount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorageRestored = true;
|
localStorageRestored = true;
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (activeAnnotationId) {
|
||||||
|
activeAnnotationId = null;
|
||||||
|
activeAnnotationPage = null;
|
||||||
|
} else if (panelOpen) {
|
||||||
|
panelOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', onKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Persist panel state whenever it changes (after initial restore).
|
// Persist panel state whenever it changes (after initial restore).
|
||||||
@@ -129,6 +143,7 @@ $effect(() => {
|
|||||||
error={fileError}
|
error={fileError}
|
||||||
bind:annotateMode={annotateMode}
|
bind:annotateMode={annotateMode}
|
||||||
bind:activeAnnotationId={activeAnnotationId}
|
bind:activeAnnotationId={activeAnnotationId}
|
||||||
|
bind:activeAnnotationPage={activeAnnotationPage}
|
||||||
onAnnotationClick={(id) => {
|
onAnnotationClick={(id) => {
|
||||||
activeAnnotationId = id;
|
activeAnnotationId = id;
|
||||||
}}
|
}}
|
||||||
@@ -146,4 +161,9 @@ $effect(() => {
|
|||||||
bind:height={panelHeight}
|
bind:height={panelHeight}
|
||||||
bind:activeTab={activeTab}
|
bind:activeTab={activeTab}
|
||||||
activeAnnotationId={activeAnnotationId}
|
activeAnnotationId={activeAnnotationId}
|
||||||
|
activeAnnotationPage={activeAnnotationPage}
|
||||||
|
onClearAnnotation={() => {
|
||||||
|
activeAnnotationId = null;
|
||||||
|
activeAnnotationPage = null;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user