feat(search): add global titlebar search filters

This commit is contained in:
2026-07-29 22:01:18 +02:00
parent 6fcd9e0e65
commit bed704eb7a
8 changed files with 477 additions and 61 deletions
+10 -49
View File
@@ -1,54 +1,21 @@
import { Search, X } from "lucide-react";
import {
useEffect,
useMemo,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent
} from "react";
import { useLocation } from "react-router-dom";
import {
useGuardedNavigate,
usePlatformUiCapabilities,
type GlobalSearchProps,
type SearchContextContribution,
type SearchContextsUiCapability
type GlobalSearchProps
} from "@govoplan/core-webui";
import { search, type SearchResult } from "../api/search";
const MIN_SUGGESTION_LENGTH = 2;
function currentContext(
pathname: string,
contexts: SearchContextContribution[]
): SearchContextContribution | null {
return [...contexts].
filter((context) =>
context.pathPrefixes.some((prefix) =>
pathname === prefix || pathname.startsWith(`${prefix}/`)
)
).
sort((left, right) => {
const leftLength = Math.max(...left.pathPrefixes.map((item) => item.length));
const rightLength = Math.max(...right.pathPrefixes.map((item) => item.length));
return rightLength - leftLength || (left.order ?? 100) - (right.order ?? 100);
})[0] ?? null;
}
export default function GlobalSearch({ settings }: GlobalSearchProps) {
const navigate = useGuardedNavigate();
const location = useLocation();
const contextCapabilities =
usePlatformUiCapabilities<SearchContextsUiCapability>("search.contexts");
const contexts = useMemo(
() => contextCapabilities.flatMap((capability) => capability.contexts),
[contextCapabilities]
);
const context = useMemo(
() => currentContext(location.pathname, contexts),
[contexts, location.pathname]
);
const [value, setValue] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [open, setOpen] = useState(false);
@@ -58,9 +25,12 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) {
useEffect(() => {
function focusSearch(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
const commandSearch =
(event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k";
if (event.key === "F3" || commandSearch) {
event.preventDefault();
inputRef.current?.focus();
inputRef.current?.select();
}
}
window.addEventListener("keydown", focusSearch);
@@ -90,10 +60,7 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) {
settings,
{
query,
modules: context ? [context.moduleId] : undefined,
resourceTypes: context?.resourceTypes,
contextKind: context ? "module" : "global",
contextId: context?.id,
contextKind: "global",
limit: 6
},
controller.signal
@@ -113,7 +80,7 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) {
window.clearTimeout(timer);
controller.abort();
};
}, [context, settings, value]);
}, [settings, value]);
function openResult(result: SearchResult) {
setOpen(false);
@@ -125,13 +92,6 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) {
const query = value.trim();
if (!query) return;
const params = new URLSearchParams({ q: query });
if (context) {
params.set("module", context.moduleId);
params.set("context", context.id);
for (const resourceType of context.resourceTypes ?? []) {
params.append("resource_type", resourceType);
}
}
setOpen(false);
navigate(`/search?${params.toString()}`);
}
@@ -172,8 +132,9 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) {
ref={inputRef}
type="search"
value={value}
placeholder={context?.placeholder ?? "Search"}
aria-label={context ? `Search ${context.label}` : "Search"}
placeholder="Search"
aria-label="Global search"
aria-keyshortcuts="F3 Control+K Meta+K"
aria-expanded={open}
aria-controls="global-search-results"
onFocus={() => setOpen(results.length > 0)}