diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx
index 210582f..ffc7060 100644
--- a/dicom-viewer/src/App.tsx
+++ b/dicom-viewer/src/App.tsx
@@ -64,10 +64,11 @@ declare global {
}
type StackEntry = {
- imageIds: string[],
- studyInstanceUID?: string,
- name?: string,
- caseId?: string,
+ imageIds: string[];
+ studyInstanceUID?: string;
+ name?: string;
+ caseId?: string;
+ studyId?: string;
};
@@ -2829,7 +2830,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
{/* Dropdown */}
{openDropdownIdx === i && (
{ if (el) el.focus(); }}
+ ref={el => {
+ if (el) el.focus();
+ }}
tabIndex={0}
style={{
position: "absolute",
@@ -2840,16 +2843,17 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
zIndex: 100,
- minWidth: 180,
- maxHeight: 220,
+ minWidth: 220,
+ maxHeight: 260,
overflowY: "auto",
overscrollBehavior: "contain",
scrollbarWidth: "thin",
+ padding: 6,
}}
role="listbox"
onMouseDown={e => e.stopPropagation()}
+ onWheel={e => { /* trap handled elsewhere */ }}
onBlur={e => {
- // close when focus leaves
setTimeout(() => {
if (!(e.currentTarget as HTMLElement).contains(document.activeElement)) {
setOpenDropdownIdx(null);
@@ -2857,48 +2861,68 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}, 0);
}}
>
- {availableStacks.map((stackObj, idx) => {
- const imageIds = (stackObj && Array.isArray((stackObj as any).imageIds)) ? (stackObj as any).imageIds : [];
- const label = (stackObj && ((stackObj as any).name || (stackObj as any).caseId))
- ? ((stackObj as any).name || `Case: ${(stackObj as any).caseId}`)
- : `Stack ${idx + 1}`;
- const count = imageIds.length;
- const invalid = !Array.isArray(imageIds) || count === 0;
+ {/* Group stacks by studyId (use 'Unspecified' for missing) */}
+ {(() => {
+ const groups: Record
= {};
+ availableStacks.forEach((stackObj, idx) => {
+ const studyKey = (stackObj as any).studyId || (stackObj as any).studyInstanceUID || "Unspecified";
+ if (!groups[studyKey]) groups[studyKey] = [];
+ groups[studyKey].push({ idx, stack: stackObj });
+ });
- return (
-
- );
- })}
+ return Object.keys(groups).map(studyKey => (
+
+
+ {studyKey === "Unspecified" ? "Unspecified study" : `Study: ${studyKey}`}
+
+
+ {groups[studyKey].map(({ idx, stack }) => {
+ const imageIds = Array.isArray((stack as any).imageIds) ? (stack as any).imageIds : [];
+ const label = (stack && ((stack as any).name || (stack as any).caseId))
+ ? ((stack as any).name || `Case: ${(stack as any).caseId}`)
+ : `Stack ${idx + 1}`;
+ const count = imageIds.length;
+ const invalid = !Array.isArray(imageIds) || count === 0;
+
+ return (
+
+ );
+ })}
+
+
+ ));
+ })()}
)}
diff --git a/dicom-viewer/src/main.tsx b/dicom-viewer/src/main.tsx
index 40b3845..04167f0 100644
--- a/dicom-viewer/src/main.tsx
+++ b/dicom-viewer/src/main.tsx
@@ -41,73 +41,76 @@ export function mountDicomViewers() {
const dataImages = container.getAttribute('data-images');
const parseToDescriptors = (parsed: any): Array => {
- // Return array of descriptors: either simple string[] => { imageIds: string[] }
- // or objects: { imageIds: string[], name?, caseId? }
- if (!parsed) return [];
+ // Return array of descriptors: either simple string[] => { imageIds: string[] }
+ // or objects: { imageIds: string[], name?, caseId?, studyId? }
+ if (!parsed) return [];
- // If top-level is an array of strings -> single unnamed stack
- if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
- return [toDescriptor(parsed)];
- }
+ // If top-level is an array of strings -> single unnamed stack
+ if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
+ return [toDescriptor(parsed)];
+ }
- if (Array.isArray(parsed)) {
- const out: any[] = [];
- parsed.forEach((item, itemIdx) => {
- // Case object with nested stacks: { caseId, stacks: [ { name, imageIds } ] }
- if (item && typeof item === "object" && Array.isArray(item.stacks)) {
- const caseId = item.caseId || item.caseUID || item.case || undefined;
- item.stacks.forEach((s: any, sIdx: number) => {
- let imageIds: string[] = [];
- if (Array.isArray(s)) imageIds = s;
- else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
- else if (Array.isArray(s.i)) imageIds = s.i;
+ if (Array.isArray(parsed)) {
+ const out: any[] = [];
+ parsed.forEach((item, itemIdx) => {
+ // Case object with nested stacks: { caseId, studyId, stacks: [ { name, imageIds } ] }
+ if (item && typeof item === "object" && Array.isArray(item.stacks)) {
+ const caseId = item.caseId || item.caseUID || item.case || undefined;
+ const studyId = item.studyId || item.studyUID || item.studyInstanceUID || undefined;
+ item.stacks.forEach((s: any, sIdx: number) => {
+ let imageIds: string[] = [];
+ if (Array.isArray(s)) imageIds = s;
+ else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
+ else if (Array.isArray(s.i)) imageIds = s.i;
- // Validation
- if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
- console.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
- return;
- }
-
- out.push({
- imageIds: toWadouri(imageIds),
- name: s.name || s.label || undefined,
- caseId,
- });
- });
+ // Validation
+ if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
+ console.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
return;
}
- // Stack object: { imageIds, i, name, caseId }
- if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
- const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
-
- if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
- console.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
- return;
- }
-
- out.push({
- imageIds: toWadouri(imageIds),
- name: item.name || item.label || undefined,
- caseId: item.caseId || item.caseUID || undefined,
- });
- return;
- }
-
- // Plain array (stack)
- if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
- out.push(toDescriptor(item));
- return;
- }
-
- // Unknown / invalid entry
- console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
+ out.push({
+ imageIds: toWadouri(imageIds),
+ name: s.name || s.label || undefined,
+ caseId,
+ studyId: s.studyId || studyId,
+ });
});
- return out;
+ return;
}
- return [];
- };
+ // Stack object: { imageIds, i, name, caseId, studyId }
+ if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
+ const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
+
+ if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
+ console.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
+ return;
+ }
+
+ out.push({
+ imageIds: toWadouri(imageIds),
+ name: item.name || item.label || undefined,
+ caseId: item.caseId || item.caseUID || undefined,
+ studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
+ });
+ return;
+ }
+
+ // Plain array (stack)
+ if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
+ out.push(toDescriptor(item));
+ return;
+ }
+
+ // Unknown / invalid entry
+ console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
+ });
+ return out;
+ }
+
+ return [];
+};
if (dataNamed) {
let parsed;