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;