Files
nexarch/web/mail-search/lib/manticoreQuery.ts
T
sysopsandClaude Sonnet 5 acc2b0c5dd SRC-06: facetten-ui-filter-chips
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
2026-08-31 23:18:08 +02:00

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;
}