feat(panels): responsive thumbnails based on panel size (#4723)

This commit is contained in:
Ibrahim 2025-01-23 14:13:44 -05:00 committed by GitHub
parent 9d07705a1f
commit d9abc3da8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 282 additions and 176 deletions

View File

@ -1,22 +1,6 @@
import { useState, useCallback, useLayoutEffect, useRef } from 'react'; import { useState, useCallback, useLayoutEffect, useRef } from 'react';
import { getPanelElement, getPanelGroupElement } from 'react-resizable-panels'; import { getPanelElement, getPanelGroupElement } from 'react-resizable-panels';
import { panelGroupDefinition } from './constants/panels';
// Id needed to grab the panel group for converting pixels to percentages
const viewerLayoutResizablePanelGroupId = 'viewerLayoutResizablePanelGroup';
const viewerLayoutResizableLeftPanelId = 'viewerLayoutResizableLeftPanel';
const viewerLayoutResizableRightPanelId = 'viewerLayoutResizableRightPanel';
const sidePanelExpandedDefaultWidth = 280;
const sidePanelExpandedInsideBorderSize = 4;
const sidePanelExpandedDefaultOffsetWidth =
sidePanelExpandedDefaultWidth + sidePanelExpandedInsideBorderSize;
const sidePanelCollapsedInsideBorderSize = 4;
const sidePanelCollapsedOutsideBorderSize = 8;
const sidePanelCollapsedWidth = 25;
const sidePanelCollapsedOffsetWidth =
sidePanelCollapsedWidth +
sidePanelCollapsedInsideBorderSize +
sidePanelCollapsedOutsideBorderSize;
/** /**
* Set the minimum and maximum css style width attributes for the given element. * Set the minimum and maximum css style width attributes for the given element.
@ -43,15 +27,15 @@ const useResizablePanels = (
setRightPanelClosed setRightPanelClosed
) => { ) => {
const [leftPanelExpandedWidth, setLeftPanelExpandedWidth] = useState( const [leftPanelExpandedWidth, setLeftPanelExpandedWidth] = useState(
sidePanelExpandedDefaultWidth panelGroupDefinition.left.initialExpandedWidth
); );
const [rightPanelExpandedWidth, setRightPanelExpandedWidth] = useState( const [rightPanelExpandedWidth, setRightPanelExpandedWidth] = useState(
sidePanelExpandedDefaultWidth panelGroupDefinition.right.initialExpandedWidth
); );
const [leftResizablePanelMinimumSize, setLeftResizablePanelMinimumSize] = useState(0);
// Percentage sizes. const [rightResizablePanelMinimumSize, setRightResizablePanelMinimumSize] = useState(0);
const [resizablePanelCollapsedSize, setResizablePanelCollapsedSize] = useState(0); const [leftResizeablePanelCollapsedSize, setLeftResizePanelCollapsedSize] = useState(0);
const [resizablePanelDefaultSize, setResizablePanelDefaultSize] = useState(0); const [rightResizePanelCollapsedSize, setRightResizePanelCollapsedSize] = useState(0);
const resizablePanelGroupElemRef = useRef(null); const resizablePanelGroupElemRef = useRef(null);
const resizableLeftPanelElemRef = useRef(null); const resizableLeftPanelElemRef = useRef(null);
@ -65,30 +49,33 @@ const useResizablePanels = (
// converting between percentages and pixels in various callbacks. // converting between percentages and pixels in various callbacks.
// - Expand those panels that are initially expanded. // - Expand those panels that are initially expanded.
useLayoutEffect(() => { useLayoutEffect(() => {
const panelGroupElem = getPanelGroupElement(viewerLayoutResizablePanelGroupId); const panelGroupElem = getPanelGroupElement(panelGroupDefinition.groupId);
resizablePanelGroupElemRef.current = panelGroupElem; resizablePanelGroupElemRef.current = panelGroupElem;
const { width: panelGroupWidth } = panelGroupElem.getBoundingClientRect(); const { width: panelGroupWidth } = panelGroupElem.getBoundingClientRect();
const leftPanelElem = getPanelElement(viewerLayoutResizableLeftPanelId); const leftPanelElem = getPanelElement(panelGroupDefinition.left.panelId);
resizableLeftPanelElemRef.current = leftPanelElem; resizableLeftPanelElemRef.current = leftPanelElem;
const rightPanelElem = getPanelElement(viewerLayoutResizableRightPanelId); const rightPanelElem = getPanelElement(panelGroupDefinition.right.panelId);
resizableRightPanelElemRef.current = rightPanelElem; resizableRightPanelElemRef.current = rightPanelElem;
const resizablePanelExpandedSize =
(sidePanelExpandedDefaultOffsetWidth / panelGroupWidth) * 100;
// Since both resizable panels are collapsed by default (i.e. their default size is zero), // Since both resizable panels are collapsed by default (i.e. their default size is zero),
// on the very first render check if either/both side panels should be expanded. // on the very first render check if either/both side panels should be expanded.
// we use the initialExpandedOffsetWidth on the first render incase the panel has min width but we want the initial state to be larger than that
if (!leftPanelClosed) { if (!leftPanelClosed) {
resizableLeftPanelAPIRef?.current?.expand(resizablePanelExpandedSize); const leftResizablePanelExpandedSize =
setMinMaxWidth(leftPanelElem, sidePanelExpandedDefaultOffsetWidth); (panelGroupDefinition.left.initialExpandedOffsetWidth / panelGroupWidth) * 100;
resizableLeftPanelAPIRef?.current?.expand(leftResizablePanelExpandedSize);
setMinMaxWidth(leftPanelElem, panelGroupDefinition.left.initialExpandedOffsetWidth);
} }
if (!rightPanelClosed) { if (!rightPanelClosed) {
resizableRightPanelAPIRef?.current?.expand(resizablePanelExpandedSize); const rightResizablePanelExpandedSize =
setMinMaxWidth(rightPanelElem, sidePanelExpandedDefaultOffsetWidth); (panelGroupDefinition.right.initialExpandedOffsetWidth / panelGroupWidth) * 100;
resizableRightPanelAPIRef?.current?.expand(rightResizablePanelExpandedSize);
setMinMaxWidth(rightPanelElem, panelGroupDefinition.right.initialExpandedOffsetWidth);
} }
}, []); // no dependencies because this useLayoutEffect is only needed on the very first render }, []); // no dependencies because this useLayoutEffect is only needed on the very first render
@ -114,13 +101,17 @@ const useResizablePanels = (
// for a panel. // for a panel.
if (!resizableLeftPanelAPIRef.current.isCollapsed()) { if (!resizableLeftPanelAPIRef.current.isCollapsed()) {
const leftSize = const leftSize =
((leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize) / panelGroupWidth) * 100; ((leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize) /
panelGroupWidth) *
100;
resizableLeftPanelAPIRef.current.resize(leftSize); resizableLeftPanelAPIRef.current.resize(leftSize);
} }
if (!resizableRightPanelAPIRef.current.isCollapsed()) { if (!resizableRightPanelAPIRef.current.isCollapsed()) {
const rightSize = const rightSize =
((rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize) / panelGroupWidth) * 100; ((rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize) /
panelGroupWidth) *
100;
resizableRightPanelAPIRef.current.resize(rightSize); resizableRightPanelAPIRef.current.resize(rightSize);
} }
@ -128,11 +119,20 @@ const useResizablePanels = (
// component is resized. This typically occurs when the browser window resizes. // component is resized. This typically occurs when the browser window resizes.
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
const defaultSize = (sidePanelExpandedDefaultOffsetWidth / panelGroupWidth) * 100; const minimumLeftSize =
(panelGroupDefinition.left.minimumExpandedOffsetWidth / panelGroupWidth) * 100;
const minimumRightSize =
(panelGroupDefinition.right.minimumExpandedOffsetWidth / panelGroupWidth) * 100;
// Set the new default and collapsed resizable panel sizes. // Set the new default and collapsed resizable panel sizes.
setResizablePanelDefaultSize(Math.min(50, defaultSize)); setLeftResizablePanelMinimumSize(Math.min(50, minimumLeftSize));
setResizablePanelCollapsedSize((sidePanelCollapsedOffsetWidth / panelGroupWidth) * 100); setRightResizablePanelMinimumSize(Math.min(50, minimumRightSize));
setLeftResizePanelCollapsedSize(
(panelGroupDefinition.left.collapsedOffsetWidth / panelGroupWidth) * 100
);
setRightResizePanelCollapsedSize(
(panelGroupDefinition.right.collapsedOffsetWidth / panelGroupWidth) * 100
);
if ( if (
resizableLeftPanelAPIRef.current.isCollapsed() && resizableLeftPanelAPIRef.current.isCollapsed() &&
@ -146,12 +146,12 @@ const useResizablePanels = (
// Determine the current widths of the two side panels. // Determine the current widths of the two side panels.
let leftPanelOffsetWidth = resizableLeftPanelAPIRef.current.isCollapsed() let leftPanelOffsetWidth = resizableLeftPanelAPIRef.current.isCollapsed()
? sidePanelCollapsedOffsetWidth ? panelGroupDefinition.left.collapsedOffsetWidth
: leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize; : leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize;
let rightPanelOffsetWidth = resizableRightPanelAPIRef.current.isCollapsed() let rightPanelOffsetWidth = resizableRightPanelAPIRef.current.isCollapsed()
? sidePanelCollapsedOffsetWidth ? panelGroupDefinition.right.collapsedOffsetWidth
: rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize; : rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize;
if ( if (
!resizableLeftPanelAPIRef.current.isCollapsed() && !resizableLeftPanelAPIRef.current.isCollapsed() &&
@ -162,9 +162,11 @@ const useResizablePanels = (
// Reduce the left panel width so that both panels might fit. // Reduce the left panel width so that both panels might fit.
leftPanelOffsetWidth = Math.max( leftPanelOffsetWidth = Math.max(
panelGroupWidth - rightPanelOffsetWidth, panelGroupWidth - rightPanelOffsetWidth,
sidePanelExpandedDefaultOffsetWidth panelGroupDefinition.left.minimumExpandedOffsetWidth
);
setLeftPanelExpandedWidth(
leftPanelOffsetWidth - panelGroupDefinition.shared.expandedInsideBorderSize
); );
setLeftPanelExpandedWidth(leftPanelOffsetWidth - sidePanelExpandedInsideBorderSize);
setMinMaxWidth(resizableLeftPanelElemRef.current, leftPanelOffsetWidth); setMinMaxWidth(resizableLeftPanelElemRef.current, leftPanelOffsetWidth);
} }
@ -177,9 +179,11 @@ const useResizablePanels = (
// Reduce the right panel width so that both panels might fit. // Reduce the right panel width so that both panels might fit.
rightPanelOffsetWidth = Math.max( rightPanelOffsetWidth = Math.max(
panelGroupWidth - leftPanelOffsetWidth, panelGroupWidth - leftPanelOffsetWidth,
sidePanelExpandedDefaultOffsetWidth panelGroupDefinition.right.minimumExpandedOffsetWidth
);
setRightPanelExpandedWidth(
rightPanelOffsetWidth - panelGroupDefinition.shared.expandedInsideBorderSize
); );
setRightPanelExpandedWidth(rightPanelOffsetWidth - sidePanelExpandedInsideBorderSize);
setMinMaxWidth(resizableRightPanelElemRef.current, rightPanelOffsetWidth); setMinMaxWidth(resizableRightPanelElemRef.current, rightPanelOffsetWidth);
} }
}); });
@ -189,7 +193,12 @@ const useResizablePanels = (
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [leftPanelExpandedWidth, resizablePanelDefaultSize, rightPanelExpandedWidth]); }, [
leftPanelExpandedWidth,
rightPanelExpandedWidth,
leftResizablePanelMinimumSize,
rightResizablePanelMinimumSize,
]);
/** /**
* Handles dragging of either side panel resize handle. * Handles dragging of either side panel resize handle.
@ -207,14 +216,14 @@ const useResizablePanels = (
if (resizableLeftPanelAPIRef?.current?.isExpanded()) { if (resizableLeftPanelAPIRef?.current?.isExpanded()) {
setMinMaxWidth( setMinMaxWidth(
resizableLeftPanelElemRef.current, resizableLeftPanelElemRef.current,
leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
); );
} }
if (resizableRightPanelAPIRef?.current?.isExpanded()) { if (resizableRightPanelAPIRef?.current?.isExpanded()) {
setMinMaxWidth( setMinMaxWidth(
resizableRightPanelElemRef.current, resizableRightPanelElemRef.current,
rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
); );
} }
} }
@ -229,15 +238,12 @@ const useResizablePanels = (
}, [setLeftPanelClosed]); }, [setLeftPanelClosed]);
const onLeftPanelOpen = useCallback(() => { const onLeftPanelOpen = useCallback(() => {
resizableLeftPanelAPIRef?.current?.expand(); const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
if (!isResizableHandleDraggingRef.current) { resizableLeftPanelAPIRef?.current?.expand(
setMinMaxWidth( (panelGroupDefinition.left.initialExpandedOffsetWidth / panelGroupWidth) * 100
resizableLeftPanelElemRef.current,
leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize
); );
}
setLeftPanelClosed(false); setLeftPanelClosed(false);
}, [leftPanelExpandedWidth, setLeftPanelClosed]); }, [setLeftPanelClosed]);
const onLeftPanelResize = useCallback(size => { const onLeftPanelResize = useCallback(size => {
if (resizableLeftPanelAPIRef.current.isCollapsed()) { if (resizableLeftPanelAPIRef.current.isCollapsed()) {
@ -245,7 +251,10 @@ const useResizablePanels = (
} }
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
setLeftPanelExpandedWidth((size / 100) * panelGroupWidth - sidePanelExpandedInsideBorderSize); const newExpandedWidth =
(size / 100) * panelGroupWidth - panelGroupDefinition.shared.expandedInsideBorderSize;
setLeftPanelExpandedWidth(newExpandedWidth);
}, []); }, []);
const onRightPanelClose = useCallback(() => { const onRightPanelClose = useCallback(() => {
@ -255,68 +264,69 @@ const useResizablePanels = (
}, [setRightPanelClosed]); }, [setRightPanelClosed]);
const onRightPanelOpen = useCallback(() => { const onRightPanelOpen = useCallback(() => {
resizableRightPanelAPIRef?.current?.expand(); const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
if (!isResizableHandleDraggingRef.current) { resizableRightPanelAPIRef?.current?.expand(
setMinMaxWidth( (panelGroupDefinition.right.initialExpandedOffsetWidth / panelGroupWidth) * 100
resizableRightPanelElemRef.current,
rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize
); );
}
setRightPanelClosed(false); setRightPanelClosed(false);
}, [rightPanelExpandedWidth, setRightPanelClosed]); }, [setRightPanelClosed]);
const onRightPanelResize = useCallback(size => { const onRightPanelResize = useCallback(size => {
if (resizableRightPanelAPIRef?.current?.isCollapsed()) { if (resizableRightPanelAPIRef?.current?.isCollapsed()) {
return; return;
} }
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
setRightPanelExpandedWidth((size / 100) * panelGroupWidth - sidePanelExpandedInsideBorderSize); const newExpandedWidth =
(size / 100) * panelGroupWidth - panelGroupDefinition.shared.expandedInsideBorderSize;
setRightPanelExpandedWidth(newExpandedWidth);
}, []); }, []);
return [ return [
{ {
expandedWidth: leftPanelExpandedWidth, expandedWidth: leftPanelExpandedWidth,
collapsedWidth: sidePanelCollapsedWidth, collapsedWidth: panelGroupDefinition.shared.collapsedWidth,
collapsedInsideBorderSize: sidePanelCollapsedInsideBorderSize, collapsedInsideBorderSize: panelGroupDefinition.shared.collapsedInsideBorderSize,
collapsedOutsideBorderSize: sidePanelCollapsedOutsideBorderSize, collapsedOutsideBorderSize: panelGroupDefinition.shared.collapsedOutsideBorderSize,
expandedInsideBorderSize: sidePanelExpandedInsideBorderSize, expandedInsideBorderSize: panelGroupDefinition.shared.expandedInsideBorderSize,
onClose: onLeftPanelClose, onClose: onLeftPanelClose,
onOpen: onLeftPanelOpen, onOpen: onLeftPanelOpen,
}, },
{ {
expandedWidth: rightPanelExpandedWidth, expandedWidth: rightPanelExpandedWidth,
collapsedWidth: sidePanelCollapsedWidth, collapsedWidth: panelGroupDefinition.shared.collapsedWidth,
collapsedInsideBorderSize: sidePanelCollapsedInsideBorderSize, collapsedInsideBorderSize: panelGroupDefinition.shared.collapsedInsideBorderSize,
collapsedOutsideBorderSize: sidePanelCollapsedOutsideBorderSize, collapsedOutsideBorderSize: panelGroupDefinition.shared.collapsedOutsideBorderSize,
expandedInsideBorderSize: sidePanelExpandedInsideBorderSize, expandedInsideBorderSize: panelGroupDefinition.shared.expandedInsideBorderSize,
onClose: onRightPanelClose, onClose: onRightPanelClose,
onOpen: onRightPanelOpen, onOpen: onRightPanelOpen,
}, },
{ direction: 'horizontal', id: viewerLayoutResizablePanelGroupId }, { direction: 'horizontal', id: panelGroupDefinition.groupId },
{ {
defaultSize: resizablePanelDefaultSize, defaultSize: leftResizablePanelMinimumSize,
minSize: resizablePanelDefaultSize, minSize: leftResizablePanelMinimumSize,
onResize: onLeftPanelResize, onResize: onLeftPanelResize,
collapsible: true, collapsible: true,
collapsedSize: resizablePanelCollapsedSize, collapsedSize: leftResizeablePanelCollapsedSize,
onCollapse: () => setLeftPanelClosed(true), onCollapse: () => setLeftPanelClosed(true),
onExpand: () => setLeftPanelClosed(false), onExpand: () => setLeftPanelClosed(false),
ref: resizableLeftPanelAPIRef, ref: resizableLeftPanelAPIRef,
order: 0, order: 0,
id: viewerLayoutResizableLeftPanelId, id: panelGroupDefinition.left.panelId,
}, },
{ order: 1, id: 'viewerLayoutResizableViewportGridPanel' }, { order: 1, id: 'viewerLayoutResizableViewportGridPanel' },
{ {
defaultSize: resizablePanelDefaultSize, defaultSize: rightResizablePanelMinimumSize,
minSize: resizablePanelDefaultSize, minSize: rightResizablePanelMinimumSize,
onResize: onRightPanelResize, onResize: onRightPanelResize,
collapsible: true, collapsible: true,
collapsedSize: resizablePanelCollapsedSize, collapsedSize: rightResizePanelCollapsedSize,
onCollapse: () => setRightPanelClosed(true), onCollapse: () => setRightPanelClosed(true),
onExpand: () => setRightPanelClosed(false), onExpand: () => setRightPanelClosed(false),
ref: resizableRightPanelAPIRef, ref: resizableRightPanelAPIRef,
order: 2, order: 2,
id: viewerLayoutResizableRightPanelId, id: panelGroupDefinition.right.panelId,
}, },
onHandleDragging, onHandleDragging,
]; ];

View File

@ -0,0 +1,38 @@
const expandedInsideBorderSize = 4;
const collapsedInsideBorderSize = 4;
const collapsedOutsideBorderSize = 8;
const collapsedWidth = 25;
const rightPanelInitialExpandedWidth = 280;
const leftPanelInitialExpandedWidth = 282;
const panelGroupDefinition = {
groupId: 'viewerLayoutResizablePanelGroup',
shared: {
expandedInsideBorderSize,
collapsedInsideBorderSize,
collapsedOutsideBorderSize,
collapsedWidth,
},
left: {
// id
panelId: 'viewerLayoutResizableLeftPanel',
// expanded width
initialExpandedWidth: leftPanelInitialExpandedWidth,
// expanded width + expanded inside border
minimumExpandedOffsetWidth: 145 + expandedInsideBorderSize,
// initial expanded width
initialExpandedOffsetWidth: leftPanelInitialExpandedWidth + expandedInsideBorderSize,
// collapsed width + collapsed inside border + collapsed outside border
collapsedOffsetWidth: collapsedWidth + collapsedInsideBorderSize + collapsedOutsideBorderSize,
},
right: {
panelId: 'viewerLayoutResizableRightPanel',
initialExpandedWidth: rightPanelInitialExpandedWidth,
minimumExpandedOffsetWidth: rightPanelInitialExpandedWidth + expandedInsideBorderSize,
initialExpandedOffsetWidth: rightPanelInitialExpandedWidth + expandedInsideBorderSize,
collapsedOffsetWidth: collapsedWidth + collapsedInsideBorderSize + collapsedOutsideBorderSize,
},
};
export { panelGroupDefinition };

View File

@ -141,7 +141,9 @@ function modeFactory({ modeConfiguration }) {
props: { props: {
// TODO: Should be optional, or required to pass empty array for slots? // TODO: Should be optional, or required to pass empty array for slots?
leftPanels: [ohif.thumbnailList], leftPanels: [ohif.thumbnailList],
leftPanelResizable: true,
rightPanels: [ohif.measurements], rightPanels: [ohif.measurements],
rightPanelResizable: true,
viewports: [ viewports: [
{ {
namespace: cs3d.viewport, namespace: cs3d.viewport,

View File

@ -150,8 +150,10 @@ function modeFactory() {
// leftPanels: [ohif.thumbnailList], // leftPanels: [ohif.thumbnailList],
// rightPanels: [dicomSeg.panel, ohif.measurements], // rightPanels: [dicomSeg.panel, ohif.measurements],
leftPanels: [tracked.thumbnailList], leftPanels: [tracked.thumbnailList],
leftPanelResizable: true,
// Can use cornerstone.measurements for all measurements // Can use cornerstone.measurements for all measurements
rightPanels: [cornerstone.panel, tracked.measurements, cornerstone.measurements], rightPanels: [cornerstone.panel, tracked.measurements, cornerstone.measurements],
rightPanelResizable: true,
// rightPanelClosed: true, // optional prop to start with collapse panels // rightPanelClosed: true, // optional prop to start with collapse panels
viewports: [ viewports: [
{ {

View File

@ -87,9 +87,11 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout, id: ohif.layout,
props: { props: {
leftPanels: [ohif.leftPanel], leftPanels: [ohif.leftPanel],
leftPanelResizable: true,
leftPanelClosed: true, // we have problem with rendering thumbnails for microscopy images leftPanelClosed: true, // we have problem with rendering thumbnails for microscopy images
rightPanelClosed: true, // we do not have the save microscopy measurements yet rightPanelClosed: true, // we do not have the save microscopy measurements yet
rightPanels: ['@ohif/extension-dicom-microscopy.panelModule.measure'], rightPanels: ['@ohif/extension-dicom-microscopy.panelModule.measure'],
rightPanelResizable: true,
viewports: [ viewports: [
{ {
namespace: '@ohif/extension-dicom-microscopy.viewportModule.microscopy-dicom', namespace: '@ohif/extension-dicom-microscopy.viewportModule.microscopy-dicom',

View File

@ -158,7 +158,9 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout, id: ohif.layout,
props: { props: {
leftPanels: [[dynamicVolume.leftPanel, cornerstone.activeViewportWindowLevel]], leftPanels: [[dynamicVolume.leftPanel, cornerstone.activeViewportWindowLevel]],
leftPanelResizable: true,
rightPanels: [], rightPanels: [],
rightPanelResizable: true,
rightPanelClosed: true, rightPanelClosed: true,
viewports: [ viewports: [
{ {

View File

@ -132,7 +132,9 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout, id: ohif.layout,
props: { props: {
leftPanels: [ohif.leftPanel], leftPanels: [ohif.leftPanel],
leftPanelResizable: true,
rightPanels: [cornerstone.panelTool], rightPanels: [cornerstone.panelTool],
rightPanelResizable: true,
// leftPanelClosed: true, // leftPanelClosed: true,
viewports: [ viewports: [
{ {

View File

@ -215,8 +215,10 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout, id: ohif.layout,
props: { props: {
leftPanels: [ohif.thumbnailList], leftPanels: [ohif.thumbnailList],
leftPanelResizable: true,
leftPanelClosed: true, leftPanelClosed: true,
rightPanels: [tmtv.tmtv, tmtv.petSUV], rightPanels: [tmtv.tmtv, tmtv.petSUV],
rightPanelResizable: true,
viewports: [ viewports: [
{ {
namespace: cs3d.viewport, namespace: cs3d.viewport,

View File

@ -1,5 +1,6 @@
describe('OHIF Video Display', function () { describe('OHIF Video Display', function () {
beforeEach(function () { beforeEach(function () {
Cypress.on('uncaught:exception', () => false);
cy.openStudyInViewer('2.25.96975534054447904995905761963464388233'); cy.openStudyInViewer('2.25.96975534054447904995905761963464388233');
}); });

View File

@ -63,7 +63,7 @@ const StudyBrowser = ({
className="ohif-scrollbar invisible-scrollbar bg-bkg-low flex flex-1 flex-col gap-[4px] overflow-auto" className="ohif-scrollbar invisible-scrollbar bg-bkg-low flex flex-1 flex-col gap-[4px] overflow-auto"
data-cy={'studyBrowser-panel'} data-cy={'studyBrowser-panel'}
> >
<div> <div className="flex flex-col gap-[4px]">
{showSettings && ( {showSettings && (
<div className="w-100 bg-bkg-low flex h-[48px] items-center justify-center gap-[10px] px-[8px] py-[10px]"> <div className="w-100 bg-bkg-low flex h-[48px] items-center justify-center gap-[10px] px-[8px] py-[10px]">
<> <>

View File

@ -6,6 +6,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
} from '../DropdownMenu/DropdownMenu'; } from '../DropdownMenu/DropdownMenu';
import { Tooltip, TooltipContent, TooltipTrigger } from '../Tooltip';
export function StudyBrowserSort({ servicesManager }: withAppTypes) { export function StudyBrowserSort({ servicesManager }: withAppTypes) {
// Todo: this should not be here, no servicesManager should be in ui-next, only // Todo: this should not be here, no servicesManager should be in ui-next, only
@ -50,11 +51,16 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) {
}, [displaySetService, selectedSort, sortDirection]); }, [displaySetService, selectedSort, sortDirection]);
return ( return (
<div className="flex items-center gap-1"> <div className="flex w-[50%] items-center gap-1">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-start rounded border bg-black p-2 text-base text-white"> <Tooltip>
<TooltipTrigger className="w-full overflow-hidden">
<DropdownMenuTrigger className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-full items-center justify-start overflow-hidden whitespace-nowrap rounded border bg-black p-2 text-base text-white">
{selectedSort.label} {selectedSort.label}
</DropdownMenuTrigger> </DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>{selectedSort.label}</TooltipContent>
</Tooltip>
<DropdownMenuContent className="bg-black"> <DropdownMenuContent className="bg-black">
{sortFunctions.map(sort => ( {sortFunctions.map(sort => (
<DropdownMenuItem <DropdownMenuItem
@ -67,6 +73,8 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) {
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Tooltip>
<TooltipTrigger>
<button <button
onClick={toggleSortDirection} onClick={toggleSortDirection}
className="flex h-[26px] items-center justify-center bg-black" className="flex h-[26px] items-center justify-center bg-black"
@ -77,6 +85,9 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) {
<Icons.SortingDescending className="text-primary-main w-2" /> <Icons.SortingDescending className="text-primary-main w-2" />
)} )}
</button> </button>
</TooltipTrigger>
<TooltipContent>Sort direction</TooltipContent>
</Tooltip>
</div> </div>
); );
} }

View File

@ -5,6 +5,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
} from '../DropdownMenu/DropdownMenu'; } from '../DropdownMenu/DropdownMenu';
import { Tooltip, TooltipContent, TooltipTrigger } from '../Tooltip';
export function StudyBrowserViewOptions({ tabs, onSelectTab, activeTabName }: withAppTypes) { export function StudyBrowserViewOptions({ tabs, onSelectTab, activeTabName }: withAppTypes) {
const handleTabChange = (tabName: string) => { const handleTabChange = (tabName: string) => {
@ -15,9 +16,14 @@ export function StudyBrowserViewOptions({ tabs, onSelectTab, activeTabName }: wi
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-start rounded border bg-black p-2 text-base text-white"> <Tooltip>
<TooltipTrigger className="w-full w-[50%] overflow-hidden">
<DropdownMenuTrigger className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-full items-center justify-start rounded border bg-black p-2 text-base text-white">
{activeTab?.label} {activeTab?.label}
</DropdownMenuTrigger> </DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>{activeTab?.label}</TooltipContent>
</Tooltip>
<DropdownMenuContent className="bg-black"> <DropdownMenuContent className="bg-black">
{tabs.map(tab => { {tabs.map(tab => {
const { name, label, studies } = tab; const { name, label, studies } = tab;

View File

@ -4,6 +4,7 @@ import classnames from 'classnames';
import { ThumbnailList } from '../ThumbnailList'; import { ThumbnailList } from '../ThumbnailList';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../Accordion'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../Accordion';
import { Tooltip, TooltipContent, TooltipTrigger } from '../Tooltip';
const StudyItem = ({ const StudyItem = ({
date, date,
@ -34,16 +35,28 @@ const StudyItem = ({
defaultValue={isActive ? 'study-item' : undefined} defaultValue={isActive ? 'study-item' : undefined}
> >
<AccordionItem value="study-item"> <AccordionItem value="study-item">
<AccordionTrigger className={classnames('hover:bg-accent bg-popover group rounded')}> <AccordionTrigger className={classnames('hover:bg-accent bg-popover group w-full rounded')}>
<div className="flex h-[40px] flex-1 flex-row"> <div className="flex h-[40px] w-full flex-row overflow-hidden">
<div className="flex w-full flex-row items-center"> <div className="flex w-full flex-row items-center justify-between">
<div className="flex flex-col items-start text-[13px]"> <div className="flex min-w-0 flex-col items-start text-[13px]">
<div className="text-white">{date}</div> <Tooltip>
<div className="text-muted-foreground h-[18px] max-w-[160px] overflow-hidden truncate whitespace-nowrap"> <TooltipContent>{date}</TooltipContent>
<TooltipTrigger className="w-full">
<div className="h-[18px] w-full max-w-[160px] overflow-hidden truncate whitespace-nowrap text-left text-white">
{date}
</div>
</TooltipTrigger>
</Tooltip>
<Tooltip>
<TooltipContent>{description}</TooltipContent>
<TooltipTrigger className="w-full">
<div className="text-muted-foreground h-[18px] w-full overflow-hidden truncate whitespace-nowrap text-left">
{description} {description}
</div> </div>
</TooltipTrigger>
</Tooltip>
</div> </div>
<div className="text-muted-foreground ml-auto flex flex-col items-end text-[12px]"> <div className="text-muted-foreground flex flex-col items-end pl-[10px] text-[12px]">
<div className="max-w-[150px] overflow-hidden text-ellipsis">{modalities}</div> <div className="max-w-[150px] overflow-hidden text-ellipsis">{modalities}</div>
<div>{numInstances}</div> <div>{numInstances}</div>
</div> </div>

View File

@ -20,17 +20,17 @@ const Thumbnail = ({
loadingProgress, loadingProgress,
countIcon, countIcon,
messages, messages,
dragData = {},
isActive, isActive,
onClick, onClick,
onDoubleClick, onDoubleClick,
viewPreset = 'thumbnails', thumbnailType,
modality, modality,
viewPreset = 'thumbnails',
isHydratedForDerivedDisplaySet = false, isHydratedForDerivedDisplaySet = false,
isTracked = false, isTracked = false,
canReject = false, canReject = false,
dragData = {},
onReject = () => {}, onReject = () => {},
thumbnailType = 'thumbnail',
onClickUntrack = () => {}, onClickUntrack = () => {},
ThumbnailMenuItems = () => {}, ThumbnailMenuItems = () => {},
}: withAppTypes): React.ReactNode => { }: withAppTypes): React.ReactNode => {
@ -135,10 +135,15 @@ const Thumbnail = ({
</div> </div>
</div> </div>
</div> </div>
<div className="mt-3 flex h-[52px] w-[128px] flex-col"> <div className="flex h-[52px] w-[128px] flex-col justify-end">
<div className="min-h-[18px] w-[128px] overflow-hidden text-ellipsis pb-0.5 pl-1 text-[12px] font-normal leading-4 text-white"> <Tooltip>
<TooltipContent>{description}</TooltipContent>
<TooltipTrigger>
<div className="min-h-[18px] w-[128px] overflow-hidden text-ellipsis whitespace-nowrap pb-0.5 pl-1 text-left text-[12px] font-normal leading-4 text-white">
{description} {description}
</div> </div>
</TooltipTrigger>
</Tooltip>
<div className="flex h-[12px] items-center gap-[7px] overflow-hidden"> <div className="flex h-[12px] items-center gap-[7px] overflow-hidden">
<div className="text-muted-foreground pl-1 text-[11px]"> S:{seriesNumber}</div> <div className="text-muted-foreground pl-1 text-[11px]"> S:{seriesNumber}</div>
<div className="text-muted-foreground text-[11px]"> <div className="text-muted-foreground text-[11px]">
@ -165,21 +170,25 @@ const Thumbnail = ({
isActive && 'bg-popover' isActive && 'bg-popover'
)} )}
> >
<div className="relative flex h-[32px] items-center gap-[8px]"> <div className="relative flex h-[32px] w-full items-center gap-[8px] overflow-hidden">
<div <div
className={classnames( className={classnames(
'h-[32px] w-[4px] rounded-[2px]', 'h-[32px] w-[4px] min-w-[4px] rounded-[2px]',
isActive || isHydratedForDerivedDisplaySet ? 'bg-highlight' : 'bg-primary/65', isActive || isHydratedForDerivedDisplaySet ? 'bg-highlight' : 'bg-primary/65',
loadingProgress && loadingProgress < 1 && 'bg-primary/25' loadingProgress && loadingProgress < 1 && 'bg-primary/25'
)} )}
></div> ></div>
<div className="flex h-full flex-col"> <div className="flex h-full w-[calc(100%-12px)] flex-col">
<div className="flex items-center gap-[7px]"> <div className="flex items-center gap-[7px]">
<div className="text-[13px] font-semibold text-white">{modality}</div> <div className="text-[13px] font-semibold text-white">{modality}</div>
<Tooltip>
<div className="max-w-[160px] overflow-hidden overflow-ellipsis whitespace-nowrap text-[13px] font-normal text-white"> <TooltipContent>{description}</TooltipContent>
<TooltipTrigger className="w-full overflow-hidden">
<div className="max-w-[160px] overflow-hidden overflow-ellipsis whitespace-nowrap text-left text-[13px] font-normal text-white">
{description} {description}
</div> </div>
</TooltipTrigger>
</Tooltip>
</div> </div>
<div className="flex h-[12px] items-center gap-[7px] overflow-hidden"> <div className="flex h-[12px] items-center gap-[7px] overflow-hidden">
@ -246,7 +255,7 @@ const Thumbnail = ({
className, className,
'bg-muted hover:bg-primary/30 group flex cursor-pointer select-none flex-col rounded outline-none', 'bg-muted hover:bg-primary/30 group flex cursor-pointer select-none flex-col rounded outline-none',
viewPreset === 'thumbnails' && 'h-[170px] w-[135px]', viewPreset === 'thumbnails' && 'h-[170px] w-[135px]',
viewPreset === 'list' && 'col-span-2 h-[40px] w-[275px]' viewPreset === 'list' && 'h-[40px] w-full'
)} )}
id={`thumbnail-${displaySetInstanceUID}`} id={`thumbnail-${displaySetInstanceUID}`}
data-cy={ data-cy={

View File

@ -11,68 +11,73 @@ const ThumbnailList = ({
activeDisplaySetInstanceUIDs = [], activeDisplaySetInstanceUIDs = [],
viewPreset, viewPreset,
ThumbnailMenuItems, ThumbnailMenuItems,
}: withAppTypes) => { }) => {
// Filter thumbnails into list items and thumbnail items
const listItems = thumbnails?.filter(
({ componentType }) => componentType === 'thumbnailNoImage' || viewPreset === 'list'
);
const thumbnailItems = thumbnails?.filter(
({ componentType }) => componentType !== 'thumbnailNoImage' && viewPreset === 'thumbnails'
);
return ( return (
<div <div className="flex flex-col gap-[4px] pt-[4px] pr-[2.5px] pl-[5px] pb-[4px]">
className="min-h-[350px]" {/* Thumbnail Items */}
style={{ {thumbnailItems.length > 0 && (
'--radix-accordion-content-height': '350px',
}}
>
<div <div
id="ohif-thumbnail-list" 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="ohif-scrollbar bg-bkg-low grid grid-cols-[repeat(auto-fit,_minmax(0,135px))] place-items-start gap-[4px] overflow-y-hidden"
> >
{thumbnails.map( {thumbnailItems.map(item => {
({ const { displaySetInstanceUID, componentType, numInstances, ...rest } = item;
displaySetInstanceUID,
description,
dragData,
seriesNumber,
numInstances,
loadingProgress,
modality,
componentType,
countIcon,
canReject,
onReject,
isTracked,
imageSrc,
messages,
imageAltText,
isHydratedForDerivedDisplaySet,
}) => {
const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID); const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID);
return ( return (
<Thumbnail <Thumbnail
key={displaySetInstanceUID} key={displaySetInstanceUID}
{...rest}
displaySetInstanceUID={displaySetInstanceUID} displaySetInstanceUID={displaySetInstanceUID}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances || 1} numInstances={numInstances || 1}
countIcon={countIcon}
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
isActive={isActive} isActive={isActive}
canReject={canReject}
onReject={onReject}
modality={modality}
viewPreset={componentType === 'thumbnailNoImage' ? 'list' : viewPreset}
thumbnailType={componentType} thumbnailType={componentType}
onClick={() => onThumbnailClick(displaySetInstanceUID)} viewPreset="thumbnails"
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)} onClick={onThumbnailClick.bind(null, displaySetInstanceUID)}
isTracked={isTracked} onDoubleClick={onThumbnailDoubleClick.bind(null, displaySetInstanceUID)}
loadingProgress={loadingProgress} onClickUntrack={onClickUntrack.bind(null, displaySetInstanceUID)}
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
ThumbnailMenuItems={ThumbnailMenuItems} ThumbnailMenuItems={ThumbnailMenuItems}
/> />
); );
} })}
)}
</div> </div>
)}
{/* List Items */}
{listItems.length > 0 && (
<div
id="ohif-thumbnail-list"
className="ohif-scrollbar bg-bkg-low grid grid-cols-[repeat(auto-fit,_minmax(0,275px))] place-items-start gap-[2px] overflow-y-hidden"
>
{listItems.map(item => {
const { displaySetInstanceUID, componentType, numInstances, ...rest } = item;
const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID);
return (
<Thumbnail
key={displaySetInstanceUID}
{...rest}
displaySetInstanceUID={displaySetInstanceUID}
numInstances={numInstances || 1}
isActive={isActive}
thumbnailType={componentType}
viewPreset="list"
onClick={onThumbnailClick.bind(null, displaySetInstanceUID)}
onDoubleClick={onThumbnailDoubleClick.bind(null, displaySetInstanceUID)}
onClickUntrack={onClickUntrack.bind(null, displaySetInstanceUID)}
ThumbnailMenuItems={ThumbnailMenuItems}
/>
);
})}
</div>
)}
</div> </div>
); );
}; };
@ -107,6 +112,7 @@ ThumbnailList.propTypes = {
onThumbnailDoubleClick: PropTypes.func.isRequired, onThumbnailDoubleClick: PropTypes.func.isRequired,
onClickUntrack: PropTypes.func.isRequired, onClickUntrack: PropTypes.func.isRequired,
viewPreset: PropTypes.string, viewPreset: PropTypes.string,
ThumbnailMenuItems: PropTypes.any,
}; };
export { ThumbnailList }; export { ThumbnailList };