mirror of
https://github.com/nasa/openmct.git
synced 2024-12-24 07:16:39 +00:00
Generate type declarations for CompositionAPI and publish with OpenMCT (#5838)
* add typescript
* update tsconfig
* convert to es6 class
* Convert more stuff to es6 class
* skip checking libs, test files
* more es6 classes!
* Fix some jsdocs
* Rename file
* Improve jsdoc types
* Rename references as well
* more types
* types for CompositionAPI
* Types for CompositionCollection
* Types for CompositionProvider
* type
* types for api
* nvm
* cleanup MCT
* Fix API type definition
* Generate types before publish
* fix openmct 👀
* rename PublicAPI -> OpenMCT and document methods
* try and fix visual test ?
* Make private methods private
* more private methods!!
* import all es6 api's so we get more types for free
* convert Selection to es6 class
* remove redundant docs
* fix Branding types
* fix openmct.start() types
* Remove useless `@memberof`
* Add parameter name
* [docs] Add a section on Types
* markdownlint
* word
* Add section on limitations / contibuting types
* Let these methods be private
* make private members private, fix a type
* fix another type
* Make method private
* Update docs for `skipMutate` and related methods
* Rename file and fix references
* `DefaultCompositionProvider` extends `CompositionProvider`
* Make private members private
* Type for `AbortSignal`
* `domainObject` must be accessible for perf tests
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
parent
b4554d2fc1
commit
41fc502564
45
openmct.js
45
openmct.js
@ -30,8 +30,53 @@ if (document.currentScript) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} BuildInfo
|
||||||
|
* @property {string} version
|
||||||
|
* @property {string} buildDate
|
||||||
|
* @property {string} revision
|
||||||
|
* @property {string} branch
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} OpenMCT
|
||||||
|
* @property {BuildInfo} buildInfo
|
||||||
|
* @property {*} selection
|
||||||
|
* @property {import('./src/api/time/TimeAPI').default} time
|
||||||
|
* @property {import('./src/api/composition/CompositionAPI').default} composition
|
||||||
|
* @property {*} objectViews
|
||||||
|
* @property {*} inspectorViews
|
||||||
|
* @property {*} propertyEditors
|
||||||
|
* @property {*} toolbars
|
||||||
|
* @property {*} types
|
||||||
|
* @property {import('./src/api/objects/ObjectAPI').default} objects
|
||||||
|
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
|
||||||
|
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
|
||||||
|
* @property {import('./src/api/user/UserAPI').default} user
|
||||||
|
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
|
||||||
|
* @property {import('./src/api/Editor').default} editor
|
||||||
|
* @property {import('./src/api/overlays/OverlayAPI')} overlays
|
||||||
|
* @property {import('./src/api/menu/MenuAPI').default} menus
|
||||||
|
* @property {import('./src/api/actions/ActionsAPI').default} actions
|
||||||
|
* @property {import('./src/api/status/StatusAPI').default} status
|
||||||
|
* @property {*} priority
|
||||||
|
* @property {import('./src/ui/router/ApplicationRouter')} router
|
||||||
|
* @property {import('./src/api/faultmanagement/FaultManagementAPI').default} faults
|
||||||
|
* @property {import('./src/api/forms/FormsAPI').default} forms
|
||||||
|
* @property {import('./src/api/Branding').default} branding
|
||||||
|
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
|
||||||
|
* @property {{(plugin: OpenMCTPlugin) => void}} install
|
||||||
|
* @property {{() => string}} getAssetPath
|
||||||
|
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
|
||||||
|
* @property {{() => void}} startHeadless
|
||||||
|
* @property {{() => void}} destroy
|
||||||
|
* @property {OpenMCTPlugin[]} plugins
|
||||||
|
* @property {OpenMCTComponent[]} components
|
||||||
|
*/
|
||||||
|
|
||||||
const MCT = require('./src/MCT');
|
const MCT = require('./src/MCT');
|
||||||
|
|
||||||
|
/** @type {OpenMCT} */
|
||||||
const openmct = new MCT();
|
const openmct = new MCT();
|
||||||
|
|
||||||
module.exports = openmct;
|
module.exports = openmct;
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
"sinon": "14.0.0",
|
"sinon": "14.0.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
|
"typescript": "4.8.4",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vue": "2.6.14",
|
"vue": "2.6.14",
|
||||||
"vue-eslint-parser": "9.1.0",
|
"vue-eslint-parser": "9.1.0",
|
||||||
@ -96,7 +97,7 @@
|
|||||||
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||||
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
||||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||||
"prepare": "npm run build:prod"
|
"prepare": "npm run build:prod && npx tsc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
10
src/MCT.js
10
src/MCT.js
@ -19,7 +19,7 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
/* eslint-disable no-undef */
|
||||||
define([
|
define([
|
||||||
'EventEmitter',
|
'EventEmitter',
|
||||||
'./api/api',
|
'./api/api',
|
||||||
@ -81,13 +81,11 @@ define([
|
|||||||
/**
|
/**
|
||||||
* The Open MCT application. This may be configured by installing plugins
|
* The Open MCT application. This may be configured by installing plugins
|
||||||
* or registering extensions before the application is started.
|
* or registering extensions before the application is started.
|
||||||
* @class MCT
|
* @constructor
|
||||||
* @memberof module:openmct
|
* @memberof module:openmct
|
||||||
* @augments {EventEmitter}
|
|
||||||
*/
|
*/
|
||||||
function MCT() {
|
function MCT() {
|
||||||
EventEmitter.call(this);
|
EventEmitter.call(this);
|
||||||
/* eslint-disable no-undef */
|
|
||||||
this.buildInfo = {
|
this.buildInfo = {
|
||||||
version: __OPENMCT_VERSION__,
|
version: __OPENMCT_VERSION__,
|
||||||
buildDate: __OPENMCT_BUILD_DATE__,
|
buildDate: __OPENMCT_BUILD_DATE__,
|
||||||
@ -101,7 +99,7 @@ define([
|
|||||||
* Tracks current selection state of the application.
|
* Tracks current selection state of the application.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
['selection', () => new Selection(this)],
|
['selection', () => new Selection.default(this)],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCT's time conductor, which may be used to synchronize view contents
|
* MCT's time conductor, which may be used to synchronize view contents
|
||||||
@ -125,7 +123,7 @@ define([
|
|||||||
* @memberof module:openmct.MCT#
|
* @memberof module:openmct.MCT#
|
||||||
* @name composition
|
* @name composition
|
||||||
*/
|
*/
|
||||||
['composition', () => new api.CompositionAPI(this)],
|
['composition', () => new api.CompositionAPI.default(this)],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry for views of domain objects which should appear in the
|
* Registry for views of domain objects which should appear in the
|
||||||
|
@ -23,8 +23,7 @@
|
|||||||
let brandingOptions = {};
|
let brandingOptions = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} BrandingOptions
|
* @typedef {object} BrandingOptions
|
||||||
* @memberOf openmct/branding
|
|
||||||
* @property {string} smallLogoImage URL to the image to use as the applications logo.
|
* @property {string} smallLogoImage URL to the image to use as the applications logo.
|
||||||
* This logo will appear on every screen and when clicked will launch the about dialog.
|
* This logo will appear on every screen and when clicked will launch the about dialog.
|
||||||
* @property {string} aboutHtml Custom content for the about screen. When defined the
|
* @property {string} aboutHtml Custom content for the about screen. When defined the
|
||||||
|
@ -37,7 +37,9 @@ define([
|
|||||||
'./types/TypeRegistry',
|
'./types/TypeRegistry',
|
||||||
'./user/UserAPI',
|
'./user/UserAPI',
|
||||||
'./annotation/AnnotationAPI'
|
'./annotation/AnnotationAPI'
|
||||||
], function (
|
],
|
||||||
|
|
||||||
|
function (
|
||||||
ActionsAPI,
|
ActionsAPI,
|
||||||
CompositionAPI,
|
CompositionAPI,
|
||||||
EditorAPI,
|
EditorAPI,
|
||||||
|
@ -20,34 +20,41 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
import DefaultCompositionProvider from './DefaultCompositionProvider';
|
||||||
'lodash',
|
import CompositionCollection from './CompositionCollection';
|
||||||
'EventEmitter',
|
|
||||||
'./DefaultCompositionProvider',
|
/**
|
||||||
'./CompositionCollection'
|
* @typedef {import('./CompositionProvider').default} CompositionProvider
|
||||||
], function (
|
*/
|
||||||
_,
|
|
||||||
EventEmitter,
|
/**
|
||||||
DefaultCompositionProvider,
|
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||||
CompositionCollection
|
*/
|
||||||
) {
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for interacting with the composition of domain objects.
|
||||||
|
* The composition of a domain object is the list of other domain objects
|
||||||
|
* it "contains" (for instance, that should be displayed beneath it
|
||||||
|
* in the tree.)
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default class CompositionAPI {
|
||||||
/**
|
/**
|
||||||
* An interface for interacting with the composition of domain objects.
|
* @param {OpenMCT} publicAPI
|
||||||
* The composition of a domain object is the list of other domain objects
|
|
||||||
* it "contains" (for instance, that should be displayed beneath it
|
|
||||||
* in the tree.)
|
|
||||||
*
|
|
||||||
* @interface CompositionAPI
|
|
||||||
* @returns {module:openmct.CompositionCollection}
|
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
*/
|
||||||
function CompositionAPI(publicAPI) {
|
constructor(publicAPI) {
|
||||||
|
/** @type {CompositionProvider[]} */
|
||||||
this.registry = [];
|
this.registry = [];
|
||||||
|
/** @type {CompositionPolicy[]} */
|
||||||
this.policies = [];
|
this.policies = [];
|
||||||
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
|
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
|
||||||
|
/** @type {OpenMCT} */
|
||||||
this.publicAPI = publicAPI;
|
this.publicAPI = publicAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a composition provider.
|
* Add a composition provider.
|
||||||
*
|
*
|
||||||
@ -55,21 +62,19 @@ define([
|
|||||||
* behavior for certain domain objects.
|
* behavior for certain domain objects.
|
||||||
*
|
*
|
||||||
* @method addProvider
|
* @method addProvider
|
||||||
* @param {module:openmct.CompositionProvider} provider the provider to add
|
* @param {CompositionProvider} provider the provider to add
|
||||||
* @memberof module:openmct.CompositionAPI#
|
|
||||||
*/
|
*/
|
||||||
CompositionAPI.prototype.addProvider = function (provider) {
|
addProvider(provider) {
|
||||||
this.registry.unshift(provider);
|
this.registry.unshift(provider);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the composition (if any) of this domain object.
|
* Retrieve the composition (if any) of this domain object.
|
||||||
*
|
*
|
||||||
* @method get
|
* @method get
|
||||||
* @returns {module:openmct.CompositionCollection}
|
* @param {DomainObject} domainObject
|
||||||
* @memberof module:openmct.CompositionAPI#
|
* @returns {CompositionCollection}
|
||||||
*/
|
*/
|
||||||
CompositionAPI.prototype.get = function (domainObject) {
|
get(domainObject) {
|
||||||
const provider = this.registry.find(p => {
|
const provider = this.registry.find(p => {
|
||||||
return p.appliesTo(domainObject);
|
return p.appliesTo(domainObject);
|
||||||
});
|
});
|
||||||
@ -79,8 +84,7 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new CompositionCollection(domainObject, provider, this.publicAPI);
|
return new CompositionCollection(domainObject, provider, this.publicAPI);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composition policy is a function which either allows or disallows
|
* A composition policy is a function which either allows or disallows
|
||||||
* placing one object in another's composition.
|
* placing one object in another's composition.
|
||||||
@ -90,52 +94,51 @@ define([
|
|||||||
* generally be written to return true in the default case.
|
* generally be written to return true in the default case.
|
||||||
*
|
*
|
||||||
* @callback CompositionPolicy
|
* @callback CompositionPolicy
|
||||||
* @memberof module:openmct.CompositionAPI~
|
* @param {DomainObject} containingObject the object which
|
||||||
* @param {module:openmct.DomainObject} containingObject the object which
|
|
||||||
* would act as a container
|
* would act as a container
|
||||||
* @param {module:openmct.DomainObject} containedObject the object which
|
* @param {DomainObject} containedObject the object which
|
||||||
* would be contained
|
* would be contained
|
||||||
* @returns {boolean} false if this composition should be disallowed
|
* @returns {boolean} false if this composition should be disallowed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a composition policy. Composition policies may disallow domain
|
* Add a composition policy. Composition policies may disallow domain
|
||||||
* objects from containing other domain objects.
|
* objects from containing other domain objects.
|
||||||
*
|
*
|
||||||
* @method addPolicy
|
* @method addPolicy
|
||||||
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
|
* @param {CompositionPolicy} policy
|
||||||
* the policy to add
|
* the policy to add
|
||||||
* @memberof module:openmct.CompositionAPI#
|
|
||||||
*/
|
*/
|
||||||
CompositionAPI.prototype.addPolicy = function (policy) {
|
addPolicy(policy) {
|
||||||
this.policies.push(policy);
|
this.policies.push(policy);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether or not a domain object is allowed to contain another
|
* Check whether or not a domain object is allowed to contain another
|
||||||
* domain object.
|
* domain object.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @method checkPolicy
|
* @method checkPolicy
|
||||||
* @param {module:openmct.DomainObject} containingObject the object which
|
* @param {DomainObject} container the object which
|
||||||
* would act as a container
|
* would act as a container
|
||||||
* @param {module:openmct.DomainObject} containedObject the object which
|
* @param {DomainObject} containee the object which
|
||||||
* would be contained
|
* would be contained
|
||||||
* @returns {boolean} false if this composition should be disallowed
|
* @returns {boolean} false if this composition should be disallowed
|
||||||
|
* @param {CompositionPolicy} policy
|
||||||
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
|
|
||||||
* the policy to add
|
* the policy to add
|
||||||
* @memberof module:openmct.CompositionAPI#
|
|
||||||
*/
|
*/
|
||||||
CompositionAPI.prototype.checkPolicy = function (container, containee) {
|
checkPolicy(container, containee) {
|
||||||
return this.policies.every(function (policy) {
|
return this.policies.every(function (policy) {
|
||||||
return policy(container, containee);
|
return policy(container, containee);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
CompositionAPI.prototype.supportsComposition = function (domainObject) {
|
/**
|
||||||
|
* Check whether or not a domainObject supports composition
|
||||||
|
*
|
||||||
|
* @param {DomainObject} domainObject
|
||||||
|
* @returns {boolean} true if the domainObject supports composition
|
||||||
|
*/
|
||||||
|
supportsComposition(domainObject) {
|
||||||
return this.get(domainObject) !== undefined;
|
return this.get(domainObject) !== undefined;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CompositionAPI;
|
|
||||||
});
|
|
||||||
|
@ -1,325 +1,319 @@
|
|||||||
define([
|
import CompositionAPI from './CompositionAPI';
|
||||||
'./CompositionAPI',
|
import CompositionCollection from './CompositionCollection';
|
||||||
'./CompositionCollection'
|
|
||||||
], function (
|
|
||||||
CompositionAPI,
|
|
||||||
CompositionCollection
|
|
||||||
) {
|
|
||||||
|
|
||||||
describe('The Composition API', function () {
|
describe('The Composition API', function () {
|
||||||
let publicAPI;
|
let publicAPI;
|
||||||
let compositionAPI;
|
let compositionAPI;
|
||||||
let topicService;
|
let topicService;
|
||||||
let mutationTopic;
|
let mutationTopic;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
|
||||||
|
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||||
|
'listen'
|
||||||
|
]);
|
||||||
|
topicService = jasmine.createSpy('topicService');
|
||||||
|
topicService.and.returnValue(mutationTopic);
|
||||||
|
publicAPI = {};
|
||||||
|
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
||||||
|
'get',
|
||||||
|
'mutate',
|
||||||
|
'observe',
|
||||||
|
'areIdsEqual'
|
||||||
|
]);
|
||||||
|
|
||||||
|
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
|
||||||
|
return id1.namespace === id2.namespace && id1.key === id2.key;
|
||||||
|
});
|
||||||
|
|
||||||
|
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
|
||||||
|
'checkPolicy'
|
||||||
|
]);
|
||||||
|
publicAPI.composition.checkPolicy.and.returnValue(true);
|
||||||
|
|
||||||
|
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
|
||||||
|
'on'
|
||||||
|
]);
|
||||||
|
publicAPI.objects.get.and.callFake(function (identifier) {
|
||||||
|
return Promise.resolve({identifier: identifier});
|
||||||
|
});
|
||||||
|
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
||||||
|
'get'
|
||||||
|
]);
|
||||||
|
publicAPI.$injector.get.and.returnValue(topicService);
|
||||||
|
compositionAPI = new CompositionAPI(publicAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns falsy if an object does not support composition', function () {
|
||||||
|
expect(compositionAPI.get({})).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('default composition', function () {
|
||||||
|
let domainObject;
|
||||||
|
let composition;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
domainObject = {
|
||||||
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
name: 'test folder',
|
||||||
'listen'
|
identifier: {
|
||||||
]);
|
namespace: 'test',
|
||||||
topicService = jasmine.createSpy('topicService');
|
key: '1'
|
||||||
topicService.and.returnValue(mutationTopic);
|
},
|
||||||
publicAPI = {};
|
composition: [
|
||||||
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
{
|
||||||
'get',
|
namespace: 'test',
|
||||||
'mutate',
|
key: 'a'
|
||||||
'observe',
|
},
|
||||||
'areIdsEqual'
|
{
|
||||||
]);
|
namespace: 'test',
|
||||||
|
key: 'b'
|
||||||
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
|
},
|
||||||
return id1.namespace === id2.namespace && id1.key === id2.key;
|
{
|
||||||
});
|
namespace: 'test',
|
||||||
|
key: 'c'
|
||||||
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
|
}
|
||||||
'checkPolicy'
|
]
|
||||||
]);
|
};
|
||||||
publicAPI.composition.checkPolicy.and.returnValue(true);
|
composition = compositionAPI.get(domainObject);
|
||||||
|
|
||||||
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
|
|
||||||
'on'
|
|
||||||
]);
|
|
||||||
publicAPI.objects.get.and.callFake(function (identifier) {
|
|
||||||
return Promise.resolve({identifier: identifier});
|
|
||||||
});
|
|
||||||
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
|
||||||
'get'
|
|
||||||
]);
|
|
||||||
publicAPI.$injector.get.and.returnValue(topicService);
|
|
||||||
compositionAPI = new CompositionAPI(publicAPI);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns falsy if an object does not support composition', function () {
|
it('returns composition collection', function () {
|
||||||
expect(compositionAPI.get({})).toBeFalsy();
|
expect(composition).toBeDefined();
|
||||||
|
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('default composition', function () {
|
it('correctly reflects composability', function () {
|
||||||
let domainObject;
|
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
||||||
let composition;
|
delete domainObject.composition;
|
||||||
|
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
it('loads composition from domain object', function () {
|
||||||
domainObject = {
|
const listener = jasmine.createSpy('addListener');
|
||||||
name: 'test folder',
|
composition.on('add', listener);
|
||||||
|
|
||||||
|
return composition.load().then(function () {
|
||||||
|
expect(listener.calls.count()).toBe(3);
|
||||||
|
expect(listener).toHaveBeenCalledWith({
|
||||||
identifier: {
|
identifier: {
|
||||||
namespace: 'test',
|
namespace: 'test',
|
||||||
key: '1'
|
key: 'a'
|
||||||
},
|
}
|
||||||
composition: [
|
|
||||||
{
|
|
||||||
namespace: 'test',
|
|
||||||
key: 'a'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: 'test',
|
|
||||||
key: 'b'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: 'test',
|
|
||||||
key: 'c'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
composition = compositionAPI.get(domainObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns composition collection', function () {
|
|
||||||
expect(composition).toBeDefined();
|
|
||||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('correctly reflects composability', function () {
|
|
||||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
|
||||||
delete domainObject.composition;
|
|
||||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads composition from domain object', function () {
|
|
||||||
const listener = jasmine.createSpy('addListener');
|
|
||||||
composition.on('add', listener);
|
|
||||||
|
|
||||||
return composition.load().then(function () {
|
|
||||||
expect(listener.calls.count()).toBe(3);
|
|
||||||
expect(listener).toHaveBeenCalledWith({
|
|
||||||
identifier: {
|
|
||||||
namespace: 'test',
|
|
||||||
key: 'a'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('supports reordering of composition', function () {
|
});
|
||||||
let listener;
|
describe('supports reordering of composition', function () {
|
||||||
beforeEach(function () {
|
let listener;
|
||||||
listener = jasmine.createSpy('reorderListener');
|
beforeEach(function () {
|
||||||
composition.on('reorder', listener);
|
listener = jasmine.createSpy('reorderListener');
|
||||||
|
composition.on('reorder', listener);
|
||||||
|
|
||||||
return composition.load();
|
return composition.load();
|
||||||
});
|
});
|
||||||
it('', function () {
|
it('', function () {
|
||||||
composition.reorder(1, 0);
|
composition.reorder(1, 0);
|
||||||
let newComposition =
|
let newComposition =
|
||||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||||
|
|
||||||
expect(reorderPlan.oldIndex).toBe(1);
|
expect(reorderPlan.oldIndex).toBe(1);
|
||||||
expect(reorderPlan.newIndex).toBe(0);
|
expect(reorderPlan.newIndex).toBe(0);
|
||||||
expect(newComposition[0].key).toEqual('b');
|
expect(newComposition[0].key).toEqual('b');
|
||||||
expect(newComposition[1].key).toEqual('a');
|
expect(newComposition[1].key).toEqual('a');
|
||||||
expect(newComposition[2].key).toEqual('c');
|
expect(newComposition[2].key).toEqual('c');
|
||||||
});
|
});
|
||||||
it('', function () {
|
it('', function () {
|
||||||
composition.reorder(0, 2);
|
composition.reorder(0, 2);
|
||||||
let newComposition =
|
let newComposition =
|
||||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||||
|
|
||||||
expect(reorderPlan.oldIndex).toBe(0);
|
expect(reorderPlan.oldIndex).toBe(0);
|
||||||
expect(reorderPlan.newIndex).toBe(2);
|
expect(reorderPlan.newIndex).toBe(2);
|
||||||
expect(newComposition[0].key).toEqual('b');
|
expect(newComposition[0].key).toEqual('b');
|
||||||
expect(newComposition[1].key).toEqual('c');
|
expect(newComposition[1].key).toEqual('c');
|
||||||
expect(newComposition[2].key).toEqual('a');
|
expect(newComposition[2].key).toEqual('a');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('supports adding an object to composition', function () {
|
||||||
|
let addListener = jasmine.createSpy('addListener');
|
||||||
|
let mockChildObject = {
|
||||||
|
identifier: {
|
||||||
|
key: 'mock-key',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
composition.on('add', addListener);
|
||||||
|
composition.add(mockChildObject);
|
||||||
|
|
||||||
|
expect(domainObject.composition.length).toBe(4);
|
||||||
|
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('static custom composition', function () {
|
||||||
|
let customProvider;
|
||||||
|
let domainObject;
|
||||||
|
let composition;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// A simple custom provider, returns the same composition for
|
||||||
|
// all objects of a given type.
|
||||||
|
customProvider = {
|
||||||
|
appliesTo: function (object) {
|
||||||
|
return object.type === 'custom-object-type';
|
||||||
|
},
|
||||||
|
load: function (object) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
namespace: 'custom',
|
||||||
|
key: 'thing'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
add: jasmine.createSpy('add'),
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
};
|
||||||
|
domainObject = {
|
||||||
|
identifier: {
|
||||||
|
namespace: 'test',
|
||||||
|
key: '1'
|
||||||
|
},
|
||||||
|
type: 'custom-object-type'
|
||||||
|
};
|
||||||
|
compositionAPI.addProvider(customProvider);
|
||||||
|
composition = compositionAPI.get(domainObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports listening and loading', function () {
|
||||||
|
const addListener = jasmine.createSpy('addListener');
|
||||||
|
composition.on('add', addListener);
|
||||||
|
|
||||||
|
return composition.load().then(function (children) {
|
||||||
|
let listenObject;
|
||||||
|
const loadedObject = children[0];
|
||||||
|
|
||||||
|
expect(addListener).toHaveBeenCalled();
|
||||||
|
|
||||||
|
listenObject = addListener.calls.mostRecent().args[0];
|
||||||
|
expect(listenObject).toEqual(loadedObject);
|
||||||
|
expect(loadedObject).toEqual({
|
||||||
|
identifier: {
|
||||||
|
namespace: 'custom',
|
||||||
|
key: 'thing'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('supports adding an object to composition', function () {
|
});
|
||||||
let addListener = jasmine.createSpy('addListener');
|
describe('Calling add or remove', function () {
|
||||||
let mockChildObject = {
|
let mockChildObject;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockChildObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'mock-key',
|
key: 'mock-key',
|
||||||
namespace: ''
|
namespace: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
composition.on('add', addListener);
|
|
||||||
composition.add(mockChildObject);
|
composition.add(mockChildObject);
|
||||||
|
|
||||||
expect(domainObject.composition.length).toBe(4);
|
|
||||||
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('static custom composition', function () {
|
|
||||||
let customProvider;
|
|
||||||
let domainObject;
|
|
||||||
let composition;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
// A simple custom provider, returns the same composition for
|
|
||||||
// all objects of a given type.
|
|
||||||
customProvider = {
|
|
||||||
appliesTo: function (object) {
|
|
||||||
return object.type === 'custom-object-type';
|
|
||||||
},
|
|
||||||
load: function (object) {
|
|
||||||
return Promise.resolve([
|
|
||||||
{
|
|
||||||
namespace: 'custom',
|
|
||||||
key: 'thing'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
add: jasmine.createSpy('add'),
|
|
||||||
remove: jasmine.createSpy('remove')
|
|
||||||
};
|
|
||||||
domainObject = {
|
|
||||||
identifier: {
|
|
||||||
namespace: 'test',
|
|
||||||
key: '1'
|
|
||||||
},
|
|
||||||
type: 'custom-object-type'
|
|
||||||
};
|
|
||||||
compositionAPI.addProvider(customProvider);
|
|
||||||
composition = compositionAPI.get(domainObject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports listening and loading', function () {
|
it('calls add on the provider', function () {
|
||||||
const addListener = jasmine.createSpy('addListener');
|
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||||
composition.on('add', addListener);
|
|
||||||
|
|
||||||
return composition.load().then(function (children) {
|
|
||||||
let listenObject;
|
|
||||||
const loadedObject = children[0];
|
|
||||||
|
|
||||||
expect(addListener).toHaveBeenCalled();
|
|
||||||
|
|
||||||
listenObject = addListener.calls.mostRecent().args[0];
|
|
||||||
expect(listenObject).toEqual(loadedObject);
|
|
||||||
expect(loadedObject).toEqual({
|
|
||||||
identifier: {
|
|
||||||
namespace: 'custom',
|
|
||||||
key: 'thing'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Calling add or remove', function () {
|
|
||||||
let mockChildObject;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockChildObject = {
|
|
||||||
identifier: {
|
|
||||||
key: 'mock-key',
|
|
||||||
namespace: ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
composition.add(mockChildObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls add on the provider', function () {
|
|
||||||
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls remove on the provider', function () {
|
|
||||||
composition.remove(mockChildObject);
|
|
||||||
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('dynamic custom composition', function () {
|
|
||||||
let customProvider;
|
|
||||||
let domainObject;
|
|
||||||
let composition;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
// A dynamic provider, loads an empty composition and exposes
|
|
||||||
// listener functions.
|
|
||||||
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
|
||||||
'appliesTo',
|
|
||||||
'load',
|
|
||||||
'on',
|
|
||||||
'off'
|
|
||||||
]);
|
|
||||||
|
|
||||||
customProvider.appliesTo.and.returnValue('true');
|
|
||||||
customProvider.load.and.returnValue(Promise.resolve([]));
|
|
||||||
|
|
||||||
domainObject = {
|
|
||||||
identifier: {
|
|
||||||
namespace: 'test',
|
|
||||||
key: '1'
|
|
||||||
},
|
|
||||||
type: 'custom-object-type'
|
|
||||||
};
|
|
||||||
compositionAPI.addProvider(customProvider);
|
|
||||||
composition = compositionAPI.get(domainObject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports listening and loading', function () {
|
it('calls remove on the provider', function () {
|
||||||
const addListener = jasmine.createSpy('addListener');
|
composition.remove(mockChildObject);
|
||||||
const removeListener = jasmine.createSpy('removeListener');
|
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||||
const addPromise = new Promise(function (resolve) {
|
|
||||||
addListener.and.callFake(resolve);
|
|
||||||
});
|
|
||||||
const removePromise = new Promise(function (resolve) {
|
|
||||||
removeListener.and.callFake(resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
composition.on('add', addListener);
|
|
||||||
composition.on('remove', removeListener);
|
|
||||||
|
|
||||||
expect(customProvider.on).toHaveBeenCalledWith(
|
|
||||||
domainObject,
|
|
||||||
'add',
|
|
||||||
jasmine.any(Function),
|
|
||||||
jasmine.any(CompositionCollection)
|
|
||||||
);
|
|
||||||
expect(customProvider.on).toHaveBeenCalledWith(
|
|
||||||
domainObject,
|
|
||||||
'remove',
|
|
||||||
jasmine.any(Function),
|
|
||||||
jasmine.any(CompositionCollection)
|
|
||||||
);
|
|
||||||
const add = customProvider.on.calls.all()[0].args[2];
|
|
||||||
const remove = customProvider.on.calls.all()[1].args[2];
|
|
||||||
|
|
||||||
return composition.load()
|
|
||||||
.then(function () {
|
|
||||||
expect(addListener).not.toHaveBeenCalled();
|
|
||||||
expect(removeListener).not.toHaveBeenCalled();
|
|
||||||
add({
|
|
||||||
namespace: 'custom',
|
|
||||||
key: 'thing'
|
|
||||||
});
|
|
||||||
|
|
||||||
return addPromise;
|
|
||||||
}).then(function () {
|
|
||||||
expect(addListener).toHaveBeenCalledWith({
|
|
||||||
identifier: {
|
|
||||||
namespace: 'custom',
|
|
||||||
key: 'thing'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
remove(addListener.calls.mostRecent().args[0]);
|
|
||||||
|
|
||||||
return removePromise;
|
|
||||||
}).then(function () {
|
|
||||||
expect(removeListener).toHaveBeenCalledWith({
|
|
||||||
identifier: {
|
|
||||||
namespace: 'custom',
|
|
||||||
key: 'thing'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dynamic custom composition', function () {
|
||||||
|
let customProvider;
|
||||||
|
let domainObject;
|
||||||
|
let composition;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// A dynamic provider, loads an empty composition and exposes
|
||||||
|
// listener functions.
|
||||||
|
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
||||||
|
'appliesTo',
|
||||||
|
'load',
|
||||||
|
'on',
|
||||||
|
'off'
|
||||||
|
]);
|
||||||
|
|
||||||
|
customProvider.appliesTo.and.returnValue('true');
|
||||||
|
customProvider.load.and.returnValue(Promise.resolve([]));
|
||||||
|
|
||||||
|
domainObject = {
|
||||||
|
identifier: {
|
||||||
|
namespace: 'test',
|
||||||
|
key: '1'
|
||||||
|
},
|
||||||
|
type: 'custom-object-type'
|
||||||
|
};
|
||||||
|
compositionAPI.addProvider(customProvider);
|
||||||
|
composition = compositionAPI.get(domainObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports listening and loading', function () {
|
||||||
|
const addListener = jasmine.createSpy('addListener');
|
||||||
|
const removeListener = jasmine.createSpy('removeListener');
|
||||||
|
const addPromise = new Promise(function (resolve) {
|
||||||
|
addListener.and.callFake(resolve);
|
||||||
|
});
|
||||||
|
const removePromise = new Promise(function (resolve) {
|
||||||
|
removeListener.and.callFake(resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
composition.on('add', addListener);
|
||||||
|
composition.on('remove', removeListener);
|
||||||
|
|
||||||
|
expect(customProvider.on).toHaveBeenCalledWith(
|
||||||
|
domainObject,
|
||||||
|
'add',
|
||||||
|
jasmine.any(Function),
|
||||||
|
jasmine.any(CompositionCollection)
|
||||||
|
);
|
||||||
|
expect(customProvider.on).toHaveBeenCalledWith(
|
||||||
|
domainObject,
|
||||||
|
'remove',
|
||||||
|
jasmine.any(Function),
|
||||||
|
jasmine.any(CompositionCollection)
|
||||||
|
);
|
||||||
|
const add = customProvider.on.calls.all()[0].args[2];
|
||||||
|
const remove = customProvider.on.calls.all()[1].args[2];
|
||||||
|
|
||||||
|
return composition.load()
|
||||||
|
.then(function () {
|
||||||
|
expect(addListener).not.toHaveBeenCalled();
|
||||||
|
expect(removeListener).not.toHaveBeenCalled();
|
||||||
|
add({
|
||||||
|
namespace: 'custom',
|
||||||
|
key: 'thing'
|
||||||
|
});
|
||||||
|
|
||||||
|
return addPromise;
|
||||||
|
}).then(function () {
|
||||||
|
expect(addListener).toHaveBeenCalledWith({
|
||||||
|
identifier: {
|
||||||
|
namespace: 'custom',
|
||||||
|
key: 'thing'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
remove(addListener.calls.mostRecent().args[0]);
|
||||||
|
|
||||||
|
return removePromise;
|
||||||
|
}).then(function () {
|
||||||
|
expect(removeListener).toHaveBeenCalledWith({
|
||||||
|
identifier: {
|
||||||
|
namespace: 'custom',
|
||||||
|
key: 'thing'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,75 +20,98 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
/**
|
||||||
'lodash'
|
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||||
], function (
|
*/
|
||||||
_
|
|
||||||
) {
|
/**
|
||||||
|
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ListenerMap
|
||||||
|
* @property {Array.<any>} add
|
||||||
|
* @property {Array.<any>} remove
|
||||||
|
* @property {Array.<any>} load
|
||||||
|
* @property {Array.<any>} reorder
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CompositionCollection represents the list of domain objects contained
|
||||||
|
* by another domain object. It provides methods for loading this
|
||||||
|
* list asynchronously, modifying this list, and listening for changes to
|
||||||
|
* this list.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```javascript
|
||||||
|
* var myViewComposition = MCT.composition.get(myViewObject);
|
||||||
|
* myViewComposition.on('add', addObjectToView);
|
||||||
|
* myViewComposition.on('remove', removeObjectFromView);
|
||||||
|
* myViewComposition.load(); // will trigger `add` for all loaded objects.
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export default class CompositionCollection {
|
||||||
|
domainObject;
|
||||||
|
#provider;
|
||||||
|
#publicAPI;
|
||||||
|
#listeners;
|
||||||
|
#mutables;
|
||||||
/**
|
/**
|
||||||
* A CompositionCollection represents the list of domain objects contained
|
* @constructor
|
||||||
* by another domain object. It provides methods for loading this
|
* @param {DomainObject} domainObject the domain object
|
||||||
* list asynchronously, modifying this list, and listening for changes to
|
|
||||||
* this list.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```javascript
|
|
||||||
* var myViewComposition = MCT.composition.get(myViewObject);
|
|
||||||
* myViewComposition.on('add', addObjectToView);
|
|
||||||
* myViewComposition.on('remove', removeObjectFromView);
|
|
||||||
* myViewComposition.load(); // will trigger `add` for all loaded objects.
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @interface CompositionCollection
|
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
|
||||||
* whose composition will be contained
|
* whose composition will be contained
|
||||||
* @param {module:openmct.CompositionProvider} provider the provider
|
* @param {import('./CompositionProvider').default} provider the provider
|
||||||
* to use to retrieve other domain objects
|
* to use to retrieve other domain objects
|
||||||
* @param {module:openmct.CompositionAPI} api the composition API, for
|
* @param {OpenMCT} publicAPI the composition API, for
|
||||||
* policy checks
|
* policy checks
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
*/
|
||||||
function CompositionCollection(domainObject, provider, publicAPI) {
|
constructor(domainObject, provider, publicAPI) {
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.provider = provider;
|
/** @type {import('./CompositionProvider').default} */
|
||||||
this.publicAPI = publicAPI;
|
this.#provider = provider;
|
||||||
this.listeners = {
|
/** @type {OpenMCT} */
|
||||||
|
this.#publicAPI = publicAPI;
|
||||||
|
/** @type {ListenerMap} */
|
||||||
|
this.#listeners = {
|
||||||
add: [],
|
add: [],
|
||||||
remove: [],
|
remove: [],
|
||||||
load: [],
|
load: [],
|
||||||
reorder: []
|
reorder: []
|
||||||
};
|
};
|
||||||
this.onProviderAdd = this.onProviderAdd.bind(this);
|
this.onProviderAdd = this.#onProviderAdd.bind(this);
|
||||||
this.onProviderRemove = this.onProviderRemove.bind(this);
|
this.onProviderRemove = this.#onProviderRemove.bind(this);
|
||||||
this.mutables = {};
|
this.#mutables = {};
|
||||||
|
|
||||||
if (this.domainObject.isMutable) {
|
if (this.domainObject.isMutable) {
|
||||||
this.returnMutables = true;
|
this.returnMutables = true;
|
||||||
let unobserve = this.domainObject.$on('$_destroy', () => {
|
let unobserve = this.domainObject.$on('$_destroy', () => {
|
||||||
Object.values(this.mutables).forEach(mutable => {
|
Object.values(this.#mutables).forEach(mutable => {
|
||||||
this.publicAPI.objects.destroyMutable(mutable);
|
this.#publicAPI.objects.destroyMutable(mutable);
|
||||||
});
|
});
|
||||||
unobserve();
|
unobserve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for changes to this composition. Supports 'add', 'remove', and
|
* Listen for changes to this composition. Supports 'add', 'remove', and
|
||||||
* 'load' events.
|
* 'load' events.
|
||||||
*
|
*
|
||||||
* @param event event to listen for, either 'add', 'remove' or 'load'.
|
* @param {string} event event to listen for, either 'add', 'remove' or 'load'.
|
||||||
* @param callback to trigger when event occurs.
|
* @param {(...args: any[]) => void} callback to trigger when event occurs.
|
||||||
* @param [context] context to use when invoking callback, optional.
|
* @param {any} [context] to use when invoking callback, optional.
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.on = function (event, callback, context) {
|
on(event, callback, context) {
|
||||||
if (!this.listeners[event]) {
|
if (!this.#listeners[event]) {
|
||||||
throw new Error('Event not supported by composition: ' + event);
|
throw new Error('Event not supported by composition: ' + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.provider.on && this.provider.off) {
|
if (this.#provider.on && this.#provider.off) {
|
||||||
if (event === 'add') {
|
if (event === 'add') {
|
||||||
this.provider.on(
|
this.#provider.on(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'add',
|
'add',
|
||||||
this.onProviderAdd,
|
this.onProviderAdd,
|
||||||
@ -97,7 +120,7 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'remove') {
|
if (event === 'remove') {
|
||||||
this.provider.on(
|
this.#provider.on(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'remove',
|
'remove',
|
||||||
this.onProviderRemove,
|
this.onProviderRemove,
|
||||||
@ -106,36 +129,34 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'reorder') {
|
if (event === 'reorder') {
|
||||||
this.provider.on(
|
this.#provider.on(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'reorder',
|
'reorder',
|
||||||
this.onProviderReorder,
|
this.#onProviderReorder,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeners[event].push({
|
this.#listeners[event].push({
|
||||||
callback: callback,
|
callback: callback,
|
||||||
context: context
|
context: context
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a listener. Must be called with same exact parameters as
|
* Remove a listener. Must be called with same exact parameters as
|
||||||
* `off`.
|
* `off`.
|
||||||
*
|
*
|
||||||
* @param event
|
* @param {string} event
|
||||||
* @param callback
|
* @param {(...args: any[]) => void} callback
|
||||||
* @param [context]
|
* @param {any} [context]
|
||||||
*/
|
*/
|
||||||
|
off(event, callback, context) {
|
||||||
CompositionCollection.prototype.off = function (event, callback, context) {
|
if (!this.#listeners[event]) {
|
||||||
if (!this.listeners[event]) {
|
|
||||||
throw new Error('Event not supported by composition: ' + event);
|
throw new Error('Event not supported by composition: ' + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this.listeners[event].findIndex(l => {
|
const index = this.#listeners[event].findIndex(l => {
|
||||||
return l.callback === callback && l.context === context;
|
return l.callback === callback && l.context === context;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -143,125 +164,116 @@ define([
|
|||||||
throw new Error('Tried to remove a listener that does not exist');
|
throw new Error('Tried to remove a listener that does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeners[event].splice(index, 1);
|
this.#listeners[event].splice(index, 1);
|
||||||
if (this.listeners[event].length === 0) {
|
if (this.#listeners[event].length === 0) {
|
||||||
this._destroy();
|
this._destroy();
|
||||||
|
|
||||||
// Remove provider listener if this is the last callback to
|
// Remove provider listener if this is the last callback to
|
||||||
// be removed.
|
// be removed.
|
||||||
if (this.provider.off && this.provider.on) {
|
if (this.#provider.off && this.#provider.on) {
|
||||||
if (event === 'add') {
|
if (event === 'add') {
|
||||||
this.provider.off(
|
this.#provider.off(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'add',
|
'add',
|
||||||
this.onProviderAdd,
|
this.onProviderAdd,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
} else if (event === 'remove') {
|
} else if (event === 'remove') {
|
||||||
this.provider.off(
|
this.#provider.off(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'remove',
|
'remove',
|
||||||
this.onProviderRemove,
|
this.onProviderRemove,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
} else if (event === 'reorder') {
|
} else if (event === 'reorder') {
|
||||||
this.provider.off(
|
this.#provider.off(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'reorder',
|
'reorder',
|
||||||
this.onProviderReorder,
|
this.#onProviderReorder,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a domain object to this composition.
|
* Add a domain object to this composition.
|
||||||
*
|
*
|
||||||
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
||||||
* must have resolved before using this method.
|
* must have resolved before using this method.
|
||||||
*
|
*
|
||||||
* @param {module:openmct.DomainObject} child the domain object to add
|
* **TODO:** Remove `skipMutate` parameter.
|
||||||
* @param {boolean} skipMutate true if the underlying provider should
|
*
|
||||||
* not be updated
|
* @param {DomainObject} child the domain object to add
|
||||||
* @memberof module:openmct.CompositionCollection#
|
* @param {boolean} skipMutate
|
||||||
* @name add
|
* **Intended for internal use ONLY.**
|
||||||
|
* true if the underlying provider should not be updated.
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.add = function (child, skipMutate) {
|
add(child, skipMutate) {
|
||||||
if (!skipMutate) {
|
if (!skipMutate) {
|
||||||
if (!this.publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||||
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
|
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.provider.add(this.domainObject, child.identifier);
|
this.#provider.add(this.domainObject, child.identifier);
|
||||||
} else {
|
} else {
|
||||||
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
|
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
|
||||||
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
|
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
|
||||||
|
|
||||||
child = this.publicAPI.objects.toMutable(child);
|
child = this.#publicAPI.objects.toMutable(child);
|
||||||
this.mutables[keyString] = child;
|
this.#mutables[keyString] = child;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('add', child);
|
this.#emit('add', child);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the domain objects in this composition.
|
* Load the domain objects in this composition.
|
||||||
*
|
*
|
||||||
* @returns {Promise.<Array.<module:openmct.DomainObject>>} a promise for
|
* @param {AbortSignal} abortSignal
|
||||||
|
* @returns {Promise.<Array.<DomainObject>>} a promise for
|
||||||
* the domain objects in this composition
|
* the domain objects in this composition
|
||||||
* @memberof {module:openmct.CompositionCollection#}
|
* @memberof {module:openmct.CompositionCollection#}
|
||||||
* @name load
|
* @name load
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.load = function (abortSignal) {
|
async load(abortSignal) {
|
||||||
this.cleanUpMutables();
|
this.#cleanUpMutables();
|
||||||
|
const children = await this.#provider.load(this.domainObject);
|
||||||
return this.provider.load(this.domainObject)
|
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
|
||||||
.then(function (children) {
|
childObjects.forEach(c => this.add(c, true));
|
||||||
return Promise.all(children.map((c) => this.publicAPI.objects.get(c, abortSignal)));
|
this.#emit('load');
|
||||||
}.bind(this))
|
|
||||||
.then(function (childObjects) {
|
|
||||||
childObjects.forEach(c => this.add(c, true));
|
|
||||||
|
|
||||||
return childObjects;
|
|
||||||
}.bind(this))
|
|
||||||
.then(function (children) {
|
|
||||||
this.emit('load');
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
return childObjects;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Remove a domain object from this composition.
|
* Remove a domain object from this composition.
|
||||||
*
|
*
|
||||||
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
||||||
* must have resolved before using this method.
|
* must have resolved before using this method.
|
||||||
*
|
*
|
||||||
* @param {module:openmct.DomainObject} child the domain object to remove
|
* **TODO:** Remove `skipMutate` parameter.
|
||||||
* @param {boolean} skipMutate true if the underlying provider should
|
*
|
||||||
* not be updated
|
* @param {DomainObject} child the domain object to remove
|
||||||
* @memberof module:openmct.CompositionCollection#
|
* @param {boolean} skipMutate
|
||||||
|
* **Intended for internal use ONLY.**
|
||||||
|
* true if the underlying provider should not be updated.
|
||||||
* @name remove
|
* @name remove
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.remove = function (child, skipMutate) {
|
remove(child, skipMutate) {
|
||||||
if (!skipMutate) {
|
if (!skipMutate) {
|
||||||
this.provider.remove(this.domainObject, child.identifier);
|
this.#provider.remove(this.domainObject, child.identifier);
|
||||||
} else {
|
} else {
|
||||||
if (this.returnMutables) {
|
if (this.returnMutables) {
|
||||||
let keyString = this.publicAPI.objects.makeKeyString(child);
|
let keyString = this.#publicAPI.objects.makeKeyString(child);
|
||||||
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
|
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
|
||||||
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
|
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
|
||||||
delete this.mutables[keyString];
|
delete this.#mutables[keyString];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('remove', child);
|
this.#emit('remove', child);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorder the domain objects in this composition.
|
* Reorder the domain objects in this composition.
|
||||||
*
|
*
|
||||||
@ -270,67 +282,75 @@ define([
|
|||||||
*
|
*
|
||||||
* @param {number} oldIndex
|
* @param {number} oldIndex
|
||||||
* @param {number} newIndex
|
* @param {number} newIndex
|
||||||
* @memberof module:openmct.CompositionCollection#
|
|
||||||
* @name remove
|
* @name remove
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.reorder = function (oldIndex, newIndex, skipMutate) {
|
reorder(oldIndex, newIndex, _skipMutate) {
|
||||||
this.provider.reorder(this.domainObject, oldIndex, newIndex);
|
this.#provider.reorder(this.domainObject, oldIndex, newIndex);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle reorder from provider.
|
* Destroy mutationListener
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.onProviderReorder = function (reorderMap) {
|
_destroy() {
|
||||||
this.emit('reorder', reorderMap);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle adds from provider.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
CompositionCollection.prototype.onProviderAdd = function (childId) {
|
|
||||||
return this.publicAPI.objects.get(childId).then(function (child) {
|
|
||||||
this.add(child, true);
|
|
||||||
|
|
||||||
return child;
|
|
||||||
}.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle removal from provider.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
CompositionCollection.prototype.onProviderRemove = function (child) {
|
|
||||||
this.remove(child, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
CompositionCollection.prototype._destroy = function () {
|
|
||||||
if (this.mutationListener) {
|
if (this.mutationListener) {
|
||||||
this.mutationListener();
|
this.mutationListener();
|
||||||
delete this.mutationListener;
|
delete this.mutationListener;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
/**
|
||||||
|
* Handle reorder from provider.
|
||||||
|
* @private
|
||||||
|
* @param {object} reorderMap
|
||||||
|
*/
|
||||||
|
#onProviderReorder(reorderMap) {
|
||||||
|
this.#emit('reorder', reorderMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle adds from provider.
|
||||||
|
* @private
|
||||||
|
* @param {import('../objects/ObjectAPI').Identifier} childId
|
||||||
|
* @returns {DomainObject}
|
||||||
|
*/
|
||||||
|
#onProviderAdd(childId) {
|
||||||
|
return this.#publicAPI.objects.get(childId).then(function (child) {
|
||||||
|
this.add(child, true);
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle removal from provider.
|
||||||
|
* @param {DomainObject} child
|
||||||
|
*/
|
||||||
|
#onProviderRemove(child) {
|
||||||
|
this.remove(child, true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit events.
|
* Emit events.
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @param {string} event
|
||||||
|
* @param {...args.<any>} payload
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.emit = function (event, ...payload) {
|
#emit(event, ...payload) {
|
||||||
this.listeners[event].forEach(function (l) {
|
this.#listeners[event].forEach(function (l) {
|
||||||
if (l.context) {
|
if (l.context) {
|
||||||
l.callback.apply(l.context, payload);
|
l.callback.apply(l.context, payload);
|
||||||
} else {
|
} else {
|
||||||
l.callback(...payload);
|
l.callback(...payload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
CompositionCollection.prototype.cleanUpMutables = function () {
|
/**
|
||||||
Object.values(this.mutables).forEach(mutable => {
|
* Destroy all mutables.
|
||||||
this.publicAPI.objects.destroyMutable(mutable);
|
* @private
|
||||||
|
*/
|
||||||
|
#cleanUpMutables() {
|
||||||
|
Object.values(this.#mutables).forEach(mutable => {
|
||||||
|
this.#publicAPI.objects.destroyMutable(mutable);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
}
|
||||||
return CompositionCollection;
|
|
||||||
});
|
|
||||||
|
262
src/api/composition/CompositionProvider.js
Normal file
262
src/api/composition/CompositionProvider.js
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import _ from 'lodash';
|
||||||
|
import objectUtils from "../objects/object-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CompositionProvider provides the underlying implementation of
|
||||||
|
* composition-related behavior for certain types of domain object.
|
||||||
|
*
|
||||||
|
* By default, a composition provider will not support composition
|
||||||
|
* modification. You can add support for mutation of composition by
|
||||||
|
* defining `add` and/or `remove` methods.
|
||||||
|
*
|
||||||
|
* If the composition of an object can change over time-- perhaps via
|
||||||
|
* server updates or mutation via the add/remove methods, then one must
|
||||||
|
* trigger events as necessary.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class CompositionProvider {
|
||||||
|
#publicAPI;
|
||||||
|
#listeningTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {OpenMCT} publicAPI
|
||||||
|
* @param {CompositionAPI} compositionAPI
|
||||||
|
*/
|
||||||
|
constructor(publicAPI, compositionAPI) {
|
||||||
|
this.#publicAPI = publicAPI;
|
||||||
|
this.#listeningTo = {};
|
||||||
|
|
||||||
|
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
|
||||||
|
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
get listeningTo() {
|
||||||
|
return this.#listeningTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
get establishTopicListener() {
|
||||||
|
return this.#establishTopicListener.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get publicAPI() {
|
||||||
|
return this.#publicAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this provider should be used to load composition for a
|
||||||
|
* particular domain object.
|
||||||
|
* @method appliesTo
|
||||||
|
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
|
||||||
|
* to check
|
||||||
|
* @returns {boolean} true if this provider can provide composition for a given domain object
|
||||||
|
*/
|
||||||
|
appliesTo(domainObject) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Load any domain objects contained in the composition of this domain
|
||||||
|
* object.
|
||||||
|
* @param {DomainObject} domainObject the domain object
|
||||||
|
* for which to load composition
|
||||||
|
* @returns {Promise<Identifier[]>} a promise for
|
||||||
|
* the Identifiers in this composition
|
||||||
|
* @method load
|
||||||
|
*/
|
||||||
|
load(domainObject) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Attach listeners for changes to the composition of a given domain object.
|
||||||
|
* Supports `add` and `remove` events.
|
||||||
|
*
|
||||||
|
* @param {DomainObject} domainObject to listen to
|
||||||
|
* @param {string} event the event to bind to, either `add` or `remove`.
|
||||||
|
* @param {Function} callback callback to invoke when event is triggered.
|
||||||
|
* @param {any} [context] to use when invoking callback.
|
||||||
|
*/
|
||||||
|
on(domainObject,
|
||||||
|
event,
|
||||||
|
callback,
|
||||||
|
context) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remove a listener that was previously added for a given domain object.
|
||||||
|
* event name, callback, and context must be the same as when the listener
|
||||||
|
* was originally attached.
|
||||||
|
*
|
||||||
|
* @param {DomainObject} domainObject to remove listener for
|
||||||
|
* @param {string} event event to stop listening to: `add` or `remove`.
|
||||||
|
* @param {Function} callback callback to remove.
|
||||||
|
* @param {any} context of callback to remove.
|
||||||
|
*/
|
||||||
|
off(domainObject,
|
||||||
|
event,
|
||||||
|
callback,
|
||||||
|
context) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remove a domain object from another domain object's composition.
|
||||||
|
*
|
||||||
|
* This method is optional; if not present, adding to a domain object's
|
||||||
|
* composition using this provider will be disallowed.
|
||||||
|
*
|
||||||
|
* @param {DomainObject} domainObject the domain object
|
||||||
|
* which should have its composition modified
|
||||||
|
* @param {Identifier} childId the domain object to remove
|
||||||
|
* @method remove
|
||||||
|
*/
|
||||||
|
remove(domainObject, childId) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a domain object to another domain object's composition.
|
||||||
|
*
|
||||||
|
* This method is optional; if not present, adding to a domain object's
|
||||||
|
* composition using this provider will be disallowed.
|
||||||
|
*
|
||||||
|
* @param {DomainObject} parent the domain object
|
||||||
|
* which should have its composition modified
|
||||||
|
* @param {Identifier} childId the domain object to add
|
||||||
|
* @method add
|
||||||
|
*/
|
||||||
|
add(parent, childId) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DomainObject} parent
|
||||||
|
* @param {Identifier} childId
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
includes(parent, childId) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DomainObject} domainObject
|
||||||
|
* @param {number} oldIndex
|
||||||
|
* @param {number} newIndex
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
reorder(domainObject, oldIndex, newIndex) {
|
||||||
|
throw new Error("This method must be implemented by a subclass.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens on general mutation topic, using injector to fetch to avoid
|
||||||
|
* circular dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#establishTopicListener() {
|
||||||
|
if (this.topicListener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||||
|
this.topicListener = () => {
|
||||||
|
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {DomainObject} parent
|
||||||
|
* @param {DomainObject} child
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
#cannotContainItself(parent, child) {
|
||||||
|
return !(parent.identifier.namespace === child.identifier.namespace
|
||||||
|
&& parent.identifier.key === child.identifier.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {DomainObject} parent
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
#supportsComposition(parent, _child) {
|
||||||
|
return this.#publicAPI.composition.supportsComposition(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mutation events. If there are active listeners for the mutated
|
||||||
|
* object, detects changes to composition and triggers necessary events.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {DomainObject} oldDomainObject
|
||||||
|
*/
|
||||||
|
#onMutation(oldDomainObject) {
|
||||||
|
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||||
|
const listeners = this.#listeningTo[id];
|
||||||
|
|
||||||
|
if (!listeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
||||||
|
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||||
|
|
||||||
|
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||||
|
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||||
|
|
||||||
|
function notify(value) {
|
||||||
|
return function (listener) {
|
||||||
|
if (listener.context) {
|
||||||
|
listener.callback.call(listener.context, value);
|
||||||
|
} else {
|
||||||
|
listener.callback(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
||||||
|
|
||||||
|
added.forEach(function (addedChild) {
|
||||||
|
listeners.add.forEach(notify(addedChild));
|
||||||
|
});
|
||||||
|
|
||||||
|
removed.forEach(function (removedChild) {
|
||||||
|
listeners.remove.forEach(notify(removedChild));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,102 +19,79 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import objectUtils from "../objects/object-utils";
|
||||||
|
import CompositionProvider from './CompositionProvider';
|
||||||
|
|
||||||
define([
|
/**
|
||||||
'lodash',
|
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||||
'objectUtils'
|
*/
|
||||||
], function (
|
|
||||||
_,
|
|
||||||
objectUtils
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* A CompositionProvider provides the underlying implementation of
|
|
||||||
* composition-related behavior for certain types of domain object.
|
|
||||||
*
|
|
||||||
* By default, a composition provider will not support composition
|
|
||||||
* modification. You can add support for mutation of composition by
|
|
||||||
* defining `add` and/or `remove` methods.
|
|
||||||
*
|
|
||||||
* If the composition of an object can change over time-- perhaps via
|
|
||||||
* server updates or mutation via the add/remove methods, then one must
|
|
||||||
* trigger events as necessary.
|
|
||||||
*
|
|
||||||
* @interface CompositionProvider
|
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
|
||||||
|
|
||||||
function DefaultCompositionProvider(publicAPI, compositionAPI) {
|
/**
|
||||||
this.publicAPI = publicAPI;
|
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||||
this.listeningTo = {};
|
*/
|
||||||
this.onMutation = this.onMutation.bind(this);
|
|
||||||
|
|
||||||
this.cannotContainItself = this.cannotContainItself.bind(this);
|
/**
|
||||||
this.supportsComposition = this.supportsComposition.bind(this);
|
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||||
|
*/
|
||||||
|
|
||||||
compositionAPI.addPolicy(this.cannotContainItself);
|
/**
|
||||||
compositionAPI.addPolicy(this.supportsComposition);
|
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||||
}
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
DefaultCompositionProvider.prototype.cannotContainItself = function (parent, child) {
|
|
||||||
return !(parent.identifier.namespace === child.identifier.namespace
|
|
||||||
&& parent.identifier.key === child.identifier.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
|
|
||||||
return this.publicAPI.composition.supportsComposition(parent);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CompositionProvider provides the underlying implementation of
|
||||||
|
* composition-related behavior for certain types of domain object.
|
||||||
|
*
|
||||||
|
* By default, a composition provider will not support composition
|
||||||
|
* modification. You can add support for mutation of composition by
|
||||||
|
* defining `add` and/or `remove` methods.
|
||||||
|
*
|
||||||
|
* If the composition of an object can change over time-- perhaps via
|
||||||
|
* server updates or mutation via the add/remove methods, then one must
|
||||||
|
* trigger events as necessary.
|
||||||
|
* @extends CompositionProvider
|
||||||
|
*/
|
||||||
|
export default class DefaultCompositionProvider extends CompositionProvider {
|
||||||
/**
|
/**
|
||||||
* Check if this provider should be used to load composition for a
|
* Check if this provider should be used to load composition for a
|
||||||
* particular domain object.
|
* particular domain object.
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
* @override
|
||||||
|
* @param {DomainObject} domainObject the domain object
|
||||||
* to check
|
* to check
|
||||||
* @returns {boolean} true if this provider can provide
|
* @returns {boolean} true if this provider can provide composition for a given domain object
|
||||||
* composition for a given domain object
|
|
||||||
* @memberof module:openmct.CompositionProvider#
|
|
||||||
* @method appliesTo
|
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.appliesTo = function (domainObject) {
|
appliesTo(domainObject) {
|
||||||
return Boolean(domainObject.composition);
|
return Boolean(domainObject.composition);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load any domain objects contained in the composition of this domain
|
* Load any domain objects contained in the composition of this domain
|
||||||
* object.
|
* object.
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
* @override
|
||||||
|
* @param {DomainObject} domainObject the domain object
|
||||||
* for which to load composition
|
* for which to load composition
|
||||||
* @returns {Promise.<Array.<module:openmct.Identifier>>} a promise for
|
* @returns {Promise<Identifier[]>} a promise for
|
||||||
* the Identifiers in this composition
|
* the Identifiers in this composition
|
||||||
* @memberof module:openmct.CompositionProvider#
|
|
||||||
* @method load
|
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.load = function (domainObject) {
|
load(domainObject) {
|
||||||
return Promise.all(domainObject.composition);
|
return Promise.all(domainObject.composition);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach listeners for changes to the composition of a given domain object.
|
* Attach listeners for changes to the composition of a given domain object.
|
||||||
* Supports `add` and `remove` events.
|
* Supports `add` and `remove` events.
|
||||||
*
|
*
|
||||||
* @param {module:openmct.DomainObject} domainObject to listen to
|
* @override
|
||||||
* @param String event the event to bind to, either `add` or `remove`.
|
* @param {DomainObject} domainObject to listen to
|
||||||
* @param Function callback callback to invoke when event is triggered.
|
* @param {string} event the event to bind to, either `add` or `remove`.
|
||||||
* @param [context] context to use when invoking callback.
|
* @param {Function} callback callback to invoke when event is triggered.
|
||||||
|
* @param {any} [context] to use when invoking callback.
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.on = function (
|
on(domainObject,
|
||||||
domainObject,
|
|
||||||
event,
|
event,
|
||||||
callback,
|
callback,
|
||||||
context
|
context) {
|
||||||
) {
|
|
||||||
this.establishTopicListener();
|
this.establishTopicListener();
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||||
let objectListeners = this.listeningTo[keyString];
|
let objectListeners = this.listeningTo[keyString];
|
||||||
|
|
||||||
@ -131,24 +108,24 @@ define([
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
context: context
|
context: context
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a listener that was previously added for a given domain object.
|
* Remove a listener that was previously added for a given domain object.
|
||||||
* event name, callback, and context must be the same as when the listener
|
* event name, callback, and context must be the same as when the listener
|
||||||
* was originally attached.
|
* was originally attached.
|
||||||
*
|
*
|
||||||
* @param {module:openmct.DomainObject} domainObject to remove listener for
|
* @override
|
||||||
* @param String event event to stop listening to: `add` or `remove`.
|
* @param {DomainObject} domainObject to remove listener for
|
||||||
* @param Function callback callback to remove.
|
* @param {string} event event to stop listening to: `add` or `remove`.
|
||||||
* @param [context] context of callback to remove.
|
* @param {Function} callback callback to remove.
|
||||||
|
* @param {any} context of callback to remove.
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.off = function (
|
off(domainObject,
|
||||||
domainObject,
|
|
||||||
event,
|
event,
|
||||||
callback,
|
callback,
|
||||||
context
|
context) {
|
||||||
) {
|
|
||||||
|
/** @type {string} */
|
||||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||||
const objectListeners = this.listeningTo[keyString];
|
const objectListeners = this.listeningTo[keyString];
|
||||||
|
|
||||||
@ -160,57 +137,64 @@ define([
|
|||||||
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
|
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
|
||||||
delete this.listeningTo[keyString];
|
delete this.listeningTo[keyString];
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a domain object from another domain object's composition.
|
* Remove a domain object from another domain object's composition.
|
||||||
*
|
*
|
||||||
* This method is optional; if not present, adding to a domain object's
|
* This method is optional; if not present, adding to a domain object's
|
||||||
* composition using this provider will be disallowed.
|
* composition using this provider will be disallowed.
|
||||||
*
|
*
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
* @override
|
||||||
|
* @param {DomainObject} domainObject the domain object
|
||||||
* which should have its composition modified
|
* which should have its composition modified
|
||||||
* @param {module:openmct.DomainObject} child the domain object to remove
|
* @param {Identifier} childId the domain object to remove
|
||||||
* @memberof module:openmct.CompositionProvider#
|
|
||||||
* @method remove
|
* @method remove
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.remove = function (domainObject, childId) {
|
remove(domainObject, childId) {
|
||||||
let composition = domainObject.composition.filter(function (child) {
|
let composition = domainObject.composition.filter(function (child) {
|
||||||
return !(childId.namespace === child.namespace
|
return !(childId.namespace === child.namespace
|
||||||
&& childId.key === child.key);
|
&& childId.key === child.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
|
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a domain object to another domain object's composition.
|
* Add a domain object to another domain object's composition.
|
||||||
*
|
*
|
||||||
* This method is optional; if not present, adding to a domain object's
|
* This method is optional; if not present, adding to a domain object's
|
||||||
* composition using this provider will be disallowed.
|
* composition using this provider will be disallowed.
|
||||||
*
|
*
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
* @override
|
||||||
|
* @param {DomainObject} parent the domain object
|
||||||
* which should have its composition modified
|
* which should have its composition modified
|
||||||
* @param {module:openmct.DomainObject} child the domain object to add
|
* @param {Identifier} childId the domain object to add
|
||||||
* @memberof module:openmct.CompositionProvider#
|
|
||||||
* @method add
|
* @method add
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.add = function (parent, childId) {
|
add(parent, childId) {
|
||||||
if (!this.includes(parent, childId)) {
|
if (!this.includes(parent, childId)) {
|
||||||
parent.composition.push(childId);
|
parent.composition.push(childId);
|
||||||
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
|
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @override
|
||||||
|
* @param {DomainObject} parent
|
||||||
|
* @param {Identifier} childId
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
DefaultCompositionProvider.prototype.includes = function (parent, childId) {
|
includes(parent, childId) {
|
||||||
return parent.composition.some(composee =>
|
return parent.composition.some(composee => this.publicAPI.objects.areIdsEqual(composee, childId));
|
||||||
this.publicAPI.objects.areIdsEqual(composee, childId));
|
}
|
||||||
};
|
|
||||||
|
|
||||||
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {DomainObject} domainObject
|
||||||
|
* @param {number} oldIndex
|
||||||
|
* @param {number} newIndex
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
reorder(domainObject, oldIndex, newIndex) {
|
||||||
let newComposition = domainObject.composition.slice();
|
let newComposition = domainObject.composition.slice();
|
||||||
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
|
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
|
||||||
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
|
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
|
||||||
@ -241,6 +225,7 @@ define([
|
|||||||
|
|
||||||
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
|
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
let id = objectUtils.makeKeyString(domainObject.identifier);
|
let id = objectUtils.makeKeyString(domainObject.identifier);
|
||||||
const listeners = this.listeningTo[id];
|
const listeners = this.listeningTo[id];
|
||||||
|
|
||||||
@ -257,66 +242,5 @@ define([
|
|||||||
listener.callback(reorderPlan);
|
listener.callback(reorderPlan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Listens on general mutation topic, using injector to fetch to avoid
|
|
||||||
* circular dependencies.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
DefaultCompositionProvider.prototype.establishTopicListener = function () {
|
|
||||||
if (this.topicListener) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.publicAPI.objects.eventEmitter.on('mutation', this.onMutation);
|
|
||||||
this.topicListener = () => {
|
|
||||||
this.publicAPI.objects.eventEmitter.off('mutation', this.onMutation);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles mutation events. If there are active listeners for the mutated
|
|
||||||
* object, detects changes to composition and triggers necessary events.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
DefaultCompositionProvider.prototype.onMutation = function (oldDomainObject) {
|
|
||||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
|
||||||
const listeners = this.listeningTo[id];
|
|
||||||
|
|
||||||
if (!listeners) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
|
||||||
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
|
||||||
|
|
||||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
|
||||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
|
||||||
|
|
||||||
function notify(value) {
|
|
||||||
return function (listener) {
|
|
||||||
if (listener.context) {
|
|
||||||
listener.callback.call(listener.context, value);
|
|
||||||
} else {
|
|
||||||
listener.callback(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
|
||||||
|
|
||||||
added.forEach(function (addedChild) {
|
|
||||||
listeners.add.forEach(notify(addedChild));
|
|
||||||
});
|
|
||||||
|
|
||||||
removed.forEach(function (removedChild) {
|
|
||||||
listeners.remove.forEach(notify(removedChild));
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return DefaultCompositionProvider;
|
|
||||||
});
|
|
||||||
|
@ -33,7 +33,7 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
|||||||
/**
|
/**
|
||||||
* Uniquely identifies a domain object.
|
* Uniquely identifies a domain object.
|
||||||
*
|
*
|
||||||
* @typedef Identifier
|
* @typedef {object} Identifier
|
||||||
* @property {string} namespace the namespace to/from which this domain
|
* @property {string} namespace the namespace to/from which this domain
|
||||||
* object should be loaded/stored.
|
* object should be loaded/stored.
|
||||||
* @property {string} key a unique identifier for the domain object
|
* @property {string} key a unique identifier for the domain object
|
||||||
@ -50,8 +50,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
|||||||
* A few common properties are defined for domain objects. Beyond these,
|
* A few common properties are defined for domain objects. Beyond these,
|
||||||
* individual types of domain objects may add more as they see fit.
|
* individual types of domain objects may add more as they see fit.
|
||||||
*
|
*
|
||||||
* @typedef DomainObject
|
* @typedef {object} DomainObject
|
||||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
* @property {Identifier} identifier a key/namespace pair which
|
||||||
* uniquely identifies this domain object
|
* uniquely identifies this domain object
|
||||||
* @property {string} type the type of domain object
|
* @property {string} type the type of domain object
|
||||||
* @property {string} name the human-readable name for this domain object
|
* @property {string} name the human-readable name for this domain object
|
||||||
@ -59,19 +59,19 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
|||||||
* object
|
* object
|
||||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||||
* epoch, at which this domain object was last modified
|
* epoch, at which this domain object was last modified
|
||||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
* @property {Identifier[]} [composition] if
|
||||||
* present, this will be used by the default composition provider
|
* present, this will be used by the default composition provider
|
||||||
* to load domain objects
|
* to load domain objects
|
||||||
* @memberof module:openmct
|
* @memberof module:openmct.ObjectAPI~
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @readonly
|
* @readonly
|
||||||
* @enum {String} SEARCH_TYPES
|
* @enum {string} SEARCH_TYPES
|
||||||
* @property {String} OBJECTS Search for objects
|
* @property {string} OBJECTS Search for objects
|
||||||
* @property {String} ANNOTATIONS Search for annotations
|
* @property {string} ANNOTATIONS Search for annotations
|
||||||
* @property {String} TAGS Search for tags
|
* @property {string} TAGS Search for tags
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for loading, saving, and manipulating domain objects.
|
* Utilities for loading, saving, and manipulating domain objects.
|
||||||
|
@ -20,250 +20,225 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(
|
import EventEmitter from 'EventEmitter';
|
||||||
[
|
import _ from 'lodash';
|
||||||
'EventEmitter',
|
|
||||||
'lodash'
|
|
||||||
],
|
|
||||||
function (
|
|
||||||
EventEmitter,
|
|
||||||
_
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Manages selection state for Open MCT
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function Selection(openmct) {
|
|
||||||
EventEmitter.call(this);
|
|
||||||
|
|
||||||
this.openmct = openmct;
|
/**
|
||||||
this.selected = [];
|
* Manages selection state for Open MCT
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export default class Selection extends EventEmitter {
|
||||||
|
constructor(openmct) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.selected = [];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the selected object.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
get() {
|
||||||
|
return this.selected;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Selects the selectable object and emits the 'change' event.
|
||||||
|
*
|
||||||
|
* @param {object} selectable an object with element and context properties
|
||||||
|
* @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
select(selectable, isMultiSelectEvent) {
|
||||||
|
if (!Array.isArray(selectable)) {
|
||||||
|
selectable = [selectable];
|
||||||
}
|
}
|
||||||
|
|
||||||
Selection.prototype = Object.create(EventEmitter.prototype);
|
let multiSelect = isMultiSelectEvent
|
||||||
|
&& this.parentSupportsMultiSelect(selectable)
|
||||||
|
&& this.isPeer(selectable)
|
||||||
|
&& !this.selectionContainsParent(selectable);
|
||||||
|
|
||||||
/**
|
if (multiSelect) {
|
||||||
* Gets the selected object.
|
this.handleMultiSelect(selectable);
|
||||||
* @public
|
} else {
|
||||||
*/
|
this.handleSingleSelect(selectable);
|
||||||
Selection.prototype.get = function () {
|
}
|
||||||
return this.selected;
|
}
|
||||||
};
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
handleMultiSelect(selectable) {
|
||||||
|
if (this.elementSelected(selectable)) {
|
||||||
|
this.remove(selectable);
|
||||||
|
} else {
|
||||||
|
this.addSelectionAttributes(selectable);
|
||||||
|
this.selected.push(selectable);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
this.emit('change', this.selected);
|
||||||
* Selects the selectable object and emits the 'change' event.
|
}
|
||||||
*
|
/**
|
||||||
* @param {object} selectable an object with element and context properties
|
* @private
|
||||||
* @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not
|
*/
|
||||||
* @private
|
handleSingleSelect(selectable) {
|
||||||
*/
|
if (!_.isEqual([selectable], this.selected)) {
|
||||||
Selection.prototype.select = function (selectable, isMultiSelectEvent) {
|
this.setSelectionStyles(selectable);
|
||||||
if (!Array.isArray(selectable)) {
|
this.selected = [selectable];
|
||||||
selectable = [selectable];
|
|
||||||
}
|
|
||||||
|
|
||||||
let multiSelect = isMultiSelectEvent
|
|
||||||
&& this.parentSupportsMultiSelect(selectable)
|
|
||||||
&& this.isPeer(selectable)
|
|
||||||
&& !this.selectionContainsParent(selectable);
|
|
||||||
|
|
||||||
if (multiSelect) {
|
|
||||||
this.handleMultiSelect(selectable);
|
|
||||||
} else {
|
|
||||||
this.handleSingleSelect(selectable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.handleMultiSelect = function (selectable) {
|
|
||||||
if (this.elementSelected(selectable)) {
|
|
||||||
this.remove(selectable);
|
|
||||||
} else {
|
|
||||||
this.addSelectionAttributes(selectable);
|
|
||||||
this.selected.push(selectable);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('change', this.selected);
|
this.emit('change', this.selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
elementSelected(selectable) {
|
||||||
|
return this.selected.some(selectionPath => _.isEqual(selectionPath, selectable));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
remove(selectable) {
|
||||||
|
this.selected = this.selected.filter(selectionPath => !_.isEqual(selectionPath, selectable));
|
||||||
|
|
||||||
|
if (this.selected.length === 0) {
|
||||||
|
this.removeSelectionAttributes(selectable);
|
||||||
|
selectable[1].element.click(); // Select the parent if there is no selection.
|
||||||
|
} else {
|
||||||
|
this.removeSelectionAttributes(selectable, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
setSelectionStyles(selectable) {
|
||||||
|
this.selected.forEach(selectionPath => this.removeSelectionAttributes(selectionPath));
|
||||||
|
this.addSelectionAttributes(selectable);
|
||||||
|
}
|
||||||
|
removeSelectionAttributes(selectionPath, keepParentStyle) {
|
||||||
|
if (selectionPath[0] && selectionPath[0].element) {
|
||||||
|
selectionPath[0].element.removeAttribute('s-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) {
|
||||||
|
selectionPath[1].element.removeAttribute('s-selected-parent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds selection attributes to the selected element and its parent.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addSelectionAttributes(selectable) {
|
||||||
|
if (selectable[0] && selectable[0].element) {
|
||||||
|
selectable[0].element.setAttribute('s-selected', "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectable[1] && selectable[1].element) {
|
||||||
|
selectable[1].element.setAttribute('s-selected-parent', "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
parentSupportsMultiSelect(selectable) {
|
||||||
|
return selectable[1] && selectable[1].context.supportsMultiSelect;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
selectionContainsParent(selectable) {
|
||||||
|
return this.selected.some(selectionPath => _.isEqual(selectionPath[0], selectable[1]));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
isPeer(selectable) {
|
||||||
|
return this.selected.some(selectionPath => _.isEqual(selectionPath[1], selectable[1]));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
isSelectable(element) {
|
||||||
|
if (!element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(element.closest('[data-selectable]'));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
capture(selectable) {
|
||||||
|
let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable);
|
||||||
|
|
||||||
|
if (!this.capturing || capturingContainsSelectable) {
|
||||||
|
this.capturing = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.capturing.push(selectable);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
selectCapture(selectable, event) {
|
||||||
|
if (!this.capturing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reversedCapturing = this.capturing.reverse();
|
||||||
|
delete this.capturing;
|
||||||
|
this.select(reversedCapturing, event.shiftKey);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Attaches the click handlers to the element.
|
||||||
|
*
|
||||||
|
* @param element an html element
|
||||||
|
* @param context object which defines item or other arbitrary properties.
|
||||||
|
* e.g. {
|
||||||
|
* item: domainObject,
|
||||||
|
* elementProxy: element,
|
||||||
|
* controller: fixedController
|
||||||
|
* }
|
||||||
|
* @param select a flag to select the element if true
|
||||||
|
* @returns a function that removes the click handlers from the element
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
selectable(element, context, select) {
|
||||||
|
if (!this.isSelectable(element)) {
|
||||||
|
return () => { };
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectable = {
|
||||||
|
context: context,
|
||||||
|
element: element
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const capture = this.capture.bind(this, selectable);
|
||||||
* @private
|
const selectCapture = this.selectCapture.bind(this, selectable);
|
||||||
*/
|
let removeMutable = false;
|
||||||
Selection.prototype.handleSingleSelect = function (selectable) {
|
|
||||||
if (!_.isEqual([selectable], this.selected)) {
|
|
||||||
this.setSelectionStyles(selectable);
|
|
||||||
this.selected = [selectable];
|
|
||||||
|
|
||||||
this.emit('change', this.selected);
|
element.addEventListener('click', capture, true);
|
||||||
|
element.addEventListener('click', selectCapture);
|
||||||
|
|
||||||
|
if (context.item && context.item.isMutable !== true) {
|
||||||
|
removeMutable = true;
|
||||||
|
context.item = this.openmct.objects.toMutable(context.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (select) {
|
||||||
|
if (typeof select === 'object') {
|
||||||
|
element.dispatchEvent(select);
|
||||||
|
} else if (typeof select === 'boolean') {
|
||||||
|
element.click();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
return (function () {
|
||||||
* @private
|
element.removeEventListener('click', capture, true);
|
||||||
*/
|
element.removeEventListener('click', selectCapture);
|
||||||
Selection.prototype.elementSelected = function (selectable) {
|
|
||||||
return this.selected.some(selectionPath => _.isEqual(selectionPath, selectable));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
if (context.item !== undefined && context.item.isMutable && removeMutable === true) {
|
||||||
* @private
|
this.openmct.objects.destroyMutable(context.item);
|
||||||
*/
|
|
||||||
Selection.prototype.remove = function (selectable) {
|
|
||||||
this.selected = this.selected.filter(selectionPath => !_.isEqual(selectionPath, selectable));
|
|
||||||
|
|
||||||
if (this.selected.length === 0) {
|
|
||||||
this.removeSelectionAttributes(selectable);
|
|
||||||
selectable[1].element.click(); // Select the parent if there is no selection.
|
|
||||||
} else {
|
|
||||||
this.removeSelectionAttributes(selectable, true);
|
|
||||||
}
|
}
|
||||||
};
|
}).bind(this);
|
||||||
|
}
|
||||||
/**
|
}
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.setSelectionStyles = function (selectable) {
|
|
||||||
this.selected.forEach(selectionPath => this.removeSelectionAttributes(selectionPath));
|
|
||||||
this.addSelectionAttributes(selectable);
|
|
||||||
};
|
|
||||||
|
|
||||||
Selection.prototype.removeSelectionAttributes = function (selectionPath, keepParentStyle) {
|
|
||||||
if (selectionPath[0] && selectionPath[0].element) {
|
|
||||||
selectionPath[0].element.removeAttribute('s-selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) {
|
|
||||||
selectionPath[1].element.removeAttribute('s-selected-parent');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Adds selection attributes to the selected element and its parent.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.addSelectionAttributes = function (selectable) {
|
|
||||||
if (selectable[0] && selectable[0].element) {
|
|
||||||
selectable[0].element.setAttribute('s-selected', "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectable[1] && selectable[1].element) {
|
|
||||||
selectable[1].element.setAttribute('s-selected-parent', "");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.parentSupportsMultiSelect = function (selectable) {
|
|
||||||
return selectable[1] && selectable[1].context.supportsMultiSelect;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.selectionContainsParent = function (selectable) {
|
|
||||||
return this.selected.some(selectionPath => _.isEqual(selectionPath[0], selectable[1]));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.isPeer = function (selectable) {
|
|
||||||
return this.selected.some(selectionPath => _.isEqual(selectionPath[1], selectable[1]));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.isSelectable = function (element) {
|
|
||||||
if (!element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(element.closest('[data-selectable]'));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.capture = function (selectable) {
|
|
||||||
let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable);
|
|
||||||
|
|
||||||
if (!this.capturing || capturingContainsSelectable) {
|
|
||||||
this.capturing = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.capturing.push(selectable);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Selection.prototype.selectCapture = function (selectable, event) {
|
|
||||||
if (!this.capturing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let reversedCapturing = this.capturing.reverse();
|
|
||||||
delete this.capturing;
|
|
||||||
this.select(reversedCapturing, event.shiftKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches the click handlers to the element.
|
|
||||||
*
|
|
||||||
* @param element an html element
|
|
||||||
* @param context object which defines item or other arbitrary properties.
|
|
||||||
* e.g. {
|
|
||||||
* item: domainObject,
|
|
||||||
* elementProxy: element,
|
|
||||||
* controller: fixedController
|
|
||||||
* }
|
|
||||||
* @param select a flag to select the element if true
|
|
||||||
* @returns a function that removes the click handlers from the element
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
Selection.prototype.selectable = function (element, context, select) {
|
|
||||||
if (!this.isSelectable(element)) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectable = {
|
|
||||||
context: context,
|
|
||||||
element: element
|
|
||||||
};
|
|
||||||
|
|
||||||
const capture = this.capture.bind(this, selectable);
|
|
||||||
const selectCapture = this.selectCapture.bind(this, selectable);
|
|
||||||
let removeMutable = false;
|
|
||||||
|
|
||||||
element.addEventListener('click', capture, true);
|
|
||||||
element.addEventListener('click', selectCapture);
|
|
||||||
|
|
||||||
if (context.item && context.item.isMutable !== true) {
|
|
||||||
removeMutable = true;
|
|
||||||
context.item = this.openmct.objects.toMutable(context.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (select) {
|
|
||||||
if (typeof select === 'object') {
|
|
||||||
element.dispatchEvent(select);
|
|
||||||
} else if (typeof select === 'boolean') {
|
|
||||||
element.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (function () {
|
|
||||||
element.removeEventListener('click', capture, true);
|
|
||||||
element.removeEventListener('click', selectCapture);
|
|
||||||
|
|
||||||
if (context.item !== undefined && context.item.isMutable && removeMutable === true) {
|
|
||||||
this.openmct.objects.destroyMutable(context.item);
|
|
||||||
}
|
|
||||||
}).bind(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
return Selection;
|
|
||||||
});
|
|
||||||
|
@ -7,19 +7,27 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": false,
|
"checkJs": false,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
"skipLibCheck": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
// matches the alias in webpack config, so that types for those imports are visible.
|
// matches the alias in webpack config, so that types for those imports are visible.
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"include": [
|
||||||
|
"src/api/**/*.js"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist"
|
"dist",
|
||||||
|
"**/*Spec.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user