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:
parent
32bb7e4c1e
commit
daa56a8449
@ -234,6 +234,7 @@ function PanelStudyBrowser({
|
|||||||
return (
|
return (
|
||||||
<StudyBrowser
|
<StudyBrowser
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
servicesManager={servicesManager}
|
||||||
activeTabName={activeTabName}
|
activeTabName={activeTabName}
|
||||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||||
|
|||||||
29
extensions/default/src/getCustomizationModule.tsx
Normal file
29
extensions/default/src/getCustomizationModule.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Note: this is an example of how the customization module can be used
|
||||||
|
* using the customization module. Below, we are adding a new custom route
|
||||||
|
* to the application at the path /custom and rendering a custom component
|
||||||
|
* Real world use cases of the having a custom route would be to add a
|
||||||
|
* custom page for the user to view their profile, or to add a custom
|
||||||
|
* page for login etc.
|
||||||
|
*/
|
||||||
|
export default function getCustomizationModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helloPage',
|
||||||
|
value: {
|
||||||
|
id: 'customRoutes',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/custom',
|
||||||
|
children: () => (
|
||||||
|
<h1 style={{ color: 'white' }}>Hello Custom Route</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import getToolbarModule from './getToolbarModule';
|
|||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||||
|
import getCustomizationModule from './getCustomizationModule';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import init from './init';
|
import init from './init';
|
||||||
|
|
||||||
@ -36,6 +37,8 @@ const defaultExtension = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCustomizationModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defaultExtension;
|
export default defaultExtension;
|
||||||
|
|||||||
@ -349,6 +349,7 @@ function PanelStudyBrowserTracking({
|
|||||||
return (
|
return (
|
||||||
<StudyBrowser
|
<StudyBrowser
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
servicesManager={servicesManager}
|
||||||
activeTabName={activeTabName}
|
activeTabName={activeTabName}
|
||||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||||
onClickStudy={_handleStudyClick}
|
onClickStudy={_handleStudyClick}
|
||||||
|
|||||||
@ -67,7 +67,13 @@ function modeFactory() {
|
|||||||
* Lifecycle hooks
|
* Lifecycle hooks
|
||||||
*/
|
*/
|
||||||
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
|
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
|
||||||
const { ToolBarService, ToolGroupService } = servicesManager.services;
|
const {
|
||||||
|
MeasurementService,
|
||||||
|
ToolBarService,
|
||||||
|
ToolGroupService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
MeasurementService.clearMeasurements();
|
||||||
|
|
||||||
// Init Default and SR ToolGroups
|
// Init Default and SR ToolGroups
|
||||||
initToolGroups(extensionManager, ToolGroupService, commandsManager);
|
initToolGroups(extensionManager, ToolGroupService, commandsManager);
|
||||||
|
|||||||
@ -40,13 +40,9 @@ export default class ExtensionManager {
|
|||||||
_extensionLifeCycleHooks,
|
_extensionLifeCycleHooks,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
const {
|
for (const service of Object.values(_servicesManager.services)) {
|
||||||
MeasurementService,
|
service?.onModeEnter?.();
|
||||||
ViewportGridService,
|
}
|
||||||
} = _servicesManager.services;
|
|
||||||
|
|
||||||
MeasurementService.clearMeasurements();
|
|
||||||
ViewportGridService.reset();
|
|
||||||
|
|
||||||
registeredExtensionIds.forEach(extensionId => {
|
registeredExtensionIds.forEach(extensionId => {
|
||||||
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
||||||
@ -69,13 +65,9 @@ export default class ExtensionManager {
|
|||||||
_extensionLifeCycleHooks,
|
_extensionLifeCycleHooks,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
const {
|
for (const service of Object.values(_servicesManager.services)) {
|
||||||
MeasurementService,
|
service?.onModeExit?.();
|
||||||
ViewportGridService,
|
}
|
||||||
} = _servicesManager.services;
|
|
||||||
|
|
||||||
MeasurementService.clearMeasurements();
|
|
||||||
ViewportGridService.reset();
|
|
||||||
|
|
||||||
registeredExtensionIds.forEach(extensionId => {
|
registeredExtensionIds.forEach(extensionId => {
|
||||||
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
||||||
@ -200,6 +192,7 @@ export default class ExtensionManager {
|
|||||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||||
case MODULE_TYPES.CONTEXT:
|
case MODULE_TYPES.CONTEXT:
|
||||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||||
|
case MODULE_TYPES.CUSTOMIZATION:
|
||||||
case MODULE_TYPES.UTILITY:
|
case MODULE_TYPES.UTILITY:
|
||||||
// Default for most extension points,
|
// Default for most extension points,
|
||||||
// Just adds each entry ready for consumption by mode.
|
// Just adds each entry ready for consumption by mode.
|
||||||
|
|||||||
@ -235,6 +235,9 @@ describe('ExtensionManager.js', () => {
|
|||||||
getUtilityModule: () => {
|
getUtilityModule: () => {
|
||||||
return [{}];
|
return [{}];
|
||||||
},
|
},
|
||||||
|
getCustomizationModule: () => {
|
||||||
|
return [{}];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await extensionManager.registerExtension(extension);
|
await extensionManager.registerExtension(extension);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
COMMANDS: 'commandsModule',
|
COMMANDS: 'commandsModule',
|
||||||
|
CUSTOMIZATION: 'customizationModule',
|
||||||
DATA_SOURCE: 'dataSourcesModule',
|
DATA_SOURCE: 'dataSourcesModule',
|
||||||
PANEL: 'panelModule',
|
PANEL: 'panelModule',
|
||||||
SOP_CLASS_HANDLER: 'sopClassHandlerModule',
|
SOP_CLASS_HANDLER: 'sopClassHandlerModule',
|
||||||
|
|||||||
@ -24,6 +24,7 @@ describe('Top level exports', () => {
|
|||||||
'OHIF',
|
'OHIF',
|
||||||
//
|
//
|
||||||
'CineService',
|
'CineService',
|
||||||
|
'CustomizationServiceRegistration',
|
||||||
'UIDialogService',
|
'UIDialogService',
|
||||||
'UIModalService',
|
'UIModalService',
|
||||||
'UINotificationService',
|
'UINotificationService',
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
|
CustomizationServiceRegistration,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||||
@ -57,6 +58,7 @@ const OHIF = {
|
|||||||
viewer: {},
|
viewer: {},
|
||||||
//
|
//
|
||||||
CineService,
|
CineService,
|
||||||
|
CustomizationServiceRegistration,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
@ -92,6 +94,7 @@ export {
|
|||||||
DICOMWeb,
|
DICOMWeb,
|
||||||
//
|
//
|
||||||
CineService,
|
CineService,
|
||||||
|
CustomizationServiceRegistration,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
|
|||||||
@ -0,0 +1,169 @@
|
|||||||
|
import CustomizationService from './CustomizationService';
|
||||||
|
import log from '../../log';
|
||||||
|
|
||||||
|
jest.mock('../../log.js', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const extensionManager = {
|
||||||
|
registeredExtensionIds: [],
|
||||||
|
moduleEntries: {},
|
||||||
|
|
||||||
|
getModuleEntry: function (id) {
|
||||||
|
return this.moduleEntries[id];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandsManager = {};
|
||||||
|
|
||||||
|
const ohifOverlayItem = {
|
||||||
|
id: 'ohif.overlayItem',
|
||||||
|
content: function (props) {
|
||||||
|
return {
|
||||||
|
label: this.label,
|
||||||
|
value: props[this.attribute],
|
||||||
|
ver: 'default',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const testItem = {
|
||||||
|
id: 'testItem',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
attribute: 'testAttribute',
|
||||||
|
label: 'testItemLabel',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CustomizationService.ts', () => {
|
||||||
|
let customizationService;
|
||||||
|
|
||||||
|
let configuration;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
log.warn.mockClear();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
configuration = {};
|
||||||
|
customizationService = new CustomizationService({
|
||||||
|
configuration,
|
||||||
|
commandsManager,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
it('init succeeds', () => {
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('configurationRegistered', () => {
|
||||||
|
configuration.testItem = testItem;
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
expect(customizationService.getGlobalCustomization('testItem')).toBe(
|
||||||
|
testItem
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaultRegistered', () => {
|
||||||
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
|
extensionManager.moduleEntries[
|
||||||
|
'@testExtension.customizationModule.default'
|
||||||
|
] = { name: 'default', value: [testItem] };
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
expect(customizationService.getGlobalCustomization('testItem')).toBe(
|
||||||
|
testItem
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('customizationType', () => {
|
||||||
|
it('inherits type', () => {
|
||||||
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
|
extensionManager.moduleEntries[
|
||||||
|
'@testExtension.customizationModule.default'
|
||||||
|
] = { name: 'default', value: [ohifOverlayItem] };
|
||||||
|
configuration.testItem = testItem;
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
const item = customizationService.getGlobalCustomization('testItem');
|
||||||
|
|
||||||
|
const props = { testAttribute: 'testAttrValue' };
|
||||||
|
const result = item.content(props);
|
||||||
|
expect(result.label).toBe(testItem.label);
|
||||||
|
expect(result.value).toBe(props.testAttribute);
|
||||||
|
expect(result.ver).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inline default inherits type', () => {
|
||||||
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
|
extensionManager.moduleEntries[
|
||||||
|
'@testExtension.customizationModule.default'
|
||||||
|
] = { name: 'default', value: [ohifOverlayItem] };
|
||||||
|
configuration.testItem = testItem;
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
const item = customizationService.getGlobalCustomization('testItem2', {
|
||||||
|
id: 'testItem2',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'otherLabel',
|
||||||
|
attribute: 'otherAttr',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Customizes the default value, as this is testItem2
|
||||||
|
const props = { otherAttr: 'other attribute value' };
|
||||||
|
const result = item.content(props);
|
||||||
|
expect(result.label).toBe('otherLabel');
|
||||||
|
expect(result.value).toBe(props.otherAttr);
|
||||||
|
expect(result.ver).toBe('default');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mode customization', () => {
|
||||||
|
it('onModeEnter can add extensions', () => {
|
||||||
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
|
extensionManager.moduleEntries[
|
||||||
|
'@testExtension.customizationModule.default'
|
||||||
|
] = { name: 'default', value: [ohifOverlayItem] };
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
customizationService.getModeCustomization('testItem')
|
||||||
|
).toBeUndefined();
|
||||||
|
|
||||||
|
customizationService.addModeCustomizations([testItem]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
customizationService.getGlobalCustomization('testItem')
|
||||||
|
).toBeUndefined();
|
||||||
|
|
||||||
|
const item = customizationService.getModeCustomization('testItem');
|
||||||
|
|
||||||
|
const props = { testAttribute: 'testAttrValue' };
|
||||||
|
const result = item.content(props);
|
||||||
|
expect(result.label).toBe(testItem.label);
|
||||||
|
expect(result.value).toBe(props.testAttribute);
|
||||||
|
expect(result.ver).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('global customizations override modes', () => {
|
||||||
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
|
extensionManager.moduleEntries[
|
||||||
|
'@testExtension.customizationModule.default'
|
||||||
|
] = { name: 'default', value: [ohifOverlayItem] };
|
||||||
|
configuration.testItem = testItem;
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
// Add a mode customization that would otherwise fail below
|
||||||
|
customizationService.addModeCustomizations([
|
||||||
|
{ ...testItem, label: 'other' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const item = customizationService.getModeCustomization('testItem');
|
||||||
|
|
||||||
|
const props = { testAttribute: 'testAttrValue' };
|
||||||
|
const result = item.content(props);
|
||||||
|
expect(result.label).toBe(testItem.label);
|
||||||
|
expect(result.value).toBe(props.testAttribute);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,268 @@
|
|||||||
|
import merge from 'lodash.merge';
|
||||||
|
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||||
|
import { Customization, NestedStrings, Obj } from './types';
|
||||||
|
|
||||||
|
const EVENTS = {
|
||||||
|
MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified',
|
||||||
|
GLOBAL_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:globalModified',
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenNestedStrings = (
|
||||||
|
strs: NestedStrings | string,
|
||||||
|
ret?: Record<string, string>
|
||||||
|
): Record<string, string> => {
|
||||||
|
if (!ret) ret = {};
|
||||||
|
if (!strs) return ret;
|
||||||
|
if (Array.isArray(strs)) {
|
||||||
|
for (const val of strs) {
|
||||||
|
flattenNestedStrings(val, ret);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret[strs] = strs;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CustomizationService allows for retrieving of custom components
|
||||||
|
* and configuration for mode and global values.
|
||||||
|
* The intent of the items is to provide a react component. This can be
|
||||||
|
* done by straight out providing an entire react component or else can be
|
||||||
|
* done by configuring a react component, or configuring a part of a react
|
||||||
|
* component. These are intended to be fairly indistinguishable in use of
|
||||||
|
* it, although the internals of how that is implemented may need to know
|
||||||
|
* about the customization service.
|
||||||
|
*
|
||||||
|
* A customization value can be:
|
||||||
|
* 1. React function, taking (React, props) and returning a rendered component
|
||||||
|
* For example, createLogoComponentFn renders a component logo for display
|
||||||
|
* 2. Custom UI component configuration, as defined by the component which uses it.
|
||||||
|
* For example, context menus define a complex structure allowing site-determined
|
||||||
|
* context menus to be set.
|
||||||
|
* 3. A string name, being the extension id for retrieving one of the above.
|
||||||
|
*
|
||||||
|
* The default values for the extension come from the app_config value 'whiteLabeling',
|
||||||
|
* The whiteLabelling can have lists of extensions to load for the default global and
|
||||||
|
* mode extensions. These are:
|
||||||
|
* 'globalExtensions' which is a list of extension id's to load for global values
|
||||||
|
* 'modeExtensions' which is a list of extension id's to load for mode values
|
||||||
|
* They default to the list ['*'] if not otherwise provided, which means to check
|
||||||
|
* every module for the given id and to load it/add it to the extensions.
|
||||||
|
*/
|
||||||
|
export default class CustomizationService extends PubSubService {
|
||||||
|
commandsManager: Record<string, unknown>;
|
||||||
|
extensionManager: Record<string, unknown>;
|
||||||
|
|
||||||
|
modeCustomizations: Record<string, Customization> = {};
|
||||||
|
globalCustomizations: Record<string, Customization> = {};
|
||||||
|
configuration: UICustomizationConfiguration;
|
||||||
|
|
||||||
|
constructor({ configuration, commandsManager }) {
|
||||||
|
super(EVENTS);
|
||||||
|
this.commandsManager = commandsManager;
|
||||||
|
this.configuration = configuration || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(extensionManager: ExtensionManager): void {
|
||||||
|
this.extensionManager = extensionManager;
|
||||||
|
this.initDefaults();
|
||||||
|
this.addReferences(this.configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
initDefaults(): void {
|
||||||
|
this.extensionManager.registeredExtensionIds.forEach(extensionId => {
|
||||||
|
const key = `${extensionId}.customizationModule.default`;
|
||||||
|
const defaultCustomizations = this.findExtensionValue(key);
|
||||||
|
if (!defaultCustomizations) return;
|
||||||
|
const { value } = defaultCustomizations;
|
||||||
|
this.addReference(value, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findExtensionValue(value: string): Obj | void {
|
||||||
|
const entry = this.extensionManager.getModuleEntry(value);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onModeEnter(): void {
|
||||||
|
super.reset();
|
||||||
|
this.modeCustomizations = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} interaction - can be undefined to run nothing
|
||||||
|
* @param {*} extraOptions to include in the commands run
|
||||||
|
*/
|
||||||
|
recordInteraction(
|
||||||
|
interaction: Customization | void,
|
||||||
|
extraOptions?: Record<string, unknown>
|
||||||
|
): void {
|
||||||
|
if (!interaction) return;
|
||||||
|
const commandsManager = this.commandsManager;
|
||||||
|
const { commands = [] } = interaction;
|
||||||
|
|
||||||
|
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||||
|
if (commandName) {
|
||||||
|
console.log('Running command', commandName);
|
||||||
|
commandsManager.runCommand(
|
||||||
|
commandName,
|
||||||
|
{
|
||||||
|
interaction,
|
||||||
|
...commandOptions,
|
||||||
|
...extraOptions,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn('No command name supplied in', interaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getModeCustomizations(): Record<string, Customization> {
|
||||||
|
return this.modeCustomizations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setModeCustomization(
|
||||||
|
customizationId: string,
|
||||||
|
customization: Customization
|
||||||
|
): void {
|
||||||
|
console.log('** Set mode customization', customizationId, customization);
|
||||||
|
this.modeCustomizations[customizationId] = merge(
|
||||||
|
this.modeCustomizations[customizationId] || {},
|
||||||
|
customization
|
||||||
|
);
|
||||||
|
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
|
||||||
|
buttons: this.modeCustomizations,
|
||||||
|
button: this.modeCustomizations[customizationId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mode customizations are changes to the behaviour of the extensions
|
||||||
|
* when running in a given mode. Reset clears mode customizations.
|
||||||
|
* Note that global customizations over-ride mode customizations.
|
||||||
|
* @param defautlValue to return if no customization specified.
|
||||||
|
*/
|
||||||
|
public getModeCustomization(
|
||||||
|
customizationId: string,
|
||||||
|
defaultValue?: Customization
|
||||||
|
): Customization | void {
|
||||||
|
const customization =
|
||||||
|
this.globalCustomizations[customizationId] ??
|
||||||
|
this.modeCustomizations[customizationId] ??
|
||||||
|
defaultValue;
|
||||||
|
return this.applyType(customization);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applies any inheritance due to UI Type customization */
|
||||||
|
public applyType(customization: Customization): Customization {
|
||||||
|
if (!customization) return customization;
|
||||||
|
const { customizationType } = customization;
|
||||||
|
if (!customizationType) return customization;
|
||||||
|
const parent = this.getModeCustomization(customizationType);
|
||||||
|
return parent
|
||||||
|
? Object.assign(Object.create(parent), customization)
|
||||||
|
: customization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addModeCustomizations(modeCustomizations): void {
|
||||||
|
if (!modeCustomizations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.addReferences(modeCustomizations, false);
|
||||||
|
|
||||||
|
this._broadcastModeCustomizationModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
_broadcastModeCustomizationModified(): void {
|
||||||
|
this._broadcastEvent(EVENTS.MODE_CUSTOMIZATION_MODIFIED, {
|
||||||
|
modeCustomizations: this.modeCustomizations,
|
||||||
|
globalCustomizations: this.globalCustomizations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Global customizations are those that affect parts of the GUI other than
|
||||||
|
* the modes. They include things like settings for the search screen.
|
||||||
|
* Reset does NOT clear global customizations.
|
||||||
|
*/
|
||||||
|
getGlobalCustomization(
|
||||||
|
id: string,
|
||||||
|
defaultValue?: Customization
|
||||||
|
): Customization | void {
|
||||||
|
return this.applyType(this.globalCustomizations[id] ?? defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobalCustomization(id: string, value: Customization): void {
|
||||||
|
console.log('*** Set global', id, value);
|
||||||
|
this.globalCustomizations[id] = value;
|
||||||
|
this._broadcastGlobalCustomizationModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setConfigGlobalCustomization(
|
||||||
|
configuration: AppConfigCustomization
|
||||||
|
): void {
|
||||||
|
this.globalCustomizations = {};
|
||||||
|
const keys = flattenNestedStrings(configuration.globalCustomizations);
|
||||||
|
this.readCustomizationTypes(
|
||||||
|
v => keys[v.name] && v.customization,
|
||||||
|
this.globalCustomizations
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO - iterate over customizations, loading them from the extension
|
||||||
|
// manager.
|
||||||
|
this._broadcastGlobalCustomizationModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
_broadcastGlobalCustomizationModified(): void {
|
||||||
|
this._broadcastEvent(EVENTS.GLOBAL_CUSTOMIZATION_MODIFIED, {
|
||||||
|
modeCustomizations: this.modeCustomizations,
|
||||||
|
globalCustomizations: this.globalCustomizations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single reference is either an an array, or a single customization value,
|
||||||
|
* whose id is the id in the object, or the parent id.
|
||||||
|
* This allows for general use to register customizationModule entries.
|
||||||
|
*/
|
||||||
|
addReference(
|
||||||
|
value?: Obj | Obj[] | string,
|
||||||
|
isGlobal = true,
|
||||||
|
id?: string
|
||||||
|
): void {
|
||||||
|
if (!value) return;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const extensionValue = this.findExtensionValue(value);
|
||||||
|
console.log('Adding extension values', value, extensionValue);
|
||||||
|
this.addReferences(extensionValue);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
this.addReferences(value, isGlobal);
|
||||||
|
} else {
|
||||||
|
const useId = value.id || id;
|
||||||
|
this[isGlobal ? 'setGlobalCustomization' : 'setModeCustomization'](
|
||||||
|
useId as string,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** References are:
|
||||||
|
* list of customizations, added in order
|
||||||
|
* object containing a customization id and value
|
||||||
|
* This format allows for the original whitelist format.
|
||||||
|
*/
|
||||||
|
addReferences(references?: Obj | Obj[], isGlobal = true): void {
|
||||||
|
if (!references) return;
|
||||||
|
if (Array.isArray(references)) {
|
||||||
|
references.forEach(item => {
|
||||||
|
this.addReference(item, isGlobal);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(references)) {
|
||||||
|
const value = references[key];
|
||||||
|
this.addReference(value, isGlobal, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
platform/core/src/services/CustomizationService/index.ts
Normal file
11
platform/core/src/services/CustomizationService/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import CustomizationService from './CustomizationService';
|
||||||
|
|
||||||
|
const CustomizationServiceRegistration = {
|
||||||
|
name: 'customizationService',
|
||||||
|
create: ({ configuration = {}, commandsManager }) => {
|
||||||
|
return new CustomizationService({ configuration, commandsManager });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomizationServiceRegistration;
|
||||||
|
export { CustomizationService, CustomizationServiceRegistration };
|
||||||
39
platform/core/src/services/CustomizationService/types.ts
Normal file
39
platform/core/src/services/CustomizationService/types.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Command from '../../types/Command';
|
||||||
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
export type Obj = Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface BaseCustomization extends Obj {
|
||||||
|
id: string;
|
||||||
|
customizationType?: string;
|
||||||
|
description?: string;
|
||||||
|
label?: string;
|
||||||
|
commands?: Command[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelCustomization extends BaseCustomization {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeCustomization extends BaseCustomization {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandCustomization extends BaseCustomization {
|
||||||
|
commands: Command[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Customization =
|
||||||
|
| BaseCustomization
|
||||||
|
| LabelCustomization
|
||||||
|
| CommandCustomization
|
||||||
|
| CodeCustomization;
|
||||||
|
|
||||||
|
export default Customization;
|
||||||
|
|
||||||
|
export type ComponentReturn = {
|
||||||
|
component: ComponentType;
|
||||||
|
props?: Obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NestedStrings = string[] | NestedStrings[];
|
||||||
@ -52,11 +52,19 @@ export default class ToolBarService {
|
|||||||
this.buttons = {};
|
this.buttons = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onModeEnter() {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} interaction
|
* @param {*} interaction - can be undefined to run nothing
|
||||||
|
* @param {*} options is an optional set of extra commandOptions
|
||||||
|
* used for calling the specified interaction. That is, the command is
|
||||||
|
* called with {...commandOptions,...options}
|
||||||
*/
|
*/
|
||||||
recordInteraction(interaction) {
|
recordInteraction(interaction, options) {
|
||||||
|
if (!interaction) return;
|
||||||
const commandsManager = this._commandsManager;
|
const commandsManager = this._commandsManager;
|
||||||
const { groupId, itemId, interactionType, commands } = interaction;
|
const { groupId, itemId, interactionType, commands } = interaction;
|
||||||
|
|
||||||
@ -64,7 +72,14 @@ export default class ToolBarService {
|
|||||||
case 'action': {
|
case 'action': {
|
||||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||||
if (commandName) {
|
if (commandName) {
|
||||||
commandsManager.runCommand(commandName, commandOptions, context);
|
commandsManager.runCommand(
|
||||||
|
commandName,
|
||||||
|
{
|
||||||
|
...commandOptions,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -171,6 +186,10 @@ export default class ToolBarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getButton(id) {
|
||||||
|
return this.buttons[id];
|
||||||
|
}
|
||||||
|
|
||||||
setButtons(buttons) {
|
setButtons(buttons) {
|
||||||
this.buttons = buttons;
|
this.buttons = buttons;
|
||||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||||
@ -24,6 +24,7 @@ class ViewportGridService {
|
|||||||
restoreCachedLayout: restoreCachedLayoutImplementation,
|
restoreCachedLayout: restoreCachedLayoutImplementation,
|
||||||
setLayout: setLayoutImplementation,
|
setLayout: setLayoutImplementation,
|
||||||
reset: resetImplementation,
|
reset: resetImplementation,
|
||||||
|
onModeExit: onModeExitImplementation,
|
||||||
set: setImplementation,
|
set: setImplementation,
|
||||||
}) {
|
}) {
|
||||||
if (getStateImplementation) {
|
if (getStateImplementation) {
|
||||||
@ -50,6 +51,9 @@ class ViewportGridService {
|
|||||||
if (restoreCachedLayoutImplementation) {
|
if (restoreCachedLayoutImplementation) {
|
||||||
this.serviceImplementation._restoreCachedLayout = restoreCachedLayoutImplementation;
|
this.serviceImplementation._restoreCachedLayout = restoreCachedLayoutImplementation;
|
||||||
}
|
}
|
||||||
|
if (onModeExitImplementation) {
|
||||||
|
this.serviceImplementation._onModeExit = onModeExitImplementation;
|
||||||
|
}
|
||||||
if (setImplementation) {
|
if (setImplementation) {
|
||||||
this.serviceImplementation._set = setImplementation;
|
this.serviceImplementation._set = setImplementation;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import guid from '../../utils/guid';
|
import guid from '../../utils/guid';
|
||||||
|
import * as Types from '../../Types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumer must implement:
|
* Consumer must implement:
|
||||||
@ -24,7 +25,7 @@ function subscribe(eventName, callback) {
|
|||||||
const listenerId = guid();
|
const listenerId = guid();
|
||||||
const subscription = { id: listenerId, callback };
|
const subscription = { id: listenerId, callback };
|
||||||
|
|
||||||
console.info(`Subscribing to '${eventName}'.`);
|
// console.info(`Subscribing to '${eventName}'.`);
|
||||||
if (Array.isArray(this.listeners[eventName])) {
|
if (Array.isArray(this.listeners[eventName])) {
|
||||||
this.listeners[eventName].push(subscription);
|
this.listeners[eventName].push(subscription);
|
||||||
} else {
|
} else {
|
||||||
@ -86,3 +87,21 @@ function _broadcastEvent(eventName, callbackProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Export a PubSubService class to be used instead of the individual items */
|
||||||
|
export class PubSubService {
|
||||||
|
constructor(EVENTS) {
|
||||||
|
this.EVENTS = EVENTS;
|
||||||
|
this.subscribe = subscribe;
|
||||||
|
this._broadcastEvent = _broadcastEvent;
|
||||||
|
this._unsubscribe = _unsubscribe;
|
||||||
|
this._isValidEvent = _isValidEvent;
|
||||||
|
this.listeners = {};
|
||||||
|
this.unsubscriptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.unsubscriptions.forEach(unsub => unsub());
|
||||||
|
this.unsubscriptions = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,12 +10,20 @@ import ToolBarService from './ToolBarService';
|
|||||||
import ViewportGridService from './ViewportGridService';
|
import ViewportGridService from './ViewportGridService';
|
||||||
import CineService from './CineService';
|
import CineService from './CineService';
|
||||||
import HangingProtocolService from './HangingProtocolService';
|
import HangingProtocolService from './HangingProtocolService';
|
||||||
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
|
import pubSubServiceInterface, {
|
||||||
|
PubSubService,
|
||||||
|
} from './_shared/pubSubServiceInterface';
|
||||||
import UserAuthenticationService from './UserAuthenticationService';
|
import UserAuthenticationService from './UserAuthenticationService';
|
||||||
|
import {
|
||||||
|
CustomizationService,
|
||||||
|
CustomizationServiceRegistration,
|
||||||
|
} from './CustomizationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
|
CustomizationService,
|
||||||
|
CustomizationServiceRegistration,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
@ -27,5 +35,6 @@ export {
|
|||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
CineService,
|
CineService,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
|
PubSubService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
};
|
};
|
||||||
|
|||||||
7
platform/core/src/types/Command.ts
Normal file
7
platform/core/src/types/Command.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Command {
|
||||||
|
commandName: string;
|
||||||
|
commandOptions?: Record<string, unknown>;
|
||||||
|
context?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Command;
|
||||||
@ -5,13 +5,21 @@ import {
|
|||||||
} from './StudyMetadata';
|
} from './StudyMetadata';
|
||||||
|
|
||||||
import Consumer from './Consumer';
|
import Consumer from './Consumer';
|
||||||
|
import { ExtensionManager } from '../extensions';
|
||||||
|
import { CustomizationService, PubSubService } from '../services';
|
||||||
import * as HangingProtocol from './HangingProtocol';
|
import * as HangingProtocol from './HangingProtocol';
|
||||||
|
import Command from './Command';
|
||||||
|
|
||||||
|
export * from '../services/CustomizationService/types';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
ExtensionManager,
|
||||||
HangingProtocol,
|
HangingProtocol,
|
||||||
StudyMetadata,
|
StudyMetadata,
|
||||||
SeriesMetadata,
|
SeriesMetadata,
|
||||||
InstanceMetadata,
|
InstanceMetadata,
|
||||||
Consumer,
|
Consumer,
|
||||||
|
PubSubService,
|
||||||
|
CustomizationService,
|
||||||
|
Command,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,6 +51,7 @@ By default, `OHIF-v3` registers the following services in the `appInit`.
|
|||||||
|
|
||||||
```js title="platform/viewer/src/appInit.js"
|
```js title="platform/viewer/src/appInit.js"
|
||||||
servicesManager.registerServices([
|
servicesManager.registerServices([
|
||||||
|
CustomizationService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
@ -86,9 +87,16 @@ export default {
|
|||||||
and the implementation of `ToolBarService` lies in the same folder at
|
and the implementation of `ToolBarService` lies in the same folder at
|
||||||
`./ToolbarSerivce.js`.
|
`./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
|
> 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
|
## Accessing Services
|
||||||
|
|
||||||
Throughout the app you can use `services` property of the service manager to
|
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
|
and the logic for your service shall be
|
||||||
|
|
||||||
```js title="extensions/customExtension/src/services/backEndService/index.js"
|
```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) {
|
export default function WrappedBackEndService(serviceManager) {
|
||||||
return {
|
return {
|
||||||
name: 'myService',
|
// Note the canonical name of lower camel case backEndService
|
||||||
|
name: 'backEndService',
|
||||||
create: ({ configuration = {} }) => {
|
create: ({ configuration = {} }) => {
|
||||||
return new backEndService(serviceManager);
|
return new BackEndService(serviceManager);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -149,8 +159,8 @@ export default function WrappedBackEndService(serviceManager) {
|
|||||||
|
|
||||||
with implementation of
|
with implementation of
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
export default class backEndService {
|
export default class BackEndService {
|
||||||
constructor(serviceManager) {
|
constructor(serviceManager) {
|
||||||
this.serviceManager = 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)
|
- [Hanging Protocol Service](../data/HangingProtocolService.md)
|
||||||
- [Toolbar Service](../data/ToolBarService.md)
|
- [Toolbar Service](../data/ToolBarService.md)
|
||||||
- [Measurement Service](../data/MeasurementService.md)
|
- [Measurement Service](../data/MeasurementService.md)
|
||||||
|
- [Customization Service](customization-service.md)
|
||||||
|
|
||||||
## Service Architecture
|
## Service Architecture
|
||||||
|
|
||||||
|
|||||||
@ -125,6 +125,17 @@ The following services is available in the `OHIF-v3`.
|
|||||||
cine
|
cine
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="./ui/customization-service">
|
||||||
|
CustomizationService
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>UI Service</td>
|
||||||
|
<td>
|
||||||
|
customizationService (NEW)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="./ui/ui-dialog-service">
|
<a href="./ui/ui-dialog-service">
|
||||||
|
|||||||
324
platform/docs/docs/platform/services/ui/customization-service.md
Normal file
324
platform/docs/docs/platform/services/ui/customization-service.md
Normal file
@ -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 -->
|
||||||
@ -26,8 +26,10 @@ const StudyBrowser = ({
|
|||||||
onDoubleClickThumbnail,
|
onDoubleClickThumbnail,
|
||||||
onClickUntrack,
|
onClickUntrack,
|
||||||
activeDisplaySetInstanceUIDs,
|
activeDisplaySetInstanceUIDs,
|
||||||
|
servicesManager,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('StudyBrowser');
|
const { t } = useTranslation('StudyBrowser');
|
||||||
|
const { customizationService } = servicesManager?.services || {};
|
||||||
|
|
||||||
const getTabContent = () => {
|
const getTabContent = () => {
|
||||||
const tabData = tabs.find(tab => tab.name === activeTabName);
|
const tabData = tabs.find(tab => tab.name === activeTabName);
|
||||||
@ -82,8 +84,13 @@ const StudyBrowser = ({
|
|||||||
const isActive = activeTabName === name;
|
const isActive = activeTabName === name;
|
||||||
const isDisabled = !studies.length;
|
const isDisabled = !studies.length;
|
||||||
// Apply the contrasting color for brighter button color visibility
|
// Apply the contrasting color for brighter button color visibility
|
||||||
// const color = isActive ? 'black' : 'default';
|
const classStudyBrowser = customizationService?.getModeCustomization(
|
||||||
const color = 'default';
|
'class:StudyBrowser'
|
||||||
|
) || {
|
||||||
|
true: 'default',
|
||||||
|
false: 'default',
|
||||||
|
};
|
||||||
|
const color = classStudyBrowser[`${isActive}`];
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={name}
|
key={name}
|
||||||
|
|||||||
@ -302,6 +302,7 @@ export function ViewportGridProvider({ children, service }) {
|
|||||||
setDisplaySetsForViewports,
|
setDisplaySetsForViewports,
|
||||||
setLayout,
|
setLayout,
|
||||||
reset,
|
reset,
|
||||||
|
onModeExit: reset,
|
||||||
setCachedLayout,
|
setCachedLayout,
|
||||||
restoreCachedLayout,
|
restoreCachedLayout,
|
||||||
set,
|
set,
|
||||||
|
|||||||
@ -3,6 +3,10 @@ window.config = {
|
|||||||
// whiteLabelling: {},
|
// whiteLabelling: {},
|
||||||
extensions: [],
|
extensions: [],
|
||||||
modes: [],
|
modes: [],
|
||||||
|
customizationService: {
|
||||||
|
// Shows a custom route -access via http://localhost:3000/custom
|
||||||
|
// helloPage: '@ohif/extension-default.customizationModule.helloPage',
|
||||||
|
},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
maxNumberOfWebWorkers: 4,
|
maxNumberOfWebWorkers: 4,
|
||||||
// below flag is for performance reasons, but it might not work for all servers
|
// below flag is for performance reasons, but it might not work for all servers
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
// whiteLabelling: {},
|
customizationService: [
|
||||||
|
'@ohif/extension-default.customizationModule.helloPage',
|
||||||
|
],
|
||||||
extensions: [],
|
extensions: [],
|
||||||
modes: [],
|
modes: [],
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
maxNumberOfWebWorkers: 3,
|
maxNumberOfWebWorkers: 4,
|
||||||
// below flag is for performance reasons, but it might not work for all servers
|
// below flag is for performance reasons, but it might not work for all servers
|
||||||
omitQuotationForMultipartRequest: true,
|
omitQuotationForMultipartRequest: true,
|
||||||
showLoadingIndicator: true,
|
showLoadingIndicator: true,
|
||||||
@ -51,25 +53,6 @@ window.config = {
|
|||||||
// Could use services manager here to bring up a dialog/modal if needed.
|
// Could use services manager here to bring up a dialog/modal if needed.
|
||||||
console.warn('test, navigate to https://ohif.org/');
|
console.warn('test, navigate to https://ohif.org/');
|
||||||
},
|
},
|
||||||
// whiteLabeling: {
|
|
||||||
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
|
||||||
// createLogoComponentFn: function (React) {
|
|
||||||
// return React.createElement(
|
|
||||||
// 'a',
|
|
||||||
// {
|
|
||||||
// target: '_self',
|
|
||||||
// rel: 'noopener noreferrer',
|
|
||||||
// className: 'text-purple-600 line-through',
|
|
||||||
// href: '/',
|
|
||||||
// },
|
|
||||||
// React.createElement('img',
|
|
||||||
// {
|
|
||||||
// src: './customLogo.svg',
|
|
||||||
// className: 'w-8 h-8',
|
|
||||||
// }
|
|
||||||
// ))
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
defaultDataSourceName: 'dicomweb',
|
defaultDataSourceName: 'dicomweb',
|
||||||
hotkeys: [
|
hotkeys: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
customizationService: [
|
customizationService: [
|
||||||
|
'@ohif/extension-default.customizationModule.helloPage',
|
||||||
|
{
|
||||||
|
id: 'class:StudyBrowser',
|
||||||
|
true: 'black',
|
||||||
|
false: 'default',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
extensions: [],
|
extensions: [],
|
||||||
modes: [],
|
modes: [],
|
||||||
|
|||||||
@ -52,16 +52,6 @@ function App({ config, defaultExtensions, defaultModes }) {
|
|||||||
const appConfigState = init.appConfig;
|
const appConfigState = init.appConfig;
|
||||||
const { routerBasename, modes, dataSources, oidc } = appConfigState;
|
const { routerBasename, modes, dataSources, oidc } = appConfigState;
|
||||||
|
|
||||||
// Use config to create routes
|
|
||||||
const appRoutes = createRoutes({
|
|
||||||
modes,
|
|
||||||
dataSources,
|
|
||||||
extensionManager,
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
hotkeysManager,
|
|
||||||
routerBasename,
|
|
||||||
});
|
|
||||||
const {
|
const {
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
@ -70,6 +60,7 @@ function App({ config, defaultExtensions, defaultModes }) {
|
|||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
CineService,
|
CineService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
|
customizationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
@ -89,6 +80,20 @@ function App({ config, defaultExtensions, defaultModes }) {
|
|||||||
|
|
||||||
let authRoutes = null;
|
let authRoutes = null;
|
||||||
|
|
||||||
|
// Should there be a generic call to init on the extension manager?
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
// Use config to create routes
|
||||||
|
const appRoutes = createRoutes({
|
||||||
|
modes,
|
||||||
|
dataSources,
|
||||||
|
extensionManager,
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
hotkeysManager,
|
||||||
|
routerBasename,
|
||||||
|
});
|
||||||
|
|
||||||
if (oidc) {
|
if (oidc) {
|
||||||
authRoutes = (
|
authRoutes = (
|
||||||
<OpenIdConnectRoutes
|
<OpenIdConnectRoutes
|
||||||
@ -115,9 +120,7 @@ App.propTypes = {
|
|||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
routerBasename: PropTypes.string.isRequired,
|
routerBasename: PropTypes.string.isRequired,
|
||||||
oidc: PropTypes.array,
|
oidc: PropTypes.array,
|
||||||
whiteLabeling: PropTypes.shape({
|
whiteLabeling: PropTypes.object,
|
||||||
createLogoComponentFn: PropTypes.func,
|
|
||||||
}),
|
|
||||||
extensions: PropTypes.array,
|
extensions: PropTypes.array,
|
||||||
}),
|
}),
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
CineService,
|
CineService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
|
CustomizationServiceRegistration,
|
||||||
// utils,
|
// utils,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
[CustomizationServiceRegistration, appConfig.customizationService],
|
||||||
ToolBarService,
|
ToolBarService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import NotFound from './NotFound';
|
|||||||
import buildModeRoutes from './buildModeRoutes';
|
import buildModeRoutes from './buildModeRoutes';
|
||||||
import PrivateRoute from './PrivateRoute';
|
import PrivateRoute from './PrivateRoute';
|
||||||
|
|
||||||
// TODO: Make these configurable
|
|
||||||
// TODO: Include "routes" debug route if dev build
|
// TODO: Include "routes" debug route if dev build
|
||||||
const bakedInRoutes = [
|
const bakedInRoutes = [
|
||||||
// WORK LIST
|
// WORK LIST
|
||||||
@ -24,10 +23,11 @@ const bakedInRoutes = [
|
|||||||
path: '/local',
|
path: '/local',
|
||||||
children: Local,
|
children: Local,
|
||||||
},
|
},
|
||||||
// NOT FOUND (404)
|
|
||||||
{ component: NotFound },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// NOT FOUND (404)
|
||||||
|
const notFoundRoute = { component: NotFound };
|
||||||
|
|
||||||
const createRoutes = ({
|
const createRoutes = ({
|
||||||
modes,
|
modes,
|
||||||
dataSources,
|
dataSources,
|
||||||
@ -47,7 +47,17 @@ const createRoutes = ({
|
|||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
const allRoutes = [...routes, ...bakedInRoutes];
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
const customRoutes = customizationService.getGlobalCustomization(
|
||||||
|
'customRoutes'
|
||||||
|
);
|
||||||
|
const allRoutes = [
|
||||||
|
...routes,
|
||||||
|
...(customRoutes?.routes || []),
|
||||||
|
...bakedInRoutes,
|
||||||
|
customRoutes?.notFoundRoute || notFoundRoute,
|
||||||
|
];
|
||||||
|
|
||||||
function RouteWithErrorBoundary({ route, ...rest }) {
|
function RouteWithErrorBoundary({ route, ...rest }) {
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user