feat(CustomizationService):Add a customization service (#2984)

* feat(customization):Add a customization service

Update UICustomizationService to v3-stable.

Also, adds an example customization for tab colors

* moved the customization service to ui category
This commit is contained in:
Bill Wallace authored and GitHub committed 2022-11-24 16:28:10 -05:00
1 parent 32bb7e4c1e
commit daa56a8449
33 files changed
+1031 -67

No files matched your search

@@ -51,6 +51,7 @@ By default, `OHIF-v3` registers the following services in the `appInit`.
```js title="platform/viewer/src/appInit.js"
servicesManager.registerServices([
CustomizationService,
UINotificationService,
UIModalService,
UIDialogService,
@@ -86,9 +87,16 @@ export default {
and the implementation of `ToolBarService` lies in the same folder at
`./ToolbarSerivce.js`.
> Note, the create method is critical for any custom service that you write and
> Note: The create method is critical for any custom service that you write and
> want to add to the list of services
> Note: For typescript definitions, the service type should be exported
> as part of the Types export on the module. This is recommended going forward
> and existing services will be migrated. As well, the capitalization of the
> name should be lower camel case, with the type being upper camel case. In
> the above example, the service instance should be `toolBarService` with the
> class being `ToolBarService`.
## Accessing Services
Throughout the app you can use `services` property of the service manager to
@@ -135,13 +143,15 @@ export default {
and the logic for your service shall be
```js title="extensions/customExtension/src/services/backEndService/index.js"
import backEndService from './backEndService';
// Canonical name of upper camel case BackEndService for the class
import BackEndService from './BackEndService';
export default function WrappedBackEndService(serviceManager) {
return {
name: 'myService',
// Note the canonical name of lower camel case backEndService
name: 'backEndService',
create: ({ configuration = {} }) => {
return new backEndService(serviceManager);
return new BackEndService(serviceManager);
},
};
}
@@ -149,8 +159,8 @@ export default function WrappedBackEndService(serviceManager) {
with implementation of
```js
export default class backEndService {
```ts
export default class BackEndService {
constructor(serviceManager) {
this.serviceManager = serviceManager;
}
@@ -160,3 +170,11 @@ export default class backEndService {
}
}
```
with a registration of
```ts title="types/index.ts"
import BackEndService from "../services/BackEndService/BackEndService";
export { BackEndService };
```
@@ -19,6 +19,7 @@ We maintain the following non-ui Services:
- [Hanging Protocol Service](../data/HangingProtocolService.md)
- [Toolbar Service](../data/ToolBarService.md)
- [Measurement Service](../data/MeasurementService.md)
- [Customization Service](customization-service.md)
## Service Architecture
@@ -125,6 +125,17 @@ The following services is available in the `OHIF-v3`.
cine
</td>
</tr>
<tr>
<td>
<a href="./ui/customization-service">
CustomizationService
</a>
</td>
<td>UI Service</td>
<td>
customizationService (NEW)
</td>
</tr>
<tr>
<td>
<a href="./ui/ui-dialog-service">
@@ -0,0 +1,324 @@
---
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.
## Registering 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. For example, the fictitious
customization 'customIcons' might be defined as below in the APP_CONFIG:
```js
window.config = {
...,
customizationService: [
{
id: 'customIcons',
backArrow: 'https://customIcons.org/backArrow.svg',
},
],
...
}
```
As well, extensions can register default customizations by providing a 'default'
name key within the extension. These are simply customizations loaded when
the extension is loaded. For example, the previous customization could have
been added in an extension as:
```js
getCustomizationModule: () => [
{
name: 'default',
value: {
id: 'customIcons',
backArrow: 'https://customIcons.org/backArrow.svg',
},
},
],
```
Note the name of this is default (thus loaded automatically instead of by
reference), and the value is a customization of customIcons.
The type and parameters of a customization are defined by the user of the
customization, based on the customization id. For example, `cornerstoneOverlay`
is a customization that is a React component, so it requires a react content,
and optionally contentProps which are used to supply values to the content.
The extension can also supply a default parent instance to inherit values from.
This allows the content or other parameters to be pre-filled, and only the
required values changed. The parent to use is specified by the `customizationType` field,
and is simply the id of another customization object. An example of this might
be a demographics overlay field, where the base version needs an actual component,
while the typed version just needs the attribute and label to use.
```js
getCustomizationModule: () => [
{
name: 'default',
value: [
// This first value defines the base type
{
id: imageDemographicOverlay,
content: function({image}) {
return (<p>{image[this.attribute]}</p>);
}
},
// The second one defines an instance.
// It may or may not use the previous type definition - it will use it
// if nothing replaces the previous definition, otherwise it will use the new one.
{
id: PatientIDOverlayItem,
customizationType: 'imageDemographicOverlay',
attribute: 'PatientID',
},
]
}
]
```
Mode-specific customizations are no different from the global ones,
except that the mode customizations are cleared before the mode `onModeEnter`
is called, and they can have new values registered in the `onModeEnter`
The following example shows first the registration of the default instances,
and then shows how they might be used.
```js
// In the cornerstone extension getCustomizationModule:
const getCustomizationModule = () => ([
{
name: 'default',
value: [
{
id: 'ohif.cornerstoneOverlay',
content: CornerstoneOverlay,
// Requires items on instances
},
{
id: 'ohif.overlayItem',
content: CornerstoneOverlayItem,
// Requires attribute and label on instances
},
],
},
]);
```
Then, in the configuration file one might have a custom overlay definition:
```js
// 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',
customizationType: 'ohif.cornerstoneOverlay',
items: [
{
id: 'PatientNameOverlay',
// Note the ohif.overlayItem is a prototype instance for this object
// The ohif.overlayItem is defined up above
customizationType: 'ohif.overlayItem',
attribute: 'PatientName',
label: 'PN:',
},
],
},
},
```
In the mode customization, the overlay is then further customized
with a bottom-right overlay, which extends the customizationService configuration.
```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 hte context menu here.
],
};
customizationService.addModeCustomizations(bottomRight);
```
## Mode Customizations
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
cornerstoneOverlay = uiConfigurationService.getModeCustomization("cornerstoneOverlay", {customizationType: "ohif.cornerstoneOverlay", ...});
const { component: overlayComponent, props} = uiConfigurationService.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 = uiConfigurationService.getModeCustomization("cornerstoneContextMenu", defaultMenu);
uiConfigurationService.recordInteraction(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.UIContextMenu =
{
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` fo rhte 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',
],
```
> 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 -->