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]);
// Load available stacks on mount
useEffect(() => {
useEffect(() => {
let mounted = true;
imageStacks()
@@ -313,7 +313,7 @@ useEffect(() => {
});
return () => { mounted = false; };
}, []);
}, []);
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
const stackObj = availableStacks[stackIdx];
@@ -613,8 +613,8 @@ useEffect(() => {
};
// --- Load from localStorage on mount ---
useEffect(() => {
// --- Load from localStorage on mount ---
useEffect(() => {
try {
const mouse = localStorage.getItem(MOUSE_SETTINGS_KEY);
const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY);
@@ -623,20 +623,20 @@ useEffect(() => {
} catch (e) {
// Ignore parse errors
}
}, []);
}, []);
// --- Save to localStorage when changed ---
useEffect(() => {
// --- Save to localStorage when changed ---
useEffect(() => {
try {
localStorage.setItem(MOUSE_SETTINGS_KEY, JSON.stringify(mouseToolBindings));
} catch (e) {}
}, [mouseToolBindings]);
} catch (e) { }
}, [mouseToolBindings]);
useEffect(() => {
useEffect(() => {
try {
localStorage.setItem(CTRL_MOUSE_SETTINGS_KEY, JSON.stringify(ctrlMouseToolBindings));
} catch (e) {}
}, [ctrlMouseToolBindings]);
} catch (e) { }
}, [ctrlMouseToolBindings]);
useEffect(() => {
window.loadDicomStackFromFiles = (files: FileList | File[]) => {
@@ -2880,61 +2880,66 @@ useEffect(() => {
}}
>
{availableStacks.map((stackObj, idx) => {
const thisUID = stackObj.studyInstanceUID;
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
const dotColor = sameStudy ? "#4caf50" : "#888";
const isEmpty = stackObj.imageIds.length === 0;
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;
return (
<div
<button
key={idx}
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={{
padding: "6px 12px",
background: selectedStackIdx[i] === idx ? "#444" : "#222",
color: isEmpty ? "#aaa" : "#fff",
cursor: isEmpty ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
borderBottom: "1px solid #333",
userSelect: "none",
opacity: isEmpty ? 0.6 : 1,
flexDirection: "column",
alignItems: "flex-start",
width: "100%",
padding: "8px 12px",
background: "transparent",
color: "#fff",
border: "none",
textAlign: "left",
cursor: invalid ? "not-allowed" : "pointer",
}}
tabIndex={-1}
onMouseDown={e => {
e.preventDefault();
if (!isEmpty) {
handleLoadStack(i, idx);
setOpenDropdownIdx(null);
onClick={(e) => {
e.stopPropagation();
if (invalid) {
console.warn(`Attempted to select invalid stack at index ${idx}`, stackObj);
return;
}
// 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={{
color: dotColor,
fontWeight: "bold",
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 style={{ fontWeight: "600", fontSize: 13 }}>{label}</div>
<div style={{ fontSize: 12, opacity: 0.7 }}>{count} image{count === 1 ? '' : 's'}</div>
</button>
);
})}
</div>
)}
</div>
</div>
)}
</div>
);
)
}
</div >
);
}
return (
return (
// App root
<div
ref={appRootRef}
@@ -3634,7 +3639,7 @@ useEffect(() => {
{/* ...rest of your UI... */}
</div >
)
)
}
+19 -3
View File
@@ -52,15 +52,22 @@ export function mountDicomViewers() {
if (Array.isArray(parsed)) {
const out: any[] = [];
parsed.forEach(item => {
parsed.forEach((item, itemIdx) => {
// 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;
item.stacks.forEach((s: any) => {
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,
@@ -73,6 +80,12 @@ export function mountDicomViewers() {
// 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,
@@ -86,6 +99,9 @@ export function mountDicomViewers() {
out.push(toDescriptor(item));
return;
}
// Unknown / invalid entry
console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
});
return out;
}