feat(new-study-panel): default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser (#4417)

This commit is contained in:
Ibrahim authored and GitHub committed 2024-10-18 11:42:44 -04:00
1 parent 1d0259451f
commit a7fd9fa5bf
74 files changed
+361 -478

No files matched your search

-1
View File
@@ -16,7 +16,6 @@ window.config = {
experimentalStudyBrowserSort: false,
strictZSpacingForVolumeViewport: true,
groupEnabledModesFirst: true,
useExperimentalUI: false,
maxNumRequests: {
interaction: 100,
thumbnail: 75,
-1
View File
@@ -8,7 +8,6 @@ window.config = {
// below flag is for performance reasons, but it might not work for all servers
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
useExperimentalUI: false,
showLoadingIndicator: true,
experimentalStudyBrowserSort: false,
strictZSpacingForVolumeViewport: true,
@@ -156,6 +156,12 @@ export default class CustomizationService extends PubSubService {
public onModeEnter(): void {
super.reset();
const modeCustomizationKeys = Array.from(this.modeCustomizations.keys());
for (const key of modeCustomizationKeys) {
this.transformedCustomizations.delete(key);
}
this.modeCustomizations.clear();
}
@@ -313,7 +319,12 @@ export default class CustomizationService extends PubSubService {
return newValue;
}
const returnValue = mergeWith({}, oldValue, newValue, mergeType === MergeEnum.Append ? appendCustomizer : mergeCustomizer);
const returnValue = mergeWith(
{},
oldValue,
newValue,
mergeType === MergeEnum.Append ? appendCustomizer : mergeCustomizer
);
return returnValue;
}
@@ -426,7 +437,12 @@ function appendCustomizer(obj, src) {
const { position, isMerge } = findPosition(key, value, newList);
if (isMerge) {
if (typeof obj[position] === 'object') {
newList[position] = mergeWith(Array.isArray(newList[position]) ? [] : {}, newList[position], value, appendCustomizer);
newList[position] = mergeWith(
Array.isArray(newList[position]) ? [] : {},
newList[position],
value,
appendCustomizer
);
} else {
newList[position] = value;
}
@@ -455,7 +471,7 @@ function findPosition(key, value, newList) {
return { isMerge: true, position: (numVal + len) % len };
}
const absPosition = Math.ceil(numVal < 0 ? len + numVal : numVal);
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) }
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) };
}
const findIndex = newList.findIndex(it => it.id === key);
if (findIndex !== -1) {
@@ -467,7 +483,7 @@ function findPosition(key, value, newList) {
return { isMerge: true, position: (priority + len) % len };
}
const absPosition = Math.ceil(priority < 0 ? len + priority : priority);
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) }
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) };
}
return { isMerge: false, position: len };
}
@@ -2128,6 +2128,81 @@ export const Icons = {
</svg>
),
DicomTagBrowser: (props: IconProps) => (
<svg
width="24px"
height="24px"
viewBox="0 0 28 28"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<title>tool-dicom-tag-browser</title>
<g
id="tool-dicom-tag-browser"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<rect
id="Rectangle"
x="0"
y="0"
width="28"
height="28"
></rect>
<g
id="Group"
transform="translate(4, 5.5)"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
>
<circle
id="Oval"
cx="1.73913043"
cy="1.73913043"
r="1.73913043"
></circle>
<line
x1="6.95652174"
y1="1.73913043"
x2="20"
y2="1.73913043"
id="Path"
></line>
<circle
id="Oval"
cx="1.73913043"
cy="8.69565217"
r="1.73913043"
></circle>
<line
x1="6.95652174"
y1="8.69565217"
x2="20"
y2="8.69565217"
id="Path"
></line>
<circle
id="Oval"
cx="1.73913043"
cy="15.6521739"
r="1.73913043"
></circle>
<line
x1="6.95652174"
y1="15.6521739"
x2="20"
y2="15.6521739"
id="Path"
></line>
</g>
</g>
</svg>
),
// Aliases
'tab-segmentation': (props: IconProps) => Icons.TabSegmentation(props),
'tab-studies': (props: IconProps) => Icons.TabStudies(props),
@@ -153,7 +153,6 @@ const SidePanel = ({
onActiveTabIndexChange,
}) => {
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [renderHeader, setRenderHeader] = useState(false);
const [activeTabIndex, setActiveTabIndex] = useState(0);
const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth);
@@ -190,9 +189,6 @@ const SidePanel = ({
[onActiveTabIndexChange, updatePanelOpen]
);
useEffect(() => {
setRenderHeader(tabs.length === 1);
}, [tabs]);
useEffect(() => {
updateActiveTabIndex(activeTabIndexProp);
}, [activeTabIndexProp, updateActiveTabIndex]);
@@ -262,7 +258,7 @@ const SidePanel = ({
return (
<div
className={classnames(
'absolute flex h-[24px] cursor-pointer items-center justify-center',
'absolute flex cursor-pointer items-center justify-center',
side === 'left' ? 'right-0' : 'left-0'
)}
style={{ width: `${closeIconWidth}px` }}
@@ -284,7 +280,6 @@ const SidePanel = ({
return (
<>
{getCloseIcon()}
<div className={classnames('flex grow justify-center')}>
<div className={classnames('bg-primary-dark text-primary-active flex flex-wrap')}>
{tabs.map((tab, tabIndex) => {
@@ -351,15 +346,26 @@ const SidePanel = ({
);
};
const getOpenStateComponent = () => {
if (tabs.length === 1) {
return null;
}
const getOneTabComponent = () => {
return (
<div
className={classnames(
'text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]'
)}
data-cy={`${tabs[0].name}-btn`}
onClick={() => updatePanelOpen(!panelOpen)}
>
{getCloseIcon()}
<span>{tabs[0].label}</span>
</div>
);
};
const getOpenStateComponent = () => {
return (
<>
<div className="bg-bkg-med flex h-[40px] select-none rounded-t p-2">
{getTabGridComponent()}
{tabs.length === 1 ? getOneTabComponent() : getTabGridComponent()}
</div>
<Separator
orientation="horizontal"
@@ -380,14 +386,7 @@ const SidePanel = ({
{getOpenStateComponent()}
{tabs.map((tab, tabIndex) => {
if (tabIndex === activeTabIndex) {
return (
<tab.content
key={tabIndex}
getCloseIcon={getCloseIcon}
tab={tab}
renderHeader={renderHeader}
/>
);
return <tab.content key={tabIndex} />;
}
return null;
})}
@@ -31,6 +31,7 @@ const StudyBrowser = ({
servicesManager,
showSettings,
viewPresets,
onThumbnailContextMenu,
}: withAppTypes) => {
const getTabContent = () => {
const tabData = tabs.find(tab => tab.name === activeTabName);
@@ -60,6 +61,7 @@ const StudyBrowser = ({
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
data-cy="thumbnail-list"
viewPreset={viewPreset}
onThumbnailContextMenu={onThumbnailContextMenu}
/>
</React.Fragment>
);
@@ -19,7 +19,8 @@ const StudyItem = ({
onDoubleClickThumbnail,
onClickUntrack,
viewPreset = 'thumbnails',
}) => {
onThumbnailContextMenu,
}: withAppTypes) => {
return (
<Accordion
type="single"
@@ -36,12 +37,12 @@ const StudyItem = ({
<div className="flex w-full flex-row items-center justify-between">
<div className="flex flex-col items-start text-[13px]">
<div className="text-white">{date}</div>
<div className="text-muted-foreground max-w-[160px] overflow-hidden truncate whitespace-nowrap">
<div className="text-muted-foreground h-[18px] max-w-[160px] overflow-hidden truncate whitespace-nowrap">
{description}
</div>
</div>
<div className="text-muted-foreground mr-2 flex flex-col items-end text-[12px]">
<div>{modalities}</div>
<div className="max-w-[150px] overflow-hidden text-ellipsis">{modalities}</div>
<div>{numInstances}</div>
</div>
</div>
@@ -60,6 +61,7 @@ const StudyItem = ({
onThumbnailDoubleClick={onDoubleClickThumbnail}
onClickUntrack={onClickUntrack}
viewPreset={viewPreset}
onThumbnailContextMenu={onThumbnailContextMenu}
/>
)}
</AccordionContent>
@@ -5,6 +5,13 @@ import { useDrag } from 'react-dnd';
import { Icons } from '../Icons';
import { DisplaySetMessageListTooltip } from '../DisplaySetMessageListTooltip';
import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip';
import { Button } from '../Button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../DropdownMenu';
/**
* Display a thumbnail for a display set.
@@ -30,8 +37,10 @@ const Thumbnail = ({
canReject = false,
onReject = () => {},
isTracked = false,
thumbnailType = 'thumbnail',
onClickUntrack = () => {},
}): React.ReactNode => {
onThumbnailContextMenu,
}: withAppTypes): React.ReactNode => {
// TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as
// this will still allow for "drag", even if there is no drop target for the
// specified item.
@@ -126,10 +135,42 @@ const Thumbnail = ({
</TooltipProvider>
)}
</div>
{/* bottom right */}
<div className="absolute bottom-0 right-0 flex items-center gap-[4px] p-[4px]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hidden group-hover:inline-flex data-[state=open]:inline-flex"
>
<Icons.More />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
asChild
hideWhenDetached
>
<DropdownMenuItem
onSelect={() => {
onThumbnailContextMenu('openDICOMTagViewer', {
displaySetInstanceUID,
});
}}
className="gap-[6px]"
>
<Icons.DicomTagBrowser />
Tag Browser
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<div className="flex h-[52px] w-[128px] flex-col">
<div className="text-[12px] text-white">{description}</div>
<div className="min-h-[18px] w-[128px] overflow-hidden text-ellipsis text-[12px] text-white">
{description}
</div>
<div className="flex h-[12px] items-center gap-[7px] overflow-hidden">
<div className="text-muted-foreground text-[12px]"> S:{seriesNumber}</div>
<div className="text-muted-foreground text-[12px]">
@@ -225,6 +266,33 @@ const Thumbnail = ({
</Tooltip>
</TooltipProvider>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hidden group-hover:inline-flex data-[state=open]:inline-flex"
>
<Icons.More />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
asChild
hideWhenDetached
>
<DropdownMenuItem
onSelect={() => {
onThumbnailContextMenu('openDICOMTagViewer', {
displaySetInstanceUID,
});
}}
className="gap-[6px]"
>
<Icons.DicomTagBrowser />
Tag Browser
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
@@ -234,12 +302,16 @@ const Thumbnail = ({
<div
className={classnames(
className,
'bg-muted hover:bg-primary/30 flex cursor-pointer select-none flex-col outline-none',
'bg-muted hover:bg-primary/30 group flex cursor-pointer select-none flex-col outline-none',
viewPreset === 'thumbnails' && 'h-[170px] w-[135px]',
viewPreset === 'list' && 'h-[40px] w-[275px]'
viewPreset === 'list' && 'col-span-2 h-[40px] w-[275px]'
)}
id={`thumbnail-${displaySetInstanceUID}`}
data-cy={`study-browser-thumbnail`}
data-cy={
thumbnailType === 'thumbnailNoImage'
? 'study-browser-thumbnail-no-image'
: 'study-browser-thumbnail'
}
data-series={seriesNumber}
onClick={onClick}
onDoubleClick={onDoubleClick}
@@ -289,6 +361,7 @@ Thumbnail.propTypes = {
isTracked: PropTypes.bool,
onClickUntrack: PropTypes.func,
countIcon: PropTypes.string,
thumbnailType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage']),
};
export { Thumbnail };
@@ -10,103 +10,70 @@ const ThumbnailList = ({
onClickUntrack,
activeDisplaySetInstanceUIDs = [],
viewPreset,
}) => {
onThumbnailContextMenu,
}: withAppTypes) => {
return (
<div
id="ohif-thumbnail-list"
className={`ohif-scrollbar bg-bkg-low grid place-items-center overflow-y-hidden pt-[4px] pr-[2.5px] pl-[2.5px] ${viewPreset === 'thumbnails' ? 'grid-cols-2 gap-[4px] pb-[12px]' : 'grid-cols-1 gap-[2px]'}`}
className="min-h-[350px]"
style={{
'--radix-accordion-content-height': '350px',
}}
>
{thumbnails.map(
({
displaySetInstanceUID,
description,
dragData,
seriesNumber,
numInstances,
loadingProgress,
modality,
componentType,
seriesDate,
countIcon,
isTracked,
canReject,
onReject,
imageSrc,
messages,
imageAltText,
isHydratedForDerivedDisplaySet,
}) => {
const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID);
switch (componentType) {
case 'thumbnail':
return (
<Thumbnail
key={displaySetInstanceUID}
displaySetInstanceUID={displaySetInstanceUID}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances || 1}
countIcon={countIcon}
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
viewPreset={viewPreset}
modality={modality}
/>
);
case 'thumbnailTracked':
return (
<Thumbnail
key={displaySetInstanceUID}
displaySetInstanceUID={displaySetInstanceUID}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances}
loadingProgress={loadingProgress}
countIcon={countIcon}
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
isTracked={isTracked}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
viewPreset={viewPreset}
modality={modality}
/>
);
case 'thumbnailNoImage':
return (
<Thumbnail
isActive={isActive}
key={displaySetInstanceUID}
displaySetInstanceUID={displaySetInstanceUID}
dragData={dragData}
modality={modality}
messages={messages}
description={description}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
viewPreset={viewPreset}
countIcon={countIcon}
seriesNumber={seriesNumber}
numInstances={numInstances || 1}
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
canReject={canReject}
onReject={onReject}
/>
);
default:
return <></>;
<div
id="ohif-thumbnail-list"
className={`ohif-scrollbar bg-bkg-low grid place-items-center overflow-y-hidden pt-[4px] pr-[2.5px] pl-[2.5px] ${viewPreset === 'thumbnails' ? 'grid-cols-2 gap-[4px] pb-[12px]' : 'grid-cols-1 gap-[2px]'}`}
>
{thumbnails.map(
({
displaySetInstanceUID,
description,
dragData,
seriesNumber,
numInstances,
loadingProgress,
modality,
componentType,
seriesDate,
countIcon,
isTracked,
canReject,
onReject,
imageSrc,
messages,
imageAltText,
isHydratedForDerivedDisplaySet,
}) => {
const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID);
return (
<Thumbnail
key={displaySetInstanceUID}
displaySetInstanceUID={displaySetInstanceUID}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances || 1}
countIcon={countIcon}
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
isActive={isActive}
modality={modality}
viewPreset={componentType === 'thumbnailNoImage' ? 'list' : viewPreset}
thumbnailType={componentType}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
isTracked={isTracked}
loadingProgress={loadingProgress}
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
canReject={canReject}
onReject={onReject}
onThumbnailContextMenu={onThumbnailContextMenu}
/>
);
}
}
)}
)}
</div>
</div>
);
};
@@ -20,9 +20,6 @@ function Toolbox({
buttonSectionId,
commandsManager,
title,
renderHeader,
getCloseIcon,
tab,
...props
}: withAppTypes) {
const { state: toolboxState, api } = useToolbox(buttonSectionId);
@@ -149,24 +146,6 @@ function Toolbox({
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"
/>
</>
)}
<ToolboxUI
{...props}
title={title}