feat(studies-panel): New OHIF study panel - under experimental flag (#4254)

This commit is contained in:
Ibrahim authored and GitHub committed 2024-09-11 14:57:19 -04:00
1 parent 4432b6b420
commit 7a96406a11
80 files changed
+4119 -253

No files matched your search

@@ -1,7 +1,8 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import { Toolbox as NewToolbox } from '@ohif/ui-next';
import { Toolbox as OldToolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
const getPanelModule = ({
@@ -13,7 +14,27 @@ const getPanelModule = ({
}: withAppTypes) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const wrappedPanelSegmentation = ({ configuration, renderHeader, getCloseIcon, tab }) => {
const [appConfig] = useAppConfig();
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
);
};
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
const [appConfig] = useAppConfig();
return (
@@ -30,9 +51,16 @@ const getPanelModule = ({
);
};
const wrappedPanelSegmentationWithTools = configuration => {
const wrappedPanelSegmentationWithTools = ({
configuration,
renderHeader,
getCloseIcon,
tab,
}) => {
const [appConfig] = useAppConfig();
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
return (
<>
<Toolbox
@@ -44,6 +72,9 @@ const getPanelModule = ({
configuration={{
...configuration,
}}
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
<PanelSegmentation
commandsManager={commandsManager}
@@ -74,6 +105,13 @@ const getPanelModule = ({
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
{
name: 'panelSegmentationNoHeader',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationNoHeader,
},
];
};
@@ -6,6 +6,7 @@ import { SegmentationPanelMode } from '../types/segmentation';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
import { Separator } from '@ohif/ui-next';
const components = {
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
@@ -17,6 +18,9 @@ export default function PanelSegmentation({
commandsManager,
extensionManager,
configuration,
renderHeader,
getCloseIcon,
tab,
}: withAppTypes) {
const {
segmentationService,
@@ -319,52 +323,76 @@ export default function PanelSegmentation({
: onSegmentationAdd;
return (
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
<>
{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"
/>
</>
)}
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderInactiveSegmentations',
value
)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
</>
);
}
@@ -1,20 +1,28 @@
import React from 'react';
import { DynamicDataPanel } from './panels';
import { Toolbox } from '@ohif/ui';
import { Toolbox as NewToolbox } from '@ohif/ui-next';
import { Toolbox as OldToolbox } from '@ohif/ui';
import { useAppConfig } from '@state';
import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
const wrappedDynamicDataPanel = () => {
const wrappedDynamicDataPanel = ({ renderHeader, getCloseIcon, tab }) => {
return (
<DynamicDataPanel
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
);
};
const wrappedDynamicToolbox = () => {
const wrappedDynamicToolbox = ({ renderHeader, getCloseIcon, tab }) => {
const [appConfig] = useAppConfig();
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
return (
<>
<Toolbox
@@ -23,6 +31,9 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
extensionManager={extensionManager}
buttonSectionId="dynamic-toolbox"
title="Threshold Tools"
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
</>
);
@@ -1,17 +1,44 @@
import React from 'react';
import PanelGenerateImage from './PanelGenerateImage';
import { Separator } from '@ohif/ui-next';
function DynamicDataPanel({ servicesManager, commandsManager }) {
function DynamicDataPanel({
servicesManager,
commandsManager,
renderHeader,
getCloseIcon,
tab,
}: withAppTypes) {
return (
<div
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
<>
{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="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
</>
);
}
@@ -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'}
@@ -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 -1
View File
@@ -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,
},
],
},
],
},
];
+12 -6
View File
@@ -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',
@@ -8,6 +8,7 @@ import dcmjs from 'dcmjs';
import callInputDialog from '../../utils/callInputDialog';
import constructSR from '../../utils/constructSR';
import { saveByteArray } from '../../utils/saveByteArray';
import { Separator } from '@ohif/ui-next';
let saving = false;
const { datasetToBuffer } = dcmjs.data;
@@ -56,6 +57,9 @@ interface IMicroscopyPanelProps extends WithTranslation {
servicesManager: AppTypes.ServicesManager;
extensionManager: ExtensionManager;
commandsManager: CommandsManager;
renderHeader?: boolean;
getCloseIcon?: PropTypes.func;
tab: PropTypes.object;
}
/**
@@ -328,6 +332,20 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) {
return (
<>
{props.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>{props.tab.label}</span>
</div>
</div>
{props.getCloseIcon()}
</div>
<Separator orientation="horizontal" className="bg-black" thickness="2px" />
</>
)}{' '}
<div
className="ohif-scrollbar overflow-y-auto overflow-x-hidden"
data-cy={'measurements-panel'}
@@ -13,7 +13,7 @@ export default function getPanelModule({
extensionManager,
servicesManager,
}: Types.Extensions.ExtensionParams) {
const wrappedMeasurementPanel = () => {
const wrappedMeasurementPanel = ({ renderHeader, getCloseIcon, tab }) => {
const [{ activeViewportId, viewports }] = useViewportGrid();
return (
@@ -25,6 +25,9 @@ export default function getPanelModule({
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
);
};
@@ -1,11 +1,13 @@
import { Types } from '@ohif/core';
import { PanelMeasurementTableTracking, PanelStudyBrowserTracking } from './panels';
import i18n from 'i18next';
import React from 'react';
// TODO:
// - No loading UI exists yet
// - cancel promises when component is destroyed
// - show errors in UI for thumbnails if promise fails
function getPanelModule({ commandsManager, extensionManager, servicesManager }): Types.Panel[] {
return [
{
@@ -13,11 +15,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
iconName: 'tab-studies',
iconLabel: 'Studies',
label: i18n.t('SidePanel:Studies'),
component: PanelStudyBrowserTracking.bind(null, {
commandsManager,
extensionManager,
servicesManager,
}),
component: props => (
<PanelStudyBrowserTracking
{...props}
commandsManager={commandsManager}
extensionManager={extensionManager}
servicesManager={servicesManager}
/>
),
},
{
@@ -25,11 +30,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
iconName: 'tab-linear',
iconLabel: 'Measure',
label: i18n.t('SidePanel:Measurements'),
component: PanelMeasurementTableTracking.bind(null, {
commandsManager,
extensionManager,
servicesManager,
}),
component: props => (
<PanelMeasurementTableTracking
{...props}
commandsManager={commandsManager}
extensionManager={extensionManager}
servicesManager={servicesManager}
/>
),
},
];
}
@@ -7,6 +7,7 @@ import { useAppConfig } from '@state';
import { useTrackedMeasurements } from '../../getContextModule';
import debounce from 'lodash.debounce';
import { useTranslation } from 'react-i18next';
import { Separator } from '@ohif/ui-next';
const { downloadCSVReport } = utils;
const { formatDate } = utils;
@@ -18,7 +19,13 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
};
function PanelMeasurementTableTracking({ servicesManager, extensionManager }: withAppTypes) {
function PanelMeasurementTableTracking({
servicesManager,
extensionManager,
renderHeader,
getCloseIcon,
tab,
}: withAppTypes) {
const [viewportGrid] = useViewportGrid();
const { t } = useTranslation('MeasurementTable');
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString());
@@ -176,6 +183,24 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi
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="invisible-scrollbar overflow-y-auto overflow-x-hidden"
ref={measurementsPanelRef}
@@ -3,8 +3,15 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { utils } from '@ohif/core';
import { StudyBrowser, useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui';
import { useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui';
import { StudyBrowser as NewStudyBrowser } from '@ohif/ui-next';
import { StudyBrowser as OldStudyBrowser } from '@ohif/ui';
import { useAppConfig } from '@state';
import { useTrackedMeasurements } from '../../getContextModule';
import { Separator } from '@ohif/ui-next';
import { PanelStudyBrowserTrackingHeader } from './PanelStudyBrowserTrackingHeader';
import { defaultActionIcons, defaultViewPresets } from './constants';
const { formatDate, createStudyBrowserTabs } = utils;
@@ -18,6 +25,9 @@ function PanelStudyBrowserTracking({
getStudiesForPatientByMRN,
requestDisplaySetCreationForStudy,
dataSource,
renderHeader,
getCloseIcon,
tab,
}: withAppTypes) {
const {
displaySetService,
@@ -26,10 +36,12 @@ function PanelStudyBrowserTracking({
uiNotificationService,
measurementService,
studyPrefetcherService,
customizationService,
} = servicesManager.services;
const navigate = useNavigate();
const { t } = useTranslation('Common');
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.
@@ -49,6 +61,29 @@ function PanelStudyBrowserTracking({
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
const [viewPresets, setViewPresets] = useState(
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
);
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
const updateActionIconValue = actionIcon => {
actionIcon.value = !actionIcon.value;
const newActionIcons = [...actionIcons];
setActionIcons(newActionIcons);
};
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;
@@ -431,23 +466,46 @@ function PanelStudyBrowserTracking({
});
};
const StudyBrowser = appConfig.useExperimentalUI ? NewStudyBrowser : OldStudyBrowser;
return (
<StudyBrowser
tabs={tabs}
servicesManager={servicesManager}
activeTabName={activeTabName}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
onClickStudy={_handleStudyClick}
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
onClickUntrack={displaySetInstanceUID => {
onClickUntrack(displaySetInstanceUID);
}}
onClickThumbnail={() => {}}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
/>
<>
{renderHeader && (
<>
<PanelStudyBrowserTrackingHeader
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}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
onClickStudy={_handleStudyClick}
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
onClickUntrack={displaySetInstanceUID => {
onClickUntrack(displaySetInstanceUID);
}}
onClickThumbnail={() => {}}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
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 PanelStudyBrowserTrackingHeader({
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 { PanelStudyBrowserTrackingHeader };
@@ -0,0 +1,11 @@
import type { actionIcon } from '../PanelStudyBrowserTracking/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 '../PanelStudyBrowserTracking/types/viewPreset';
const defaultViewPresets = [
{
id: 'list',
iconName: 'ListView',
selected: false,
},
{
id: 'thumbnails',
iconName: 'ThumbnailView',
selected: true,
},
] as viewPreset[];
export { defaultViewPresets };
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
//
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
@@ -25,6 +25,9 @@ function WrappedPanelStudyBrowserTracking({
commandsManager,
extensionManager,
servicesManager,
renderHeader,
getCloseIcon,
tab,
}: withAppTypes) {
const dataSource = extensionManager.getActiveDataSource()[0];
@@ -46,6 +49,9 @@ function WrappedPanelStudyBrowserTracking({
getImageSrc={_getImageSrcFromImageId}
getStudiesForPatientByMRN={_getStudiesForPatientByMRN}
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
);
}
@@ -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 };
+126 -96
View File
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { PanelSection, Input, Button } from '@ohif/ui';
import { DicomMetadataStore } from '@ohif/core';
import { useTranslation } from 'react-i18next';
import { Separator } from '@ohif/ui-next';
const DEFAULT_MEATADATA = {
PatientWeight: null,
@@ -22,7 +23,13 @@ const DEFAULT_MEATADATA = {
* @param param0
* @returns
*/
export default function PanelPetSUV({ servicesManager, commandsManager }: withAppTypes) {
export default function PanelPetSUV({
servicesManager,
commandsManager,
renderHeader,
getCloseIcon,
tab,
}: withAppTypes) {
const { t } = useTranslation('PanelSUV');
const { displaySetService, toolGroupService, toolbarService, hangingProtocolService } =
servicesManager.services;
@@ -125,104 +132,127 @@ export default function PanelPetSUV({ servicesManager, commandsManager }: withAp
}, 0);
}
return (
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
<div className="flex min-h-0 flex-1 flex-col bg-black text-[13px] font-[300]">
<PanelSection title={t('Patient Information')}>
<div className="flex flex-col">
<div className="bg-primary-dark flex flex-col gap-4 p-2">
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Patient Sex')}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.PatientSex || ''}
onChange={e => {
handleMetadataChange({
PatientSex: e.target.value,
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Weight')}
labelChildren={<span className="text-aqua-pale"> kg</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.PatientWeight || ''}
onChange={e => {
handleMetadataChange({
PatientWeight: e.target.value,
});
}}
id="weight-input"
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Total Dose')}
labelChildren={<span className="text-aqua-pale"> bq</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''}
onChange={e => {
handleMetadataChange({
RadiopharmaceuticalInformationSequence: {
RadionuclideTotalDose: e.target.value,
},
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Half Life')}
labelChildren={<span className="text-aqua-pale"> s</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''}
onChange={e => {
handleMetadataChange({
RadiopharmaceuticalInformationSequence: {
RadionuclideHalfLife: e.target.value,
},
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Injection Time')}
labelChildren={<span className="text-aqua-pale"> s</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={
metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || ''
}
onChange={e => {
handleMetadataChange({
RadiopharmaceuticalInformationSequence: {
RadiopharmaceuticalStartTime: e.target.value,
},
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Acquisition Time')}
labelChildren={<span className="text-aqua-pale"> s</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.SeriesTime || ''}
onChange={() => {}}
/>
<Button
className="!h-[26px] !w-[115px] self-end !p-0"
onClick={updateMetadata}
>
Reload Data
</Button>
<>
{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>
</PanelSection>
<Separator
orientation="horizontal"
className="bg-black"
thickness="2px"
/>
</>
)}
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
<div className="flex min-h-0 flex-1 flex-col bg-black text-[13px] font-[300]">
<PanelSection title={t('Patient Information')}>
<div className="flex flex-col">
<div className="bg-primary-dark flex flex-col gap-4 p-2">
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Patient Sex')}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.PatientSex || ''}
onChange={e => {
handleMetadataChange({
PatientSex: e.target.value,
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Weight')}
labelChildren={<span className="text-aqua-pale"> kg</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.PatientWeight || ''}
onChange={e => {
handleMetadataChange({
PatientWeight: e.target.value,
});
}}
id="weight-input"
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Total Dose')}
labelChildren={<span className="text-aqua-pale"> bq</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={
metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''
}
onChange={e => {
handleMetadataChange({
RadiopharmaceuticalInformationSequence: {
RadionuclideTotalDose: e.target.value,
},
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Half Life')}
labelChildren={<span className="text-aqua-pale"> s</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''}
onChange={e => {
handleMetadataChange({
RadiopharmaceuticalInformationSequence: {
RadionuclideHalfLife: e.target.value,
},
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Injection Time')}
labelChildren={<span className="text-aqua-pale"> s</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={
metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime ||
''
}
onChange={e => {
handleMetadataChange({
RadiopharmaceuticalInformationSequence: {
RadiopharmaceuticalStartTime: e.target.value,
},
});
}}
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}
label={t('Acquisition Time')}
labelChildren={<span className="text-aqua-pale"> s</span>}
labelClassName="text-[13px] font-inter text-white"
className="!m-0 !h-[26px] !w-[117px]"
value={metadata.SeriesTime || ''}
onChange={() => {}}
/>
<Button
className="!h-[26px] !w-[115px] self-end !p-0"
onClick={updateMetadata}
>
Reload Data
</Button>
</div>
</div>
</PanelSection>
</div>
</div>
</div>
</>
);
}
+16 -3
View File
@@ -1,6 +1,8 @@
import React from 'react';
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
import { Toolbox } from '@ohif/ui';
import { Toolbox as NewToolbox } from '@ohif/ui-next';
import { Toolbox as OldToolbox } from '@ohif/ui';
import { useAppConfig } from '@state';
// TODO:
// - No loading UI exists yet
@@ -8,16 +10,24 @@ import { Toolbox } from '@ohif/ui';
// - show errors in UI for thumbnails if promise fails
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
const wrappedPanelPetSuv = () => {
const wrappedPanelPetSuv = ({ renderHeader, getCloseIcon, tab }) => {
return (
<PanelPetSUV
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
);
};
const wrappedROIThresholdToolbox = () => {
const wrappedROIThresholdToolbox = ({ renderHeader, getCloseIcon, tab }: withAppTypes) => {
const [appConfig] = useAppConfig();
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
return (
<>
<Toolbox
@@ -26,6 +36,9 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
extensionManager={extensionManager}
buttonSectionId="ROIThresholdToolbox"
title="Threshold Tools"
renderHeader={renderHeader}
getCloseIcon={getCloseIcon}
tab={tab}
/>
</>
);