diff --git a/frontend/messages/de.json b/frontend/messages/de.json index c3ca442c..e9d4d967 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -291,5 +291,7 @@ "enrich_skip": "Überspringen", "enrich_done_heading": "Alles erledigt!", "enrich_done_body": "Alle Dokumente wurden bearbeitet.", - "enrich_back_to_list": "Zurück zur Liste" + "enrich_back_to_list": "Zurück zur Liste", + "comment_empty_hint": "Noch keine Kommentare – starte die Diskussion!", + "comment_start_discussion": "Diskussion starten →" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e8a86475..61e34dc9 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -291,5 +291,7 @@ "enrich_skip": "Skip", "enrich_done_heading": "All done!", "enrich_done_body": "All documents have been processed.", - "enrich_back_to_list": "Back to list" + "enrich_back_to_list": "Back to list", + "comment_empty_hint": "No comments yet – start the discussion!", + "comment_start_discussion": "Start discussion →" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 55b7bba5..51d7ebe8 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -291,5 +291,7 @@ "enrich_skip": "Omitir", "enrich_done_heading": "¡Todo listo!", "enrich_done_body": "Todos los documentos han sido procesados.", - "enrich_back_to_list": "Volver a la lista" + "enrich_back_to_list": "Volver a la lista", + "comment_empty_hint": "Aún no hay comentarios – ¡inicia la discusión!", + "comment_start_discussion": "Iniciar discusión →" } diff --git a/frontend/src/lib/components/CommentThread.svelte b/frontend/src/lib/components/CommentThread.svelte index 12975503..212add7e 100644 --- a/frontend/src/lib/components/CommentThread.svelte +++ b/frontend/src/lib/components/CommentThread.svelte @@ -167,6 +167,9 @@ function cancelReply() { onMount(() => { if (loadOnMount) { reload(); + } else { + const total = initialComments.reduce((s, c) => s + 1 + c.replies.length, 0); + onCountChange?.(total); } }); @@ -245,6 +248,24 @@ onMount(() => { {/snippet}
+ {#if comments.length === 0} +
+ + + +

{m.comment_empty_hint()}

+
+ {/if} {#each comments as thread, ti (thread.id)}
0 ? 'border-t border-line pt-4' : ''}> diff --git a/frontend/src/lib/components/CommentThread.svelte.spec.ts b/frontend/src/lib/components/CommentThread.svelte.spec.ts new file mode 100644 index 00000000..a444a7ce --- /dev/null +++ b/frontend/src/lib/components/CommentThread.svelte.spec.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import CommentThread from './CommentThread.svelte'; +import type { Comment } from '$lib/types'; + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +function makeComment(id: string, content = 'Hello'): Comment { + return { + id, + authorId: 'user-1', + authorName: 'Alice', + content, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + replies: [] + }; +} + +const baseProps = { + documentId: 'doc-1', + canComment: true, + currentUserId: 'user-1', + canAdmin: false +}; + +describe('CommentThread – empty state', () => { + it('shows empty state hint when there are no comments', async () => { + render(CommentThread, { ...baseProps, initialComments: [] }); + await expect + .element(page.getByText('Noch keine Kommentare – starte die Diskussion!')) + .toBeInTheDocument(); + }); + + it('does not show empty state hint when comments exist', async () => { + render(CommentThread, { ...baseProps, initialComments: [makeComment('c-1')] }); + await expect + .element(page.getByText('Noch keine Kommentare – starte die Diskussion!')) + .not.toBeInTheDocument(); + }); +}); + +describe('CommentThread – onCountChange', () => { + it('calls onCountChange with initial SSR count on mount', async () => { + const onCountChange = vi.fn(); + render(CommentThread, { + ...baseProps, + initialComments: [makeComment('c-1'), makeComment('c-2')], + onCountChange + }); + expect(onCountChange).toHaveBeenCalledWith(2); + }); + + it('calls onCountChange with 0 when no initial comments', async () => { + const onCountChange = vi.fn(); + render(CommentThread, { ...baseProps, initialComments: [], onCountChange }); + expect(onCountChange).toHaveBeenCalledWith(0); + }); + + it('counts replies in the total', async () => { + const onCountChange = vi.fn(); + const comment = { ...makeComment('c-1'), replies: [makeComment('r-1') as never] }; + render(CommentThread, { ...baseProps, initialComments: [comment], onCountChange }); + expect(onCountChange).toHaveBeenCalledWith(2); + }); +}); diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index 94acb09d..a2aba5b8 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -98,6 +98,12 @@ const tabs: { id: DocumentPanelTab; label: () => string }[] = [ ]; const panelHeight = $derived(open ? height : MIN_HEIGHT); + +let discussionCount = $state((() => comments.reduce((s, c) => s + 1 + c.replies.length, 0))()); + +function handleCountChange(count: number) { + discussionCount = count; +}
{tab.label()} + {#if tab.id === 'discussion'} + {discussionCount} + {/if} {/each} @@ -165,6 +178,7 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT); canComment={canComment} currentUserId={currentUserId} canAdmin={canAdmin} + onCountChange={handleCountChange} /> {:else if activeTab === 'history'} diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts b/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts new file mode 100644 index 00000000..975d36e6 --- /dev/null +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import DocumentBottomPanel from './DocumentBottomPanel.svelte'; +import type { Comment } from '$lib/types'; + +afterEach(cleanup); + +function makeComment(id: string): Comment { + return { + id, + authorId: 'user-1', + authorName: 'Alice', + content: 'Hello', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + replies: [] + }; +} + +const doc = { id: 'doc-1', title: 'Test' }; + +const baseProps = { + doc, + canComment: true, + currentUserId: 'user-1', + canAdmin: false, + height: 300, + activeTab: 'discussion' as const +}; + +describe('DocumentBottomPanel – discussion badge', () => { + it('always shows a badge on the Discussion tab', async () => { + render(DocumentBottomPanel, { ...baseProps, comments: [], open: true }); + await expect.element(page.getByTestId('discussion-count-badge')).toBeInTheDocument(); + await expect.element(page.getByTestId('discussion-count-badge')).toHaveTextContent('0'); + }); + + it('shows the correct count when comments exist', async () => { + render(DocumentBottomPanel, { + ...baseProps, + comments: [makeComment('c-1'), makeComment('c-2')], + open: true + }); + await expect.element(page.getByTestId('discussion-count-badge')).toHaveTextContent('2'); + }); +}); diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte index bbed758a..291cf5c1 100644 --- a/frontend/src/lib/components/PanelDiscussion.svelte +++ b/frontend/src/lib/components/PanelDiscussion.svelte @@ -8,9 +8,11 @@ type Props = { canComment: boolean; currentUserId: string | null; canAdmin: boolean; + onCountChange?: (count: number) => void; }; -let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props = $props(); +let { documentId, initialComments, canComment, currentUserId, canAdmin, onCountChange }: Props = + $props();
@@ -20,5 +22,6 @@ let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props canComment={canComment} currentUserId={currentUserId} canAdmin={canAdmin} + onCountChange={onCountChange} />
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 57f40f8e..7fff1e51 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -90,7 +90,7 @@ $effect(() => { {#if data.incompleteCount > 0}
{ class="h-6 w-6 opacity-60" />
-

+

{m.enrich_needs_metadata_title()}

-

+

{m.enrich_needs_metadata_count({ count: data.incompleteCount })}

{m.enrich_needs_metadata_cta()} →