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

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

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
+1
View File
@@ -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',
+5 -5
View File
@@ -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 */
}