feat(search): wire theme chip removal to URL navigation in +page.svelte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,9 +58,10 @@ test.describe('NL (smart) search — happy path', () => {
|
|||||||
// Loading panel announced to screen readers.
|
// Loading panel announced to screen readers.
|
||||||
await expect(page.getByText(/Archiv wird befragt/)).toBeVisible();
|
await expect(page.getByText(/Archiv wird befragt/)).toBeVisible();
|
||||||
|
|
||||||
// Directional chip (Walter → Emma) + keyword chip render once the fixture resolves.
|
// Directional chip (Walter → Emma) + keyword chip + theme chip render once the fixture resolves.
|
||||||
await expect(page.getByText('→')).toBeVisible();
|
await expect(page.getByText('→')).toBeVisible();
|
||||||
await expect(page.getByText('Stichwort: krieg')).toBeVisible();
|
await expect(page.getByText('Stichwort: krieg')).toBeVisible();
|
||||||
|
await expect(page.getByText(/Thema:.*Weltkrieg/)).toBeVisible();
|
||||||
|
|
||||||
// Accessibility — light mode.
|
// Accessibility — light mode.
|
||||||
const lightScan = await new AxeBuilder({ page })
|
const lightScan = await new AxeBuilder({ page })
|
||||||
@@ -82,4 +83,31 @@ test.describe('NL (smart) search — happy path', () => {
|
|||||||
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
||||||
await expect(page).toHaveURL(/receiverId=22222222-2222-2222-2222-222222222222/);
|
await expect(page).toHaveURL(/receiverId=22222222-2222-2222-2222-222222222222/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('removing the last theme chip drops tag/tagOp but keeps person params', async ({ page }) => {
|
||||||
|
await page.route('**/api/search/nl', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(nlResponse)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/documents');
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
await page.getByRole('button', { name: /Text/ }).click();
|
||||||
|
|
||||||
|
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…');
|
||||||
|
await input.fill('Was hat Walter an Emma im Krieg geschrieben?');
|
||||||
|
await input.press('Enter');
|
||||||
|
|
||||||
|
await expect(page.getByText(/Thema:.*Weltkrieg/)).toBeVisible();
|
||||||
|
|
||||||
|
// Remove the single theme chip — URL must carry sender UUID but no tag/tagOp.
|
||||||
|
await page.getByRole('button', { name: 'Filter entfernen: Thema: Weltkrieg' }).click();
|
||||||
|
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
||||||
|
const url = page.url();
|
||||||
|
expect(url).not.toMatch(/tag=/);
|
||||||
|
expect(url).not.toMatch(/tagOp=/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import TimelineDensityFilter from '$lib/document/TimelineDensityFilter.svelte';
|
|||||||
import SmartSearchStatus from '../search/SmartSearchStatus.svelte';
|
import SmartSearchStatus from '../search/SmartSearchStatus.svelte';
|
||||||
import InterpretationChipRow from '../search/InterpretationChipRow.svelte';
|
import InterpretationChipRow from '../search/InterpretationChipRow.svelte';
|
||||||
import type { ChipType } from '../search/chip-types.js';
|
import type { ChipType } from '../search/chip-types.js';
|
||||||
|
import { buildThemeRemovalUrl } from './theme-chip-removal.js';
|
||||||
import DisambiguationPicker from '../search/DisambiguationPicker.svelte';
|
import DisambiguationPicker from '../search/DisambiguationPicker.svelte';
|
||||||
import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte';
|
import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte';
|
||||||
import { getErrorMessage, parseBackendError } from '$lib/shared/errors';
|
import { getErrorMessage, parseBackendError } from '$lib/shared/errors';
|
||||||
@@ -284,6 +285,11 @@ function removeChip(type: ChipType, value?: string) {
|
|||||||
} else if (type === 'keyword' && value) {
|
} else if (type === 'keyword' && value) {
|
||||||
const remaining = nlInterpretation.keywords.filter((keyword) => keyword !== value);
|
const remaining = nlInterpretation.keywords.filter((keyword) => keyword !== value);
|
||||||
p.q = remaining.join(' ');
|
p.q = remaining.join(' ');
|
||||||
|
} else if (type === 'theme' && value) {
|
||||||
|
const url = buildThemeRemovalUrl(nlInterpretation, value);
|
||||||
|
resetNlState();
|
||||||
|
goto(url, { keepFocus: true, noScroll: true });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
applyResolvedAndSearch(p);
|
applyResolvedAndSearch(p);
|
||||||
}
|
}
|
||||||
|
|||||||
85
frontend/src/routes/documents/theme-chip-removal.spec.ts
Normal file
85
frontend/src/routes/documents/theme-chip-removal.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { buildThemeRemovalUrl } from './theme-chip-removal.js';
|
||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
|
||||||
|
|
||||||
|
function makeInterp(overrides: Partial<NlQueryInterpretation> = {}): NlQueryInterpretation {
|
||||||
|
return {
|
||||||
|
resolvedPersons: [],
|
||||||
|
ambiguousPersons: [],
|
||||||
|
keywords: [],
|
||||||
|
resolvedTags: [],
|
||||||
|
rawQuery: '',
|
||||||
|
keywordsApplied: false,
|
||||||
|
tagsApplied: true,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTag(id: string, name: string, color?: string) {
|
||||||
|
return color ? { id, name, color } : { id, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('buildThemeRemovalUrl', () => {
|
||||||
|
it('N remaining tags → N tag params + tagOp=OR', () => {
|
||||||
|
const interp = makeInterp({
|
||||||
|
resolvedTags: [
|
||||||
|
makeTag('aaa', 'Hochzeit'),
|
||||||
|
makeTag('bbb', 'Weltkrieg'),
|
||||||
|
makeTag('ccc', 'Familie')
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const url = buildThemeRemovalUrl(interp, 'Hochzeit');
|
||||||
|
const params = new URL(url, 'http://x').searchParams;
|
||||||
|
expect(params.getAll('tag')).toEqual(['Weltkrieg', 'Familie']);
|
||||||
|
expect(params.get('tagOp')).toBe('OR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('last tag removed → no tag or tagOp params in URL', () => {
|
||||||
|
const interp = makeInterp({
|
||||||
|
resolvedTags: [makeTag('aaa', 'Hochzeit')]
|
||||||
|
});
|
||||||
|
const url = buildThemeRemovalUrl(interp, 'Hochzeit');
|
||||||
|
const params = new URL(url, 'http://x').searchParams;
|
||||||
|
expect(params.getAll('tag')).toEqual([]);
|
||||||
|
expect(params.get('tagOp')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('last tag removed with resolved sender person → sender param intact', () => {
|
||||||
|
const interp = makeInterp({
|
||||||
|
resolvedPersons: [{ id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter' }],
|
||||||
|
resolvedTags: [makeTag('aaa', 'Hochzeit')]
|
||||||
|
});
|
||||||
|
const url = buildThemeRemovalUrl(interp, 'Hochzeit');
|
||||||
|
const params = new URL(url, 'http://x').searchParams;
|
||||||
|
expect(params.get('senderId')).toBe('11111111-1111-1111-1111-111111111111');
|
||||||
|
expect(params.getAll('tag')).toEqual([]);
|
||||||
|
expect(params.get('tagOp')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('null-color tag → tag name emitted correctly; color does not affect params', () => {
|
||||||
|
const interp = makeInterp({
|
||||||
|
resolvedTags: [makeTag('aaa', 'Erbschaft'), makeTag('bbb', 'Migration')]
|
||||||
|
});
|
||||||
|
const url = buildThemeRemovalUrl(interp, 'Erbschaft');
|
||||||
|
const params = new URL(url, 'http://x').searchParams;
|
||||||
|
expect(params.getAll('tag')).toEqual(['Migration']);
|
||||||
|
expect(params.get('tagOp')).toBe('OR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('directional pair → senderId and receiverId both emitted', () => {
|
||||||
|
const interp = makeInterp({
|
||||||
|
resolvedPersons: [
|
||||||
|
{ id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter' },
|
||||||
|
{ id: '22222222-2222-2222-2222-222222222222', displayName: 'Emma' }
|
||||||
|
],
|
||||||
|
resolvedTags: [makeTag('aaa', 'Krieg'), makeTag('bbb', 'Heimat')]
|
||||||
|
});
|
||||||
|
const url = buildThemeRemovalUrl(interp, 'Krieg');
|
||||||
|
const params = new URL(url, 'http://x').searchParams;
|
||||||
|
expect(params.get('senderId')).toBe('11111111-1111-1111-1111-111111111111');
|
||||||
|
expect(params.get('receiverId')).toBe('22222222-2222-2222-2222-222222222222');
|
||||||
|
expect(params.getAll('tag')).toEqual(['Heimat']);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
frontend/src/routes/documents/theme-chip-removal.ts
Normal file
26
frontend/src/routes/documents/theme-chip-removal.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
|
||||||
|
|
||||||
|
export function buildThemeRemovalUrl(
|
||||||
|
interp: NlQueryInterpretation,
|
||||||
|
removedTagName: string
|
||||||
|
): string {
|
||||||
|
const remaining = interp.resolvedTags.filter((t) => t.name !== removedTagName);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
const resolved = interp.resolvedPersons;
|
||||||
|
if (resolved.length >= 1) params.set('senderId', resolved[0].id);
|
||||||
|
if (resolved.length >= 2) params.set('receiverId', resolved[1].id);
|
||||||
|
if (interp.dateFrom) params.set('from', interp.dateFrom);
|
||||||
|
if (interp.dateTo) params.set('to', interp.dateTo);
|
||||||
|
if (interp.keywordsApplied && interp.keywords.length > 0) {
|
||||||
|
params.set('q', interp.keywords.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining.forEach((tag) => params.append('tag', tag.name));
|
||||||
|
if (remaining.length > 0) params.set('tagOp', 'OR');
|
||||||
|
|
||||||
|
const qs = params.toString();
|
||||||
|
return qs ? `/documents?${qs}` : '/documents';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user