feat(persons): show merge action inline with danger hint, remove Gefahrenzone collapsible (#342) #347
@@ -15,7 +15,7 @@ let mergeTargetId = $state('');
|
||||
let showMergeConfirm = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="mb-10 overflow-hidden rounded-sm border border-line bg-surface shadow-sm">
|
||||
<div class="mb-10 overflow-hidden rounded-sm border border-red-200 bg-surface shadow-sm">
|
||||
<div class="p-6 md:p-8">
|
||||
<h2 class="mb-1 font-serif text-lg text-ink">{m.person_merge_heading()}</h2>
|
||||
<p class="mb-5 font-sans text-sm text-ink-2">
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import PersonMergePanel from './PersonMergePanel.svelte';
|
||||
|
||||
vi.mock('$app/forms', () => ({ enhance: () => () => {} }));
|
||||
vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
|
||||
default: () => null
|
||||
}));
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const makePerson = (overrides = {}) => ({
|
||||
displayName: 'Hans Müller',
|
||||
...overrides
|
||||
});
|
||||
|
||||
// ─── Danger indicator ────────────────────────────────────────────────────────
|
||||
|
||||
describe('PersonMergePanel — danger indicator', () => {
|
||||
it('renders outer container with red border class', () => {
|
||||
const { container } = render(PersonMergePanel, {
|
||||
props: { person: makePerson(), form: null }
|
||||
});
|
||||
const panel = container.firstElementChild as HTMLElement;
|
||||
expect(panel?.classList.contains('border-red-200')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Initial state ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('PersonMergePanel — initial state', () => {
|
||||
it('renders merge heading', async () => {
|
||||
render(PersonMergePanel, { props: { person: makePerson(), form: null } });
|
||||
const heading = page.getByRole('heading', { level: 2 });
|
||||
await expect.element(heading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('merge button is disabled when no target selected', async () => {
|
||||
render(PersonMergePanel, { props: { person: makePerson(), form: null } });
|
||||
const mergeBtn = page.getByRole('button', { name: /zusammenführen/i });
|
||||
await expect.element(mergeBtn).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Error state ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('PersonMergePanel — error state', () => {
|
||||
it('renders mergeError when form contains error', async () => {
|
||||
render(PersonMergePanel, {
|
||||
props: { person: makePerson(), form: { mergeError: 'Zielperson nicht gefunden.' } }
|
||||
});
|
||||
await expect.element(page.getByText('Zielperson nicht gefunden.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import BackButton from '$lib/components/BackButton.svelte';
|
||||
import PersonEditForm from './PersonEditForm.svelte';
|
||||
import PersonEditSaveBar from './PersonEditSaveBar.svelte';
|
||||
import NameHistoryEditCard from './NameHistoryEditCard.svelte';
|
||||
import PersonDangerZone from './PersonDangerZone.svelte';
|
||||
import PersonMergePanel from '../PersonMergePanel.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
const person = $derived(data.person);
|
||||
@@ -35,7 +35,9 @@ const person = $derived(data.person);
|
||||
|
||||
<NameHistoryEditCard aliases={data.aliases} canWrite={true} aliasError={form?.aliasError} />
|
||||
|
||||
<PersonDangerZone person={person} form={form} />
|
||||
{#key person.id}
|
||||
<PersonMergePanel person={person} form={form} />
|
||||
{/key}
|
||||
|
||||
<PersonEditSaveBar discardHref="/persons/{person.id}" formId="person-edit-form" />
|
||||
</div>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import PersonMergePanel from '../PersonMergePanel.svelte';
|
||||
|
||||
let {
|
||||
person,
|
||||
form
|
||||
}: {
|
||||
person: { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||
form?: { mergeError?: string } | null;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="mt-8 overflow-hidden rounded-sm border border-red-200 bg-surface shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (open = !open)}
|
||||
class="flex w-full items-center justify-between px-6 py-4 text-left"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span class="text-sm font-bold tracking-widest text-red-600 uppercase">
|
||||
{m.person_danger_zone_heading()}
|
||||
</span>
|
||||
<svg
|
||||
class="h-4 w-4 text-red-400 transition-transform {open ? 'rotate-180' : ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div class="border-t border-red-100 px-6 py-4">
|
||||
{#key person.id}
|
||||
<PersonMergePanel person={person} form={form} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user