Implement drag-and-drop functionality for stack selection and enhance stack grouping by studyId in the DICOM viewer

This commit is contained in:
Ross
2025-09-15 14:14:47 +01:00
parent 1a8fb26a29
commit 654abaaaf4
+95 -7
View File
@@ -2073,7 +2073,6 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const numVisible = viewportGrid.rows * viewportGrid.cols;
const viewports = [];
//for (let i = 0; i < MAX_VIEWPORTS; i++) {
// Viewports rendering loop
for (let i = 0; i < numVisible; i++) {
// Determine if this viewport should be visible in the current grid
@@ -2095,20 +2094,28 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
gridRow: Math.floor(i / viewportGrid.cols) + 1,
gridColumn: (i % viewportGrid.cols) + 1,
height: "100%",
//visibility: isVisible ? "visible" : "hidden",
//pointerEvents: isVisible ? "auto" : "none",
// Remove outline, use only boxShadow for indicator
boxShadow: activeViewport === i
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
: undefined,
// cursor: "pointer",
zIndex: activeViewport === i ? 10 : 1,
}}
onClick={() => {
setActiveViewport(i);
setViewportMenuOpen(false); // <-- Add this line to close the grid selector
setViewportMenuOpen(false);
}}
onDoubleClick={() => handleDoubleClick(i)}
onDragOver={e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}}
onDrop={e => {
e.preventDefault();
const idxStr = e.dataTransfer.getData('text/stack-idx');
const stackIdx = parseInt(idxStr, 10);
if (!isNaN(stackIdx)) {
handleLoadStack(i, stackIdx);
}
}}
>
<button
style={{
@@ -2937,7 +2944,6 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
return (
// App root
<div
ref={appRootRef}
style={{
@@ -2976,6 +2982,88 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</div>
)}
{/* Top bar stack browser */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
minHeight: 54,
background: "#23272bE6", // slightly transparent for layering
color: "#fff",
zIndex: 1001, // higher than most overlays but below modals
display: "flex",
alignItems: "center",
borderBottom: "2px solid #444",
padding: "0 16px",
gap: 18,
overflowX: "auto",
userSelect: "none",
pointerEvents: "auto",
}}
>
{/* Group stacks by studyId or studyInstanceUID or 'Unspecified' */}
{(() => {
const groups: Record<string, { idx: number; stack: StackEntry }[]> = {};
availableStacks.forEach((stackObj, idx) => {
const studyKey = stackObj.studyId || stackObj.studyInstanceUID || 'Unspecified';
if (!groups[studyKey]) groups[studyKey] = [];
groups[studyKey].push({ idx, stack: stackObj });
});
return Object.keys(groups).map(studyKey => (
<div key={studyKey} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color: '#aaa', fontSize: 13, fontWeight: 700, marginRight: 6 }}>
{studyKey === 'Unspecified' ? 'Unspecified study' : `Study: ${studyKey}`}
</span>
{groups[studyKey].map(({ idx, stack }) => {
const imageIds = Array.isArray(stack.imageIds) ? stack.imageIds : [];
const label = stack.name || (stack.caseId ? `Case: ${stack.caseId}` : `Stack ${idx + 1}`);
const count = imageIds.length;
const invalid = !Array.isArray(imageIds) || count === 0;
// Highlight if this stack is selected in any viewport
const isSelected = selectedStackIdx.includes(idx);
return (
<div
key={idx}
draggable={!invalid}
onDragStart={e => {
if (invalid) return;
e.dataTransfer.setData('text/stack-idx', String(idx));
e.dataTransfer.effectAllowed = 'copy';
}}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
minWidth: 90,
maxWidth: 220,
padding: '7px 14px',
margin: '0 2px',
background: isSelected ? '#1976d2' : '#333',
color: isSelected ? '#fff' : '#eee',
border: isSelected ? '2px solid #90caf9' : 'none',
borderRadius: '4px',
fontWeight: isSelected ? 'bold' : 'normal',
fontSize: 15,
cursor: invalid ? 'not-allowed' : 'grab',
opacity: invalid ? 0.5 : 1,
transition: 'background 0.2s, border 0.2s',
boxShadow: isSelected ? '0 2px 8px #1976d2aa' : undefined,
}}
title={invalid ? 'Invalid stack descriptor' : `${label}${count} image${count === 1 ? '' : 's'}`}
>
<span>{label}</span>
<span style={{ fontSize: 12, opacity: 0.7, marginLeft: 8 }}>{count} img{count === 1 ? '' : 's'}</span>
</div>
);
})}
</div>
));
})()}
</div>
<div>
{/* Side menu toggle button */}
<button