@kit-data-manager/react-search-component
Version:
All-in-one component for rendering an elastic search UI for searching anything. Built-in support for visualizing related items in a graph and resolving unique identifiers.
197 lines • 14.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { RelationsGraphContext } from "../../components/graph/RelationsGraphContext";
import { ReactSearchComponentContext } from "../../components/ReactSearchComponentContext";
import { useStore } from "zustand/index";
import { resultCache } from "../../lib/ResultCache";
import { DateTime } from "luxon";
import { ChevronDown, Download, GitFork, LoaderCircle, Microscope, Pencil, PlusIcon, SearchIcon, TableProperties } from "lucide-react";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../../components/ui/dropdown-menu";
import { GenericResultViewTag } from "../../components/result/GenericResultViewTag";
import { z } from "zod";
import { GenericResultViewImage } from "../../components/result/GenericResultViewImage";
import { GraphNodeUtils } from "../../components/graph/GraphNodeUtils";
import { Dialog, DialogContent, DialogTitle } from "../../components/ui/dialog";
import { PidComponent } from "@kit-data-manager/react-pid-component";
import { autoUnwrap, autoUnwrapArray, toArray } from "../../lib/utils";
const HTTP_REGEX = /https?:\/\/.*/;
/**
* Configurable result view component that can be customized for specific use cases. Will display a card for each search result from elastic. If this component
* doesn't fit your needs, feel free to implement your own result view.
*/
export function GenericResultView({ result, titleField = "name", descriptionField = "description", imageField, invertImageInDarkMode = false, landingPageLocationField = "landingPageLocation", digitalObjectLocationField = "digitalObjectLocation", pidField = "pid", childItemPidField = "isMetadataFor", parentItemPidField = "hasMetadata", creationDateField = "creationDate", editedDateField = "editedDate", additionalIdentifierField = "identifier", relatedItemsPrefetchOptions = { searchFields: { pid: {} } }, tags = [], showOpenInFairDoScope = true, showInspectFDO = true }) {
const { openOrAddToRelationsGraph } = useContext(RelationsGraphContext);
const { searchTerm, elasticConnector, searchFor, config } = useContext(ReactSearchComponentContext);
const addToResultCache = useStore(resultCache, (s) => s.set);
const [loadingRelatedItems, setLoadingRelatedItems] = useState(false);
const [showInspectDialog, setShowInspectDialog] = useState(false);
const getField = useCallback((field) => {
try {
const value = autoUnwrap(z
.string()
.or(z.number())
.or(z.object({ raw: z.string().or(z.number()) }))
.optional()
.parse(result[field]));
return value ? value + "" : undefined;
}
catch (e) {
console.warn(`Parsing field ${field} failed`, e);
return undefined;
}
}, [result]);
const getArrayField = useCallback((field) => {
try {
const value = autoUnwrapArray(z
.string()
.array()
.or(z.number().array())
.or(z.object({ raw: z.string().array() }))
.or(z.object({ raw: z.number().array() }))
.optional()
.parse(result[field]));
return value ? value.map((v) => v + "") : [];
}
catch (e) {
console.warn(`Parsing array field ${field} failed`, e);
return [];
}
}, [result]);
const getArrayOrSingleField = useCallback((field) => {
const _field = result[field];
if (Array.isArray(_field) || (typeof _field === "object" && _field && "raw" in _field && Array.isArray(_field.raw))) {
return getArrayField(field);
}
else {
return getField(field);
}
}, [getArrayField, getField, result]);
const extractPid = useCallback((pidFieldValue) => {
if (pidFieldValue.startsWith("https://")) {
return /https:\/\/.*?\/(.*)/.exec(pidFieldValue)?.[1] ?? pidFieldValue;
}
else {
return pidFieldValue;
}
}, []);
const pid = useMemo(() => {
const _pid = getField(pidField ?? "pid");
return _pid ? extractPid(_pid) : _pid;
}, [extractPid, getField, pidField]);
const title = useMemo(() => {
const maybeArray = getArrayOrSingleField(titleField ?? "name");
return Array.isArray(maybeArray) ? maybeArray.join(", ") : maybeArray;
}, [getArrayOrSingleField, titleField]);
const description = useMemo(() => {
const maybeArray = getArrayOrSingleField(descriptionField ?? "description");
return Array.isArray(maybeArray) ? maybeArray.join("\n\r") : maybeArray;
}, [descriptionField, getArrayOrSingleField]);
const doLocation = useMemo(() => {
const value = getField(digitalObjectLocationField ?? "digitalObjectLocation");
if (!value)
return undefined;
if (HTTP_REGEX.test(value))
return value;
else
return `https://doi.org/${value}`;
}, [digitalObjectLocationField, getField]);
const landingPageLocation = useMemo(() => {
return getField(landingPageLocationField ?? "landingPageLocation");
}, [getField, landingPageLocationField]);
const previewImage = useMemo(() => {
const images = getArrayOrSingleField(imageField ?? "imageURL");
const normalized = Array.isArray(images) ? (images.length === 1 ? images[0] : images) : images;
if (config.imageProxy && normalized) {
return Array.isArray(normalized) ? normalized.map(config.imageProxy) : config.imageProxy(normalized);
}
else
return normalized;
}, [config, getArrayOrSingleField, imageField]);
const identifier = useMemo(() => {
const maybeArray = getArrayOrSingleField(additionalIdentifierField ?? "identifier");
return Array.isArray(maybeArray) ? maybeArray.join(" - ") : maybeArray;
}, [getArrayOrSingleField, additionalIdentifierField]);
const isMetadataFor = useMemo(() => {
const val = getArrayOrSingleField(childItemPidField ?? "isMetadataFor");
return val ? toArray(val) : undefined;
}, [getArrayOrSingleField, childItemPidField]);
const creationDate = useMemo(() => {
const value = getField(creationDateField ?? "dateCreated");
if (!value)
return undefined;
const dateTime = DateTime.fromISO(value);
return dateTime.isValid ? dateTime.toLocaleString() : value;
}, [creationDateField, getField]);
const editedDate = useMemo(() => {
const value = getField(editedDateField ?? "editedDate");
if (!value)
return undefined;
const dateTime = DateTime.fromISO(value);
return dateTime.isValid ? dateTime.toLocaleString() : value;
}, [editedDateField, getField]);
const hasMetadata = useMemo(() => {
const val = getArrayOrSingleField(parentItemPidField ?? "hasMetadata");
return val ? toArray(val) : undefined;
}, [getArrayOrSingleField, parentItemPidField]);
const fetchRelatedItems = useCallback(async (term, amount) => {
try {
const search = await elasticConnector?.onSearch({ searchTerm: term, resultsPerPage: amount + 5 }, {
result_fields: {},
searchTerm: term,
search_fields: relatedItemsPrefetchOptions?.searchFields ?? { [pidField ?? "pid"]: {} },
resultsPerPage: amount
});
if (search) {
for (const entry of search.results) {
const pid = autoUnwrap(entry[pidField ?? "pid"]);
if (!pid)
continue;
addToResultCache(pid, entry);
}
}
}
catch (e) {
console.warn("Failed to fetch related items", e);
alert("Failed to fetch related items, graph may be incomplete");
}
}, [addToResultCache, elasticConnector, pidField, relatedItemsPrefetchOptions?.searchFields]);
const showRelatedItemsGraph = useCallback(async () => {
if (!pid)
return;
setLoadingRelatedItems(true);
if (isMetadataFor)
await fetchRelatedItems(isMetadataFor.join(" "), isMetadataFor.length);
if (hasMetadata)
await fetchRelatedItems(hasMetadata.join(" "), hasMetadata.length);
setLoadingRelatedItems(false);
const nodes = GraphNodeUtils.buildSequentialGraphFromIds("result", hasMetadata ?? [], pid, isMetadataFor ?? []);
openOrAddToRelationsGraph(nodes, {
focusedNodes: [pid]
});
}, [fetchRelatedItems, hasMetadata, isMetadataFor, openOrAddToRelationsGraph, pid]);
const showRelatedItemsButton = useMemo(() => {
return (hasMetadata && hasMetadata.length > 0) || (isMetadataFor && isMetadataFor.length > 0);
}, [hasMetadata, isMetadataFor]);
const relatedItemsAmount = useMemo(() => {
return (hasMetadata ? hasMetadata.length : 0) + (isMetadataFor ? isMetadataFor.length : 0);
}, [hasMetadata, isMetadataFor]);
const exactPidMatch = useMemo(() => {
return searchTerm === pid || searchTerm === doLocation;
}, [doLocation, pid, searchTerm]);
const searchForThis = useCallback(() => {
if (!pid)
return;
setTimeout(() => {
searchFor(pid);
}, 100);
}, [pid, searchFor]);
useEffect(() => {
if (pid) {
addToResultCache(pid, result);
}
}, [addToResultCache, pid, result]);
return (_jsxs("div", { className: `rfs-m-2 rfs-rounded-lg rfs-border rfs-border-border rfs-bg-background rfs-p-4 rfs-group/resultView ${exactPidMatch ? "rfs-outline-primary rfs-outline" : ""}`, children: [_jsxs("div", { className: `rfs-grid ${imageField ? "rfs-grid-rows-[150px_1fr] md:rfs-grid-cols-[200px_1fr] md:rfs-grid-rows-1" : ""} rfs-gap-4 rfs-overflow-x-auto md:rfs-max-w-full`, children: [imageField && _jsx(GenericResultViewImage, { previewImage: previewImage, title: title, invertImageInDarkMode: invertImageInDarkMode }), _jsxs("div", { className: "rfs-flex rfs-flex-col rfs-overflow-x-auto md:rfs-max-w-full", children: [exactPidMatch && (_jsx("div", { className: "rfs-mb-2", children: _jsx(Badge, { children: "Exact Match" }) })), _jsxs("div", { children: [_jsx("div", { className: "rfs-font-bold md:rfs-text-xl rfs-mr-2", children: title }), _jsxs("div", { className: "rfs-flex rfs-text-sm rfs-font-normal rfs-text-muted-foreground rfs-gap-3", children: [identifier, creationDate && (_jsxs("div", { className: "rfs-flex rfs-items-center", children: [_jsx(PlusIcon, { className: "rfs-size-3 rfs-mr-0.5" }), " ", creationDate] })), editedDate && (_jsxs("div", { className: "rfs-flex rfs-items-center", children: [_jsx(Pencil, { className: "rfs-size-3 rfs-mr-0.5" }), " ", editedDate] }))] })] }), _jsx("a", { href: `https://hdl.handle.net/${pid}?noredirect`, target: "_blank", className: "rfs-mb-2 rfs-block rfs-leading-3 hover:rfs-underline", children: _jsx("span", { className: "rfs-text-sm rfs-text-muted-foreground", children: pid }) }), _jsx("div", { className: "rfs-flex rfs-flex-wrap rfs-gap-2 rfs-truncate rfs-items-center", children: tags && tags.map((tag, i) => _jsx(GenericResultViewTag, { result: result, ...tag }, i)) }), _jsx("div", { className: "rfs-grow", children: description }), _jsxs("div", { className: "rfs-mt-8 rfs-flex rfs-flex-col rfs-flex-wrap rfs-justify-end rfs-gap-2 md:rfs-flex-row md:rfs-items-center md:rfs-gap-4", children: [showRelatedItemsButton && (_jsxs("div", { className: "rfs-flex rfs-items-center", children: [_jsxs(Button, { className: "rfs-grow rfs-rounded-r-none", disabled: loadingRelatedItems, size: "sm", variant: "secondary", onClick: showRelatedItemsGraph, children: [loadingRelatedItems ? (_jsx(LoaderCircle, { className: "rfs-mr-1 rfs-size-4 rfs-animate-spin" })) : (_jsx(GitFork, { className: "rfs-mr-1 rfs-size-4" })), "Show Related Items"] }), _jsx(Button, { className: "rfs-rounded-l-none rfs-border-l rfs-border-l-border rfs-text-xs rfs-font-bold", size: "sm", variant: "secondary", onClick: showRelatedItemsGraph, children: relatedItemsAmount })] })), landingPageLocation && (_jsxs("div", { className: "rfs-flex rfs-items-center", children: [_jsx("a", { href: landingPageLocation, target: "_blank", className: "rfs-grow", children: _jsx(Button, { size: "sm", className: "rfs-w-full rfs-rounded-r-none rfs-px-4", children: "Open" }) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", className: "rfs-rounded-l-none rfs-border-l", children: _jsx(ChevronDown, { className: "rfs-mr-1 rfs-size-4" }) }) }), _jsxs(DropdownMenuContent, { children: [_jsx("a", { href: doLocation, target: "_blank", children: _jsxs(DropdownMenuItem, { children: [_jsx(Download, { className: "rfs-mr-1 rfs-size-4" }), " Download Digital Object"] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: searchForThis, children: [_jsx(SearchIcon, { className: "rfs-mr-1 rfs-size-4" }), " Search for this"] }), showOpenInFairDoScope && (_jsx("a", { href: `https://kit-data-manager.github.io/fairdoscope/?pid=${pid}`, target: "_blank", children: _jsxs(DropdownMenuItem, { children: [_jsx(Microscope, { className: "rfs-mr-1 rfs-size-4" }), " Open in FAIR-DOscope"] }) })), showInspectFDO && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => setShowInspectDialog(true), children: [_jsx(TableProperties, { className: "rfs-size-4 rfs-mr-1" }), " Inspect FDO"] })] }))] })] })] }))] })] })] }), showInspectFDO && (_jsx(Dialog, { open: showInspectDialog, onOpenChange: setShowInspectDialog, children: _jsxs(DialogContent, { className: "!rfs-max-w-[calc(100vw-40px)] !rfs-min-w-[min(1200px,calc(100vw-40px))]", children: [_jsx(DialogTitle, { children: "Inspect Result" }), _jsx("div", { className: "rfs-overflow-auto", children: _jsx(PidComponent, { openByDefault: true, value: pid, levelOfSubcomponents: 10 }) })] }) }))] }));
}
//# sourceMappingURL=GenericResultView.js.map