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,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'}
|
||||
|
||||
+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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
+75
-17
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+72
@@ -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 };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import type { actionIcon } from '../PanelStudyBrowserTracking/types/actionsIcon';
|
||||
|
||||
const defaultActionIcons = [
|
||||
{
|
||||
id: 'settings',
|
||||
iconName: 'Settings',
|
||||
value: false,
|
||||
},
|
||||
] as actionIcon[];
|
||||
|
||||
export { defaultActionIcons };
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { defaultActionIcons } from './actionIcons';
|
||||
import { defaultViewPresets } from './viewPresets';
|
||||
|
||||
export { defaultActionIcons, defaultViewPresets };
|
||||
+16
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+7
@@ -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 };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
type viewPreset = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type { viewPreset };
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user