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 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
|
||||
|
||||
Reference in New Issue
Block a user