refactor(document): extract DocumentTopBarActions from DocumentTopBar
Third Phase 5 split. The desktop action buttons — transcribe, transcribe-stop, edit link, download link — become their own component with a focused props interface (documentId, canWrite, isPdf, transcribeMode bindable, filePath, originalFilename, fileUrl). TDD: 8 tests covering empty render, transcribe button gating (canWrite × isPdf × transcribeMode), stop-transcribe rendering, edit link with documentId href, download link with filePath gating, all hidden when in transcribe mode. After the test was red the component was created. DocumentTopBar dropped from 303 lines to 166. The orchestrator now just composes BackButton, DocumentTopBarTitle, PersonChipRow, OverflowPillButton, the details toggle, DocumentTopBarActions, DocumentMobileMenu, and DocumentMetadataDrawer — each visual region named in one or two words. Refs #496. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import PersonChipRow from '$lib/person/PersonChipRow.svelte';
|
||||
import OverflowPillButton from '$lib/shared/primitives/OverflowPillButton.svelte';
|
||||
import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte';
|
||||
import DocumentTopBarTitle from './DocumentTopBarTitle.svelte';
|
||||
import DocumentTopBarActions from './DocumentTopBarActions.svelte';
|
||||
import DocumentMobileMenu from './DocumentMobileMenu.svelte';
|
||||
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
||||
|
||||
@@ -120,85 +121,15 @@ const overflowPersons = $derived(receivers.slice(2));
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex shrink-0 items-center gap-1.5 font-sans">
|
||||
{#if canWrite && isPdf && !transcribeMode}
|
||||
<button
|
||||
onclick={() => (transcribeMode = true)}
|
||||
aria-label={m.transcription_mode_label()}
|
||||
aria-pressed={false}
|
||||
class="hidden items-center gap-1.5 rounded border border-primary px-3 py-1.5 font-sans text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary md:flex"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
{m.transcription_mode_label()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if transcribeMode}
|
||||
<button
|
||||
onclick={() => (transcribeMode = false)}
|
||||
aria-label={m.transcription_mode_stop()}
|
||||
aria-pressed={true}
|
||||
class="flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 font-sans text-[16px] font-medium text-primary-fg transition focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
{m.transcription_mode_stop()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if canWrite && !transcribeMode}
|
||||
<a
|
||||
href="/documents/{doc.id}/edit"
|
||||
aria-label={m.btn_edit()}
|
||||
class="flex items-center gap-1.5 rounded border border-primary bg-transparent px-3 py-1.5 text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<img
|
||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span class="hidden sm:inline">{m.btn_edit()}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if doc.filePath && !transcribeMode}
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={doc.originalFilename}
|
||||
class="hidden rounded border border-transparent bg-muted p-1.5 text-ink transition hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary md:block"
|
||||
title={m.doc_download_title()}
|
||||
>
|
||||
<img
|
||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Download-MD.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-5 w-5 shrink-0"
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
<DocumentTopBarActions
|
||||
documentId={doc.id}
|
||||
canWrite={canWrite}
|
||||
isPdf={!!isPdf}
|
||||
bind:transcribeMode={transcribeMode}
|
||||
filePath={doc.filePath}
|
||||
originalFilename={doc.originalFilename}
|
||||
fileUrl={fileUrl}
|
||||
/>
|
||||
|
||||
{#if (canWrite && isPdf) || doc.filePath}
|
||||
<div class="md:hidden">
|
||||
|
||||
103
frontend/src/lib/document/DocumentTopBarActions.svelte
Normal file
103
frontend/src/lib/document/DocumentTopBarActions.svelte
Normal file
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
type Props = {
|
||||
documentId: string;
|
||||
canWrite: boolean;
|
||||
isPdf: boolean;
|
||||
transcribeMode: boolean;
|
||||
filePath?: string | null;
|
||||
originalFilename?: string | null;
|
||||
fileUrl: string;
|
||||
};
|
||||
|
||||
let {
|
||||
documentId,
|
||||
canWrite,
|
||||
isPdf,
|
||||
transcribeMode = $bindable(),
|
||||
filePath = null,
|
||||
originalFilename = null,
|
||||
fileUrl
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if canWrite && isPdf && !transcribeMode}
|
||||
<button
|
||||
onclick={() => (transcribeMode = true)}
|
||||
aria-label={m.transcription_mode_label()}
|
||||
aria-pressed={false}
|
||||
class="hidden items-center gap-1.5 rounded border border-primary px-3 py-1.5 font-sans text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary md:flex"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
{m.transcription_mode_label()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if transcribeMode}
|
||||
<button
|
||||
onclick={() => (transcribeMode = false)}
|
||||
aria-label={m.transcription_mode_stop()}
|
||||
aria-pressed={true}
|
||||
class="flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 font-sans text-[16px] font-medium text-primary-fg transition focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
{m.transcription_mode_stop()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if canWrite && !transcribeMode}
|
||||
<a
|
||||
href="/documents/{documentId}/edit"
|
||||
aria-label={m.btn_edit()}
|
||||
class="flex items-center gap-1.5 rounded border border-primary bg-transparent px-3 py-1.5 text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<img
|
||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span class="hidden sm:inline">{m.btn_edit()}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if filePath && !transcribeMode}
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={originalFilename}
|
||||
class="hidden rounded border border-transparent bg-muted p-1.5 text-ink transition hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary md:block"
|
||||
title={m.doc_download_title()}
|
||||
>
|
||||
<img
|
||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Download-MD.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-5 w-5 shrink-0"
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import DocumentTopBarActions from './DocumentTopBarActions.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const baseProps = {
|
||||
documentId: 'd1',
|
||||
canWrite: false,
|
||||
isPdf: false,
|
||||
transcribeMode: false,
|
||||
filePath: null as string | null,
|
||||
originalFilename: 'brief.pdf' as string | null,
|
||||
fileUrl: ''
|
||||
};
|
||||
|
||||
describe('DocumentTopBarActions', () => {
|
||||
it('renders nothing visible when canWrite is false and no file is present', async () => {
|
||||
render(DocumentTopBarActions, { props: baseProps });
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: /transkribieren/i }))
|
||||
.not.toBeInTheDocument();
|
||||
await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument();
|
||||
await expect.element(page.getByTitle('Herunterladen')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the transcribe button when canWrite, isPdf, and not transcribing', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: { ...baseProps, canWrite: true, isPdf: true, filePath: 'docs/x.pdf' }
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('button', { name: /transkribieren/i })).toBeVisible();
|
||||
});
|
||||
|
||||
it('omits the transcribe button when not a PDF', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: { ...baseProps, canWrite: true, isPdf: false, filePath: 'docs/x.jpg' }
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: /transkribieren/i }))
|
||||
.not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the stop-transcribe button when transcribeMode is true', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: {
|
||||
...baseProps,
|
||||
canWrite: true,
|
||||
isPdf: true,
|
||||
transcribeMode: true,
|
||||
filePath: 'docs/x.pdf'
|
||||
}
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('button', { name: /fertig/i })).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders the edit link to the document edit route when canWrite and not transcribing', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: { ...baseProps, canWrite: true, documentId: 'doc-42' }
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /bearbeiten/i }))
|
||||
.toHaveAttribute('href', '/documents/doc-42/edit');
|
||||
});
|
||||
|
||||
it('hides the edit link when transcribeMode is true', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: { ...baseProps, canWrite: true, transcribeMode: true }
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the download link when filePath is set and not in transcribe mode', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: { ...baseProps, filePath: 'docs/x.pdf', fileUrl: '/api/docs/x' }
|
||||
});
|
||||
|
||||
await expect.element(page.getByTitle('Herunterladen')).toBeVisible();
|
||||
});
|
||||
|
||||
it('hides the download link when transcribeMode is true', async () => {
|
||||
render(DocumentTopBarActions, {
|
||||
props: { ...baseProps, filePath: 'docs/x.pdf', fileUrl: '/api/docs/x', transcribeMode: true }
|
||||
});
|
||||
|
||||
await expect.element(page.getByTitle('Herunterladen')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user