feat: implement durable permission-aware search
This commit is contained in:
@@ -27,6 +27,8 @@ export type SearchResult = {
|
||||
breadcrumbs: string[];
|
||||
external_reference?: SearchExternalReference | null;
|
||||
metadata: Record<string, unknown>;
|
||||
source_revision?: string | null;
|
||||
provenance: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SearchResponse = {
|
||||
@@ -37,6 +39,8 @@ export type SearchResponse = {
|
||||
status: "unavailable";
|
||||
message: string;
|
||||
}>;
|
||||
next_cursor?: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
export type SearchRequest = {
|
||||
@@ -47,6 +51,8 @@ export type SearchRequest = {
|
||||
contextId?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
cursor?: string;
|
||||
language?: string;
|
||||
};
|
||||
|
||||
export function search(
|
||||
@@ -63,7 +69,9 @@ export function search(
|
||||
context_kind: request.contextKind,
|
||||
context_id: request.contextId,
|
||||
limit: request.limit,
|
||||
offset: request.offset
|
||||
offset: request.offset,
|
||||
cursor: request.cursor,
|
||||
language: request.language
|
||||
}),
|
||||
{ signal }
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -222,6 +222,36 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search-load-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-top: 14px;
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(
|
||||
var(--control-gradient-start),
|
||||
var(--control-gradient-end)
|
||||
);
|
||||
color: var(--control-text);
|
||||
cursor: pointer;
|
||||
padding: 7px 12px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-load-more:hover:not(:disabled) {
|
||||
background: linear-gradient(
|
||||
var(--control-gradient-start),
|
||||
var(--control-gradient-end-hover)
|
||||
);
|
||||
}
|
||||
|
||||
.search-load-more:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.global-search {
|
||||
width: 34px;
|
||||
|
||||
Reference in New Issue
Block a user