feat: implement durable permission-aware search
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ExternalLink, Search } from "lucide-react";
|
||||
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { ChevronDown, ExternalLink, Search } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
DismissibleAlert,
|
||||
@@ -15,26 +15,31 @@ 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 moduleKey = params.getAll("module").join("\u001f");
|
||||
const resourceTypeKey = params.getAll("resource_type").join("\u001f");
|
||||
const contextId = params.get("context") ?? undefined;
|
||||
const modules = useMemo(
|
||||
() => moduleKey ? moduleKey.split("\u001f") : [],
|
||||
[moduleKey]
|
||||
);
|
||||
const resourceTypes = useMemo(
|
||||
() => resourceTypeKey ? resourceTypeKey.split("\u001f") : [],
|
||||
[resourceTypeKey]
|
||||
);
|
||||
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]
|
||||
() => JSON.stringify([query, moduleKey, resourceTypeKey, contextId]),
|
||||
[contextId, moduleKey, query, resourceTypeKey]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(query);
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!query.trim()) {
|
||||
setResponse(null);
|
||||
return;
|
||||
}
|
||||
const loadResults = useCallback((cursor?: string) => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
@@ -45,20 +50,48 @@ export default function SearchPage({ settings }: PlatformRouteContext) {
|
||||
modules,
|
||||
resourceTypes,
|
||||
contextKind: modules.length ? "module" : "global",
|
||||
contextId: params.get("context") ?? undefined,
|
||||
limit: 100
|
||||
contextId,
|
||||
limit: 50,
|
||||
cursor
|
||||
},
|
||||
controller.signal
|
||||
).
|
||||
then(setResponse).
|
||||
then((next) => {
|
||||
setResponse((current) =>
|
||||
cursor && current ?
|
||||
{
|
||||
...next,
|
||||
results: [...current.results, ...next.results],
|
||||
diagnostics: [
|
||||
...current.diagnostics,
|
||||
...next.diagnostics.filter((item) =>
|
||||
!current.diagnostics.some(
|
||||
(currentItem) => currentItem.provider_id === item.provider_id
|
||||
)
|
||||
)
|
||||
]
|
||||
} :
|
||||
next
|
||||
);
|
||||
}).
|
||||
catch((reason) => {
|
||||
if ((reason as Error).name !== "AbortError") {
|
||||
setError(reason instanceof Error ? reason.message : "Search failed.");
|
||||
}
|
||||
}).
|
||||
finally(() => setLoading(false));
|
||||
return controller;
|
||||
}, [contextId, modules, query, resourceTypes, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!query.trim()) {
|
||||
setResponse(null);
|
||||
return;
|
||||
}
|
||||
setResponse(null);
|
||||
const controller = loadResults();
|
||||
return () => controller.abort();
|
||||
}, [requestKey, settings]);
|
||||
}, [loadResults, requestKey]);
|
||||
|
||||
function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
@@ -123,6 +156,16 @@ export default function SearchPage({ settings }: PlatformRouteContext) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{response?.next_cursor &&
|
||||
<button
|
||||
type="button"
|
||||
className="search-load-more"
|
||||
disabled={loading}
|
||||
onClick={() => loadResults(response.next_cursor ?? undefined)}>
|
||||
<ChevronDown size={16} />
|
||||
<span>Load more</span>
|
||||
</button>
|
||||
}
|
||||
</PageScrollViewport>
|
||||
</main>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user