Skip to content

Commit 5d8bebd

Browse files
committed
Updated and introduced new FHIRViewer
1 parent 53fc7c1 commit 5d8bebd

File tree

2 files changed

+183
-92
lines changed

2 files changed

+183
-92
lines changed
Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
---
2-
32
// Props passed from MDX
4-
const {
5-
definition,
6-
src,
7-
filePath,
8-
id = "fhir-viewer",
9-
title
10-
} = Astro.props;
3+
const { definition, src, filePath, id = 'fhir-viewer', title } = Astro.props;
114
import fs from 'node:fs/promises';
125
import { existsSync } from 'fs';
136
import FHIRSchemaViewer from '@components/SchemaExplorer/FHIRSchemaViewer'; // <-- path to your React component
14-
import {resolveProjectPath, getAbsoluteFilePathForAstroFile } from '@utils/files';
7+
import { resolveProjectPath, getAbsoluteFilePathForAstroFile } from '@utils/files';
158
169
let schema;
1710
@@ -26,14 +19,11 @@ try {
2619
}
2720
}
2821
} catch (error) {
29-
console.log('Failed to process schemas');
30-
console.log(error);
22+
console.log('Failed to process schemas');
23+
console.log(error);
3124
}
3225
---
3326

3427
<div class="my-4">
35-
<FHIRSchemaViewer
36-
structureDefinition={schema}
37-
title={title}
38-
/>
28+
<FHIRSchemaViewer structureDefinition={schema} title={title} client:only="react" />
3929
</div>
Lines changed: 178 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,257 @@
1-
import React, { useMemo } from "react";
1+
import React, { useMemo } from 'react';
22

3-
import $RefParser from '@apidevtools/json-schema-ref-parser';
4-
import pkg from 'fhir-react';
5-
const {FhirResource, fhirVersions} = pkg;
63
interface FHIRSchemaViewerProps {
74
structureDefinition: any;
85
title?: string;
96
}
107

