feat(customization): new customization service api (#4688)
No files matched your search
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -67,12 +67,12 @@ customizationService.addModeCustomizations([
|
||||
customizationService.addModeCustomizations([
|
||||
// To disable editing in the SegmentationTable
|
||||
{
|
||||
id: 'PanelSegmentation.disableEditing',
|
||||
id: 'panelSegmentation.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
// To disable editing in the MeasurementTable
|
||||
{
|
||||
id: 'PanelMeasurement.disableEditing',
|
||||
id: 'panelMeasurement.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
])
|
||||
@@ -106,11 +106,11 @@ customizationService.addModeCustomizations([
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
id: 'panelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
id: 'panelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Customization Service
|
||||
---
|
||||
|
||||
# CustomizationService
|
||||
|
||||
|
||||
|
||||
**Key Changes:**
|
||||
|
||||
|
||||
1. **Unified Customization Getter:**
|
||||
- The `getCustomization` method now uniformly retrieves customizations, prioritizing `global`, then `mode`, and finally `default` customizations.
|
||||
- The `defaultValue` parameter in `getCustomization` is no longer used for setting defaults. It simply returns if no customization is found.
|
||||
- The methods `getModeCustomization` and `getGlobalCustomization` are deprecated.
|
||||
|
||||
2. **Simplified Customization Registration:**
|
||||
- The `customizationType` property in customization definitions is renamed to `inheritsFrom`.
|
||||
- The `merge` property in customization definitions is removed. Instead, a customization is merged using the helper methods. The basic update commands are listed in the table below, and you can learn more about the helper methods [here](../../../platform/services/customization-service/customizationService.md).
|
||||
|
||||
| Command | Description | Example |
|
||||
| :------- | :---------------------------------------- | :------------------------------------------------ |
|
||||
| `$set` | Replace a value entirely | Replace a list or object |
|
||||
| `$push` | Append items to an array | Add to the end of a list |
|
||||
| `$unshift` | Prepend items to an array | Add to the start of a list |
|
||||
| `$splice` | Insert, remove, or replace at specific index | Modify specific indices in a list |
|
||||
| `$merge` | Update specific fields in an object | Change a subset of fields |
|
||||
| `$apply` | Compute the new value dynamically | Apply a function to transform values |
|
||||
| `$filter` | Find and update specific items in arrays | Target nested structures based on matching criteria |
|
||||
|
||||
|
||||
3. **New `$transform` command:**
|
||||
- If you were using the `transform` command, you should now use the `$transform` command. Just a simple rename to make it more consistent with the other commands.
|
||||
|
||||
|
||||
5. **Renamed `CornerstoneOverlay` customizations:**
|
||||
- The `cornerstoneOverlay` customizations (`cornerstoneOverlayTopLeft`, `cornerstoneOverlayTopRight`, `cornerstoneOverlayBottomLeft`, `cornerstoneOverlayBottomRight`) have been renamed to `viewportOverlay.topLeft`, `viewportOverlay.topRight`, `viewportOverlay.bottomLeft`, and `viewportOverlay.bottomRight`. See dedicated page for customizing viewport overlays [here](../../../platform/services/customization-service/viewportOverlay.md).
|
||||
|
||||
6. **Renamed `customRoutes`:**
|
||||
- The `customRoutes` customization is renamed to `routes.customRoutes`.
|
||||
|
||||
7. **`contextMenu` customization:**
|
||||
- The `contextMenu` customization now uses the `inheritsFrom` property to inherit from other context menus, previously it was called `customizationType`
|
||||
|
||||
8. **New `immutability-helper` dependency:**
|
||||
The `immutability-helper` library is now used for merging customizations. If you encounter an error related to it, you'll need to install it - though OHIF should really handle the installation for you, so this is pretty much just a heads up.
|
||||
|
||||
**Migration Steps:**
|
||||
|
||||
1. **Replace `getModeCustomization` and `getGlobalCustomization` with `getCustomization`:**
|
||||
|
||||
- **Before:**
|
||||
|
||||
```javascript
|
||||
const tools = customizationService.getModeCustomization(
|
||||
'cornerstone.overlayViewportTools'
|
||||
)?.tools;
|
||||
const globalValue = customizationService.getGlobalCustomization('someGlobalKey');
|
||||
```
|
||||
|
||||
- **After:**
|
||||
|
||||
```javascript
|
||||
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
|
||||
const globalValue = customizationService.getCustomization('someGlobalKey');
|
||||
```
|
||||
|
||||
|
||||
:::note
|
||||
The returned value is the actual customization value, not an object that needs to be broken down.
|
||||
:::
|
||||
|
||||
2. **Update Customization Definitions:**
|
||||
- We've moved away from using random items in the customization definition, and now we use the `id` property to identify the customization as a value. Previously, it was referred to as `value`, `values`, and so on, but now an `id` is used to reference the customization. This approach really simplifies things - when you need to grab the customization, you can just use the `id` to get it, and you don't have to bother with destructuring the value from the object.
|
||||
|
||||
|
||||
|
||||
|
||||
**Example: Customizing a Panel**
|
||||
|
||||
**Before (v3.9):**
|
||||
|
||||
```javascript
|
||||
// the default value was hardcoded inside the panel itself - bad idea!
|
||||
// default was given in the panel itself
|
||||
|
||||
// PanelSegmentation.tsx
|
||||
|
||||
// Retrieve the onSegmentationAdd customization
|
||||
const { onSegmentationAdd } = customizationService.getCustomization(
|
||||
'PanelSegmentation.onSegmentationAdd',
|
||||
{
|
||||
id: 'segmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: handlers.onSegmentationAdd,
|
||||
}
|
||||
);
|
||||
|
||||
// Retrieve the disableEditing customization
|
||||
const { disableEditing } = customizationService.getCustomization(
|
||||
'PanelSegmentation.disableEditing',
|
||||
{
|
||||
id: 'default.disableEditing',
|
||||
disableEditing: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
// mode was customizing it via
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.showAddSegment',
|
||||
showAddSegment: false,
|
||||
},
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
**After (v3.10):**
|
||||
|
||||
```javascript
|
||||
// cornerstone extension getCustomizationModule
|
||||
// centralized customization location for all extensions - good!
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: {
|
||||
'panelSegmentation.disableEditing': false,
|
||||
'panelSegmentation.showAddSegment': true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// inside panelSegmentation.tsx
|
||||
const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing');
|
||||
const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment');
|
||||
|
||||
|
||||
// mode can customize it via $ operators for mode customizations
|
||||
customizationService.setCustomizations({
|
||||
'panelSegmentation.disableEditing': { $set: true },
|
||||
'panelSegmentation.showAddSegment': { $set: false },
|
||||
});
|
||||
|
||||
|
||||
//or via configuration for global customizations
|
||||
window.config = {
|
||||
// rest of config
|
||||
customizationService: [
|
||||
{
|
||||
'panelSegmentation.disableEditing': {
|
||||
$set: true, // Disables editing of segmentations in the panel
|
||||
},
|
||||
},
|
||||
],
|
||||
// rest of config
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Example: Updating a Customization**
|
||||
|
||||
Let's say you have a customization in v3.9 that adds a custom overlay item to the top-left corner:
|
||||
|
||||
**Before (v3.9):**
|
||||
|
||||
```javascript
|
||||
// In your mode's onModeEnter
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'cornerstoneOverlayTopLeft',
|
||||
items: [
|
||||
{
|
||||
id: 'myCustomOverlay',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
attribute: 'PatientName',
|
||||
label: 'Patient:',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
**After (v3.10):**
|
||||
|
||||
```javascript
|
||||
// In your mode's onModeEnter or elsewhere
|
||||
customizationService.setCustomizations({
|
||||
'viewportOverlay.topLeft': {
|
||||
$push: [
|
||||
{
|
||||
id: 'myCustomOverlay',
|
||||
inheritsFrom: 'ohif.overlayItem',
|
||||
attribute: 'PatientName',
|
||||
label: 'Patient:',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Note:**
|
||||
|
||||
- The `customizationType` is replaced with `inheritsFrom`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Renaming
|
||||
|
||||
To keep our customization system consistent, you should be aware of a few key renaming conventions. We now follow a straightforward naming convention for customizations: `scopeName.customizationItem`.
|
||||
|
||||
|
||||
|
||||
| Customization Key (Old) | Customization Key (New) | Description |
|
||||
| :------------------------------------------ | :------------------------------------------- | :-------------------------------------------------------------------------- |
|
||||
| `PanelMeasurement.disableEditing` | `panelMeasurement.disableEditing` | Disables editing measurements in the Measurement Panel and after SR hydration. |
|
||||
| `PanelSegmentation.CustomDropdownMenuContent` | `panelSegmentation.customDropdownMenuContent` | Custom content for the dropdown menu in the Segmentation Panel. |
|
||||
| `PanelSegmentation.disableEditing` | `panelSegmentation.disableEditing` | Disables editing segmentations in the Segmentation Panel. |
|
||||
| `PanelSegmentation.showAddSegment` | `panelSegmentation.showAddSegment` | Controls visibility of the "Add Segment" button in the Segmentation Panel. |
|
||||
| `PanelSegmentation.onSegmentationAdd` | `panelSegmentation.onSegmentationAdd` | Custom function to execute when a new segmentation is added. |
|
||||
| `PanelSegmentation.tableMode` | `panelSegmentation.tableMode` | Controls the table mode (collapsed/expanded) in the Segmentation Panel. |
|
||||
| `PanelSegmentation.readableText` | `panelSegmentation.readableText` | Custom readable text labels for the Segmentation Panel. |
|
||||
| `PanelStudyBrowser.studyMode` | `studyBrowser.studyMode` | Controls the study mode (all/primary/recent) in the Study Browser Panel. |
|
||||
| `customRoutes` | `routes.customRoutes` | Defines custom routes for the application. |
|
||||
| `cornerstoneOverlayTopLeft` | `viewportOverlay.topLeft` | Custom overlay items for the top-left corner of the viewport. |
|
||||
| `cornerstoneOverlayTopRight` | `viewportOverlay.topRight` | Custom overlay items for the top-right corner of the viewport. |
|
||||
| `cornerstoneOverlayBottomLeft` | `viewportOverlay.bottomLeft` | Custom overlay items for the bottom-left corner of the viewport. |
|
||||
| `cornerstoneOverlayBottomRight` | `viewportOverlay.bottomRight` | Custom overlay items for the bottom-right corner of the viewport. |
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: General
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Measurements
|
||||
---
|
||||
|
||||
|
||||
|
||||
import { measurementsCustomizations, TableGenerator } from './sampleCustomizations';
|
||||
|
||||
{TableGenerator(measurementsCustomizations)}
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Segmentation
|
||||
---
|
||||
|
||||
|
||||
|
||||
import { segmentationCustomizations, TableGenerator } from './sampleCustomizations';
|
||||
|
||||
{TableGenerator(segmentationCustomizations)}
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: Study Browser
|
||||
---
|
||||
|
||||
# Study Browser
|
||||
|
||||
The Study Browser is a component that allows users to browse and manage studies.
|
||||
|
||||
import { studyBrowserCustomizations, TableGenerator } from './sampleCustomizations';
|
||||
|
||||
{TableGenerator(studyBrowserCustomizations)}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Customization Service",
|
||||
"position": 4
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Advanced Customization
|
||||
---
|
||||
|
||||
|
||||
Below is an overview of how `transform` and `inheritsFrom` work within this customization system. They allow you to build a hierarchy of customizations in which items can inherit fields from a parent and then optionally apply a transformation before returning the final result.
|
||||
|
||||
|
||||
## `inheritsFrom`
|
||||
|
||||
### Purpose
|
||||
Indicates that the current customization should inherit and merge fields from another customization. The system fetches the parent customization, merges its properties, and returns a combined object.
|
||||
|
||||
### How It Works
|
||||
1. When you request or transform a customization that has `inheritsFrom: "parentCustomizationId"`, the service looks up `parentCustomizationId` via `getCustomization(...)`.
|
||||
2. Properties from the parent get copied into the child, but the child’s own properties overwrite any matching ones from the parent.
|
||||
3. If the child has a `transform` function, it runs after the merge.
|
||||
|
||||
### Example
|
||||
```js
|
||||
export default {
|
||||
measurementsContextMenu: {
|
||||
$set: {
|
||||
inheritsFrom: 'ohif.contextMenu',
|
||||
menus: [
|
||||
{
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
Here, `measurementsContextMenu` inherits from `ohif.contextMenu`. During retrieval or transformation, the system merges `ohif.contextMenu` into `measurementsContextMenu`.
|
||||
|
||||
---
|
||||
|
||||
## `transform`
|
||||
|
||||
### Purpose
|
||||
Specifies a function that can modify or enhance the customization object at runtime. Often used to run extra setup code or combine fields in a special way.
|
||||
|
||||
### How It Works
|
||||
1. You define a `transform(customizationService)` function inside your customization object.
|
||||
2. When the system retrieves the customization, after merging any inherited fields, it calls `transform`.
|
||||
3. The function may return an updated object, clone existing properties, or apply logic to nested items.
|
||||
|
||||
### Example
|
||||
```js
|
||||
export default {
|
||||
'@ohif/contextMenuAnnotationCode': {
|
||||
$transform: function (customizationService) {
|
||||
const { code: codeRef } = this;
|
||||
if (!codeRef) {
|
||||
throw new Error(`item ${this} has no code ref`);
|
||||
}
|
||||
const codingValues = customizationService.getCustomization('codingValues');
|
||||
const code = codingValues[codeRef];
|
||||
|
||||
return {
|
||||
...this,
|
||||
codeRef,
|
||||
code: { ref: codeRef, ...code },
|
||||
label: this.label || code.text || codeRef,
|
||||
commands: [{ commandName: 'updateMeasurement' }],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
In this snippet, the `transform` function:
|
||||
- Reads a code reference from `this`.
|
||||
- Looks up more data for that code in `codingValues`.
|
||||
- Merges those details back into `this` before returning the final object.
|
||||
|
||||
---
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Base and Specialized Customizations**
|
||||
Use `inheritsFrom` to define a broad, general customization (e.g., a generic context menu) and then create specialized versions that only override certain fields.
|
||||
|
||||
2. **Dynamic Assembly**
|
||||
Use `transform` when you need to compute or modify fields based on application state or other registered customizations.
|
||||
|
||||
3. **Nested Items**
|
||||
If an item within the customization also has `inheritsFrom`, it will follow the same inheritance flow and can run its own `transform` logic.
|
||||
|
||||
---
|
||||
|
||||
**Key Points**
|
||||
- `inheritsFrom` is a reference to another customization’s ID.
|
||||
- If `transform` is defined, it always runs after inheritance is resolved.
|
||||
- Merging is shallow: child properties override the parent’s.
|
||||
- You can nest multiple levels of inheritance, each possibly having its own `transform` step.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
sidebar_label: Context Menu
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Context Menu
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Context menus can be created by defining the menu structure and click
|
||||
interaction, as defined in the `ContextMenu/types`. There are examples
|
||||
below specific to the cornerstone context, because the actual click
|
||||
handler and attributes used to decide when and how to display the menu
|
||||
are specific to the context used for where the menu is displayed.
|
||||
|
||||
## Cornerstone Context Menu
|
||||
|
||||
The default cornerstone context menu can be customized by setting the
|
||||
`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
sidebar_label: Custom Routes
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# customRoutes
|
||||
|
||||
* Name: `routes.customRoutes` global
|
||||
* Attributes:
|
||||
** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add.
|
||||
** Should any element of routes match an existing baked in element, the baked in one will be replaced.
|
||||
** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes)
|
||||
|
||||
### Example
|
||||
|
||||
Since custom routes use React, they should be defined as modules inside the extension that is providing them. And cannot be
|
||||
in the AppConfig (yet).
|
||||
|
||||
|
||||
```js
|
||||
export default function getCustomizationModule({ servicesManager, extensionManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'helloPage',
|
||||
value: {
|
||||
'routes.customRoutes': {
|
||||
routes: {
|
||||
$push: [
|
||||
{
|
||||
path: '/custom',
|
||||
children: () => <h1 style={{ color: 'white' }}>Hello Custom Route</h1>,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Then after you define the module, you can add it to the customizationService in the AppConfig and reference it by the name you provided.
|
||||
|
||||
```js
|
||||
customizationService: [
|
||||
// Shows a custom route -access via http://localhost:3000/custom
|
||||
'@ohif/extension-default.customizationModule.helloPage',
|
||||
],
|
||||
```
|
||||
|
||||
You can provide multiple custom routes in the same module, for instance another extension can also push to the routes array.
|
||||
|
||||
```js
|
||||
export default function getCustomizationModule({ servicesManager, extensionManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'secondPage',
|
||||
value: {
|
||||
customRoutes: {
|
||||
routes: {
|
||||
$push: [
|
||||
{
|
||||
path: '/second',
|
||||
children: () => <h1 style={{ color: 'white' }}>Hello Second Route</h1>,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then you can add it to the customizationService in the AppConfig and reference it by the name you provided.
|
||||
|
||||
```js
|
||||
customizationService: [
|
||||
// Shows a custom route -access via http://localhost:3000/custom
|
||||
'@ohif/extension-default.customizationModule.helloPage',
|
||||
// Shows a custom route -access via http://localhost:3000/second
|
||||
'@ohif/extension-default.customizationModule.secondPage',
|
||||
],
|
||||
```
|
||||
@@ -0,0 +1,544 @@
|
||||
---
|
||||
sidebar_label: Introduction
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
import { customizations, TableGenerator } from './sampleCustomizations';
|
||||
import Heading from '@theme/Heading';
|
||||
import TOCInline from '@theme/TOCInline';
|
||||
|
||||
# Customization Service
|
||||
|
||||
There are a lot of places where users may want to configure certain elements
|
||||
differently between different modes or for different deployments. A mode
|
||||
example might be the use of a custom overlay showing mode related DICOM header
|
||||
information such as radiation dose or patient age.
|
||||
|
||||
The use of `customizationService` enables these to be defined in a typed fashion by
|
||||
providing an easy way to set default values for this, but to allow a
|
||||
non-default value to be specified by the configuration or mode.
|
||||
|
||||
|
||||
:::note
|
||||
|
||||
`customizationService` itself doesn't implement the actual customization,
|
||||
but rather just provide mechanism to register reusable prototypes, to configure
|
||||
those prototypes with actual configurations, and to use the configured objects
|
||||
(components, data, whatever).
|
||||
|
||||
Actual implementation of the customization is totally up to the component that
|
||||
supports customization.
|
||||
:::
|
||||
|
||||
|
||||
## General Overview
|
||||
|
||||
This framework allows you to configure many features, or "slots," through customization modules. Extensions can choose to offer their own module, which outlines which values can be changed. By looking at each extension's getCustomizationModule(), you can see which objects or components are open to customization.
|
||||
|
||||
Below is a high-level example of how you might define a default customization and then consume and override it:
|
||||
|
||||
1. **Defining a Customizable Default**
|
||||
|
||||
In your extension, you might export a set of default configurations (for instance, a list that appears in a panel). Here, you provide an identifier and store the default list under that identifier. This makes the item discoverable by the customization service:
|
||||
|
||||
```js
|
||||
// Inside your extension’s customization module
|
||||
export default function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: {
|
||||
defaultList: ['Item A', 'Item B'],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
By naming it `default`, it is automatically registered.
|
||||
|
||||
:::info
|
||||
You might want to have customizations ready to use in your application without actually applying them. In such cases, you can name them something other than `default`. For example, in your mode, you can do this:
|
||||
|
||||
```js
|
||||
customizationService.setCustomizations([
|
||||
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
|
||||
]);
|
||||
```
|
||||
|
||||
This is really useful when you want to apply a set of customizations as a pack, kind of like a bundle.
|
||||
:::
|
||||
|
||||
3. **Retrieving the Default Customization**
|
||||
In the panel or component (or whatever) that needs the list, you retrieve it using `getCustomization`:
|
||||
|
||||
```js
|
||||
const myList = customizationService.getCustomization('defaultList');
|
||||
// If unmodified, this returns ['Item A', 'Item B']
|
||||
```
|
||||
|
||||
This allows your component to always fetch the most current version (original default or overridden).
|
||||
|
||||
4. **Overriding from Outside**
|
||||
To customize this list outside your extension, call `setCustomizations` with the identifier (`'defaultList'`). For example, a mode can modify the list to add or change items:
|
||||
|
||||
```js
|
||||
// From within a mode (or globally)
|
||||
customizationService.setCustomizations({
|
||||
'defaultList': {
|
||||
$set: ['New Item 1', 'New Item 2'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The next time any panel calls `getCustomization('defaultList')`, it will get the updated list.
|
||||
|
||||
Don't worry we will go over the `$set` syntax in more detail later.
|
||||
|
||||
---
|
||||
|
||||
## Scope of Customization
|
||||
|
||||
|
||||
Customizations can be declared at three different scopes, each with its own priority and lifecycle. These scopes determine how and when customizations are applied.
|
||||
|
||||
|
||||
### 1. **Default Scope**
|
||||
- **Purpose**: Establish baseline or "fallback" values that extensions provide.
|
||||
- **Options**:
|
||||
1. **Via Extensions**:
|
||||
- Implement a `getCustomizationModule` function in your extension and name it `default`.
|
||||
```tsx
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: {
|
||||
'studyBrowser.sortFunctions': {
|
||||
$set: [
|
||||
{
|
||||
label: 'Default Sort Function',
|
||||
sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
2. **Using the `setCustomizations` Method**:
|
||||
- Call `setCustomizations` in your application and specify `CustomizationScope.Default` as the second argument:
|
||||
```tsx
|
||||
customizationService.setCustomizations(
|
||||
{
|
||||
'studyBrowser.sortFunctions': {
|
||||
$set: [
|
||||
{
|
||||
label: 'Default Sort Function',
|
||||
sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
CustomizationScope.Default
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
### 2. **Mode Scope**
|
||||
- **Purpose**: Apply customizations specific to a particular mode.
|
||||
- **Lifecycle**: These customizations are cleared or reset when switching between modes.
|
||||
- **Example**: Use the `setCustomizations` method to define mode-specific behavior.
|
||||
```tsx
|
||||
customizationService.setCustomizations({
|
||||
'studyBrowser.sortFunctions': {
|
||||
$set: [
|
||||
{
|
||||
label: 'Mode-Specific Sort Function',
|
||||
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3. **Global Scope**
|
||||
- **Purpose**: Apply system-wide customizations that override both default and mode-scoped values.
|
||||
- **How to Configure**:
|
||||
1. Add global customizations directly to the application's configuration file:
|
||||
```jsx
|
||||
window.config = {
|
||||
name: 'config/default.js',
|
||||
routerBasename: '/',
|
||||
customizationService: [
|
||||
{
|
||||
'studyBrowser.sortFunctions': {
|
||||
$push: [
|
||||
{
|
||||
label: 'Global Sort Function',
|
||||
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
2. Use Namespaced Extensions:
|
||||
- Instead of directly specifying customizations in the configuration, you can refer to a predefined customization module within an extension:
|
||||
|
||||
```jsx
|
||||
window.config = {
|
||||
name: 'config/default.js',
|
||||
routerBasename: '/',
|
||||
customizationService: [
|
||||
'@ohif/extension-cornerstone.customizationModule.newCustomization',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- In this example, the `newCustomization` module within the `@ohif/extension-cornerstone` extension contains the global customizations. The application will load and apply these settings globally.
|
||||
|
||||
```tsx
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'newCustomization',
|
||||
value: {
|
||||
'studyBrowser.sortFunctions': {
|
||||
$push: [
|
||||
{
|
||||
label: 'Global Namespace Sort Function',
|
||||
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Priority of Scopes
|
||||
When a customization is retrieved:
|
||||
1. **Global Scope**: Takes precedence if defined.
|
||||
2. **Mode Scope**: Used if no global customization is defined.
|
||||
3. **Default Scope**: Fallback when neither global nor mode-specific values are available.
|
||||
|
||||
|
||||
As you have guessed the `.setCustomizations` accept a second argument which is the scope. By default it is set to `mode`.
|
||||
|
||||
|
||||
## Customization Syntax
|
||||
|
||||
|
||||
The customization syntax is designed to offer **flexibility** when modifying configurations. Instead of simply replacing values, you can perform granular updates like appending items to arrays, inserting at specific indices, updating deeply nested fields, or applying filters. This flexibility ensures that updates are efficient, targeted, and suitable for complex data structures.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Why a Special Syntax?
|
||||
</summary>
|
||||
|
||||
Traditional value replacement might not be ideal in scenarios such as:
|
||||
- **Appending or prepending** to an existing list instead of overwriting it.
|
||||
- **Selective updates** for specific fields in an object without affecting other fields.
|
||||
- **Filtering or merging** nested items in arrays or objects while preserving other parts.
|
||||
|
||||
To address these needs, the customization service uses a **special syntax** inspired by [immutability-helper](https://github.com/kolodny/immutability-helper) commands. Below are examples of each operation.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 1. Replace a Value (`$set`)
|
||||
|
||||
Use `$set` to entirely replace a value. This is the simplest operation which would replace the entire value.
|
||||
|
||||
```js
|
||||
// Before: someKey = 'Old Value'
|
||||
customizationService.setCustomizations({
|
||||
someKey: { $set: 'New Value' },
|
||||
});
|
||||
// After: someKey = 'New Value'
|
||||
```
|
||||
|
||||
Example with study browser:
|
||||
|
||||
```js
|
||||
// Before: studyBrowser.sortFunctions = []
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'studyBrowser.sortFunctions': {
|
||||
$set: [
|
||||
{
|
||||
label: 'Sort by Patient ID',
|
||||
sortFunction: (a, b) => a.PatientID.localeCompare(b.PatientID),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// After: studyBrowser.sortFunctions = [{label: 'Sort by Patient ID', sortFunction: ...}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Add to an Array (`$push` and `$unshift`)
|
||||
|
||||
- **`$push`**: Appends items to the end of an array.
|
||||
- **`$unshift`**: Adds items to the beginning of an array.
|
||||
|
||||
```js
|
||||
// Before: NumbersList = [1, 2, 3]
|
||||
|
||||
// Push items to the end
|
||||
customizationService.setCustomizations({
|
||||
'NumbersList': { $push: [5, 6] },
|
||||
});
|
||||
// After: NumbersList = [1, 2, 3, 5, 6]
|
||||
|
||||
// Unshift items to the front
|
||||
customizationService.setCustomizations({
|
||||
'NumbersList': { $unshift: [0] },
|
||||
});
|
||||
// After: NumbersList = [0, 1, 2, 3, 5, 6]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Insert at Specific Index (`$splice`)
|
||||
|
||||
Use `$splice` to insert, replace, or remove items at a specific index in an array.
|
||||
|
||||
```js
|
||||
// Before: NumbersList = [1, 2, 3]
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'NumbersList': {
|
||||
$splice: [
|
||||
[2, 0, 99], // Insert 99 at index 2
|
||||
],
|
||||
},
|
||||
});
|
||||
// After: NumbersList = [1, 2, 99, 3]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Merge Object Properties (`$merge`)
|
||||
|
||||
Use `$merge` to update specific fields in an object without affecting other fields.
|
||||
|
||||
```js
|
||||
// Before: SeriesInfo = { label: 'Original Label', sortFunction: oldFunc }
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'SeriesInfo': {
|
||||
$merge: {
|
||||
label: 'Updated Label',
|
||||
extraField: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
// After: SeriesInfo = { label: 'Updated Label', sortFunction: oldFunc, extraField: true }
|
||||
```
|
||||
|
||||
Example with nested merge:
|
||||
```js
|
||||
// Before: SeriesInfo = { advanced: { subKey: 'oldValue' } }
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'SeriesInfo': {
|
||||
advanced: {
|
||||
$merge: {
|
||||
subKey: 'updatedSubValue',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// After: SeriesInfo = { advanced: { subKey: 'updatedSubValue' } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Apply a Function (`$apply`)
|
||||
|
||||
Use `$apply` when you need to compute the new value dynamically.
|
||||
|
||||
```js
|
||||
// Before: SeriesInfo = { label: 'Old Label', data: 123 }
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'SeriesInfo': {
|
||||
$apply: oldValue => ({
|
||||
...oldValue,
|
||||
label: 'Computed Label',
|
||||
}),
|
||||
},
|
||||
});
|
||||
// After: SeriesInfo = { label: 'Computed Label', data: 123 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Filter and Modify (`$filter`)
|
||||
|
||||
Use `$filter` to find specific items in arrays (or objects) and apply changes.
|
||||
|
||||
```js
|
||||
// Before: advanced = {
|
||||
// functions: [
|
||||
// { id: 'seriesDate', label: 'Original Label' },
|
||||
// { id: 'other', label: 'Other Label' }
|
||||
// ]
|
||||
// }
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'advanced': {
|
||||
$filter: {
|
||||
match: { id: 'seriesDate' },
|
||||
$merge: {
|
||||
label: 'Updated via Filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// After: advanced = {
|
||||
// functions: [
|
||||
// { id: 'seriesDate', label: 'Updated via Filter' },
|
||||
// { id: 'other', label: 'Other Label' }
|
||||
// ]
|
||||
// }
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
Note `$filter` will look recursively for
|
||||
an object that matches the `match` criteria and then apply the `$merge` or `$set` operation to it.
|
||||
|
||||
Note in the example above we are not doing anything with the `functions` array.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
Example with deeply nested filter:
|
||||
```js
|
||||
// Before: advanced = {
|
||||
// functions: [{
|
||||
// id: 'seriesDate',
|
||||
// viewFunctions: [
|
||||
// { id: 'axial', label: 'Original Axial' }
|
||||
// ]
|
||||
// }]
|
||||
// }
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'advanced': {
|
||||
$filter: {
|
||||
match: { id: 'axial' },
|
||||
$merge: {
|
||||
label: 'Axial (via Filter)',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// After: advanced = {
|
||||
// functions: [{
|
||||
// id: 'seriesDate',
|
||||
// viewFunctions: [
|
||||
// { id: 'axial', label: 'Axial (via Filter)' }
|
||||
// ]
|
||||
// }]
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Summary of Commands
|
||||
|
||||
| **Command** | **Purpose** | **Example** |
|
||||
|-------------|-----------------------------------------------|---------------------------------------|
|
||||
| `$set` | Replace a value entirely | Replace a list or object |
|
||||
| `$push` | Append items to an array | Add to the end of a list |
|
||||
| `$unshift` | Prepend items to an array | Add to the start of a list |
|
||||
| `$splice` | Insert, remove, or replace at specific index | Modify specific indices in a list |
|
||||
| `$merge` | Update specific fields in an object | Change a subset of fields |
|
||||
| `$apply` | Compute the new value dynamically | Apply a function to transform values |
|
||||
| `$filter` | Find and update specific items in arrays | Target nested structures |
|
||||
| `$transform`| Apply a function to transform the customization | Apply a function to transform values |
|
||||
|
||||
## Building Customizations Across Multiple Extensions
|
||||
|
||||
Sometimes it is useful to build customizations across multiple extensions. For example, you may want to build a default list of tools inside a vieweport. But then each extension may want to add their own tools to the list.
|
||||
|
||||
Lets say i have one default sorting function in my default extension.
|
||||
|
||||
```js
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: {
|
||||
'studyBrowser.sortFunctions': [
|
||||
{
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => {
|
||||
return a?.SeriesNumber - b?.SeriesNumber;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
This will result in having only series number as the default sorting function.
|
||||
|
||||
but now in another extension let's say dicom-seg extension we can add another sorting function.
|
||||
|
||||
```js
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: "dicom-seg-sorts",
|
||||
value: {
|
||||
"studyBrowser.sortFunctions": {
|
||||
$push: [
|
||||
{
|
||||
label: "Series Date",
|
||||
sortFunction: (a, b) => {
|
||||
return a?.SeriesDate - b?.SeriesDate;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
But since the module is not `default` it will not get applied, but in my segmentation mode i can do
|
||||
|
||||
|
||||
```js
|
||||
onModeEnter() {
|
||||
customizationService.setCustomizations([
|
||||
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
needless to say if you opted to choose `name: default` in the `getCustomizationModule` it was applied globally.
|
||||
|
||||
## Customizable Parts of OHIF
|
||||
|
||||
Below we are providing the example configuration for global scenario (using the configuration file), however, you can also use the `setCustomizations` method to set the customizations.
|
||||
|
||||
{TableGenerator(customizations)}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
import { viewportOverlayCustomizations , TableGenerator } from './sampleCustomizations';
|
||||
|
||||
# Viewport Overlay
|
||||
|
||||
Viewport Overlays are the information that is displayed on the viewport.
|
||||
|
||||

|
||||
|
||||
There are 4 viewport overlays customization end points
|
||||
|
||||
- `viewportOverlay.topRight`
|
||||
- `viewportOverlay.topLeft`
|
||||
- `viewportOverlay.bottomLeft`
|
||||
- `viewportOverlay.bottomRight`
|
||||
|
||||
|
||||
|
||||
{TableGenerator(viewportOverlayCustomizations)}
|
||||
@@ -127,7 +127,7 @@ The following services is available in the `OHIF-v3`.
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./ui/customization-service">
|
||||
<a href="./customization-service">
|
||||
CustomizationService
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -1,504 +0,0 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
sidebar_label: Customization Service
|
||||
---
|
||||
# Customization Service
|
||||
|
||||
There are a lot of places where users may want to configure certain elements
|
||||
differently between different modes or for different deployments. A mode
|
||||
example might be the use of a custom overlay showing mode related DICOM header
|
||||
information such as radiation dose or patient age.
|
||||
|
||||
The use of this service enables these to be defined in a typed fashion by
|
||||
providing an easy way to set default values for this, but to allow a
|
||||
non-default value to be specified by the configuration or mode.
|
||||
|
||||
This service is a UI service in that part of the registration allows for registering
|
||||
UI components and types to deal with, but it does not directly provide an UI
|
||||
displayable elements unless customized to do so.
|
||||
|
||||
<b>Note:</b> Customization Service itself doesn't implement the actual customization,
|
||||
but rather just provide mechanism to register reusable prototypes, to configure
|
||||
those prototypes with actual configurations, and to use the configured objects
|
||||
(components, data, whatever).
|
||||
Actual implementation of the customization is totally up to the component that
|
||||
supports customization. (for example, `CustomizableViewportOverlay` component uses
|
||||
`CustomizationService` to implement viewport overlay that is easily customizable
|
||||
from configuration.)
|
||||
|
||||
## Global, Default and Mode customizations
|
||||
There are various customization sets that define the lifetime/setup of the
|
||||
customization. The global customizations are those used for overriding
|
||||
customizations defined elsewhere, and allow replacing a customization.
|
||||
|
||||
Mode customizations are only registered for the lifetime of the mode, allowing
|
||||
the mode definition to update/modify the underlying behaviour. This is related
|
||||
to default customizations, which provide a fallback if the mode or global customization
|
||||
isn't defined. Default customizations may only be defined once, otherwise throwing
|
||||
an exception.
|
||||
|
||||
## Append and Merge Customizations
|
||||
In addition to the replace a customization, there is the ability to merge or append
|
||||
a customization. The merge customization simply applies the lodash merge functionality
|
||||
to the existing customization, with the new one, while the append customization
|
||||
modifies the customization by appending to the value.
|
||||
|
||||
### Append Behaviour
|
||||
When a list is found in the destination object, the append source object is
|
||||
examined to see how to handle the change. If the source is simply a list,
|
||||
then the list object is appended, and no additional changes are performed.
|
||||
However, if the source is an object other than a list, then the iterable
|
||||
attributes of the object are examined to match child objects to the destination list,
|
||||
according to the following table:
|
||||
|
||||
* Natural or zero number value - match the given index location and merge at the point
|
||||
* Fractional number value - insert at a new point in the list, starting from the end or beginning
|
||||
* keyword - match a value having the same id as the keyword, inserting at the end, or at _priority as defined in the keywords above.
|
||||
|
||||
#### Example Append
|
||||
|
||||
```javascript
|
||||
const destination = [
|
||||
1,
|
||||
{id: 'two', value: 2},
|
||||
{id: 'three', value: 3}
|
||||
]
|
||||
|
||||
const source = {
|
||||
two: { value: 'updated2' },
|
||||
1: { extraValue: 2 },
|
||||
1.0001: { id: 'inserted', value: 1.0001 },
|
||||
-1: { value: -3 },
|
||||
}
|
||||
```
|
||||
|
||||
Results in two updates to `destination[1]`, the first using an id match on 'two', while the second one
|
||||
does a positional match on `1`, resulting in the value `{id: 'two', value: 'updated2', extraValue: 2 }`
|
||||
|
||||
Then, it inserts the id 'inserted' after position 1.
|
||||
|
||||
Finally, position -1 (the end position) is updated from value 3 to value -3.
|
||||
|
||||
The ordering is not specified on any of these insertions, so can happen out of order. Use multiple updates to perform order specific inserts.
|
||||
|
||||
## Registering customizable modules (or defining customization prototypes)
|
||||
|
||||
Extensions and Modes can register customization templates they support.
|
||||
It is done by adding `getCustomizationModule()` in the extension or mode definition.
|
||||
|
||||
Below is the protocol of the `getCustomizationModule()`, if defined in Typescript.
|
||||
|
||||
```typescript
|
||||
getCustomizationModule() : { name: string, value: any }[]
|
||||
```
|
||||
|
||||
If the name is 'default', it is the a default customization, while if it
|
||||
is 'global', then it is a priority/over-riding customization.
|
||||
|
||||
In the `value` of each customizations, you will define customization prototype(s).
|
||||
These customization prototype(s) can be considered like "Prototype" in Javascript.
|
||||
These can be used to extend the customization definitions from configurations.
|
||||
Default customizations will be often used to define all the customization prototypes,
|
||||
Default customizations will be often used to define all the customization prototypes,
|
||||
as they will be loaded automatically along with the defining extension or mode.
|
||||
|
||||
|
||||
For example, the `@ohif/extension-default` extension defines,
|
||||
|
||||
```js
|
||||
getCustomizationModule: () => [
|
||||
//...
|
||||
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
{
|
||||
id: 'ohif.overlayItem',
|
||||
content: function (props) {
|
||||
if (this.condition && !this.condition(props)) return null;
|
||||
|
||||
const { instance } = props;
|
||||
const value =
|
||||
instance && this.attribute
|
||||
? instance[this.attribute]
|
||||
: this.contentF && typeof this.contentF === 'function'
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: this.color || undefined }}
|
||||
title={this.title || ''}
|
||||
>
|
||||
{this.label && (
|
||||
<span className="mr-1 shrink-0">{this.label}</span>
|
||||
)}
|
||||
<span className="font-light">{value}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//...
|
||||
],
|
||||
```
|
||||
|
||||
And this `ohif.overlayItem` object will be used as a prototype (and template) to define items
|
||||
to be displayed on `CustomizableViewportOverlay`. See how we use the `ohif.overlayItem` in
|
||||
the example below.
|
||||
|
||||
## Configuring customizations
|
||||
|
||||
There are several ways to register customizations. The
|
||||
`APP_CONFIG.customizationService`
|
||||
field is used as a per-configuration entry. This object can list single
|
||||
configurations by id, or it can list sets of customizations by referring to
|
||||
the `customizationModule` in an extension.
|
||||
|
||||
NOTE that these definitions from APP_CONFIG will be loaded by default, just like
|
||||
extension/modes default customization.
|
||||
|
||||
Below is the example configuration for `CustomizableViewportOverlay` component
|
||||
customization, using the customization prototype `ohif.overlayItem` defined in
|
||||
`ohif/extension-defaul` extension.:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
//...
|
||||
|
||||
// in the APP_CONFIG file set the top right area to show the patient name
|
||||
// using PN: as a prefix when the study has a non-empty patient name.
|
||||
customizationService: {
|
||||
cornerstoneOverlayTopRight: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
items: [
|
||||
{
|
||||
id: 'PatientNameOverlay',
|
||||
// Note below that here we are using the customization prototype of
|
||||
// `ohif.overlayItem` which was registered to the customization module in
|
||||
// `ohif/extension-default` extension.
|
||||
customizationType: 'ohif.overlayItem',
|
||||
// the following props are passed to the `ohif.overlayItem` prototype
|
||||
// which is used to render the overlay item based on the label, color,
|
||||
// conditions, etc.
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
title: 'Patient Name',
|
||||
color: 'yellow',
|
||||
condition: ({ instance }) =>
|
||||
instance &&
|
||||
instance.PatientName &&
|
||||
instance.PatientName.Alphabetic,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName.Alphabetic) +
|
||||
' ' +
|
||||
(instance.PatientSex ? '(' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
In the customization configuration, you can use `customizationType` fields to
|
||||
define the prototype that customization object should inherit from.
|
||||
The `customizationType` field is simply the id of another customization object.
|
||||
|
||||
|
||||
## Implementing customization using CustomizationService
|
||||
|
||||
### Mode Customizations
|
||||
|
||||
Mode-specific customizations are no different from the global ones,
|
||||
except that the mode customizations are specific to one mode and
|
||||
are not globally applied. Mode-specific customizations are also cleared
|
||||
before the mode `onModeEnter` is called, and they can have new values registered in the `onModeEnter`
|
||||
|
||||
Following on our example above to customize the overlay, we can now add a mode customization
|
||||
with a bottom-right overlay.
|
||||
|
||||
```js
|
||||
// Import the type from the extension itself
|
||||
import OverlayUICustomization from "@ohif/cornerstone-extension";
|
||||
|
||||
// In the mode itself, customizations can be registered:
|
||||
onModeEnter: {
|
||||
// Note how the object can be strongly typed
|
||||
const bottomRight: OverlayUICustomization = {
|
||||
id: 'cornerstoneOverlayBottomRight',
|
||||
// Note the type is the previously registered ohif.cornerstoneOverlay
|
||||
customizationType: 'ohif.cornerstoneOverlay',
|
||||
// The cornerstoneOverlay definition requires an items list here.
|
||||
items: [
|
||||
// Custom definitions for the context menu here.
|
||||
],
|
||||
};
|
||||
customizationService.addModeCustomizations(bottomRight);
|
||||
}
|
||||
```
|
||||
|
||||
The mode customizations are retrieved via the `getModeCustomization` function,
|
||||
providing an id, and optionally a default value. The retrieval will return,
|
||||
in order:
|
||||
|
||||
1. Global customization with the given id.
|
||||
2. Mode customization with the id.
|
||||
3. The default value specified.
|
||||
|
||||
The return value then inherits the `customizationType` instance, so that the
|
||||
value can be typed and have default values and functionality provided. The object
|
||||
can then be used in a way defined by the extension provided that customization
|
||||
point.
|
||||
|
||||
```ts
|
||||
const cornerstoneOverlay = customizationService.getModeCustomization(
|
||||
"cornerstoneOverlay",
|
||||
{ customizationType: "ohif.cornerstoneOverlay" },
|
||||
);
|
||||
|
||||
const { component: overlayComponent, props } =
|
||||
customizationService.getComponent(cornerstoneOverlay);
|
||||
|
||||
return (
|
||||
<defaultComponent {...props} overlay={cornerstoneOverlay}></defaultComponent>
|
||||
);
|
||||
```
|
||||
|
||||
This example shows fetching the default component to render this object. The
|
||||
returned object would be a sub-type of ohif.cornerstoneOverlay if defined. This
|
||||
object can be a React component or other object such as a commands list, for
|
||||
example (this example comes from the context menu customizations as that one
|
||||
uses commands lists):
|
||||
|
||||
```ts
|
||||
cornerstoneContextMenu = customizationService.get(
|
||||
"cornerstoneContextMenu",
|
||||
defaultMenu,
|
||||
);
|
||||
commandsManager.run(cornerstoneContextMenu, extraProps);
|
||||
```
|
||||
|
||||
### Global Customizations
|
||||
|
||||
Global customizations are retrieved in the same was as mode customizations, except
|
||||
that the `getGlobalCustomization` is called instead of the mode call.
|
||||
|
||||
### Types
|
||||
|
||||
Some types for the customization service are provided by the `@ohif/ui` types
|
||||
export. Additionally, extensions can provide a Types export with custom
|
||||
typing, allowing for better typing for the extension specific capabilities.
|
||||
This allows for having strong typing when declaring customizations, for example:
|
||||
|
||||
```ts
|
||||
import { Types } from '@ohif/ui';
|
||||
|
||||
const customContextMenu: Types.ContextMenu.Menu =
|
||||
{
|
||||
id: 'cornerstoneContextMenu',
|
||||
customizationType: 'ohif.contextMenu',
|
||||
// items will be type checked to be in accordance with UIContextMenu.items
|
||||
items: [ ... ]
|
||||
},
|
||||
```
|
||||
|
||||
### Inheritance
|
||||
|
||||
JavaScript property inheritance can be supplied by defining customizations
|
||||
with id corresponding to the customizationType value. For example:
|
||||
|
||||
```js
|
||||
getCustomizationModule = () => ([
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
{
|
||||
id: 'ohif.overlayItem',
|
||||
content: function (props) {
|
||||
return (<p><b>{this.label}</b> {props.instance[this.attribute]}</p>)
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
defines an overlay item which has a React content object as the render value.
|
||||
This can then be used by specifying a `customizationType` of `ohif.overlayItem`, for example:
|
||||
|
||||
```js
|
||||
const overlayItem: Types.UIOverlayItem = {
|
||||
id: 'anOverlayItem',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
};
|
||||
```
|
||||
|
||||
# Customizations
|
||||
|
||||
This section can be used to specify various customization capabilities.
|
||||
|
||||
## Text color for StudyBrowser tabs
|
||||
|
||||
This is the recommended pattern for deep customization of class attributes,
|
||||
making it fine grained, and have it apply a set of attributes, mostly from
|
||||
tailwind. In this case it is a double indirection, as the buttons class
|
||||
uses it's own internal class names.
|
||||
|
||||
* Name: 'class:StudyBrowser'
|
||||
* Attributes:
|
||||
** `true` for the is active true text color
|
||||
** `false` for the is active false text color.
|
||||
** Values are button colors, from the Button class, eg default, white, black
|
||||
|
||||
## customRoutes
|
||||
|
||||
* Name: `customRoutes` global
|
||||
* Attributes:
|
||||
** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add.
|
||||
** Should any element of routes match an existing baked in element, the baked in one will be replaced.
|
||||
** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes)
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'customRoutes',
|
||||
routes: [
|
||||
{
|
||||
path: '/myroute',
|
||||
children: MyRouteReactFunction,
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
There is a usage of this example commented out in config/default.js that
|
||||
looks like the code below. This example is provided by the default extension,
|
||||
again with commented out code. Uncomment the getCustomizationModule customRoutes
|
||||
code in the default module to activate this, and then go to: `http://localhost:3000/custom`
|
||||
to see the custom route.
|
||||
|
||||
Note the name of this is the customization module name, which usually won't match
|
||||
the id, and in fact there can be multiple customization objects defined for a single
|
||||
customization module, to allow for customizing sets of related values.
|
||||
|
||||
```js
|
||||
customizationService: [
|
||||
// Shows a custom route -access via http://localhost:3000/custom
|
||||
'@ohif/extension-default.customizationModule.helloPage',
|
||||
],
|
||||
```
|
||||
|
||||
## Customizable Viewport Overlay
|
||||
|
||||
Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay.
|
||||
|
||||
There are working examples that can be run with:
|
||||
```
|
||||
set APP_CONFIG=config/customization.js
|
||||
yarn dev
|
||||
```
|
||||
|
||||
```javascript
|
||||
// this is part of customization.js, an example customization dataset
|
||||
window.config = {
|
||||
|
||||
// This shows how to append to the customization data
|
||||
customizationService: [
|
||||
{
|
||||
id: '@ohif/cornerstoneOverlay',
|
||||
// Append recursively, rather than replacing
|
||||
merge: 'Append',
|
||||
topRightItems: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
items: [
|
||||
{
|
||||
id: 'PatientNameOverlay',
|
||||
// Note below that here we are using the customization prototype of
|
||||
// `ohif.overlayItem` which was registered to the customization module in
|
||||
// `ohif/extension-default` extension.
|
||||
customizationType: 'ohif.overlayItem',
|
||||
// the following props are passed to the `ohif.overlayItem` prototype
|
||||
// which is used to render the overlay item based on the label, color,
|
||||
// conditions, etc.
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
title: 'Patient Name',
|
||||
color: 'yellow',
|
||||
condition: ({ instance }) => instance?.PatientName,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName) +
|
||||
(instance.PatientSex ? ' (' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
topLeftItems: {
|
||||
items: {
|
||||
// Note the -10000 means -10000 + length of existing list, which is
|
||||
// much before the start of hte list, so put the new value at the start.
|
||||
'-10000':
|
||||
{
|
||||
id: 'Species',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Species:',
|
||||
color: 'red',
|
||||
background: 'green',
|
||||
condition: ({ instance }) =>
|
||||
instance?.PatientSpeciesDescription,
|
||||
contentF: ({ instance }) =>
|
||||
instance.PatientSpeciesDescription +
|
||||
'/' +
|
||||
instance.PatientBreedDescription,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
<img src="../../../assets/img/customizable-overlay.jpeg" />
|
||||
|
||||
## Context Menus
|
||||
|
||||
Context menus can be created by defining the menu structure and click
|
||||
interaction, as defined in the `ContextMenu/types`. There are examples
|
||||
below specific to the cornerstone context, because the actual click
|
||||
handler and attributes used to decide when and how to display the menu
|
||||
are specific to the context used for where the menu is displayed.
|
||||
|
||||
## Cornerstone Context Menu
|
||||
|
||||
The default cornerstone context menu can be customized by setting the
|
||||
`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`.
|
||||
|
||||
## Customizable Cornerstone Viewport Click Behaviour
|
||||
|
||||
The behaviour on clicking on the cornerstone viewport can be customized
|
||||
by setting the `cornerstoneViewportClickCommands`. This is intended to
|
||||
support both the cornerstone 3D internal commands as well as things like
|
||||
context menus. Currently it supports buttons 1-3, as well as modifier keys
|
||||
by associating a commands list with the button to click. See `initContextMenu`
|
||||
for more details.
|
||||
|
||||
## Please add additional customizations above this section
|
||||
> 3rd Party implementers may be added to this table via pull requests.
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
|
||||
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
|
||||
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
|
||||
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -578,6 +578,16 @@ ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Nested bullet points */
|
||||
ul ul {
|
||||
list-style-type: circle;
|
||||
|
||||
@@ -67,7 +67,7 @@ customizationService.addModeCustomizations([
|
||||
customizationService.addModeCustomizations([
|
||||
// To disable editing in the SegmentationTable
|
||||
{
|
||||
id: 'PanelSegmentation.disableEditing',
|
||||
id: 'panelSegmentation.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
// To disable editing in the MeasurementTable
|
||||
@@ -106,11 +106,11 @@ customizationService.addModeCustomizations([
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
id: 'panelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
id: 'panelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
|
||||