diff --git a/extensions/cornerstone-dynamic-volume/src/panels/WorkflowPanel.tsx b/extensions/cornerstone-dynamic-volume/src/panels/WorkflowPanel.tsx
index 224273d29..e21a33d4d 100644
--- a/extensions/cornerstone-dynamic-volume/src/panels/WorkflowPanel.tsx
+++ b/extensions/cornerstone-dynamic-volume/src/panels/WorkflowPanel.tsx
@@ -1,15 +1,11 @@
-import React, { useMemo } from 'react';
+import React from 'react';
+import type { ServicesManager } from '@ohif/core';
-function WorkflowPanel({ servicesManager, extensionManager }) {
- const ProgressDropdownWithService = useMemo(() => {
- const defaultComponents = extensionManager.getModuleEntry(
- '@ohif/extension-default.customizationModule.default'
- ).value;
-
- return defaultComponents.find(
- component => component.id === 'progressDropdownWithServiceComponent'
+function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }) {
+ const ProgressDropdownWithService =
+ servicesManager.services.customizationService.getCustomization(
+ 'progressDropdownWithServiceComponent'
).component;
- }, [extensionManager]);
return (
/src/__tests__/globalSetup.js'],
// rootDir: "../.."
diff --git a/platform/core/jest.config.js b/platform/core/jest.config.js
index 2978b062e..f57711b3f 100644
--- a/platform/core/jest.config.js
+++ b/platform/core/jest.config.js
@@ -3,7 +3,6 @@ const pkg = require('./package');
module.exports = {
...base,
- name: pkg.name,
displayName: pkg.name,
// rootDir: "../.."
// testMatch: [
diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts
index 45f668e02..f834c1855 100644
--- a/platform/core/src/extensions/ExtensionManager.ts
+++ b/platform/core/src/extensions/ExtensionManager.ts
@@ -130,6 +130,10 @@ export default class ExtensionManager extends PubSubService {
);
}
+ public getRegisteredExtensionIds() {
+ return [...this.registeredExtensionIds];
+ }
+
/**
* Calls all the services and extension on mode enters.
* The service onModeEnter is called first
diff --git a/platform/core/src/services/CustomizationService/CustomizationService.test.js b/platform/core/src/services/CustomizationService/CustomizationService.test.js
index 763bd62e3..98c407fe9 100644
--- a/platform/core/src/services/CustomizationService/CustomizationService.test.js
+++ b/platform/core/src/services/CustomizationService/CustomizationService.test.js
@@ -1,4 +1,4 @@
-import CustomizationService from './CustomizationService';
+import CustomizationService, { CustomizationType, MergeEnum } from './CustomizationService';
import log from '../../log';
jest.mock('../../log.js', () => ({
@@ -11,6 +11,8 @@ const extensionManager = {
registeredExtensionIds: [],
moduleEntries: {},
+ getRegisteredExtensionIds: () => extensionManager.registeredExtensionIds,
+
getModuleEntry: function (id) {
return this.moduleEntries[id];
},
@@ -49,6 +51,8 @@ describe('CustomizationService.ts', () => {
configuration,
commandsManager,
});
+ extensionManager.registeredExtensionIds = [];
+ extensionManager.moduleEntries = {};
});
describe('init', () => {
@@ -101,7 +105,7 @@ describe('CustomizationService.ts', () => {
configuration.testItem = testItem;
customizationService.init(extensionManager);
- const item = customizationService.getGlobalCustomization('testItem2', {
+ const item = customizationService.getCustomization('testItem2', {
id: 'testItem2',
customizationType: 'ohif.overlayItem',
label: 'otherLabel',
@@ -133,6 +137,7 @@ describe('CustomizationService.ts', () => {
expect(customizationService.getGlobalCustomization('testItem')).toBeUndefined();
const item = customizationService.getModeCustomization('testItem');
+ expect(item).not.toBeUndefined();
const props = { testAttribute: 'testAttrValue' };
const result = item.content(props);
@@ -143,7 +148,7 @@ describe('CustomizationService.ts', () => {
it('global customizations override modes', () => {
extensionManager.registeredExtensionIds.push('@testExtension');
- extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
+ extensionManager.moduleEntries['@testExtension.customizationModule.global'] = {
name: 'default',
value: [ohifOverlayItem],
};
@@ -160,5 +165,149 @@ describe('CustomizationService.ts', () => {
expect(result.label).toBe(testItem.label);
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);
+ });
});
});
diff --git a/platform/core/src/services/CustomizationService/CustomizationService.ts b/platform/core/src/services/CustomizationService/CustomizationService.ts
index cd1085233..df4d0b94d 100644
--- a/platform/core/src/services/CustomizationService/CustomizationService.ts
+++ b/platform/core/src/services/CustomizationService/CustomizationService.ts
@@ -1,7 +1,9 @@
-import mergeWith from 'lodash.mergewith';
+import { mergeWith, cloneDeepWith } from 'lodash';
+
import { PubSubService } from '../_shared/pubSubServiceInterface';
-import { Customization, NestedStrings } from './types';
-import { CommandsManager } from '../../classes';
+import type { Customization, NestedStrings } from './types';
+import type { CommandsManager } from '../../classes';
+import type { ExtensionManager } from '../../extensions';
const EVENTS = {
MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified',
@@ -43,6 +45,12 @@ export enum MergeEnum {
Replace = 'Replace',
}
+export enum CustomizationType {
+ Global = 'Global',
+ Mode = 'Mode',
+ Default = 'Default',
+}
+
/**
* The CustomizationService allows for retrieving of custom components
* and configuration for mode and global values.
@@ -78,10 +86,34 @@ export default class CustomizationService extends PubSubService {
};
commandsManager: CommandsManager;
- extensionManager: Record;
+ 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();
+ /**
+ * 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();
+
+ /**
+ * 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();
+
+ /**
+ * Has the transformed/final customization value. This avoids needing to
+ * transform every time a customization is requested.
+ */
+ private transformedCustomizations = new Map();
- modeCustomizations: Record = {};
- globalCustomizations: Record = {};
configuration: any;
constructor({ configuration, commandsManager }) {
@@ -92,33 +124,42 @@ export default class CustomizationService extends PubSubService {
public init(extensionManager: ExtensionManager): void {
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.addReferences(this.configuration);
}
initDefaults(): void {
- this.extensionManager.registeredExtensionIds.forEach(extensionId => {
- const key = `${extensionId}.customizationModule.default`;
- const defaultCustomizations = this.findExtensionValue(key);
- if (!defaultCustomizations) {
- return;
+ this.extensionManager.getRegisteredExtensionIds().forEach(extensionId => {
+ const keyDefault = `${extensionId}.customizationModule.default`;
+ const defaultCustomizations = this.findExtensionValue(keyDefault);
+ if (defaultCustomizations) {
+ 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) {
const entry = this.extensionManager.getModuleEntry(value);
- return entry;
+ return entry as { value: Customization };
}
public onModeEnter(): void {
super.reset();
- this.modeCustomizations = {};
+ this.modeCustomizations.clear();
}
- public getModeCustomizations(): Record {
+ public getModeCustomizations(): Map {
return this.modeCustomizations;
}
@@ -127,53 +168,81 @@ export default class CustomizationService extends PubSubService {
customization: Customization,
merge = MergeEnum.Merge
): void {
- this.modeCustomizations[customizationId] = this.mergeValue(
- this.modeCustomizations[customizationId],
- customization,
- merge
+ const defaultCustomization = this.defaultCustomizations.get(customizationId);
+ const modeCustomization = this.modeCustomizations.get(customizationId);
+ const globCustomization = this.globalCustomizations.get(customizationId);
+ 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, {
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 defaultValue - is the default value to return. Note this value
- * may have been extended with any customizationType extensions provided,
- * so you cannot just use `|| defaultValue`
+ * @param defaultValue - is the default value to return.
+ * This value will be assigned as the default customization if there isn't
+ * 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,
- * both enhanced with any customizationType inheritance (see transform)
+ * both enhanced with any customizationType inheritance (see transform)
*/
- public getCustomization(
- customizationId: string,
- defaultValue?: Customization
- ): Customization | void {
- return this.getModeCustomization(customizationId, defaultValue);
+ public getCustomization(customizationId: string, defaultValue?: Customization): Customization {
+ const transformed = this.transformedCustomizations.get(customizationId);
+ if (transformed) {
+ return transformed;
+ }
+ 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
* 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.
*/
- public getModeCustomization(
- customizationId: string,
- defaultValue?: Customization
- ): Customization {
- const customization =
- this.globalCustomizations[customizationId] ??
- this.modeCustomizations[customizationId] ??
- defaultValue;
- return this.transform(customization);
+ public getModeCustomization = this.getCustomization;
+
+ /**
+ * Returns true if there is a mode customization. Doesn't include defaults, but
+ * does return global overrides.
+ */
+ public hasModeCustomization(customizationId: string) {
+ return (
+ 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
* which will return both mode and global customizations, and should be
@@ -209,7 +278,7 @@ export default class CustomizationService extends PubSubService {
if (!modeCustomizations) {
return;
}
- this.addReferences(modeCustomizations, false);
+ this.addReferences(modeCustomizations, CustomizationType.Mode);
this._broadcastModeCustomizationModified();
}
@@ -226,24 +295,53 @@ export default class CustomizationService extends PubSubService {
* Reset does NOT clear global customizations.
*/
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) {
if (mergeType === MergeEnum.Replace) {
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 {
- 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();
}
+ 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 {
- this.globalCustomizations = {};
+ this.globalCustomizations.clear();
const keys = flattenNestedStrings(configuration.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,
* 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) {
return;
}
@@ -271,16 +369,16 @@ export default class CustomizationService extends PubSubService {
const extensionValue = this.findExtensionValue(value);
// 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
- this.addReference(extensionValue.value, isGlobal, extensionValue.name, extensionValue.merge);
+ this.addReference(extensionValue.value, type, extensionValue.name, extensionValue.merge);
} else if (Array.isArray(value)) {
- this.addReferences(value, isGlobal);
+ this.addReferences(value, type);
} else {
const useId = value.id || id;
- this[isGlobal ? 'setGlobalCustomization' : 'setModeCustomization'](
- useId as string,
- value,
- merge
- );
+ const setName =
+ (type === CustomizationType.Global && 'setGlobalCustomization') ||
+ (type === CustomizationType.Default && 'setDefaultCustomization') ||
+ '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 customization.
*/
- addReferences(references?, isGlobal = true): void {
+ addReferences(references?, type = CustomizationType.Global): void {
if (!references) {
return;
}
if (Array.isArray(references)) {
references.forEach(item => {
- this.addReference(item, isGlobal);
+ this.addReference(item, type);
});
} else {
for (const key of Object.keys(references)) {
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) {
- if (merge === MergeEnum.Append && Array.isArray(obj)) {
+function appendCustomizer(obj, src) {
+ 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 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;
+ }
}
diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md
index 7fc5a60ee..7142b57e8 100644
--- a/platform/docs/docs/platform/services/ui/customization-service.md
+++ b/platform/docs/docs/platform/services/ui/customization-service.md
@@ -26,6 +26,61 @@ supports customization. (for example, `CustomizableViewportOverlay` component us
`CustomizationService` to implement viewport overlay that is easily customizable
from configuration.)
+## Global, Default and Mode customizations
+There are various customization sets that define the lifetime/setup of the
+customization. The global customizations are those used for overriding
+customizations defined elsewhere, and allow replacing a customization.
+
+Mode customizations are only registered for the lifetime of the mode, allowing
+the mode definition to update/modify the underlying behaviour. This is related
+to default customizations, which provide a fallback if the mode or global customization
+isn't defined. Default customizations may only be defined once, otherwise throwing
+an exception.
+
+## Append and Merge Customizations
+In addition to the replace a customization, there is the ability to merge or append
+a customization. The merge customization simply applies the lodash merge functionality
+to the existing customization, with the new one, while the append customization
+modifies the customization by appending to the value.
+
+### Append Behaviour
+When a list is found in the destination object, the append source object is
+examined to see how to handle the change. If the source is simply a list,
+then the list object is appended, and no additional changes are performed.
+However, if the source is an object other than a list, then the iterable
+attributes of the object are examined to match child objects to the destination list,
+according to the following table:
+
+* Natural or zero number value - match the given index location and merge at the point
+* Fractional number value - insert at a new point in the list, starting from the end or beginning
+* keyword - match a value having the same id as the keyword, inserting at the end, or at _priority as defined in the keywords above.
+
+#### Example Append
+
+```javascript
+const destination = [
+ 1,
+ {id: 'two', value: 2},
+ {id: 'three', value: 3}
+]
+
+const source = {
+ two: { value: 'updated2' },
+ 1: { extraValue: 2 },
+ 1.0001: { id: 'inserted', value: 1.0001 },
+ -1: { value: -3 },
+}
+```
+
+Results in two updates to `destination[1]`, the first using an id match on 'two', while the second one
+does a positional match on `1`, resulting in the value `{id: 'two', value: 'updated2', extraValue: 2 }`
+
+Then, it inserts the id 'inserted' after position 1.
+
+Finally, position -1 (the end position) is updated from value 3 to value -3.
+
+The ordering is not specified on any of these insertions, so can happen out of order. Use multiple updates to perform order specific inserts.
+
## Registering customizable modules (or defining customization prototypes)
Extensions and Modes can register customization templates they support.
@@ -37,8 +92,8 @@ Below is the protocol of the `getCustomizationModule()`, if defined in Typescrip
getCustomizationModule() : { name: string, value: any }[]
```
-If the name is 'default', it is the Default customization, which is loaded
-automatically when the extension or mode is loaded.
+If the name is 'default', it is the a default customization, while if it
+is 'global', then it is a priority/over-riding customization.
In the `value` of each customizations, you will define customization prototype(s).
These customization prototype(s) can be considered like "Prototype" in Javascript.
diff --git a/platform/ui/jest.config.js b/platform/ui/jest.config.js
index 2978b062e..f57711b3f 100644
--- a/platform/ui/jest.config.js
+++ b/platform/ui/jest.config.js
@@ -3,7 +3,6 @@ const pkg = require('./package');
module.exports = {
...base,
- name: pkg.name,
displayName: pkg.name,
// rootDir: "../.."
// testMatch: [
diff --git a/platform/ui/package.json b/platform/ui/package.json
index 473d25658..de7842143 100644
--- a/platform/ui/package.json
+++ b/platform/ui/package.json
@@ -51,7 +51,7 @@
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dom": "^18.3.1",
- "react-draggable": "4.4.3",
+ "react-draggable": "^4.4.6",
"react-error-boundary": "^3.1.3",
"react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0",