From c5e28ac18e69372838d09f762e49a77d86a4e8fc Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 22:52:12 +0100 Subject: [PATCH] feat(#68): lead new document form with file upload, all metadata optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the "New Document" page so users can save quickly: - FileSectionNew becomes the first element, redesigned as a prominent upload zone with an icon and large click target - Title field is rendered standalone below the upload zone; it auto-populates from the filename (via parseFilename + stripExtension fallback) unless the user has already typed something - All remaining metadata (who/when, description, transcription) moves into a collapsible "Weitere Details" section that auto-expands when URL prefill data or a form error is present, or when filename parsing detects a date/person - title is no longer required — the form can be saved with only a file - DescriptionSection gains a `hideTitle` prop for use in this layout - `form_label_title` translation key no longer carries a hardcoded `*`; the asterisk is rendered by the template only when `titleRequired` is set (currently only the edit form) - E2E tests added for all three scenarios from the issue Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/documents.spec.ts | 56 ++++++++++++- frontend/messages/de.json | 3 +- frontend/messages/en.json | 3 +- frontend/messages/es.json | 3 +- .../document/DescriptionSection.svelte | 54 ++++++------- .../src/routes/documents/new/+page.svelte | 80 ++++++++++++++++--- .../documents/new/FileSectionNew.svelte | 71 +++++++++++----- 7 files changed, 203 insertions(+), 67 deletions(-) diff --git a/frontend/e2e/documents.spec.ts b/frontend/e2e/documents.spec.ts index 20c9a976..3f122c29 100644 --- a/frontend/e2e/documents.spec.ts +++ b/frontend/e2e/documents.spec.ts @@ -77,12 +77,49 @@ test.describe('Document detail', () => { }); test.describe('New document', () => { - test('renders the upload form', async ({ page }) => { + test('renders the upload form with file input first', async ({ page }) => { await page.goto('/documents/new'); + await page.waitForSelector('[data-hydrated]'); await expect(page.getByRole('heading', { name: /Neues Dokument/i })).toBeVisible(); - await expect(page.getByLabel('Titel')).toBeVisible(); + // File input comes before the title field in DOM order + const fileInput = page.locator('input[type="file"]'); + const titleInput = page.getByLabel('Titel'); + await expect(fileInput).toBeVisible(); + await expect(titleInput).toBeVisible(); + const fileBox = await fileInput.boundingBox(); + const titleBox = await titleInput.boundingBox(); + expect(fileBox!.y).toBeLessThan(titleBox!.y); await page.screenshot({ path: 'test-results/e2e/document-new.png' }); }); + + test('title field is pre-filled from filename when a file is selected', async ({ page }) => { + await page.goto('/documents/new'); + await page.waitForSelector('[data-hydrated]'); + const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); + const fileInput = page.locator('input[type="file"]'); + await fileInput.setInputFiles({ + name: 'Brief_1965.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + }); + await expect(page.getByLabel('Titel')).toHaveValue('Brief_1965'); + await page.screenshot({ path: 'test-results/e2e/document-new-filename-prefill.png' }); + }); + + test('typed title is not overwritten when a file is selected', async ({ page }) => { + await page.goto('/documents/new'); + await page.waitForSelector('[data-hydrated]'); + await page.getByLabel('Titel').fill('Weihnachtsbrief 1965'); + const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); + const fileInput = page.locator('input[type="file"]'); + await fileInput.setInputFiles({ + name: 'Brief_1965.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + }); + await expect(page.getByLabel('Titel')).toHaveValue('Weihnachtsbrief 1965'); + await page.screenshot({ path: 'test-results/e2e/document-new-title-not-overwritten.png' }); + }); }); test.describe('Document creation', () => { @@ -97,6 +134,21 @@ test.describe('Document creation', () => { await expect(page.getByRole('heading', { name: 'E2E Testbrief' })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/document-create.png' }); }); + + test('user saves a document with only a file — title comes from filename', async ({ page }) => { + await page.goto('/documents/new'); + await page.waitForSelector('[data-hydrated]'); + const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); + await page.locator('input[type="file"]').setInputFiles({ + name: 'Brief_1965.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + }); + await page.getByRole('button', { name: 'Speichern', exact: true }).click(); + await expect(page).toHaveURL(/\/documents\/[^/]+$/); + await expect(page.getByRole('heading', { name: 'Brief_1965' })).toBeVisible(); + await page.screenshot({ path: 'test-results/e2e/document-create-file-only.png' }); + }); }); test.describe('Document editing', () => { diff --git a/frontend/messages/de.json b/frontend/messages/de.json index e9d4d967..16718738 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -39,7 +39,7 @@ "form_placeholder_location": "z.B. Berlin, Wien…", "form_label_sender": "Absender", "form_label_receivers": "Empfänger", - "form_label_title": "Titel *", + "form_label_title": "Titel", "form_label_tags": "Schlagworte", "form_label_content": "Inhalt", "form_placeholder_content": "Kurze Beschreibung des Inhalts…", @@ -75,6 +75,7 @@ "doc_file_replace_label": "Neue Datei hochladen", "doc_file_replace_note": "(ersetzt die aktuelle Datei)", "doc_current_file_label": "Aktuelle Datei:", + "doc_more_details": "Weitere Details", "doc_new_heading": "Neues Dokument", "doc_edit_heading": "Bearbeiten", "doc_section_details": "Details", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 61e34dc9..462dea3b 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -39,7 +39,7 @@ "form_placeholder_location": "e.g. Berlin, Vienna…", "form_label_sender": "Sender", "form_label_receivers": "Recipients", - "form_label_title": "Title *", + "form_label_title": "Title", "form_label_tags": "Tags", "form_label_content": "Content", "form_placeholder_content": "Brief description of the content…", @@ -75,6 +75,7 @@ "doc_file_replace_label": "Upload new file", "doc_file_replace_note": "(replaces the current file)", "doc_current_file_label": "Current file:", + "doc_more_details": "More details", "doc_new_heading": "New document", "doc_edit_heading": "Edit", "doc_section_details": "Details", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 51d7ebe8..0ae0b91a 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -39,7 +39,7 @@ "form_placeholder_location": "p.ej. Berlín, Viena…", "form_label_sender": "Remitente", "form_label_receivers": "Destinatarios", - "form_label_title": "Título *", + "form_label_title": "Título", "form_label_tags": "Etiquetas", "form_label_content": "Contenido", "form_placeholder_content": "Breve descripción del contenido…", @@ -75,6 +75,7 @@ "doc_file_replace_label": "Subir nuevo archivo", "doc_file_replace_note": "(reemplaza el archivo actual)", "doc_current_file_label": "Archivo actual:", + "doc_more_details": "Más detalles", "doc_new_heading": "Nuevo documento", "doc_edit_heading": "Editar", "doc_section_details": "Detalles", diff --git a/frontend/src/lib/components/document/DescriptionSection.svelte b/frontend/src/lib/components/document/DescriptionSection.svelte index 6d2c13c4..b83ac296 100644 --- a/frontend/src/lib/components/document/DescriptionSection.svelte +++ b/frontend/src/lib/components/document/DescriptionSection.svelte @@ -9,7 +9,8 @@ let { initialDocumentLocation = '', initialSummary = '', titleRequired = false, - suggestedTitle = '' + suggestedTitle = '', + hideTitle = false }: { tags?: string[]; initialTitle?: string; @@ -17,17 +18,12 @@ let { initialSummary?: string; titleRequired?: boolean; suggestedTitle?: string; + hideTitle?: boolean; } = $props(); -let titleValue = $state(untrack(() => initialTitle)); let titleDirty = $state(false); - -$effect(() => { - const suggested = suggestedTitle; - if (suggested && !untrack(() => titleDirty)) { - titleValue = suggested; - } -}); +let titleOverride = $state(untrack(() => initialTitle)); +let titleValue = $derived(titleDirty ? titleOverride : suggestedTitle || titleOverride);
@@ -36,25 +32,27 @@ $effect(() => {
- -
- - { - titleValue = (e.target as HTMLInputElement).value; - titleDirty = true; - }} - required={titleRequired} - class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink" - /> -
+ {#if !hideTitle} + +
+ + { + titleOverride = (e.target as HTMLInputElement).value; + titleDirty = true; + }} + required={titleRequired} + class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink" + /> +
+ {/if}
diff --git a/frontend/src/routes/documents/new/+page.svelte b/frontend/src/routes/documents/new/+page.svelte index ac2d8a6f..5e84accb 100644 --- a/frontend/src/routes/documents/new/+page.svelte +++ b/frontend/src/routes/documents/new/+page.svelte @@ -17,6 +17,30 @@ let selectedReceivers: { id: string; firstName: string; lastName: string }[] = $ ); let parsedSuggestion = $state({}); + +// Title is derived from the filename suggestion unless the user has typed something +let titleDirty = $state(false); +let titleOverride = $state(''); +let titleValue = $derived( + titleDirty ? titleOverride : (parsedSuggestion.suggestedTitle ?? titleOverride) +); + +// Details panel: starts open when prefill data is present or a form error occurred. +// Auto-opens when filename parsing finds a date/sender, but never force-closes — user +// can always collapse the section manually. +let detailsOpen = $state( + !!( + untrack(() => data.initialSenderId) || + untrack(() => data.initialReceivers).length > 0 || + untrack(() => form)?.error + ) +); + +$effect(() => { + if (parsedSuggestion.dateIso || senderId || selectedReceivers.length > 0) { + detailsOpen = true; + } +});
@@ -49,21 +73,51 @@ let parsedSuggestion = $state({}); {/if}
- - - + (parsedSuggestion = r)} /> + +
+ + { + titleOverride = (e.target as HTMLInputElement).value; + titleDirty = true; + }} + class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink" + placeholder="Titel eingeben…" + /> +
+ + +
+ + {m.doc_more_details()} + +
+ + + +
+
+
import { m } from '$lib/paraglide/messages.js'; -import { parseFilename, type FilenameParseResult } from '$lib/utils/filename'; +import { parseFilename, stripExtension, type FilenameParseResult } from '$lib/utils/filename'; let { onfileParsed @@ -10,29 +10,58 @@ let { function handleFileChange(e: Event) { const file = (e.target as HTMLInputElement).files?.[0]; - if (file) onfileParsed?.(parseFilename(file.name)); + if (!file) return; + const parsed = parseFilename(file.name); + const result: FilenameParseResult = { + ...parsed, + suggestedTitle: parsed.suggestedTitle ?? stripExtension(file.name) + }; + onfileParsed?.(result); } -
-

- {m.doc_section_file()} -

+
+
+

+ {m.doc_section_file()} +

+
-