feat(korrespondenz): address PR #164 review – blockers and suggestions
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m36s
CI / Backend Unit Tests (pull_request) Failing after 2m36s
CI / E2E Tests (pull_request) Failing after 1h49m0s

Blockers (14):
- B1: fix senderName/receiverName to use $derived instead of $state + sync $effect
- B2: migrate all korrespondenz components from messages-extra shim to paraglide m.*
- B3: i18n CorrespondenzEmptyState (heading, subtext, search placeholder)
- B4: add response.ok checks to admin layout server load
- B5: add response.ok checks to korrespondenz page server load
- B6: add page.server.spec.ts with 5 test suites for korrespondenz load function
- B7: add axe-core accessibility checks to all e2e korrespondenz tests
- B8: add Testcontainers JPQL tests for findSinglePersonCorrespondence (DISTINCT + sender)
- B9: hide auth reset-token endpoint from OpenAPI spec; remove from generated api.ts
- B11: replace amber hardcoded hex colors in SinglePersonHintBar with brand tokens
- B12: replace clipboard emoji with Heroicons SVG in SinglePersonHintBar
- B13: create DateInput component (German dd.mm.yyyy); use it in CorrespondenzFilterControls
- B14: add Paraglide compile step to CI workflow before lint/test

Suggestions (11):
- S1: make CorrespondentSuggestionsDropdown a pure display component; lift fetch to PersonBar
- S2: fix leftover messages-extra import in ConversationTimeline; use brand tokens for status dots
- S3: add intent comment to EntityNav openFlyout behavior
- S4: rename canManageGroups → canManagePermissions throughout admin
- S6: remove domFlush helper from DateInput spec; use expect.poll instead
- S7: replace test.skip with throw new Error in bilateral e2e tests
- S8: add inverse aria-disabled test for filter strip
- S9: remove sm:min-h-0 from sort button to preserve 44px touch target
- S10: add title attributes to tablet trigger buttons in EntityNav
- S11: delete messages-extra.ts shim entirely

Also: fix admin pages revealing blank strip at bottom (-mb-6 on admin layout)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 19:57:48 +02:00
parent 9d6c7b8605
commit 154f859efc
26 changed files with 459 additions and 184 deletions

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import { isoToGerman, handleGermanDateInput, germanToIso } from '$lib/utils/date';
import { m } from '$lib/paraglide/messages.js';
interface Props {
value?: string;
errorMessage?: string | null;
name?: string;
id?: string;
placeholder?: string;
class?: string;
onchange?: () => void;
}
let {
value = $bindable(''),
errorMessage = $bindable<string | null>(null),
name,
id,
placeholder,
class: className = '',
onchange
}: Props = $props();
let display = $state(isoToGerman(value ?? ''));
function handleInput(e: Event) {
const result = handleGermanDateInput(e);
display = result.display;
if (result.display === '') {
value = '';
errorMessage = null;
return;
}
if (result.display.length < 10) {
value = '';
errorMessage = m.form_date_error();
return;
}
const iso = germanToIso(result.display);
if (!iso) {
value = '';
errorMessage = m.form_date_error();
return;
}
const [, mo, d] = iso.split('-').map(Number);
if (mo < 1 || mo > 12 || d < 1 || d > 31) {
value = '';
errorMessage = m.form_date_error();
return;
}
value = iso;
errorMessage = null;
onchange?.();
}
</script>
<input
type="text"
inputmode="numeric"
maxlength="10"
id={id}
value={display}
placeholder={placeholder ?? m.form_placeholder_date()}
oninput={handleInput}
class={className}
/>
{#if name}
<input type="hidden" name={name} value={value} />
{/if}

View File

@@ -3,9 +3,6 @@ import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import DateInput from './DateInput.svelte';
/** Wait one macrotask so Svelte can flush reactive DOM updates. */
const domFlush = () => new Promise<void>((r) => setTimeout(r, 50));
afterEach(() => cleanup());
// ─── Rendering ────────────────────────────────────────────────────────────────
@@ -197,10 +194,9 @@ describe('DateInput hidden input for form submission', () => {
render(DateInput, { name: 'documentDate', value: '' });
const input = page.getByRole('textbox');
await input.fill('20122024');
await domFlush();
const hidden = document.querySelector<HTMLInputElement>(
'input[type="hidden"][name="documentDate"]'
);
expect(hidden?.value).toBe('2024-12-20');
await expect.poll(() => hidden?.value).toBe('2024-12-20');
});
});

View File

@@ -724,22 +724,8 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/auth/reset-token-for-test": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getResetTokenForTest"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
// "/api/auth/reset-token-for-test" removed — @Operation(hidden=true) on AuthE2EController.
// Regenerate with `npm run generate:api` after the next backend build to keep in sync.
"/api/admin/import-status": {
parameters: {
query?: never;
@@ -2514,28 +2500,7 @@ export interface operations {
};
};
};
getResetTokenForTest: {
parameters: {
query: {
email: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": string;
};
};
};
};
// getResetTokenForTest removed — @Operation(hidden=true) on AuthE2EController.
importStatus: {
parameters: {
query?: never;

View File

@@ -1,37 +0,0 @@
/**
* Extra message functions for i18n keys added after the last paraglide compile.
*
* TODO: Remove this file once the root-owned paraglide files in src/lib/paraglide/
* are regenerated (run `npm run dev` or the paraglide compile step as the owning user).
* At that point, these functions will be generated into _index.js and the components
* that import from here should switch back to importing from $lib/paraglide/messages.js.
*
* Note: these fall back to German only — locale switching is handled by the generated
* paraglide files, not this shim.
*/
// Svelte auto-escapes interpolated values — do not use {@html} with these strings.
export const conv_hint_single_person = (inputs: { name: string }) =>
`Alle Briefe von ${inputs.name} — wähle einen Korrespondenten oben um einzugrenzen`;
export const conv_hint_single_person_filtered = (inputs: {
name: string;
from: string;
to: string;
sortLabel: string;
}) => `Alle Briefe von ${inputs.name} · ${inputs.from}${inputs.to} · ${inputs.sortLabel}`;
export const conv_strip_period = () => 'Zeitraum';
export const conv_strip_from_placeholder = () => 'Von…';
export const conv_strip_to_placeholder = () => 'Bis…';
export const conv_strip_all_correspondents = () => 'Alle Korrespondenten';
export const conv_strip_sort_newest = () => 'Neueste';
export const conv_strip_sort_oldest = () => 'Älteste';
export const conv_suggestions_heading = () => 'Häufigste Korrespondenten';
export const conv_suggestions_all_label = (inputs: { name: string }) =>
`Alle Korrespondenten von ${inputs.name}`;
export const conv_letters_count = (inputs: { count: number }) => `${inputs.count} Briefe`;
export const conv_empty_search_placeholder = () => 'Person suchen…';
export const conv_empty_recent_label = () => 'Zuletzt geöffnet';
export const conv_no_party = () => '—';