Skip to content

Commit 4912d84

Browse files
committedMar 25, 2025·
feat: dicom mvp
1 parent 4500893 commit 4912d84

File tree

7 files changed

+305
-710
lines changed

7 files changed

+305
-710
lines changed
 

‎src/components/NewLibrary/components/FileCard.tsx

+1-8
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,7 @@ export const SubFileCard: React.FC<SubFileCardProps> = ({
319319
isOpen={preview}
320320
onClose={() => setIsPreview(false)}
321321
>
322-
<FileDetailView
323-
selectedFile={file}
324-
preview="large"
325-
list={list}
326-
fetchMore={fetchMore}
327-
handlePagination={handlePagination}
328-
filesLoading={filesLoading}
329-
/>
322+
<FileDetailView selectedFile={file} preview="large" />
330323
</Modal>
331324
</>
332325
);

‎src/components/NewLibrary/components/LibraryTable.tsx

+1-9
Original file line numberDiff line numberDiff line change
@@ -346,15 +346,7 @@ const LibraryTable: React.FC<TableProps> = ({
346346
placement="right"
347347
>
348348
{selectedFile && (
349-
<FileDetailView
350-
gallery={true}
351-
selectedFile={selectedFile}
352-
preview="large"
353-
list={data.files}
354-
fetchMore={fetchMore}
355-
handlePagination={handlePagination}
356-
filesLoading={filesLoading}
357-
/>
349+
<FileDetailView selectedFile={selectedFile} preview="large" />
358350
)}
359351
</Drawer>
360352
<Table
+27-326
Original file line numberDiff line numberDiff line change
@@ -1,346 +1,47 @@
1-
import type { FileBrowserFolderFile, PACSFile } from "@fnndsc/chrisapi";
2-
import {
3-
Button,
4-
Label,
5-
Text,
6-
Toolbar,
7-
ToolbarItem,
8-
Tooltip,
9-
} from "@patternfly/react-core";
10-
import ResetIcon from "@patternfly/react-icons/dist/esm/icons/history-icon";
11-
import { useMutation } from "@tanstack/react-query";
12-
import * as dcmjs from "dcmjs";
13-
import React, {
14-
Fragment,
15-
useCallback,
16-
useEffect,
17-
useState,
18-
type ReactElement,
19-
} from "react";
1+
import React from "react";
202
import { ErrorBoundary } from "react-error-boundary";
21-
import {
22-
type IFileBlob,
23-
fileViewerMap,
24-
getFileExtension,
25-
} from "../../api/model";
26-
import { useAppSelector } from "../../store/hooks";
3+
import { Label, Text } from "@patternfly/react-core";
274
import { SpinContainer } from "../Common";
28-
import {
29-
AddIcon,
30-
BrightnessIcon,
31-
InfoIcon,
32-
PauseIcon,
33-
PlayIcon,
34-
RotateIcon,
35-
RulerIcon,
36-
SearchIcon,
37-
ZoomIcon,
38-
} from "../Icons"; // Import PlayIcon
39-
import { TagInfoModal } from "./HelperComponent";
40-
41-
const ViewerDisplay = React.lazy(() => import("./displays/ViewerDisplay"));
5+
import type { FileBrowserFolderFile, PACSFile } from "@fnndsc/chrisapi";
6+
import { getFileExtension, fileViewerMap } from "../../api/model";
7+
import ViewerDisplay from "./displays/ViewerDisplay";
428

439
interface AllProps {
4410
selectedFile?: FileBrowserFolderFile | PACSFile;
45-
isDicom?: boolean;
4611
preview: "large" | "small";
4712
handleNext?: () => void;
4813
handlePrevious?: () => void;
49-
gallery?: boolean;
50-
// These props enable pagination and fetch on scroll
51-
list?: IFileBlob[];
52-
fetchMore?: boolean;
53-
handlePagination?: () => void;
54-
filesLoading?: boolean;
55-
}
56-
57-
export interface ActionState {
58-
[key: string]: boolean | string;
5914
}
6015

61-
const FileDetailView = (props: AllProps) => {
62-
const [tagInfo, setTagInfo] = useState<any>();
63-
const [actionState, setActionState] = useState<ActionState>({
64-
Zoom: false,
65-
previouslyActive: "",
66-
});
67-
const [parsingError, setParsingError] = useState<string>("");
68-
const drawerState = useAppSelector((state) => state.drawers);
69-
70-
const handleKeyboardEvents = useCallback(
71-
(event: any) => {
72-
switch (event.keyCode) {
73-
case 39: {
74-
event.preventDefault();
75-
props.handleNext?.();
76-
break;
77-
}
78-
79-
case 37: {
80-
event.preventDefault();
81-
props.handlePrevious?.();
82-
break;
83-
}
84-
85-
default:
86-
break;
87-
}
88-
},
89-
[props],
90-
);
91-
92-
useEffect(() => {
93-
window.addEventListener("keydown", handleKeyboardEvents);
94-
95-
return () => {
96-
window.removeEventListener("keydown", handleKeyboardEvents);
97-
};
98-
}, [handleKeyboardEvents]);
99-
100-
const displayTagInfo = useCallback(async () => {
101-
try {
102-
const blob = await selectedFile?.getFileBlob();
103-
if (!blob) {
104-
setParsingError("Failed to retrieve the file blob");
105-
return;
106-
}
107-
108-
const arrayBuffer = await blob.arrayBuffer();
109-
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
110-
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
111-
dicomData.dict,
112-
);
113-
// Sort the dataset keys alphabetically
114-
const sortedDataset = Object.keys(dataset)
115-
.sort()
116-
.reduce((sortedObj: any, key) => {
117-
sortedObj[key] = dataset[key];
118-
return sortedObj;
119-
}, {});
120-
121-
setTagInfo(sortedDataset);
122-
} catch (error) {
123-
console.error("Error parsing DICOM file:", error);
124-
setParsingError("Failed to parse the file for DICOM tags");
125-
}
126-
}, []);
127-
128-
const mutation = useMutation({
129-
mutationFn: displayTagInfo,
130-
onError: (error: any) => {
131-
setParsingError(error.message);
132-
},
133-
});
134-
135-
const { selectedFile, preview, fetchMore, handlePagination, filesLoading } =
136-
props;
16+
export default function FileDetailView(props: AllProps) {
17+
const { selectedFile, preview } = props;
13718
let viewerName = "";
138-
const fileType = getFileExtension(selectedFile?.data.fname);
139-
if (fileType) {
140-
if (!fileViewerMap[fileType]) {
141-
viewerName = "CatchallDisplay";
142-
} else {
19+
if (selectedFile) {
20+
const fileType = getFileExtension(selectedFile.data?.fname);
21+
if (fileType && fileViewerMap[fileType]) {
14322
viewerName = fileViewerMap[fileType];
23+
} else {
24+
viewerName = "CatchallDisplay";
14425
}
14526
}
14627

147-
const handleEvents = (action: string, previouslyActive: string) => {
148-
if (action === "TagInfo" && selectedFile) {
149-
mutation.mutate();
150-
}
151-
const currentAction = actionState[action];
152-
setActionState({
153-
[action]: !currentAction,
154-
previouslyActive,
155-
});
156-
};
157-
158-
const handleModalToggle = (
159-
actionName: string,
160-
value: boolean,
161-
previouslyActive: string,
162-
) => {
163-
setActionState({
164-
[actionName]: value,
165-
previouslyActive,
166-
});
167-
};
168-
169-
const previewType = preview === "large" ? "large-preview" : "small-preview";
170-
171-
const errorComponent = (error?: string) => (
172-
<span>
173-
<Label
174-
icon={<InfoIcon className="pf-v5-svg" />}
175-
color="red"
176-
href="#filled"
177-
>
178-
<Text component="p">
179-
{error
180-
? error
181-
: "Oh snap! Looks like there was an error. Please refresh the browser or try again."}
182-
</Text>
183-
</Label>
184-
</span>
28+
const errorComponent = (err?: string) => (
29+
<Label color="red">
30+
<Text>{err ?? "Error. Refresh or try again."}</Text>
31+
</Label>
18532
);
18633

187-
const fullScreen = drawerState.preview.maximized === true;
188-
18934
return (
190-
<Fragment>
191-
<React.Suspense fallback={<SpinContainer title="" />}>
192-
<ErrorBoundary fallback={errorComponent()}>
193-
<div className={previewType}>
194-
{previewType === "large-preview" && (
195-
<DicomHeader
196-
viewerName={viewerName}
197-
handleEvents={handleEvents}
198-
fullScreen={fullScreen}
199-
actionState={actionState}
200-
/>
201-
)}
202-
203-
{selectedFile && (
204-
<ViewerDisplay
205-
preview={preview}
206-
viewerName={viewerName}
207-
actionState={actionState}
208-
selectedFile={selectedFile}
209-
// Optional for dicom scrolling
210-
list={props.list}
211-
fetchMore={fetchMore}
212-
handlePagination={handlePagination}
213-
filesLoading={filesLoading}
214-
/>
215-
)}
216-
</div>
217-
218-
<TagInfoModal
219-
isDrawer={true}
220-
handleModalToggle={(actionState, toolState) => {
221-
const previouslyActive = Object.keys(actionState)[0];
222-
handleModalToggle(actionState, toolState, previouslyActive);
223-
}}
224-
isModalOpen={actionState.TagInfo as boolean}
225-
output={tagInfo}
226-
parsingError={parsingError}
35+
<React.Suspense fallback={<SpinContainer title="Loading..." />}>
36+
<ErrorBoundary fallback={errorComponent()}>
37+
{selectedFile && (
38+
<ViewerDisplay
39+
preview={preview}
40+
viewerName={viewerName}
41+
selectedFile={selectedFile}
22742
/>
228-
</ErrorBoundary>
229-
</React.Suspense>
230-
</Fragment>
43+
)}
44+
</ErrorBoundary>
45+
</React.Suspense>
23146
);
232-
};
233-
234-
export default FileDetailView;
235-
236-
const actions = [
237-
{
238-
name: "Zoom",
239-
icon: <ZoomIcon />,
240-
},
241-
{
242-
name: "Pan",
243-
icon: <SearchIcon />,
244-
},
245-
{
246-
name: "Magnify",
247-
icon: (
248-
// We could be using a better icon here.
249-
<AddIcon />
250-
),
251-
},
252-
{
253-
name: "PlanarRotate",
254-
icon: <RotateIcon />,
255-
},
256-
{
257-
name: "WindowLevel",
258-
icon: <BrightnessIcon />,
259-
},
260-
{
261-
name: "Reset",
262-
icon: <ResetIcon />,
263-
},
264-
{
265-
name: "Length",
266-
icon: <RulerIcon />,
267-
},
268-
{
269-
name: "TagInfo",
270-
icon: <InfoIcon />,
271-
},
272-
{
273-
name: "Play",
274-
icon: <PlayIcon />, // Add Play icon
275-
},
276-
];
277-
278-
const getViewerSpecificActions: {
279-
[key: string]: { name: string; icon: ReactElement }[];
280-
} = {
281-
DcmDisplay: actions,
282-
NiftiDisplay: actions,
283-
ImageDisplay: actions,
284-
};
285-
286-
export const DicomHeader = ({
287-
handleEvents,
288-
viewerName,
289-
fullScreen,
290-
actionState,
291-
}: {
292-
viewerName: string;
293-
handleEvents: (action: string, previouslyActive: string) => void;
294-
fullScreen: boolean;
295-
actionState: ActionState;
296-
}) => {
297-
const specificActions = getViewerSpecificActions[viewerName];
298-
299-
const appLauncherItems =
300-
specificActions &&
301-
specificActions.length > 0 &&
302-
specificActions.map((action) => {
303-
const spacer: {
304-
xl?: "spacerLg";
305-
lg?: "spacerLg";
306-
md?: "spacerMd";
307-
sm?: "spacerSm";
308-
} = {
309-
xl: "spacerLg",
310-
lg: "spacerLg",
311-
md: "spacerMd",
312-
sm: "spacerSm",
313-
};
314-
315-
// Dynamically set the icon for Play/Pause button
316-
let icon = action.icon;
317-
if (action.name === "Play") {
318-
icon = actionState.Play ? <PauseIcon /> : <PlayIcon />;
319-
}
320-
321-
return (
322-
<ToolbarItem spacer={spacer} key={action.name}>
323-
<Tooltip content={<span>{action.name}</span>}>
324-
<Button
325-
className={`${
326-
fullScreen ? "large-button" : "small-button"
327-
} button-style`}
328-
variant={
329-
actionState[action.name] === true ? "primary" : "control"
330-
}
331-
size="sm"
332-
icon={icon}
333-
onClick={(ev) => {
334-
const previouslyActive = Object.keys(actionState)[0];
335-
ev.preventDefault();
336-
handleEvents(action.name, previouslyActive);
337-
}}
338-
aria-label={action.name}
339-
/>
340-
</Tooltip>
341-
</ToolbarItem>
342-
);
343-
});
344-
345-
return <Toolbar className="centered-container">{appLauncherItems}</Toolbar>;
346-
};
47+
}

‎src/components/Preview/HelperComponent.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const TagInfoModal = ({
8585
placement="right"
8686
onClose={() => handleModalToggle("TagInfo", !isModalOpen)}
8787
open={isModalOpen}
88-
width={720} // You can adjust this value as needed
88+
width={720}
8989
>
9090
{content}
9191
</Drawer>

‎src/components/Preview/displays/DcmDisplay.tsx

+268-355
Large diffs are not rendered by default.

‎src/components/Preview/displays/ViewerDisplay.tsx

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { FileBrowserFolderFile, PACSFile } from "@fnndsc/chrisapi";
22
import type * as React from "react";
3-
import type { IFileBlob } from "../../../api/model";
4-
import type { ActionState } from "../FileDetailView";
53
import {
64
CatchallDisplay,
75
DcmDisplay,
@@ -29,14 +27,9 @@ const components = {
2927
};
3028

3129
interface ViewerDisplayProps {
30+
selectedFile: FileBrowserFolderFile | PACSFile;
3231
viewerName: string;
3332
preview?: string;
34-
actionState: ActionState;
35-
selectedFile: FileBrowserFolderFile | PACSFile;
36-
list?: IFileBlob[];
37-
fetchMore?: boolean;
38-
handlePagination?: () => void;
39-
filesLoading?: boolean;
4033
}
4134

4235
const ViewerDisplay: React.FC<ViewerDisplayProps> = (

‎src/components/Preview/displays/dicomUtils/utils.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const handleEvents = (
142142
const activeTool = Object.keys(actionState)[0];
143143
const previousTool = actionState.previouslyActive as string;
144144
const id = toolGroup?.id;
145-
if (activeTool === "TagInfo" || activeTool === "Play") return;
145+
146146
if (id) {
147147
toolGroup?.setToolPassive(previousTool);
148148
if (activeTool === "Reset") {
@@ -239,8 +239,11 @@ export const displayDicomImage = async (
239239
viewport,
240240
renderingEngine,
241241
};
242-
} catch (e) {
243-
throw new Error(e as string);
242+
} catch (e: unknown) {
243+
if (e instanceof Error) {
244+
throw new Error(e.message);
245+
}
246+
throw new Error("Failed to render");
244247
}
245248
};
246249

0 commit comments

Comments
 (0)
Please sign in to comment.