Initial commit from prod-batam
This commit is contained in:
55
extensions/default/src/Panels/DataSourceSelector.tsx
Normal file
55
extensions/default/src/Panels/DataSourceSelector.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
import { Button, ButtonEnums } from '@ohif/ui';
|
||||
|
||||
function DataSourceSelector() {
|
||||
const [appConfig] = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// This is frowned upon, but the raw config is needed here to provide
|
||||
// the selector
|
||||
const dsConfigs = appConfig.dataSources;
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<div className="flex h-screen w-screen items-center justify-center ">
|
||||
<div className="bg-secondary-dark mx-auto space-y-2 rounded-lg py-8 px-8 drop-shadow-md">
|
||||
<img
|
||||
className="mx-auto block h-14"
|
||||
src="./ohif-logo.svg"
|
||||
alt="OHIF"
|
||||
/>
|
||||
<div className="space-y-2 pt-4 text-center">
|
||||
{dsConfigs
|
||||
.filter(it => it.sourceName !== 'dicomjson' && it.sourceName !== 'dicomlocal')
|
||||
.map(ds => (
|
||||
<div key={ds.sourceName}>
|
||||
<h1 className="text-white">
|
||||
{ds.configuration?.friendlyName || ds.friendlyName}
|
||||
</h1>
|
||||
<Button
|
||||
type={ButtonEnums.type.primary}
|
||||
className={classnames('ml-2')}
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: '/',
|
||||
search: `datasources=${ds.sourceName}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ds.sourceName}
|
||||
</Button>
|
||||
<br />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceSelector;
|
||||
392
extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx
Normal file
392
extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { StudyBrowser } from '@ohif/ui-next';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
|
||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||
|
||||
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} param0
|
||||
*/
|
||||
function PanelStudyBrowser({
|
||||
servicesManager,
|
||||
getImageSrc,
|
||||
getStudiesForPatientByMRN,
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
commandsManager,
|
||||
}: withAppTypes) {
|
||||
const { hangingProtocolService, displaySetService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
const { StudyInstanceUIDs } = useImageViewer();
|
||||
const [{ activeViewportId, viewports, isHangingProtocolLayout }, viewportGridService] =
|
||||
useViewportGrid();
|
||||
const [activeTabName, setActiveTabName] = useState('all');
|
||||
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
||||
...StudyInstanceUIDs,
|
||||
]);
|
||||
const [hasLoadedViewports, setHasLoadedViewports] = useState(false);
|
||||
const [studyDisplayList, setStudyDisplayList] = useState([]);
|
||||
const [displaySets, setDisplaySets] = useState([]);
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
|
||||
const [viewPresets, setViewPresets] = useState(
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
|
||||
);
|
||||
|
||||
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
|
||||
|
||||
// multiple can be true or false
|
||||
const updateActionIconValue = actionIcon => {
|
||||
actionIcon.value = !actionIcon.value;
|
||||
const newActionIcons = [...actionIcons];
|
||||
setActionIcons(newActionIcons);
|
||||
};
|
||||
|
||||
// only one is true at a time
|
||||
const updateViewPresetValue = viewPreset => {
|
||||
if (!viewPreset) {
|
||||
return;
|
||||
}
|
||||
const newViewPresets = viewPresets.map(preset => {
|
||||
preset.selected = preset.id === viewPreset.id;
|
||||
return preset;
|
||||
});
|
||||
setViewPresets(newViewPresets);
|
||||
};
|
||||
|
||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||
let updatedViewports = [];
|
||||
const viewportId = activeViewportId;
|
||||
try {
|
||||
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
isHangingProtocolLayout
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
uiNotificationService.show({
|
||||
title: 'Thumbnail Double Click',
|
||||
message: 'The selected display sets could not be added to the viewport.',
|
||||
type: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
};
|
||||
|
||||
// ~~ studyDisplayList
|
||||
useEffect(() => {
|
||||
// Fetch all studies for the patient in each primary study
|
||||
async function fetchStudiesForPatient(StudyInstanceUID) {
|
||||
// current study qido
|
||||
const qidoForStudyUID = await dataSource.query.studies.search({
|
||||
studyInstanceUid: StudyInstanceUID,
|
||||
});
|
||||
|
||||
if (!qidoForStudyUID?.length) {
|
||||
navigate('/notfoundstudy', '_self');
|
||||
throw new Error('Invalid study URL');
|
||||
}
|
||||
|
||||
let qidoStudiesForPatient = qidoForStudyUID;
|
||||
|
||||
// try to fetch the prior studies based on the patientID if the
|
||||
// server can respond.
|
||||
try {
|
||||
qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
|
||||
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
|
||||
return {
|
||||
studyInstanceUid: qidoStudy.StudyInstanceUID,
|
||||
date: formatDate(qidoStudy.StudyDate),
|
||||
description: qidoStudy.StudyDescription,
|
||||
modalities: qidoStudy.ModalitiesInStudy,
|
||||
numInstances: qidoStudy.NumInstances,
|
||||
};
|
||||
});
|
||||
|
||||
setStudyDisplayList(prevArray => {
|
||||
const ret = [...prevArray];
|
||||
for (const study of actuallyMappedStudies) {
|
||||
if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) {
|
||||
ret.push(study);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
|
||||
}, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]);
|
||||
|
||||
// // ~~ Initial Thumbnails
|
||||
useEffect(() => {
|
||||
if (!hasLoadedViewports) {
|
||||
if (activeViewportId) {
|
||||
// Once there is an active viewport id, it means the layout is ready
|
||||
// so wait a bit of time to allow the viewports preferential loading
|
||||
// which improves user experience of responsiveness significantly on slower
|
||||
// systems.
|
||||
window.setTimeout(() => setHasLoadedViewports(true), 250);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDisplaySets = displaySetService.activeDisplaySets;
|
||||
currentDisplaySets.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
|
||||
if (!imageId || displaySet?.unsupported) {
|
||||
return;
|
||||
}
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId);
|
||||
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
});
|
||||
}, [
|
||||
StudyInstanceUIDs,
|
||||
dataSource,
|
||||
displaySetService,
|
||||
getImageSrc,
|
||||
hasLoadedViewports,
|
||||
activeViewportId,
|
||||
]);
|
||||
|
||||
// ~~ displaySets
|
||||
useEffect(() => {
|
||||
// TODO: Are we sure `activeDisplaySets` will always be accurate?
|
||||
const currentDisplaySets = displaySetService.activeDisplaySets;
|
||||
const mappedDisplaySets = _mapDisplaySets(currentDisplaySets, thumbnailImageSrcMap);
|
||||
sortStudyInstances(mappedDisplaySets);
|
||||
|
||||
setDisplaySets(mappedDisplaySets);
|
||||
}, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]);
|
||||
|
||||
// ~~ subscriptions --> displaySets
|
||||
useEffect(() => {
|
||||
// DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
|
||||
const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
data => {
|
||||
// for some reason this breaks thumbnail loading
|
||||
// if (!hasLoadedViewports) {
|
||||
// return;
|
||||
// }
|
||||
const { displaySetsAdded } = data;
|
||||
displaySetsAdded.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
||||
if (displaySet?.unsupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
imageId,
|
||||
dSet.initialViewport
|
||||
);
|
||||
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
SubscriptionDisplaySetsAdded.unsubscribe();
|
||||
};
|
||||
}, [getImageSrc, dataSource, displaySetService]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Will this always hold _all_ the displaySets we care about?
|
||||
// DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets`
|
||||
const SubscriptionDisplaySetsChanged = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
||||
changedDisplaySets => {
|
||||
const mappedDisplaySets = _mapDisplaySets(changedDisplaySets, thumbnailImageSrcMap);
|
||||
setDisplaySets(mappedDisplaySets);
|
||||
}
|
||||
);
|
||||
|
||||
const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||
() => {
|
||||
const mappedDisplaySets = _mapDisplaySets(
|
||||
displaySetService.getActiveDisplaySets(),
|
||||
thumbnailImageSrcMap
|
||||
);
|
||||
|
||||
setDisplaySets(mappedDisplaySets);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
SubscriptionDisplaySetsChanged.unsubscribe();
|
||||
SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
|
||||
};
|
||||
}, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]);
|
||||
|
||||
const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets);
|
||||
|
||||
// TODO: Should not fire this on "close"
|
||||
function _handleStudyClick(StudyInstanceUID) {
|
||||
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
|
||||
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
||||
? // eslint-disable-next-line prettier/prettier
|
||||
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
|
||||
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||
|
||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||
|
||||
if (!shouldCollapseStudy) {
|
||||
const madeInClient = true;
|
||||
requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient);
|
||||
}
|
||||
}
|
||||
|
||||
const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
|
||||
|
||||
const onThumbnailContextMenu = (commandName, options) => {
|
||||
commandsManager.runCommand(commandName, options);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<PanelStudyBrowserHeader
|
||||
viewPresets={viewPresets}
|
||||
updateViewPresetValue={updateViewPresetValue}
|
||||
actionIcons={actionIcons}
|
||||
updateActionIconValue={updateActionIconValue}
|
||||
/>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
servicesManager={servicesManager}
|
||||
activeTabName={activeTabName}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
||||
viewPresets={viewPresets}
|
||||
onThumbnailContextMenu={onThumbnailContextMenu}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PanelStudyBrowser;
|
||||
|
||||
/**
|
||||
* Maps from the DataSource's format to a naturalized object
|
||||
*
|
||||
* @param {*} studies
|
||||
*/
|
||||
function _mapDataSourceStudies(studies) {
|
||||
return studies.map(study => {
|
||||
// TODO: Why does the data source return in this format?
|
||||
return {
|
||||
AccessionNumber: study.accession,
|
||||
StudyDate: study.date,
|
||||
StudyDescription: study.description,
|
||||
NumInstances: study.instances,
|
||||
ModalitiesInStudy: study.modalities,
|
||||
PatientID: study.mrn,
|
||||
PatientName: study.patientName,
|
||||
StudyInstanceUID: study.studyInstanceUid,
|
||||
StudyTime: study.time,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
||||
const thumbnailDisplaySets = [];
|
||||
const thumbnailNoImageDisplaySets = [];
|
||||
|
||||
displaySets
|
||||
.filter(ds => !ds.excludeFromThumbnailBrowser)
|
||||
.forEach(ds => {
|
||||
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
||||
const componentType = _getComponentType(ds);
|
||||
|
||||
const array =
|
||||
componentType === 'thumbnail' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
||||
|
||||
array.push({
|
||||
displaySetInstanceUID: ds.displaySetInstanceUID,
|
||||
description: ds.SeriesDescription || '',
|
||||
seriesNumber: ds.SeriesNumber,
|
||||
modality: ds.Modality,
|
||||
seriesDate: ds.SeriesDate,
|
||||
seriesTime: ds.SeriesTime,
|
||||
numInstances: ds.numImageFrames,
|
||||
countIcon: ds.countIcon,
|
||||
StudyInstanceUID: ds.StudyInstanceUID,
|
||||
messages: ds.messages,
|
||||
componentType,
|
||||
imageSrc,
|
||||
dragData: {
|
||||
type: 'displayset',
|
||||
displaySetInstanceUID: ds.displaySetInstanceUID,
|
||||
// .. Any other data to pass
|
||||
},
|
||||
isHydratedForDerivedDisplaySet: ds.isHydrated,
|
||||
});
|
||||
});
|
||||
|
||||
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
|
||||
}
|
||||
|
||||
const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||
|
||||
function _getComponentType(ds) {
|
||||
if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
|
||||
// TODO probably others.
|
||||
return 'thumbnailNoImage';
|
||||
}
|
||||
|
||||
return 'thumbnail';
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { actionIcon, viewPreset } from './types';
|
||||
|
||||
function PanelStudyBrowserHeader({
|
||||
viewPresets,
|
||||
updateViewPresetValue,
|
||||
actionIcons,
|
||||
updateActionIconValue,
|
||||
}: {
|
||||
viewPresets: viewPreset[];
|
||||
updateViewPresetValue: (viewPreset: viewPreset) => void;
|
||||
actionIcons: actionIcon[];
|
||||
updateActionIconValue: (actionIcon: actionIcon) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-muted flex h-[40px] select-none rounded-t p-2">
|
||||
<div className={'flex h-[24px] w-full select-none justify-center self-center text-[14px]'}>
|
||||
<div className="flex w-full items-center gap-[10px]">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="text-primary-active flex items-center space-x-1">
|
||||
{actionIcons.map((icon: actionIcon, index) =>
|
||||
React.createElement(Icons[icon.iconName] || Icons.MissingIcon, {
|
||||
key: index,
|
||||
onClick: () => updateActionIconValue(icon),
|
||||
className: `cursor-pointer`,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex h-full items-center justify-center">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={viewPresets.filter(preset => preset.selected)[0].id}
|
||||
onValueChange={value => {
|
||||
const selectedViewPreset = viewPresets.find(preset => preset.id === value);
|
||||
updateViewPresetValue(selectedViewPreset);
|
||||
}}
|
||||
>
|
||||
{viewPresets.map((viewPreset: viewPreset, index) => (
|
||||
<ToggleGroupItem
|
||||
key={index}
|
||||
aria-label={viewPreset.id}
|
||||
value={viewPreset.id}
|
||||
className="text-actions-primary"
|
||||
>
|
||||
{React.createElement(Icons[viewPreset.iconName] || Icons.MissingIcon)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { PanelStudyBrowserHeader };
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { actionIcon } from '../types/actionsIcon';
|
||||
|
||||
const defaultActionIcons = [
|
||||
{
|
||||
id: 'settings',
|
||||
iconName: 'Settings',
|
||||
value: false,
|
||||
},
|
||||
] as actionIcon[];
|
||||
|
||||
export { defaultActionIcons };
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defaultActionIcons } from './actionIcons';
|
||||
import { defaultViewPresets } from './viewPresets';
|
||||
|
||||
export { defaultActionIcons, defaultViewPresets };
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { viewPreset } from '../types/viewPreset';
|
||||
|
||||
const defaultViewPresets = [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
] as viewPreset[];
|
||||
|
||||
export { defaultViewPresets };
|
||||
@@ -0,0 +1,7 @@
|
||||
type actionIcon = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
export type { actionIcon };
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { actionIcon } from './actionIcon';
|
||||
import type { viewPreset } from './viewPreset';
|
||||
|
||||
export type { actionIcon, viewPreset };
|
||||
@@ -0,0 +1,7 @@
|
||||
type viewPreset = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type { viewPreset };
|
||||
69
extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx
Normal file
69
extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
||||
import getStudiesForPatientByMRN from './getStudiesForPatientByMRN';
|
||||
import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
|
||||
|
||||
/**
|
||||
* Wraps the PanelStudyBrowser and provides features afforded by managers/services
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} commandsManager
|
||||
* @param {object} extensionManager
|
||||
*/
|
||||
function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) {
|
||||
// TODO: This should be made available a different way; route should have
|
||||
// already determined our datasource
|
||||
const dataSource = extensionManager.getDataSources()[0];
|
||||
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource);
|
||||
const _getImageSrcFromImageId = useCallback(
|
||||
_createGetImageSrcFromImageIdFn(extensionManager),
|
||||
[]
|
||||
);
|
||||
const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind(
|
||||
null,
|
||||
dataSource
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelStudyBrowser
|
||||
servicesManager={servicesManager}
|
||||
dataSource={dataSource}
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByMRN={_getStudiesForPatientByMRN}
|
||||
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs cornerstone library reference using a dependent command from
|
||||
* the @ohif/extension-cornerstone extension. Then creates a helper function
|
||||
* that can take an imageId and return an image src.
|
||||
*
|
||||
* @param {func} getCommand - CommandManager's getCommand method
|
||||
* @returns {func} getImageSrcFromImageId - A utility function powered by
|
||||
* cornerstone
|
||||
*/
|
||||
function _createGetImageSrcFromImageIdFn(extensionManager) {
|
||||
const utilities = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
|
||||
try {
|
||||
const { cornerstone } = utilities.exports.getCornerstoneLibraries();
|
||||
return getImageSrcFromImageId.bind(null, cornerstone);
|
||||
} catch (ex) {
|
||||
throw new Error('Required command not found');
|
||||
}
|
||||
}
|
||||
|
||||
WrappedPanelStudyBrowser.propTypes = {
|
||||
commandsManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.object.isRequired,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default WrappedPanelStudyBrowser;
|
||||
134
extensions/default/src/Panels/createReportDialogPrompt.tsx
Normal file
134
extensions/default/src/Panels/createReportDialogPrompt.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui';
|
||||
import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
export default function CreateReportDialogPrompt(uiDialogService, { extensionManager }) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let dialogId = undefined;
|
||||
|
||||
const _handleClose = () => {
|
||||
// Dismiss dialog
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
// Notify of cancel action
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} param0.action - value of action performed
|
||||
* @param {string} param0.value - value from input field
|
||||
*/
|
||||
const _handleFormSubmit = ({ action, value }) => {
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
value: value.label,
|
||||
dataSourceName: value.dataSourceName,
|
||||
});
|
||||
break;
|
||||
case 'cancel':
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const dataSourcesOpts = Object.keys(extensionManager.dataSourceMap)
|
||||
.filter(ds => {
|
||||
const configuration = extensionManager.dataSourceDefs[ds]?.configuration;
|
||||
const supportsStow = configuration?.supportsStow ?? configuration?.wadoRoot;
|
||||
return supportsStow;
|
||||
})
|
||||
.map(ds => {
|
||||
return {
|
||||
value: ds,
|
||||
label: ds,
|
||||
placeHolder: ds,
|
||||
};
|
||||
});
|
||||
|
||||
dialogId = uiDialogService.create({
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
content: Dialog,
|
||||
useLastPosition: false,
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Create Report',
|
||||
value: {
|
||||
label: '',
|
||||
dataSourceName: extensionManager.activeDataSource,
|
||||
},
|
||||
noCloseButton: true,
|
||||
onClose: _handleClose,
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
|
||||
],
|
||||
// TODO: Should be on button press...
|
||||
onSubmit: _handleFormSubmit,
|
||||
body: ({ value, setValue }) => {
|
||||
const onChangeHandler = event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
};
|
||||
const onKeyPressHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
value: value.label,
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{dataSourcesOpts.length > 1 && window.config?.allowMultiSelectExport && (
|
||||
<div>
|
||||
<label className="text-[14px] leading-[1.2] text-white">Data Source</label>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mt-2 bg-black"
|
||||
options={dataSourcesOpts}
|
||||
placeholder={
|
||||
dataSourcesOpts.find(option => option.value === value.dataSourceName)
|
||||
.placeHolder
|
||||
}
|
||||
value={value.dataSourceName}
|
||||
onChange={evt => {
|
||||
setValue(v => ({ ...v, dataSourceName: evt.value }));
|
||||
}}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<Input
|
||||
autoFocus
|
||||
label="Enter the report name"
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
25
extensions/default/src/Panels/debounce.js
Normal file
25
extensions/default/src/Panels/debounce.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function () {
|
||||
var context = this,
|
||||
args = arguments;
|
||||
var later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default debounce;
|
||||
16
extensions/default/src/Panels/getImageSrcFromImageId.js
Normal file
16
extensions/default/src/Panels/getImageSrcFromImageId.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @param {*} cornerstone
|
||||
* @param {*} imageId
|
||||
*/
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas({ canvas, imageId, thumbnail: true })
|
||||
.then(imageId => {
|
||||
resolve(canvas.toDataURL());
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
export default getImageSrcFromImageId;
|
||||
12
extensions/default/src/Panels/getStudiesForPatientByMRN.js
Normal file
12
extensions/default/src/Panels/getStudiesForPatientByMRN.js
Normal file
@@ -0,0 +1,12 @@
|
||||
async function getStudiesForPatientByMRN(dataSource, qidoForStudyUID) {
|
||||
if (qidoForStudyUID && qidoForStudyUID.length && qidoForStudyUID[0].mrn) {
|
||||
return dataSource.query.studies.search({
|
||||
patientId: qidoForStudyUID[0].mrn,
|
||||
disableWildcard: true,
|
||||
});
|
||||
}
|
||||
console.log('No mrn found for', qidoForStudyUID);
|
||||
return qidoForStudyUID;
|
||||
}
|
||||
|
||||
export default getStudiesForPatientByMRN;
|
||||
5
extensions/default/src/Panels/index.js
Normal file
5
extensions/default/src/Panels/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser';
|
||||
import createReportDialogPrompt from './createReportDialogPrompt';
|
||||
|
||||
export { PanelStudyBrowser, WrappedPanelStudyBrowser, createReportDialogPrompt };
|
||||
@@ -0,0 +1,19 @@
|
||||
function requestDisplaySetCreationForStudy(
|
||||
dataSource,
|
||||
displaySetService,
|
||||
StudyInstanceUID,
|
||||
madeInClient
|
||||
) {
|
||||
// TODO: is this already short-circuited by the map of Retrieve promises?
|
||||
if (
|
||||
displaySetService.activeDisplaySets.some(
|
||||
displaySet => displaySet.StudyInstanceUID === StudyInstanceUID
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
|
||||
}
|
||||
|
||||
export default requestDisplaySetCreationForStudy;
|
||||
Reference in New Issue
Block a user