Skip to content

Commit 71ed5db

Browse files
committed
Refactor relations modal context and allow specifying custom node types
1 parent da65504 commit 71ed5db

11 files changed

+199
-86
lines changed

src/components/FairDOElasticSearch.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { FairDOConfig } from "@/config/FairDOConfig"
44
import type { SearchContextState } from "@elastic/search-ui"
55
import { FairDOSearchProvider } from "@/components/FairDOSearchProvider"
6-
import { GlobalModalProvider } from "@/components/GlobalModalProvider"
6+
import { RelationsGraphProvider } from "@/components/graph/RelationsGraphProvider"
77
import { ClearFilters } from "@/components/search/ClearFilters"
88
import { DefaultFacet, OptionViewProps } from "@/components/search/DefaultFacet"
99
import { DefaultSearchBox } from "@/components/search/DefaultSearchBox"
@@ -20,6 +20,7 @@ import { TooltipProvider } from "./ui/tooltip"
2020
import { useAutoDarkMode } from "@/components/utils"
2121
import { PlaceholderResultView } from "@/components/result/PlaceholderResultView"
2222
import { DefaultSorting } from "@/components/search/DefaultSorting"
23+
import { NodeTypes } from "@xyflow/react"
2324

2425
/**
2526
* All-in-one component for rendering an elastic search UI based on the provided configuration. Includes
@@ -32,21 +33,41 @@ import { DefaultSorting } from "@/components/search/DefaultSorting"
3233
*
3334
* #### 🖌️ Customization
3435
* You can customize the default behaviour by overriding the default result view (resultView) or the views of the facet
35-
* options (facetOptionView)
36+
* options (facetOptionView).
37+
*
38+
* You can also specify your own graph nodes to dynamically render any relationships between objects. ([Package Docs](https://reactflow.dev/learn/customization/custom-nodes))
3639
*/
3740
export function FairDOElasticSearch({
3841
config: rawConfig,
3942
resultView,
4043
facetOptionView,
41-
dark
44+
dark,
45+
graphNodeTypes
4246
}: {
4347
/**
4448
* Make sure the config is either memoized or constant (defined outside any components)
4549
*/
4650
config: FairDOConfig
51+
52+
/**
53+
* React Component that will be used to render the results from the current search. Consider using the `GenericResultView`
54+
*/
4755
resultView: ComponentType<ResultViewProps>
56+
57+
/**
58+
* React Component that will be used to render the individual options (text right of the checkboxes) in a facet.
59+
*/
4860
facetOptionView?: ComponentType<OptionViewProps>
4961

62+
/**
63+
* Specify additional node types to render in the relations graph. Optional. The "result" node type is present by default
64+
* and can be overwritten here.
65+
* > **⚠ Important**: Make sure to memoize the object passed to this prop, or pass a constant object.
66+
*
67+
* Consult the [React Flow Documentation](https://reactflow.dev/learn/customization/custom-nodes) on how to specify nodes. **Make sure your node has one `target` and one `source` Handle.**
68+
*/
69+
graphNodeTypes?: NodeTypes
70+
5071
/**
5172
* Set to true to enable dark mode
5273
*/
@@ -74,7 +95,7 @@ export function FairDOElasticSearch({
7495
<SearchProvider config={elasticConfig}>
7596
<FairDOSearchProvider config={rawConfig}>
7697
<TooltipProvider>
77-
<GlobalModalProvider resultView={actualResultView} dark={dark}>
98+
<RelationsGraphProvider resultView={actualResultView} dark={dark} nodeTypes={graphNodeTypes}>
7899
<WithSearch
79100
mapContextToProps={({ wasSearched, isLoading }: SearchContextState) => ({
80101
wasSearched,
@@ -181,7 +202,7 @@ export function FairDOElasticSearch({
181202
)
182203
}}
183204
</WithSearch>
184-
</GlobalModalProvider>
205+
</RelationsGraphProvider>
185206
</TooltipProvider>
186207
</FairDOSearchProvider>
187208
</SearchProvider>

src/components/GlobalModalProvider.tsx

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/components/RFS_GlobalModalContext.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/components/graph/GraphNodeUtils.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
import { GraphNode } from "@/components/graph/GraphNode"
22
import { toArray } from "@/components/result"
33

4+
/**
5+
* Utilities for working with the RelationsGraph
6+
*/
47
export class GraphNodeUtils {
5-
static buildNodesSequential(type: string, ...ids: (string | string[])[]): GraphNode[] {
8+
/**
9+
* Build a sequential graph (n:n:n:...) by passing just the identifiers of the nodes in layers.
10+
* @param type Type of the nodes (use "result" to display your resultView)
11+
* @param ids Each entry resembled one layer in the sequential graph
12+
* @example
13+
* buildSequentialGraphFromIds("result", "a", ["b", "c", "d"], "e")
14+
* // result:
15+
* // b
16+
* // / \
17+
* // a - c - e
18+
* // \ /
19+
* // d
20+
*/
21+
static buildSequentialGraphFromIds(type: string, ...ids: (string | string[])[]): GraphNode[] {
622
const nodes: GraphNode[] = []
723

824
for (let layer = 0; layer < ids.length; layer++) {

src/components/graph/RelationsGraph.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Controls,
77
MiniMap,
88
NodeProps,
9+
NodeTypes,
910
ReactFlow,
1011
useEdgesState,
1112
useNodesInitialized,
@@ -28,6 +29,7 @@ export function RelationsGraph(props: {
2829
options?: RelationsGraphOptions
2930
resultView: ComponentType<ResultViewProps>
3031
dark?: boolean
32+
nodeTypes?: NodeTypes
3133
}) {
3234
const [colorMode, setColorMode] = useState<ColorMode>(props.dark ? "dark" : "light")
3335

@@ -43,9 +45,10 @@ export function RelationsGraph(props: {
4345

4446
const nodeTypes = useMemo(() => {
4547
return {
46-
result: (nodeProps: NodeProps) => <ResultViewWrapper {...nodeProps} resultView={props.resultView} />
48+
result: (nodeProps: NodeProps) => <ResultViewWrapper {...nodeProps} resultView={props.resultView} />,
49+
...props.nodeTypes
4750
}
48-
}, [props.resultView])
51+
}, [props.nodeTypes, props.resultView])
4952

5053
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
5154
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createContext } from "react"
2+
import { GraphNode } from "@/components/graph/GraphNode"
3+
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
4+
5+
export const RelationsGraphContext = createContext<{
6+
/**
7+
* Open the relations graph modal with the specified nodes and options. If the graph is already open, the current graph is replaced
8+
* with the specified nodes and options.
9+
* @param nodes Nodes to display in the Graph. Consider using GraphNodeUtils for constructing the nodes.
10+
* @param options Options to further configure the graph
11+
*/
12+
openRelationsGraph: (nodes: GraphNode[], options?: RelationsGraphOptions) => void
13+
14+
/**
15+
* Open the relations graph modal with the specified nodes and options. If the graph is already open, the current graph is extended
16+
* with the specified nodes. Iff options are specified, they will override the options of the currently open graph.
17+
* @param nodes Nodes to display in the Graph or add to the already open Graph. Consider using GraphNodeUtils for constructing the nodes.
18+
* @param options Options to further configure the Graph.
19+
*/
20+
openOrAddToRelationsGraph: (nodes: GraphNode[], options?: RelationsGraphOptions) => void
21+
22+
/**
23+
* Close the relations graph modal
24+
*/
25+
closeRelationsGraph: () => void
26+
}>({
27+
openRelationsGraph: (): void => {
28+
throw "RelationsGraphProvider not mounted"
29+
},
30+
openOrAddToRelationsGraph: () => {
31+
throw "RelationsGraphProvider not mounted"
32+
},
33+
closeRelationsGraph: (): void => {
34+
throw "RelationsGraphProvider not mounted"
35+
}
36+
})

src/components/graph/RelationsGraphModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ import { GraphNode } from "@/components/graph/GraphNode"
88
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
99
import { Button } from "@/components/ui/button"
1010
import { X } from "lucide-react"
11+
import { NodeTypes } from "@xyflow/react"
1112

1213
export function RelationsGraphModal({
1314
isOpen,
1415
onOpenChange,
1516
nodes,
1617
resultView,
1718
options,
18-
dark
19+
dark,
20+
nodeTypes
1921
}: {
2022
isOpen: boolean
2123
onOpenChange: (val: boolean) => void
2224
nodes: GraphNode[]
2325
options?: RelationsGraphOptions
2426
resultView: ComponentType<ResultViewProps>
2527
dark?: boolean
28+
nodeTypes?: NodeTypes
2629
}) {
2730
const searchContext = useContext(FairDOSearchContext)
2831

@@ -50,7 +53,7 @@ export function RelationsGraphModal({
5053
config: searchContext.config
5154
}}
5255
>
53-
<RelationsGraph nodes={nodes} resultView={resultView} options={options} dark={dark} />
56+
<RelationsGraph nodes={nodes} resultView={resultView} options={options} dark={dark} nodeTypes={nodeTypes} />
5457
</FairDOSearchContext.Provider>
5558

5659
<div className="rfs-absolute rfs-right-4 rfs-top-4">
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client"
2+
3+
import { ComponentType, PropsWithChildren, useCallback, useState } from "react"
4+
import { RelationsGraphModal } from "@/components/graph/RelationsGraphModal"
5+
import { NodeTypes, ReactFlowProvider } from "@xyflow/react"
6+
import { RelationsGraphContext } from "./RelationsGraphContext"
7+
import { ResultViewProps } from "@elastic/react-search-ui-views"
8+
import { GraphNode } from "@/components/graph/GraphNode"
9+
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
10+
11+
export function RelationsGraphProvider(
12+
props: PropsWithChildren<{ resultView: ComponentType<ResultViewProps>; dark?: boolean; nodeTypes?: NodeTypes }>
13+
) {
14+
const [state, setState] = useState<{
15+
nodes: GraphNode[]
16+
options: RelationsGraphOptions
17+
isOpen: boolean
18+
}>({
19+
nodes: [],
20+
options: {},
21+
isOpen: false
22+
})
23+
24+
const openRelationsGraph = useCallback((nodes: GraphNode[], options?: RelationsGraphOptions) => {
25+
setState({
26+
nodes,
27+
isOpen: true,
28+
options: options ?? {}
29+
})
30+
}, [])
31+
32+
const openOrAddToRelationsGraph = useCallback((nodes: GraphNode[], options?: RelationsGraphOptions) => {
33+
setState((prev) => ({
34+
nodes: prev.nodes
35+
.concat(nodes)
36+
.reduce<GraphNode[]>((acc, node) => (acc.find((inner) => inner.id === node.id) ? acc : acc.concat(node)), []),
37+
isOpen: true,
38+
options: options ?? prev.options ?? {}
39+
}))
40+
}, [])
41+
42+
const onRelationsGraphOpenChange = useCallback((isOpen: boolean) => {
43+
setState((prev) => ({ ...prev, isOpen }))
44+
}, [])
45+
46+
const closeRelationsGraph = useCallback(() => {
47+
onRelationsGraphOpenChange(false)
48+
}, [onRelationsGraphOpenChange])
49+
50+
return (
51+
<RelationsGraphContext.Provider value={{ openRelationsGraph, closeRelationsGraph, openOrAddToRelationsGraph }}>
52+
<ReactFlowProvider>
53+
<RelationsGraphModal
54+
nodes={state.nodes}
55+
isOpen={state.isOpen}
56+
onOpenChange={onRelationsGraphOpenChange}
57+
resultView={props.resultView}
58+
options={state.options}
59+
dark={props.dark}
60+
nodeTypes={props.nodeTypes}
61+
/>
62+
63+
{props.children}
64+
</ReactFlowProvider>
65+
</RelationsGraphContext.Provider>
66+
)
67+
}

src/components/result/GenericResultView.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useContext, useEffect, useMemo, useState } from "react"
2-
import { RFS_GlobalModalContext } from "@/components/RFS_GlobalModalContext"
2+
import { RelationsGraphContext } from "@/components/graph/RelationsGraphContext"
33
import { FairDOSearchContext } from "@/components/FairDOSearchContext"
44
import { useStore } from "zustand/index"
55
import { resultCache } from "@/lib/ResultCache"
@@ -132,7 +132,7 @@ export function GenericResultView({
132132
showOpenInFairDoScope = true,
133133
showInspectFDO = true
134134
}: GenericResultViewProps) {
135-
const { openRelationGraph } = useContext(RFS_GlobalModalContext)
135+
const { openRelationsGraph } = useContext(RelationsGraphContext)
136136
const { searchTerm, elasticConnector, searchFor, config } = useContext(FairDOSearchContext)
137137
const addToResultCache = useStore(resultCache, (s) => s.set)
138138
const [loadingRelatedItems, setLoadingRelatedItems] = useState(false)
@@ -299,11 +299,11 @@ export function GenericResultView({
299299
if (hasMetadata) await fetchRelatedItems(hasMetadata.join(" "), hasMetadata.length)
300300
setLoadingRelatedItems(false)
301301

302-
const nodes = GraphNodeUtils.buildNodesSequential("result", hasMetadata ?? [], pid, isMetadataFor ?? [])
303-
openRelationGraph(nodes, {
302+
const nodes = GraphNodeUtils.buildSequentialGraphFromIds("result", hasMetadata ?? [], pid, isMetadataFor ?? [])
303+
openRelationsGraph(nodes, {
304304
focusedNodes: [pid]
305305
})
306-
}, [fetchRelatedItems, hasMetadata, isMetadataFor, openRelationGraph, pid])
306+
}, [fetchRelatedItems, hasMetadata, isMetadataFor, openRelationsGraph, pid])
307307

308308
const showRelatedItemsButton = useMemo(() => {
309309
return (hasMetadata && hasMetadata.length > 0) || (isMetadataFor && isMetadataFor.length > 0)

0 commit comments

Comments
 (0)