Implement drag-and-drop functionality for stack selection and enhance stack grouping by studyId in the DICOM viewer
This commit is contained in:
@@ -2073,7 +2073,6 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
|
|
||||||
const numVisible = viewportGrid.rows * viewportGrid.cols;
|
const numVisible = viewportGrid.rows * viewportGrid.cols;
|
||||||
const viewports = [];
|
const viewports = [];
|
||||||
//for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
|
||||||
// Viewports rendering loop
|
// Viewports rendering loop
|
||||||
for (let i = 0; i < numVisible; i++) {
|
for (let i = 0; i < numVisible; i++) {
|
||||||
// Determine if this viewport should be visible in the current grid
|
// 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,
|
gridRow: Math.floor(i / viewportGrid.cols) + 1,
|
||||||
gridColumn: (i % viewportGrid.cols) + 1,
|
gridColumn: (i % viewportGrid.cols) + 1,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
//visibility: isVisible ? "visible" : "hidden",
|
|
||||||
//pointerEvents: isVisible ? "auto" : "none",
|
|
||||||
// Remove outline, use only boxShadow for indicator
|
|
||||||
boxShadow: activeViewport === i
|
boxShadow: activeViewport === i
|
||||||
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
|
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
|
||||||
: undefined,
|
: undefined,
|
||||||
// cursor: "pointer",
|
|
||||||
zIndex: activeViewport === i ? 10 : 1,
|
zIndex: activeViewport === i ? 10 : 1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveViewport(i);
|
setActiveViewport(i);
|
||||||
setViewportMenuOpen(false); // <-- Add this line to close the grid selector
|
setViewportMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => handleDoubleClick(i)}
|
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
|
<button
|
||||||
style={{
|
style={{
|
||||||
@@ -2937,7 +2944,6 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// App root
|
|
||||||
<div
|
<div
|
||||||
ref={appRootRef}
|
ref={appRootRef}
|
||||||
style={{
|
style={{
|
||||||
@@ -2976,6 +2982,88 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
</div>
|
</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>
|
<div>
|
||||||
{/* Side menu toggle button */}
|
{/* Side menu toggle button */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user