feat(customization): new customization service api (#4688)

This commit is contained in:
Alireza authored and GitHub committed 2025-01-23 14:24:13 -05:00
1 parent fab52808cb
commit 55ad8efbab
124 files changed
+4261 -2517

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

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.
![](../../../assets/img/viewportOverlay-customization.png)
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 -->
+10
View File
@@ -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');
},