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",