abbe7a18fc
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import { apiClient } from './client';
|
|
// TODO: P2-F3 — Replace hardcoded ENTITY_URL_MAP with dynamic backend config
|
|
|
|
export interface SearchResult {
|
|
type: string;
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
url: string;
|
|
score?: number;
|
|
data?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface SearchFacet {
|
|
value: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface SearchFilters {
|
|
entityTypes?: string[];
|
|
tags?: string[];
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
sort?: 'relevance' | 'date' | 'name';
|
|
}
|
|
|
|
export interface SearchResponse {
|
|
results: SearchResult[];
|
|
facets?: Record<string, SearchFacet[]>;
|
|
summary?: string;
|
|
suggestions?: string[];
|
|
}
|
|
|
|
export interface FacetsResponse {
|
|
entity_types: string[];
|
|
tags: string[];
|
|
date_ranges: Record<string, { min: string | null; max: string | null }>;
|
|
}
|
|
|
|
// Map backend entity_type to frontend route URL
|
|
const ENTITY_URL_MAP: Record<string, (id: string, data?: Record<string, unknown>) => string> = {
|
|
contact: (id) => `/contacts/${id}`,
|
|
company: (id) => `/contacts/${id}`,
|
|
mail: (id) => `/mail?message_id=${id}`,
|
|
file: (id) => `/dms?file_id=${id}`,
|
|
event: (id) => `/calendar?event_id=${id}`,
|
|
message: (_id, data) => data?.conversation_id ? `/communication?conversation_id=${data.conversation_id}` : '/communication',
|
|
};
|
|
|
|
function mapBackendResult(r: Record<string, unknown>): SearchResult {
|
|
const entityType = (r.entity_type as string) || 'unknown';
|
|
const entityId = (r.entity_id as string) || (r.id as string) || '';
|
|
const title = (r.title as string) || (r.name as string) || '';
|
|
const snippet = (r.snippet as string) || (r.description as string) || '';
|
|
const data = (r.data as Record<string, unknown>) || {};
|
|
const urlFn = ENTITY_URL_MAP[entityType];
|
|
const url = urlFn ? urlFn(entityId, data) : '#';
|
|
return {
|
|
type: entityType,
|
|
id: entityId,
|
|
name: title,
|
|
description: snippet,
|
|
url,
|
|
score: (r.score as number) || 0,
|
|
data,
|
|
};
|
|
}
|
|
|
|
export async function search(query: string, filters: SearchFilters = {}, limit = 20): Promise<SearchResponse> {
|
|
const r = await apiClient.post('/search', {
|
|
query,
|
|
entity_types: filters.entityTypes,
|
|
tags: filters.tags,
|
|
date_from: filters.dateFrom,
|
|
date_to: filters.dateTo,
|
|
sort: filters.sort || 'relevance',
|
|
limit,
|
|
});
|
|
const data = r.data;
|
|
// Backend returns { results: [{entity_type, entity_id, title, snippet, score, data}], ... }
|
|
const rawResults = data.results || [];
|
|
return {
|
|
results: rawResults.map(mapBackendResult),
|
|
facets: data.facets,
|
|
summary: data.summary,
|
|
suggestions: data.suggestions,
|
|
};
|
|
}
|
|
|
|
export async function fetchFacets(): Promise<FacetsResponse> {
|
|
const r = await apiClient.get('/search/facets');
|
|
return r.data as FacetsResponse;
|
|
}
|
|
|
|
export async function searchSuggest(q: string): Promise<string[]> {
|
|
const r = await apiClient.get('/search/suggest', { params: { q } });
|
|
return r.data.suggestions || [];
|
|
}
|
|
|
|
export async function searchSimilar(entityType: string, entityId: string): Promise<SearchResult[]> {
|
|
const r = await apiClient.post('/search/similar', {
|
|
entity_type: entityType,
|
|
entity_id: entityId,
|
|
});
|
|
const similar = r.data.similar || {};
|
|
const all: SearchResult[] = [];
|
|
for (const items of Object.values(similar) as Record<string, unknown>[][]) {
|
|
for (const item of items) {
|
|
all.push(mapBackendResult(item));
|
|
}
|
|
}
|
|
return all;
|
|
}
|