ui(components): New Dialog and Modal components for ui-next (#4772)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukasandsedghi authored and GitHub committed 2025-02-28 15:17:55 -05:00
1 parent 62b4fa3e89
commit a3b51aa8db
90 files changed
+3459 -1357

No files matched your search

+77 -128
View File
@@ -1,160 +1,107 @@
import React from 'react';
import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
import { LabellingFlow } from '@ohif/ui';
import { InputDialog } from '@ohif/ui-next';
interface InputDialogDefaultProps {
hide: () => void;
onSave: (value: string) => void;
placeholder: string;
defaultValue: string;
submitOnEnter: boolean;
}
function InputDialogDefault({
hide,
onSave,
placeholder = 'Enter value',
defaultValue = '',
submitOnEnter,
}: InputDialogDefaultProps) {
return (
<InputDialog
submitOnEnter={submitOnEnter}
defaultValue={defaultValue}
>
<InputDialog.Field>
<InputDialog.Input placeholder={placeholder} />
</InputDialog.Field>
<InputDialog.Actions>
<InputDialog.ActionsSecondary onClick={hide}>Cancel</InputDialog.ActionsSecondary>
<InputDialog.ActionsPrimary
onClick={value => {
onSave(value);
hide();
}}
>
Save
</InputDialog.ActionsPrimary>
</InputDialog.Actions>
</InputDialog>
);
}
/**
*
* @param {*} data
* @param {*} data.text
* @param {*} data.label
* @param {*} event
* @param {*} callback
* @param {*} isArrowAnnotateInputDialog
* @param {*} dialogConfig
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
* @param {string?} dialogConfig.inputLabel - show label above the input
* Shows an input dialog for entering text with customizable options
* @param uiDialogService - Service for showing UI dialogs
* @param onSave - Callback function called when save button is clicked with entered value
* @param defaultValue - Initial value to show in input field
* @param title - Title text to show in dialog header
* @param placeholder - Placeholder text for input field
* @param submitOnEnter - Whether to submit dialog when Enter key is pressed
*/
export function callInputDialog(
export async function callInputDialog({
uiDialogService,
data,
callback,
isArrowAnnotateInputDialog = true,
dialogConfig: any = {}
) {
defaultValue = '',
title = 'Annotation',
placeholder = '',
submitOnEnter = true,
}: {
uiDialogService: AppTypes.UIDialogService;
}) {
const dialogId = 'dialog-enter-annotation';
const label = data ? (isArrowAnnotateInputDialog ? data.text : data.label) : '';
const {
dialogTitle = 'Annotation',
inputLabel = 'Enter your annotation',
validateFunc = value => true,
} = dialogConfig;
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
if (typeof validateFunc === 'function' && !validateFunc(value.label)) {
return;
}
callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
const value = await new Promise<string>(resolve => {
uiDialogService.show({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
content: InputDialogDefault,
title: title,
shouldCloseOnEsc: true,
contentProps: {
title: dialogTitle,
value: { label },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
return (
<Input
autoFocus
className="border-primary-main bg-black"
type="text"
id="annotation"
label={inputLabel}
labelClassName="text-white text-[14px] leading-[1.2]"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
onSave: value => {
resolve(value);
},
placeholder,
defaultValue,
submitOnEnter,
},
});
}
}
export function callLabelAutocompleteDialog(
uiDialogService,
callback,
dialogConfig,
labelConfig,
renderContent = LabellingFlow
) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
const { validateFunc = value => true } = dialogConfig;
const labellingDoneCallback = value => {
if (typeof value === 'string') {
if (typeof validateFunc === 'function' && !validateFunc(value)) {
return;
}
callback(value, 'save');
} else {
callback('', 'cancel');
}
uiDialogService.dismiss({ id: 'select-annotation' });
};
uiDialogService.create({
id: 'select-annotation',
centralize: true,
isDraggable: false,
showOverlay: true,
content: renderContent,
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: { label: '' },
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
});
return value;
}
export function showLabelAnnotationPopup(
export async function callInputDialogAutoComplete({
measurement,
uiDialogService,
labelConfig,
renderContent = LabellingFlow
) {
renderContent = LabellingFlow,
}) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
return new Promise<Map<any, any>>((resolve, reject) => {
const value = await new Promise<Map<string, string>>((resolve, reject) => {
const labellingDoneCallback = value => {
uiDialogService.dismiss({ id: 'select-annotation' });
uiDialogService.hide('select-annotation');
if (typeof value === 'string') {
measurement.label = value;
}
resolve(measurement);
};
uiDialogService.create({
uiDialogService.show({
id: 'select-annotation',
isDraggable: false,
showOverlay: true,
title: 'Annotation',
content: renderContent,
defaultPosition: {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
},
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: measurement,
@@ -164,6 +111,8 @@ export function showLabelAnnotationPopup(
},
});
});
return value;
}
export default callInputDialog;
@@ -1,58 +1,39 @@
import React from 'react';
import { Dialog } from '@ohif/ui';
import React, { useState } from 'react';
import { ChromePicker } from 'react-color';
import { FooterAction } from '@ohif/ui-next';
import './colorPickerDialog.css';
function colorPickerDialog(uiDialogService, rgbaColor, callback) {
const dialogId = 'pick-color';
function ColorPickerDialog({ value, hide, onSave }) {
const [color, setColor] = useState(value);
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.rgbaColor, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
const handleChange = color => {
setColor(color.rgb);
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment Color',
value: { rgbaColor },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
const handleChange = color => {
setValue({ rgbaColor: color.rgb });
};
return (
<ChromePicker
color={value.rgbaColor}
onChange={handleChange}
presetColors={[]}
width={300}
/>
);
},
},
});
}
return (
<div>
<ChromePicker
color={color}
onChange={handleChange}
presetColors={[]}
width={300}
/>
<FooterAction>
<FooterAction.Right>
<FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
<FooterAction.Primary
onClick={() => {
hide();
onSave(color);
}}
>
Save
</FooterAction.Primary>
</FooterAction.Right>
</FooterAction>
</div>
);
}
export default colorPickerDialog;
export default ColorPickerDialog;
@@ -1,42 +1,46 @@
import { showLabelAnnotationPopup } from './callInputDialog';
import { callInputDialogAutoComplete } from './callInputDialog';
function promptLabelAnnotation({ servicesManager }, ctx, evt) {
const { measurementService, customizationService, toolGroupService } = servicesManager.services;
const { measurementService, customizationService, toolGroupService, uiDialogService } =
servicesManager.services;
const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId, toolName } = evt;
return new Promise(async function (resolve) {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const activeToolOptions = toolGroup.getToolConfiguration(toolName);
if(activeToolOptions.getTextCallback) {
resolve({
StudyInstanceUID,
SeriesInstanceUID,
viewportId,
})
} else {
const labelConfig = customizationService.getCustomization('measurementLabels');
const measurement = measurementService.getMeasurement(measurementId);
const renderContent = customizationService.getCustomization('ui.labellingComponent');
const value = await showLabelAnnotationPopup(
measurement,
servicesManager.services.uiDialogService,
labelConfig,
renderContent
);
return new Promise(resolve => {
(async () => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const activeToolOptions = toolGroup.getToolConfiguration(toolName);
if (activeToolOptions.getTextCallback) {
resolve({
StudyInstanceUID,
SeriesInstanceUID,
viewportId,
});
} else {
const labelConfig = customizationService.getCustomization('measurementLabels');
const measurement = measurementService.getMeasurement(measurementId);
const renderContent = customizationService.getCustomization('ui.labellingComponent');
measurementService.update(
measurementId,
{
...value,
},
true
);
const value = await callInputDialogAutoComplete({
measurement,
uiDialogService,
labelConfig,
renderContent,
});
resolve({
StudyInstanceUID,
SeriesInstanceUID,
viewportId,
});
}
measurementService.update(
measurementId,
{
...value,
},
true
);
resolve({
StudyInstanceUID,
SeriesInstanceUID,
viewportId,
});
}
})();
});
}
@@ -1,75 +0,0 @@
import createReportAsync from '../Actions/createReportAsync';
import { createReportDialogPrompt } from '../Panels';
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
const { uiDialogService, measurementService, displaySetService } = servicesManager.services;
const viewportId = evt.viewportId === undefined ? evt.data.viewportId : evt.viewportId;
const isBackupSave = evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
const StudyInstanceUID = evt?.data?.StudyInstanceUID;
const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
const { trackedStudy, trackedSeries } = ctx;
let displaySetInstanceUIDs;
try {
const promptResult = await createReportDialogPrompt(uiDialogService, {
extensionManager,
});
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const measurements = measurementService.getMeasurements();
const trackedMeasurements = measurements
.filter(
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
)
.filter(m => m.referencedImageId != null);
const SeriesDescription =
// isUndefinedOrEmpty
promptResult.value === undefined || promptResult.value === ''
? 'Research Derived Series' // default
: promptResult.value; // provided value
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
const getReport = async () => {
return commandsManager.runCommand(
'storeMeasurements',
{
measurementData: trackedMeasurements,
dataSource,
additionalFindingTypes: ['ArrowAnnotate'],
options: {
SeriesDescription,
SeriesNumber,
},
},
'CORNERSTONE_STRUCTURED_REPORT'
);
};
displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport,
});
} else if (promptResult.action === RESPONSE.CANCEL) {
// Do nothing
}
return {
userResponse: promptResult.action,
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
StudyInstanceUID,
SeriesInstanceUID,
viewportId,
isBackupSave,
};
} catch (error) {
return null;
}
}
export default promptSaveReport;
@@ -0,0 +1,109 @@
import createReportAsync from '../Actions/createReportAsync';
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
import createReportDialogPrompt from '../Panels/createReportDialogPrompt';
/**
* Prompts the user to save a report and handles the report creation process
* @param services - Object containing required services and managers
* @param ctx - The current context containing tracked study and series information
* @param evt - The event object containing viewport and save-related data
*/
async function promptSaveReport(services, ctx, evt) {
const { servicesManager, extensionManager, commandsManager } = services;
const { measurementService, displaySetService } = servicesManager.services;
const viewportId = evt.viewportId ?? evt.data?.viewportId;
const isBackupSave = evt.isBackupSave ?? evt.data?.isBackupSave;
const { StudyInstanceUID, SeriesInstanceUID } = evt?.data ?? {};
const { trackedStudy, trackedSeries } = ctx;
const dataSources = extensionManager.getDataSources();
const {
value: reportName,
dataSourceName: dataSource,
action,
} = await createReportDialogPrompt({
servicesManager,
extensionManager,
});
let displaySetInstanceUIDs;
try {
if (action === PROMPT_RESPONSES.CREATE_REPORT) {
const selectedDataSource = dataSource ?? dataSources[0];
const trackedMeasurements = getTrackedMeasurements(
measurementService,
trackedStudy,
trackedSeries
);
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
displaySetInstanceUIDs = await handleReportCreation({
servicesManager,
commandsManager,
trackedMeasurements,
selectedDataSource,
reportName,
SeriesNumber,
});
}
return {
userResponse: action,
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
StudyInstanceUID,
SeriesInstanceUID,
viewportId,
isBackupSave,
};
} catch (error) {
return null;
}
}
/**
* Gets tracked measurements based on study and series criteria
*/
function getTrackedMeasurements(measurementService, trackedStudy, trackedSeries) {
return measurementService
.getMeasurements()
.filter(
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
)
.filter(m => m.referencedImageId != null);
}
/**
* Handles the creation of the report using the measurement service
*/
async function handleReportCreation({
servicesManager,
commandsManager,
trackedMeasurements,
selectedDataSource,
reportName,
SeriesNumber,
}) {
return createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand(
'storeMeasurements',
{
measurementData: trackedMeasurements,
dataSource: selectedDataSource,
additionalFindingTypes: ['ArrowAnnotate'],
options: {
SeriesDescription: reportName,
SeriesNumber,
},
},
'CORNERSTONE_STRUCTURED_REPORT'
),
});
}
export default promptSaveReport;