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

@@ -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',
}),
},
];
+1 -2
View File
@@ -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,
},
+6 -10
View File
@@ -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,
};
+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;