docs: improved migration guides for 3p10 (#5110)
This commit is contained in:
parent
635877f184
commit
b194f672c0
@ -1,92 +1,94 @@
|
|||||||
/**
|
// Todo: fix this test does not work
|
||||||
* Add tests to ensure image consistency and quality
|
|
||||||
*/
|
|
||||||
|
|
||||||
const testPixel = (dx, dy, expectedPixel) => {
|
// /**
|
||||||
cy.get('.cornerstone-canvas').then(v => {
|
// * Add tests to ensure image consistency and quality
|
||||||
const canvas = v[0];
|
// */
|
||||||
cy.log(
|
|
||||||
'testPixel canvas',
|
|
||||||
dx,
|
|
||||||
dy,
|
|
||||||
expectedPixel,
|
|
||||||
canvas.width,
|
|
||||||
canvas.height,
|
|
||||||
canvas.style.width,
|
|
||||||
canvas.style.height
|
|
||||||
);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
cy.window()
|
|
||||||
.its('cornerstone')
|
|
||||||
.then(cornerstone => {
|
|
||||||
const { viewport } = cornerstone.getEnabledElements()[0];
|
|
||||||
const imageData = viewport.getImageData();
|
|
||||||
// cy.log("imageData", imageData);
|
|
||||||
const origin = viewport.worldToCanvas(imageData?.origin);
|
|
||||||
const orX = origin[0] * devicePixelRatio;
|
|
||||||
const orY = origin[1] * devicePixelRatio;
|
|
||||||
const x = Math.round(orX + dx);
|
|
||||||
const y = Math.round(orY + dy);
|
|
||||||
cy.log('testPixel origin x,y point x,y', orX, orY, x, y);
|
|
||||||
// cy.log('world origin', imageData.origin);
|
|
||||||
// cy.log('focal', viewport.getCamera().focalPoint,
|
|
||||||
// viewport.worldToCanvas(viewport.getCamera().focalPoint));
|
|
||||||
const pixelData = ctx.getImageData(x, y, 1, 1);
|
|
||||||
|
|
||||||
expect(pixelData.data[0]).closeTo(expectedPixel, 1);
|
// const testPixel = (dx, dy, expectedPixel) => {
|
||||||
});
|
// cy.get('.cornerstone-canvas').then(v => {
|
||||||
});
|
// const canvas = v[0];
|
||||||
};
|
// cy.log(
|
||||||
|
// 'testPixel canvas',
|
||||||
|
// dx,
|
||||||
|
// dy,
|
||||||
|
// expectedPixel,
|
||||||
|
// canvas.width,
|
||||||
|
// canvas.height,
|
||||||
|
// canvas.style.width,
|
||||||
|
// canvas.style.height
|
||||||
|
// );
|
||||||
|
// const ctx = canvas.getContext('2d');
|
||||||
|
// cy.window()
|
||||||
|
// .its('cornerstone')
|
||||||
|
// .then(cornerstone => {
|
||||||
|
// const { viewport } = cornerstone.getEnabledElements()[0];
|
||||||
|
// const imageData = viewport.getImageData();
|
||||||
|
// // cy.log("imageData", imageData);
|
||||||
|
// const origin = viewport.worldToCanvas(imageData?.origin);
|
||||||
|
// const orX = origin[0] * devicePixelRatio;
|
||||||
|
// const orY = origin[1] * devicePixelRatio;
|
||||||
|
// const x = Math.round(orX + dx);
|
||||||
|
// const y = Math.round(orY + dy);
|
||||||
|
// cy.log('testPixel origin x,y point x,y', orX, orY, x, y);
|
||||||
|
// // cy.log('world origin', imageData.origin);
|
||||||
|
// // cy.log('focal', viewport.getCamera().focalPoint,
|
||||||
|
// // viewport.worldToCanvas(viewport.getCamera().focalPoint));
|
||||||
|
// const pixelData = ctx.getImageData(x, y, 1, 1);
|
||||||
|
|
||||||
describe('CS3D Image Consistency and Quality', () => {
|
// expect(pixelData.data[0]).closeTo(expectedPixel, 1);
|
||||||
const setupStudySeries = (studyUID, seriesUID) => {
|
// });
|
||||||
cy.checkStudyRouteInViewer(
|
// });
|
||||||
studyUID,
|
// };
|
||||||
`&seriesInstanceUID=${seriesUID}&hangingProtocolId=@ohif/hpScale`
|
|
||||||
);
|
|
||||||
cy.initCornerstoneToolsAliases();
|
|
||||||
|
|
||||||
const skipMarkers = true;
|
// describe('CS3D Image Consistency and Quality', () => {
|
||||||
cy.initCommonElementsAliases(skipMarkers);
|
// const setupStudySeries = (studyUID, seriesUID) => {
|
||||||
};
|
// cy.checkStudyRouteInViewer(
|
||||||
|
// studyUID,
|
||||||
|
// `&seriesInstanceUID=${seriesUID}&hangingProtocolId=@ohif/hpScale`
|
||||||
|
// );
|
||||||
|
// cy.initCornerstoneToolsAliases();
|
||||||
|
|
||||||
it('TG18 Resolution Test Displayed 1:1', () => {
|
// const skipMarkers = true;
|
||||||
setupStudySeries(
|
// cy.initCommonElementsAliases(skipMarkers);
|
||||||
'2.16.124.113543.6004.101.103.20021117.061159.1',
|
// };
|
||||||
'2.16.124.113543.6004.101.103.20021117.061159.1.004'
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.wait(3000);
|
// it('TG18 Resolution Test Displayed 1:1', () => {
|
||||||
testPixel(1018, 1028, 255);
|
// setupStudySeries(
|
||||||
// Horizontal and vertical delta from this should not be contaminated
|
// '2.16.124.113543.6004.101.103.20021117.061159.1',
|
||||||
// by values from center
|
// '2.16.124.113543.6004.101.103.20021117.061159.1.004'
|
||||||
testPixel(1019, 1028, 0);
|
// );
|
||||||
testPixel(1018, 1029, 0);
|
|
||||||
testPixel(1017, 1028, 0);
|
|
||||||
testPixel(1018, 1027, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Missing test data - todo
|
// cy.wait(3000);
|
||||||
it.skip('8 bit image displayable', () => {
|
// testPixel(1018, 1028, 255);
|
||||||
setupStudySeries('1.3.46.670589.17.1.7.1.1.7', '1.3.46.670589.17.1.7.2.1.7');
|
// // Horizontal and vertical delta from this should not be contaminated
|
||||||
|
// // by values from center
|
||||||
|
// testPixel(1019, 1028, 0);
|
||||||
|
// testPixel(1018, 1029, 0);
|
||||||
|
// testPixel(1017, 1028, 0);
|
||||||
|
// testPixel(1018, 1027, 0);
|
||||||
|
// });
|
||||||
|
|
||||||
cy.wait(1000);
|
// // Missing test data - todo
|
||||||
|
// it.skip('8 bit image displayable', () => {
|
||||||
|
// setupStudySeries('1.3.46.670589.17.1.7.1.1.7', '1.3.46.670589.17.1.7.2.1.7');
|
||||||
|
|
||||||
// Compare with dcm2jpg generated values or by manually computing WL values
|
// cy.wait(1000);
|
||||||
testPixel(258, 257, 171);
|
|
||||||
testPixel(259, 257, 166);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('12 bit image displayable and zoom with pixel spacing', () => {
|
// // Compare with dcm2jpg generated values or by manually computing WL values
|
||||||
setupStudySeries(
|
// testPixel(258, 257, 171);
|
||||||
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
|
// testPixel(259, 257, 166);
|
||||||
'1.3.6.1.4.1.25403.345050719074.3824.20170125113608.5'
|
// });
|
||||||
);
|
|
||||||
|
|
||||||
cy.wait(1000);
|
// it.skip('12 bit image displayable and zoom with pixel spacing', () => {
|
||||||
|
// setupStudySeries(
|
||||||
|
// '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
|
||||||
|
// '1.3.6.1.4.1.25403.345050719074.3824.20170125113608.5'
|
||||||
|
// );
|
||||||
|
|
||||||
// Compare with dcm2jpg generated values or by manually computing WL values
|
// cy.wait(1000);
|
||||||
testPixel(258, 277, 120);
|
|
||||||
testPixel(259, 277, 122);
|
// // Compare with dcm2jpg generated values or by manually computing WL values
|
||||||
});
|
// testPixel(258, 277, 120);
|
||||||
});
|
// testPixel(259, 277, 122);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|||||||
@ -133,6 +133,67 @@ import { Icons } from '@ohif/ui-next';
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Creating New Custom Icons
|
||||||
|
|
||||||
|
This section explains how to migrate your custom icons from the old SVG import method to the new React component-based system in `@ohif/ui-next`. The new approach improves consistency, allows for better tree-shaking, and provides type safety.
|
||||||
|
|
||||||
|
The process involves converting your existing SVG files into React components and then registering them.
|
||||||
|
|
||||||
|
#### 1. Convert SVG to a React Component
|
||||||
|
|
||||||
|
First, take your raw SVG file and convert it into a `.tsx` React functional component.
|
||||||
|
|
||||||
|
**Before: Raw SVG File**
|
||||||
|
|
||||||
|
Previously, you might have had a file like `Baseline.svg`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
// Baseline.svg
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
< some svg content>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After: React Icon Component**
|
||||||
|
|
||||||
|
Now, create a `.tsx` file that exports a React component. Note the following changes:
|
||||||
|
- SVG attributes like `text-anchor` and `stroke-width` are converted to camel case (i.e. `textAnchor`, `strokeWidth`).
|
||||||
|
- The component accepts `IconProps` and spreads them onto the root `<svg>` element.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Baseline.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import type { IconProps } from '../types';
|
||||||
|
|
||||||
|
export const Baseline = (props: IconProps) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" {...props}>
|
||||||
|
< some svg content>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Baseline;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Register the New Icon
|
||||||
|
|
||||||
|
After creating the component, you must register it with the `Icons` service from `@ohif/ui-next`. This is typically done in a centralized location where you initialize your UI components (for example, an extension's preRegistration is a good spot for this). You'll need a unique name for the icon, which can be managed with an enum for consistency.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: in your setup/initialization code
|
||||||
|
import { Icons, IconNameEnum } from '@ohif/ui-next';
|
||||||
|
import Baseline from './sources/Baseline'; // Import your new icon component
|
||||||
|
|
||||||
|
// Add the icon to the registry
|
||||||
|
Icons.addIcon(IconNameEnum.BASELINE, Baseline);
|
||||||
|
```
|
||||||
|
|
||||||
|
By following these steps, your custom icon will be available for use throughout the application just like any of the default icons.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Detailed Renaming Table
|
### Detailed Renaming Table
|
||||||
|
|
||||||
| Old Icon Name | New Icon Component Name | Example Usage (`Icons.`) | Notes |
|
| Old Icon Name | New Icon Component Name | Example Usage (`Icons.`) | Notes |
|
||||||
|
|||||||
@ -60,6 +60,32 @@ This guide details the migration steps for the `uiDialogService` API changes, ba
|
|||||||
| `shouldCloseOnOverlayClick` | Default off for dialogs - Controls whether clicking the overlay background will close the dialog. |
|
| `shouldCloseOnOverlayClick` | Default off for dialogs - Controls whether clicking the overlay background will close the dialog. |
|
||||||
|
|
||||||
|
|
||||||
|
### Frequently Asked Questions
|
||||||
|
|
||||||
|
**Q: Why did my dialog's background color change?**
|
||||||
|
|
||||||
|
A: This can happen if you were previously setting the background color on the dialog's content directly. With the new API, the dialog's content is wrapped in a container. You should now pass any background or text color classes using the `containerClassName` property.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```diff
|
||||||
|
- containerClassName: 'w-[70%] max-w-[900px]',
|
||||||
|
+ containerClassName: 'w-[70%] max-w-[900px] bg-primary-dark text-foreground',
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: How do I create a dialog without the default container, like for a context menu?**
|
||||||
|
|
||||||
|
A: If you need to render dialog content without the standard dialog container (e.g., for a context menu), you can use the `unstyled: true` prop. This will render your component without the default dialog wrapper, giving you full control over its appearance.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
uiDialogService.show({
|
||||||
|
id: 'context-menu',
|
||||||
|
content: MyContextMenuComponent,
|
||||||
|
contentProps: { /* ... */ },
|
||||||
|
unstyled: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
**Migration Steps:**
|
**Migration Steps:**
|
||||||
|
|
||||||
1. **Replace `.create()` with `.show()`:**
|
1. **Replace `.create()` with `.show()`:**
|
||||||
@ -118,47 +144,109 @@ This guide details the migration steps for the `uiDialogService` API changes, ba
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
6. **Update Footer Action Buttons:**
|
||||||
|
|
||||||
|
Previously, footer buttons might have been implemented using generic `<Button>` components. The new approach uses a dedicated `<FooterAction>` component for better structure and consistency.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <Button
|
||||||
|
- name="Cancel"
|
||||||
|
- size={ButtonEnums.size.medium}
|
||||||
|
- type={ButtonEnums.type.secondary}
|
||||||
|
- onClick={onClose}
|
||||||
|
- > Cancel </Button>
|
||||||
|
+ <FooterAction>
|
||||||
|
+ <FooterAction.Right>
|
||||||
|
+ <FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
|
||||||
|
+ </FooterAction.Right>
|
||||||
|
+ </FooterAction>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
**Example: Updating a Simple Alert Dialog**
|
**Example: Migrating a Custom Dialog with Actions**
|
||||||
|
|
||||||
|
This example shows how to migrate a dialog that used the generic `Dialog` component with a `body` function and an `actions` array to the new component-based pattern.
|
||||||
|
|
||||||
|
**Before: Using a Generic `Dialog` with `contentProps`**
|
||||||
|
|
||||||
|
Previously, you might have constructed a dialog by passing a title, a body-rendering function, and an actions array directly into `contentProps`. This approach mixed content, presentation, and logic in the `create` call.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Before (using deprecated API)
|
// Before
|
||||||
let dialogId;
|
const dialogId = 'my-complex-dialog';
|
||||||
const showAlert = (message) => {
|
const showCompletionDialog = (successMessage) => {
|
||||||
dialogId = uiDialogService.create({
|
const dismiss = () => uiDialogService.dismiss({ id: dialogId });
|
||||||
|
uiDialogService.create({
|
||||||
|
id: dialogId,
|
||||||
centralize: true,
|
centralize: true,
|
||||||
isDraggable: false,
|
isDraggable: false,
|
||||||
content: Dialog,
|
content: Dialog, // A generic Dialog component
|
||||||
contentProps: {
|
contentProps: {
|
||||||
title: 'Alert',
|
title: 'Action Completed',
|
||||||
body: () => <p>{message}</p>,
|
noCloseButton: true,
|
||||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
onClose: dismiss,
|
||||||
|
onSubmit: dismiss,
|
||||||
|
actions: [
|
||||||
|
{ id: 'proceed', text: 'Proceed', type: 'primary' },
|
||||||
|
],
|
||||||
|
body: () => (
|
||||||
|
<div className="text-secondary-light">{successMessage}</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**After: Using a Dedicated Component**
|
||||||
|
|
||||||
|
The new pattern involves creating a dedicated React component for your dialog's content. This component encapsulates its own layout, logic, and actions, leading to cleaner and more maintainable code.
|
||||||
|
|
||||||
|
**1. Create a dedicated component for your dialog's content.**
|
||||||
|
|
||||||
|
This component receives `hide` and any custom data via props. It manages its own UI, including the footer buttons, and handles the logic for what happens when a user interacts with it.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// After: MyCompletionDialog.tsx
|
||||||
|
function MyCompletionDialog({ hide, successMessage }) {
|
||||||
|
const closeAndProceed = () => {
|
||||||
|
hide();
|
||||||
|
// You can now handle any post-dialog logic here,
|
||||||
|
// such as navigating to a different page.
|
||||||
|
// navigate('/next-page');
|
||||||
|
};
|
||||||
|
|
||||||
// After (using new API)
|
|
||||||
function AlertDialog({ message, hide }) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col gap-4">
|
||||||
<p>{message}</p>
|
<div className="text-body-text">{successMessage}</div>
|
||||||
<button onClick={hide}>OK</button>
|
<FooterAction>
|
||||||
|
<FooterAction.Right>
|
||||||
|
<FooterAction.Primary onClick={closeAndProceed}>
|
||||||
|
Proceed
|
||||||
|
</FooterAction.Primary>
|
||||||
|
</FooterAction.Right>
|
||||||
|
</FooterAction>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAlert = (message) => {
|
|
||||||
uiDialogService.show({
|
|
||||||
id: 'alert-dialog',
|
|
||||||
title: 'Alert',
|
|
||||||
content: AlertDialog,
|
|
||||||
contentProps: { message },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**2. Call `uiDialogService.show` with the new component.**
|
||||||
|
|
||||||
|
The call to show the dialog is now much simpler. You pass the component itself to the `content` property and any necessary data through `contentProps`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// After: Calling the service
|
||||||
|
const showCompletionDialog = (successMessage) => {
|
||||||
|
uiDialogService.show({
|
||||||
|
id: 'my-complex-dialog',
|
||||||
|
title: 'Action Completed',
|
||||||
|
content: MyCompletionDialog,
|
||||||
|
contentProps: {
|
||||||
|
successMessage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,17 @@ summary: Migration guide for the uiModalService in OHIF 3.10, covering props tha
|
|||||||
| `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. |
|
| `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. |
|
||||||
|
|
||||||
|
|
||||||
|
### Frequently Asked Questions
|
||||||
|
|
||||||
|
**Q: Why did my dialog's background color change?**
|
||||||
|
|
||||||
|
A: This can happen if you were previously setting the background color on the dialog's content directly. With the new API, the dialog's content is wrapped in a container. You should now pass any background or text color classes using the `containerClassName` property.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```diff
|
||||||
|
- containerClassName: 'w-[70%] max-w-[900px]',
|
||||||
|
+ containerClassName: 'w-[70%] max-w-[900px] bg-primary-dark text-foreground',
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
**Migration Steps:**
|
**Migration Steps:**
|
||||||
@ -103,3 +114,60 @@ Previously, you had to pass in the `onClose` as `hide` function automatically ad
|
|||||||
+ },
|
+ },
|
||||||
+ });
|
+ });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
6. **Update Footer Action Buttons:**
|
||||||
|
|
||||||
|
Previously, footer buttons might have been implemented using generic `<Button>` components. The new approach uses a dedicated `<FooterAction>` component for better structure and consistency.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <Button
|
||||||
|
- name="Cancel"
|
||||||
|
- size={ButtonEnums.size.medium}
|
||||||
|
- type={ButtonEnums.type.secondary}
|
||||||
|
- onClick={onClose}
|
||||||
|
- > Cancel </Button>
|
||||||
|
+ <FooterAction>
|
||||||
|
+ <FooterAction.Right>
|
||||||
|
+ <FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
|
||||||
|
+ </FooterAction.Right>
|
||||||
|
+ </FooterAction>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Simple Alert Modal**
|
||||||
|
|
||||||
|
This example shows how to migrate a simple alert modal.
|
||||||
|
|
||||||
|
*Before:*
|
||||||
|
|
||||||
|
```js
|
||||||
|
uiModalService.show({
|
||||||
|
title: 'Untrack Series',
|
||||||
|
content: UntrackSeriesModal,
|
||||||
|
contentProps: {
|
||||||
|
onConfirm,
|
||||||
|
hide, // passed in automatically in the background
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
*After:*
|
||||||
|
|
||||||
|
```js
|
||||||
|
function UntrackSeriesModal({ onConfirm, hide }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Modal content */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
uiModalService.show({
|
||||||
|
title: 'Untrack Series',
|
||||||
|
content: UntrackSeriesModal,
|
||||||
|
contentProps: {
|
||||||
|
onConfirm,
|
||||||
|
hide, // passed in automatically in the background
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user