feat: customization service append and customize functionality should run once (#4238)
This commit is contained in:
parent
88575c6c09
commit
e462fd31f7
@ -1,15 +1,11 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
|
import type { ServicesManager } from '@ohif/core';
|
||||||
|
|
||||||
function WorkflowPanel({ servicesManager, extensionManager }) {
|
function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }) {
|
||||||
const ProgressDropdownWithService = useMemo(() => {
|
const ProgressDropdownWithService =
|
||||||
const defaultComponents = extensionManager.getModuleEntry(
|
servicesManager.services.customizationService.getCustomization(
|
||||||
'@ohif/extension-default.customizationModule.default'
|
'progressDropdownWithServiceComponent'
|
||||||
).value;
|
|
||||||
|
|
||||||
return defaultComponents.find(
|
|
||||||
component => component.id === 'progressDropdownWithServiceComponent'
|
|
||||||
).component;
|
).component;
|
||||||
}, [extensionManager]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -3,7 +3,6 @@ const pkg = require('./package');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
name: pkg.name,
|
|
||||||
displayName: pkg.name,
|
displayName: pkg.name,
|
||||||
// rootDir: "../.."
|
// rootDir: "../.."
|
||||||
// testMatch: [
|
// testMatch: [
|
||||||
|
|||||||
@ -3,7 +3,6 @@ const pkg = require('./package');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
name: pkg.name,
|
|
||||||
displayName: pkg.name,
|
displayName: pkg.name,
|
||||||
setupFilesAfterEnv: ['<rootDir>/src/__tests__/globalSetup.js'],
|
setupFilesAfterEnv: ['<rootDir>/src/__tests__/globalSetup.js'],
|
||||||
// rootDir: "../.."
|
// rootDir: "../.."
|
||||||
|
|||||||
@ -3,7 +3,6 @@ const pkg = require('./package');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
name: pkg.name,
|
|
||||||
displayName: pkg.name,
|
displayName: pkg.name,
|
||||||
// rootDir: "../.."
|
// rootDir: "../.."
|
||||||
// testMatch: [
|
// testMatch: [
|
||||||
|
|||||||
@ -130,6 +130,10 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRegisteredExtensionIds() {
|
||||||
|
return [...this.registeredExtensionIds];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls all the services and extension on mode enters.
|
* Calls all the services and extension on mode enters.
|
||||||
* The service onModeEnter is called first
|
* The service onModeEnter is called first
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import CustomizationService from './CustomizationService';
|
import CustomizationService, { CustomizationType, MergeEnum } from './CustomizationService';
|
||||||
import log from '../../log';
|
import log from '../../log';
|
||||||
|
|
||||||
jest.mock('../../log.js', () => ({
|
jest.mock('../../log.js', () => ({
|
||||||
@ -11,6 +11,8 @@ const extensionManager = {
|
|||||||
registeredExtensionIds: [],
|
registeredExtensionIds: [],
|
||||||
moduleEntries: {},
|
moduleEntries: {},
|
||||||
|
|
||||||
|
getRegisteredExtensionIds: () => extensionManager.registeredExtensionIds,
|
||||||
|
|
||||||
getModuleEntry: function (id) {
|
getModuleEntry: function (id) {
|
||||||
return this.moduleEntries[id];
|
return this.moduleEntries[id];
|
||||||
},
|
},
|
||||||
@ -49,6 +51,8 @@ describe('CustomizationService.ts', () => {
|
|||||||
configuration,
|
configuration,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
});
|
});
|
||||||
|
extensionManager.registeredExtensionIds = [];
|
||||||
|
extensionManager.moduleEntries = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
@ -101,7 +105,7 @@ describe('CustomizationService.ts', () => {
|
|||||||
configuration.testItem = testItem;
|
configuration.testItem = testItem;
|
||||||
customizationService.init(extensionManager);
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
const item = customizationService.getGlobalCustomization('testItem2', {
|
const item = customizationService.getCustomization('testItem2', {
|
||||||
id: 'testItem2',
|
id: 'testItem2',
|
||||||
customizationType: 'ohif.overlayItem',
|
customizationType: 'ohif.overlayItem',
|
||||||
label: 'otherLabel',
|
label: 'otherLabel',
|
||||||
@ -133,6 +137,7 @@ describe('CustomizationService.ts', () => {
|
|||||||
expect(customizationService.getGlobalCustomization('testItem')).toBeUndefined();
|
expect(customizationService.getGlobalCustomization('testItem')).toBeUndefined();
|
||||||
|
|
||||||
const item = customizationService.getModeCustomization('testItem');
|
const item = customizationService.getModeCustomization('testItem');
|
||||||
|
expect(item).not.toBeUndefined();
|
||||||
|
|
||||||
const props = { testAttribute: 'testAttrValue' };
|
const props = { testAttribute: 'testAttrValue' };
|
||||||
const result = item.content(props);
|
const result = item.content(props);
|
||||||
@ -143,7 +148,7 @@ describe('CustomizationService.ts', () => {
|
|||||||
|
|
||||||
it('global customizations override modes', () => {
|
it('global customizations override modes', () => {
|
||||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
extensionManager.moduleEntries['@testExtension.customizationModule.global'] = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
value: [ohifOverlayItem],
|
value: [ohifOverlayItem],
|
||||||
};
|
};
|
||||||
@ -160,5 +165,149 @@ describe('CustomizationService.ts', () => {
|
|||||||
expect(result.label).toBe(testItem.label);
|
expect(result.label).toBe(testItem.label);
|
||||||
expect(result.value).toBe(props.testAttribute);
|
expect(result.value).toBe(props.testAttribute);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('mode customizations override default', () => {
|
||||||
|
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||||
|
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
||||||
|
name: 'default',
|
||||||
|
value: [ohifOverlayItem, testItem],
|
||||||
|
};
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
// Add a mode customization that would otherwise fail below
|
||||||
|
customizationService.addModeCustomizations([{ ...testItem, label: 'other' }]);
|
||||||
|
|
||||||
|
const item = customizationService.getCustomization('testItem');
|
||||||
|
|
||||||
|
const props = { testAttribute: 'testAttrValue' };
|
||||||
|
const result = item.content(props);
|
||||||
|
expect(result.label).toBe('other');
|
||||||
|
expect(result.value).toBe(props.testAttribute);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('merge', () => {
|
||||||
|
it('appends to global configuration', () => {
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
customizationService.setGlobalCustomization('appendSet', {
|
||||||
|
values: [{ id: 'one' }, { id: 'two' }],
|
||||||
|
});
|
||||||
|
const appendSet = customizationService.getCustomization('appendSet');
|
||||||
|
expect(appendSet.values.length).toBe(2);
|
||||||
|
|
||||||
|
customizationService.setGlobalCustomization(
|
||||||
|
'appendSet',
|
||||||
|
{
|
||||||
|
values: [{ id: 'three' }],
|
||||||
|
},
|
||||||
|
MergeEnum.Append
|
||||||
|
);
|
||||||
|
const appendSet2 = customizationService.getCustomization('appendSet');
|
||||||
|
expect(appendSet2.values.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends mode to default without touching default', () => {
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
customizationService.setDefaultCustomization('appendSet', {
|
||||||
|
values: [{ id: 'one' }, { id: 'two' }],
|
||||||
|
});
|
||||||
|
const appendSet = customizationService.get('appendSet');
|
||||||
|
expect(appendSet.values.length).toBe(2);
|
||||||
|
|
||||||
|
customizationService.setModeCustomization(
|
||||||
|
'appendSet',
|
||||||
|
{
|
||||||
|
values: [{ id: 'three' }],
|
||||||
|
},
|
||||||
|
MergeEnum.Append
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(appendSet.values.length).toBe(2);
|
||||||
|
const appendSet2 = customizationService.getModeCustomization('appendSet');
|
||||||
|
expect(appendSet2.values.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges values by name/position', () => {
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
customizationService.setDefaultCustomization('appendSet', {
|
||||||
|
values: [{ id: 'one', obj: { v: '5' }, list: [1, 2, 3] }, { id: 'two' }],
|
||||||
|
});
|
||||||
|
const appendSet = customizationService.get('appendSet');
|
||||||
|
expect(appendSet.values.length).toBe(2);
|
||||||
|
|
||||||
|
customizationService.setModeCustomization(
|
||||||
|
'appendSet',
|
||||||
|
{
|
||||||
|
values: [{ id: 'three', obj: { v: 2 }, list: [3, 2, 1, 4] }],
|
||||||
|
},
|
||||||
|
MergeEnum.Merge,
|
||||||
|
);
|
||||||
|
|
||||||
|
const appendSet2 = customizationService.get('appendSet');
|
||||||
|
const [value0] = appendSet2.values;
|
||||||
|
expect(value0.id).toBe('three');
|
||||||
|
expect(value0.list).toEqual([3, 2, 1, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges functions', () => {
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
customizationService.setDefaultCustomization('appendSet', {
|
||||||
|
values: [{ f: () => 0, id: '0' }, { f: () => 5, id: '5' }],
|
||||||
|
});
|
||||||
|
const appendSet = customizationService.get('appendSet');
|
||||||
|
expect(appendSet.values.length).toBe(2);
|
||||||
|
|
||||||
|
customizationService.setModeCustomization(
|
||||||
|
'appendSet',
|
||||||
|
{
|
||||||
|
values: [{ f: () => 2, id: '2' }]
|
||||||
|
},
|
||||||
|
MergeEnum.Merge,
|
||||||
|
);
|
||||||
|
|
||||||
|
const appendSet2 = customizationService.get('appendSet');
|
||||||
|
const [value0, value1] = appendSet2.values;
|
||||||
|
expect(value0.f()).toBe(2);
|
||||||
|
expect(value1.f()).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges list with object', () => {
|
||||||
|
customizationService.init(extensionManager);
|
||||||
|
|
||||||
|
const destination = [
|
||||||
|
1,
|
||||||
|
{ id: 'two', value: 2, list: [5, 6], },
|
||||||
|
{ id: 'three', value: 3 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const source = {
|
||||||
|
two: { value: 'updated2', list: { 0: 8 } },
|
||||||
|
1: { extraValue: 2, list: [7], },
|
||||||
|
1.0001: { id: 'inserted', value: 1.0001 },
|
||||||
|
'-1': {
|
||||||
|
value: -3
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
customizationService.setDefaultCustomization('appendSet', {
|
||||||
|
values: destination,
|
||||||
|
});
|
||||||
|
customizationService.setModeCustomization('appendSet', {
|
||||||
|
values: source,
|
||||||
|
}, MergeEnum.Append);
|
||||||
|
|
||||||
|
const { values } = customizationService.getCustomization('appendSet');
|
||||||
|
const [zero, one, two, three] = values;
|
||||||
|
expect(zero).toBe(1);
|
||||||
|
expect(one.value).toBe('updated2');
|
||||||
|
expect(one.extraValue).toBe(2);
|
||||||
|
expect(one.list).toEqual([8, 6, 7]);
|
||||||
|
expect(two.id).toBe('inserted');
|
||||||
|
expect(three.value).toBe(-3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import mergeWith from 'lodash.mergewith';
|
import { mergeWith, cloneDeepWith } from 'lodash';
|
||||||
|
|
||||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||||
import { Customization, NestedStrings } from './types';
|
import type { Customization, NestedStrings } from './types';
|
||||||
import { CommandsManager } from '../../classes';
|
import type { CommandsManager } from '../../classes';
|
||||||
|
import type { ExtensionManager } from '../../extensions';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified',
|
MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified',
|
||||||
@ -43,6 +45,12 @@ export enum MergeEnum {
|
|||||||
Replace = 'Replace',
|
Replace = 'Replace',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CustomizationType {
|
||||||
|
Global = 'Global',
|
||||||
|
Mode = 'Mode',
|
||||||
|
Default = 'Default',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CustomizationService allows for retrieving of custom components
|
* The CustomizationService allows for retrieving of custom components
|
||||||
* and configuration for mode and global values.
|
* and configuration for mode and global values.
|
||||||
@ -78,10 +86,34 @@ export default class CustomizationService extends PubSubService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
commandsManager: CommandsManager;
|
commandsManager: CommandsManager;
|
||||||
extensionManager: Record<string, unknown>;
|
extensionManager: ExtensionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mode customizations are changes to the default behaviour which are reset
|
||||||
|
* every time a new mode is entered. This allows the mode to define custom
|
||||||
|
* behaviour, and not interfere with other modes.
|
||||||
|
*/
|
||||||
|
private modeCustomizations = new Map<string, Customization>();
|
||||||
|
/**
|
||||||
|
* global customizations, are customizations which are set as a global default
|
||||||
|
* This allows changes across the board to be applied, essentially as a priority
|
||||||
|
* setting.
|
||||||
|
*/
|
||||||
|
private globalCustomizations = new Map<string, Customization>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default customizations allow applying default values. The intent is that
|
||||||
|
* there is only one customization of that type, and it is registered at setup
|
||||||
|
* time.
|
||||||
|
*/
|
||||||
|
private defaultCustomizations = new Map<string, Customization>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has the transformed/final customization value. This avoids needing to
|
||||||
|
* transform every time a customization is requested.
|
||||||
|
*/
|
||||||
|
private transformedCustomizations = new Map<string, Customization>();
|
||||||
|
|
||||||
modeCustomizations: Record<string, Customization> = {};
|
|
||||||
globalCustomizations: Record<string, Customization> = {};
|
|
||||||
configuration: any;
|
configuration: any;
|
||||||
|
|
||||||
constructor({ configuration, commandsManager }) {
|
constructor({ configuration, commandsManager }) {
|
||||||
@ -92,33 +124,42 @@ export default class CustomizationService extends PubSubService {
|
|||||||
|
|
||||||
public init(extensionManager: ExtensionManager): void {
|
public init(extensionManager: ExtensionManager): void {
|
||||||
this.extensionManager = extensionManager;
|
this.extensionManager = extensionManager;
|
||||||
|
// Clear defaults as those are defined by the customization modules
|
||||||
|
this.defaultCustomizations.clear();
|
||||||
|
// Clear modes because those are defined in onModeEnter functions.
|
||||||
|
this.modeCustomizations.clear();
|
||||||
this.initDefaults();
|
this.initDefaults();
|
||||||
this.addReferences(this.configuration);
|
this.addReferences(this.configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
initDefaults(): void {
|
initDefaults(): void {
|
||||||
this.extensionManager.registeredExtensionIds.forEach(extensionId => {
|
this.extensionManager.getRegisteredExtensionIds().forEach(extensionId => {
|
||||||
const key = `${extensionId}.customizationModule.default`;
|
const keyDefault = `${extensionId}.customizationModule.default`;
|
||||||
const defaultCustomizations = this.findExtensionValue(key);
|
const defaultCustomizations = this.findExtensionValue(keyDefault);
|
||||||
if (!defaultCustomizations) {
|
if (defaultCustomizations) {
|
||||||
return;
|
const { value } = defaultCustomizations;
|
||||||
|
this.addReference(value, CustomizationType.Default);
|
||||||
|
}
|
||||||
|
const keyGlobal = `${extensionId}.customizationModule.global`;
|
||||||
|
const globalCustomizations = this.findExtensionValue(keyGlobal);
|
||||||
|
if (globalCustomizations) {
|
||||||
|
const { value } = globalCustomizations;
|
||||||
|
this.addReference(value, CustomizationType.Global);
|
||||||
}
|
}
|
||||||
const { value } = defaultCustomizations;
|
|
||||||
this.addReference(value, true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findExtensionValue(value: string) {
|
findExtensionValue(value: string) {
|
||||||
const entry = this.extensionManager.getModuleEntry(value);
|
const entry = this.extensionManager.getModuleEntry(value);
|
||||||
return entry;
|
return entry as { value: Customization };
|
||||||
}
|
}
|
||||||
|
|
||||||
public onModeEnter(): void {
|
public onModeEnter(): void {
|
||||||
super.reset();
|
super.reset();
|
||||||
this.modeCustomizations = {};
|
this.modeCustomizations.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getModeCustomizations(): Record<string, Customization> {
|
public getModeCustomizations(): Map<string, Customization> {
|
||||||
return this.modeCustomizations;
|
return this.modeCustomizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,53 +168,81 @@ export default class CustomizationService extends PubSubService {
|
|||||||
customization: Customization,
|
customization: Customization,
|
||||||
merge = MergeEnum.Merge
|
merge = MergeEnum.Merge
|
||||||
): void {
|
): void {
|
||||||
this.modeCustomizations[customizationId] = this.mergeValue(
|
const defaultCustomization = this.defaultCustomizations.get(customizationId);
|
||||||
this.modeCustomizations[customizationId],
|
const modeCustomization = this.modeCustomizations.get(customizationId);
|
||||||
customization,
|
const globCustomization = this.globalCustomizations.get(customizationId);
|
||||||
merge
|
const sourceCustomization =
|
||||||
|
modeCustomization ||
|
||||||
|
(globCustomization && cloneDeepWith(globCustomization, cloneCustomizer)) ||
|
||||||
|
defaultCustomization ||
|
||||||
|
{};
|
||||||
|
|
||||||
|
this.modeCustomizations.set(
|
||||||
|
customizationId,
|
||||||
|
this.mergeValue(sourceCustomization, customization, merge)
|
||||||
);
|
);
|
||||||
|
this.transformedCustomizations.clear();
|
||||||
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
|
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
|
||||||
buttons: this.modeCustomizations,
|
buttons: this.modeCustomizations,
|
||||||
button: this.modeCustomizations[customizationId],
|
button: this.modeCustomizations.get(customizationId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is the preferred getter for all customizations,
|
/**
|
||||||
* getting mode customizations first and otherwise global customizations.
|
* This is the preferred getter for all customizations,
|
||||||
|
* getting global (priority) customizations first,
|
||||||
|
* then mode customizations, and finally the default customization.
|
||||||
*
|
*
|
||||||
* @param customizationId - the customization id to look for
|
* @param customizationId - the customization id to look for
|
||||||
* @param defaultValue - is the default value to return. Note this value
|
* @param defaultValue - is the default value to return.
|
||||||
* may have been extended with any customizationType extensions provided,
|
* This value will be assigned as the default customization if there isn't
|
||||||
* so you cannot just use `|| defaultValue`
|
* currently a default customization, and thus, the first default provided
|
||||||
|
* will be used as the default - you cannot update this after or have it depend
|
||||||
|
* on changing values.
|
||||||
|
* Also, the value returned by the get customization has merges/updates applied,
|
||||||
|
* and is thus may be modified from the value provided, and may not be the original
|
||||||
|
* default provided. This allows applying the defaults for things like inheritance.
|
||||||
* @return A customization to use if one is found, or the default customization,
|
* @return A customization to use if one is found, or the default customization,
|
||||||
* both enhanced with any customizationType inheritance (see transform)
|
* both enhanced with any customizationType inheritance (see transform)
|
||||||
*/
|
*/
|
||||||
public getCustomization(
|
public getCustomization(customizationId: string, defaultValue?: Customization): Customization {
|
||||||
customizationId: string,
|
const transformed = this.transformedCustomizations.get(customizationId);
|
||||||
defaultValue?: Customization
|
if (transformed) {
|
||||||
): Customization | void {
|
return transformed;
|
||||||
return this.getModeCustomization(customizationId, defaultValue);
|
}
|
||||||
|
if (defaultValue && !this.defaultCustomizations.has(customizationId)) {
|
||||||
|
this.setDefaultCustomization(customizationId, defaultValue);
|
||||||
|
}
|
||||||
|
const customization =
|
||||||
|
this.globalCustomizations.get(customizationId) ??
|
||||||
|
this.modeCustomizations.get(customizationId) ??
|
||||||
|
this.defaultCustomizations.get(customizationId);
|
||||||
|
const newTransformed = this.transform(customization);
|
||||||
|
if (newTransformed !== undefined) {
|
||||||
|
this.transformedCustomizations.set(customizationId, newTransformed);
|
||||||
|
}
|
||||||
|
return newTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mode customizations are changes to the behavior of the extensions
|
/** Mode customizations are changes to the behavior of the extensions
|
||||||
* when running in a given mode. Reset clears mode customizations.
|
* when running in a given mode. Reset clears mode customizations.
|
||||||
* Note that global customizations over-ride mode customizations.
|
*
|
||||||
|
* Note that global customizations over-ride mode customizations
|
||||||
|
*
|
||||||
* @param defaultValue to return if no customization specified.
|
* @param defaultValue to return if no customization specified.
|
||||||
*/
|
*/
|
||||||
public getModeCustomization(
|
public getModeCustomization = this.getCustomization;
|
||||||
customizationId: string,
|
|
||||||
defaultValue?: Customization
|
/**
|
||||||
): Customization {
|
* Returns true if there is a mode customization. Doesn't include defaults, but
|
||||||
const customization =
|
* does return global overrides.
|
||||||
this.globalCustomizations[customizationId] ??
|
*/
|
||||||
this.modeCustomizations[customizationId] ??
|
public hasModeCustomization(customizationId: string) {
|
||||||
defaultValue;
|
return (
|
||||||
return this.transform(customization);
|
this.globalCustomizations.has(customizationId) || this.modeCustomizations.has(customizationId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasModeCustomization(customizationId: string) {
|
|
||||||
return this.globalCustomizations[customizationId] || this.modeCustomizations[customizationId];
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* get is an alias for getModeCustomization, as it is the generic getter
|
* get is an alias for getModeCustomization, as it is the generic getter
|
||||||
* which will return both mode and global customizations, and should be
|
* which will return both mode and global customizations, and should be
|
||||||
@ -209,7 +278,7 @@ export default class CustomizationService extends PubSubService {
|
|||||||
if (!modeCustomizations) {
|
if (!modeCustomizations) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.addReferences(modeCustomizations, false);
|
this.addReferences(modeCustomizations, CustomizationType.Mode);
|
||||||
|
|
||||||
this._broadcastModeCustomizationModified();
|
this._broadcastModeCustomizationModified();
|
||||||
}
|
}
|
||||||
@ -226,24 +295,53 @@ export default class CustomizationService extends PubSubService {
|
|||||||
* Reset does NOT clear global customizations.
|
* Reset does NOT clear global customizations.
|
||||||
*/
|
*/
|
||||||
getGlobalCustomization(id: string, defaultValue?: Customization): Customization | void {
|
getGlobalCustomization(id: string, defaultValue?: Customization): Customization | void {
|
||||||
return this.transform(this.globalCustomizations[id] ?? defaultValue);
|
return this.transform(
|
||||||
|
this.globalCustomizations.get(id) ?? this.defaultCustomizations.get(id) ?? defaultValue
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a merge, creating a new instance value - that is, not referencing
|
||||||
|
* the old one. This only works if you run once for the merge, so in general,
|
||||||
|
* the source value should be global, while the appends should be mode based.
|
||||||
|
* However, you can append to a global value too, as long as you ensure it
|
||||||
|
* only gets merged once.
|
||||||
|
*/
|
||||||
private mergeValue(oldValue, newValue, mergeType = MergeEnum.Replace) {
|
private mergeValue(oldValue, newValue, mergeType = MergeEnum.Replace) {
|
||||||
if (mergeType === MergeEnum.Replace) {
|
if (mergeType === MergeEnum.Replace) {
|
||||||
return newValue;
|
return newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeWith(oldValue || {}, newValue, mergeCustomizer.bind(null, mergeType));
|
const returnValue = mergeWith({}, oldValue, newValue, mergeType === MergeEnum.Append ? appendCustomizer : mergeCustomizer);
|
||||||
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setGlobalCustomization(id: string, value: Customization, merge = MergeEnum.Replace): void {
|
public setGlobalCustomization(id: string, value: Customization, merge = MergeEnum.Replace): void {
|
||||||
this.globalCustomizations[id] = this.mergeValue(this.globalCustomizations[id], value, merge);
|
this.globalCustomizations.set(
|
||||||
|
id,
|
||||||
|
this.mergeValue(this.globalCustomizations.get(id), value, merge)
|
||||||
|
);
|
||||||
|
this.transformedCustomizations.clear();
|
||||||
this._broadcastGlobalCustomizationModified();
|
this._broadcastGlobalCustomizationModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setDefaultCustomization(
|
||||||
|
id: string,
|
||||||
|
value: Customization,
|
||||||
|
merge = MergeEnum.Replace
|
||||||
|
): void {
|
||||||
|
if (this.defaultCustomizations.has(id)) {
|
||||||
|
throw new Error(`Trying to update existing default for customization ${id}`);
|
||||||
|
}
|
||||||
|
this.transformedCustomizations.clear();
|
||||||
|
this.defaultCustomizations.set(
|
||||||
|
id,
|
||||||
|
this.mergeValue(this.defaultCustomizations.get(id), value, merge)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected setConfigGlobalCustomization(configuration: AppConfigCustomization): void {
|
protected setConfigGlobalCustomization(configuration: AppConfigCustomization): void {
|
||||||
this.globalCustomizations = {};
|
this.globalCustomizations.clear();
|
||||||
const keys = flattenNestedStrings(configuration.globalCustomizations);
|
const keys = flattenNestedStrings(configuration.globalCustomizations);
|
||||||
this.readCustomizationTypes(v => keys[v.name] && v.customization, this.globalCustomizations);
|
this.readCustomizationTypes(v => keys[v.name] && v.customization, this.globalCustomizations);
|
||||||
|
|
||||||
@ -263,7 +361,7 @@ export default class CustomizationService extends PubSubService {
|
|||||||
* A single reference is either an string to be loaded from a module,
|
* A single reference is either an string to be loaded from a module,
|
||||||
* or a customization itself.
|
* or a customization itself.
|
||||||
*/
|
*/
|
||||||
addReference(value?, isGlobal = true, id?: string, merge?: MergeEnum): void {
|
addReference(value?, type = CustomizationType.Global, id?: string, merge?: MergeEnum): void {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -271,16 +369,16 @@ export default class CustomizationService extends PubSubService {
|
|||||||
const extensionValue = this.findExtensionValue(value);
|
const extensionValue = this.findExtensionValue(value);
|
||||||
// The child of a reference is only a set of references when an array,
|
// The child of a reference is only a set of references when an array,
|
||||||
// so call the addReference direct. It could be a secondary reference perhaps
|
// so call the addReference direct. It could be a secondary reference perhaps
|
||||||
this.addReference(extensionValue.value, isGlobal, extensionValue.name, extensionValue.merge);
|
this.addReference(extensionValue.value, type, extensionValue.name, extensionValue.merge);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
this.addReferences(value, isGlobal);
|
this.addReferences(value, type);
|
||||||
} else {
|
} else {
|
||||||
const useId = value.id || id;
|
const useId = value.id || id;
|
||||||
this[isGlobal ? 'setGlobalCustomization' : 'setModeCustomization'](
|
const setName =
|
||||||
useId as string,
|
(type === CustomizationType.Global && 'setGlobalCustomization') ||
|
||||||
value,
|
(type === CustomizationType.Default && 'setDefaultCustomization') ||
|
||||||
merge
|
'setModeCustomization';
|
||||||
);
|
this[setName](useId as string, value, merge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,28 +387,89 @@ export default class CustomizationService extends PubSubService {
|
|||||||
* or as an object whose key is the reference id, and the value is the string
|
* or as an object whose key is the reference id, and the value is the string
|
||||||
* or customization.
|
* or customization.
|
||||||
*/
|
*/
|
||||||
addReferences(references?, isGlobal = true): void {
|
addReferences(references?, type = CustomizationType.Global): void {
|
||||||
if (!references) {
|
if (!references) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(references)) {
|
if (Array.isArray(references)) {
|
||||||
references.forEach(item => {
|
references.forEach(item => {
|
||||||
this.addReference(item, isGlobal);
|
this.addReference(item, type);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for (const key of Object.keys(references)) {
|
for (const key of Object.keys(references)) {
|
||||||
const value = references[key];
|
const value = references[key];
|
||||||
this.addReference(value, isGlobal, key);
|
this.addReference(value, type, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom merging function, to handle merging arrays
|
* Custom merging function, to handle merging arrays and copying functions
|
||||||
*/
|
*/
|
||||||
function mergeCustomizer(merge: MergeEnum, obj, src) {
|
function appendCustomizer(obj, src) {
|
||||||
if (merge === MergeEnum.Append && Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
|
const srcArray = Array.isArray(src);
|
||||||
|
if (srcArray) {
|
||||||
|
return obj.concat(...src);
|
||||||
|
}
|
||||||
|
if (typeof src === 'object') {
|
||||||
|
const newList = obj.map(value => cloneDeepWith(value, cloneCustomizer));
|
||||||
|
for (const [key, value] of Object.entries(src)) {
|
||||||
|
const { position, isMerge } = findPosition(key, value, newList);
|
||||||
|
if (isMerge) {
|
||||||
|
if (typeof obj[position] === 'object') {
|
||||||
|
newList[position] = mergeWith(Array.isArray(newList[position]) ? [] : {}, newList[position], value, appendCustomizer);
|
||||||
|
} else {
|
||||||
|
newList[position] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newList.splice(position, 0, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
return obj.concat(src);
|
return obj.concat(src);
|
||||||
}
|
}
|
||||||
|
return cloneCustomizer(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeCustomizer(obj, src) {
|
||||||
|
return cloneCustomizer(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPosition(key, value, newList) {
|
||||||
|
const numVal = Number(key);
|
||||||
|
const isNumeric = !isNaN(numVal);
|
||||||
|
const { length: len } = newList;
|
||||||
|
|
||||||
|
if (isNumeric) {
|
||||||
|
if (newList[(numVal + len) % len]) {
|
||||||
|
return { isMerge: true, position: (numVal + len) % len };
|
||||||
|
}
|
||||||
|
const absPosition = Math.ceil(numVal < 0 ? len + numVal : numVal);
|
||||||
|
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) }
|
||||||
|
}
|
||||||
|
const findIndex = newList.findIndex(it => it.id === key);
|
||||||
|
if (findIndex !== -1) {
|
||||||
|
return { isMerge: true, position: findIndex };
|
||||||
|
}
|
||||||
|
const { _priority: priority } = value;
|
||||||
|
if (priority !== undefined) {
|
||||||
|
if (newList[(priority + len) % len]) {
|
||||||
|
return { isMerge: true, position: (priority + len) % len };
|
||||||
|
}
|
||||||
|
const absPosition = Math.ceil(priority < 0 ? len + priority : priority);
|
||||||
|
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) }
|
||||||
|
}
|
||||||
|
return { isMerge: false, position: len };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom cloning function to just copy function reference
|
||||||
|
*/
|
||||||
|
function cloneCustomizer(value) {
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,61 @@ supports customization. (for example, `CustomizableViewportOverlay` component us
|
|||||||
`CustomizationService` to implement viewport overlay that is easily customizable
|
`CustomizationService` to implement viewport overlay that is easily customizable
|
||||||
from configuration.)
|
from configuration.)
|
||||||
|
|
||||||
|
## Global, Default and Mode customizations
|
||||||
|
There are various customization sets that define the lifetime/setup of the
|
||||||
|
customization. The global customizations are those used for overriding
|
||||||
|
customizations defined elsewhere, and allow replacing a customization.
|
||||||
|
|
||||||
|
Mode customizations are only registered for the lifetime of the mode, allowing
|
||||||
|
the mode definition to update/modify the underlying behaviour. This is related
|
||||||
|
to default customizations, which provide a fallback if the mode or global customization
|
||||||
|
isn't defined. Default customizations may only be defined once, otherwise throwing
|
||||||
|
an exception.
|
||||||
|
|
||||||
|
## Append and Merge Customizations
|
||||||
|
In addition to the replace a customization, there is the ability to merge or append
|
||||||
|
a customization. The merge customization simply applies the lodash merge functionality
|
||||||
|
to the existing customization, with the new one, while the append customization
|
||||||
|
modifies the customization by appending to the value.
|
||||||
|
|
||||||
|
### Append Behaviour
|
||||||
|
When a list is found in the destination object, the append source object is
|
||||||
|
examined to see how to handle the change. If the source is simply a list,
|
||||||
|
then the list object is appended, and no additional changes are performed.
|
||||||
|
However, if the source is an object other than a list, then the iterable
|
||||||
|
attributes of the object are examined to match child objects to the destination list,
|
||||||
|
according to the following table:
|
||||||
|
|
||||||
|
* Natural or zero number value - match the given index location and merge at the point
|
||||||
|
* Fractional number value - insert at a new point in the list, starting from the end or beginning
|
||||||
|
* keyword - match a value having the same id as the keyword, inserting at the end, or at _priority as defined in the keywords above.
|
||||||
|
|
||||||
|
#### Example Append
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const destination = [
|
||||||
|
1,
|
||||||
|
{id: 'two', value: 2},
|
||||||
|
{id: 'three', value: 3}
|
||||||
|
]
|
||||||
|
|
||||||
|
const source = {
|
||||||
|
two: { value: 'updated2' },
|
||||||
|
1: { extraValue: 2 },
|
||||||
|
1.0001: { id: 'inserted', value: 1.0001 },
|
||||||
|
-1: { value: -3 },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Results in two updates to `destination[1]`, the first using an id match on 'two', while the second one
|
||||||
|
does a positional match on `1`, resulting in the value `{id: 'two', value: 'updated2', extraValue: 2 }`
|
||||||
|
|
||||||
|
Then, it inserts the id 'inserted' after position 1.
|
||||||
|
|
||||||
|
Finally, position -1 (the end position) is updated from value 3 to value -3.
|
||||||
|
|
||||||
|
The ordering is not specified on any of these insertions, so can happen out of order. Use multiple updates to perform order specific inserts.
|
||||||
|
|
||||||
## Registering customizable modules (or defining customization prototypes)
|
## Registering customizable modules (or defining customization prototypes)
|
||||||
|
|
||||||
Extensions and Modes can register customization templates they support.
|
Extensions and Modes can register customization templates they support.
|
||||||
@ -37,8 +92,8 @@ Below is the protocol of the `getCustomizationModule()`, if defined in Typescrip
|
|||||||
getCustomizationModule() : { name: string, value: any }[]
|
getCustomizationModule() : { name: string, value: any }[]
|
||||||
```
|
```
|
||||||
|
|
||||||
If the name is 'default', it is the Default customization, which is loaded
|
If the name is 'default', it is the a default customization, while if it
|
||||||
automatically when the extension or mode is loaded.
|
is 'global', then it is a priority/over-riding customization.
|
||||||
|
|
||||||
In the `value` of each customizations, you will define customization prototype(s).
|
In the `value` of each customizations, you will define customization prototype(s).
|
||||||
These customization prototype(s) can be considered like "Prototype" in Javascript.
|
These customization prototype(s) can be considered like "Prototype" in Javascript.
|
||||||
|
|||||||
@ -3,7 +3,6 @@ const pkg = require('./package');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
name: pkg.name,
|
|
||||||
displayName: pkg.name,
|
displayName: pkg.name,
|
||||||
// rootDir: "../.."
|
// rootDir: "../.."
|
||||||
// testMatch: [
|
// testMatch: [
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
"react-dnd": "14.0.2",
|
"react-dnd": "14.0.2",
|
||||||
"react-dnd-html5-backend": "14.0.0",
|
"react-dnd-html5-backend": "14.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-draggable": "4.4.3",
|
"react-draggable": "^4.4.6",
|
||||||
"react-error-boundary": "^3.1.3",
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-modal": "3.11.2",
|
"react-modal": "3.11.2",
|
||||||
"react-outside-click-handler": "^1.3.0",
|
"react-outside-click-handler": "^1.3.0",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user