feat: Add theme/config-point capability and docs. (#2641)
This is the initial addition of the config point settings to allow run time loading of GUI settings via "theme" files/parameters. The intent is to add an example PR that shows when to use config point, how to configure it, write documentation for it etc.
This commit is contained in:
parent
84d9ec34fc
commit
72bb074f21
@ -186,7 +186,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const viewportInfo = getEnabledElement(i);
|
const viewportInfo = getEnabledElement(i);
|
||||||
const hasCornerstoneContext =
|
const hasCornerstoneContext = viewportInfo.context &&
|
||||||
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
|
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
|
||||||
|
|
||||||
if (hasCornerstoneContext) {
|
if (hasCornerstoneContext) {
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.7.6",
|
"@babel/runtime": "7.7.6",
|
||||||
|
"config-point": "^0.3.4",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
"cornerstone-core": "2.6.0",
|
"cornerstone-core": "2.6.0",
|
||||||
"wslink": "^0.1.8"
|
"wslink": "^0.1.8"
|
||||||
@ -107,17 +108,17 @@
|
|||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
"react-hot-loader": "^4.13.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"serve": "^11.1.0",
|
"serve": "^11.1.0",
|
||||||
|
"shader-loader": "^1.3.1",
|
||||||
"start-server-and-test": "^1.10.0",
|
"start-server-and-test": "^1.10.0",
|
||||||
"style-loader": "^1.0.0",
|
"style-loader": "^1.0.0",
|
||||||
"terser-webpack-plugin": "^5.1.4",
|
"terser-webpack-plugin": "^5.1.4",
|
||||||
"unused-webpack-plugin": "2.4.0",
|
"unused-webpack-plugin": "2.4.0",
|
||||||
|
"webpack": "^5.50.0",
|
||||||
"webpack-cli": "^4.7.2",
|
"webpack-cli": "^4.7.2",
|
||||||
"webpack-dev-server": "^3.11.2",
|
"webpack-dev-server": "^3.11.2",
|
||||||
"webpack-hot-middleware": "^2.25.0",
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
"workbox-webpack-plugin": "^6.1.5",
|
"workbox-webpack-plugin": "^6.1.5",
|
||||||
"shader-loader": "^1.3.1",
|
|
||||||
"webpack": "^5.50.0",
|
|
||||||
"worker-loader": "^3.0.8"
|
"worker-loader": "^3.0.8"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
|
|||||||
@ -82,7 +82,7 @@ const StackManager = {
|
|||||||
return stackMap[displaySetInstanceUID];
|
return stackMap[displaySetInstanceUID];
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Find a stack or reate one if it has not been created yet
|
* Find a stack or create one if it has not been created yet
|
||||||
* @param displaySet The set of images to make the stack from
|
* @param displaySet The set of images to make the stack from
|
||||||
* @return {Array} Array with image IDs
|
* @return {Array} Array with image IDs
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,6 +10,20 @@ makes it easier for our community members to keep their "secret sauce" private,
|
|||||||
and incentives contributions back to the platform. The `@ohif/viewer` project of
|
and incentives contributions back to the platform. The `@ohif/viewer` project of
|
||||||
the platform is the lynchpin that combines everything to create our application.
|
the platform is the lynchpin that combines everything to create our application.
|
||||||
|
|
||||||
|
There are two configuration mechanisms, one for run-time configuration, allowing
|
||||||
|
for changes to be decided based on including different configuration files as
|
||||||
|
specified by the 'theme' URL parameters. This mechanism is intended for
|
||||||
|
modifications of data exposed as configurable items by the existing code. See
|
||||||
|
sections below on configuring this type of value.
|
||||||
|
|
||||||
|
The other mechanism is the code-configuration mechanism that specifies load
|
||||||
|
time configuration. This is intended to load things that require code level
|
||||||
|
changes to OHIF such as adding a new viewer configuration. This is also used
|
||||||
|
for base definitions that are shared site-wide such as the data sources. This
|
||||||
|
was the original configuration mechanism provided, and some of the configurations
|
||||||
|
specified there are better suited to the run time loading, but are currently
|
||||||
|
left alone as there hasn't been time to move them.
|
||||||
|
|
||||||
We maintain a number of common viewer application configurations at
|
We maintain a number of common viewer application configurations at
|
||||||
[`<root>/platform/viewer/public/configs`][config-dir].
|
[`<root>/platform/viewer/public/configs`][config-dir].
|
||||||
|
|
||||||
@ -59,6 +73,40 @@ window.config = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Run Time Configuration (Config-Point)
|
||||||
|
There is a library [config-point](https://github.com/OHIF/config-point)
|
||||||
|
used to allow loading of configuration values dynamically,
|
||||||
|
that is, at load time rather than being built into the runtime configuration.
|
||||||
|
A user of OHIF can specify a dynamic configuration by adding one or more theme
|
||||||
|
parameters, for example:
|
||||||
|
```
|
||||||
|
https://ohif.hospital.org/?theme=mgHP&theme=euroKeyboard
|
||||||
|
```
|
||||||
|
to load two hypothetical theme settings files mgHP and euroKeyboard to add
|
||||||
|
mammographic hanging protocols and European keyboard settings.
|
||||||
|
|
||||||
|
A site can add such settings by creating custom files in the deployment
|
||||||
|
directory (which is wherever the deployed OHIF is located.) For a deployment
|
||||||
|
running off a straight build of OHIF, this would be:
|
||||||
|
```
|
||||||
|
...Viewers/platform/viewer/dist/theme/mgHP.json5
|
||||||
|
...Viewers/platform/viewer/dist/theme/euroKeyboard.json5
|
||||||
|
```
|
||||||
|
A site might build such different themes to support various user preferences
|
||||||
|
or site differences between users, such as themes to support specific clinics
|
||||||
|
or differences in user groups such as left on right mammography viewing versus
|
||||||
|
right on left mammography viewing.
|
||||||
|
|
||||||
|
The decision to use the JSON5 parser for this was primarily aimed at allowing
|
||||||
|
comments in the configuration files, an important consideration for sites
|
||||||
|
wanting to document their settings.
|
||||||
|
|
||||||
|
See [theme-configuration](theme-configuration.md) for more details on the
|
||||||
|
specific configuration settings which can be applied.
|
||||||
|
|
||||||
|
See [config-point-service](../platform/services/config-point-service.md) for
|
||||||
|
information on how to add your own config-point based extensions to the code.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
LINKS
|
LINKS
|
||||||
-->
|
-->
|
||||||
|
|||||||
287
platform/docs/docs/configuration/theme-configuration.md
Normal file
287
platform/docs/docs/configuration/theme-configuration.md
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# Theme Configuration
|
||||||
|
When adding new theme extendible configuration items, please document
|
||||||
|
them here. See [Theme Configuration with Config Point](#configPoint) on how to modify certain types of
|
||||||
|
configuration values using the config-point defintions.
|
||||||
|
|
||||||
|
## Hanging Protocols
|
||||||
|
It is possible to customize the available hanging protocols by defining them
|
||||||
|
in a new theme file OR by defining the hanging protocols in a custom mode.
|
||||||
|
If done correctly, the mode defined hanging protocols are automatically
|
||||||
|
applied when using a particular view mode, allowing for further customization
|
||||||
|
by theme files.
|
||||||
|
|
||||||
|
The default hanging protocols for the cornerstone mode is defined in
|
||||||
|
themeProtocolProvider.js, in a configuration point named
|
||||||
|
`ThemeProtocols.protocols` To define a new
|
||||||
|
hanging protocol, it is possible to simply extend the protocols list with
|
||||||
|
a new definition, for example, in the file mgHP.json5, the following definition
|
||||||
|
would add a new hanging protocol:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
ThemeProtocols: {
|
||||||
|
protocols: {
|
||||||
|
MG: {
|
||||||
|
// This is a working HP, but isn't MG specific...
|
||||||
|
id: 'MG',
|
||||||
|
locked: true,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: '1x2',
|
||||||
|
createdDate: '2021-11-01T18:32:42.849Z',
|
||||||
|
modifiedDate: '2021-11-01T18:32:42.849Z',
|
||||||
|
availableTo: {},
|
||||||
|
editableBy: {},
|
||||||
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'NumberOfStudyRelatedSeries>1',
|
||||||
|
weight: 10,
|
||||||
|
attribute: 'NumberOfStudyRelatedSeries',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
name: 'OneByTwo',
|
||||||
|
viewportStructure: {
|
||||||
|
type: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewport: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
numberOfPriorsReferenced: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The MG protocol doesn't initially exist, so this would add a new hanging
|
||||||
|
protocol, which would be defined in the normal hanging protocol definition.
|
||||||
|
|
||||||
|
## Query List
|
||||||
|
One of the suggested areas for customization is the columns in the query table.
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Demographics Overlay
|
||||||
|
Another recommended change is to configure the demographics overlay using
|
||||||
|
themes to allow site or mode specific demographics overlays.
|
||||||
|
This should be done at several levels. An overall level, defining the system
|
||||||
|
defaults, and then an overlay for each mode type, to allow mode specific information
|
||||||
|
to be added to the general model. Note how that allows customizing at two
|
||||||
|
or more levels to specify only the required change (aspect oriented programming).
|
||||||
|
|
||||||
|
# <a name="configPoint" />Theme Configuration with Config Point
|
||||||
|
This section explains the syntax used for declaring various types of theme
|
||||||
|
configurations, as well as where to place theme files.
|
||||||
|
|
||||||
|
The configuration schema is based on the
|
||||||
|
[config-point](https://github.com/OHIF/config-point)
|
||||||
|
library. This library is design to allow developers to create configuration
|
||||||
|
points for their code by declaring static values, which can be modified externally.
|
||||||
|
See [config-point-service](../platform/services/config-point-service.md) for
|
||||||
|
internal details and development documentation.
|
||||||
|
|
||||||
|
## Theme Files
|
||||||
|
Theme files are simple json5 definition files,
|
||||||
|
available from the endpoint `https://ohif/theme/`
|
||||||
|
for the site deployment. How the files get there is the responsibility of the
|
||||||
|
site deployment. JSON5 is an extension of JSON,
|
||||||
|
for which the primary addition here is the ability to use comments within the
|
||||||
|
JSON structure. The attribute definition can also be an unquoted string when
|
||||||
|
it is a simple attribute value.
|
||||||
|
|
||||||
|
The default example themes are located in `Viewers/platform/viewer/public/theme/`,
|
||||||
|
for example, there is a theme there named 'theme.json5'. These files are
|
||||||
|
automatically included in a default distribution.
|
||||||
|
|
||||||
|
Their content looks like a static object declaration in JavaScript, where
|
||||||
|
the object has one or more attributes declared. The name of the attribute
|
||||||
|
matches the configuration point for the given configuration item. For example:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
// This extension modifies the ThemeProtocols
|
||||||
|
// by adding a new hanging protocol for MG and modifying the existing
|
||||||
|
// 2x2 layout to make it not preferred.
|
||||||
|
ThemeProtocols: {
|
||||||
|
// The original protocols declaration was a list,
|
||||||
|
// doing this as an object matches by the id value specified here.
|
||||||
|
// With no ID being found, creates a new entry.
|
||||||
|
protocols: {
|
||||||
|
// MG is just a referencable key, that matches the MG name here
|
||||||
|
// In this case, it does not match any existing id, so it is net new.
|
||||||
|
MG: {
|
||||||
|
id: 'MG',
|
||||||
|
// ... rest of definition of hanging protocol
|
||||||
|
},
|
||||||
|
// 2x2 is an existing hanging protocol, matching by id
|
||||||
|
// Thus, it modifies values rather than replacing/updating it
|
||||||
|
'2x2': {
|
||||||
|
protocolMatchingRules: {
|
||||||
|
// The actual change is weight:5 instead of weight:20, to make this
|
||||||
|
// a non-preferred hanging protocol according to the HP definitions.
|
||||||
|
'NumberOfStudyRelatedSeries>2': { weight: 5 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
that modifies the hanging protocols, both by adding and updating
|
||||||
|
elements. Note how this is a deep modification to a value. The intent is
|
||||||
|
to allow modifying configuration values which are heavily nested and/or list
|
||||||
|
based.
|
||||||
|
|
||||||
|
## Working with Objects
|
||||||
|
The object tree is matched by the simple attribute name. The attribute tree
|
||||||
|
is then merged with the existing objects. That is, suppose we have:
|
||||||
|
```js
|
||||||
|
// base object definition
|
||||||
|
baseObject: {
|
||||||
|
value1: 5,
|
||||||
|
value2: { subValue1: true, leftAlone: "value to be unchanged', },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
then value1 and subValue1 can be changed by the following theme configuration:
|
||||||
|
```js
|
||||||
|
// ... base object extension:
|
||||||
|
baseObject: {
|
||||||
|
value1: 7,
|
||||||
|
value2: {subValue1: false, subValue2: 'new value', },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
changing value1 to 7 and subValue1 to false, and adding subValue2.
|
||||||
|
|
||||||
|
This is the basic modification for all changes - match the path and replace one
|
||||||
|
or more left (primitive) values.
|
||||||
|
|
||||||
|
## Working with Arrays
|
||||||
|
Arrays in JSON and JSON5 only allow natural plus zero indices, and it isn't easy
|
||||||
|
to specify sparse array values or "next" values. To address this, a base
|
||||||
|
array, declared exactly as a normal array value can be extended with an
|
||||||
|
object where the key of the object matches the extension value in some way.
|
||||||
|
For example, the base array:
|
||||||
|
```js
|
||||||
|
array: ['value1', {id: 'value2', ...}, 'value3']
|
||||||
|
// can be extended with the changes in an array, modifying only
|
||||||
|
// the array[1] and adding an array[3] element.
|
||||||
|
array: [null, {...extensions for value2},null,{id:'new array element'}]
|
||||||
|
// Or, this can be re-written as:
|
||||||
|
array: {
|
||||||
|
'value1': 'new-string-for-value1',
|
||||||
|
'value2': {...extensions for value2},
|
||||||
|
'value4': {id:'new array element',...},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
matches value1 by simple comparison, getting `array[0]` as the value to change.
|
||||||
|
Then matching 'value2' to `array[1]` by `array[1].id==='value2'` and finally not
|
||||||
|
matching any value with `id==='new array element'` so adding it to the end
|
||||||
|
(warning, adding to the end MAY be replaced by adding in the middle of the
|
||||||
|
array such that the id is between an id smaller than the new id and larger
|
||||||
|
than the new id, still TBD along with one or two other small enhancements).
|
||||||
|
|
||||||
|
## Custom Mappings (configOperation)
|
||||||
|
There are a number of default custom mappings available for config-point.
|
||||||
|
The general format is:
|
||||||
|
```js
|
||||||
|
valueName: {configOperation: 'opName', value?: 'default-value', reference?: 'named-reference',
|
||||||
|
source?: 'name-of-config-point-for-reference', ...}
|
||||||
|
```
|
||||||
|
which defines an operation to perform instead of using the value literally.
|
||||||
|
The value provided can then be later extended/updated in the usual way, for
|
||||||
|
example:
|
||||||
|
```js
|
||||||
|
valueName: 'alternateStringValue for valueName',
|
||||||
|
```
|
||||||
|
would replace valueName that the config operation acts on.
|
||||||
|
|
||||||
|
### Immediate operations
|
||||||
|
Immediate operations perform the action immediately, and can thus be
|
||||||
|
extended further. They support the additional keys:
|
||||||
|
* position to modify something at a given position
|
||||||
|
|
||||||
|
The immediate operations are:
|
||||||
|
* replace, insert, delete
|
||||||
|
|
||||||
|
For example, supposing there was a configuration point ModalityList, then
|
||||||
|
the following extensions could be applied:
|
||||||
|
```js
|
||||||
|
// Replace the entire list
|
||||||
|
ModalityList: {configOperation: 'replace', value: [
|
||||||
|
'CT', 'MR', 'CR',
|
||||||
|
]}
|
||||||
|
|
||||||
|
// Replace an item at position 3
|
||||||
|
ModalityList: [ {configOperation: 'replace', position: 3, value: {id: 'MRI', description:'Magnetic resonance imaging'}}]
|
||||||
|
|
||||||
|
// Delete an item with value or id 'MR'
|
||||||
|
ModalityList: {
|
||||||
|
'MR': {configOperation: 'delete'},
|
||||||
|
|
||||||
|
// Insert before the item with id 'MG'
|
||||||
|
ModalityList: {
|
||||||
|
'MG': {configOperation: 'insert', value: 'MGTomo'},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getter Operations
|
||||||
|
Getter operations are performed at the time the attribute is accessed, and then
|
||||||
|
are stored. These attributes typically have parameters:
|
||||||
|
* reference to get a value relative to the current context or the source value.
|
||||||
|
* source to get a value for reference from another ConfigPoint root
|
||||||
|
* value to get a literal, immediate value
|
||||||
|
* transform to modify the value(s)
|
||||||
|
|
||||||
|
The default getter operations are:
|
||||||
|
* sort to generate a sorted list
|
||||||
|
* reference to get another value from elsewhere
|
||||||
|
but the configurations below may also list new getter operations.
|
||||||
|
|
||||||
|
An example for sort might be:
|
||||||
|
```js
|
||||||
|
// Define a basic list:
|
||||||
|
list: ['CT', {id:'MR', name: 'Magnetic Resonance Imaging'}, 'CR']
|
||||||
|
// Declared to be sorted like this (can be done before/after the base declaration)
|
||||||
|
list: {configOperation: 'sort', sortKey: 'priority', usePositionAsSortKey: true},
|
||||||
|
// Then extended via:
|
||||||
|
list: {
|
||||||
|
// Change the priority and add a name to the CT value
|
||||||
|
// This makes it an object instead of a string
|
||||||
|
'CT': {id:'CT', name:'Computed Tomography', priority: 3},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and and example for reference could be:
|
||||||
|
```js
|
||||||
|
// Base definition:
|
||||||
|
MGHangingProtocol: // ... full definition of MG HP here
|
||||||
|
// Reference to it
|
||||||
|
ThemeProtocols: {
|
||||||
|
protocols: {
|
||||||
|
MGHangingProtocol: {configOperation:reference, source:"MGHangingProtocol"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or, a bit of definition for a table element combining two values to render:
|
||||||
|
StudyInstancesColumn: {
|
||||||
|
id: 'StudyInstancesColumn',
|
||||||
|
title: '# of Series and Instances',
|
||||||
|
value: {
|
||||||
|
configOperation: 'reference',
|
||||||
|
value: '`Se: ${study.NumberOfStudyRelatedSeries} Obj: ${study.NumberOfStudyRelatedInstances}`',
|
||||||
|
transform: ${
|
||||||
|
configOperation: "reference",
|
||||||
|
source: 'ConfigPointOperation',
|
||||||
|
reference: 'safeFunction'}
|
||||||
|
```
|
||||||
|
Note how in the last example, the transform itself contains a reference. This
|
||||||
|
is a function that generates a javascript function taking props, where the
|
||||||
|
props are available directly. Thus, this props would need `study` containing
|
||||||
|
the appropriate child objects.
|
||||||
224
platform/docs/docs/platform/services/config-point-service.md
Normal file
224
platform/docs/docs/platform/services/config-point-service.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# Config Point Service
|
||||||
|
The Config Point service is based on the external library
|
||||||
|
[config-point](https://github.com/OHIF/config-point).
|
||||||
|
It is a service that allows exposing internal "static" configuration data
|
||||||
|
for modification by sites at load time by defining "theme" files. For
|
||||||
|
information on the configuration side of things, see [theme-configuration](../../configuration/theme-conffiguration.md).
|
||||||
|
|
||||||
|
The service isn't a traditional OHIF service avaialble in the services
|
||||||
|
deployment, but is rather a service which exposes static declarations of
|
||||||
|
data as configurable data. For example, suppose a list of modalities
|
||||||
|
was required for the search constraints. The core code might decide to
|
||||||
|
supply such a list by default, but sites may want to customize it to
|
||||||
|
only list the actual modalities they use. Further, they might want to
|
||||||
|
change the name of some of these. The core code could declare the modalities
|
||||||
|
list in a file like this:
|
||||||
|
```js
|
||||||
|
export default const { ModalitiesList } = ConfigPoint.register({
|
||||||
|
ModalitiesList: [
|
||||||
|
"CR",
|
||||||
|
{id: "MR", description: "Magnetic Resonance Imaging"},
|
||||||
|
{id: "CT", name: "Computed Tomography"},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
The CR modality is just a plain name, whereas MR includes a description,
|
||||||
|
and CT includes an alternate name. That list is used exactly as a straight
|
||||||
|
list for display, so the code needs to understand both simple strings and
|
||||||
|
the simple object definitions with id and/or name and description.
|
||||||
|
|
||||||
|
Now, a site might also happen to have an Ultrasound modality, so they would
|
||||||
|
want to extend the list. They could then follow the instructions in the
|
||||||
|
theme-configuration area to add the "US" device. One way of doing that is
|
||||||
|
by editing the `platform/viewer/public/theme/theme.json5` file, and adding
|
||||||
|
the following element to it:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
...
|
||||||
|
ModalitiesList: {
|
||||||
|
US: {id: 'US', name: 'Ultrasound'},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The intent of the config point service is to expose a configuration point
|
||||||
|
that can be further modified. The exposed point needs to be basically
|
||||||
|
a constant/static definition. This may involve reworking some of the
|
||||||
|
code design to extract the configuration value from the dynamic code. For
|
||||||
|
example, in the above modalities list, the original declaration is:
|
||||||
|
```js
|
||||||
|
inputProps: {
|
||||||
|
options: [
|
||||||
|
{ value: 'AR', label: 'AR' },
|
||||||
|
{ value: 'ASMT', label: 'ASMT' },
|
||||||
|
...
|
||||||
|
```
|
||||||
|
so the constant should be extracted to it's own file, but it is a fairly
|
||||||
|
simple list, so extracting it that way is fairly easy. A more complex example
|
||||||
|
might be the columns displayed in the search page. These are directly
|
||||||
|
referenced in the WorkList as bits of code that have both the configuration
|
||||||
|
and the ReactJs functionality. To allow configuring this, the change would
|
||||||
|
need to extract what should be displayed into a config point, from the actual
|
||||||
|
rendering logic to render a table.
|
||||||
|
|
||||||
|
A table rendering component might be defined externally, something like:
|
||||||
|
```js
|
||||||
|
import {patientInfoFilter} from './filtersMeta.js';
|
||||||
|
|
||||||
|
export default const {WorkListColumns} = ConfigPoint.register({
|
||||||
|
WorkListColumns: [
|
||||||
|
{ // This one has custom row and query rendering
|
||||||
|
id: 'PatientInfo',
|
||||||
|
rowRender: (props) => {... function to render a row },
|
||||||
|
queryRender: patientInfoFilter,
|
||||||
|
},
|
||||||
|
{ // This one defaults the row and query to fetching StudyDescription
|
||||||
|
id: 'Description',
|
||||||
|
rowData: 'StudyDescription',
|
||||||
|
}
|
||||||
|
... rest of columns
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
Note how this version includes functions as static data, as well as
|
||||||
|
the basic data structure. It would then need to be rendered by iterating
|
||||||
|
over the elements of the WorkListColumns.
|
||||||
|
|
||||||
|
Always the basic idea is that the code extracts a literal declaration of
|
||||||
|
data, defaulting to the base behaviour of the application, allowing it to
|
||||||
|
be exposed later to enhancements in a declarative fashion. The remaining
|
||||||
|
sections below simply expand on the idea with some more advanced concepts.
|
||||||
|
|
||||||
|
## Automatic Sorting of Data
|
||||||
|
Sometimes, data needs to be sorted in the provided order. The above
|
||||||
|
examples show how that can be done, but in other cases, it is desirable
|
||||||
|
to allow sorting the data, either with the initially provided sort order,
|
||||||
|
or with additional sort constraints. This can be done by adding a
|
||||||
|
configOperation to the literal value. The operation is applied at the
|
||||||
|
time the value is first retrieved, and then is cached (unless the value is
|
||||||
|
further updated, in which case it is re-calculated).
|
||||||
|
|
||||||
|
In the WorkListColumns example, this would look like:
|
||||||
|
```js
|
||||||
|
WorkListColumns: {
|
||||||
|
configOperation: 'sort',
|
||||||
|
sortKey: 'priority',
|
||||||
|
usePositionAsSortKey: true,
|
||||||
|
value: [ ... definition above of original columns]
|
||||||
|
},
|
||||||
|
```
|
||||||
|
This would sort the value in the original order it was defined in, and then
|
||||||
|
any news items would get sorted into position.
|
||||||
|
|
||||||
|
## Transform Data
|
||||||
|
It is possible that you want a transformation of the data. For example,
|
||||||
|
there might be a simple declarative form for a value, but you want a custom
|
||||||
|
version to be used for display. For example, setting default values for
|
||||||
|
missing attributes, or for turning a simple string into a full definition of
|
||||||
|
a name. Here is a possible example:
|
||||||
|
```js
|
||||||
|
// Declared as part of the base configuration, which is always included
|
||||||
|
// first
|
||||||
|
configBase: {
|
||||||
|
ModalitiesList: {
|
||||||
|
configOperation: 'reference',
|
||||||
|
transform: list => list.map(str => ({id:str, name:str, value:str})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Then, for this example, the same name is referenced to assign the value,
|
||||||
|
ModalitiesList: ["CR", "CT"],
|
||||||
|
```
|
||||||
|
This type of transform needs to be declared in code because of the need
|
||||||
|
to reference a function, but allows for having a uniform format, but
|
||||||
|
including an easy declarative form.
|
||||||
|
|
||||||
|
## Reference Data
|
||||||
|
Sometimes you want to separate long declarations into their own configuration
|
||||||
|
item. You can do this via the reference operation, for example:
|
||||||
|
```js
|
||||||
|
export default const { HangingProtocols } = ConfigPoint.register({
|
||||||
|
MGHangingProtocol: {
|
||||||
|
rightOnLeft: // Definition for MG Hanging Protocol right on left,
|
||||||
|
leftOnLeft: // Definition for MG hanging protocol left on left
|
||||||
|
},
|
||||||
|
CRHangingProtocol: // Defn for CR hanging protocol
|
||||||
|
...
|
||||||
|
HangingProtocols: {
|
||||||
|
default: // Definition of default HP
|
||||||
|
protocols: [
|
||||||
|
// Reference the child element rightOnLeft of MGHangingProtocols
|
||||||
|
{configOperation: 'reference', source: "MGHangingProtocols", reference: "rightOnLeft"},
|
||||||
|
// Just reference the entire object CRHangingProtocol
|
||||||
|
{configOperation: 'reference', source: 'CRHangingProtocol',},
|
||||||
|
// Reference default within the current config point instance
|
||||||
|
{configOperation: 'reference', reference: 'default',}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme Load Timing
|
||||||
|
The theme values are intended to be constants for a given instance of OHIF.
|
||||||
|
However, because the actual theme files are loaded at startup time, if an initial
|
||||||
|
render is performed before all the theme files have been fully loaded, it may
|
||||||
|
be necessary to re-render after all the theme loads have completed. There is
|
||||||
|
a listener service which can be used to listen for load events, and then to
|
||||||
|
re-render the display. It works like:
|
||||||
|
```js
|
||||||
|
if (!this._listenThemeProtocols) {
|
||||||
|
this._listenThemeProtocols = this.listenThemeProtocols.bind(this);
|
||||||
|
}
|
||||||
|
ConfigPoint.addLoadListener(ThemeProtocols, this._listenThemeProtocols);
|
||||||
|
```
|
||||||
|
|
||||||
|
This is NOT intended for listening for programatic changes to the configuration,
|
||||||
|
but is intended only for load time updates. Again, the idea is that the
|
||||||
|
configuration points are constants loaded from configuration files.
|
||||||
|
|
||||||
|
## Mode Changes to Configuration Point
|
||||||
|
If the config points are "constants", then one might ask how different modes
|
||||||
|
can end up apply different configuration values. The answer to that is to
|
||||||
|
add a new configuration point specific to that mode, and to change the referenced
|
||||||
|
name of which configuration gets used. For example, suppose a "Mammo Mode" wanted
|
||||||
|
to specify a bunch of customizations to configuration such as the set of
|
||||||
|
hanging protocols to apply. One way to do that is to have the mode specify
|
||||||
|
the name of the configuration, and then to use the initial/default configuration
|
||||||
|
as a base, and extend it, something like this:
|
||||||
|
```js
|
||||||
|
const {MGHangingProtocols} = ConfigPoint.register({
|
||||||
|
MGHangingProtocols: {
|
||||||
|
// The config base says to START with the value from the named
|
||||||
|
// config point as the base values.
|
||||||
|
configBase: 'HangingProtocols',
|
||||||
|
// Now, just extend the protocols list with the mg protocols.
|
||||||
|
protocols: {
|
||||||
|
mgProtocol1: ...
|
||||||
|
mgProtocol2: ...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
As an alternative to extending the protocols, they can be replaced via:
|
||||||
|
```js
|
||||||
|
protocols: {
|
||||||
|
configOperation: 'replace',
|
||||||
|
value: [
|
||||||
|
// mgProtocol1,
|
||||||
|
// mgProtocol2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The replace operation is an immediate operation, and allows the protocols
|
||||||
|
list to be extended in the normal fashion.
|
||||||
|
|
||||||
|
The MGHangingProtocols value would then just be set as the mode hanging
|
||||||
|
protocols object, and it would then be used directly. It could also have
|
||||||
|
been set by name instead of value, depending on the desired context.
|
||||||
|
|
||||||
|
The site can then extend the MGHangingProtocols in the usual way, by creating
|
||||||
|
custom theme files.
|
||||||
|
|
||||||
|
# Concluding Remarks
|
||||||
|
Use the configuration point service to extract things that might need to be
|
||||||
|
configured by some sites by extracting the data into a constant declaration,
|
||||||
|
and then register it with config point to expose it. Then, document
|
||||||
|
your configuration points in the [theme-configuration](../../configuration/theme-configuration.md)
|
||||||
|
guide. That will allow sites to make declarative configuration changes to
|
||||||
|
the values.
|
||||||
@ -57,6 +57,7 @@
|
|||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"@types/react": "^16.0.0",
|
"@types/react": "^16.0.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"config-point": "^0.3.4",
|
||||||
"core-js": "^3.16.1",
|
"core-js": "^3.16.1",
|
||||||
"cornerstone-math": "^0.1.9",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "6.0.2",
|
"cornerstone-tools": "6.0.2",
|
||||||
|
|||||||
3
platform/viewer/public/theme/theme.json5
Normal file
3
platform/viewer/public/theme/theme.json5
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
// Currently empty because nothing is configurable yet with themes
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
errorHandler
|
errorHandler
|
||||||
// utils,
|
// utils,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
import ConfigPoint from 'config-point';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
||||||
@ -49,6 +50,10 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
appConfig,
|
appConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load the default theme settings
|
||||||
|
const defaultTheme = config && config.defaultTheme || 'theme';
|
||||||
|
ConfigPoint.load(defaultTheme, '/theme', 'theme');
|
||||||
|
|
||||||
servicesManager.registerServices([
|
servicesManager.registerServices([
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
|
|||||||
29
yarn.lock
29
yarn.lock
@ -1114,7 +1114,7 @@
|
|||||||
core-js-pure "^3.15.0"
|
core-js-pure "^3.15.0"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@7.1.2", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
"@babel/runtime@7.1.2", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||||
version "7.7.6"
|
version "7.7.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
||||||
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
||||||
@ -5238,9 +5238,9 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001243:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001243:
|
||||||
version "1.0.30001244"
|
version "1.0.30001291"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001244.tgz#a6dc49ad5fa02d81d04373ec3f5ceabc3da06abf"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001291.tgz"
|
||||||
integrity sha512-Wb4UFZPkPoJoKKVfELPWytRzpemjP/s0pe22NriANru1NoI+5bGNxzKtk7edYL8rmCWTfQO8eRiF0pn1Dqzx7Q==
|
integrity sha512-roMV5V0HNGgJ88s42eE70sstqGW/gwFndosYrikHthw98N5tLnOTxFqMLQjZVRxTWFlJ4rn+MsgXrR7MDPY4jA==
|
||||||
|
|
||||||
capture-exit@^2.0.0:
|
capture-exit@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@ -5872,6 +5872,14 @@ config-chain@^1.1.11:
|
|||||||
ini "^1.3.4"
|
ini "^1.3.4"
|
||||||
proto-list "~1.2.1"
|
proto-list "~1.2.1"
|
||||||
|
|
||||||
|
config-point@^0.3.4:
|
||||||
|
version "0.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/config-point/-/config-point-0.3.4.tgz#ac4ec8f39432400b6f31b0fe5d6edcc18fe8650a"
|
||||||
|
integrity sha512-/5heLx9DqfZKduBF09w64FPxLzPBD+yYepNvGS6rexUVfIBCKiE0hQkALvriqEzX6MvEA5U2gxdrd+mQLlTCtw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.14.6"
|
||||||
|
json5 "^2.2.0"
|
||||||
|
|
||||||
configstore@^5.0.1:
|
configstore@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
||||||
@ -7000,9 +7008,9 @@ dezalgo@^1.0.0:
|
|||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
dicom-parser@^1.8.9:
|
dicom-parser@^1.8.9:
|
||||||
version "1.8.9"
|
version "1.8.11"
|
||||||
resolved "https://registry.yarnpkg.com/dicom-parser/-/dicom-parser-1.8.9.tgz#c69b036b8234df1cfad9978d0d6f0caad1582e74"
|
resolved "https://registry.yarnpkg.com/dicom-parser/-/dicom-parser-1.8.11.tgz#ffd86289a6b85bb0047379084b57fcc1b28fe7f8"
|
||||||
integrity sha512-OdLBIh460sy0aVeBZ/XSjbzEvXDS18XWxTdTinYmIHNi2jZfo6UuxscX4/VuYslduQ9LaeY6sdEXe9P9e6qlvA==
|
integrity sha512-KbxEDqv4Stynv5AxDh90+jY7MtPvI2uyOG62yD8zKu/pcZqoQYfPX44rB0ULjpxt6YIfXfajrs7B1L97RbjNsg==
|
||||||
|
|
||||||
dicomweb-client@^0.6.0:
|
dicomweb-client@^0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
@ -15480,11 +15488,16 @@ regenerate@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
|
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
|
||||||
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
|
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
|
||||||
|
|
||||||
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
|
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
|
||||||
version "0.13.7"
|
version "0.13.7"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||||
|
|
||||||
|
regenerator-runtime@^0.13.2:
|
||||||
|
version "0.13.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||||
|
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||||
|
|
||||||
regenerator-transform@^0.14.2:
|
regenerator-transform@^0.14.2:
|
||||||
version "0.14.5"
|
version "0.14.5"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
|
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user