feat: implement permission-aware search baseline
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { ExternalLink, Search } from "lucide-react";
|
||||
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
DismissibleAlert,
|
||||
LoadingIndicator,
|
||||
PageScrollViewport,
|
||||
useGuardedNavigate,
|
||||
type PlatformRouteContext
|
||||
} from "@govoplan/core-webui";
|
||||
import { search, type SearchResponse } from "../../api/search";
|
||||
|
||||
|
||||
export default function SearchPage({ settings }: PlatformRouteContext) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const query = params.get("q") ?? "";
|
||||
const modules = params.getAll("module");
|
||||
const resourceTypes = params.getAll("resource_type");
|
||||
const [draft, setDraft] = useState(query);
|
||||
const [response, setResponse] = useState<SearchResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const requestKey = useMemo(
|
||||
() => JSON.stringify([query, modules, resourceTypes]),
|
||||
[query, modules, resourceTypes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(query);
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!query.trim()) {
|
||||
setResponse(null);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
search(
|
||||
settings,
|
||||
{
|
||||
query,
|
||||
modules,
|
||||
resourceTypes,
|
||||
contextKind: modules.length ? "module" : "global",
|
||||
contextId: params.get("context") ?? undefined,
|
||||
limit: 100
|
||||
},
|
||||
controller.signal
|
||||
).
|
||||
then(setResponse).
|
||||
catch((reason) => {
|
||||
if ((reason as Error).name !== "AbortError") {
|
||||
setError(reason instanceof Error ? reason.message : "Search failed.");
|
||||
}
|
||||
}).
|
||||
finally(() => setLoading(false));
|
||||
return () => controller.abort();
|
||||
}, [requestKey, settings]);
|
||||
|
||||
function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
const next = new URLSearchParams(params);
|
||||
if (draft.trim()) next.set("q", draft.trim());
|
||||
else next.delete("q");
|
||||
setParams(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="search-page">
|
||||
<div className="search-page-toolbar">
|
||||
<form className="search-page-form" onSubmit={submit}>
|
||||
<Search size={18} aria-hidden="true" />
|
||||
<input
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
aria-label="Search query"
|
||||
placeholder="Search"
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
{loading && <LoadingIndicator size="sm" label="Searching" />}
|
||||
</div>
|
||||
<PageScrollViewport className="search-results-viewport">
|
||||
{error &&
|
||||
<DismissibleAlert tone="danger" onDismiss={() => setError("")}>
|
||||
{error}
|
||||
</DismissibleAlert>
|
||||
}
|
||||
{response?.diagnostics.map((diagnostic) =>
|
||||
<DismissibleAlert
|
||||
key={diagnostic.provider_id}
|
||||
tone="warning"
|
||||
compact>
|
||||
{diagnostic.message}
|
||||
</DismissibleAlert>
|
||||
)}
|
||||
{query && response && response.results.length === 0 &&
|
||||
<div className="search-empty">No results for “{query}”.</div>
|
||||
}
|
||||
<div className="search-result-list">
|
||||
{response?.results.map((result) =>
|
||||
<button
|
||||
type="button"
|
||||
className="search-result"
|
||||
key={`${result.module_id}:${result.resource_type}:${result.resource_id}`}
|
||||
onClick={() => navigate(result.url)}>
|
||||
<span className="search-result-heading">
|
||||
<strong>{result.title}</strong>
|
||||
{result.external_reference?.canonical_url &&
|
||||
<ExternalLink size={14} aria-label="External result" />
|
||||
}
|
||||
</span>
|
||||
{result.summary && <span className="search-result-summary">{result.summary}</span>}
|
||||
<span className="search-result-meta">
|
||||
{result.breadcrumbs.length > 0 ?
|
||||
result.breadcrumbs.join(" · ") :
|
||||
`${result.module_id} · ${result.resource_type}`}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PageScrollViewport>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user