Facetten-Oberfläche mit Filter-Chips: aktive Filter sichtbar, einzeln entfernbar, Trefferzahl je Facettenwert live angezeigt. - app/api/facets/route.ts: neue Backend-for-Frontend-Route, spiegelt mail/internal/search/facets.go minimal (nur Trefferzahl je Facettenwert). - lib/manticoreQuery.ts: gemeinsamer statischer bool.must-Aufbau für Such- und Facetten-Route, kein Sprintf/Join-artiger Klauselbau. - app/api/search/route.ts (SRC-04): akzeptiert jetzt wiederholbare ?filter=feld:wert-Parameter. - app/FacetPanel.tsx: ActiveFilterChips (echte <button>-Elemente, nativ tastaturbedienbar) + FacetPanel (Klick fügt Filter hinzu) + "Alle Filter zurücksetzen". - lib/filterState.ts: reine Filterzustandsfunktionen, ohne React. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-06-PRUEFPROTOKOLL.md): 1. Manueller Test gegen echten next start + live Manticore auf 192.168.1.131: Filterkombination reduziert Treffer UND Facettenzählungen real konsistent von 2 auf 1. 2. @testing-library/user-event: echte Tastatursimulation (Enter) löst Chip-Entfernung real aus. 3. 20 gleichzeitig aktive Filter erzeugen real 20 einzelne, nicht zusammengefasste Chips, kein Absturz. Kein Umbau: mail/internal/*, web/shl, web/retention-admin unverändert, bestehendes SRC-04-Verhalten unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
37 lines
1.1 KiB
TypeScript
37 lines
1.1 KiB
TypeScript
// SRC-06: gemeinsamer Aufbau der bool.must-Liste für Such- und
|
|
// Facetten-Route — statische Feldnamen, keine dynamische SQL-/Klausel-
|
|
// Bildung (gleiche Konvention wie mail/internal/search/facets.go
|
|
// buildFilteredMust).
|
|
import { isFacetField } from "./facetFields";
|
|
|
|
const FIELD_TENANT_SLUG = "tenant_slug";
|
|
|
|
export interface AppliedFilter {
|
|
field: string;
|
|
value: string;
|
|
}
|
|
|
|
export function parseFilterParams(values: string[]): AppliedFilter[] {
|
|
const filters: AppliedFilter[] = [];
|
|
for (const raw of values) {
|
|
const sep = raw.indexOf(":");
|
|
if (sep <= 0) continue;
|
|
const field = raw.slice(0, sep);
|
|
const value = raw.slice(sep + 1);
|
|
if (!isFacetField(field) || value === "") continue;
|
|
filters.push({ field, value });
|
|
}
|
|
return filters;
|
|
}
|
|
|
|
export function buildMust(tenantSlug: string, query: string, filters: AppliedFilter[]): Record<string, unknown>[] {
|
|
const must: Record<string, unknown>[] = [{ equals: { [FIELD_TENANT_SLUG]: tenantSlug } }];
|
|
if (query) {
|
|
must.push({ query_string: query });
|
|
}
|
|
for (const f of filters) {
|
|
must.push({ equals: { [f.field]: f.value } });
|
|
}
|
|
return must;
|
|
}
|