UNPKG

@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
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