From 98a06894673b19943f3db14d72dd0eaf0359e26a Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh <148532903+Adithyan-Dinesh-Trenser@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:37:53 +0530 Subject: [PATCH 01/14] Support for more Google sheet features * Optimized Series filtering in COD * Modifications to support the additional sheet features and fixed the bugs --- .../cornerstone/src/components/OPFSManagementTool/utils.ts | 6 +++--- .../src/DicomWebDataSource/codDicomWebServerWrapper.js | 6 ++++-- extensions/default/src/DicomWebDataSource/getCodImageId.js | 2 +- .../default/src/DicomWebDataSource/retrieveStudyMetadata.js | 2 +- .../default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx | 1 + .../PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx | 1 + platform/core/src/utils/createStudyBrowserTabs.ts | 4 +++- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/extensions/cornerstone/src/components/OPFSManagementTool/utils.ts b/extensions/cornerstone/src/components/OPFSManagementTool/utils.ts index 24d80f5d690..70bd235f4ba 100644 --- a/extensions/cornerstone/src/components/OPFSManagementTool/utils.ts +++ b/extensions/cornerstone/src/components/OPFSManagementTool/utils.ts @@ -120,9 +120,9 @@ function structureData(fileDetails: FileDetails[]): Study[] { } const firstInstance = Object.values(metadataFile.data.cod.instances)[0]; - const studyDescription = firstInstance.metadata['00081030']?.Value[0]; - const seriesDescription = firstInstance.metadata['0008103E']?.Value[0]; - const seriesModality = firstInstance.metadata['00080060']?.Value[0]; + const studyDescription = firstInstance.metadata['00081030']?.Value?.[0]; + const seriesDescription = firstInstance.metadata['0008103E']?.Value?.[0]; + const seriesModality = firstInstance.metadata['00080060']?.Value?.[0]; let totalSeriesSize = 0; let seriesMostRecentModified = 0; diff --git a/extensions/default/src/DicomWebDataSource/codDicomWebServerWrapper.js b/extensions/default/src/DicomWebDataSource/codDicomWebServerWrapper.js index e462e3685a3..8fbdb12cf61 100644 --- a/extensions/default/src/DicomWebDataSource/codDicomWebServerWrapper.js +++ b/extensions/default/src/DicomWebDataSource/codDicomWebServerWrapper.js @@ -230,9 +230,11 @@ class CodDicomWebServerClient { studyFound = this._findStudy(this._studiesMetadata, { StudyInstanceUID: studyInstanceUID }); } + const seriesUID = queryParams?.SeriesInstanceUID; + const seriesUIDs = seriesUID ? (Array.isArray(seriesUID) ? seriesUID : [seriesUID]) : []; // In COD format, the DeidSeriesInstanceUID is used to identify series instead of SeriesInstanceUID. - const seriesFound = studyFound?.series.find( - ({ deidSeriesInstanceUID }) => deidSeriesInstanceUID === queryParams?.SeriesInstanceUID + const seriesFound = studyFound?.series.find(({ deidSeriesInstanceUID }) => + seriesUIDs.includes(deidSeriesInstanceUID) ); return new Promise(resolve => { diff --git a/extensions/default/src/DicomWebDataSource/getCodImageId.js b/extensions/default/src/DicomWebDataSource/getCodImageId.js index a681bce6914..e7c1e221715 100644 --- a/extensions/default/src/DicomWebDataSource/getCodImageId.js +++ b/extensions/default/src/DicomWebDataSource/getCodImageId.js @@ -21,7 +21,7 @@ export default function getCodImageId({ instance, frame, config }) { wadoRsImageId = getWADORSImageId(instance, config, frame); } - if (config.useURLParams && !instance.imageId && instance.BucketPath) { + if (config.useURLParams && (!instance.imageId || frame) && instance.BucketPath) { wadoRsImageId = wadoRsImageId.replace( config.wadoRoot, `${config.wadoRoot}/${instance.BucketPath}` diff --git a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js index 9c3c5901fdc..f94d1287969 100644 --- a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js +++ b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js @@ -38,7 +38,7 @@ export function retrieveStudyMetadata( throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`); } - const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}`; + const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}:${filters?.seriesInstanceUID?.toString() || 'NO-SERIES-FILTER'}`; // Already waiting on result? Return cached promise if (StudyMetaDataPromises.has(promiseId)) { diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index 4e87b502721..48718e7a4e3 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -536,6 +536,7 @@ function _mapDisplaySets(displaySets, displaySetLoadingState, thumbnailImageSrcM countIcon: ds.countIcon, messages: ds.messages, StudyInstanceUID: ds.StudyInstanceUID, + SeriesInstanceUID: ds.SeriesInstanceUID, componentType, imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID], dragData: { diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 93ae5e12c9b..6d530c09dd7 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -110,6 +110,7 @@ export default function PanelStudyBrowserTracking({ countIcon: ds.countIcon, messages: ds.messages, StudyInstanceUID: ds.StudyInstanceUID, + SeriesInstanceUID: ds.SeriesInstanceUID, componentType, imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID], dragData: { diff --git a/platform/core/src/utils/createStudyBrowserTabs.ts b/platform/core/src/utils/createStudyBrowserTabs.ts index aa565567df5..e8f2846c101 100644 --- a/platform/core/src/utils/createStudyBrowserTabs.ts +++ b/platform/core/src/utils/createStudyBrowserTabs.ts @@ -34,7 +34,9 @@ export function createStudyBrowserTabs( studyDisplayList.forEach(study => { const displaySetsForStudy = displaySets.filter( - ds => ds.StudyInstanceUID === study.studyInstanceUid + ds => + ds.StudyInstanceUID === study.studyInstanceUid && + (!seriesUIdsToFilter.length || seriesUIdsToFilter.includes(ds.SeriesInstanceUID)) ); // sort them by seriesInstanceUID From c365e13022cccb48a1f454097e28de5fa34a84a3 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh <148532903+Adithyan-Dinesh-Trenser@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:58:52 +0530 Subject: [PATCH 02/14] Disabling the 'series filter failed' notification temporarily due to being out of context in some sheet-integrated scenarios --- platform/app/src/routes/Mode/defaultRouteInit.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index d30ca76611e..44f5a24e8cf 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -52,6 +52,8 @@ export async function defaultRouteInit( function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) { const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID); + /* The 'series filter failed' notification is out of context in + some sheet-integrated scenarios, so disabling it temporarily. // checks if the series filter was used, if it exists const seriesInstanceUIDs = filters?.seriesInstanceUID; if ( @@ -68,6 +70,7 @@ export async function defaultRouteInit( duration: 7000, }); } + */ displaySetService.makeDisplaySets(seriesMetadata.instances, { madeInClient }); } From 07ef9116a1dee6033805dcfc91d23c00798c4739 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh <148532903+Adithyan-Dinesh-Trenser@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:38:13 +0530 Subject: [PATCH 03/14] Made the right form panel open by default in longitudinal mode --- modes/longitudinal/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts index 566cf042afd..2ec7272a683 100644 --- a/modes/longitudinal/src/index.ts +++ b/modes/longitudinal/src/index.ts @@ -237,8 +237,8 @@ function modeFactory({ modeConfiguration }) { props: { leftPanels: [tracked.thumbnailList], leftPanelResizable: true, - rightPanels: [cornerstone.segmentation, tracked.measurements, gradienthealth.form], - rightPanelClosed: true, + rightPanels: [gradienthealth.form, cornerstone.segmentation, tracked.measurements], + rightPanelClosed: false, rightPanelResizable: true, viewports: [ { From d7dd1d243a68cd4d4775e305810ed66cfab465c8 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh <148532903+Adithyan-Dinesh-Trenser@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:03:15 +0530 Subject: [PATCH 04/14] Made the right form panel open by default in segmentation mode and handled the evaluate function for segmentation tools --- modes/segmentation/src/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 91ebc2f8779..03cc20a7ffc 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -115,6 +115,11 @@ function modeFactory({ modeConfiguration }) { 'Shapes', ]); toolbarService.createButtonSection('brushToolsSection', ['Brush', 'Eraser', 'Threshold']); + // Making the 'cornerstone.panelTool' the default/first right panel will automaically + // handle the evaluate functions for segmentation panel tools through the toolbox components. + // But since we changed the order, we need to call this here to handle the evaluate functions. + const sectionToolProps = toolbarService.getButtonPropsInButtonSection('segmentationToolbox'); + sectionToolProps.forEach(props => toolbarService.handleEvaluateNested(props)); customizationService.setCustomizations({ 'panelSegmentation.tableMode': { @@ -195,7 +200,7 @@ function modeFactory({ modeConfiguration }) { props: { leftPanels: [ohif.leftPanel], leftPanelResizable: true, - rightPanels: [cornerstone.panelTool, cornerstone.measurements, gradienthealth.form], + rightPanels: [gradienthealth.form, cornerstone.panelTool, cornerstone.measurements], rightPanelResizable: true, // leftPanelClosed: true, viewports: [ From 806715d4c150c82980873aa34ba484acf4431f61 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh <148532903+Adithyan-Dinesh-Trenser@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:01:58 +0530 Subject: [PATCH 05/14] Updated the cod-dicomweb-server version --- extensions/default/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/default/package.json b/extensions/default/package.json index 6ee1439efc5..2a2dc95d716 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -49,6 +49,6 @@ "@cornerstonejs/calculate-suv": "^1.1.0", "lodash.get": "^4.4.2", "lodash.uniqby": "^4.7.0", - "cod-dicomweb-server": "^1.3.11" + "cod-dicomweb-server": "^1.3.12" } } diff --git a/yarn.lock b/yarn.lock index be216805991..5b7174adeab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7806,10 +7806,10 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== -cod-dicomweb-server@^1.3.11: - version "1.3.11" - resolved "https://registry.yarnpkg.com/cod-dicomweb-server/-/cod-dicomweb-server-1.3.11.tgz#a8e0d0111ac890bd5651887a1deaaa6f4bcdb17f" - integrity sha512-HqcshVYzt4dWAKZjEsK+QYSE3vqJ4L5xXqCklDmGyQhRyANDN3Y6MedM3K9WBhl+Utw9qvenrLc+lbGrjt3L0A== +cod-dicomweb-server@^1.3.12: + version "1.3.12" + resolved "https://registry.yarnpkg.com/cod-dicomweb-server/-/cod-dicomweb-server-1.3.12.tgz#3e84e6e0b8e9b3e649524fb11f7d47ecb1ae86f9" + integrity sha512-X56eSXhlpMkL+Wki24cx1KtC06xD+Qf47jWNJ6F6AvMsyjVg57PhQ8nSu1vmAVAHOfKRfGZMV6BGfWwAeMWsQQ== dependencies: comlink "^4.4.2" dicom-parser "^1.8.21" From fad70db89f29ad266b112389ae0e71a7d1b9136c Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh <148532903+Adithyan-Dinesh-Trenser@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:06:10 +0530 Subject: [PATCH 06/14] OPFS management tool improvements * Added manual purge dropdown to delete OPFS files older than a specific time. * Displayed total file size under the table(bottom-center). * Displayed the collective file size of selected rows next to the selected rows count( bottom-left ). * Fixed the issue with StudyDescription not included in the global search. * Fixed the issue with LastUpdated using the formatted value for sorting. --- .../OPFSManagementTool/OPFSManagementTool.tsx | 88 +++++++++++++------ .../OPFSManagementTool/constants.ts | 27 ++++++ .../components/OPFSManagementTool/utils.ts | 70 +++++++++++++++ 3 files changed, 158 insertions(+), 27 deletions(-) diff --git a/extensions/cornerstone/src/components/OPFSManagementTool/OPFSManagementTool.tsx b/extensions/cornerstone/src/components/OPFSManagementTool/OPFSManagementTool.tsx index 4eb0d6fcce4..d6c4adb92c5 100644 --- a/extensions/cornerstone/src/components/OPFSManagementTool/OPFSManagementTool.tsx +++ b/extensions/cornerstone/src/components/OPFSManagementTool/OPFSManagementTool.tsx @@ -28,9 +28,12 @@ import { Study } from './types'; import { clearPreviousOPFSVersionData, deleteFoldersFromOPFS, + formatSize, getOPFSData, hybridGlobalFilter, + purgeOldFilesFromOPFS, } from './utils'; +import { OPFS_PURGE_METADATA } from './constants'; const columnHelper = createColumnHelper(); @@ -66,23 +69,21 @@ const columns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - StudyInstanceUID - + StudyInstanceUID ); }, cell: ({ row }) =>
{row.getValue('study-uid')}
, }, - { - accessorKey: 'study-description', + columnHelper.accessor('study-description', { + accessorFn: row => row['study-description'] || '', header: ({ column }) => { return ( ); }, @@ -97,7 +98,7 @@ const columns: ColumnDef[] = [ ); }, - }, + }), columnHelper.accessor('study-modalities', { accessorFn: row => row['study-modalities'].sort().join(', '), header: ({ column }) => { @@ -106,8 +107,7 @@ const columns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - Modalities - + Modalities ); }, @@ -126,18 +126,7 @@ const columns: ColumnDef[] = [ columnHelper.accessor('study-size', { accessorFn: row => { const size: number = row['study-size']; - const oneGB = 1024 * 1024 * 1024, - oneMB = 1024 * 1024, - oneKB = 1024; - - if (size >= oneGB) { - return `${(size / oneGB).toFixed(2)} GB`; - } else if (size >= oneMB) { - return `${(size / oneMB).toFixed(2)} MB`; - } else if (size >= oneKB) { - return `${(size / oneKB).toFixed(2)} KB`; - } - return `${size} bytes`; + return formatSize(size); }, sortingFn: (rowA, rowB, columnId) => { const sizeA = rowA.original[columnId]; @@ -157,27 +146,30 @@ const columns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - Size - + Size ); }, cell: ({ row }) =>
{row.getValue('study-size')}
, }), columnHelper.accessor('study-last-modified', { - accessorFn: row => new Date(row['study-last-modified']).toGMTString(), + accessorFn: row => new Date(row['study-last-modified']), + sortingFn: 'datetime', header: ({ column }) => { return ( ); }, - cell: ({ row }) =>
{row.getValue('study-last-modified')}
, + cell: ({ row }) => { + const value: Date = row.getValue('study-last-modified'); + const formattedDate = value.toDateString() + ', ' + value.toLocaleTimeString(); + return
{formattedDate}
; + }, }), { id: 'actions', @@ -247,6 +239,7 @@ export default function OPFSManagementTool() { const refreshOPFSData = async () => { const fetchedData = await getOPFSData(); + table.toggleAllPageRowsSelected(false); setData(fetchedData); }; @@ -265,6 +258,19 @@ export default function OPFSManagementTool() { } }; + const purgeOldFiles = async (time: number) => { + await purgeOldFilesFromOPFS(time); + refreshOPFSData(); + }; + + const calculateTotalSize = (list: Study[]) => { + const totalSize = list.reduce((total, study) => { + return total + study['study-size']; + }, 0); + + return formatSize(totalSize); + }; + return (
@@ -300,6 +306,28 @@ export default function OPFSManagementTool() { > Debug Copy + + + + + + Files older than + {OPFS_PURGE_METADATA.map(option => ( + purgeOldFiles(option.time)} + > + {option.label} + + ))} + +
+
+ Total size: {calculateTotalSize(data)}