import { useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@arronqzy/ui"; import { useI18n } from "@arronqzy/i18n/react"; import { buildEndpointSuggestions, filterEndpointSuggestions, resolveFetchRequestUrl, type FetchHttpMethod, type SwaggerApiEndpoint, } from "@arronqzy/blueprint-dsl"; export type FetchUrlAutocompleteProps = { value: string; apiBaseUrl: string; endpoints: SwaggerApiEndpoint[]; placeholder?: string; /** 为 true 时仅能从列表选择,不可自由输入 URL */ selectOnly?: boolean; onChange: (value: string) => void; onSelectEndpoint?: (endpoint: SwaggerApiEndpoint, fullUrl: string) => void; }; export function FetchUrlAutocomplete({ value, apiBaseUrl, endpoints, placeholder, selectOnly = false, onChange, onSelectEndpoint, }: FetchUrlAutocompleteProps) { const { t } = useI18n(); const [open, setOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const [filterQuery, setFilterQuery] = useState(""); const containerRef = useRef(null); const suggestions = useMemo( () => buildEndpointSuggestions(apiBaseUrl, endpoints), [apiBaseUrl, endpoints] ); const query = selectOnly ? filterQuery : value; const filtered = useMemo( () => filterEndpointSuggestions(suggestions, query), [suggestions, query] ); const selectedSuggestion = useMemo( () => suggestions.find((item) => item.path === value), [suggestions, value] ); const inputValue = selectOnly ? open ? filterQuery : selectedSuggestion?.label ?? value : value; useEffect(() => { setActiveIndex(0); }, [query, filtered.length]); useEffect(() => { if (!open) return; const onPointerDown = (event: MouseEvent) => { if (!containerRef.current?.contains(event.target as globalThis.Node)) { setOpen(false); setFilterQuery(""); } }; window.addEventListener("pointerdown", onPointerDown); return () => window.removeEventListener("pointerdown", onPointerDown); }, [open]); const showSuggestions = open && endpoints.length > 0; const selectSuggestion = (index: number) => { const item = filtered[index]; if (!item) return; const nextValue = apiBaseUrl.trim() ? item.path : item.fullUrl; onChange(nextValue); onSelectEndpoint?.(item, item.fullUrl); setFilterQuery(""); setOpen(false); }; const openDropdown = () => { setOpen(true); if (selectOnly) { setFilterQuery(""); } }; const closeDropdown = () => { setOpen(false); setFilterQuery(""); }; return (
{ if (selectOnly) { setFilterQuery(e.target.value); setOpen(true); return; } onChange(e.target.value); setOpen(true); }} onFocus={openDropdown} onBlur={() => { if (selectOnly) { closeDropdown(); } }} onKeyDown={(e) => { if (!showSuggestions || filtered.length === 0) { if (selectOnly && e.key === "Escape") { closeDropdown(); } return; } if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((prev) => Math.max(prev - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); selectSuggestion(activeIndex); } else if (e.key === "Escape") { closeDropdown(); } }} className={cn( "flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-[11px] text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary", selectOnly && !open ? "cursor-pointer font-medium" : "font-mono" )} /> {showSuggestions ? (
{filtered.length === 0 ? (
{t("blueprint.config.noMatchingEndpoint")}
) : ( filtered.map((item, index) => ( )) )}
) : null}
); } export function resolveFetchUrlPreview( apiBaseUrl: string, url: string, endpoints: SwaggerApiEndpoint[] ): string { const matched = endpoints.find((item) => item.path === url); const resolvedPath = matched?.path ?? url; try { return resolveFetchRequestUrl({ url: resolvedPath, apiBaseUrl }); } catch { return url; } } export type { FetchHttpMethod };