118
interface FhirElementNode {
9+
key: string;
1210
id: string;
1311
path: string;
1412
name: string;
1513
min: number;
1614
max: string;
17-
types: string []
15+
types: string[];
1816
sliceName?: string;
17+
18+
short?: string;
19+
definition?: string;
20+
comment?: string;
21+
alias?: string[];
22+
binding?: {
23+
strength?: string;
24+
valueSet?: string;
25+
};
26+
mapping?: {
27+
identity: string;
28+
map: string;
29+
}[];
30+
1931
children: FhirElementNode[];
2032
}
21-
2233
/**
2334
* Take snapshot.element[] and transform the flat list
2435
* into a nested tree based on the element paths.
2536
*/
2637
function buildElementTree(elements: any[]): FhirElementNode[] {
27-
const nodeMap = new Map<string, FhirElementNode>();
2838
const roots: FhirElementNode[] = [];
29-
console.log(elements.length);
30-
// First pass: build nodes
31-
elements.forEach((el: any) => {
32-
const path = el.path;
33-
const parts = path.split(".");
34-
const name = el.sliceName ?? parts[parts.length - 1];
39+
const stack: { depth: number; node: FhirElementNode }[] = [];
40+
41+
elements.forEach((el: any, index: number) => {
42+
const depth = el.path.split('.').length;
43+
3544
const node: FhirElementNode = {
36-
id: el.id, // FHIR ID (with slice names)
37-
path, // hierarchical path
38-
name, // display name
45+
key: `${index}`,
46+
id: el.id,
47+
path: el.path,
48+
name: el.name,
3949
min: el.min ?? 0,
40-
max: el.max ?? "1",
41-
types: Array.isArray(el.type)
42-
? el.type.map((t: any) => t.code)
43-
: [],
50+
max: el.max ?? '1',
51+
types: Array.isArray(el.type) ? el.type.map((t: any) => t.code) : [],
4452
sliceName: el.sliceName,
45-
children: [],
46-
};
4753

48-
nodeMap.set(path, node);
49-
});
54+
short: el.short,
55+
definition: el.definition,
56+
comment: el.comment,
57+
alias: el.alias,
58+
binding: el.binding
59+
? {
60+
strength: el.binding.strength,
61+
valueSet: el.binding.valueSet,
62+
}
63+
: undefined,
64+
mapping: el.mapping,
5065

51-
// Second pass: connect nodes into a tree
52-
elements.forEach((el: any) => {
53-
const path = el.path;
54-
const parts = path.split(".");
55-
const parentPath = parts.slice(0, -1).join(".");
66+
children: [],
67+
};
5668

57-
const node = nodeMap.get(path);
69+
// Pop stack until we find the correct parent depth
70+
while (stack.length && stack[stack.length - 1].depth >= depth) {
71+
stack.pop();
72+
}
5873

59-
if (parentPath && nodeMap.has(parentPath)) {
60-
nodeMap.get(parentPath)!.children.push(node!);
74+
if (stack.length === 0) {
75+
roots.push(node);
6176
} else {
62-
// no parent => top-level root
63-
roots.push(node!);
77+
stack[stack.length - 1].node.children.push(node);
6478
}
79+
80+
stack.push({ depth, node });
6581
});
6682

6783
return roots;
6884
}
69-
7085
/**
7186
* Simple recursive UI renderer for FHIR element tree.
7287
*/
73-
import { useState } from "react";
88+
import { useState } from 'react';
89+
90+
function ElementDetails({ node }: { node: FhirElementNode }) {
91+
return (
92+
<div
93+
style={{
94+
marginTop: 6,
95+
padding: '10px 14px',
96+
background: '#f9fafb',
97+
borderLeft: '3px solid #3b82f6',
98+
fontSize: '0.85em',
99+
borderRadius: 4,
100+
}}
101+
>
102+
{node.short && (
103+
<div>
104+
<strong>Short description</strong>
105+
<br />
106+
{node.short}
107+
</div>
108+
)}
109+
110+
{node.alias?.length && (
111+
<div style={{ marginTop: 6 }}>
112+
<strong>Alternate names</strong>
113+
<br />
114+
{node.alias.join(', ')}
115+
</div>
116+
)}
117+
118+
{node.definition && (
119+
<div style={{ marginTop: 6 }}>
120+
<strong>Definition</strong>
121+
<br />
122+
{node.definition}
123+
</div>
124+
)}
125+
126+
{node.comment && (
127+
<div style={{ marginTop: 6 }}>
128+
<strong>Comments</strong>
129+
<br />
130+
{node.comment}
131+
</div>
132+
)}
133+
134+
{node.binding && (
135+
<div style={{ marginTop: 6 }}>
136+
<strong>Binding</strong>
137+
<br />
138+
{node.binding.strength}{' '}
139+
<a href={node.binding.valueSet} target="_blank" rel="noreferrer">
140+
{node.binding.valueSet}
141+
</a>
142+
</div>
143+
)}
144+
145+
{node.mapping?.length && (
146+
<div style={{ marginTop: 6 }}>
147+
<strong>Mappings</strong>
148+
<ul style={{ marginLeft: 16 }}>
149+
{node.mapping.map((m, i) => (
150+
<li key={i}>
151+
<strong>{m.identity}:</strong> {m.map}
152+
</li>
153+
))}
154+
</ul>
155+
</div>
156+
)}
157+
</div>
158+
);
159+
}
74160

75161
function ElementNodeView({ node, depth }: { node: FhirElementNode; depth: number }) {
76-
const [expanded, setExpanded] = useState(depth === 0); // root expanded by default
162+
const [expanded, setExpanded] = useState(depth === 0);
163+
const [selected, setSelected] = useState(false);
77164

78-
const hasChildren = node.children && node.children.length > 0;
165+
const hasChildren = node.children.length > 0;
79166

80167
return (
81168
<div style={{ marginLeft: depth * 18 }}>
82169
<div
83170
style={{
84-
display: "flex",
85-
alignItems: "center",
86-
cursor: hasChildren ? "pointer" : "default",
87-
lineHeight: "1.5em",
171+
display: 'flex',
172+
alignItems: 'center',
173+
cursor: 'pointer',
174+
lineHeight: '1.6em',
175+
padding: '2px 4px',
176+
borderRadius: 4,
177+
}}
178+
onMouseEnter={(e) => {
179+
e.currentTarget.style.background = '#f3f4f6';
180+
}}
181+
onMouseLeave={(e) => {
182+
e.currentTarget.style.background = 'transparent';
183+
}}
184+
onClick={(e) => {
185+
e.stopPropagation(); // 🔑 critical
186+
setSelected(!selected);
88187
}}
89-
onClick={() => hasChildren && setExpanded(!expanded)}
90188
>
91-
{/* Expand / collapse triangle */}
92189
{hasChildren ? (
93-
<span style={{ width: 14, display: "inline-block", fontSize: 12 }}>
94-
{expanded ? "▼" : "▶"}
190+
<span
191+
onClick={(e) => {
192+
e.stopPropagation();
193+
setExpanded(!expanded);
194+
}}
195+
style={{ width: 14, display: 'inline-block', fontSize: 12 }}
196+
>
197+
{expanded ? '▼' : '▶'}
95198
</span>
96199
) : (
97-
<span style={{ width: 14, display: "inline-block" }} />
200+
<span style={{ width: 14 }} />
98201
)}
99202

100-
{/* Element path */}
101-
<span style={{ fontWeight: depth === 0 ? "bold" : "normal" }}>
102-
{node.path}
103-
</span>
203+
<span style={{ fontWeight: depth === 0 ? 'bold' : 'normal' }}>{node.path}</span>
104204

105-
{/* Min/max */}
106205
<span style={{ opacity: 0.6, marginLeft: 6 }}>
107206
({node.min}..{node.max})
108207
</span>
109208

110-
{/* Type info */}
111-
{(node.types?.length ?? 0) > 0 && (
112-
<span style={{ marginLeft: 8, fontSize: "0.9em", opacity: 0.8 }}>
113-
: {node.types!.join(" | ")}
209+
{node.types.length > 0 && (
210+
<span style={{ marginLeft: 8 }}>
211+
:
212+
{node.types.map((t, i) => (
213+
<a
214+
key={t}
215+
href={`/fhir/datatypes/${t}`}
216+
style={{
217+
marginLeft: 4,
218+
color: '#2563eb',
219+
textDecoration: 'none',
220+
fontSize: '0.9em',
221+
}}
222+
>
223+
{t}
224+
{i < node.types.length - 1 ? ' |' : ''}
225+
</a>
226+
))}
114227
</span>
115228
)}
116229
</div>
117230

118-
{/* Child nodes */}
119-
{expanded &&
120-
hasChildren &&
121-
node.children.map((child) => (
122-
<ElementNodeView key={child.path} node={child} depth={depth + 1} />
123-
))}
231+
{selected && <ElementDetails node={node} />}
232+
233+
{expanded && node.children.map((child) => <ElementNodeView key={child.key} node={child} depth={depth + 1} />)}
124234
</div>
125235
);
126236
}
127237
/**
128238
* Main viewer component.
129239
*/
130240
export default function FHIRSchemaViewer({ structureDefinition, title }: FHIRSchemaViewerProps) {
131-
132-
133241
if (!structureDefinition) {
134242
return <div>No StructureDefinition provided.</div>;
135243
}
136244

137245
return (
138-
<div style={{ padding: "1rem", fontFamily: "Arial" }}>
139-
<h3 style={{ marginBottom: "0.5rem" }}>
140-
{title ?? structureDefinition.title ?? structureDefinition.name}
141-
</h3>
246+
<div style={{ padding: '1rem', fontFamily: 'Arial' }}>
247+
<h3 style={{ marginBottom: '0.5rem' }}>{title ?? structureDefinition.title ?? structureDefinition.name}</h3>
142248

143-
<div style={{ fontSize: "0.9rem", color: "#555", marginBottom: "1rem" }}>
144-
{structureDefinition.description}
145-
</div>
249+
<div style={{ fontSize: '0.9rem', color: '#555', marginBottom: '1rem' }}>{structureDefinition.description}</div>
146250

147251
{/* Render root elements */}
148-
149-
<FhirResource
150-
fhirResource={structureDefinition}
151-
fhirVersion={fhirVersions.R4}
152-
/>
153-
252+
{buildElementTree(structureDefinition.snapshot.element).map((rootNode) => (
253+
<ElementNodeView key={rootNode.path} node={rootNode} depth={0} />
254+
))}
154255
</div>
155256
);
156257
}

0 commit comments

Comments
 (0)