Enhance stack handling by adding studyId support and improving descriptor parsing logic
This commit is contained in:
+73
-49
@@ -64,10 +64,11 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StackEntry = {
|
type StackEntry = {
|
||||||
imageIds: string[],
|
imageIds: string[];
|
||||||
studyInstanceUID?: string,
|
studyInstanceUID?: string;
|
||||||
name?: string,
|
name?: string;
|
||||||
caseId?: string,
|
caseId?: string;
|
||||||
|
studyId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -2829,7 +2830,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
{openDropdownIdx === i && (
|
{openDropdownIdx === i && (
|
||||||
<div
|
<div
|
||||||
ref={el => { if (el) el.focus(); }}
|
ref={el => {
|
||||||
|
if (el) el.focus();
|
||||||
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -2840,16 +2843,17 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
|
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
minWidth: 180,
|
minWidth: 220,
|
||||||
maxHeight: 220,
|
maxHeight: 260,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
overscrollBehavior: "contain",
|
overscrollBehavior: "contain",
|
||||||
scrollbarWidth: "thin",
|
scrollbarWidth: "thin",
|
||||||
|
padding: 6,
|
||||||
}}
|
}}
|
||||||
role="listbox"
|
role="listbox"
|
||||||
onMouseDown={e => e.stopPropagation()}
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
onWheel={e => { /* trap handled elsewhere */ }}
|
||||||
onBlur={e => {
|
onBlur={e => {
|
||||||
// close when focus leaves
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!(e.currentTarget as HTMLElement).contains(document.activeElement)) {
|
if (!(e.currentTarget as HTMLElement).contains(document.activeElement)) {
|
||||||
setOpenDropdownIdx(null);
|
setOpenDropdownIdx(null);
|
||||||
@@ -2857,48 +2861,68 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{availableStacks.map((stackObj, idx) => {
|
{/* Group stacks by studyId (use 'Unspecified' for missing) */}
|
||||||
const imageIds = (stackObj && Array.isArray((stackObj as any).imageIds)) ? (stackObj as any).imageIds : [];
|
{(() => {
|
||||||
const label = (stackObj && ((stackObj as any).name || (stackObj as any).caseId))
|
const groups: Record<string, { idx: number; stack: any }[]> = {};
|
||||||
? ((stackObj as any).name || `Case: ${(stackObj as any).caseId}`)
|
availableStacks.forEach((stackObj, idx) => {
|
||||||
: `Stack ${idx + 1}`;
|
const studyKey = (stackObj as any).studyId || (stackObj as any).studyInstanceUID || "Unspecified";
|
||||||
const count = imageIds.length;
|
if (!groups[studyKey]) groups[studyKey] = [];
|
||||||
const invalid = !Array.isArray(imageIds) || count === 0;
|
groups[studyKey].push({ idx, stack: stackObj });
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return Object.keys(groups).map(studyKey => (
|
||||||
<button
|
<div key={studyKey} style={{ marginBottom: 6 }}>
|
||||||
key={idx}
|
<div style={{ fontSize: 12, color: "#aaa", padding: "4px 8px", fontWeight: 700 }}>
|
||||||
role="option"
|
{studyKey === "Unspecified" ? "Unspecified study" : `Study: ${studyKey}`}
|
||||||
aria-selected={false}
|
</div>
|
||||||
title={invalid ? `Invalid stack descriptor` : `${label} — ${count} image${count === 1 ? '' : 's'}${(stackObj as any)?.caseId ? ` — case ${(stackObj as any).caseId}` : ''}`}
|
<div>
|
||||||
style={{
|
{groups[studyKey].map(({ idx, stack }) => {
|
||||||
display: "flex",
|
const imageIds = Array.isArray((stack as any).imageIds) ? (stack as any).imageIds : [];
|
||||||
flexDirection: "column",
|
const label = (stack && ((stack as any).name || (stack as any).caseId))
|
||||||
alignItems: "flex-start",
|
? ((stack as any).name || `Case: ${(stack as any).caseId}`)
|
||||||
width: "100%",
|
: `Stack ${idx + 1}`;
|
||||||
padding: "8px 12px",
|
const count = imageIds.length;
|
||||||
background: "transparent",
|
const invalid = !Array.isArray(imageIds) || count === 0;
|
||||||
color: "#fff",
|
|
||||||
border: "none",
|
return (
|
||||||
textAlign: "left",
|
<button
|
||||||
cursor: invalid ? "not-allowed" : "pointer",
|
key={idx}
|
||||||
}}
|
role="option"
|
||||||
onClick={(e) => {
|
aria-selected={false}
|
||||||
e.stopPropagation();
|
title={invalid ? `Invalid stack descriptor` : `${label} — ${count} image${count === 1 ? '' : 's'}${(stack as any)?.caseId ? ` — case ${(stack as any).caseId}` : ''}`}
|
||||||
if (invalid) {
|
style={{
|
||||||
console.warn(`Attempted to select invalid stack at index ${idx}`, stackObj);
|
display: "flex",
|
||||||
return;
|
flexDirection: "column",
|
||||||
}
|
alignItems: "flex-start",
|
||||||
// Use existing handler to load the stack into this viewport
|
width: "100%",
|
||||||
handleLoadStack(i, idx).catch(err => console.error("Failed to load stack:", err));
|
padding: "8px 12px",
|
||||||
setOpenDropdownIdx(null);
|
background: "transparent",
|
||||||
}}
|
color: "#fff",
|
||||||
>
|
border: "none",
|
||||||
<div style={{ fontWeight: "600", fontSize: 13 }}>{label}</div>
|
textAlign: "left",
|
||||||
<div style={{ fontSize: 12, opacity: 0.7 }}>{count} image{count === 1 ? '' : 's'}</div>
|
cursor: invalid ? "not-allowed" : "pointer",
|
||||||
</button>
|
}}
|
||||||
);
|
onClick={(e) => {
|
||||||
})}
|
e.stopPropagation();
|
||||||
|
if (invalid) {
|
||||||
|
console.warn(`Attempted to select invalid stack at index ${idx}`, stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleLoadStack(i, idx).catch(err => console.error("Failed to load stack:", err));
|
||||||
|
setOpenDropdownIdx(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: "600", fontSize: 13 }}>{label}</div>
|
||||||
|
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
||||||
|
{count} image{count === 1 ? '' : 's'}{(stack as any)?.caseId ? ` • case ${(stack as any).caseId}` : ''}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+61
-58
@@ -41,73 +41,76 @@ export function mountDicomViewers() {
|
|||||||
const dataImages = container.getAttribute('data-images');
|
const dataImages = container.getAttribute('data-images');
|
||||||
|
|
||||||
const parseToDescriptors = (parsed: any): Array<any> => {
|
const parseToDescriptors = (parsed: any): Array<any> => {
|
||||||
// Return array of descriptors: either simple string[] => { imageIds: string[] }
|
// Return array of descriptors: either simple string[] => { imageIds: string[] }
|
||||||
// or objects: { imageIds: string[], name?, caseId? }
|
// or objects: { imageIds: string[], name?, caseId?, studyId? }
|
||||||
if (!parsed) return [];
|
if (!parsed) return [];
|
||||||
|
|
||||||
// If top-level is an array of strings -> single unnamed stack
|
// If top-level is an array of strings -> single unnamed stack
|
||||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||||
return [toDescriptor(parsed)];
|
return [toDescriptor(parsed)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
const out: any[] = [];
|
const out: any[] = [];
|
||||||
parsed.forEach((item, itemIdx) => {
|
parsed.forEach((item, itemIdx) => {
|
||||||
// Case object with nested stacks: { caseId, stacks: [ { name, imageIds } ] }
|
// Case object with nested stacks: { caseId, studyId, stacks: [ { name, imageIds } ] }
|
||||||
if (item && typeof item === "object" && Array.isArray(item.stacks)) {
|
if (item && typeof item === "object" && Array.isArray(item.stacks)) {
|
||||||
const caseId = item.caseId || item.caseUID || item.case || undefined;
|
const caseId = item.caseId || item.caseUID || item.case || undefined;
|
||||||
item.stacks.forEach((s: any, sIdx: number) => {
|
const studyId = item.studyId || item.studyUID || item.studyInstanceUID || undefined;
|
||||||
let imageIds: string[] = [];
|
item.stacks.forEach((s: any, sIdx: number) => {
|
||||||
if (Array.isArray(s)) imageIds = s;
|
let imageIds: string[] = [];
|
||||||
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
|
if (Array.isArray(s)) imageIds = s;
|
||||||
else if (Array.isArray(s.i)) imageIds = s.i;
|
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
|
||||||
|
else if (Array.isArray(s.i)) imageIds = s.i;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
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);
|
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack object: { imageIds, i, name, caseId }
|
out.push({
|
||||||
if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
|
imageIds: toWadouri(imageIds),
|
||||||
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
|
name: s.name || s.label || undefined,
|
||||||
|
caseId,
|
||||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
studyId: s.studyId || studyId,
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
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) {
|
if (dataNamed) {
|
||||||
let parsed;
|
let parsed;
|
||||||
|
|||||||
Reference in New Issue
Block a user