ui(components): New Dialog and Modal components for ui-next (#4772)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
1 parent
62b4fa3e89
commit
a3b51aa8db
90 files changed
+3459
-1357
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { Types } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import {
|
||||
@@ -8,11 +7,13 @@ import {
|
||||
utilities,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
@@ -51,6 +52,7 @@ const commandsModule = ({
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
} = servicesManager.services as AppTypes.Services;
|
||||
|
||||
const actions = {
|
||||
@@ -226,14 +228,6 @@ const commandsModule = ({
|
||||
* otherwise throws an error.
|
||||
*/
|
||||
storeSegmentation: async ({ segmentationId, dataSource }) => {
|
||||
const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action !== 1 && !promptResult.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
if (!segmentation) {
|
||||
@@ -241,33 +235,50 @@ const commandsModule = ({
|
||||
}
|
||||
|
||||
const { label } = segmentation;
|
||||
const SeriesDescription = promptResult.value || label || 'Research Derived Series';
|
||||
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource();
|
||||
|
||||
const generatedData = actions.generateSegmentation({
|
||||
segmentationId,
|
||||
options: {
|
||||
SeriesDescription,
|
||||
},
|
||||
const {
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
action,
|
||||
} = await createReportDialogPrompt({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
title: 'Store Segmentation',
|
||||
});
|
||||
|
||||
if (!generatedData || !generatedData.dataset) {
|
||||
throw new Error('Error during segmentation generation');
|
||||
if (action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
try {
|
||||
const selectedDataSourceConfig = selectedDataSource
|
||||
? extensionManager.getDataSources(selectedDataSource)[0]
|
||||
: defaultDataSource;
|
||||
|
||||
const generatedData = actions.generateSegmentation({
|
||||
segmentationId,
|
||||
options: {
|
||||
SeriesDescription: reportName || label || 'Research Derived Series',
|
||||
},
|
||||
});
|
||||
|
||||
if (!generatedData || !generatedData.dataset) {
|
||||
throw new Error('Error during segmentation generation');
|
||||
}
|
||||
|
||||
const { dataset: naturalizedReport } = generatedData;
|
||||
|
||||
await selectedDataSourceConfig.store.dicom(naturalizedReport);
|
||||
|
||||
// add the information for where we stored it to the instance as well
|
||||
naturalizedReport.wadoRoot = selectedDataSourceConfig.getConfig().wadoRoot;
|
||||
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
return naturalizedReport;
|
||||
} catch (error) {
|
||||
console.debug('Error storing segmentation:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const { dataset: naturalizedReport } = generatedData;
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
// automatically calls makeDisplaySets
|
||||
|
||||
// add the information for where we stored it to the instance as well
|
||||
naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot;
|
||||
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
return naturalizedReport;
|
||||
},
|
||||
/**
|
||||
* Converts segmentations into RTSS for download.
|
||||
|
||||
@@ -146,6 +146,11 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
|
||||
const { viewportId, element } = evt.detail;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEnabledElement(viewportId, element);
|
||||
setEnabledVPElement(element);
|
||||
|
||||
@@ -323,10 +328,10 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
// Set up the window level action menu in the viewport action corners.
|
||||
useEffect(() => {
|
||||
const windowLevelActionMenu = customizationService.getCustomization(
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
);
|
||||
const segmentationOverlay = customizationService.getCustomization(
|
||||
'viewportActionMenu.segmentationOverlay'
|
||||
'viewportActionMenu.segmentationOverlay'
|
||||
);
|
||||
|
||||
if (windowLevelActionMenu?.enabled) {
|
||||
@@ -362,13 +367,7 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
location: segmentationOverlay.location,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
displaySets,
|
||||
viewportId,
|
||||
viewportActionCornersService,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
]);
|
||||
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
|
||||
|
||||
const { ref: resizeRef } = useResizeDetector({
|
||||
onResize,
|
||||
|
||||
@@ -18,21 +18,19 @@ import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import { Types as OhifTypes, utils } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import {
|
||||
callLabelAutocompleteDialog,
|
||||
showLabelAnnotationPopup,
|
||||
callInputDialogAutoComplete,
|
||||
createReportAsync,
|
||||
callInputDialog,
|
||||
colorPickerDialog,
|
||||
callInputDialog,
|
||||
} from '@ohif/extension-default';
|
||||
import { vec3, mat4 } from 'gl-matrix';
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
||||
const toggleSyncFunctions = {
|
||||
imageSlice: toggleImageSliceSync,
|
||||
@@ -200,26 +198,55 @@ function commandsModule({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Show the measurement labelling input dialog and update the label
|
||||
* on the measurement with a response if not cancelled.
|
||||
* Common logic for handling measurement label updates through dialog
|
||||
* @param uid - measurement uid
|
||||
* @returns Promise that resolves when the label is updated
|
||||
*/
|
||||
setMeasurementLabel: ({ uid }) => {
|
||||
_handleMeasurementLabelDialog: async uid => {
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent).then(
|
||||
(val: Map<any, any>) => {
|
||||
measurementService.update(
|
||||
uid,
|
||||
{
|
||||
...val,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
if (!measurement) {
|
||||
console.debug('No measurement found for label editing');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!labelConfig) {
|
||||
const label = await callInputDialog({
|
||||
uiDialogService,
|
||||
title: 'Edit Measurement Label',
|
||||
placeholder: measurement.label || 'Enter new label',
|
||||
defaultValue: measurement.label,
|
||||
});
|
||||
|
||||
if (label !== undefined && label !== null) {
|
||||
measurementService.update(uid, { ...measurement, label }, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const val = await callInputDialogAutoComplete({
|
||||
measurement,
|
||||
uiDialogService,
|
||||
labelConfig,
|
||||
renderContent,
|
||||
});
|
||||
|
||||
if (val !== undefined && val !== null) {
|
||||
measurementService.update(uid, { ...val }, true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Show the measurement labelling input dialog and update the label
|
||||
* on the measurement with a response if not cancelled.
|
||||
*/
|
||||
setMeasurementLabel: async ({ uid }) => {
|
||||
await actions._handleMeasurementLabelDialog(uid);
|
||||
},
|
||||
renameMeasurement: async ({ uid }) => {
|
||||
await actions._handleMeasurementLabelDialog(uid);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param props - containing the updates to apply
|
||||
@@ -321,23 +348,6 @@ function commandsModule({
|
||||
measurementService.remove(uid);
|
||||
},
|
||||
|
||||
renameMeasurement: ({ uid }) => {
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent).then(
|
||||
val => {
|
||||
measurementService.update(
|
||||
uid,
|
||||
{
|
||||
...val,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
toggleLockMeasurement: ({ uid }) => {
|
||||
measurementService.toggleLockMeasurement(uid);
|
||||
},
|
||||
@@ -375,10 +385,10 @@ function commandsModule({
|
||||
|
||||
viewportGridService.setActiveViewportId(viewportId);
|
||||
},
|
||||
arrowTextCallback: ({ callback, data, uid }) => {
|
||||
arrowTextCallback: ({ callback }) => {
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, renderContent);
|
||||
callInputDialogAutoComplete(uiDialogService, callback, {}, labelConfig, renderContent);
|
||||
},
|
||||
toggleCine: () => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
@@ -568,6 +578,7 @@ function commandsModule({
|
||||
],
|
||||
});
|
||||
},
|
||||
// capture viewport
|
||||
showDownloadViewportModal: () => {
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
|
||||
@@ -589,10 +600,9 @@ function commandsModule({
|
||||
title: 'Download High Quality Image',
|
||||
contentProps: {
|
||||
activeViewportId,
|
||||
onClose: uiModalService.hide,
|
||||
cornerstoneViewportService,
|
||||
},
|
||||
containerDimensions: 'w-[70%] max-w-[900px]',
|
||||
containerClassName: 'max-w-4xl p-4',
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1138,14 +1148,12 @@ function commandsModule({
|
||||
*/
|
||||
storeSegmentationCommand: async ({ segmentationId }) => {
|
||||
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||
const datasources = extensionManager.getActiveDataSource();
|
||||
|
||||
const displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport: () =>
|
||||
commandsManager.runCommand('storeSegmentation', {
|
||||
segmentationId,
|
||||
dataSource: datasources[0],
|
||||
}),
|
||||
reportType: 'Segmentation',
|
||||
});
|
||||
@@ -1271,7 +1279,7 @@ function commandsModule({
|
||||
segmentationService.setStyle({ type }, { fillAlphaInactive: value });
|
||||
},
|
||||
|
||||
editSegmentLabel: ({ segmentationId, segmentIndex }) => {
|
||||
editSegmentLabel: async ({ segmentationId, segmentIndex }) => {
|
||||
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -1280,19 +1288,14 @@ function commandsModule({
|
||||
}
|
||||
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { label } = segment;
|
||||
|
||||
const callback = (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
callInputDialog({
|
||||
uiDialogService,
|
||||
title: 'Edit Segment Label',
|
||||
placeholder: 'Enter new label',
|
||||
defaultValue: segment.label,
|
||||
}).then(label => {
|
||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||
};
|
||||
|
||||
callInputDialog(uiDialogService, label, callback, false, {
|
||||
dialogTitle: 'Edit Segment Label',
|
||||
inputLabel: 'Enter new label',
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1306,17 +1309,13 @@ function commandsModule({
|
||||
|
||||
const { label } = segmentation;
|
||||
|
||||
const callback = (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
callInputDialog({
|
||||
uiDialogService,
|
||||
title: 'Edit Segmentation Label',
|
||||
placeholder: 'Enter new label',
|
||||
defaultValue: label,
|
||||
}).then(label => {
|
||||
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
|
||||
};
|
||||
|
||||
callInputDialog(uiDialogService, label, callback, false, {
|
||||
dialogTitle: 'Edit Segmentation Label',
|
||||
inputLabel: 'Enter new label',
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1333,13 +1332,16 @@ function commandsModule({
|
||||
a: color[3] / 255.0,
|
||||
};
|
||||
|
||||
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||
if (actionId === 'cancel') {
|
||||
return;
|
||||
}
|
||||
|
||||
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
||||
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
||||
uiDialogService.show({
|
||||
content: colorPickerDialog,
|
||||
title: 'Segment Color',
|
||||
contentProps: {
|
||||
value: rgbaColor,
|
||||
onSave: newRgbaColor => {
|
||||
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
||||
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1395,6 +1397,9 @@ function commandsModule({
|
||||
setMeasurementLabel: {
|
||||
commandFn: actions.setMeasurementLabel,
|
||||
},
|
||||
renameMeasurement: {
|
||||
commandFn: actions.renameMeasurement,
|
||||
},
|
||||
updateMeasurement: {
|
||||
commandFn: actions.updateMeasurement,
|
||||
},
|
||||
@@ -1407,9 +1412,6 @@ function commandsModule({
|
||||
removeMeasurement: {
|
||||
commandFn: actions.removeMeasurement,
|
||||
},
|
||||
renameMeasurement: {
|
||||
commandFn: actions.renameMeasurement,
|
||||
},
|
||||
toggleLockMeasurement: {
|
||||
commandFn: actions.toggleLockMeasurement,
|
||||
},
|
||||
|
||||
+4
-6
@@ -10,21 +10,19 @@ export function VolumeRenderingPresets({
|
||||
commandsManager,
|
||||
volumeRenderingPresets,
|
||||
}: VolumeRenderingPresetsProps): ReactElement {
|
||||
const { uiModalService } = servicesManager.services;
|
||||
const { uiDialogService } = servicesManager.services;
|
||||
|
||||
const onClickPresets = () => {
|
||||
uiModalService.show({
|
||||
uiDialogService.show({
|
||||
id: 'volume-rendering-presets',
|
||||
content: VolumeRenderingPresetsContent,
|
||||
title: 'Rendering Presets',
|
||||
movable: true,
|
||||
isDraggable: true,
|
||||
contentProps: {
|
||||
onClose: uiModalService.hide,
|
||||
presets: volumeRenderingPresets,
|
||||
viewportId,
|
||||
commandsManager,
|
||||
},
|
||||
containerDimensions: 'h-[543px] w-[460px]',
|
||||
contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+55
-68
@@ -1,29 +1,24 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { Icons, FooterAction } from '@ohif/ui-next';
|
||||
import React, { ReactElement, useState, useCallback } from 'react';
|
||||
import { Button, InputFilterText } from '@ohif/ui';
|
||||
import { PresetDialog } from '@ohif/ui-next';
|
||||
import { ViewportPreset, VolumeRenderingPresetsContentProps } from '../../types/ViewportPresets';
|
||||
|
||||
interface Props extends VolumeRenderingPresetsContentProps {
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
export function VolumeRenderingPresetsContent({
|
||||
presets,
|
||||
viewportId,
|
||||
commandsManager,
|
||||
onClose,
|
||||
}: VolumeRenderingPresetsContentProps): ReactElement {
|
||||
const [filteredPresets, setFilteredPresets] = useState(presets);
|
||||
hide,
|
||||
}: Props): ReactElement {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [selectedPreset, setSelectedPreset] = useState<ViewportPreset | null>(null);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchValue(value);
|
||||
const filtered = value
|
||||
? presets.filter(preset => preset.name.toLowerCase().includes(value.toLowerCase()))
|
||||
: presets;
|
||||
setFilteredPresets(filtered);
|
||||
},
|
||||
[presets]
|
||||
);
|
||||
const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(
|
||||
props => {
|
||||
@@ -34,62 +29,54 @@ export function VolumeRenderingPresetsContent({
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const filteredPresets = searchValue
|
||||
? presets.filter(preset => preset.name.toLowerCase().includes(searchValue.toLowerCase()))
|
||||
: presets;
|
||||
|
||||
const formatLabel = (label: string, maxChars: number) => {
|
||||
return label.length > maxChars ? `${label.slice(0, maxChars)}...` : label;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full w-full flex-col justify-between">
|
||||
<div className="border-secondary-light h-[433px] w-full overflow-hidden rounded border bg-black px-2.5">
|
||||
<div className="flex h-[46px] w-full items-center justify-start">
|
||||
<div className="h-[26px] w-[200px]">
|
||||
<InputFilterText
|
||||
value={searchValue}
|
||||
onDebounceChange={handleSearchChange}
|
||||
placeholder={'Search all'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ohif-scrollbar overflow h-[385px] w-full overflow-y-auto">
|
||||
<div className="grid grid-cols-4 gap-3 pt-2 pr-3">
|
||||
{filteredPresets.map((preset, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex cursor-pointer flex-col items-start"
|
||||
onClick={() => {
|
||||
setSelectedPreset(preset);
|
||||
handleApply({ preset: preset.name, viewportId });
|
||||
}}
|
||||
>
|
||||
<Icons.ByName
|
||||
name={preset.name}
|
||||
className={
|
||||
selectedPreset?.name === preset.name
|
||||
? 'border-primary-light h-[75px] w-[95px] max-w-none rounded border-2'
|
||||
: 'hover:border-primary-light h-[75px] w-[95px] max-w-none rounded border-2 border-black'
|
||||
}
|
||||
/>
|
||||
<label className="text-aqua-pale mt-2 text-left text-xs">
|
||||
{formatLabel(preset.name, 11)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="flex h-[60px] w-full items-center justify-end">
|
||||
<div className="flex">
|
||||
<Button
|
||||
name="Cancel"
|
||||
size={ButtonEnums.size.medium}
|
||||
type={ButtonEnums.type.secondary}
|
||||
onClick={onClose}
|
||||
>
|
||||
{' '}
|
||||
Cancel{' '}
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<PresetDialog className="h-[500px]">
|
||||
<PresetDialog.PresetBody>
|
||||
<PresetDialog.PresetFilter>
|
||||
<PresetDialog.PresetSearch
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Search all"
|
||||
/>
|
||||
</PresetDialog.PresetFilter>
|
||||
<PresetDialog.PresetGrid>
|
||||
{filteredPresets.map((preset, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex cursor-pointer flex-col items-start"
|
||||
onClick={() => {
|
||||
setSelectedPreset(preset);
|
||||
handleApply({ preset: preset.name, viewportId });
|
||||
}}
|
||||
>
|
||||
<Icons.ByName
|
||||
name={preset.name}
|
||||
className={
|
||||
selectedPreset?.name === preset.name
|
||||
? 'border-highlight h-[75px] w-[95px] max-w-none rounded border-2'
|
||||
: 'hover:border-highlight h-[75px] w-[95px] max-w-none rounded border-2 border-black'
|
||||
}
|
||||
/>
|
||||
<label className="text-muted-foreground mt-1 text-left text-xs">
|
||||
{formatLabel(preset.name, 11)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</PresetDialog.PresetGrid>
|
||||
</PresetDialog.PresetBody>
|
||||
<FooterAction className="mt-4 flex-shrink-0">
|
||||
<FooterAction.Right>
|
||||
<FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
|
||||
</FooterAction.Right>
|
||||
</FooterAction>
|
||||
</PresetDialog>
|
||||
);
|
||||
}
|
||||
+5
-2
@@ -21,8 +21,9 @@ export function getWindowLevelActionMenu({
|
||||
const { volumeRenderingPresets, volumeRenderingQualityRange } =
|
||||
customizationService.getCustomization('cornerstone.3dVolumeRendering');
|
||||
const WindowLevelActionMenu = customizationService.getCustomization(
|
||||
'cornerstone.windowLevelActionMenu'
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
);
|
||||
console.debug('🚀 ~ WindowLevelActionMenu:', WindowLevelActionMenu);
|
||||
const displaySetPresets = displaySets
|
||||
.filter(displaySet => presets[displaySet.Modality])
|
||||
.map(displaySet => {
|
||||
@@ -37,8 +38,10 @@ export function getWindowLevelActionMenu({
|
||||
return null;
|
||||
}
|
||||
|
||||
const WindowLevelActionMenuComponent = WindowLevelActionMenu?.component;
|
||||
|
||||
return (
|
||||
<WindowLevelActionMenu
|
||||
<WindowLevelActionMenuComponent
|
||||
viewportId={viewportId}
|
||||
element={element}
|
||||
presets={displaySetPresets}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ImageModal, FooterAction } from '@ohif/ui-next';
|
||||
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
const DEFAULT_FILENAME = 'image';
|
||||
|
||||
interface ViewportDownloadFormNewProps {
|
||||
onClose: () => void;
|
||||
defaultSize: number;
|
||||
fileTypeOptions: Array<{ value: string; label: string }>;
|
||||
viewportId: string;
|
||||
showAnnotations: boolean;
|
||||
onAnnotationsChange: (show: boolean) => void;
|
||||
dimensions: { width: number; height: number };
|
||||
onDimensionsChange: (dimensions: { width: number; height: number }) => void;
|
||||
onEnableViewport: (element: HTMLElement) => void;
|
||||
onDisableViewport: () => void;
|
||||
onDownload: (filename: string, fileType: string) => void;
|
||||
warningState: { enabled: boolean; value: string };
|
||||
}
|
||||
|
||||
function ViewportDownloadFormNew({
|
||||
onClose,
|
||||
defaultSize,
|
||||
fileTypeOptions,
|
||||
viewportId,
|
||||
showAnnotations,
|
||||
onAnnotationsChange,
|
||||
dimensions,
|
||||
warningState,
|
||||
onDimensionsChange,
|
||||
onEnableViewport,
|
||||
onDisableViewport,
|
||||
onDownload,
|
||||
}: ViewportDownloadFormNewProps) {
|
||||
const [viewportElement, setViewportElement] = useState<HTMLElement | null>(null);
|
||||
const [showWarningMessage, setShowWarningMessage] = useState(true);
|
||||
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
||||
const [fileType, setFileType] = useState('jpg');
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewportElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
onEnableViewport(viewportElement);
|
||||
|
||||
return () => {
|
||||
onDisableViewport();
|
||||
};
|
||||
}, [onDisableViewport, onEnableViewport, viewportElement]);
|
||||
|
||||
return (
|
||||
<ImageModal>
|
||||
<ImageModal.Body>
|
||||
<ImageModal.ImageVisual>
|
||||
<div
|
||||
style={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width,
|
||||
position: 'relative',
|
||||
}}
|
||||
data-viewport-uid={viewportId}
|
||||
ref={setViewportElement}
|
||||
>
|
||||
{warningState.enabled && showWarningMessage && (
|
||||
<div
|
||||
className="text-foreground absolute left-1/2 bottom-[5px] z-[1000] -translate-x-1/2 whitespace-nowrap rounded bg-black p-3 text-xs font-bold"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{warningState.value}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ImageModal.ImageVisual>
|
||||
|
||||
<ImageModal.ImageOptions>
|
||||
<div className="flex items-end space-x-2">
|
||||
<ImageModal.Filename
|
||||
value={filename}
|
||||
onChange={e => setFilename(e.target.value)}
|
||||
>
|
||||
File name
|
||||
</ImageModal.Filename>
|
||||
<ImageModal.Filetype
|
||||
selected={fileType}
|
||||
onSelect={setFileType}
|
||||
options={fileTypeOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ImageModal.ImageSize
|
||||
width={dimensions.width.toString()}
|
||||
height={dimensions.height.toString()}
|
||||
onWidthChange={e => {
|
||||
onDimensionsChange({
|
||||
...dimensions,
|
||||
width: parseInt(e.target.value) || defaultSize,
|
||||
});
|
||||
}}
|
||||
onHeightChange={e => {
|
||||
onDimensionsChange({
|
||||
...dimensions,
|
||||
height: parseInt(e.target.value) || defaultSize,
|
||||
});
|
||||
}}
|
||||
maxWidth={MAX_TEXTURE_SIZE.toString()}
|
||||
maxHeight={MAX_TEXTURE_SIZE.toString()}
|
||||
>
|
||||
Image size <span className="text-muted-foreground">px</span>
|
||||
</ImageModal.ImageSize>
|
||||
|
||||
<ImageModal.SwitchOption
|
||||
defaultChecked={showAnnotations}
|
||||
checked={showAnnotations}
|
||||
onCheckedChange={onAnnotationsChange}
|
||||
>
|
||||
Include annotations
|
||||
</ImageModal.SwitchOption>
|
||||
{warningState.enabled && (
|
||||
<ImageModal.SwitchOption
|
||||
defaultChecked={showWarningMessage}
|
||||
checked={showWarningMessage}
|
||||
onCheckedChange={setShowWarningMessage}
|
||||
>
|
||||
Include warning message
|
||||
</ImageModal.SwitchOption>
|
||||
)}
|
||||
<FooterAction className="mt-2">
|
||||
<FooterAction.Right>
|
||||
<FooterAction.Secondary onClick={onClose}>Cancel</FooterAction.Secondary>
|
||||
<FooterAction.Primary
|
||||
onClick={() => {
|
||||
onDownload(filename || DEFAULT_FILENAME, fileType);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</FooterAction.Primary>
|
||||
</FooterAction.Right>
|
||||
</FooterAction>
|
||||
</ImageModal.ImageOptions>
|
||||
</ImageModal.Body>
|
||||
</ImageModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
'ohif.captureViewportModal': ViewportDownloadFormNew,
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import viewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
|
||||
import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu';
|
||||
|
||||
export default {
|
||||
'viewportActionMenu.windowLevelActionMenu': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
component: WindowLevelActionMenu,
|
||||
},
|
||||
'viewportActionMenu.segmentationOverlay': {
|
||||
enabled: true,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
'viewportDownload.warningMessage': {
|
||||
enabled: true,
|
||||
value: 'Not For Diagnostic Use',
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu';
|
||||
|
||||
export default {
|
||||
'cornerstone.windowLevelActionMenu': WindowLevelActionMenu,
|
||||
};
|
||||
@@ -8,7 +8,8 @@ import volumeRenderingCustomization from './customizations/volumeRenderingCustom
|
||||
import colorbarCustomization from './customizations/colorbarCustomization';
|
||||
import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization';
|
||||
import miscCustomization from './customizations/miscCustomization';
|
||||
import windowLevelActionMenuCustomization from './customizations/windowLevelActionMenuCustomization';
|
||||
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
|
||||
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
|
||||
import viewportActionMenuCustomizations from './customizations/viewportActionMenuCustomizations';
|
||||
|
||||
function getCustomizationModule({ commandsManager, servicesManager }) {
|
||||
@@ -26,7 +27,8 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
|
||||
...colorbarCustomization,
|
||||
...windowLevelPresetsCustomization,
|
||||
...miscCustomization,
|
||||
...windowLevelActionMenuCustomization,
|
||||
...captureViewportModalCustomization,
|
||||
...viewportDownloadWarningCustomization,
|
||||
...viewportActionMenuCustomizations,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@ import PanelMeasurement from './panels/PanelMeasurement';
|
||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||
import { useSegmentations } from './hooks/useSegmentations';
|
||||
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
import utils from './utils';
|
||||
|
||||
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
||||
@@ -255,6 +256,7 @@ export {
|
||||
PanelMeasurement,
|
||||
DicomUpload,
|
||||
StudySummaryFromMetadata,
|
||||
CornerstoneViewportDownloadForm,
|
||||
utils,
|
||||
};
|
||||
export default cornerstoneExtension;
|
||||
@@ -84,35 +84,14 @@ export function onCompletedCalibrationLine(
|
||||
return;
|
||||
}
|
||||
|
||||
callInputDialog(
|
||||
callInputDialog({
|
||||
uiDialogService,
|
||||
{
|
||||
text: '',
|
||||
label: `${length}`,
|
||||
},
|
||||
(value, id) => {
|
||||
if (id === 'save') {
|
||||
adjustCalibration(Number.parseFloat(value));
|
||||
resolve(true);
|
||||
} else {
|
||||
reject('cancel');
|
||||
}
|
||||
},
|
||||
false,
|
||||
{
|
||||
dialogTitle: 'Calibration',
|
||||
inputLabel: 'Actual Physical distance (mm)',
|
||||
|
||||
// the input value must be a number
|
||||
validateFunc: val => {
|
||||
try {
|
||||
const v = Number.parseFloat(val);
|
||||
return !isNaN(v) && v !== 0.0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
title: 'Calibration',
|
||||
placeholder: 'Actual Physical distance (mm)',
|
||||
defaultValue: `${length}`,
|
||||
}).then(newValue => {
|
||||
adjustCalibration(Number.parseFloat(newValue));
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,53 +1,66 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import html2canvas from 'html2canvas';
|
||||
import {
|
||||
Enums,
|
||||
getEnabledElement,
|
||||
getOrCreateCanvas,
|
||||
StackViewport,
|
||||
BaseVolumeViewport,
|
||||
} from '@cornerstonejs/core';
|
||||
import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core';
|
||||
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||
import { ViewportDownloadForm } from '@ohif/ui';
|
||||
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||
import { useSystem } from '@ohif/core/src';
|
||||
|
||||
const MINIMUM_SIZE = 100;
|
||||
const DEFAULT_SIZE = 512;
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
|
||||
|
||||
const FILE_TYPE_OPTIONS = [
|
||||
{
|
||||
value: 'jpg',
|
||||
label: 'JPG',
|
||||
},
|
||||
{
|
||||
value: 'png',
|
||||
label: 'PNG',
|
||||
},
|
||||
];
|
||||
|
||||
type ViewportDownloadFormProps = {
|
||||
hide: () => void;
|
||||
activeViewportId: string;
|
||||
};
|
||||
|
||||
const CornerstoneViewportDownloadForm = ({
|
||||
onClose,
|
||||
hide,
|
||||
activeViewportId: activeViewportIdProp,
|
||||
cornerstoneViewportService,
|
||||
}: withAppTypes) => {
|
||||
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
|
||||
const activeViewportElement = enabledElement?.element;
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
}: ViewportDownloadFormProps) => {
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService, cornerstoneViewportService } = servicesManager.services;
|
||||
const [showAnnotations, setShowAnnotations] = useState(true);
|
||||
const [viewportDimensions, setViewportDimensions] = useState({
|
||||
width: DEFAULT_SIZE,
|
||||
height: DEFAULT_SIZE,
|
||||
});
|
||||
|
||||
const {
|
||||
viewportId: activeViewportId,
|
||||
renderingEngineId,
|
||||
viewport: activeViewport,
|
||||
} = activeViewportEnabledElement;
|
||||
const warningState = customizationService.getCustomization('viewportDownload.warningMessage') as {
|
||||
enabled: boolean;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const refViewportEnabledElementOHIF = OHIFgetEnabledElement(activeViewportIdProp);
|
||||
const activeViewportElement = refViewportEnabledElementOHIF?.element;
|
||||
const { viewportId: activeViewportId, renderingEngineId } =
|
||||
getEnabledElement(activeViewportElement);
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
|
||||
|
||||
const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce((acc, toolName) => {
|
||||
const tool = toolGroup.toolOptions[toolName];
|
||||
const { mode, bindings } = tool;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[toolName]: {
|
||||
mode,
|
||||
bindings,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
useEffect(() => {
|
||||
const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce((acc, toolName) => {
|
||||
const tool = toolGroup.toolOptions[toolName];
|
||||
const { mode, bindings } = tool;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[toolName]: { mode, bindings },
|
||||
};
|
||||
}, {});
|
||||
|
||||
return () => {
|
||||
Object.keys(toolModeAndBindings).forEach(toolName => {
|
||||
const { mode, bindings } = toolModeAndBindings[toolName];
|
||||
@@ -56,157 +69,92 @@ const CornerstoneViewportDownloadForm = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const enableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
const { renderingEngine, viewport } = getEnabledElement(activeViewportElement);
|
||||
|
||||
const viewportInput = {
|
||||
viewportId: VIEWPORT_ID,
|
||||
element: viewportElement,
|
||||
type: viewport.type,
|
||||
defaultOptions: {
|
||||
background: viewport.defaultOptions.background,
|
||||
orientation: viewport.defaultOptions.orientation,
|
||||
},
|
||||
};
|
||||
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
}
|
||||
};
|
||||
|
||||
const disableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
const { renderingEngine } = getEnabledElement(viewportElement);
|
||||
return new Promise(resolve => {
|
||||
renderingEngine.disableElement(VIEWPORT_ID);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateViewportPreview = (downloadViewportElement, internalCanvas, fileType) =>
|
||||
new Promise(resolve => {
|
||||
const enabledElement = getEnabledElement(downloadViewportElement);
|
||||
|
||||
const { viewport: downloadViewport, renderingEngine } = enabledElement;
|
||||
|
||||
// Note: Since any trigger of dimensions will update the viewport,
|
||||
// we need to resize the offScreenCanvas to accommodate for the new
|
||||
// dimensions, this is due to the reason that we are using the GPU offScreenCanvas
|
||||
// to render the viewport for the downloadViewport.
|
||||
renderingEngine.resize();
|
||||
|
||||
// Trigger the render on the viewport to update the on screen
|
||||
// downloadViewport.resetCamera();
|
||||
downloadViewport.render();
|
||||
|
||||
downloadViewportElement.addEventListener(
|
||||
Enums.Events.IMAGE_RENDERED,
|
||||
function updateViewport(event) {
|
||||
const enabledElement = getEnabledElement(event.target);
|
||||
const { viewport } = enabledElement;
|
||||
const { element } = viewport;
|
||||
|
||||
const downloadCanvas = getOrCreateCanvas(element);
|
||||
|
||||
const type = 'image/' + fileType;
|
||||
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
||||
|
||||
let newWidth = element.offsetHeight;
|
||||
let newHeight = element.offsetWidth;
|
||||
|
||||
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
||||
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
||||
newHeight *= multiplier;
|
||||
newWidth *= multiplier;
|
||||
}
|
||||
|
||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||
|
||||
downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport);
|
||||
|
||||
// for some reason we need a reset camera here, and I don't know why
|
||||
downloadViewport.resetCamera();
|
||||
const presentation = activeViewport.getViewPresentation();
|
||||
if (downloadViewport.setView) {
|
||||
downloadViewport.setView(activeViewport.getViewReference(), presentation);
|
||||
}
|
||||
downloadViewport.render();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const loadImage = (activeViewportElement, viewportElement, width, height) =>
|
||||
new Promise(resolve => {
|
||||
if (activeViewportElement && viewportElement) {
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
|
||||
if (!activeViewportEnabledElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewport } = activeViewportEnabledElement;
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
|
||||
|
||||
if (downloadViewport instanceof StackViewport) {
|
||||
const imageId = viewport.getCurrentImageId();
|
||||
const properties = viewport.getProperties();
|
||||
|
||||
downloadViewport.setStack([imageId]).then(() => {
|
||||
try {
|
||||
downloadViewport.setProperties(properties);
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||
|
||||
resolve({ width: newWidth, height: newHeight });
|
||||
} catch (e) {
|
||||
// Happens on clicking the cancel button
|
||||
console.warn('Unable to set properties', e);
|
||||
}
|
||||
});
|
||||
} else if (downloadViewport instanceof BaseVolumeViewport) {
|
||||
const actors = viewport.getActors();
|
||||
// downloadViewport.setActors(actors);
|
||||
actors.forEach(actor => {
|
||||
downloadViewport.addActor(actor);
|
||||
});
|
||||
|
||||
downloadViewport.render();
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||
|
||||
resolve({ width: newWidth, height: newHeight });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toggleAnnotations = (toggle, viewportElement, activeViewportElement) => {
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
|
||||
const downloadViewportElement = getEnabledElement(viewportElement);
|
||||
|
||||
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
|
||||
const { viewportId: downloadViewportId } = downloadViewportElement;
|
||||
|
||||
if (!activeViewportEnabledElement || !downloadViewportElement) {
|
||||
const handleEnableViewport = (viewportElement: HTMLElement) => {
|
||||
if (!viewportElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
|
||||
const { viewport } = getEnabledElement(activeViewportElement);
|
||||
|
||||
// add the viewport to the toolGroup
|
||||
const viewportInput = {
|
||||
viewportId: VIEWPORT_ID,
|
||||
element: viewportElement,
|
||||
type: viewport.type,
|
||||
defaultOptions: {
|
||||
background: viewport.defaultOptions.background,
|
||||
orientation: viewport.defaultOptions.orientation,
|
||||
},
|
||||
};
|
||||
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
};
|
||||
|
||||
const handleDisableViewport = async () => {
|
||||
renderingEngine.disableElement(VIEWPORT_ID);
|
||||
};
|
||||
|
||||
const handleLoadImage = async (width: number, height: number) => {
|
||||
if (!activeViewportElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
if (!activeViewportEnabledElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewport } = activeViewportEnabledElement;
|
||||
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
|
||||
|
||||
try {
|
||||
if (downloadViewport instanceof StackViewport) {
|
||||
const imageId = viewport.getCurrentImageId();
|
||||
const properties = viewport.getProperties();
|
||||
|
||||
await downloadViewport.setStack([imageId]);
|
||||
downloadViewport.setProperties(properties);
|
||||
|
||||
return {
|
||||
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
|
||||
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
|
||||
};
|
||||
} else if (downloadViewport instanceof BaseVolumeViewport) {
|
||||
const volumeIds = viewport.getAllVolumeIds();
|
||||
downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]);
|
||||
|
||||
return {
|
||||
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
|
||||
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAnnotations = (show: boolean) => {
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
if (!activeViewportEnabledElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
|
||||
if (!downloadViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
|
||||
const { id: downloadViewportId } = downloadViewport;
|
||||
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
|
||||
toolGroup.addViewport(downloadViewportId, renderingEngineId);
|
||||
|
||||
Object.keys(toolGroup.getToolInstances()).forEach(toolName => {
|
||||
// make all tools Enabled so that they can not be interacted with
|
||||
// in the download viewport
|
||||
if (toggle && toolName !== 'Crosshairs') {
|
||||
if (show && toolName !== 'Crosshairs') {
|
||||
try {
|
||||
toolGroup.setToolEnabled(toolName);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} catch (error) {
|
||||
console.debug('Error enabling tool:', error);
|
||||
}
|
||||
} else {
|
||||
toolGroup.setToolDisabled(toolName);
|
||||
@@ -214,33 +162,50 @@ const CornerstoneViewportDownloadForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const downloadBlob = (filename, fileType) => {
|
||||
const file = `${filename}.${fileType}`;
|
||||
useEffect(() => {
|
||||
if (viewportDimensions.width && viewportDimensions.height) {
|
||||
setTimeout(() => {
|
||||
handleLoadImage(viewportDimensions.width, viewportDimensions.height);
|
||||
handleToggleAnnotations(showAnnotations);
|
||||
}, 100);
|
||||
}
|
||||
}, [viewportDimensions, showAnnotations]);
|
||||
|
||||
const handleDownload = async (filename: string, fileType: string) => {
|
||||
const divForDownloadViewport = document.querySelector(
|
||||
`div[data-viewport-uid="${VIEWPORT_ID}"]`
|
||||
);
|
||||
|
||||
html2canvas(divForDownloadViewport).then(canvas => {
|
||||
const link = document.createElement('a');
|
||||
link.download = file;
|
||||
link.href = canvas.toDataURL(fileType, 1.0);
|
||||
link.click();
|
||||
});
|
||||
if (!divForDownloadViewport) {
|
||||
console.debug('No viewport found for download');
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = await html2canvas(divForDownloadViewport as HTMLElement);
|
||||
const link = document.createElement('a');
|
||||
link.download = `${filename}.${fileType}`;
|
||||
link.href = canvas.toDataURL(`image/${fileType}`, 1.0);
|
||||
link.click();
|
||||
};
|
||||
|
||||
const ViewportDownloadFormNew = customizationService.getCustomization(
|
||||
'ohif.captureViewportModal'
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewportDownloadForm
|
||||
onClose={onClose}
|
||||
minimumSize={MINIMUM_SIZE}
|
||||
maximumSize={MAX_TEXTURE_SIZE}
|
||||
<ViewportDownloadFormNew
|
||||
onClose={hide}
|
||||
defaultSize={DEFAULT_SIZE}
|
||||
activeViewportElement={activeViewportElement}
|
||||
enableViewport={enableViewport}
|
||||
disableViewport={disableViewport}
|
||||
updateViewportPreview={updateViewportPreview}
|
||||
loadImage={loadImage}
|
||||
toggleAnnotations={toggleAnnotations}
|
||||
downloadBlob={downloadBlob}
|
||||
fileTypeOptions={FILE_TYPE_OPTIONS}
|
||||
viewportId={VIEWPORT_ID}
|
||||
showAnnotations={showAnnotations}
|
||||
onAnnotationsChange={setShowAnnotations}
|
||||
dimensions={viewportDimensions}
|
||||
onDimensionsChange={setViewportDimensions}
|
||||
onEnableViewport={handleEnableViewport}
|
||||
onDisableViewport={handleDisableViewport}
|
||||
onDownload={handleDownload}
|
||||
warningState={warningState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,17 +11,13 @@ async function createReportAsync({
|
||||
reportType = 'measurement',
|
||||
}: withAppTypes) {
|
||||
const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services;
|
||||
const loadingDialogId = uiDialogService.create({
|
||||
showOverlay: true,
|
||||
isDraggable: false,
|
||||
centralize: true,
|
||||
content: Loading,
|
||||
});
|
||||
|
||||
try {
|
||||
const naturalizedReport = await getReport();
|
||||
|
||||
if (!naturalizedReport) return;
|
||||
if (!naturalizedReport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
@@ -47,12 +43,8 @@ async function createReportAsync({
|
||||
});
|
||||
throw new Error(`Failed to store ${reportType}. Error: ${error.message || 'Unknown error'}`);
|
||||
} finally {
|
||||
uiDialogService.dismiss({ id: loadingDialogId });
|
||||
uiDialogService.hide('loading-dialog');
|
||||
}
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div className="text-primary-active">Loading...</div>;
|
||||
}
|
||||
|
||||
export default createReportAsync;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useModal } from '@ohif/ui';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { Icons, useModal } from '@ohif/ui-next';
|
||||
import { Types } from '@ohif/core';
|
||||
import DataSourceConfigurationModalComponent from './DataSourceConfigurationModalComponent';
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class ContextMenuController {
|
||||
}
|
||||
|
||||
closeContextMenu() {
|
||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||
this.services.uiDialogService.hide('context-menu');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,26 +71,24 @@ export default class ContextMenuController {
|
||||
menuId
|
||||
);
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ContextMenu = this.services.customizationService.getCustomization('ui.contextMenu');
|
||||
|
||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||
this.services.uiDialogService.create({
|
||||
this.services.uiDialogService.hide('context-menu');
|
||||
this.services.uiDialogService.show({
|
||||
id: 'context-menu',
|
||||
isDraggable: false,
|
||||
preservePosition: false,
|
||||
preventCutOf: true,
|
||||
defaultPosition: ContextMenuController._getDefaultPosition(
|
||||
defaultPointsPosition,
|
||||
event?.detail || event,
|
||||
viewportElement
|
||||
),
|
||||
event,
|
||||
content: ContextMenu,
|
||||
|
||||
// This naming is part of the uiDialogService convention
|
||||
// Clicking outside simply closes the dialog box.
|
||||
onClickOutside: () => this.services.uiDialogService.dismiss({ id: 'context-menu' }),
|
||||
|
||||
shouldCloseOnEsc: true,
|
||||
shouldCloseOnOverlayClick: true,
|
||||
unstyled: true,
|
||||
contentProps: {
|
||||
items,
|
||||
selectorProps,
|
||||
@@ -100,7 +98,7 @@ export default class ContextMenuController {
|
||||
eventData: event?.detail || event,
|
||||
|
||||
onClose: () => {
|
||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||
this.services.uiDialogService.hide('context-menu');
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
|
||||
.dicom-tag-browser-content {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding-bottom: 50px;
|
||||
/*height: 500px;*/
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ const DicomTagBrowser = ({
|
||||
<InputFilterText
|
||||
placeholder="Search metadata..."
|
||||
onDebounceChange={setFilterValue}
|
||||
className="text-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ const RowComponent = ({
|
||||
<div
|
||||
style={{ ...style, ...rowStyle }}
|
||||
className={classNames(
|
||||
'hover:bg-secondary-main border-secondary-light flex w-full flex-row items-center break-all bg-black text-base transition duration-300',
|
||||
'hover:bg-secondary-main border-secondary-light text-foreground flex w-full flex-row items-center break-all bg-black text-base transition duration-300',
|
||||
lineHeightClassName
|
||||
)}
|
||||
key={keyPrefix}
|
||||
@@ -291,7 +291,7 @@ function DicomTagTable({ rows }: { rows: Row[] }) {
|
||||
itemCount={visibleRows.length}
|
||||
itemSize={getItemSize(visibleRows)}
|
||||
width={'100%'}
|
||||
className="ohif-scrollbar"
|
||||
className="ohif-scrollbar text-foreground"
|
||||
>
|
||||
{getRowComponent({ rows: visibleRows })}
|
||||
</List>
|
||||
|
||||
@@ -1,132 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui';
|
||||
import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
export default function CreateReportDialogPrompt(uiDialogService, { extensionManager }) {
|
||||
export default function CreateReportDialogPrompt({
|
||||
title = 'Create Report',
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}): Promise<{
|
||||
value: string;
|
||||
dataSourceName: string;
|
||||
action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES];
|
||||
}> {
|
||||
const { uiDialogService, customizationService } = servicesManager.services;
|
||||
const dataSources = extensionManager.getDataSourcesForUI();
|
||||
const ReportDialog = customizationService.getCustomization('ohif.createReportDialog');
|
||||
|
||||
const allowMultipleDataSources = window.config.allowMultiSelectExport;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
let dialogId = undefined;
|
||||
|
||||
const _handleClose = () => {
|
||||
// Dismiss dialog
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
// Notify of cancel action
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} param0.action - value of action performed
|
||||
* @param {string} param0.value - value from input field
|
||||
*/
|
||||
const _handleFormSubmit = ({ action, value }) => {
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
uiDialogService.show({
|
||||
id: 'report-dialog',
|
||||
title,
|
||||
content: ReportDialog,
|
||||
contentProps: {
|
||||
dataSources: allowMultipleDataSources ? dataSources : undefined,
|
||||
onSave: async ({ reportName, dataSource: selectedDataSource }) => {
|
||||
resolve({
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
value: value.label,
|
||||
dataSourceName: value.dataSourceName,
|
||||
});
|
||||
break;
|
||||
case 'cancel':
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const dataSourcesOpts = Object.keys(extensionManager.dataSourceMap)
|
||||
.filter(ds => {
|
||||
const configuration = extensionManager.dataSourceDefs[ds]?.configuration;
|
||||
const supportsStow = configuration?.supportsStow ?? configuration?.wadoRoot;
|
||||
return supportsStow;
|
||||
})
|
||||
.map(ds => {
|
||||
return {
|
||||
value: ds,
|
||||
label: ds,
|
||||
placeHolder: ds,
|
||||
};
|
||||
});
|
||||
|
||||
dialogId = uiDialogService.create({
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
content: Dialog,
|
||||
useLastPosition: false,
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Create Report',
|
||||
value: {
|
||||
label: '',
|
||||
dataSourceName: extensionManager.activeDataSource,
|
||||
},
|
||||
noCloseButton: true,
|
||||
onClose: _handleClose,
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
|
||||
],
|
||||
// TODO: Should be on button press...
|
||||
onSubmit: _handleFormSubmit,
|
||||
body: ({ value, setValue }) => {
|
||||
const onChangeHandler = event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
};
|
||||
const onKeyPressHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
value: value.label,
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{dataSourcesOpts.length > 1 && window.config?.allowMultiSelectExport && (
|
||||
<div>
|
||||
<label className="text-[14px] leading-[1.2] text-white">Data Source</label>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mt-2 bg-black"
|
||||
options={dataSourcesOpts}
|
||||
placeholder={
|
||||
dataSourcesOpts.find(option => option.value === value.dataSourceName)
|
||||
.placeHolder
|
||||
}
|
||||
value={value.dataSourceName}
|
||||
onChange={evt => {
|
||||
setValue(v => ({ ...v, dataSourceName: evt.value }));
|
||||
}}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<Input
|
||||
autoFocus
|
||||
label="Enter the report name"
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,23 +2,17 @@ import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { UserPreferences, AboutModal, useModal } from '@ohif/ui';
|
||||
import { Header } from '@ohif/ui-next';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { Header, useModal } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { Toolbar } from '../Toolbar/Toolbar';
|
||||
import HeaderPatientInfo from './HeaderPatientInfo';
|
||||
import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo';
|
||||
import { preserveQueryParameters, publicUrl } from '@ohif/app';
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }>) {
|
||||
const { servicesManager, extensionManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
function ViewerHeader({
|
||||
hotkeysManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
appConfig,
|
||||
}: withAppTypes<{ appConfig: AppTypes.Config }>) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -42,10 +36,10 @@ function ViewerHeader({
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { show, hide } = useModal();
|
||||
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
||||
const versionNumber = process.env.VERSION_NUMBER;
|
||||
const commitHash = process.env.COMMIT_HASH;
|
||||
const { show } = useModal();
|
||||
|
||||
const AboutModal = customizationService.getCustomization('ohif.aboutModal');
|
||||
const UserPreferencesModal = customizationService.getCustomization('ohif.userPreferencesModal');
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
@@ -55,8 +49,7 @@ function ViewerHeader({
|
||||
show({
|
||||
content: AboutModal,
|
||||
title: t('AboutModal:About OHIF Viewer'),
|
||||
contentProps: { versionNumber, commitHash },
|
||||
containerDimensions: 'max-w-4xl max-h-4xl',
|
||||
containerClassName: 'max-w-md',
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -64,30 +57,9 @@ function ViewerHeader({
|
||||
icon: 'settings',
|
||||
onClick: () =>
|
||||
show({
|
||||
content: UserPreferencesModal,
|
||||
title: t('UserPreferencesModal:User preferences'),
|
||||
content: UserPreferences,
|
||||
containerDimensions: 'w-[70%] max-w-[900px]',
|
||||
contentProps: {
|
||||
hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
|
||||
hotkeyDefinitions,
|
||||
currentLanguage: currentLanguage(),
|
||||
availableLanguages,
|
||||
defaultLanguage,
|
||||
onCancel: () => {
|
||||
hotkeys.stopRecord();
|
||||
hotkeys.unpause();
|
||||
hide();
|
||||
},
|
||||
onSubmit: ({ hotkeyDefinitions, language }) => {
|
||||
if (language.value !== currentLanguage().value) {
|
||||
i18n.changeLanguage(language.value);
|
||||
}
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
hide();
|
||||
},
|
||||
onReset: () => hotkeysManager.restoreDefaultBindings(),
|
||||
hotkeysModule: hotkeys,
|
||||
},
|
||||
containerClassName: 'flex max-w-4xl p-6 flex-col',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -505,10 +505,9 @@ const commandsModule = ({
|
||||
contentProps: {
|
||||
displaySets,
|
||||
displaySetInstanceUID: defaultDisplaySetInstanceUID,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
containerDimensions: 'w-[70%] max-w-[900px]',
|
||||
title: 'DICOM Tag Browser',
|
||||
containerClassName: 'max-w-3xl',
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { AboutModal } from '@ohif/ui-next';
|
||||
import detect from 'browser-detect';
|
||||
|
||||
function AboutModalDefault() {
|
||||
const { os, version, name } = detect();
|
||||
const browser = `${name[0].toUpperCase()}${name.substr(1)} ${version}`;
|
||||
const versionNumber = process.env.VERSION_NUMBER;
|
||||
const commitHash = process.env.COMMIT_HASH;
|
||||
|
||||
const [main, beta] = versionNumber.split('-');
|
||||
|
||||
return (
|
||||
<AboutModal className="w-[400px]">
|
||||
<AboutModal.ProductName>OHIF Viewer</AboutModal.ProductName>
|
||||
<AboutModal.ProductVersion>{main}</AboutModal.ProductVersion>
|
||||
{beta && <AboutModal.ProductBeta>{beta}</AboutModal.ProductBeta>}
|
||||
|
||||
<AboutModal.Body>
|
||||
<AboutModal.DetailItem
|
||||
label="Commit Hash"
|
||||
value={commitHash}
|
||||
/>
|
||||
<AboutModal.DetailItem
|
||||
label="Current Browser & OS"
|
||||
value={`${browser}, ${os}`}
|
||||
/>
|
||||
<AboutModal.SocialItem
|
||||
icon="SocialGithub"
|
||||
url="OHIF/Viewers"
|
||||
text="github.com/OHIF/Viewers"
|
||||
/>
|
||||
</AboutModal.Body>
|
||||
</AboutModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
'ohif.aboutModal': AboutModalDefault,
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react';
|
||||
import { InputDialog } from '@ohif/ui-next';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ohif/ui-next';
|
||||
|
||||
type DataSource = {
|
||||
value: string;
|
||||
label: string;
|
||||
placeHolder: string;
|
||||
};
|
||||
|
||||
type ReportDialogProps = {
|
||||
dataSources: DataSource[];
|
||||
hide: () => void;
|
||||
onSave: (data: { reportName: string; dataSource: string | null }) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) {
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
|
||||
dataSources?.[0]?.value ?? null
|
||||
);
|
||||
|
||||
const handleSave = (reportName: string) => {
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
});
|
||||
hide();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel();
|
||||
hide();
|
||||
};
|
||||
|
||||
const showDataSourceSelect = dataSources?.length > 1;
|
||||
|
||||
return (
|
||||
<div className="text-foreground mt-2 flex min-w-[400px] max-w-md flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className={showDataSourceSelect ? 'flex gap-4' : ''}>
|
||||
{showDataSourceSelect && (
|
||||
<div className="mt-1 w-1/3">
|
||||
<Select
|
||||
value={selectedDataSource}
|
||||
onValueChange={setSelectedDataSource}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a data source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dataSources.map(source => (
|
||||
<SelectItem
|
||||
key={source.value}
|
||||
value={source.value}
|
||||
>
|
||||
{source.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className={showDataSourceSelect ? 'mt-1 w-2/3' : 'w-full'}>
|
||||
<InputDialog>
|
||||
<InputDialog.Field>
|
||||
<InputDialog.Input placeholder="Report name" />
|
||||
</InputDialog.Field>
|
||||
</InputDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<InputDialog>
|
||||
<InputDialog.Actions>
|
||||
<InputDialog.ActionsSecondary onClick={handleCancel}>
|
||||
Cancel
|
||||
</InputDialog.ActionsSecondary>
|
||||
<InputDialog.ActionsPrimary onClick={handleSave}>Save</InputDialog.ActionsPrimary>
|
||||
</InputDialog.Actions>
|
||||
</InputDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ReportDialog };
|
||||
export default {
|
||||
'ohif.createReportDialog': ReportDialog,
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSystem, hotkeys as hotkeysModule } from '@ohif/core';
|
||||
import { UserPreferencesModal, FooterAction } from '@ohif/ui-next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@ohif/ui-next';
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage: currentLanguageFn } = i18n;
|
||||
|
||||
interface HotkeyDefinition {
|
||||
keys: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HotkeyDefinitions {
|
||||
[key: string]: HotkeyDefinition;
|
||||
}
|
||||
|
||||
function UserPreferencesModalDefault({ hide }: { hide: () => void }) {
|
||||
const { hotkeysManager } = useSystem();
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
|
||||
const { hotkeyDefinitions = {}, hotkeyDefaults = {} } = hotkeysManager;
|
||||
|
||||
const currentLanguage = currentLanguageFn();
|
||||
|
||||
const [state, setState] = useState({
|
||||
hotkeyDefinitions: hotkeyDefinitions as HotkeyDefinitions,
|
||||
languageValue: currentLanguage.value,
|
||||
});
|
||||
|
||||
const onLanguageChangeHandler = (value: string) => {
|
||||
setState(state => ({ ...state, languageValue: value }));
|
||||
};
|
||||
|
||||
const onHotkeyChangeHandler = (id: string, newKeys: string) => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
hotkeyDefinitions: {
|
||||
...state.hotkeyDefinitions,
|
||||
[id]: {
|
||||
...state.hotkeyDefinitions[id],
|
||||
keys: newKeys,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const onResetHandler = () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
languageValue: defaultLanguage.value,
|
||||
hotkeyDefinitions: hotkeyDefaults as HotkeyDefinitions,
|
||||
}));
|
||||
|
||||
hotkeysManager.restoreDefaultBindings();
|
||||
};
|
||||
|
||||
return (
|
||||
<UserPreferencesModal>
|
||||
<UserPreferencesModal.Body>
|
||||
{/* Language Section */}
|
||||
<div className="mb-3 flex items-center space-x-14">
|
||||
<UserPreferencesModal.SubHeading>{t('Language')}</UserPreferencesModal.SubHeading>
|
||||
<Select
|
||||
defaultValue={state.languageValue}
|
||||
onValueChange={onLanguageChangeHandler}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-60"
|
||||
aria-label="Language"
|
||||
>
|
||||
<SelectValue placeholder={t('Select language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableLanguages.map(lang => (
|
||||
<SelectItem
|
||||
key={lang.value}
|
||||
value={lang.value}
|
||||
>
|
||||
{lang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<UserPreferencesModal.SubHeading>{t('Hotkeys')}</UserPreferencesModal.SubHeading>
|
||||
<UserPreferencesModal.HotkeysGrid>
|
||||
{Object.entries(state.hotkeyDefinitions).map(([id, definition]) => (
|
||||
<UserPreferencesModal.Hotkey
|
||||
key={id}
|
||||
label={t(definition.label)}
|
||||
value={definition.keys}
|
||||
onChange={newKeys => onHotkeyChangeHandler(id, newKeys)}
|
||||
placeholder={definition.keys}
|
||||
hotkeys={hotkeysModule}
|
||||
/>
|
||||
))}
|
||||
</UserPreferencesModal.HotkeysGrid>
|
||||
</UserPreferencesModal.Body>
|
||||
<FooterAction>
|
||||
<FooterAction.Left>
|
||||
<FooterAction.Auxiliary onClick={onResetHandler}>
|
||||
{t('Reset to defaults')}
|
||||
</FooterAction.Auxiliary>
|
||||
</FooterAction.Left>
|
||||
<FooterAction.Right>
|
||||
<FooterAction.Secondary
|
||||
onClick={() => {
|
||||
hotkeysModule.stopRecord();
|
||||
hotkeysModule.unpause();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</FooterAction.Secondary>
|
||||
<FooterAction.Primary
|
||||
onClick={() => {
|
||||
if (state.languageValue !== currentLanguage.value) {
|
||||
i18n.changeLanguage(state.languageValue);
|
||||
}
|
||||
hotkeysManager.setHotkeys(state.hotkeyDefinitions);
|
||||
hotkeysModule.stopRecord();
|
||||
hotkeysModule.unpause();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{t('Save')}
|
||||
</FooterAction.Primary>
|
||||
</FooterAction.Right>
|
||||
</FooterAction>
|
||||
</UserPreferencesModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
'ohif.userPreferencesModal': UserPreferencesModalDefault,
|
||||
};
|
||||
@@ -18,6 +18,9 @@ import progressLoadingBarCustomization from './customizations/progressLoadingBar
|
||||
import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization';
|
||||
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
|
||||
import viewportNotificationCustomization from './customizations/notificationCustomization';
|
||||
import aboutModalCustomization from './customizations/aboutModalCustomization';
|
||||
import userPreferencesCustomization from './customizations/userPreferencesCustomization';
|
||||
import reportDialogCustomization from './customizations/reportDialogCustomization';
|
||||
import hotkeyBindingsCustomization from './customizations/hotkeyBindingsCustomization';
|
||||
import onboardingCustomization from './customizations/onboardingCustomization';
|
||||
/**
|
||||
@@ -63,6 +66,9 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
...labellingFlowCustomization,
|
||||
...contextMenuUICustomization,
|
||||
...viewportNotificationCustomization,
|
||||
...aboutModalCustomization,
|
||||
...userPreferencesCustomization,
|
||||
...reportDialogCustomization,
|
||||
...hotkeyBindingsCustomization,
|
||||
...onboardingCustomization,
|
||||
},
|
||||
|
||||
@@ -12,9 +12,10 @@ import getCustomizationModule from './getCustomizationModule';
|
||||
import getViewportModule from './getViewportModule';
|
||||
import { id } from './id';
|
||||
import preRegistration from './init';
|
||||
import { createReportDialogPrompt } from './Panels';
|
||||
|
||||
import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu';
|
||||
import * as dicomWebUtils from './DicomWebDataSource/utils';
|
||||
import { createReportDialogPrompt } from './Panels';
|
||||
import createReportAsync from './Actions/createReportAsync';
|
||||
import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient';
|
||||
import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils';
|
||||
@@ -25,11 +26,7 @@ import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore'
|
||||
import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
|
||||
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||
import {
|
||||
callLabelAutocompleteDialog,
|
||||
showLabelAnnotationPopup,
|
||||
callInputDialog,
|
||||
} from './utils/callInputDialog';
|
||||
import { callInputDialogAutoComplete, callInputDialog } from './utils/callInputDialog';
|
||||
import colorPickerDialog from './utils/colorPickerDialog';
|
||||
|
||||
import promptSaveReport from './utils/promptSaveReport';
|
||||
@@ -82,7 +79,6 @@ export {
|
||||
CustomizableContextMenuTypes,
|
||||
getStudiesForPatientByMRN,
|
||||
dicomWebUtils,
|
||||
createReportDialogPrompt,
|
||||
createReportAsync,
|
||||
StaticWadoClient,
|
||||
cleanDenaturalizedDataset,
|
||||
@@ -94,9 +90,7 @@ export {
|
||||
useUIStateStore,
|
||||
useViewportGridStore,
|
||||
useViewportsByPositionStore,
|
||||
showLabelAnnotationPopup,
|
||||
callLabelAutocompleteDialog,
|
||||
callInputDialog,
|
||||
callInputDialogAutoComplete,
|
||||
promptSaveReport,
|
||||
promptLabelAnnotation,
|
||||
colorPickerDialog,
|
||||
@@ -105,4 +99,6 @@ export {
|
||||
utils,
|
||||
MoreDropdownMenu,
|
||||
requestDisplaySetCreationForStudy,
|
||||
callInputDialog,
|
||||
createReportDialogPrompt,
|
||||
};
|
||||
@@ -1,160 +1,107 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
|
||||
import { LabellingFlow } from '@ohif/ui';
|
||||
import { InputDialog } from '@ohif/ui-next';
|
||||
|
||||
interface InputDialogDefaultProps {
|
||||
hide: () => void;
|
||||
onSave: (value: string) => void;
|
||||
placeholder: string;
|
||||
defaultValue: string;
|
||||
submitOnEnter: boolean;
|
||||
}
|
||||
|
||||
function InputDialogDefault({
|
||||
hide,
|
||||
onSave,
|
||||
placeholder = 'Enter value',
|
||||
defaultValue = '',
|
||||
submitOnEnter,
|
||||
}: InputDialogDefaultProps) {
|
||||
return (
|
||||
<InputDialog
|
||||
submitOnEnter={submitOnEnter}
|
||||
defaultValue={defaultValue}
|
||||
>
|
||||
<InputDialog.Field>
|
||||
<InputDialog.Input placeholder={placeholder} />
|
||||
</InputDialog.Field>
|
||||
<InputDialog.Actions>
|
||||
<InputDialog.ActionsSecondary onClick={hide}>Cancel</InputDialog.ActionsSecondary>
|
||||
<InputDialog.ActionsPrimary
|
||||
onClick={value => {
|
||||
onSave(value);
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</InputDialog.ActionsPrimary>
|
||||
</InputDialog.Actions>
|
||||
</InputDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} data
|
||||
* @param {*} data.text
|
||||
* @param {*} data.label
|
||||
* @param {*} event
|
||||
* @param {*} callback
|
||||
* @param {*} isArrowAnnotateInputDialog
|
||||
* @param {*} dialogConfig
|
||||
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
|
||||
* @param {string?} dialogConfig.inputLabel - show label above the input
|
||||
* Shows an input dialog for entering text with customizable options
|
||||
* @param uiDialogService - Service for showing UI dialogs
|
||||
* @param onSave - Callback function called when save button is clicked with entered value
|
||||
* @param defaultValue - Initial value to show in input field
|
||||
* @param title - Title text to show in dialog header
|
||||
* @param placeholder - Placeholder text for input field
|
||||
* @param submitOnEnter - Whether to submit dialog when Enter key is pressed
|
||||
*/
|
||||
|
||||
export function callInputDialog(
|
||||
export async function callInputDialog({
|
||||
uiDialogService,
|
||||
data,
|
||||
callback,
|
||||
isArrowAnnotateInputDialog = true,
|
||||
dialogConfig: any = {}
|
||||
) {
|
||||
defaultValue = '',
|
||||
title = 'Annotation',
|
||||
placeholder = '',
|
||||
submitOnEnter = true,
|
||||
}: {
|
||||
uiDialogService: AppTypes.UIDialogService;
|
||||
}) {
|
||||
const dialogId = 'dialog-enter-annotation';
|
||||
const label = data ? (isArrowAnnotateInputDialog ? data.text : data.label) : '';
|
||||
const {
|
||||
dialogTitle = 'Annotation',
|
||||
inputLabel = 'Enter your annotation',
|
||||
validateFunc = value => true,
|
||||
} = dialogConfig;
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
if (typeof validateFunc === 'function' && !validateFunc(value.label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(value.label, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
const value = await new Promise<string>(resolve => {
|
||||
uiDialogService.show({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
content: InputDialogDefault,
|
||||
title: title,
|
||||
shouldCloseOnEsc: true,
|
||||
contentProps: {
|
||||
title: dialogTitle,
|
||||
value: { label },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
return (
|
||||
<Input
|
||||
autoFocus
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
id="annotation"
|
||||
label={inputLabel}
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
value={value.label}
|
||||
onChange={event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
}}
|
||||
onKeyPress={event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
onSave: value => {
|
||||
resolve(value);
|
||||
},
|
||||
placeholder,
|
||||
defaultValue,
|
||||
submitOnEnter,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function callLabelAutocompleteDialog(
|
||||
uiDialogService,
|
||||
callback,
|
||||
dialogConfig,
|
||||
labelConfig,
|
||||
renderContent = LabellingFlow
|
||||
) {
|
||||
const exclusive = labelConfig ? labelConfig.exclusive : false;
|
||||
const dropDownItems = labelConfig ? labelConfig.items : [];
|
||||
|
||||
const { validateFunc = value => true } = dialogConfig;
|
||||
|
||||
const labellingDoneCallback = value => {
|
||||
if (typeof value === 'string') {
|
||||
if (typeof validateFunc === 'function' && !validateFunc(value)) {
|
||||
return;
|
||||
}
|
||||
callback(value, 'save');
|
||||
} else {
|
||||
callback('', 'cancel');
|
||||
}
|
||||
uiDialogService.dismiss({ id: 'select-annotation' });
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'select-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: renderContent,
|
||||
contentProps: {
|
||||
labellingDoneCallback: labellingDoneCallback,
|
||||
measurementData: { label: '' },
|
||||
componentClassName: {},
|
||||
labelData: dropDownItems,
|
||||
exclusive: exclusive,
|
||||
},
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function showLabelAnnotationPopup(
|
||||
export async function callInputDialogAutoComplete({
|
||||
measurement,
|
||||
uiDialogService,
|
||||
labelConfig,
|
||||
renderContent = LabellingFlow
|
||||
) {
|
||||
renderContent = LabellingFlow,
|
||||
}) {
|
||||
const exclusive = labelConfig ? labelConfig.exclusive : false;
|
||||
const dropDownItems = labelConfig ? labelConfig.items : [];
|
||||
return new Promise<Map<any, any>>((resolve, reject) => {
|
||||
|
||||
const value = await new Promise<Map<string, string>>((resolve, reject) => {
|
||||
const labellingDoneCallback = value => {
|
||||
uiDialogService.dismiss({ id: 'select-annotation' });
|
||||
uiDialogService.hide('select-annotation');
|
||||
if (typeof value === 'string') {
|
||||
measurement.label = value;
|
||||
}
|
||||
resolve(measurement);
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
uiDialogService.show({
|
||||
id: 'select-annotation',
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
title: 'Annotation',
|
||||
content: renderContent,
|
||||
defaultPosition: {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
},
|
||||
contentProps: {
|
||||
labellingDoneCallback: labellingDoneCallback,
|
||||
measurementData: measurement,
|
||||
@@ -164,6 +111,8 @@ export function showLabelAnnotationPopup(
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default callInputDialog;
|
||||
@@ -1,58 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Dialog } from '@ohif/ui';
|
||||
import React, { useState } from 'react';
|
||||
import { ChromePicker } from 'react-color';
|
||||
import { FooterAction } from '@ohif/ui-next';
|
||||
|
||||
import './colorPickerDialog.css';
|
||||
|
||||
function colorPickerDialog(uiDialogService, rgbaColor, callback) {
|
||||
const dialogId = 'pick-color';
|
||||
function ColorPickerDialog({ value, hide, onSave }) {
|
||||
const [color, setColor] = useState(value);
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
callback(value.rgbaColor, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
const handleChange = color => {
|
||||
setColor(color.rgb);
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Segment Color',
|
||||
value: { rgbaColor },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
const handleChange = color => {
|
||||
setValue({ rgbaColor: color.rgb });
|
||||
};
|
||||
|
||||
return (
|
||||
<ChromePicker
|
||||
color={value.rgbaColor}
|
||||
onChange={handleChange}
|
||||
presetColors={[]}
|
||||
width={300}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<ChromePicker
|
||||
color={color}
|
||||
onChange={handleChange}
|
||||
presetColors={[]}
|
||||
width={300}
|
||||
/>
|
||||
<FooterAction>
|
||||
<FooterAction.Right>
|
||||
<FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
|
||||
<FooterAction.Primary
|
||||
onClick={() => {
|
||||
hide();
|
||||
onSave(color);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</FooterAction.Primary>
|
||||
</FooterAction.Right>
|
||||
</FooterAction>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default colorPickerDialog;
|
||||
export default ColorPickerDialog;
|
||||
@@ -1,42 +1,46 @@
|
||||
import { showLabelAnnotationPopup } from './callInputDialog';
|
||||
import { callInputDialogAutoComplete } from './callInputDialog';
|
||||
|
||||
function promptLabelAnnotation({ servicesManager }, ctx, evt) {
|
||||
const { measurementService, customizationService, toolGroupService } = servicesManager.services;
|
||||
const { measurementService, customizationService, toolGroupService, uiDialogService } =
|
||||
servicesManager.services;
|
||||
const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId, toolName } = evt;
|
||||
return new Promise(async function (resolve) {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
const activeToolOptions = toolGroup.getToolConfiguration(toolName);
|
||||
if(activeToolOptions.getTextCallback) {
|
||||
resolve({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
})
|
||||
} else {
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const measurement = measurementService.getMeasurement(measurementId);
|
||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||
const value = await showLabelAnnotationPopup(
|
||||
measurement,
|
||||
servicesManager.services.uiDialogService,
|
||||
labelConfig,
|
||||
renderContent
|
||||
);
|
||||
return new Promise(resolve => {
|
||||
(async () => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
const activeToolOptions = toolGroup.getToolConfiguration(toolName);
|
||||
if (activeToolOptions.getTextCallback) {
|
||||
resolve({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
});
|
||||
} else {
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const measurement = measurementService.getMeasurement(measurementId);
|
||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||
|
||||
measurementService.update(
|
||||
measurementId,
|
||||
{
|
||||
...value,
|
||||
},
|
||||
true
|
||||
);
|
||||
const value = await callInputDialogAutoComplete({
|
||||
measurement,
|
||||
uiDialogService,
|
||||
labelConfig,
|
||||
renderContent,
|
||||
});
|
||||
|
||||
resolve({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
});
|
||||
}
|
||||
measurementService.update(
|
||||
measurementId,
|
||||
{
|
||||
...value,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
resolve({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
});
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import { createReportDialogPrompt } from '../Panels';
|
||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||
|
||||
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
|
||||
const { uiDialogService, measurementService, displaySetService } = servicesManager.services;
|
||||
const viewportId = evt.viewportId === undefined ? evt.data.viewportId : evt.viewportId;
|
||||
const isBackupSave = evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
|
||||
const StudyInstanceUID = evt?.data?.StudyInstanceUID;
|
||||
const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
|
||||
|
||||
const { trackedStudy, trackedSeries } = ctx;
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
try {
|
||||
const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const measurements = measurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements
|
||||
.filter(
|
||||
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
|
||||
)
|
||||
.filter(m => m.referencedImageId != null);
|
||||
|
||||
const SeriesDescription =
|
||||
// isUndefinedOrEmpty
|
||||
promptResult.value === undefined || promptResult.value === ''
|
||||
? 'Research Derived Series' // default
|
||||
: promptResult.value; // provided value
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData: trackedMeasurements,
|
||||
dataSource,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options: {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
},
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
);
|
||||
};
|
||||
displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport,
|
||||
});
|
||||
} else if (promptResult.action === RESPONSE.CANCEL) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return {
|
||||
userResponse: promptResult.action,
|
||||
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
isBackupSave,
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default promptSaveReport;
|
||||
@@ -0,0 +1,109 @@
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||
import createReportDialogPrompt from '../Panels/createReportDialogPrompt';
|
||||
|
||||
/**
|
||||
* Prompts the user to save a report and handles the report creation process
|
||||
* @param services - Object containing required services and managers
|
||||
* @param ctx - The current context containing tracked study and series information
|
||||
* @param evt - The event object containing viewport and save-related data
|
||||
*/
|
||||
async function promptSaveReport(services, ctx, evt) {
|
||||
const { servicesManager, extensionManager, commandsManager } = services;
|
||||
|
||||
const { measurementService, displaySetService } = servicesManager.services;
|
||||
|
||||
const viewportId = evt.viewportId ?? evt.data?.viewportId;
|
||||
const isBackupSave = evt.isBackupSave ?? evt.data?.isBackupSave;
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = evt?.data ?? {};
|
||||
|
||||
const { trackedStudy, trackedSeries } = ctx;
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
|
||||
const {
|
||||
value: reportName,
|
||||
dataSourceName: dataSource,
|
||||
action,
|
||||
} = await createReportDialogPrompt({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
});
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
try {
|
||||
if (action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
const selectedDataSource = dataSource ?? dataSources[0];
|
||||
const trackedMeasurements = getTrackedMeasurements(
|
||||
measurementService,
|
||||
trackedStudy,
|
||||
trackedSeries
|
||||
);
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
displaySetInstanceUIDs = await handleReportCreation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
trackedMeasurements,
|
||||
selectedDataSource,
|
||||
reportName,
|
||||
SeriesNumber,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
userResponse: action,
|
||||
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportId,
|
||||
isBackupSave,
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tracked measurements based on study and series criteria
|
||||
*/
|
||||
function getTrackedMeasurements(measurementService, trackedStudy, trackedSeries) {
|
||||
return measurementService
|
||||
.getMeasurements()
|
||||
.filter(
|
||||
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
|
||||
)
|
||||
.filter(m => m.referencedImageId != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation of the report using the measurement service
|
||||
*/
|
||||
async function handleReportCreation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
trackedMeasurements,
|
||||
selectedDataSource,
|
||||
reportName,
|
||||
SeriesNumber,
|
||||
}) {
|
||||
return createReportAsync({
|
||||
servicesManager,
|
||||
getReport: () =>
|
||||
commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData: trackedMeasurements,
|
||||
dataSource: selectedDataSource,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options: {
|
||||
SeriesDescription: reportName,
|
||||
SeriesNumber,
|
||||
},
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export default promptSaveReport;
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { callInputDialog } from '@ohif/extension-default';
|
||||
import { ExtensionManager, CommandsManager, DicomMetadataStore } from '@ohif/core';
|
||||
import { MeasurementTable } from '@ohif/ui';
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { EVENTS as MicroscopyEvents } from '../../services/MicroscopyService';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { callInputDialog } from '@ohif/extension-default';
|
||||
import constructSR from '../../utils/constructSR';
|
||||
import { saveByteArray } from '../../utils/saveByteArray';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
let saving = false;
|
||||
const { datasetToBuffer } = dcmjs.data;
|
||||
@@ -140,12 +139,8 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) {
|
||||
uiDialogService,
|
||||
title: 'Enter description of the Series',
|
||||
defaultValue: '',
|
||||
callback: (value: string, action: string) => {
|
||||
switch (action) {
|
||||
case 'save': {
|
||||
saveFunction(value);
|
||||
}
|
||||
}
|
||||
onSave: (value: string) => {
|
||||
saveFunction(value);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,13 +29,9 @@ export default function getCommandsModule({
|
||||
callInputDialog({
|
||||
uiDialogService,
|
||||
defaultValue: '',
|
||||
callback: (value: string, action: string) => {
|
||||
switch (action) {
|
||||
case 'save': {
|
||||
roiAnnotation.setLabel(value);
|
||||
microscopyService.triggerRelabel(roiAnnotation);
|
||||
}
|
||||
}
|
||||
onSave: (value: string) => {
|
||||
roiAnnotation.setLabel(value);
|
||||
microscopyService.triggerRelabel(roiAnnotation);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
+11
-111
@@ -11,8 +11,14 @@ import { useTrackedMeasurements } from '../../getContextModule';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
import { MoreDropdownMenu, PanelStudyBrowserHeader } from '@ohif/extension-default';
|
||||
import { defaultActionIcons } from './constants';
|
||||
import { UntrackSeriesModal } from './untrackSeriesModal';
|
||||
const { formatDate, createStudyBrowserTabs } = utils;
|
||||
|
||||
const DIALOG_ID = {
|
||||
UNTRACK_SERIES: 'untrack-series',
|
||||
REJECT_REPORT: 'ds-reject-sr',
|
||||
};
|
||||
|
||||
const thumbnailNoImageModalities = [
|
||||
'SR',
|
||||
'SEG',
|
||||
@@ -44,6 +50,7 @@ export default function PanelStudyBrowserTracking({
|
||||
measurementService,
|
||||
studyPrefetcherService,
|
||||
customizationService,
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
const studyMode = customizationService.getCustomization('studyBrowser.studyMode');
|
||||
@@ -446,47 +453,11 @@ export default function PanelStudyBrowserTracking({
|
||||
});
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'untrack-series',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
uiModalService.show({
|
||||
title: 'Untrack Series',
|
||||
content: UntrackSeriesModal,
|
||||
contentProps: {
|
||||
title: 'Untrack Series',
|
||||
body: () => (
|
||||
<div className="bg-primary-dark p-4 text-white">
|
||||
<p>Are you sure you want to untrack this series?</p>
|
||||
<p className="mt-2">
|
||||
This action cannot be undone and will delete all your existing measurements.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
id: 'cancel',
|
||||
text: 'Cancel',
|
||||
type: ButtonEnums.type.secondary,
|
||||
},
|
||||
{
|
||||
id: 'yes',
|
||||
text: 'Yes',
|
||||
type: ButtonEnums.type.primary,
|
||||
classes: ['untrack-yes-button'],
|
||||
},
|
||||
],
|
||||
onClose: () => uiDialogService.dismiss({ id: 'untrack-series' }),
|
||||
onSubmit: async ({ action }) => {
|
||||
switch (action.id) {
|
||||
case 'yes':
|
||||
onConfirm();
|
||||
uiDialogService.dismiss({ id: 'untrack-series' });
|
||||
break;
|
||||
case 'cancel':
|
||||
uiDialogService.dismiss({ id: 'untrack-series' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
onConfirm,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -630,77 +601,6 @@ function _mapDisplaySets(
|
||||
isHydratedForDerivedDisplaySet: ds.isHydrated,
|
||||
};
|
||||
|
||||
if (componentType === 'thumbnailNoImage') {
|
||||
if (dataSource.reject && dataSource.reject.series) {
|
||||
thumbnailProps.canReject = !ds?.unsupported;
|
||||
thumbnailProps.onReject = () => {
|
||||
uiDialogService.create({
|
||||
id: 'ds-reject-sr',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Delete Report',
|
||||
body: () => (
|
||||
<div className="bg-primary-dark p-4 text-white">
|
||||
<p>Are you sure you want to delete this report?</p>
|
||||
<p className="mt-2">This action cannot be undone.</p>
|
||||
</div>
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
id: 'cancel',
|
||||
text: 'Cancel',
|
||||
type: ButtonEnums.type.secondary,
|
||||
},
|
||||
{
|
||||
id: 'yes',
|
||||
text: 'Yes',
|
||||
type: ButtonEnums.type.primary,
|
||||
classes: ['reject-yes-button'],
|
||||
},
|
||||
],
|
||||
onClose: () => uiDialogService.dismiss({ id: 'ds-reject-sr' }),
|
||||
onShow: () => {
|
||||
const yesButton = document.querySelector('.reject-yes-button');
|
||||
|
||||
yesButton.focus();
|
||||
},
|
||||
onSubmit: async ({ action }) => {
|
||||
switch (action.id) {
|
||||
case 'yes':
|
||||
try {
|
||||
await dataSource.reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID);
|
||||
displaySetService.deleteDisplaySet(displaySetInstanceUID);
|
||||
uiDialogService.dismiss({ id: 'ds-reject-sr' });
|
||||
uiNotificationService.show({
|
||||
title: 'Delete Report',
|
||||
message: 'Report deleted successfully',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
uiDialogService.dismiss({ id: 'ds-reject-sr' });
|
||||
uiNotificationService.show({
|
||||
title: 'Delete Report',
|
||||
message: 'Failed to delete report',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'cancel':
|
||||
uiDialogService.dismiss({ id: 'ds-reject-sr' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
} else {
|
||||
thumbnailProps.canReject = false;
|
||||
}
|
||||
}
|
||||
|
||||
array.push(thumbnailProps);
|
||||
});
|
||||
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { FooterAction } from '@ohif/ui-next';
|
||||
|
||||
export function UntrackSeriesModal({ hide, onConfirm }) {
|
||||
return (
|
||||
<div className="text-foreground">
|
||||
<div>
|
||||
<p>Are you sure you want to untrack this series?</p>
|
||||
<p className="mt-2">
|
||||
This action cannot be undone and will delete all your existing measurements.
|
||||
</p>
|
||||
</div>
|
||||
<FooterAction className="mt-4">
|
||||
<FooterAction.Right>
|
||||
<FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
|
||||
<FooterAction.Primary
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Untrack
|
||||
</FooterAction.Primary>
|
||||
</FooterAction.Right>
|
||||
</FooterAction>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
|
||||
function segmentationItemEditHandler({ id, servicesManager }: withAppTypes) {
|
||||
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(id);
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
...segmentation,
|
||||
...value,
|
||||
});
|
||||
}
|
||||
}
|
||||
uiDialogService.dismiss({ id: 'enter-annotation' });
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'enter-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Enter your Segmentation',
|
||||
noCloseButton: true,
|
||||
value: { label: segmentation.label || '' },
|
||||
body: ({ value, setValue }) => {
|
||||
const onChangeHandler = event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
};
|
||||
|
||||
const onKeyPressHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Input
|
||||
autoFocus
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
/>
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default segmentationItemEditHandler;
|
||||
Reference in new issue
Block a user