feat(studies-panel): New OHIF study panel - under experimental flag (#4254)
This commit is contained in:
1 parent
4432b6b420
commit
7a96406a11
80 files changed
+4119
-253
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { SidePanel } from '@ohif/ui';
|
||||
import { SidePanel as NewSidePanel } from '@ohif/ui-next';
|
||||
import { SidePanel as OldSidePanel } from '@ohif/ui';
|
||||
import { useAppConfig } from '@state';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
export type SidePanelWithServicesProps = {
|
||||
@@ -26,6 +28,7 @@ const SidePanelWithServices = ({
|
||||
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
||||
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const handleSidePanelOpen = useCallback(() => {
|
||||
setHasBeenOpened(true);
|
||||
@@ -75,6 +78,8 @@ const SidePanelWithServices = ({
|
||||
};
|
||||
}, [tabs, hasBeenOpened, panelService]);
|
||||
|
||||
const SidePanel = appConfig?.useExperimentalUI ? NewSidePanel : OldSidePanel;
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
{...props}
|
||||
|
||||
@@ -17,6 +17,7 @@ import createReportDialogPrompt, {
|
||||
} from './createReportDialogPrompt';
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
|
||||
@@ -24,6 +25,9 @@ export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes): React.FunctionComponent {
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
|
||||
@@ -212,6 +216,24 @@ export default function PanelMeasurementTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="ohif-scrollbar overflow-y-auto overflow-x-hidden"
|
||||
data-cy={'measurements-panel'}
|
||||
|
||||
+77
-18
@@ -1,8 +1,14 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { StudyBrowser as NewStudyBrowser } from '@ohif/ui-next';
|
||||
import { StudyBrowser as OldStudyBrowser } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
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;
|
||||
|
||||
@@ -16,10 +22,14 @@ function PanelStudyBrowser({
|
||||
getStudiesForPatientByMRN,
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const { hangingProtocolService, displaySetService, uiNotificationService } =
|
||||
const { hangingProtocolService, displaySetService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
// 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.
|
||||
@@ -36,6 +46,31 @@ function PanelStudyBrowser({
|
||||
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;
|
||||
@@ -164,9 +199,10 @@ function PanelStudyBrowser({
|
||||
const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
data => {
|
||||
if (!hasLoadedViewports) {
|
||||
return;
|
||||
}
|
||||
// for some reason this breaks thumbnail loading
|
||||
// if (!hasLoadedViewports) {
|
||||
// return;
|
||||
// }
|
||||
const { displaySetsAdded, options } = data;
|
||||
displaySetsAdded.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
@@ -236,7 +272,7 @@ function PanelStudyBrowser({
|
||||
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
|
||||
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
||||
? // eslint-disable-next-line prettier/prettier
|
||||
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
|
||||
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
|
||||
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||
|
||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||
@@ -249,19 +285,42 @@ function PanelStudyBrowser({
|
||||
|
||||
const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
|
||||
|
||||
const StudyBrowser = appConfig?.useExperimentalUI ? NewStudyBrowser : OldStudyBrowser;
|
||||
|
||||
return (
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
servicesManager={servicesManager}
|
||||
activeTabName={activeTabName}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<PanelStudyBrowserHeader
|
||||
tab={tab}
|
||||
getCloseIcon={getCloseIcon}
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { actionIcon, viewPreset } from './types';
|
||||
|
||||
function PanelStudyBrowserHeader({
|
||||
tab,
|
||||
getCloseIcon,
|
||||
viewPresets,
|
||||
updateViewPresetValue,
|
||||
actionIcons,
|
||||
updateActionIconValue,
|
||||
}: {
|
||||
tab: any;
|
||||
getCloseIcon: () => JSX.Element;
|
||||
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 justify-between">
|
||||
<div className="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 className="text-muted-foreground flex items-center justify-center">
|
||||
{' '}
|
||||
<span>{tab.label}</span>{' '}
|
||||
</div>
|
||||
|
||||
<div className="mr-[30px] flex items-center justify-center">
|
||||
<div className="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>
|
||||
</div>
|
||||
{getCloseIcon()}
|
||||
</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 };
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import PanelStudyBrowser from './PanelStudyBrowser';
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
||||
import getStudiesForPatientByMRN from './getStudiesForPatientByMRN';
|
||||
import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
|
||||
@@ -13,7 +13,14 @@ import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStu
|
||||
* @param {object} commandsManager
|
||||
* @param {object} extensionManager
|
||||
*/
|
||||
function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) {
|
||||
function WrappedPanelStudyBrowser({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}) {
|
||||
// TODO: This should be made available a different way; route should have
|
||||
// already determined our datasource
|
||||
const dataSource = extensionManager.getDataSources()[0];
|
||||
@@ -34,6 +41,9 @@ function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesM
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByMRN={_getStudiesForPatientByMRN}
|
||||
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PanelStudyBrowser from './PanelStudyBrowser';
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser';
|
||||
import PanelMeasurementTable from './PanelMeasurementTable';
|
||||
import createReportDialogPrompt from './createReportDialogPrompt';
|
||||
|
||||
@@ -184,6 +184,22 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.viewPresets',
|
||||
// change your default selected preset here
|
||||
value: [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,12 +8,15 @@ import i18n from 'i18next';
|
||||
// - show errors in UI for thumbnails if promise fails
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedMeasurementPanel = () => {
|
||||
const wrappedMeasurementPanel = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
return (
|
||||
<PanelMeasurementTable
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -24,11 +27,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
iconName: 'tab-studies',
|
||||
iconLabel: 'Studies',
|
||||
label: i18n.t('SidePanel:Studies'),
|
||||
component: WrappedPanelStudyBrowser.bind(null, {
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}),
|
||||
component: props => (
|
||||
<WrappedPanelStudyBrowser
|
||||
{...props}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'measurements',
|
||||
|
||||
Reference in new issue
Block a user