fix(SR): Added support for spline and live wire SR items. (#5870)
* fix(SR): Added support for spline and live wire SR items. * Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Add a script to checkout a worktree for test builds * fix: Allow download for testing sr validator * Remove script that wasn't intended to be included * Bump CS3D version. * PR comments - simplify code and use single codepath for download * Allow both download and save buttons for SEG and RTSTRUCT --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
13 files changed
+220
-160
No files matched your search
@@ -1,5 +1,3 @@
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} servicesManager
|
||||
@@ -7,7 +5,8 @@ import { DicomMetadataStore } from '@ohif/core';
|
||||
async function createReportAsync({
|
||||
servicesManager,
|
||||
getReport,
|
||||
reportType = 'measurement',
|
||||
reportType = 'Measurements',
|
||||
successMessage,
|
||||
}: withAppTypes) {
|
||||
const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services;
|
||||
|
||||
@@ -18,18 +17,15 @@ async function createReportAsync({
|
||||
return;
|
||||
}
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
// automatically calls makeDisplaySets
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
// addInstances is called by the store command (storeMeasurements/storeSegmentation),
|
||||
// so the display set should already exist at this point.
|
||||
const displaySet = displaySetService.getMostRecentDisplaySet();
|
||||
|
||||
const displaySetInstanceUID = displaySet.displaySetInstanceUID;
|
||||
|
||||
uiNotificationService.show({
|
||||
title: 'Create Report',
|
||||
message: `${reportType} saved successfully`,
|
||||
message: successMessage ?? `${reportType} saved successfully`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function CreateReportDialogPrompt({
|
||||
predecessorImageId,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
enableDownload = false,
|
||||
}): Promise<{
|
||||
value: string;
|
||||
dataSourceName: string;
|
||||
@@ -59,6 +60,7 @@ export default function CreateReportDialogPrompt({
|
||||
predecessorImageId,
|
||||
minSeriesNumber,
|
||||
modality,
|
||||
enableDownload,
|
||||
onSave: async ({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Types, DicomMetadataStore } from '@ohif/core';
|
||||
import { Types, DicomMetadataStore, utils } from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
const { downloadBlob } = utils;
|
||||
|
||||
import { ContextMenuController } from './CustomizableContextMenu';
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
@@ -768,6 +771,67 @@ const commandsModule = ({
|
||||
|
||||
setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a store function based on the data source type.
|
||||
* @param dataSource - 'download', 'copyToClipboard', or a named data source
|
||||
* @param defaultFileName - Default filename for download/clipboard
|
||||
* @param defaultContentType - Default content type for clipboard
|
||||
* @returns A store function, or null if no valid store exists
|
||||
*/
|
||||
createStoreFunction: ({ dataSource, defaultFileName, defaultContentType }) => {
|
||||
if (dataSource === 'download') {
|
||||
return async dicom => {
|
||||
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
||||
DicomMetadataStore.addInstances(instances, true);
|
||||
if (instances.length !== 1) {
|
||||
throw new Error('Download only supports a single DICOM instance');
|
||||
}
|
||||
const reportBlob = dcmjs.data.datasetToBlob(instances[0]);
|
||||
downloadBlob(reportBlob, { filename: defaultFileName || 'dicom.dcm' });
|
||||
};
|
||||
}
|
||||
|
||||
if (dataSource === 'copyToClipboard') {
|
||||
return async dicom => {
|
||||
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
||||
DicomMetadataStore.addInstances(instances, true);
|
||||
if (instances.length !== 1) {
|
||||
throw new Error('Copy to clipboard only supports a single DICOM instance');
|
||||
}
|
||||
const reportBlob = dcmjs.data.datasetToBlob(instances[0]);
|
||||
const type = defaultContentType || 'application/dicom';
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({ [type]: new Blob([reportBlob], { type }) }),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
// DICOM STOW path — resolve the named data source
|
||||
const dataSources = extensionManager.getDataSources(dataSource);
|
||||
const resolvedDataSource = dataSources?.[0];
|
||||
if (!resolvedDataSource?.store?.dicom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return async (dicom, { dicomDict } = {}) => {
|
||||
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
||||
const config = resolvedDataSource.getConfig?.();
|
||||
if (config?.wadoRoot) {
|
||||
instances.forEach(instance => {
|
||||
instance.wadoRoot = config.wadoRoot;
|
||||
});
|
||||
}
|
||||
DicomMetadataStore.addInstances(instances, true);
|
||||
for (const instance of instances) {
|
||||
await resolvedDataSource.store.dicom(instance, null, dicomDict);
|
||||
}
|
||||
const studyUIDs = new Set(instances.map(i => i.StudyInstanceUID).filter(Boolean));
|
||||
for (const uid of studyUIDs) {
|
||||
resolvedDataSource.deleteStudyMetadataPromise(uid);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@@ -796,6 +860,7 @@ const commandsModule = ({
|
||||
scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView,
|
||||
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
|
||||
removeDisplaySetLayer: actions.removeDisplaySetLayer,
|
||||
createStoreFunction: actions.createStoreFunction,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InputDialog } from '@ohif/ui-next';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
@@ -21,6 +22,7 @@ type ReportDialogProps = {
|
||||
priorSeriesNumber: number;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
enableDownload?: boolean;
|
||||
};
|
||||
|
||||
function ReportDialog({
|
||||
@@ -31,8 +33,11 @@ function ReportDialog({
|
||||
hide,
|
||||
onSave,
|
||||
onCancel,
|
||||
enableDownload = false,
|
||||
}: ReportDialogProps) {
|
||||
const { t } = useTranslation('Buttons');
|
||||
const { servicesManager } = useSystem();
|
||||
const actionTakenRef = useRef(false);
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
|
||||
dataSources?.[0]?.value ?? null
|
||||
);
|
||||
@@ -72,6 +77,7 @@ function ReportDialog({
|
||||
}, [selectedSeries, seriesOptions]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
actionTakenRef.current = true;
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
@@ -82,11 +88,33 @@ function ReportDialog({
|
||||
}, [selectedDataSource, selectedSeries, reportName, hide, onSave]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
actionTakenRef.current = true;
|
||||
onCancel();
|
||||
hide();
|
||||
}, [onCancel, hide]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
actionTakenRef.current = true;
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: 'download',
|
||||
priorSeriesNumber: Math.max(...seriesOptions.map(it => it.seriesNumber)),
|
||||
series: selectedSeries,
|
||||
});
|
||||
hide();
|
||||
}, [selectedDataSource, selectedSeries, reportName, hide, onSave]);
|
||||
|
||||
// Handles the close dialog button/external close as a cancel
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!actionTakenRef.current) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
}, [onCancel]);
|
||||
|
||||
const showDataSourceSelect = dataSources?.length > 1;
|
||||
const showDownloadButton = enableDownload;
|
||||
|
||||
return (
|
||||
<div className="text-foreground flex min-w-[400px] max-w-md flex-col">
|
||||
@@ -181,9 +209,11 @@ function ReportDialog({
|
||||
<div className="flex justify-end gap-2">
|
||||
<InputDialog>
|
||||
<InputDialog.Actions>
|
||||
<InputDialog.ActionsSecondary onClick={handleCancel}>
|
||||
Cancel
|
||||
</InputDialog.ActionsSecondary>
|
||||
{showDownloadButton && (
|
||||
<InputDialog.ActionsSecondary onClick={handleDownload}>
|
||||
{t('Download')}
|
||||
</InputDialog.ActionsSecondary>
|
||||
)}
|
||||
<InputDialog.ActionsPrimary onClick={handleSave}>Save</InputDialog.ActionsPrimary>
|
||||
</InputDialog.Actions>
|
||||
</InputDialog>
|
||||
|
||||
@@ -35,21 +35,19 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
minSeriesNumber: 3000,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
enableDownload: true,
|
||||
});
|
||||
|
||||
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources(promptResult.dataSourceName);
|
||||
const dataSource = dataSources[0];
|
||||
|
||||
const { series, priorSeriesNumber, value: reportName } = promptResult;
|
||||
const { series, priorSeriesNumber, value: reportName, dataSourceName } = promptResult;
|
||||
const SeriesDescription = reportName || defaultSaveTitle;
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
const getReport = async () =>
|
||||
commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData,
|
||||
dataSource,
|
||||
dataSource: dataSourceName,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options: {
|
||||
SeriesDescription,
|
||||
@@ -59,12 +57,12 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
);
|
||||
};
|
||||
|
||||
displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport,
|
||||
});
|
||||
} else if (promptResult.action === RESPONSE.CANCEL) {
|
||||
} else if (promptResult.action === PROMPT_RESPONSES.CANCEL) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user