From 8ce96294b0769e77163131cea64f182864e3d8cf Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 25 Apr 2026 17:17:33 +0200 Subject: [PATCH] =?UTF-8?q?fix(bulk-edit):=20cycle-2=20blockers=20?= =?UTF-8?q?=E2=80=94=20restore=20initial-*=20props,=20missing=20import,=20?= =?UTF-8?q?scope=20Esc,=20edit-mode=20topbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Felix B1 (data-loss regression on /documents/[id]/edit) — DocumentEditLayout still passes initialDateIso, initialLocation, initialDocumentLocation, but my cycle-1 cleanup removed those props. Result: existing values rendered empty and a save would have overwritten them with "". Restored the props on WhoWhenSection and DescriptionSection; initialisation now lives in onMount so it runs exactly once and never stomps a parent-driven update on a later prop change. Felix B2 — `DescriptionSection.svelte:36` still had the top-level `currentTitle = untrack(() => initialTitle)` mutation that I cleaned up in WhoWhenSection but missed here. Same onMount-once treatment. Leonie B5 — `enrich/+page.svelte:105` referenced `` but the import was lost in a prettier pass; svelte-check errored out and the bar never rendered, leaving an 8 rem dead zone from the pb-32 reservation. One-line fix: add the import. Leonie B6 — Esc handler in `BulkSelectionBar` was unscoped and stole Escape from NotificationBell, ConfirmDialog, HelpPopover, etc. (e.g. selecting docs → opening notification bell → Esc would close the bell AND silently wipe the selection). Now bails when an open dialog, expanded menu, or popover is detected. Elicit C1 — `BulkDocumentEditLayout` topbar now branches on `mode`: shows "Massenbearbeitung" + "{count} werden bearbeitet" in edit mode instead of the upload-flavoured "Mehrere Dokumente hochladen" + "werden erstellt" copy. New i18n keys `bulk_edit_topbar_title` and `bulk_edit_count_pill` in DE/EN/ES. Tests added: - DocumentControllerTest.patchBulk_stripsCarriageReturnsAndNewlinesFromErrorMessages (Sara C2 follow-up — pin sanitizeForLog as a regression test) - BulkSelectionBar.spec — count=1 → "1 Dokument", count=2 → "2 Dokumente" (Sara C6 follow-up — pin the new bulk_edit_n_selected_one/_other branch) Refs #225, PR #331 Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentControllerTest.java | 26 +++++++++++++++++++ frontend/messages/de.json | 4 ++- frontend/messages/en.json | 4 ++- frontend/messages/es.json | 4 ++- .../document/BulkDocumentEditLayout.svelte | 12 +++++++-- .../document/BulkSelectionBar.svelte | 14 +++++++--- .../document/BulkSelectionBar.svelte.spec.ts | 17 ++++++++++++ .../document/DescriptionSection.svelte | 17 +++++++++--- .../components/document/WhoWhenSection.svelte | 22 +++++++++++++--- frontend/src/routes/enrich/+page.svelte | 1 + 10 files changed, 104 insertions(+), 17 deletions(-) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index 19eadf2b..5d2ea90b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -1175,6 +1175,32 @@ class DocumentControllerTest { .andExpect(jsonPath("$[0].pdfUrl").value("/api/documents/" + id + "/file")); } + @Test + @WithMockUser(authorities = "WRITE_ALL") + void patchBulk_stripsCarriageReturnsAndNewlinesFromErrorMessages() throws Exception { + // Nora C4 — DocumentController.sanitizeForLog defends against + // CWE-117 (log injection) by replacing CR/LF in any free-form string + // it interpolates. Same helper now sanitises BulkEditError.message + // before it round-trips to the frontend. + when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); + UUID badId = UUID.randomUUID(); + when(documentService.applyBulkEditToDocument(eq(badId), any(), any())) + .thenThrow(org.raddatz.familienarchiv.exception.DomainException.notFound( + org.raddatz.familienarchiv.exception.ErrorCode.DOCUMENT_NOT_FOUND, + "evil\r\nFAKE LOG ENTRY: admin logged in")); + + mockMvc.perform(patch("/api/documents/bulk") + .contentType(MediaType.APPLICATION_JSON) + .content(bulkBody(badId.toString()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.errors[0].message", + org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString("\n")))) + .andExpect(jsonPath("$.errors[0].message", + org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString("\r")))) + .andExpect(jsonPath("$.errors[0].message", + org.hamcrest.Matchers.containsString("evil_"))); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void patchBulk_returnsPartialFailureShape_whenServiceThrowsForOneDocument() throws Exception { diff --git a/frontend/messages/de.json b/frontend/messages/de.json index cd9e60d8..0384e776 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -896,5 +896,7 @@ "bulk_edit_clear_selection": "Auswahl aufheben", "bulk_edit_clear_hint_keyboard": "Esc: Auswahl aufheben", "bulk_edit_loading": "Dokumente werden geladen…", - "bulk_edit_all_x_failed": "Filter konnte nicht abgerufen werden — bitte erneut versuchen." + "bulk_edit_all_x_failed": "Filter konnte nicht abgerufen werden — bitte erneut versuchen.", + "bulk_edit_topbar_title": "Massenbearbeitung", + "bulk_edit_count_pill": "{count} werden bearbeitet" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9c505a67..5e75ef71 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -896,5 +896,7 @@ "bulk_edit_clear_selection": "Clear selection", "bulk_edit_clear_hint_keyboard": "Esc: clear selection", "bulk_edit_loading": "Loading documents…", - "bulk_edit_all_x_failed": "Could not load filter results — please retry." + "bulk_edit_all_x_failed": "Could not load filter results — please retry.", + "bulk_edit_topbar_title": "Bulk edit", + "bulk_edit_count_pill": "{count} will be edited" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index fdd5dd22..12c2c5c9 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -896,5 +896,7 @@ "bulk_edit_clear_selection": "Limpiar selección", "bulk_edit_clear_hint_keyboard": "Esc: limpiar selección", "bulk_edit_loading": "Cargando documentos…", - "bulk_edit_all_x_failed": "No se pudieron cargar los resultados del filtro; vuelve a intentarlo." + "bulk_edit_all_x_failed": "No se pudieron cargar los resultados del filtro; vuelve a intentarlo.", + "bulk_edit_topbar_title": "Edición masiva", + "bulk_edit_count_pill": "Se editarán {count}" } diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte index 6dd18490..3e062058 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte @@ -324,12 +324,20 @@ async function retrySave() { - {isMulti ? m.bulk_title_multi() : m.bulk_title_single()} + {#if mode === 'edit'} + {m.bulk_edit_topbar_title()} + {:else} + {isMulti ? m.bulk_title_multi() : m.bulk_title_single()} + {/if} {#if isMulti} - {m.bulk_count_pill({ count: files.size })} + {#if mode === 'edit'} + {m.bulk_edit_count_pill({ count: files.size })} + {:else} + {m.bulk_count_pill({ count: files.size })} + {/if}