Skip to content

Commit 3502119

Browse files
committed
Fixes and allow relation graph to display n:n relations
1 parent 0751e2c commit 3502119

9 files changed

+146
-91
lines changed

src/components/GlobalModalProvider.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ import { RFS_GlobalModalContext } from "./RFS_GlobalModalContext"
99

1010
export function GlobalModalProvider(props: PropsWithChildren) {
1111
const [relationGraphState, setRelationGraphState] = useState<{
12-
base: RelationNode
13-
referenced: RelationNode[]
12+
source: RelationNode[]
13+
target: RelationNode[]
1414
isOpen: boolean
1515
}>({
16-
base: { id: "", label: "" },
17-
referenced: [],
16+
source: [],
17+
target: [],
1818
isOpen: false
1919
})
2020

21-
const openRelationGraph = useCallback((base: RelationNode, referenced: RelationNode[]) => {
21+
const openRelationGraph = useCallback((source: RelationNode[], target: RelationNode[]) => {
2222
setRelationGraphState({
23-
base,
24-
referenced,
23+
source,
24+
target,
2525
isOpen: true
2626
})
2727
}, [])
@@ -36,8 +36,8 @@ export function GlobalModalProvider(props: PropsWithChildren) {
3636
<RelationsGraphModal
3737
isOpen={relationGraphState.isOpen}
3838
onOpenChange={onRelationGraphOpenChange}
39-
base={relationGraphState.base}
40-
referenced={relationGraphState.referenced}
39+
source={relationGraphState.source}
40+
target={relationGraphState.target}
4141
/>
4242

4343
{props.children}

src/components/RFS_GlobalModalContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { RelationNode } from "@/lib/RelationNode"
22
import { createContext } from "react"
33

44
export const RFS_GlobalModalContext = createContext<{
5-
openRelationGraph: (base: RelationNode, referenced: RelationNode[]) => void
5+
openRelationGraph: (source: RelationNode[], target: RelationNode[]) => void
66
}>({
77
openRelationGraph: (): void => {
88
throw "GlobalModalProvider not mounted"

src/components/graph/RelationsGraph.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ export function RelationsGraph(props: {
1919
/**
2020
* Source of the relation
2121
*/
22-
base: RelationNode
22+
source: RelationNode[]
2323
/**
2424
* Targets of the relation. Will be connected to the base (source) only
2525
*/
26-
referenced: RelationNode[]
26+
target: RelationNode[]
2727
}) {
2828
const { initialEdges, initialNodes } = useMemo(() => {
29-
return buildGraphForReferences(props.base, props.referenced)
30-
}, [props.base, props.referenced])
29+
return buildGraphForReferences(props.source, props.target)
30+
}, [props.source, props.target])
3131

3232
const [nodes, , onNodesChange] = useNodesState(initialNodes)
3333
const [edges, , onEdgesChange] = useEdgesState(initialEdges)

src/components/graph/RelationsGraphModal.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import { useCallback, useContext } from "react"
88
export function RelationsGraphModal({
99
isOpen,
1010
onOpenChange,
11-
base,
12-
referenced
11+
source,
12+
target
1313
}: {
1414
isOpen: boolean
1515
onOpenChange: (val: boolean) => void
16-
base: RelationNode
17-
referenced: RelationNode[]
16+
source: RelationNode[]
17+
target: RelationNode[]
1818
}) {
1919
const searchContext = useContext(FairDOSearchContext)
2020

@@ -31,8 +31,7 @@ export function RelationsGraphModal({
3131
<DialogContent className="rfs-h-max rfs-max-h-[min(100vh,800px)] rfs-min-h-[500px] rfs-min-w-[500px] !rfs-max-w-[min(calc(100vw-40px),1500px)] rfs-p-0">
3232
<VisuallyHidden.Root>
3333
<DialogTitle>
34-
FDOs related to
35-
{base.label}
34+
Relationship graph between {source.map((s) => s.label).join(", ")} and {target.map((s) => s.label).join(", ")}
3635
</DialogTitle>
3736
</VisuallyHidden.Root>
3837

@@ -43,7 +42,7 @@ export function RelationsGraphModal({
4342
searchForBackground: searchContext.searchForBackground
4443
}}
4544
>
46-
<RelationsGraph base={base} referenced={referenced} />
45+
<RelationsGraph source={source} target={target} />
4746
</FairDOSearchContext.Provider>
4847
</DialogContent>
4948
</Dialog>

src/components/graph/helpers.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import type { RelationNode } from "@/lib/RelationNode"
22

3-
export function buildGraphForReferences(base: RelationNode, _referenced: RelationNode[]) {
4-
const referenced = _referenced.filter((pid) => pid !== base)
5-
const yStart = -((referenced.length - 1) * 100) / 2
6-
const nodes = [{ id: base.id, type: "plain", position: { x: 0, y: 0 }, data: { ...base } }]
3+
export function buildGraphForReferences(source: RelationNode[], _referenced: RelationNode[]) {
4+
const referenced = _referenced.filter((pid) => !source.find((e) => e.id === pid.id))
5+
const yStartSource = -((source.length - 1) * 100) / 2
6+
const yStartReferenced = -((referenced.length - 1) * 100) / 2
7+
const nodes: { id: string; type: string; position: { x: number; y: number }; data: Record<string, unknown> }[] = []
78
const edges: { id: string; source: string; target: string }[] = []
89

10+
for (let i = 0; i < source.length; i++) {
11+
nodes.push({
12+
id: source[i].id,
13+
type: "plain",
14+
position: { x: 0, y: yStartSource + i * 100 },
15+
data: { ...source[i] }
16+
})
17+
}
18+
919
for (let i = 0; i < referenced.length; i++) {
1020
nodes.push({
1121
id: referenced[i].id,
1222
type: "plain",
13-
position: { x: 1000, y: yStart + i * 100 },
23+
position: { x: 1000, y: yStartReferenced + i * 100 },
1424
data: { ...referenced[i] }
1525
})
16-
edges.push({
17-
id: `e-${base.id}-${referenced[i].id}`,
18-
source: base.id,
19-
target: referenced[i].id
20-
})
26+
}
27+
28+
for (let x = 0; x < source.length; x++) {
29+
for (let y = 0; y < referenced.length; y++) {
30+
edges.push({
31+
id: `e-${source[x].id}-${referenced[y].id}`,
32+
source: source[x].id,
33+
target: referenced[y].id
34+
})
35+
}
2136
}
2237

2338
return { initialNodes: nodes, initialEdges: edges }

src/components/result/GenericResultView.tsx

Lines changed: 93 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { RFS_GlobalModalContext } from "@/components/RFS_GlobalModalContext"
33
import { FairDOSearchContext } from "@/components/FairDOSearchContext"
44
import { useStore } from "zustand/index"
55
import { resultCache } from "@/lib/ResultCache"
6-
import { autoUnwrap, autoUnwrapArray } from "@/components/result/utils"
6+
import { autoUnwrap, autoUnwrapArray, toArray } from "@/components/result/utils"
77
import { DateTime } from "luxon"
88
import { BasicRelationNode } from "@/lib/RelationNode"
99
import { BookText, ChevronDown, GitFork, ImageOff, LinkIcon, Microscope } from "lucide-react"
@@ -64,9 +64,9 @@ export interface GenericResultViewProps {
6464
relatedItemPidsField?: string
6565

6666
/**
67-
* Options for prefetching of related items in the relations graph. It is recommended to defined this if the default settings don't work properly.
67+
* Options for prefetching of related items in the relations graph. It is recommended to define this if the default settings don't work properly.
6868
*/
69-
relatedItemsPrefetch?: { prefetchAmount?: number; searchFields?: Record<string, SearchFieldConfiguration> }
69+
relatedItemsPrefetch?: { searchFields?: Record<string, SearchFieldConfiguration> }
7070

7171
/**
7272
* The elastic field where the unique identifier of the parent item (metadata item) of the current FDO will be read from. Will be accessible via a `Find Metadata` button
@@ -111,7 +111,7 @@ export function GenericResultView({
111111
parentItemPidField = "hasMetadata",
112112
creationDateField = "creationDate",
113113
additionalIdentifierField = "identifier",
114-
relatedItemsPrefetch = { prefetchAmount: 20, searchFields: { pid: {} } },
114+
relatedItemsPrefetch = { searchFields: { pid: {} } },
115115
tags = [],
116116
showOpenInFairDoScope = true
117117
}: GenericResultViewProps) {
@@ -207,8 +207,9 @@ export function GenericResultView({
207207
}, [getField, additionalIdentifierField])
208208

209209
const isMetadataFor = useMemo(() => {
210-
return getArrayField(relatedItemPidsField ?? "isMetadataFor")
211-
}, [getArrayField, relatedItemPidsField])
210+
const val = getArrayOrSingleField(relatedItemPidsField ?? "isMetadataFor")
211+
return val ? toArray(val) : undefined
212+
}, [getArrayOrSingleField, relatedItemPidsField])
212213

213214
const creationDate = useMemo(() => {
214215
const value = getField(creationDateField ?? "dateCreated")
@@ -218,54 +219,85 @@ export function GenericResultView({
218219
}, [creationDateField, getField])
219220

220221
const hasMetadata = useMemo(() => {
221-
return getField(parentItemPidField ?? "hasMetadata")
222-
}, [getField, parentItemPidField])
223-
224-
const fetchRelatedItems = useCallback(async () => {
225-
const search = await elasticConnector?.onSearch(
226-
{ searchTerm: pid, resultsPerPage: relatedItemsPrefetch?.prefetchAmount },
227-
{
228-
result_fields: {},
229-
searchTerm: pid,
230-
search_fields: relatedItemsPrefetch?.searchFields ?? { [pidField ?? "pid"]: {} },
231-
resultsPerPage: relatedItemsPrefetch?.prefetchAmount
222+
const val = getArrayOrSingleField(parentItemPidField ?? "hasMetadata")
223+
return val ? toArray(val) : undefined
224+
}, [getArrayOrSingleField, parentItemPidField])
225+
226+
const fetchRelatedItems = useCallback(
227+
async (term: string, amount: number) => {
228+
const search = await elasticConnector?.onSearch(
229+
{ searchTerm: term, resultsPerPage: amount },
230+
{
231+
result_fields: {},
232+
searchTerm: term,
233+
search_fields: relatedItemsPrefetch?.searchFields ?? { [pidField ?? "pid"]: {} },
234+
resultsPerPage: amount
235+
}
236+
)
237+
238+
if (search) {
239+
for (const entry of search.results) {
240+
const pid = autoUnwrap(entry[pidField ?? "pid"])
241+
if (!pid) continue
242+
addToResultCache(pid, {
243+
pid,
244+
name: autoUnwrap(entry[titleField ?? "name"]) ?? ""
245+
})
246+
}
232247
}
233-
)
234-
235-
if (search) {
236-
for (const entry of search.results) {
237-
const pid = autoUnwrap(entry[pidField ?? "pid"])
238-
if (!pid) continue
239-
addToResultCache(pid, {
240-
pid,
241-
name: autoUnwrap(entry[titleField ?? "name"]) ?? ""
248+
},
249+
[addToResultCache, elasticConnector, pidField, relatedItemsPrefetch?.searchFields, titleField]
250+
)
251+
252+
const showRelatedItemsGraph = useCallback(async () => {
253+
if (!isMetadataFor || !pid) return
254+
await fetchRelatedItems(pid, isMetadataFor.length)
255+
256+
if (isMetadataFor.length === 1) {
257+
searchFor(isMetadataFor[0])
258+
} else {
259+
openRelationGraph(
260+
[
261+
{
262+
id: pid ?? "source",
263+
label: title ?? "Source",
264+
tag: "Current",
265+
remoteURL: doLocation,
266+
searchQuery: pid
267+
}
268+
],
269+
isMetadataFor.map((pid) => {
270+
const cached = getResultFromCache(pid)
271+
return new BasicRelationNode(pid, "Related", cached?.name)
242272
})
243-
}
273+
)
244274
}
245-
}, [addToResultCache, elasticConnector, pid, pidField, relatedItemsPrefetch?.prefetchAmount, relatedItemsPrefetch?.searchFields, titleField])
246-
247-
const showRelatedItems = useCallback(async () => {
248-
await fetchRelatedItems()
249-
250-
openRelationGraph(
251-
{
252-
id: pid ?? "source",
253-
label: title ?? "Source",
254-
tag: "Current",
255-
remoteURL: doLocation,
256-
searchQuery: pid
257-
},
258-
isMetadataFor.map((pid) => {
259-
const cached = getResultFromCache(pid)
260-
return new BasicRelationNode(pid, "Related", cached?.name)
261-
})
262-
)
263-
}, [doLocation, fetchRelatedItems, getResultFromCache, isMetadataFor, openRelationGraph, pid, title])
275+
}, [doLocation, fetchRelatedItems, getResultFromCache, isMetadataFor, openRelationGraph, pid, searchFor, title])
264276

265-
const goToMetadata = useCallback(() => {
277+
const showHasMetadataGraph = useCallback(async () => {
266278
if (!hasMetadata) return
267-
searchFor(hasMetadata)
268-
}, [hasMetadata, searchFor])
279+
await fetchRelatedItems(hasMetadata.join(" "), hasMetadata.length)
280+
281+
if (hasMetadata.length === 1) {
282+
searchFor(hasMetadata[0])
283+
} else {
284+
openRelationGraph(
285+
hasMetadata.map((pid) => {
286+
const cached = getResultFromCache(pid)
287+
return new BasicRelationNode(pid, "Metadata", cached?.name)
288+
}),
289+
[
290+
{
291+
id: pid ?? "current",
292+
label: title ?? "Current",
293+
tag: "Current",
294+
remoteURL: doLocation,
295+
searchQuery: pid
296+
}
297+
]
298+
)
299+
}
300+
}, [doLocation, fetchRelatedItems, getResultFromCache, hasMetadata, openRelationGraph, pid, searchFor, title])
269301

270302
const exactPidMatch = useMemo(() => {
271303
return searchTerm === pid || searchTerm === doLocation
@@ -282,10 +314,10 @@ export function GenericResultView({
282314

283315
return (
284316
<div
285-
className={`rfs-m-2 rfs-rounded-lg rfs-border rfs-border-border rfs-p-4 rfs-group/resultView ${exactPidMatch ? "rfs-animate-outline-ping" : ""}`}
317+
className={`rfs-m-2 rfs-rounded-lg rfs-border rfs-border-border rfs-p-4 rfs-group/resultView ${exactPidMatch ? "rfs-animate-rfs-outline-ping" : ""}`}
286318
>
287319
<div
288-
className={`rfs-grid ${imageField ? "rfs-grid-rows-[100px_1fr] md:rfs-grid-cols-[200px_1fr] md:rfs-grid-rows-1" : ""} rfs-gap-4 rfs-overflow-x-auto md:rfs-max-w-full`}
320+
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`}
289321
>
290322
{imageField && (
291323
<div
@@ -295,7 +327,11 @@ export function GenericResultView({
295327
Array.isArray(previewImage) ? (
296328
<GenericResultViewImageCarousel images={previewImage} title={title} />
297329
) : (
298-
<img className="md:rfs-size-[200px]" src={previewImage} alt={`Preview for ${title}`} />
330+
<img
331+
className="md:rfs-max-h-[200px] md:rfs-max-w-[200px] rfs-object-contain"
332+
src={previewImage}
333+
alt={`Preview for ${title}`}
334+
/>
299335
)
300336
) : (
301337
<div className="rfs-flex rfs-flex-col rfs-justify-center dark:rfs-text-background">
@@ -324,30 +360,30 @@ export function GenericResultView({
324360
</div>
325361
<div className="rfs-grow">{description}</div>
326362
<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">
327-
{isMetadataFor.length > 0 && (
363+
{isMetadataFor && isMetadataFor.length > 0 && (
328364
<div className="rfs-flex rfs-items-center">
329-
<Button className="rfs-grow rfs-rounded-r-none" size="sm" variant="secondary" onClick={showRelatedItems}>
365+
<Button className="rfs-grow rfs-rounded-r-none" size="sm" variant="secondary" onClick={showRelatedItemsGraph}>
330366
<GitFork className="rfs-mr-1 rfs-size-4" /> Show Related Items
331367
</Button>
332368
<Button
333369
className="rfs-rounded-l-none rfs-border-l rfs-border-l-border rfs-text-xs rfs-font-bold"
334370
size="sm"
335371
variant="secondary"
336-
onClick={showRelatedItems}
372+
onClick={showRelatedItemsGraph}
337373
>
338374
{isMetadataFor.length}
339375
</Button>
340376
</div>
341377
)}
342378
{hasMetadata && (
343-
<Button className="" size="sm" variant="secondary" onClick={goToMetadata}>
379+
<Button className="" size="sm" variant="secondary" onClick={showHasMetadataGraph}>
344380
<BookText className="rfs-mr-1 rfs-size-4" /> Find Metadata
345381
</Button>
346382
)}
347383

348384
{landingPageLocation && (
349385
<div className="rfs-flex rfs-items-center">
350-
<a href={landingPageLocation} target="_blank" className="grow">
386+
<a href={landingPageLocation} target="_blank" className="rfs-grow">
351387
<Button size="sm" className="rfs-w-full rfs-rounded-r-none rfs-px-4">
352388
Open
353389
</Button>

src/components/result/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ export function autoUnwrapArray(item?: string[] | { raw?: string[] }) {
2222
return [JSON.stringify(item)]
2323
}
2424
}
25+
26+
export function toArray<E>(element: E | E[]) {
27+
if (Array.isArray(element)) return element
28+
else return [element]
29+
}

0 commit comments

Comments
 (0)