ui(components): New Dialog and Modal components for ui-next (#4772)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
1 parent
62b4fa3e89
commit
a3b51aa8db
90 files changed
+3459
-1357
No files matched your search
@@ -1,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;
|
||||
Reference in new issue
Block a user