Enhance validation for DICOM viewer input data

- Added validation checks for nested stacks and stack objects to ensure imageIds are valid arrays of strings.
- Implemented console warnings for invalid entries, including nested stacks and stack objects, to aid in debugging.
- Improved error handling for unknown or invalid entries in the parsed data.
This commit is contained in:
Ross
2025-09-15 12:33:35 +01:00
parent 8de4efc3e2
commit 044057675e
2 changed files with 758 additions and 737 deletions
+53 -48
View File
@@ -257,7 +257,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}, [annotationJson, viewerState, container_id]); }, [annotationJson, viewerState, container_id]);
// Load available stacks on mount // Load available stacks on mount
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
imageStacks() imageStacks()
@@ -313,7 +313,7 @@ useEffect(() => {
}); });
return () => { mounted = false; }; return () => { mounted = false; };
}, []); }, []);
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => { const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
const stackObj = availableStacks[stackIdx]; const stackObj = availableStacks[stackIdx];
@@ -613,8 +613,8 @@ useEffect(() => {
}; };
// --- Load from localStorage on mount --- // --- Load from localStorage on mount ---
useEffect(() => { useEffect(() => {
try { try {
const mouse = localStorage.getItem(MOUSE_SETTINGS_KEY); const mouse = localStorage.getItem(MOUSE_SETTINGS_KEY);
const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY); const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY);
@@ -623,20 +623,20 @@ useEffect(() => {
} catch (e) { } catch (e) {
// Ignore parse errors // Ignore parse errors
} }
}, []); }, []);
// --- Save to localStorage when changed --- // --- Save to localStorage when changed ---
useEffect(() => { useEffect(() => {
try { try {
localStorage.setItem(MOUSE_SETTINGS_KEY, JSON.stringify(mouseToolBindings)); localStorage.setItem(MOUSE_SETTINGS_KEY, JSON.stringify(mouseToolBindings));
} catch (e) {} } catch (e) { }
}, [mouseToolBindings]); }, [mouseToolBindings]);
useEffect(() => { useEffect(() => {
try { try {
localStorage.setItem(CTRL_MOUSE_SETTINGS_KEY, JSON.stringify(ctrlMouseToolBindings)); localStorage.setItem(CTRL_MOUSE_SETTINGS_KEY, JSON.stringify(ctrlMouseToolBindings));
} catch (e) {} } catch (e) { }
}, [ctrlMouseToolBindings]); }, [ctrlMouseToolBindings]);
useEffect(() => { useEffect(() => {
window.loadDicomStackFromFiles = (files: FileList | File[]) => { window.loadDicomStackFromFiles = (files: FileList | File[]) => {
@@ -2880,61 +2880,66 @@ useEffect(() => {
}} }}
> >
{availableStacks.map((stackObj, idx) => { {availableStacks.map((stackObj, idx) => {
const thisUID = stackObj.studyInstanceUID; const imageIds = (stackObj && Array.isArray((stackObj as any).imageIds)) ? (stackObj as any).imageIds : [];
const sameStudy = referenceUID && thisUID && referenceUID === thisUID; const label = (stackObj && ((stackObj as any).name || (stackObj as any).caseId))
const dotColor = sameStudy ? "#4caf50" : "#888"; ? ((stackObj as any).name || `Case: ${(stackObj as any).caseId}`)
const isEmpty = stackObj.imageIds.length === 0; : `Stack ${idx + 1}`;
const count = imageIds.length;
const invalid = !Array.isArray(imageIds) || count === 0;
return ( return (
<div <button
key={idx} key={idx}
role="option" role="option"
aria-selected={selectedStackIdx[i] === idx} aria-selected={false}
title={invalid ? `Invalid stack descriptor` : `${label}${count} image${count === 1 ? '' : 's'}${(stackObj as any)?.caseId ? ` — case ${(stackObj as any).caseId}` : ''}`}
style={{ style={{
padding: "6px 12px",
background: selectedStackIdx[i] === idx ? "#444" : "#222",
color: isEmpty ? "#aaa" : "#fff",
cursor: isEmpty ? "not-allowed" : "pointer",
display: "flex", display: "flex",
alignItems: "center", flexDirection: "column",
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal", alignItems: "flex-start",
borderBottom: "1px solid #333", width: "100%",
userSelect: "none", padding: "8px 12px",
opacity: isEmpty ? 0.6 : 1, background: "transparent",
color: "#fff",
border: "none",
textAlign: "left",
cursor: invalid ? "not-allowed" : "pointer",
}} }}
tabIndex={-1} onClick={(e) => {
onMouseDown={e => { e.stopPropagation();
e.preventDefault(); if (invalid) {
if (!isEmpty) { console.warn(`Attempted to select invalid stack at index ${idx}`, stackObj);
handleLoadStack(i, idx); return;
setOpenDropdownIdx(null);
} }
// Select the stack for this viewport
// Use the shared handler so selection behavior is consistent
handleLoadStack(i, idx).catch((err) => {
console.error("Failed to load stack:", err);
});
// Close the dropdown
setOpenDropdownIdx(null);
}} }}
> >
<span style={{ <div style={{ fontWeight: "600", fontSize: 13 }}>{label}</div>
color: dotColor, <div style={{ fontSize: 12, opacity: 0.7 }}>{count} image{count === 1 ? '' : 's'}</div>
fontWeight: "bold", </button>
marginRight: 8,
fontSize: "16px",
verticalAlign: "middle",
}}></span>
Stack {idx + 1} ({stackObj.imageIds.length} images
{isEmpty && <span style={{ color: "#f44336", marginLeft: 6 }}>(empty)</span>})
</div>
); );
})} })}
</div> </div>
)} )}
</div> </div>
</div> </div>
)} )
</div>
);
} }
</div >
);
}
return ( return (
// App root // App root
<div <div
ref={appRootRef} ref={appRootRef}
@@ -3634,7 +3639,7 @@ useEffect(() => {
{/* ...rest of your UI... */} {/* ...rest of your UI... */}
</div > </div >
) )
} }
+19 -3
View File
@@ -52,15 +52,22 @@ export function mountDicomViewers() {
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
const out: any[] = []; const out: any[] = [];
parsed.forEach(item => { parsed.forEach((item, itemIdx) => {
// Case object with nested stacks: { caseId, stacks: [ { name, imageIds } ] } // Case object with nested stacks: { caseId, 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) => { item.stacks.forEach((s: any, sIdx: number) => {
let imageIds: string[] = []; let imageIds: string[] = [];
if (Array.isArray(s)) imageIds = s; if (Array.isArray(s)) imageIds = s;
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds; else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
else if (Array.isArray(s.i)) imageIds = s.i; 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({ out.push({
imageIds: toWadouri(imageIds), imageIds: toWadouri(imageIds),
name: s.name || s.label || undefined, name: s.name || s.label || undefined,
@@ -73,6 +80,12 @@ export function mountDicomViewers() {
// Stack object: { imageIds, i, name, caseId } // Stack object: { imageIds, i, name, caseId }
if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) { if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : 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({ out.push({
imageIds: toWadouri(imageIds), imageIds: toWadouri(imageIds),
name: item.name || item.label || undefined, name: item.name || item.label || undefined,
@@ -86,6 +99,9 @@ export function mountDicomViewers() {
out.push(toDescriptor(item)); out.push(toDescriptor(item));
return; return;
} }
// Unknown / invalid entry
console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
}); });
return out; return out;
} }