ui(components): New Dialog and Modal components for ui-next (#4772)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
parent
62b4fa3e89
commit
a3b51aa8db
@ -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,
|
||||
},
|
||||
|
||||
@ -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]',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
109
extensions/default/src/utils/promptSaveReport.tsx
Normal file
109
extensions/default/src/utils/promptSaveReport.tsx
Normal file
@ -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,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);
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -105,14 +105,8 @@ function modeFactory({ modeConfiguration }) {
|
||||
]);
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const {
|
||||
toolGroupService,
|
||||
measurementService,
|
||||
toolbarService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
uiDialogService.dismissAll();
|
||||
const { toolGroupService, uiDialogService, uiModalService } = servicesManager.services;
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
},
|
||||
|
||||
@ -128,7 +128,7 @@ function modeFactory() {
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
|
||||
uiDialogService.dismissAll();
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
|
||||
@ -147,7 +147,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
|
||||
_activatePanelTriggersSubscriptions = [];
|
||||
|
||||
uiDialogService.dismissAll();
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
|
||||
@ -12,12 +12,7 @@ const colorsByOrientation = {
|
||||
coronal: 'rgb(0, 200, 0)',
|
||||
};
|
||||
|
||||
function initDefaultToolGroup(
|
||||
extensionManager,
|
||||
toolGroupService,
|
||||
commandsManager,
|
||||
toolGroupId
|
||||
) {
|
||||
function initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, toolGroupId) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
);
|
||||
@ -308,12 +303,7 @@ function initVolume3DToolGroup(extensionManager, toolGroupService) {
|
||||
}
|
||||
|
||||
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
|
||||
initDefaultToolGroup(
|
||||
extensionManager,
|
||||
toolGroupService,
|
||||
commandsManager,
|
||||
'default'
|
||||
);
|
||||
initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default');
|
||||
initSRToolGroup(extensionManager, toolGroupService);
|
||||
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||
initVolume3DToolGroup(extensionManager, toolGroupService);
|
||||
|
||||
@ -57,7 +57,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const { toolbarService, uiDialogService, uiModalService } = servicesManager.services;
|
||||
|
||||
uiDialogService.dismissAll();
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolbarService.reset();
|
||||
},
|
||||
|
||||
@ -81,7 +81,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
|
||||
uiDialogService.dismissAll();
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
|
||||
@ -154,7 +154,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
} = servicesManager.services;
|
||||
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
uiDialogService.dismissAll();
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
|
||||
@ -6,7 +6,13 @@ window.config = {
|
||||
// whiteLabeling: {},
|
||||
extensions: [],
|
||||
modes: [],
|
||||
customizationService: {},
|
||||
customizationService: {
|
||||
'viewportActionMenu.windowLevelActionMenu': {
|
||||
$merge: {
|
||||
location: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
showStudyList: true,
|
||||
// some windows systems have issues with more than 3 web workers
|
||||
maxNumberOfWebWorkers: 3,
|
||||
@ -17,6 +23,7 @@ window.config = {
|
||||
experimentalStudyBrowserSort: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
groupEnabledModesFirst: true,
|
||||
allowMultiSelectExport: true,
|
||||
maxNumRequests: {
|
||||
interaction: 100,
|
||||
thumbnail: 75,
|
||||
|
||||
@ -15,9 +15,6 @@ import {
|
||||
SystemContextProvider,
|
||||
} from '@ohif/core';
|
||||
import {
|
||||
DialogProvider,
|
||||
Modal,
|
||||
ModalProvider,
|
||||
ThemeWrapper,
|
||||
ViewportDialogProvider,
|
||||
CineProvider,
|
||||
@ -27,8 +24,12 @@ import {
|
||||
ThemeWrapper as ThemeWrapperNext,
|
||||
NotificationProvider,
|
||||
ViewportGridProvider,
|
||||
DialogProvider,
|
||||
TooltipProvider,
|
||||
ToolboxProvider,
|
||||
Modal as ModalNext,
|
||||
ManagedDialog,
|
||||
ModalProvider,
|
||||
} from '@ohif/ui-next';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
@ -122,8 +123,8 @@ function App({
|
||||
[CineProvider, { service: cineService }],
|
||||
[NotificationProvider, { service: uiNotificationService }],
|
||||
[TooltipProvider],
|
||||
[DialogProvider, { service: uiDialogService }],
|
||||
[ModalProvider, { service: uiModalService, modal: Modal }],
|
||||
[DialogProvider, { service: uiDialogService, dialog: ManagedDialog }],
|
||||
[ModalProvider, { service: uiModalService, modal: ModalNext }],
|
||||
[ShepherdJourneyProvider],
|
||||
];
|
||||
|
||||
|
||||
@ -199,7 +199,6 @@ function DataSourceWrapper(props: withAppTypes) {
|
||||
|
||||
servicesManager.services.uiModalService.show({
|
||||
title: 'Data Source Connection Error',
|
||||
containerDimensions: 'w-1/2',
|
||||
content: () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import filtersMeta from './filtersMeta.js';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useDebounce, useSearchParams } from '@hooks';
|
||||
import { utils, hotkeys } from '@ohif/core';
|
||||
import { utils } from '@ohif/core';
|
||||
import publicUrl from '../../utils/publicUrl';
|
||||
|
||||
import {
|
||||
@ -19,9 +19,6 @@ import {
|
||||
StudyListTable,
|
||||
StudyListPagination,
|
||||
StudyListFilter,
|
||||
useModal,
|
||||
AboutModal,
|
||||
UserPreferences,
|
||||
useSessionStorage,
|
||||
InvestigationalUseDialog,
|
||||
Button,
|
||||
@ -35,21 +32,19 @@ import {
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Clipboard,
|
||||
useModal,
|
||||
Onboarding,
|
||||
ScrollArea,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
import { Types } from '@ohif/ui';
|
||||
|
||||
import i18n from '@ohif/i18n';
|
||||
import { preserveQueryParameters, preserveQueryStrings } from '../../utils/preserveQueryParameters';
|
||||
|
||||
const PatientInfoVisibility = Types.PatientInfoVisibility;
|
||||
|
||||
const { sortBySeriesDate } = utils;
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
const seriesInStudiesMap = new Map();
|
||||
|
||||
/**
|
||||
@ -102,6 +97,7 @@ function WorkList({
|
||||
const sortModifier = sortDirection === 'descending' ? 1 : -1;
|
||||
const defaultSortValues =
|
||||
shouldUseDefaultSort && canSort ? { sortBy: 'studyDate', sortDirection: 'ascending' } : {};
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const sortedStudies = useMemo(() => {
|
||||
if (!canSort) {
|
||||
@ -470,8 +466,9 @@ function WorkList({
|
||||
});
|
||||
|
||||
const hasStudies = numOfStudies > 0;
|
||||
const versionNumber = process.env.VERSION_NUMBER;
|
||||
const commitHash = process.env.COMMIT_HASH;
|
||||
|
||||
const AboutModal = customizationService.getCustomization('ohif.aboutModal');
|
||||
const UserPreferencesModal = customizationService.getCustomization('ohif.userPreferencesModal');
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
@ -479,10 +476,9 @@ function WorkList({
|
||||
icon: 'info',
|
||||
onClick: () =>
|
||||
show({
|
||||
content: AboutModal,
|
||||
content: AboutModal as React.ComponentType,
|
||||
title: t('AboutModal:About OHIF Viewer'),
|
||||
contentProps: { versionNumber, commitHash },
|
||||
containerDimensions: 'max-w-4xl max-h-4xl',
|
||||
containerClassName: 'max-w-md ',
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -491,24 +487,8 @@ function WorkList({
|
||||
onClick: () =>
|
||||
show({
|
||||
title: t('UserPreferencesModal:User preferences'),
|
||||
content: UserPreferences,
|
||||
contentProps: {
|
||||
hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
|
||||
hotkeyDefinitions,
|
||||
onCancel: hide,
|
||||
currentLanguage: currentLanguage(),
|
||||
availableLanguages,
|
||||
defaultLanguage,
|
||||
onSubmit: state => {
|
||||
if (state.language.value !== currentLanguage().value) {
|
||||
i18n.changeLanguage(state.language.value);
|
||||
}
|
||||
hotkeysManager.setHotkeys(state.hotkeyDefinitions);
|
||||
hide();
|
||||
},
|
||||
onReset: () => hotkeysManager.restoreDefaultBindings(),
|
||||
hotkeysModule: hotkeys,
|
||||
},
|
||||
content: UserPreferencesModal as React.ComponentType,
|
||||
containerClassName: 'flex max-w-4xl flex-col',
|
||||
}),
|
||||
},
|
||||
];
|
||||
@ -523,7 +503,6 @@ function WorkList({
|
||||
});
|
||||
}
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
const LoadingIndicatorProgress = customizationService.getCustomization(
|
||||
'ui.loadingIndicatorProgress'
|
||||
);
|
||||
|
||||
@ -70,7 +70,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
|
||||
uiDialogService.dismissAll();
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
|
||||
@ -135,7 +135,7 @@ const bindings = [
|
||||
},
|
||||
{
|
||||
commandName: 'cancelMeasurement',
|
||||
label: 'Cancel Cornerstone Measurement',
|
||||
label: 'Cancel Measurement',
|
||||
keys: ['esc'],
|
||||
},
|
||||
{
|
||||
|
||||
@ -382,6 +382,10 @@ export default class ExtensionManager extends PubSubService {
|
||||
return this.dataSourceMap[dataSourceName];
|
||||
};
|
||||
|
||||
getDataSourceInstance = dataSourceName => {
|
||||
return this.dataSourceMap[dataSourceName][0];
|
||||
};
|
||||
|
||||
getActiveDataSource = () => {
|
||||
return this.dataSourceMap[this.activeDataSource];
|
||||
};
|
||||
@ -409,6 +413,29 @@ export default class ExtensionManager extends PubSubService {
|
||||
return this.getDataSourceDefinition(this.activeDataSource);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a formatted list of data sources suitable for UI display/selection.
|
||||
* Only returns data sources that support STOW or have a WADO root.
|
||||
* @returns Array of data source options with value, label, and placeholder
|
||||
*/
|
||||
getDataSourcesForUI = () => {
|
||||
// If multi-select export is not allowed, return empty list
|
||||
if (this._appConfig?.allowMultiSelectExport === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this.dataSourceMap)
|
||||
.filter(ds => {
|
||||
const configuration = this.dataSourceDefs[ds]?.configuration;
|
||||
return configuration?.supportsStow ?? configuration?.wadoRoot;
|
||||
})
|
||||
.map(ds => ({
|
||||
value: ds,
|
||||
label: ds,
|
||||
placeHolder: ds,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} moduleType
|
||||
|
||||
@ -9,7 +9,6 @@ export interface BaseCustomization extends Obj {
|
||||
description?: string;
|
||||
label?: string;
|
||||
commands?: Command[];
|
||||
content?: (...props: any) => React.JSX.Element;
|
||||
}
|
||||
|
||||
export interface LabelCustomization extends BaseCustomization {
|
||||
@ -29,6 +28,7 @@ export interface ComponentCustomization extends BaseCustomization {
|
||||
}
|
||||
|
||||
export type Customization =
|
||||
| React.ComponentType
|
||||
| BaseCustomization
|
||||
| LabelCustomization
|
||||
| CommandCustomization
|
||||
|
||||
@ -1,80 +1,95 @@
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import type { ManagedDialogProps } from 'platform/ui-next/src/contextProviders/ManagedDialog';
|
||||
type DialogOptions = ManagedDialogProps;
|
||||
|
||||
class UIDialogService extends PubSubService {
|
||||
public static readonly EVENTS = {};
|
||||
const name = 'uiDialogService';
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'uiDialogService',
|
||||
const serviceImplementation = {
|
||||
_show: (options: DialogOptions) => {
|
||||
console.warn('show() NOT IMPLEMENTED');
|
||||
return '';
|
||||
},
|
||||
_hide: (id: string) => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_hideAll: () => console.warn('hideAll() NOT IMPLEMENTED'),
|
||||
_isEmpty: () => {
|
||||
console.warn('isEmpty() NOT IMPLEMENTED');
|
||||
return true;
|
||||
},
|
||||
_customComponent: null,
|
||||
};
|
||||
|
||||
class UIDialogService {
|
||||
static REGISTRATION = {
|
||||
name,
|
||||
altName: 'UIDialogService',
|
||||
create: ({ configuration = {} }) => {
|
||||
create: (): UIDialogService => {
|
||||
return new UIDialogService();
|
||||
},
|
||||
};
|
||||
|
||||
serviceImplementation = {
|
||||
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
|
||||
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
|
||||
_create: () => console.warn('create() NOT IMPLEMENTED'),
|
||||
};
|
||||
readonly name = name;
|
||||
|
||||
constructor() {
|
||||
super(UIDialogService.EVENTS);
|
||||
this.serviceImplementation = {
|
||||
...this.serviceImplementation,
|
||||
};
|
||||
/**
|
||||
* Show a new UI dialog
|
||||
*
|
||||
* @param {DialogOptions} options - The dialog options
|
||||
* @returns {string} The dialog id
|
||||
*/
|
||||
show(options: DialogOptions): string {
|
||||
return serviceImplementation._show(options);
|
||||
}
|
||||
|
||||
public create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
centralize = false,
|
||||
preservePosition = true,
|
||||
isDraggable = true,
|
||||
showOverlay = false,
|
||||
defaultPosition,
|
||||
onClickOutside,
|
||||
}) {
|
||||
return this.serviceImplementation._create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
centralize,
|
||||
preservePosition,
|
||||
isDraggable,
|
||||
showOverlay,
|
||||
defaultPosition,
|
||||
onClickOutside,
|
||||
});
|
||||
/**
|
||||
* Hide a specific dialog by id
|
||||
*
|
||||
* @param {string} id - The dialog id to hide
|
||||
*/
|
||||
hide(id: string): void {
|
||||
return serviceImplementation._hide(id);
|
||||
}
|
||||
|
||||
public dismiss({ id }) {
|
||||
return this.serviceImplementation._dismiss({ id });
|
||||
/**
|
||||
* Hide all currently shown dialogs
|
||||
*/
|
||||
hideAll(): void {
|
||||
return serviceImplementation._hideAll();
|
||||
}
|
||||
|
||||
public dismissAll() {
|
||||
return this.serviceImplementation._dismissAll();
|
||||
/**
|
||||
* Check if there are any dialogs currently shown
|
||||
*
|
||||
* @returns {boolean} True if no dialogs are shown
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return serviceImplementation._isEmpty();
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
dismiss: dismissImplementation,
|
||||
dismissAll: dismissAllImplementation,
|
||||
create: createImplementation,
|
||||
}) {
|
||||
if (dismissImplementation) {
|
||||
this.serviceImplementation._dismiss = dismissImplementation;
|
||||
/**
|
||||
* This provides flexibility in customizing the Modal's default component
|
||||
*
|
||||
* @returns {React.Component}
|
||||
*/
|
||||
getCustomComponent() {
|
||||
return serviceImplementation._customComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the service implementation
|
||||
*/
|
||||
setServiceImplementation({ show, hide, hideAll, isEmpty, customComponent }: any): void {
|
||||
if (show) {
|
||||
serviceImplementation._show = show;
|
||||
}
|
||||
if (dismissAllImplementation) {
|
||||
this.serviceImplementation._dismissAll = dismissAllImplementation;
|
||||
if (hide) {
|
||||
serviceImplementation._hide = hide;
|
||||
}
|
||||
if (createImplementation) {
|
||||
this.serviceImplementation._create = createImplementation;
|
||||
if (hideAll) {
|
||||
serviceImplementation._hideAll = hideAll;
|
||||
}
|
||||
if (isEmpty) {
|
||||
serviceImplementation._isEmpty = isEmpty;
|
||||
}
|
||||
if (customComponent) {
|
||||
serviceImplementation._customComponent = customComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,3 @@
|
||||
/**
|
||||
* UI Modal
|
||||
*
|
||||
* @typedef {Object} ModalProps
|
||||
* @property {ReactElement|HTMLElement} [content=null] Modal content.
|
||||
* @property {Object} [contentProps=null] Modal content props.
|
||||
* @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key.
|
||||
* @property {boolean} [isOpen=true] Make the Modal visible or hidden.
|
||||
* @property {boolean} [closeButton=true] Should the modal body render the close button.
|
||||
* @property {string} [title=null] Should the modal render the title independently of the body content.
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const name = 'uiModalService';
|
||||
|
||||
const serviceImplementation = {
|
||||
@ -38,30 +25,20 @@ class UIModalService {
|
||||
show({
|
||||
content = null,
|
||||
contentProps = null,
|
||||
shouldCloseOnEsc = true,
|
||||
isOpen = true,
|
||||
closeButton = true,
|
||||
title = null,
|
||||
customClassName = null,
|
||||
movable = false,
|
||||
containerDimensions = null,
|
||||
contentDimensions = null,
|
||||
className = null,
|
||||
shouldCloseOnEsc = true,
|
||||
shouldCloseOnOverlayClick = true,
|
||||
shouldCloseImmediately = false,
|
||||
containerClassName = null,
|
||||
}) {
|
||||
return serviceImplementation._show({
|
||||
content,
|
||||
contentProps,
|
||||
shouldCloseOnEsc,
|
||||
isOpen,
|
||||
closeButton,
|
||||
title,
|
||||
customClassName,
|
||||
movable,
|
||||
containerDimensions,
|
||||
contentDimensions,
|
||||
className,
|
||||
shouldCloseOnOverlayClick,
|
||||
shouldCloseImmediately,
|
||||
containerClassName,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
BIN
platform/docs/docs/assets/img/aboutModal.png
Normal file
BIN
platform/docs/docs/assets/img/aboutModal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
BIN
platform/docs/docs/assets/img/captureViewportModal.png
Normal file
BIN
platform/docs/docs/assets/img/captureViewportModal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
BIN
platform/docs/docs/assets/img/viewport-download-warning.png
Normal file
BIN
platform/docs/docs/assets/img/viewport-download-warning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
@ -0,0 +1,314 @@
|
||||
---
|
||||
title: uiDialogService
|
||||
---
|
||||
|
||||
|
||||
## DialogService
|
||||
|
||||
This guide details the migration steps for the `uiDialogService` API changes, based on the provided diff. The most significant change is a shift from methods like `.create()` and `.dismiss()` to `.show()` and `.hide()`, alongside structural changes in how dialogs are defined and rendered. The changes aim for a more streamlined and flexible dialog management, leveraging React context for state management.
|
||||
|
||||
**Key Changes:**
|
||||
|
||||
* **`uiDialogService.create()` and `uiDialogService.dismiss()` are deprecated.** They have been replaced with `uiDialogService.show()` and `uiDialogService.hide()`. This change
|
||||
makes it consistent with the `uiModalService` and `uiNotificationService` APIs.
|
||||
* **`content` property now expects a React Component Type**, not an instance. Props for the content component are passed via `contentProps`.
|
||||
* **The `dialogId` is now consistently passed as `id` within the options** to `uiDialogService.show()`.
|
||||
|
||||
|
||||
### Props Kept same as before
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `id` | Still required, but we don't return it from the `show` method anymore. |
|
||||
| `content` | This is now expected to be a *React component type* (a function or class that returns JSX) |
|
||||
| `contentProps` | This continues to be the way to pass data *to* your custom dialog component. However, several specific props that *used* to be passed here (like `onClose`, `actions`) are no longer valid. |
|
||||
| `isDraggable` | Controls whether the dialog can be moved by dragging. |
|
||||
| `defaultPosition` | Allows you to specify an initial `{ x, y }` position for the dialog. |
|
||||
| `title` | The title text to display in the dialog header. |
|
||||
| `showOverlay` | default true - if the dialog is draggable the overlay is not shown by default |
|
||||
|
||||
|
||||
### Removed Props:
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `centralize` | Dialogs are now centered by default via CSS if you don't want center you pass defaultPosition |
|
||||
| `preservePosition` | Work in progress and will be available in future |
|
||||
| `contentDimensions` | Removed - should be specified directly in dialogs |
|
||||
| `onStart` | Removed |
|
||||
| `onDrag` | Removed |
|
||||
| `onStop` | Removed |
|
||||
| `onClickOutside` | Removed - if you want to close the dialog on click outside, you can use the `shouldCloseOnOverlayClick` prop |
|
||||
|
||||
|
||||
### Renamed Props:
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `containerDimensions` | renamed to `containerClassName` |
|
||||
|
||||
|
||||
|
||||
|
||||
### New Props:
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `unstyled` | A boolean prop to render the dialog without the default styling. It is used for context menu dialogs |
|
||||
| `shouldCloseOnEsc` | Default off for dialogs - Controls whether pressing the Escape key will close the dialog. |
|
||||
| `shouldCloseOnOverlayClick` | Default off for dialogs - Controls whether clicking the overlay background will close the dialog. |
|
||||
|
||||
|
||||
**Migration Steps:**
|
||||
|
||||
1. **Replace `.create()` with `.show()`:**
|
||||
|
||||
```diff
|
||||
- const dialogId = uiDialogService.create({
|
||||
- id: 'my-dialog',
|
||||
- content: MyDialogComponent,
|
||||
- contentProps: { prop1: 'value1' },
|
||||
- // ... other options ...
|
||||
- });
|
||||
|
||||
+ uiDialogService.show({
|
||||
+ id: 'my-dialog',
|
||||
+ content: MyDialogComponent,
|
||||
+ contentProps: { prop1: 'value1' },
|
||||
+ // ... other options ...
|
||||
+ });
|
||||
```
|
||||
|
||||
2. Rename `containerDimensions` to `containerClassName`
|
||||
|
||||
```diff
|
||||
- containerDimensions: 'w-[70%] max-w-[900px]',
|
||||
+ containerClassName: 'w-[70%] max-w-[900px]',
|
||||
```
|
||||
|
||||
3. **Replace `.dismiss({ id: dialogId })` with `.hide(dialogId)`:**
|
||||
|
||||
```diff
|
||||
- uiDialogService.dismiss({ id: dialogId });
|
||||
|
||||
+ uiDialogService.hide(dialogId);
|
||||
```
|
||||
|
||||
4. **Replace `dismissAll` with `hideAll`**
|
||||
```diff
|
||||
- uiDialogService.dismissAll();
|
||||
+ uiDialogService.hideAll();
|
||||
```
|
||||
5. **Update Dialog Content:**
|
||||
|
||||
* Ensure your dialog content is defined as a React component (functional or class-based).
|
||||
* Pass props to the component via `contentProps`.
|
||||
* You don't need to pass in `onClose` or `hide` as they are now handled passed in automatically.
|
||||
|
||||
```javascript
|
||||
// Example: MyDialogComponent.tsx
|
||||
function MyDialogComponent({ prop1, hide }) {
|
||||
return (
|
||||
<div>
|
||||
<p>Value of prop1: {prop1}</p>
|
||||
<button onClick={hide}>Close</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Example: Updating a Simple Alert Dialog**
|
||||
|
||||
```javascript
|
||||
// Before (using deprecated API)
|
||||
let dialogId;
|
||||
const showAlert = (message) => {
|
||||
dialogId = uiDialogService.create({
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Alert',
|
||||
body: () => <p>{message}</p>,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// After (using new API)
|
||||
function AlertDialog({ message, hide }) {
|
||||
return (
|
||||
<div>
|
||||
<p>{message}</p>
|
||||
<button onClick={hide}>OK</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const showAlert = (message) => {
|
||||
uiDialogService.show({
|
||||
id: 'alert-dialog',
|
||||
title: 'Alert',
|
||||
content: AlertDialog,
|
||||
contentProps: { message },
|
||||
});
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### CreateReportDialogPrompt
|
||||
|
||||
**Key Changes:**
|
||||
|
||||
* **Function Signature Update:** The function now accepts an object with `servicesManager`, `extensionManager`, `title`(optional).
|
||||
* **Return Value Structure:** The function now returns an object containing `value` (the report name), `dataSourceName` (the selected data source, if applicable), and `action` (indicating the user's choice).
|
||||
|
||||
**Migration Steps:**
|
||||
|
||||
1. **Update Function Call:**
|
||||
|
||||
Previously, the function was called with separate arguments. You should now pass an object:
|
||||
|
||||
```diff
|
||||
- const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
- extensionManager,
|
||||
- });
|
||||
|
||||
+ const promptResult = await createReportDialogPrompt({
|
||||
+ servicesManager,
|
||||
+ extensionManager,
|
||||
+ title: 'Store Segmentation', // Optional title
|
||||
+ });
|
||||
|
||||
```
|
||||
|
||||
### promptSaveReport
|
||||
|
||||
Not changed, just a javascript to typescript migration.
|
||||
|
||||
### callLabelAutocompleteDialog
|
||||
|
||||
`callLabelAutocompleteDialog` is deprecated and has been replaced by `callInputDialogAutoComplete`. This new function simplifies the asynchronous handling of user input by using `uiDialogService.show()` and returning a promise.
|
||||
|
||||
|
||||
|
||||
### showLabelAnnotationPopup
|
||||
|
||||
`showLabelAnnotationPopup` has been replaced with `callInputDialogAutoComplete`. This update also uses `uiDialogService.show()` and promises, and it removes the callback function.
|
||||
|
||||
- The function now expects an object with `measurement`, `uiDialogService`, `labelConfig`, and `renderContent`.
|
||||
|
||||
```diff
|
||||
- const value = await showLabelAnnotationPopup(
|
||||
- measurement,
|
||||
- servicesManager.services.uiDialogService,
|
||||
- labelConfig,
|
||||
- renderContent
|
||||
- );
|
||||
+ const value = await callInputDialogAutoComplete({
|
||||
+ measurement,
|
||||
+ uiDialogService,
|
||||
+ labelConfig,
|
||||
+ renderContent,
|
||||
+ });
|
||||
```
|
||||
|
||||
### callInputDialog
|
||||
|
||||
- expects an objects now and returns the value of the input which you can then use for actions
|
||||
|
||||
|
||||
```diff
|
||||
|
||||
- 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)',
|
||||
- validateFunc: val => {
|
||||
- const v = Number.parseFloat(val);
|
||||
- return !isNaN(v) && v !== 0.0;
|
||||
- },
|
||||
- }
|
||||
- );
|
||||
+ callInputDialog({
|
||||
+ uiDialogService,
|
||||
+ title: 'Calibration',
|
||||
+ placeholder: 'Actual Physical distance (mm)',
|
||||
+ defaultValue: `${length}`,
|
||||
+ }).then(newValue => {
|
||||
+ adjustCalibration(Number.parseFloat(newValue));
|
||||
+ resolve(true);
|
||||
+ });
|
||||
```
|
||||
|
||||
or another one
|
||||
|
||||
```diff
|
||||
- callInputDialog(
|
||||
- uiDialogService,
|
||||
- { text: '', label: 'Enter description' },
|
||||
- (value, action) => {
|
||||
- if (action === 'save') {
|
||||
- saveFunction(value);
|
||||
- }
|
||||
- }
|
||||
- );
|
||||
+ callInputDialog({
|
||||
+ uiDialogService,
|
||||
+ title: 'Enter description of the Series',
|
||||
+ defaultValue: '',
|
||||
+ }).then(value => {
|
||||
+ saveFunction(value);
|
||||
+ });
|
||||
```
|
||||
|
||||
|
||||
### colorPickerDialog
|
||||
|
||||
Instead of calling `colorPickerDialog(uiDialogService, rgbaColor, callback)`, use `uiDialogService.show()` with `ColorPickerDialog` as the content.
|
||||
|
||||
|
||||
```diff
|
||||
- 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);
|
||||
- });
|
||||
|
||||
// after
|
||||
|
||||
+ 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);
|
||||
+ },
|
||||
+ },
|
||||
+ });
|
||||
```
|
||||
@ -0,0 +1,104 @@
|
||||
---
|
||||
title: uiModalService
|
||||
---
|
||||
|
||||
|
||||
## ModalService
|
||||
|
||||
|
||||
### Props Kept same as before
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `content` | This is now expected to be a *React component type* (a function or class that returns JSX) |
|
||||
| `contentProps` | This continues to be the way to pass data *to* your custom dialog component. However, several specific props that *used* to be passed here (like `onClose`, `actions`) are no longer valid. |
|
||||
| `title` | The title text to display in the dialog header. |
|
||||
| `shouldCloseOnEsc` | Allows closing the modal when the escape key is pressed. |
|
||||
| `shouldCloseOnOverlayClick` | Allows closing the modal when the overlay is clicked. |
|
||||
|
||||
### Renamed Props:
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `containerDimensions` | renamed to `containerClassName` |
|
||||
|
||||
|
||||
|
||||
### Removed Props:
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `movable` | It's removed because modals shouldn't be movable. If you need to move a dialog, use `uidDialogService` and `dialogs` instead. |
|
||||
| `isOpen` | always assumed `true` when `show` is called. |
|
||||
| `contentDimensions` | Removed, it is now component's responsibility to set the size for the content |
|
||||
| `customClassName` | renamed to `className` |
|
||||
| `closeButton` | The component now manages modal closing internally. If you need a close button, you can add one, perhaps by checking out the `FooterActions` component. |
|
||||
|
||||
|
||||
|
||||
|
||||
**Migration Steps:**
|
||||
|
||||
|
||||
### Rename of `containerDimensions` to `containerClassName` and removal of `contentDimensions`
|
||||
|
||||
|
||||
Before
|
||||
|
||||
```js
|
||||
uiModalService.show({
|
||||
title: 'Download High-Quality Image',
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
contentProps: {
|
||||
activeViewportId,
|
||||
},
|
||||
containerDimensions: 'w-[70%] max-w-[900px]',
|
||||
contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]',
|
||||
});
|
||||
```
|
||||
|
||||
After: the component is responsible for setting the size
|
||||
|
||||
```js
|
||||
function CornerstoneViewportDownloadForm({ activeViewportId }) {
|
||||
return (
|
||||
<div className="h-[493px] w-[460px] pl-[12px] pr-[12px]">
|
||||
<h2 className="text-lg font-bold">Download Image</h2>
|
||||
<p>Viewport ID: {activeViewportId}</p>
|
||||
<button className="mt-4 bg-blue-500 text-white p-2 rounded">Download</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
uiModalService.show({
|
||||
title: 'Download High-Quality Image',
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
contentProps: { activeViewportId },
|
||||
containerClassName: 'w-[70%] max-w-[900px]',
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### onClose
|
||||
Previously, you had to pass in the `onClose` as `hide` function automatically added to the component.
|
||||
|
||||
```diff
|
||||
- uiModalService.show({
|
||||
- title: 'Untrack Series',
|
||||
- content: UntrackSeriesModal,
|
||||
- contentProps: { onConfirm },
|
||||
- onClose: () => uiModalService.hide(),
|
||||
- });
|
||||
|
||||
+ uiModalService.show({
|
||||
+ title: 'Untrack Series',
|
||||
+ content: UntrackSeriesModal,
|
||||
+ contentProps: {
|
||||
+ onConfirm,
|
||||
+ hide, // passed in automatically in the background
|
||||
+ },
|
||||
+ });
|
||||
```
|
||||
@ -16,6 +16,7 @@ import loadingIndicatorProgress from '../../../assets/img/loading-indicator-icon
|
||||
import loadingIndicatorPercent from '../../../assets/img/loading-indicator-percent.png';
|
||||
import viewportActionCorners from '../../../assets/img/viewport-action-corners.png';
|
||||
import contextMenu from '../../../assets/img/context-menu.jpg';
|
||||
import viewportDownloadWarning from '../../../assets/img/viewport-download-warning.png';
|
||||
import segmentationOverlay from '../../../assets/img/segmentation-overlay.png';
|
||||
|
||||
import segDisplayEditingTrue from '../../../assets/img/segDisplayEditingTrue.png';
|
||||
@ -24,6 +25,8 @@ import thumbnailMenuItemsImage from '../../../assets/img/thumbnailMenuItemsImage
|
||||
import studyMenuItemsImage from '../../../assets/img/studyMenuItemsImage.png';
|
||||
import windowLevelActionMenu from '../../../assets/img/windowLevelActionMenu.png';
|
||||
import viewPortNotificationImage from '../../../assets/img/viewport-notification.png';
|
||||
import captureViewportModal from '../../../assets/img/captureViewportModal.png';
|
||||
import aboutModal from '../../../assets/img/aboutModal.png';
|
||||
|
||||
export const viewportOverlayCustomizations = [
|
||||
{
|
||||
@ -269,24 +272,6 @@ window.config = {
|
||||
default: 'The CinePlayer component in the UI',
|
||||
configuration: null,
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.windowLevelActionMenu',
|
||||
description: 'Window level action menu for the cornerstone viewport.',
|
||||
image: windowLevelActionMenu,
|
||||
default: null,
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'cornerstone.windowLevelActionMenu': {
|
||||
$set: CustomizedComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.windowLevelPresets',
|
||||
description: 'Window level presets for the cornerstone viewport.',
|
||||
@ -785,10 +770,142 @@ window.config = {
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportDownload.warningMessage',
|
||||
description: 'Customizes the warning message for the viewport download form.',
|
||||
image: viewportDownloadWarning,
|
||||
default: {
|
||||
enabled: true,
|
||||
value: 'Not For Diagnostic Use',
|
||||
},
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportDownload.warningMessage': {
|
||||
$set: {
|
||||
enabled: true,
|
||||
value: 'Careful! This is not for diagnostic use.',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'ohif.captureViewportModal',
|
||||
description: 'The modal for capturing the viewport image.',
|
||||
image: captureViewportModal,
|
||||
default: 'Our own default component',
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
|
||||
// You can use the component from ImageModal and FooterAction
|
||||
// to build your own custom component
|
||||
customizationService: [
|
||||
{
|
||||
'ohif.captureViewportModal': {
|
||||
$set: CustomizedComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'ohif.aboutModal',
|
||||
description: 'The About modal',
|
||||
image: aboutModal,
|
||||
default: 'Our own default component',
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
|
||||
// You can use the component from AboutModal
|
||||
// to build your own custom component
|
||||
customizationService: [
|
||||
{
|
||||
'ohif.aboutModal': {
|
||||
$set: CustomizedComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportDownload.warningMessage',
|
||||
description: 'Customizes the warning message for the viewport download form.',
|
||||
image: viewportDownloadWarning,
|
||||
default: {
|
||||
enabled: true,
|
||||
value: 'Not For Diagnostic Use',
|
||||
},
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportDownload.warningMessage': {
|
||||
$set: {
|
||||
enabled: true,
|
||||
value: 'Careful! This is not for diagnostic use.',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'ohif.captureViewportModal',
|
||||
description: 'The modal for capturing the viewport image.',
|
||||
image: captureViewportModal,
|
||||
default: 'Our own default component',
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
|
||||
// You can use the component from ImageModal and FooterAction
|
||||
// to build your own custom component
|
||||
customizationService: [
|
||||
{
|
||||
'ohif.captureViewportModal': {
|
||||
$set: CustomizedComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'ohif.aboutModal',
|
||||
description: 'The About modal',
|
||||
image: aboutModal,
|
||||
default: 'Our own default component',
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
|
||||
// You can use the component from AboutModal
|
||||
// to build your own custom component
|
||||
customizationService: [
|
||||
{
|
||||
'ohif.aboutModal': {
|
||||
$set: CustomizedComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportActionMenu.windowLevelActionMenu',
|
||||
description:
|
||||
'Configures the display and location of the window level action menu in the viewport.',
|
||||
'Configures the display and location of the window level action menu in the viewport.',
|
||||
image: windowLevelActionMenu,
|
||||
default: null,
|
||||
configuration: `
|
||||
@ -797,9 +914,8 @@ window.config = {
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.windowLevelActionMenu': {
|
||||
$set: {
|
||||
enabled: true,
|
||||
location: 1, // Set the location of the menu in the viewport.
|
||||
$merge: {
|
||||
location: 0, // Set the location of the menu in the viewport.
|
||||
// 0: topLeft
|
||||
// 1: topRight
|
||||
// 2: bottomLeft
|
||||
@ -822,7 +938,7 @@ window.config = {
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.segmentationOverlay': {
|
||||
$set: {
|
||||
$merge: {
|
||||
enabled: true,
|
||||
location: 1, // Set the location of the overlay in the viewport.
|
||||
// 0: topLeft
|
||||
|
||||
@ -136,6 +136,7 @@ module.exports = {
|
||||
prism: {
|
||||
theme: require('prism-react-renderer').themes.github,
|
||||
darkTheme: require('prism-react-renderer').themes.dracula,
|
||||
additionalLanguages: ['diff'],
|
||||
},
|
||||
algolia: {
|
||||
appId: 'EFLT6YIHHZ',
|
||||
|
||||
@ -233,11 +233,6 @@ input[type='number'] {
|
||||
-moz-appearance: textfield; /* For Firefox */
|
||||
}
|
||||
|
||||
.navbar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar__item svg {
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
@ -642,6 +637,7 @@ html[data-theme='dark'] .markdown hr {
|
||||
/* Markdown code block styling */
|
||||
.markdown pre {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.theme-code-block {
|
||||
@ -725,3 +721,7 @@ a.dropdown__link[href='/3.9/migration-guide/3p8-to-3p9/']::after {
|
||||
font-size: 0.9em;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
margin-top: 0.5em; /* Adjust '1em' to your desired spacing */
|
||||
}
|
||||
|
||||
@ -98,8 +98,7 @@ function initI18n(
|
||||
},
|
||||
},
|
||||
react: {
|
||||
useSuspense: false, // TODO: Was seeing weird errors without this
|
||||
wait: true,
|
||||
useSuspense: true,
|
||||
bindI18n: 'languageChanged editorSaved',
|
||||
},
|
||||
});
|
||||
@ -123,7 +122,7 @@ function initI18n(
|
||||
},
|
||||
detection,
|
||||
react: {
|
||||
wait: true,
|
||||
useSuspense: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -190,7 +190,8 @@ const DataRow: React.FC<DataRowProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={rowRef}
|
||||
className={`flex flex-col ${isVisible ? '' : 'opacity-60'}`}>
|
||||
className={`flex flex-col ${isVisible ? '' : 'opacity-60'}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center ${
|
||||
isSelected ? 'bg-popover' : 'bg-muted'
|
||||
@ -291,7 +292,11 @@ const DataRow: React.FC<DataRowProps> = ({
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
// this was causing issue for auto focus on input dialog
|
||||
onCloseAutoFocus={e => e.preventDefault()}
|
||||
>
|
||||
<>
|
||||
<DropdownMenuItem onClick={e => handleAction('Rename', e)}>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
|
||||
@ -3,8 +3,41 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useDraggable } from './useDraggable';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
interface DialogContextValue {
|
||||
isDraggable?: boolean;
|
||||
shouldCloseOnEsc?: boolean;
|
||||
shouldCloseOnOverlayClick?: boolean;
|
||||
showOverlay?: boolean;
|
||||
}
|
||||
|
||||
const DialogContext = React.createContext<DialogContextValue>({
|
||||
isDraggable: false,
|
||||
shouldCloseOnEsc: true,
|
||||
shouldCloseOnOverlayClick: true,
|
||||
});
|
||||
|
||||
interface DialogRootProps extends DialogPrimitive.DialogProps {
|
||||
isDraggable?: boolean;
|
||||
shouldCloseOnEsc?: boolean;
|
||||
shouldCloseOnOverlayClick?: boolean;
|
||||
showOverlay?: boolean;
|
||||
}
|
||||
|
||||
const Dialog = ({
|
||||
isDraggable,
|
||||
shouldCloseOnEsc = true,
|
||||
shouldCloseOnOverlayClick = true,
|
||||
showOverlay = true,
|
||||
...props
|
||||
}: DialogRootProps) => (
|
||||
<DialogContext.Provider
|
||||
value={{ isDraggable, shouldCloseOnEsc, shouldCloseOnOverlayClick, showOverlay }}
|
||||
>
|
||||
<DialogPrimitive.Root {...props} />
|
||||
</DialogContext.Provider>
|
||||
);
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
@ -14,12 +47,14 @@ const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
|
||||
className?: string;
|
||||
}
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-40 bg-black/60',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -27,36 +62,83 @@ const DialogOverlay = React.forwardRef<
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {}
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
DialogContentProps & {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { isDraggable, shouldCloseOnEsc, shouldCloseOnOverlayClick, showOverlay } =
|
||||
React.useContext(DialogContext);
|
||||
|
||||
const { handlePointerDown, setRefs, initialTransform } = useDraggable(
|
||||
{
|
||||
enabled: isDraggable,
|
||||
},
|
||||
ref
|
||||
);
|
||||
|
||||
// When not isDraggable, Tailwind centers the dialog.
|
||||
// When isDraggable, we remove the built‑in centering so our inline transform takes over.
|
||||
const contentClassName = cn(
|
||||
'max-w-md w-full bg-muted data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid gap-4 p-4 shadow-lg duration-200 sm:rounded-lg',
|
||||
!isDraggable ? 'translate-x-[-50%] translate-y-[-50%]' : '',
|
||||
className
|
||||
);
|
||||
|
||||
const style = isDraggable ? { ...props.style, transform: initialTransform } : props.style;
|
||||
|
||||
const content = (
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
ref={setRefs}
|
||||
className={contentClassName}
|
||||
{...props}
|
||||
style={style}
|
||||
onPointerDown={isDraggable ? handlePointerDown : props.onPointerDown}
|
||||
onEscapeKeyDown={event => {
|
||||
if (!shouldCloseOnEsc) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
onInteractOutside={event => {
|
||||
if (!shouldCloseOnOverlayClick) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<Cross2Icon className="text-primary h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogPortal>
|
||||
{showOverlay && !isDraggable && <DialogOverlay />}
|
||||
{content}
|
||||
</DialogPortal>
|
||||
);
|
||||
});
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
'drag-handle relative flex select-none flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
@ -69,11 +151,13 @@ DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> & {
|
||||
className?: string;
|
||||
}
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-xl font-semibold leading-none tracking-tight', className)}
|
||||
className={cn('text-primary-light text-xl font-normal leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@ -81,11 +165,13 @@ DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> & {
|
||||
className?: string;
|
||||
}
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-base', className)}
|
||||
className={cn('text-base', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
108
platform/ui-next/src/components/Dialog/useDraggable.ts
Normal file
108
platform/ui-next/src/components/Dialog/useDraggable.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface Offset {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
startX: number;
|
||||
startY: number;
|
||||
initialOffset: Offset;
|
||||
}
|
||||
|
||||
interface UseDraggableProps {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface UseDraggableReturn {
|
||||
offset: React.RefObject<Offset>;
|
||||
internalRef: React.RefObject<HTMLDivElement>;
|
||||
handlePointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;
|
||||
setRefs: (node: HTMLDivElement) => void;
|
||||
initialTransform: string | undefined;
|
||||
}
|
||||
|
||||
export function useDraggable(
|
||||
props: UseDraggableProps,
|
||||
ref?: React.ForwardedRef<HTMLDivElement>
|
||||
): UseDraggableReturn {
|
||||
const { enabled = false } = props;
|
||||
const offsetRef = React.useRef<Offset>({ x: 0, y: 0 });
|
||||
const internalRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const dragState = React.useRef<DragState | null>(null);
|
||||
|
||||
const handlePointerMove = React.useCallback((e: PointerEvent) => {
|
||||
if (!dragState.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = e.clientX - dragState.current.startX;
|
||||
const deltaY = e.clientY - dragState.current.startY;
|
||||
const newOffset = {
|
||||
x: dragState.current.initialOffset.x + deltaX,
|
||||
y: dragState.current.initialOffset.y + deltaY,
|
||||
};
|
||||
|
||||
offsetRef.current = newOffset;
|
||||
if (internalRef.current) {
|
||||
internalRef.current.style.transform = `translate(-50%, -50%) translate(${newOffset.x}px, ${newOffset.y}px)`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePointerUp = React.useCallback(() => {
|
||||
if (internalRef.current) {
|
||||
internalRef.current.style.transition = '';
|
||||
}
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
dragState.current = null;
|
||||
}, [handlePointerMove]);
|
||||
|
||||
const handlePointerDown = React.useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.drag-handle')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (internalRef.current) {
|
||||
internalRef.current.style.transition = 'none';
|
||||
}
|
||||
dragState.current = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
initialOffset: { ...offsetRef.current },
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
},
|
||||
[handlePointerMove, handlePointerUp]
|
||||
);
|
||||
|
||||
const setRefs = React.useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
internalRef.current = node;
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
const initialTransform = enabled
|
||||
? `translate(-50%, -50%) translate(${offsetRef.current.x}px, ${offsetRef.current.y}px)`
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
offset: offsetRef,
|
||||
internalRef,
|
||||
handlePointerDown,
|
||||
setRefs,
|
||||
initialTransform,
|
||||
};
|
||||
}
|
||||
@ -62,6 +62,11 @@ const DropdownMenuContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
onKeyDown={e => {
|
||||
// Todo: or maybe we just want to prevent arrows?
|
||||
e.stopPropagation();
|
||||
props.onKeyDown?.(e);
|
||||
}}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground border-input z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
|
||||
102
platform/ui-next/src/components/FooterAction/FooterAction.tsx
Normal file
102
platform/ui-next/src/components/FooterAction/FooterAction.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../Button/Button';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface FooterActionProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ActionProps extends FooterActionProps {
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type FooterActionComponent = React.FC<FooterActionProps> & {
|
||||
Left: React.FC<FooterActionProps>;
|
||||
Right: React.FC<FooterActionProps>;
|
||||
Primary: React.FC<ActionProps>;
|
||||
Secondary: React.FC<ActionProps>;
|
||||
Auxiliary: React.FC<ActionProps>;
|
||||
};
|
||||
|
||||
export const FooterAction: FooterActionComponent = ({ children, className }: FooterActionProps) => {
|
||||
// Convert children to array for easier inspection
|
||||
const arrayChildren = React.Children.toArray(children);
|
||||
|
||||
// Check if we have a <FooterAction.Left> or <FooterAction.Right> among children
|
||||
const hasLeft = arrayChildren.some(
|
||||
(child: any) => child.type?.displayName === 'FooterAction.Left'
|
||||
);
|
||||
const hasRight = arrayChildren.some(
|
||||
(child: any) => child.type?.displayName === 'FooterAction.Right'
|
||||
);
|
||||
|
||||
// Decide on the justification class based on presence of Left/Right
|
||||
let justifyClass = 'justify-between'; // default
|
||||
if (hasLeft && !hasRight) {
|
||||
justifyClass = 'justify-start';
|
||||
} else if (!hasLeft && hasRight) {
|
||||
justifyClass = 'justify-end';
|
||||
}
|
||||
// If both or neither are present, keep justify-between (or adjust if you like)
|
||||
return (
|
||||
<div className={cn('flex w-full flex-shrink-0 items-center', justifyClass, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FooterAction.displayName = 'FooterAction';
|
||||
|
||||
FooterAction.Left = ({ children }: FooterActionProps) => {
|
||||
return <div className="flex items-center">{children}</div>;
|
||||
};
|
||||
FooterAction.Left.displayName = 'FooterAction.Left';
|
||||
|
||||
FooterAction.Right = ({ children }: FooterActionProps) => {
|
||||
return <div className="flex items-center space-x-2">{children}</div>;
|
||||
};
|
||||
FooterAction.Right.displayName = 'FooterAction.Right';
|
||||
|
||||
// Primary action: Solid button (default)
|
||||
FooterAction.Primary = ({ children, onClick, className = 'min-w-[80px]' }: ActionProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
FooterAction.Primary.displayName = 'FooterAction.Primary';
|
||||
|
||||
// Secondary action: Ghost button
|
||||
FooterAction.Secondary = ({ children, onClick, className = 'min-w-[80px]' }: ActionProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
FooterAction.Secondary.displayName = 'FooterAction.Secondary';
|
||||
|
||||
// Tertiary action: Ghost button with different styling
|
||||
FooterAction.Auxiliary = ({ children, onClick, className }: ActionProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
FooterAction.Auxiliary.displayName = 'FooterAction.Auxiliary';
|
||||
3
platform/ui-next/src/components/FooterAction/index.tsx
Normal file
3
platform/ui-next/src/components/FooterAction/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { FooterAction } from './FooterAction';
|
||||
|
||||
export { FooterAction };
|
||||
@ -46,6 +46,7 @@ import Show from './Sources/Show';
|
||||
import SidePanelCloseLeft from './Sources/SidePanelCloseLeft';
|
||||
import SidePanelCloseRight from './Sources/SidePanelCloseRight';
|
||||
import SortingAscending from './Sources/SortingAscending';
|
||||
import SocialGithub from './Sources/SocialGithub';
|
||||
import SortingDescending from './Sources/SortingDescending';
|
||||
import StatusError from './Sources/StatusError';
|
||||
import StatusSuccess from './Sources/StatusSuccess';
|
||||
@ -471,6 +472,7 @@ export const Icons = {
|
||||
Show,
|
||||
SidePanelCloseLeft,
|
||||
SidePanelCloseRight,
|
||||
SocialGithub,
|
||||
SortingAscending,
|
||||
SortingDescending,
|
||||
Sorting,
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const SocialGithub = (props: IconProps) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
>
|
||||
<g
|
||||
id="SocialGithub"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<g
|
||||
id="github-mark"
|
||||
transform="translate(1, 1)"
|
||||
fill="#FFFFFF"
|
||||
>
|
||||
<path
|
||||
d="M11.009102,0 C4.92135296,0 0,5.04164559 0,11.2788487 C0,16.2645778 3.15328046,20.4848935 7.52771627,21.9785956 C8.07463342,22.0908868 8.2749669,21.7359091 8.2749669,21.4373062 C8.2749669,21.1758281 8.25693914,20.279561 8.25693914,19.3457108 C5.19447352,20.018083 4.55674153,18.0011956 4.55674153,18.0011956 C4.0645837,16.6940344 3.33536083,16.3580774 3.33536083,16.3580774 C2.33301741,15.6671428 3.40837326,15.6671428 3.40837326,15.6671428 C4.52023532,15.7418508 5.10365868,16.824888 5.10365868,16.824888 C6.08774899,18.5427141 7.67351578,18.0573412 8.31147311,17.7585091 C8.4025133,17.0302205 8.69433765,16.5260559 9.00418977,16.2460154 C6.56165372,15.9845373 3.99179662,15.0135622 3.99179662,10.6811845 C3.99179662,9.44873132 4.42896978,8.44040221 5.12168644,7.65619715 C5.01239315,7.37615666 4.62952861,6.21818233 5.23120508,4.66833465 C5.23120508,4.66833465 6.16076142,4.36950256 8.25671379,5.8260798 C9.15406554,5.57919102 10.0794899,5.45359714 11.009102,5.4525397 C11.9386583,5.4525397 12.8862424,5.58339332 13.7612648,5.8260798 C15.8574425,4.36950256 16.7869988,4.66833465 16.7869988,4.66833465 C17.3886753,6.21818233 17.0055854,7.37615666 16.8962921,7.65619715 C17.6072619,8.44040221 18.0264073,9.44873132 18.0264073,10.6811845 C18.0264073,15.0135622 15.4565502,15.9657457 12.995761,16.2460154 C13.3968787,16.6007639 13.7430117,17.2729069 13.7430117,18.3373817 C13.7430117,19.8498753 13.7249839,21.0637661 13.7249839,21.437077 C13.7249839,21.7359091 13.9255427,22.0908868 14.4722345,21.9788248 C18.8466703,20.4846643 22,16.2645778 22,11.2788487 C22.0179786,5.04164559 17.0785978,0 11.009102,0 Z"
|
||||
id="Path"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SocialGithub;
|
||||
44
platform/ui-next/src/components/Modal/Modal.tsx
Normal file
44
platform/ui-next/src/components/Modal/Modal.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../Dialog';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
contentClassName?: string;
|
||||
shouldCloseOnEsc?: boolean;
|
||||
shouldCloseOnOverlayClick?: boolean;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
shouldCloseOnEsc = true,
|
||||
shouldCloseOnOverlayClick = true,
|
||||
containerClassName,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={open => !open && onClose()}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
>
|
||||
<DialogContent className={containerClassName}>
|
||||
{title && (
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
)}
|
||||
<div className={cn('mt-2')}>{children}</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
258
platform/ui-next/src/components/OHIFDialogs/InputDialog.tsx
Normal file
258
platform/ui-next/src/components/OHIFDialogs/InputDialog.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
import React, { createContext, useContext, useRef, useEffect } from 'react';
|
||||
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||
import { Label, Input as InputComponent, FooterAction } from '../../components';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface InputDialogContextValue {
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
submitOnEnter?: boolean;
|
||||
}
|
||||
|
||||
const InputDialogContext = createContext<InputDialogContextValue | null>(null);
|
||||
|
||||
export type InputDialogRootProps = {
|
||||
/** The controlled value of the input */
|
||||
value?: string;
|
||||
/** The default value for uncontrolled usage */
|
||||
defaultValue?: string;
|
||||
/** Callback when input value changes */
|
||||
onChange?: (value: string) => void;
|
||||
/** Optional className for the root container */
|
||||
className?: string;
|
||||
/** Enable save on Enter key press */
|
||||
submitOnEnter?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const InputDialogRoot = React.forwardRef<HTMLDivElement, InputDialogRootProps>(
|
||||
({ value, defaultValue = '', onChange, className, submitOnEnter, children }, ref) => {
|
||||
const [internalValue, setInternalValue] = useControllableState({
|
||||
prop: value,
|
||||
defaultProp: defaultValue,
|
||||
onChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<InputDialogContext.Provider
|
||||
value={{
|
||||
value: internalValue,
|
||||
setValue: setInternalValue,
|
||||
submitOnEnter,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</InputDialogContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputDialogRoot.displayName = 'InputDialog';
|
||||
|
||||
export interface InputDialogFieldProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Optional className for the field container */
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Field = React.forwardRef<HTMLDivElement, InputDialogFieldProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('mb-4 flex flex-col space-y-2', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Field.displayName = 'InputDialog.Field';
|
||||
|
||||
export interface InputDialogInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
/** ID for the input field */
|
||||
id?: string;
|
||||
/** Optional className for the input container */
|
||||
className?: string;
|
||||
/** Save handler */
|
||||
onSave?: (value: string) => void;
|
||||
/** Placeholder text for the input field */
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const InputDialogInput = React.forwardRef<HTMLInputElement, InputDialogInputProps>(
|
||||
({ id = 'dialog-input', className, onSave, ...props }, ref) => {
|
||||
const context = useContext(InputDialogContext);
|
||||
if (!context) {
|
||||
throw new Error('InputDialog.Input must be used within an InputDialog');
|
||||
}
|
||||
|
||||
const { value, setValue } = context;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Combine the forwarded ref with our local ref
|
||||
React.useImperativeHandle(ref, () => inputRef.current);
|
||||
|
||||
// Focus the input when it mounts
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (context.submitOnEnter && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const saveButton = document.querySelector(
|
||||
'[data-cy="input-dialog-save-button"]'
|
||||
) as HTMLButtonElement;
|
||||
if (saveButton) {
|
||||
saveButton.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
<InputComponent
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputDialogInput.displayName = 'InputDialog.Input';
|
||||
|
||||
export interface InputDialogLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
||||
/** Optional className for the label */
|
||||
className?: string;
|
||||
/** For attribute to match input ID */
|
||||
htmlFor?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const InputDialogLabel = React.forwardRef<HTMLLabelElement, InputDialogLabelProps>(
|
||||
({ className, htmlFor = 'dialog-input', children, ...props }, ref) => {
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(className)}
|
||||
htmlFor={htmlFor}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputDialogLabel.displayName = 'InputDialog.Label';
|
||||
|
||||
export interface InputDialogActionsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Optional className for the actions container */
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Actions = React.forwardRef<HTMLDivElement, InputDialogActionsProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<FooterAction className={cn(className)}>
|
||||
<FooterAction.Right>{children}</FooterAction.Right>
|
||||
</FooterAction>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Actions.displayName = 'InputDialog.Actions';
|
||||
|
||||
export interface InputDialogActionButtonProps {
|
||||
/** Optional className for the button */
|
||||
className?: string;
|
||||
/** Click handler that receives the current input value */
|
||||
onClick: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ActionsSecondary = React.forwardRef<HTMLDivElement, InputDialogActionButtonProps>(
|
||||
({ className, onClick, children, ...props }, ref) => {
|
||||
const context = useContext(InputDialogContext);
|
||||
if (!context) {
|
||||
throw new Error('InputDialog.ActionsSecondary must be used within an InputDialog');
|
||||
}
|
||||
|
||||
const { value } = context;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<FooterAction.Secondary
|
||||
onClick={() => onClick(value)}
|
||||
className={cn(className)}
|
||||
>
|
||||
{children}
|
||||
</FooterAction.Secondary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ActionsSecondary.displayName = 'InputDialog.ActionsSecondary';
|
||||
|
||||
const ActionsPrimary = React.forwardRef<HTMLDivElement, InputDialogActionButtonProps>(
|
||||
({ className, onClick, children, ...props }, ref) => {
|
||||
const context = useContext(InputDialogContext);
|
||||
if (!context) {
|
||||
throw new Error('InputDialog.ActionsPrimary must be used within an InputDialog');
|
||||
}
|
||||
|
||||
const { value } = context;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
data-cy="input-dialog-save-button"
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
<FooterAction.Primary
|
||||
onClick={() => onClick(value)}
|
||||
className={cn(className)}
|
||||
>
|
||||
{children}
|
||||
</FooterAction.Primary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ActionsPrimary.displayName = 'InputDialog.ActionsPrimary';
|
||||
|
||||
export const InputDialog = Object.assign(InputDialogRoot, {
|
||||
Label: InputDialogLabel,
|
||||
Input: InputDialogInput,
|
||||
Field,
|
||||
Actions,
|
||||
ActionsSecondary,
|
||||
ActionsPrimary,
|
||||
});
|
||||
106
platform/ui-next/src/components/OHIFDialogs/PresetDialog.tsx
Normal file
106
platform/ui-next/src/components/OHIFDialogs/PresetDialog.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import * as React from 'react';
|
||||
import { Input } from '../Input';
|
||||
import { ScrollArea } from '../ScrollArea';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface PresetDialogProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PresetDialog({ children, className }: PresetDialogProps) {
|
||||
return <div className={cn('flex h-[500px] max-w-lg flex-col', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent: PresetBody
|
||||
* A dark "box" container that wraps the filter and grid area.
|
||||
* Adjust bg color, padding, border, etc. to match your design.
|
||||
*/
|
||||
interface PresetBodyProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function PresetBody({ children, className }: PresetBodyProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// Adjust these classes for your desired look
|
||||
'flex min-h-0 flex-1 flex-col rounded-md border border-white/10 bg-black p-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent: PresetFilter
|
||||
*/
|
||||
interface PresetFilterProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function PresetFilter({ children, className }: PresetFilterProps) {
|
||||
return (
|
||||
<div className={cn('mb-2 flex w-full flex-shrink-0 items-center space-x-2', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent: PresetSearch
|
||||
*/
|
||||
interface PresetSearchProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
className?: string;
|
||||
}
|
||||
function PresetSearch({ className, ...props }: PresetSearchProps) {
|
||||
return (
|
||||
<Input
|
||||
className={cn('w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent: PresetGrid
|
||||
*/
|
||||
interface PresetGridProps {
|
||||
children: React.ReactNode;
|
||||
maxHeight?: string;
|
||||
className?: string;
|
||||
}
|
||||
function PresetGrid({ children, maxHeight = 'flex-1', className }: PresetGridProps) {
|
||||
return (
|
||||
<ScrollArea className={cn('min-h-0 flex-1', className)}>
|
||||
<div className="grid grid-cols-4 gap-2 pr-2">{children}</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent: PresetOption
|
||||
*/
|
||||
interface PresetOptionProps {
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
function PresetOption({ label = 'Label', className }: PresetOptionProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-start space-y-1', className)}>
|
||||
{/* Default dark placeholder box (swap in an <img> if you like) */}
|
||||
<div className="bg-popover h-16 w-24 rounded" />
|
||||
<div className="text-muted-foreground text-left text-base">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Attach subcomponents to PresetDialog as static properties */
|
||||
PresetDialog.PresetBody = PresetBody;
|
||||
PresetDialog.PresetFilter = PresetFilter;
|
||||
PresetDialog.PresetSearch = PresetSearch;
|
||||
PresetDialog.PresetGrid = PresetGrid;
|
||||
PresetDialog.PresetOption = PresetOption;
|
||||
2
platform/ui-next/src/components/OHIFDialogs/index.ts
Normal file
2
platform/ui-next/src/components/OHIFDialogs/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { InputDialog } from './InputDialog';
|
||||
export { PresetDialog } from './PresetDialog';
|
||||
114
platform/ui-next/src/components/OHIFModals/AboutModal.tsx
Normal file
114
platform/ui-next/src/components/OHIFModals/AboutModal.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../Button';
|
||||
import { Icons } from '../Icons';
|
||||
|
||||
interface AboutModalProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AboutModal({ children, className }: AboutModalProps) {
|
||||
return <div className={cn('space-y-1 text-center', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
/** Sub component: Product Name */
|
||||
interface ProductNameProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function ProductName({ children, className }: ProductNameProps) {
|
||||
return (
|
||||
<div className={cn('text-foreground pt-3 text-2xl font-medium leading-none', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sub-component: Product Version */
|
||||
interface ProductVersionProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function ProductVersion({ children, className }: ProductVersionProps) {
|
||||
return (
|
||||
<div className={cn('text-muted-foreground text-2xl font-light leading-none', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sub-component: Product Beta */
|
||||
interface ProductBetaProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function ProductBeta({ children, className }: ProductBetaProps) {
|
||||
return (
|
||||
<div className={cn('text-muted-foreground text-xl font-light', className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sub-component: Body (wraps all detail items) */
|
||||
interface BodyProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function Body({ children, className }: BodyProps) {
|
||||
return (
|
||||
<div className={cn('my-3 flex flex-col items-center space-y-0', className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sub-component: Detail Item */
|
||||
interface DetailItemProps {
|
||||
label: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
function DetailItem({ label, value, className }: DetailItemProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center', className)}>
|
||||
<div className="text-muted-foreground pt-2 text-sm font-semibold tracking-wide">{label}</div>
|
||||
<div className="text-muted-foreground text-sm">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sub-component: Social Item */
|
||||
interface SocialItemProps {
|
||||
icon: string;
|
||||
url: string;
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
function SocialItem({ icon, url, text, className }: SocialItemProps) {
|
||||
return (
|
||||
<div className={cn('text-foreground flex items-center', className)}>
|
||||
<div className="inline-block">
|
||||
<Icons.ByName name={icon} />
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="py-6 text-lg"
|
||||
>
|
||||
<a
|
||||
href={`https://github.com/${url}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Attach sub-components to AboutModal as static properties */
|
||||
AboutModal.ProductName = ProductName;
|
||||
AboutModal.ProductVersion = ProductVersion;
|
||||
AboutModal.ProductBeta = ProductBeta;
|
||||
AboutModal.Body = Body;
|
||||
AboutModal.DetailItem = DetailItem;
|
||||
AboutModal.SocialItem = SocialItem;
|
||||
231
platform/ui-next/src/components/OHIFModals/ImageModal.tsx
Normal file
231
platform/ui-next/src/components/OHIFModals/ImageModal.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import * as React from 'react';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../Select/Select';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface ImageModalProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main ImageModal container. By default, we do not force any
|
||||
* layout here. We'll use a "Body" subcomponent for the main area
|
||||
* that sets up a flex row with a 70/30 split.
|
||||
*/
|
||||
export function ImageModal({ children, className }: ImageModalProps) {
|
||||
return <div className={cn(className)}>{children}</div>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Body subcomponent */
|
||||
|
||||
interface ImageBodyProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function ImageBody({ children, className }: ImageBodyProps) {
|
||||
return <div className={cn('flex flex-col sm:flex-row', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ImageVisual subcomponent */
|
||||
|
||||
interface ImageVisualProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
function ImageVisual({ children, className }: ImageVisualProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 items-center justify-center rounded-2xl bg-black/80 p-4 sm:flex-[7]',
|
||||
'flex', // ensure the container is a flex box
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="h-[512px] w-[512px] overflow-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ImageOptions subcomponent */
|
||||
|
||||
interface ImageOptionsProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function ImageOptions({ children, className }: ImageOptionsProps) {
|
||||
return <div className={cn('flex-1 space-y-5 p-4 sm:flex-[3]', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Filename subcomponent */
|
||||
|
||||
interface FilenameProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'children'> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
/** Handler is optional. If not provided, default to a no‐op. */
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
function Filename({ children, className, value, onChange, ...props }: FilenameProps) {
|
||||
return (
|
||||
<div className={cn('text-foreground space-y-1', className)}>
|
||||
<label className="block text-base">{children}</label>
|
||||
<Input
|
||||
{...props}
|
||||
className={cn('w-full', className)}
|
||||
value={value}
|
||||
onChange={onChange ?? (() => {})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Filetype subcomponent */
|
||||
|
||||
interface FiletypeProps {
|
||||
selected: string;
|
||||
/** Handler is optional. If not provided, we do nothing. */
|
||||
onSelect?: (val: string) => void;
|
||||
className?: string;
|
||||
/** Array of file type options */
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
}
|
||||
|
||||
function Filetype({ selected, onSelect, className, options = [] }: FiletypeProps) {
|
||||
const defaultOptions = [
|
||||
{ value: 'jpg', label: 'JPG' },
|
||||
{ value: 'png', label: 'PNG' },
|
||||
];
|
||||
|
||||
const fileTypeOptions = options.length ? options : defaultOptions;
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={selected}
|
||||
onValueChange={val => onSelect?.(val)}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label="File type"
|
||||
className={cn('w-[5.5rem] sm:w-24', className)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fileTypeOptions.map(option => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ImageSize subcomponent */
|
||||
|
||||
interface ImageSizeProps {
|
||||
children: React.ReactNode;
|
||||
width: string;
|
||||
height: string;
|
||||
/** Handlers optional. If not provided, default no‐op. */
|
||||
onWidthChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
onHeightChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
className?: string;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
}
|
||||
|
||||
function ImageSize({
|
||||
children,
|
||||
width,
|
||||
height,
|
||||
onWidthChange,
|
||||
onHeightChange,
|
||||
className,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
}: ImageSizeProps) {
|
||||
return (
|
||||
<div className={cn('text-foreground space-y-1', className)}>
|
||||
<label className="block text-base">{children}</label>
|
||||
|
||||
{/* Flex container for width/height inputs */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Width group */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-foreground text-base">W</span>
|
||||
<Input
|
||||
value={width}
|
||||
onChange={onWidthChange ?? (() => {})}
|
||||
placeholder="Width"
|
||||
className="w-20"
|
||||
max={maxWidth}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Height group */}
|
||||
<div className="text-foreground flex items-center space-x-2 text-base">
|
||||
<span className="text-foreground text-base">H</span>
|
||||
<Input
|
||||
value={height}
|
||||
onChange={onHeightChange ?? (() => {})}
|
||||
placeholder="Height"
|
||||
className="w-20"
|
||||
max={maxHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* SwitchOption subcomponent */
|
||||
|
||||
interface SwitchOptionProps {
|
||||
children: React.ReactNode;
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function SwitchOption({
|
||||
children,
|
||||
checked,
|
||||
defaultChecked,
|
||||
onCheckedChange,
|
||||
className,
|
||||
}: SwitchOptionProps) {
|
||||
return (
|
||||
<div className={cn('text-foreground flex items-center space-x-2', className)}>
|
||||
<Switch
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
onCheckedChange={val => onCheckedChange?.(val)}
|
||||
/>
|
||||
<span className="text-base">{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Attach subcomponents onto the main ImageModal function. */
|
||||
|
||||
ImageModal.Body = ImageBody;
|
||||
ImageModal.ImageVisual = ImageVisual;
|
||||
ImageModal.ImageOptions = ImageOptions;
|
||||
ImageModal.Filename = Filename;
|
||||
ImageModal.Filetype = Filetype;
|
||||
ImageModal.ImageSize = ImageSize;
|
||||
ImageModal.SwitchOption = SwitchOption;
|
||||
@ -0,0 +1,120 @@
|
||||
import * as React from 'react';
|
||||
import { Label } from '../Label';
|
||||
import { Input } from '../Input';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface UserPreferencesModalProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UserPreferencesModal({ children, className }: UserPreferencesModalProps) {
|
||||
return (
|
||||
<div className={cn('flex max-h-[80vh] w-full max-w-4xl flex-col overflow-hidden', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Body
|
||||
* Automatically wraps content in a scrollable area.
|
||||
*/
|
||||
interface BodyProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function Body({ children, className }: BodyProps) {
|
||||
return (
|
||||
<div className={cn('flex-1 overflow-y-auto', className)}>
|
||||
<div className={cn('mt-1 mb-4 flex flex-col space-y-4', className)}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Subheading
|
||||
* Section labels
|
||||
*/
|
||||
interface SubHeadingProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function SubHeading({ children, className }: SubHeadingProps) {
|
||||
return <span className={cn('text-muted-foreground text-lg', className)}>{children}</span>;
|
||||
}
|
||||
|
||||
/** Responsive 3-column grid for hotkeys, etc. */
|
||||
interface HotkeysGridProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function HotkeysGrid({ children, className }: HotkeysGridProps) {
|
||||
return (
|
||||
<div className={cn('grid grid-cols-1 gap-3 gap-x-16 md:grid-cols-2 lg:grid-cols-3', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** A single hotkey row: label + input */
|
||||
interface HotkeyProps {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
hotkeys?: {
|
||||
record: (callback: (sequence: string[]) => void) => void;
|
||||
pause: () => void;
|
||||
unpause: () => void;
|
||||
startRecording: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
function Hotkey({ label, placeholder, className, value, onChange, hotkeys }: HotkeyProps) {
|
||||
const [isRecording, setIsRecording] = React.useState(false);
|
||||
|
||||
const onInputKeyDown = (event: React.KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
hotkeys?.record((sequence: string[]) => {
|
||||
const keys = sequence.join('+');
|
||||
hotkeys?.unpause();
|
||||
setIsRecording(false);
|
||||
onChange?.(keys);
|
||||
});
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
setIsRecording(true);
|
||||
hotkeys?.pause();
|
||||
hotkeys?.startRecording();
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
setIsRecording(false);
|
||||
hotkeys?.unpause();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center justify-between space-x-2', className)}>
|
||||
<Label className="whitespace-nowrap">{label}</Label>
|
||||
<Input
|
||||
className={cn(
|
||||
'w-16 text-center transition-colors',
|
||||
isRecording && 'bg-accent text-accent-foreground caret-accent-foreground'
|
||||
)}
|
||||
placeholder={isRecording ? 'Press keys...' : placeholder}
|
||||
value={value}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
readOnly={!isRecording}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Attach subcomponents as static properties for a nicer API */
|
||||
UserPreferencesModal.Body = Body;
|
||||
UserPreferencesModal.HotkeysGrid = HotkeysGrid;
|
||||
UserPreferencesModal.Hotkey = Hotkey;
|
||||
UserPreferencesModal.SubHeading = SubHeading;
|
||||
3
platform/ui-next/src/components/OHIFModals/index.ts
Normal file
3
platform/ui-next/src/components/OHIFModals/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { UserPreferencesModal } from './UserPreferencesModal';
|
||||
export { ImageModal } from './ImageModal';
|
||||
export { AboutModal } from './AboutModal';
|
||||
@ -54,6 +54,10 @@ import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
|
||||
import { ToolboxUI, Toolbox } from './OHIFToolbox';
|
||||
import Numeric from './Numeric';
|
||||
import { InputDialog, PresetDialog } from './OHIFDialogs';
|
||||
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
|
||||
import Modal from './Modal/Modal';
|
||||
import { FooterAction } from './FooterAction';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -225,4 +229,11 @@ export {
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
InputDialog,
|
||||
PresetDialog,
|
||||
Modal,
|
||||
AboutModal,
|
||||
ImageModal,
|
||||
UserPreferencesModal,
|
||||
FooterAction,
|
||||
};
|
||||
|
||||
89
platform/ui-next/src/contextProviders/DialogProvider.tsx
Normal file
89
platform/ui-next/src/contextProviders/DialogProvider.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useState, createContext, useContext, useCallback, useEffect, useMemo } from 'react';
|
||||
import ManagedDialog, { ManagedDialogProps } from './ManagedDialog';
|
||||
|
||||
interface DialogContextType {
|
||||
show: (options: ManagedDialogProps) => string;
|
||||
hide: (id: string) => void;
|
||||
hideAll: () => void;
|
||||
isEmpty: () => boolean;
|
||||
}
|
||||
|
||||
interface DialogService {
|
||||
setServiceImplementation: (implementation: DialogContextType) => void;
|
||||
getCustomComponent?: () => React.ComponentType<ManagedDialogProps> | null;
|
||||
}
|
||||
|
||||
interface DialogProviderProps {
|
||||
children: React.ReactNode;
|
||||
dialog?: React.ComponentType<ManagedDialogProps>;
|
||||
service?: DialogService | null;
|
||||
}
|
||||
|
||||
const DialogContext = createContext<DialogContextType | null>(null);
|
||||
|
||||
export const useDialog = () => {
|
||||
const context = useContext(DialogContext);
|
||||
if (!context) {
|
||||
throw new Error('useDialog must be used within a DialogProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const DialogProvider: React.FC<DialogProviderProps> = ({
|
||||
children,
|
||||
dialog: DialogComponent = ManagedDialog,
|
||||
service = null,
|
||||
}) => {
|
||||
const [dialogs, setDialogs] = useState<(ManagedDialogProps & { id: string })[]>([]);
|
||||
|
||||
const show = useCallback((options: ManagedDialogProps) => {
|
||||
const id = options.id;
|
||||
setDialogs(prev => [...prev, { ...options, id }]);
|
||||
return id;
|
||||
}, []);
|
||||
|
||||
const hide = useCallback((id: string) => {
|
||||
setDialogs(prev => prev.filter(dialog => dialog.id !== id));
|
||||
}, []);
|
||||
|
||||
const hideAll = useCallback(() => {
|
||||
setDialogs([]);
|
||||
}, []);
|
||||
|
||||
const isEmpty = useCallback(() => dialogs.length === 0, [dialogs]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
show,
|
||||
hide,
|
||||
hideAll,
|
||||
isEmpty,
|
||||
}),
|
||||
[show, hide, hideAll, isEmpty]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation(contextValue);
|
||||
}
|
||||
}, [service, contextValue]);
|
||||
|
||||
const CustomDialog = service?.getCustomComponent?.();
|
||||
const RenderedDialog = CustomDialog || DialogComponent;
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={contextValue}>
|
||||
{dialogs.map(dialog => (
|
||||
<RenderedDialog
|
||||
key={dialog.id}
|
||||
onClose={hide}
|
||||
isOpen={true}
|
||||
{...dialog}
|
||||
/>
|
||||
))}
|
||||
{children}
|
||||
</DialogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { DialogProvider };
|
||||
76
platform/ui-next/src/contextProviders/ManagedDialog.tsx
Normal file
76
platform/ui-next/src/contextProviders/ManagedDialog.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../components/Dialog/Dialog';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
export interface ManagedDialogProps {
|
||||
id: string;
|
||||
isOpen?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
content: React.ComponentType<{ hide?: () => void }>;
|
||||
contentProps?: Record<string, unknown>;
|
||||
isDraggable?: boolean;
|
||||
shouldCloseOnEsc?: boolean;
|
||||
shouldCloseOnOverlayClick?: boolean;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
onClose?: (id: string) => void;
|
||||
unstyled?: boolean;
|
||||
showOverlay?: boolean;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const ManagedDialog: React.FC<ManagedDialogProps> = ({
|
||||
id,
|
||||
isOpen,
|
||||
title,
|
||||
content: DialogContentComponent,
|
||||
contentProps,
|
||||
isDraggable,
|
||||
shouldCloseOnEsc = false,
|
||||
shouldCloseOnOverlayClick = false,
|
||||
showOverlay = true,
|
||||
defaultPosition,
|
||||
onClose,
|
||||
unstyled,
|
||||
containerClassName,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
modal={false} // keep modal behavior off for independent windows
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
onClose(id);
|
||||
}
|
||||
}}
|
||||
isDraggable={isDraggable}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
showOverlay={showOverlay}
|
||||
>
|
||||
<DialogContent
|
||||
className={cn(unstyled ? 'p-0' : '', containerClassName)}
|
||||
style={{
|
||||
...(defaultPosition
|
||||
? {
|
||||
position: 'fixed',
|
||||
left: `${defaultPosition.x}px`,
|
||||
top: `${defaultPosition.y}px`,
|
||||
transform: 'translate(0, 0)',
|
||||
margin: 0,
|
||||
animation: 'none',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{!unstyled && <DialogHeader>{title && <DialogTitle>{title}</DialogTitle>}</DialogHeader>}
|
||||
<DialogContentComponent
|
||||
{...contentProps}
|
||||
hide={() => onClose(id)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagedDialog;
|
||||
102
platform/ui-next/src/contextProviders/ModalProvider.tsx
Normal file
102
platform/ui-next/src/contextProviders/ModalProvider.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React, { useState, createContext, useContext, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ModalOptions {
|
||||
title?: string;
|
||||
shouldCloseOnEsc?: boolean;
|
||||
content?: React.ComponentType;
|
||||
contentProps?: Record<string, unknown>;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
interface ModalContextType {
|
||||
show: (options: Partial<ModalOptions>) => void;
|
||||
hide: () => void;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
interface ModalComponentProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ModalContext = createContext<ModalContextType | null>(null);
|
||||
|
||||
export const useModal = () => {
|
||||
const ctx = useContext(ModalContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useModal must be used within a ModalProvider');
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS: ModalOptions = {
|
||||
title: '',
|
||||
shouldCloseOnEsc: true,
|
||||
};
|
||||
|
||||
interface ModalService {
|
||||
setServiceImplementation: (implementation: ModalContextType) => void;
|
||||
getCustomComponent: () => React.ComponentType<ModalComponentProps> | null;
|
||||
}
|
||||
|
||||
interface ModalProviderProps {
|
||||
children: React.ReactNode;
|
||||
modal: React.ComponentType<ModalComponentProps>;
|
||||
service?: ModalService | null;
|
||||
}
|
||||
|
||||
const ModalProvider: React.FC<ModalProviderProps> = ({
|
||||
children,
|
||||
modal: ModalComponent,
|
||||
service = null,
|
||||
}) => {
|
||||
const { t } = useTranslation('Modals');
|
||||
const [options, setOptions] = useState<ModalOptions>(DEFAULT_OPTIONS);
|
||||
|
||||
const ModalContent = options.content;
|
||||
|
||||
const show = useCallback((props: Partial<ModalOptions>) => {
|
||||
setOptions(prev => ({ ...prev, ...props }));
|
||||
}, []);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
setOptions(DEFAULT_OPTIONS);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({ show, hide });
|
||||
}
|
||||
}, [hide, service, show]);
|
||||
|
||||
const { title } = options;
|
||||
|
||||
const CustomModal = service?.getCustomComponent();
|
||||
const RenderedModal = CustomModal || ModalComponent;
|
||||
|
||||
return (
|
||||
<ModalContext.Provider value={{ show, hide }}>
|
||||
{ModalContent && (
|
||||
<RenderedModal
|
||||
isOpen={true}
|
||||
onClose={hide}
|
||||
title={t(title)}
|
||||
{...options}
|
||||
>
|
||||
<ModalContent
|
||||
{...options.contentProps}
|
||||
show={show}
|
||||
hide={hide}
|
||||
/>
|
||||
</RenderedModal>
|
||||
)}
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { ModalProvider };
|
||||
export const ModalConsumer = ModalContext.Consumer;
|
||||
@ -1,7 +1,13 @@
|
||||
import NotificationProvider, { useNotification } from './NotificationProvider';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
|
||||
import { ToolboxProvider, useToolbox } from './ToolboxContext';
|
||||
import { ModalProvider, useModal } from './ModalProvider';
|
||||
import { DialogProvider, useDialog } from './DialogProvider';
|
||||
import ManagedDialog from './ManagedDialog';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
export { ToolboxProvider, useToolbox };
|
||||
export { ModalProvider, useModal };
|
||||
export { DialogProvider, useDialog };
|
||||
export { ManagedDialog };
|
||||
|
||||
@ -100,6 +100,13 @@ import {
|
||||
ToolButtonListDivider,
|
||||
Toolbox,
|
||||
Numeric,
|
||||
InputDialog,
|
||||
PresetDialog,
|
||||
Modal,
|
||||
AboutModal,
|
||||
ImageModal,
|
||||
UserPreferencesModal,
|
||||
FooterAction,
|
||||
} from './components';
|
||||
import { DataRow } from './components/DataRow';
|
||||
|
||||
@ -108,6 +115,11 @@ import {
|
||||
NotificationProvider,
|
||||
useToolbox,
|
||||
ToolboxProvider,
|
||||
useModal,
|
||||
ModalProvider,
|
||||
DialogProvider,
|
||||
useDialog,
|
||||
ManagedDialog,
|
||||
} from './contextProviders';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
|
||||
import * as utils from './utils';
|
||||
@ -225,4 +237,16 @@ export {
|
||||
useToolbox,
|
||||
utils,
|
||||
Numeric,
|
||||
AboutModal,
|
||||
ImageModal,
|
||||
UserPreferencesModal,
|
||||
InputDialog,
|
||||
PresetDialog,
|
||||
Modal,
|
||||
useModal,
|
||||
ModalProvider,
|
||||
FooterAction,
|
||||
DialogProvider,
|
||||
useDialog,
|
||||
ManagedDialog,
|
||||
};
|
||||
|
||||
@ -37,6 +37,7 @@ class LabellingFlow extends Component<PropType> {
|
||||
this.state = {
|
||||
location,
|
||||
label,
|
||||
hide: props.hide,
|
||||
componentClassName: className,
|
||||
confirmationState: false,
|
||||
displayComponent: true,
|
||||
@ -56,33 +57,19 @@ class LabellingFlow extends Component<PropType> {
|
||||
displayComponent={this.state.displayComponent}
|
||||
onTransitionExit={this.props.labellingDoneCallback}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={className}
|
||||
ref={this.mainElement}
|
||||
>
|
||||
{this.labellingStateFragment()}
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={className}
|
||||
ref={this.mainElement}
|
||||
>
|
||||
{this.labellingStateFragment()}
|
||||
</div>
|
||||
</LabellingTransition>
|
||||
);
|
||||
}
|
||||
|
||||
closePopup = () => {
|
||||
this.setState({
|
||||
displayComponent: false,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
displayComponent: false,
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
selectTreeSelectCalback = (event, itemSelected) => {
|
||||
const label = itemSelected.value;
|
||||
this.closePopup();
|
||||
this.props.hide();
|
||||
return this.props.labellingDoneCallback(label);
|
||||
};
|
||||
|
||||
@ -92,7 +79,7 @@ class LabellingFlow extends Component<PropType> {
|
||||
items={this.currentItems}
|
||||
columns={1}
|
||||
onSelected={this.selectTreeSelectCalback}
|
||||
closePopup={this.closePopup}
|
||||
closePopup={this.props.hide}
|
||||
selectTreeFirstTitle="Annotation"
|
||||
exclusive={this.props.exclusive}
|
||||
label={this.state.label}
|
||||
|
||||
@ -22,7 +22,7 @@ const Modal = ({
|
||||
onClose,
|
||||
children,
|
||||
shouldCloseOnOverlayClick = true,
|
||||
movable = false,
|
||||
isDraggable = false,
|
||||
containerDimensions = null,
|
||||
contentDimensions = null,
|
||||
}) => {
|
||||
@ -75,7 +75,7 @@ const Modal = ({
|
||||
: 'relative max-h-full w-11/12 text-white outline-none lg:w-10/12 xl:w-9/12'
|
||||
}
|
||||
overlayClassName={
|
||||
movable
|
||||
isDraggable
|
||||
? 'fixed top-0 left-0 right-0 bottom-0 z-50 flex items-center justify-center py-16 pointer-events-none'
|
||||
: 'fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-center justify-center py-16'
|
||||
}
|
||||
@ -85,7 +85,7 @@ const Modal = ({
|
||||
title={title}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
>
|
||||
{movable ? (
|
||||
{isDraggable ? (
|
||||
<Draggable
|
||||
handle=".drag-handle"
|
||||
defaultClassName="bg-primary-dark pointer-events-auto"
|
||||
@ -107,7 +107,7 @@ Modal.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
||||
shouldCloseOnOverlayClick: PropTypes.bool,
|
||||
movable: PropTypes.bool,
|
||||
isDraggable: PropTypes.bool,
|
||||
containerDimensions: PropTypes.string,
|
||||
contentDimensions: PropTypes.string,
|
||||
};
|
||||
|
||||
@ -168,20 +168,9 @@ export class SelectTree extends Component<PropType> {
|
||||
|
||||
headerItem = () => {
|
||||
const inputLeftPadding = this.props.items.length > 0 ? 'pl-8' : 'pl-4';
|
||||
const title = this.props.selectTreeFirstTitle;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between border-b-2 border-solid border-black p-4">
|
||||
<div className="text-primary-active m-0 mb-5 p-2 leading-tight">
|
||||
<span className="text-primary-light align-sub text-xl">{title}</span>
|
||||
<div className="float-right">
|
||||
<Icons.Close
|
||||
className="cursor-pointer"
|
||||
onClick={() => this.props.closePopup()}
|
||||
fill="#a3a3a3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between border-b-2 border-solid border-black p-0.5">
|
||||
{this.props.searchEnabled && (
|
||||
<div className="flex w-full flex-col">
|
||||
{this.props.items.length > 0 && (
|
||||
|
||||
@ -31,7 +31,7 @@ const ModalProvider = ({ children, modal: Modal, service = null }) => {
|
||||
closeButton: true,
|
||||
title: null,
|
||||
customClassName: '',
|
||||
movable: false,
|
||||
isDraggable: false,
|
||||
containerDimensions: null,
|
||||
contentDimensions: null,
|
||||
};
|
||||
@ -78,7 +78,7 @@ const ModalProvider = ({ children, modal: Modal, service = null }) => {
|
||||
shouldCloseOnEsc,
|
||||
closeButton,
|
||||
shouldCloseOnOverlayClick,
|
||||
movable,
|
||||
isDraggable,
|
||||
containerDimensions,
|
||||
contentDimensions,
|
||||
} = options;
|
||||
@ -96,7 +96,7 @@ const ModalProvider = ({ children, modal: Modal, service = null }) => {
|
||||
closeButton={closeButton}
|
||||
onClose={hide}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
movable={movable}
|
||||
isDraggable={isDraggable}
|
||||
containerDimensions={containerDimensions}
|
||||
contentDimensions={contentDimensions}
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user