Skip to content

Commit d1ab714

Browse files
committed
Add Orcid resolving
1 parent 106120b commit d1ab714

File tree

7 files changed

+132
-27
lines changed

7 files changed

+132
-27
lines changed

src/components/result/GenericResultView.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useStore } from "zustand/index"
55
import { resultCache } from "@/lib/ResultCache"
66
import { autoUnwrap, autoUnwrapArray, toArray } from "@/components/result/utils"
77
import { DateTime } from "luxon"
8-
import { ChevronDown, GitFork, ImageOff, LinkIcon, Microscope, SearchIcon } from "lucide-react"
8+
import { ChevronDown, Download, GitFork, ImageOff, Microscope, Pencil, PlusIcon, SearchIcon } from "lucide-react"
99
import { Badge } from "@/components/ui/badge"
1010
import { Button } from "@/components/ui/button"
1111
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
@@ -73,10 +73,15 @@ export interface GenericResultViewProps {
7373
parentItemPidField?: string
7474

7575
/**
76-
* The elastic field where the creation date of the FDO will be read from. Will be parsed as an ISO String.
76+
* The elastic field where the creation date of the FDO will be read from. Will be parsed as an ISO Date.
7777
*/
7878
creationDateField?: string
7979

80+
/**
81+
* The elastic field where the edited date of the FDO will be read from. Will be parsed as an ISO Date.
82+
*/
83+
editedDateField?: string
84+
8085
/**
8186
* The elastic field where an additional identifier will be read from. You don't need to provide this if you don't have any additional identifiers apart from the PID.
8287
*/
@@ -109,6 +114,7 @@ export function GenericResultView({
109114
relatedItemPidsField = "isMetadataFor",
110115
parentItemPidField = "hasMetadata",
111116
creationDateField = "creationDate",
117+
editedDateField = "editedDate",
112118
additionalIdentifierField = "identifier",
113119
relatedItemsPrefetch = { searchFields: { pid: {} } },
114120
tags = [],
@@ -229,6 +235,13 @@ export function GenericResultView({
229235
return dateTime.isValid ? dateTime.toLocaleString() : value
230236
}, [creationDateField, getField])
231237

238+
const editedDate = useMemo(() => {
239+
const value = getField(editedDateField ?? "editedDate")
240+
if (!value) return undefined
241+
const dateTime = DateTime.fromISO(value)
242+
return dateTime.isValid ? dateTime.toLocaleString() : value
243+
}, [editedDateField, getField])
244+
232245
const hasMetadata = useMemo(() => {
233246
const val = getArrayOrSingleField(parentItemPidField ?? "hasMetadata")
234247
return val ? toArray(val) : undefined
@@ -326,9 +339,19 @@ export function GenericResultView({
326339
)}
327340
<div className="rfs-font-bold md:rfs-text-xl">
328341
{title}
329-
<span className="rfs-ml-2 rfs-text-sm rfs-font-normal rfs-text-muted-foreground">
330-
{identifier} {identifier && creationDate ? "-" : ""} {creationDate}
331-
</span>
342+
<div className="rfs-flex rfs-ml-2 rfs-text-sm rfs-font-normal rfs-text-muted-foreground rfs-gap-3">
343+
{identifier}
344+
{creationDate && (
345+
<div className="rfs-flex rfs-items-center">
346+
<PlusIcon className="rfs-size-3 rfs-mr-0.5" /> {creationDate}
347+
</div>
348+
)}
349+
{editedDate && (
350+
<div className="rfs-flex rfs-items-center">
351+
<Pencil className="rfs-size-3 rfs-mr-0.5" /> {editedDate}
352+
</div>
353+
)}
354+
</div>
332355
</div>
333356
<a href={`https://hdl.handle.net/${pid}`} target="_blank" className="rfs-mb-2 rfs-block rfs-leading-3 hover:rfs-underline">
334357
<span className="rfs-text-sm rfs-text-muted-foreground">{pid}</span>
@@ -370,7 +393,7 @@ export function GenericResultView({
370393
<DropdownMenuContent>
371394
<a href={doLocation} target="_blank">
372395
<DropdownMenuItem>
373-
<LinkIcon className="rfs-mr-1 rfs-size-4" /> Digital Object
396+
<Download className="rfs-mr-1 rfs-size-4" /> Download Digital Object
374397
</DropdownMenuItem>
375398
</a>
376399
<DropdownMenuItem onClick={searchForThis}>

src/components/result/GenericResultViewTag.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode, useMemo } from "react"
1+
import { ReactNode, useCallback, useMemo } from "react"
22
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
33
import { Badge } from "@/components/ui/badge"
44
import { SearchResult } from "@elastic/search-ui"
@@ -9,7 +9,7 @@ export interface GenericResultViewTagProps {
99
result: SearchResult
1010
icon?: ReactNode
1111
label?: string
12-
valueMapper?: (value: string) => string
12+
valueMapper?: (value: string | string[]) => string | ReactNode
1313
}
1414

1515
export function GenericResultViewTag({ field, result, icon, label, valueMapper }: GenericResultViewTagProps) {
@@ -20,22 +20,34 @@ export function GenericResultViewTag({ field, result, icon, label, valueMapper }
2020
else return value
2121
}, [field, result, valueMapper])
2222

23-
const base = useMemo(() => {
24-
return (
25-
<Badge variant="secondary" className="rfs-truncate">
26-
<span className="rfs-flex rfs-truncate">
27-
{icon} {value}
28-
</span>
29-
</Badge>
30-
)
31-
}, [icon, value])
23+
const base = useCallback(
24+
(value: string) => {
25+
return (
26+
<Badge variant="secondary" className="rfs-truncate">
27+
<span className="rfs-flex rfs-truncate">
28+
{icon} {value}
29+
</span>
30+
</Badge>
31+
)
32+
},
33+
[icon]
34+
)
3235

33-
if (!label) return base
36+
if (!label) return base(value)
3437
if (!value) return null
3538

39+
if (Array.isArray(value)) {
40+
return value.map((entry, i) => (
41+
<Tooltip delayDuration={500} key={i}>
42+
<TooltipTrigger>{base(entry)}</TooltipTrigger>
43+
<TooltipContent>{label}</TooltipContent>
44+
</Tooltip>
45+
))
46+
}
47+
3648
return (
3749
<Tooltip delayDuration={500}>
38-
<TooltipTrigger>{base}</TooltipTrigger>
50+
<TooltipTrigger>{base(value)}</TooltipTrigger>
3951
<TooltipContent>{label}</TooltipContent>
4052
</Tooltip>
4153
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useEffect, useMemo } from "react"
2+
import useSWRImmutable from "swr/immutable"
3+
4+
async function orcidFetch(orcid: string) {
5+
const req = await fetch(`https://pub.orcid.org/v3.0/${orcid}`, {
6+
headers: {
7+
Accept: "application/json"
8+
}
9+
})
10+
return req.json()
11+
}
12+
13+
export function OrcidDisplay({ orcid }: { orcid: string }) {
14+
const formattedOrcid = useMemo(() => {
15+
if (orcid.startsWith("https://orcid.org/")) return orcid.replace("https://orcid.org/", "")
16+
return orcid
17+
}, [orcid])
18+
19+
const { data, error } = useSWRImmutable(formattedOrcid, orcidFetch)
20+
21+
const familyName = useMemo(() => {
22+
if (!data) return null
23+
return data["person"]["name"]["family-name"]["value"]
24+
}, [data])
25+
26+
const givenName = useMemo(() => {
27+
if (!data) return null
28+
return data["person"]["name"]["given-names"]["value"]
29+
}, [data])
30+
31+
useEffect(() => {
32+
if (error) console.log(error)
33+
}, [error])
34+
35+
if (familyName && givenName) return givenName + " " + familyName
36+
return null
37+
}

src/components/result/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from "./GenericResultView"
2+
export type * from "./GenericResultView"
3+
export * from "./GenericResultViewImageCarousel"
4+
export type * from "./GenericResultViewImageCarousel"
5+
export * from "./PidDisplay"
6+
export type * from "./PidDisplay"
7+
export * from "./utils"

src/components/search/DefaultFacetOption.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export function DefaultFacetOption({
2929

3030
return (
3131
<div key={value} className="rfs-flex rfs-max-w-full rfs-items-center rfs-gap-2 rfs-break-words rfs-p-1 rfs-pb-2">
32-
<Checkbox id={value} checked={option.selected} onCheckedChange={(v) => (v ? onSelect(value) : onRemove(value))} />
33-
<Label htmlFor={value} className="rfs-min-w-0 rfs-grow rfs-break-words">
32+
<Checkbox id={value + facetConfig.key} checked={option.selected} onCheckedChange={(v) => (v ? onSelect(value) : onRemove(value))} />
33+
<Label htmlFor={value + facetConfig.key} className="rfs-min-w-0 rfs-grow rfs-break-words">
3434
{value ? (
3535
facetConfig.usePidResolver ? (
3636
<PidDisplay pid={value} />

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { GenericResultView } from "@/components/result/GenericResultView"
33
export type { ResultViewProps } from "@elastic/react-search-ui-views"
44
export * from "@/config/FairDOConfig"
55
export * from "@/config/FairDOConfigBuilder"
6+
export * from "@/components/result"

src/stories/FairDOElasticSearch.stories.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import type { FairDOConfig } from "@/config/FairDOConfig.ts"
33
import type { Meta, StoryObj } from "@storybook/react"
44
import { FairDOElasticSearch } from "@/components/FairDOElasticSearch"
55
import { GenericResultView } from "@/components/result/GenericResultView"
6-
import { AtomIcon, GlobeIcon, GraduationCap, ScaleIcon } from "lucide-react"
6+
import { AtomIcon, AudioLines, CircleDot, GlobeIcon, GraduationCap, Microscope, ScaleIcon, UserIcon } from "lucide-react"
77
import { tryURLPrettyPrint } from "@/lib/utils"
8+
import { PidDisplay } from "@/components/result/PidDisplay"
9+
import { OrcidDisplay } from "@/components/result/OrcidDisplay"
810

911
const meta = {
1012
component: FairDOElasticSearch,
@@ -23,7 +25,7 @@ const demoConfig: FairDOConfig = {
2325
apiKey: "UGNoTW1KUUJ3WmluUHBTcEVpalo6cGloOUVKZ0tTdnlMYVlpTzV4SXBrUQ==",
2426
indices: [
2527
{
26-
name: "fdo-test-5",
28+
name: "fdo-test-6",
2729
facets: [
2830
{
2931
key: "resourceType.keyword",
@@ -151,6 +153,12 @@ export const GenericResultRenderer: Story = {
151153
result={props.result}
152154
invertImageInDarkMode
153155
tags={[
156+
{
157+
icon: <UserIcon className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
158+
label: "Contact",
159+
field: "contact",
160+
valueMapper: (v) => (Array.isArray(v) ? v.map((vv) => <OrcidDisplay orcid={vv} />) : <OrcidDisplay orcid={v} />)
161+
},
154162
{
155163
icon: <GraduationCap className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
156164
label: "Resource Type",
@@ -159,24 +167,41 @@ export const GenericResultRenderer: Story = {
159167
{
160168
icon: <GlobeIcon className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
161169
field: "hadPrimarySource",
162-
valueMapper: tryURLPrettyPrint,
170+
valueMapper: (v) => tryURLPrettyPrint(v + ""),
163171
label: "Source"
164172
},
165173
{
166174
icon: <ScaleIcon className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
167175
field: "licenseURL",
168-
valueMapper: tryURLPrettyPrint,
176+
valueMapper: (v) => tryURLPrettyPrint(v + ""),
169177
label: "License URL"
170178
},
171179
{
172180
icon: <AtomIcon className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
173181
field: "Compound.Molar_mass",
174182
label: "Molar Mass",
175183
valueMapper: (v) => v + " g/mol"
184+
},
185+
{
186+
icon: <Microscope className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
187+
label: "NMR Method",
188+
field: "NMR_Method",
189+
valueMapper: (v) => <PidDisplay pid={v + ""} />
190+
},
191+
{
192+
icon: <AudioLines className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
193+
label: "Pulse Sequence Name",
194+
field: "Pulse_Sequence_Name"
195+
},
196+
{
197+
icon: <CircleDot className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
198+
label: "Acquisition Nucleus",
199+
field: "Acquisition_Nucleus"
176200
}
177201
]}
178202
titleField="name"
179203
creationDateField="dateCreatedRfc3339"
204+
editedDateField={"dateModified"}
180205
additionalIdentifierField="identifier"
181206
digitalObjectLocationField="digitalObjectLocation"
182207
imageField="locationPreview/Sample"
@@ -206,13 +231,13 @@ export const CompoundSlider: Story = {
206231
{
207232
icon: <GlobeIcon className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
208233
field: "hadPrimarySource",
209-
valueMapper: tryURLPrettyPrint,
234+
valueMapper: (v) => tryURLPrettyPrint(v + ""),
210235
label: "Source"
211236
},
212237
{
213238
icon: <ScaleIcon className="rfs-shrink-0 rfs-size-4 rfs-mr-2" />,
214239
field: "licenseURL",
215-
valueMapper: tryURLPrettyPrint,
240+
valueMapper: (v) => tryURLPrettyPrint(v + ""),
216241
label: "License URL"
217242
},
218243
{

0 commit comments

Comments
 (0)